├── LICENSE ├── README.md ├── build_scripts └── PKGBUILD ├── images ├── NotificationCenterExample.png ├── notification_center_screenshot.png ├── notification_popup.png ├── notiflut-logo.png └── popup_screenshot.png ├── notiflut_ctl ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ ├── cli.rs │ ├── dbus_client.rs │ ├── dto.rs │ └── main.rs └── notiflut_daemon ├── .gitignore ├── .metadata ├── Cargo.lock ├── Cargo.toml ├── analysis_options.yaml ├── lib ├── main.dart ├── services │ ├── cache_service.dart │ ├── event_dispatcher.dart │ ├── events_handlers.dart │ ├── mainwindow_service.dart │ ├── mediaplayer_service.dart │ ├── subwindow_service.dart │ └── theme_service.dart ├── utils.dart ├── widgets │ ├── category.dart │ ├── mediaPlayer.dart │ ├── notification.dart │ ├── notification_center.dart │ └── popups_list.dart └── window_utils.dart ├── linux ├── .gitignore ├── CMakeLists.txt ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake ├── main.cc ├── my_application.cc └── my_application.h ├── messages ├── app_event.proto ├── daemon_event.proto ├── settings_event.proto └── theme_event.proto ├── native ├── README.md ├── hub │ ├── Cargo.toml │ └── src │ │ ├── bridge │ │ ├── generated.io.rs │ │ ├── generated.rs │ │ ├── generated.web.rs │ │ ├── interface.rs │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── utils.rs │ │ └── with_request.rs └── notification_server │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── dbus │ └── introspection.xml │ └── src │ ├── api.rs │ ├── cache.rs │ ├── config │ ├── general_settings.rs │ ├── mod.rs │ └── theme.rs │ ├── db.rs │ ├── desktop_file_manager.rs │ ├── lib.rs │ └── notification_dbus │ ├── dbus_definition.rs │ ├── dbus_notification_handler.rs │ ├── mod.rs │ ├── models │ ├── mod.rs │ └── notification.rs │ └── notification_server_core.rs ├── notiflut.systemd.service ├── org.lucacoduriv.notiflut.service ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /README.md: -------------------------------------------------------------------------------- 1 | ![Notiflut-Land logo](/images/notiflut-logo.png) 2 | 3 | # Notiflut [Work in progress] 4 | 5 | This project is a notification center designed specifically for wlroots-based Wayland compositors, implemented using Rust and Flutter. The notification center provides a seamless and intuitive user interface for managing and interacting with notifications on Wayland-based systems. 6 | 7 | ## Screenshots 8 | 9 | ![Notification center sreenshot](/images/notification_center_screenshot.png) 10 | ![Notification popup screenshot](/images/popup_screenshot.png) 11 | 12 | 13 | ## Features 14 | 15 | ### Freedesktop specifications 16 | 17 | - [x] actions: The server will provide the specified actions to the user. Even if this cap is missing, actions may still be specified by the client, however the server is free to ignore them. 18 | 19 | - [x] body: Supports body text. Some implementations may only show the summary (for instance, onscreen displays, marquee/scrollers) 20 | - [x] body-hyperlinks: The server supports hyperlinks in the notifications. 21 | - [x] body-images: The server supports images in the notifications. 22 | - [x] body-markup: Supports markup in the body text. If marked up text is sent to a server that does not give this cap, the markup will show through as regular text so must be stripped clientside. 23 | - [x] icon-static: Supports display of exactly 1 frame of any given image array. This value is mutually exclusive with "icon-multi", it is a protocol error for the server to specify both. 24 | - [x] persistence: The server supports persistence of notifications. Notifications will be retained until they are acknowledged or removed by the user or recalled by the sender. The presence of this capability allows clients to depend on the server to ensure a notification is seen and eliminate the need for the client to display a reminding function (such as a status icon) of its own. 25 | 26 | ### More 27 | 28 | - [x] notification group 29 | - [x] Filter notifications 30 | - [x] support for waybar 31 | - [x] Media player controller 32 | - [x] custom styling 33 | - [ ] Widgets (Maybe a bad idea) 34 | 35 | ## requirement 36 | 37 | - Flutter v. > 3.0 [Download](https://docs.flutter.dev/get-started/install) 38 | - Rust [Download](https://rustup.rs/) 39 | - Rust in Flutter 40 | - gtk-layer-shell 41 | 42 | ## Build daemon 43 | 44 | First go to the right folder: `cd ./notiflut_daemon` 45 | 46 | Then you need to generates code from protobuf with this command: 47 | `dart run rust_in_flutter message` 48 | 49 | This project also uses well known types for dates. 50 | `protoc --dart_out=./lib/messages google/protobuf/timestamp.proto google/protobuf/empty.proto` 51 | 52 | If you have any trouble running these commands look first for help here: https://docs.cunarist.com/rust-in-flutter/ 53 | 54 | Now you should be able to compile the code with: `flutter build linux --release` 55 | 56 | ## Installation Guide 57 | 58 | 1. **Navigate to the Build Scripts Directory:** 59 | 60 | ```bash 61 | cd ./build_scripts 62 | ``` 63 | 64 | 2. **Install Dependencies:** 65 | If the `makepkg -si` command fails due to missing dependencies, manually download and install them. You can find dependency information higher in this file. 66 | 67 | 3. **Build and Install Package:** 68 | If you have resolved the dependencies, run the following command to build and install the package: 69 | 70 | ```bash 71 | makepkg -si 72 | ``` 73 | 74 | If you needed to install dependencies manually, use the following command instead: 75 | 76 | ```bash 77 | makepkg -di 78 | ``` 79 | 80 | This will build the package and install it along with its dependencies. 81 | 82 | ## how to use 83 | 84 | Once notiflut is running, use notiflut_ctl to control it. 85 | ``` 86 | Usage: notiflut_ctl 87 | 88 | Commands: 89 | show Shows the notification center 90 | hide Hides the notification center 91 | toggle Toggle the notification center 92 | status Get notifications status 93 | count get notifications count 94 | help Print this message or the help of the given subcommand(s) 95 | 96 | Options: 97 | -h, --help Print help 98 | -V, --version Print version 99 | ``` 100 | 101 | ## configuration 102 | 103 | The configuration file is typically located in the default XDG configuration directory. In most cases, it should be: `$HOME/.config/notiflut/conf.toml` 104 | 105 | ### Configuration File Example 106 | 107 | ```toml 108 | do_not_disturb = false 109 | 110 | [[emitters_settings]] 111 | name = "spotify" 112 | ignore = true 113 | urgency_low_as = "Critical" 114 | urgency_normal_as = "Normal" 115 | urgency_critical_as = "Low" 116 | ``` 117 | 118 | ### Do Not Disturb Mode 119 | 120 | - do_not_disturb: Set this to true to enable "Do Not Disturb" mode and suppress notifications temporarily. 121 | 122 | ### Emitter Settings 123 | 124 | - **name**: Assign a unique name to each emitter for identification purposes. 125 | - **ignore**: Toggle the ignore flag to true if you want to ignore notifications from a specific emitter. 126 | - **Urgency Level Mapping (Low, Normal, Critical)**: Low -> do not persist in center, Normal -> persist in center, Critical -> persist in center + popup need user action to close. 127 | - urgency_low_as: Customize the behavior of low urgency notifications. In this example, it's set to "Critical," allowing you to elevate the importance of low urgency notifications. 128 | - urgency_normal_as: Map normal urgency notifications to a custom urgency (same as urgency low). 129 | - urgency_critical_as: Map critical urgency notifications to a custom urgency (same as urgency low). 130 | 131 | ## Styling 132 | 133 | Here is an example of `~/.config/notiflut/style.toml` 134 | 135 | ```toml 136 | [light.notification_center] 137 | background_color = 0xFF000000 138 | 139 | [light.popup] 140 | background_color = 0xFF000000 141 | 142 | [light.notification] 143 | background_color = 0xFFE0E0E0 144 | border_radius = 10 145 | text_color = 0xFFFFFFFF 146 | border_width = 1 147 | border_color = 0xFFFF00FF 148 | title_text_color = 0xFFFF00FF 149 | subtitle_text_color = 0xFFFF00FF 150 | body_text_color = 0xFF0000FF 151 | button_text_color = 0xFFFF00FF 152 | 153 | [dark.notification_center] 154 | background_color = 0x45FFFFFF 155 | 156 | [dark.popup] 157 | background_color = 0xFF000000 158 | 159 | [dark.notification] 160 | background_color = 0x00000000 161 | border_radius = 10 162 | text_color = 0xFF000000 163 | border_width = 1 164 | border_color = 0xFFFF00FF 165 | title_text_color = 0xFFFF00FF 166 | subtitle_text_color = 0xFFFF00FF 167 | body_text_color = 0xFF0000FF 168 | button_text_color = 0xFFFF00FF 169 | ``` 170 | 171 | ## Waybar 172 | 173 | Here is an example of a custom module that can be used with Waybar. 174 | 175 | ```json 176 | "custom/notification": { 177 | "tooltip": false, 178 | "format": "{icon}", 179 | "format-icons": { 180 | "0": "", 181 | "1": "1", 182 | "2": "2", 183 | "3": "3", 184 | "4": "4", 185 | "5": "5", 186 | "6": "6", 187 | "7": "7", 188 | "8": "8", 189 | "9": "9", 190 | "more": "9+" 191 | }, 192 | "return-type": "json", 193 | "exec-if": "which notiflut_ctl", 194 | "exec": "notiflut_ctl status", 195 | "on-click": "notiflut_ctl toggle", 196 | "interval": 5, 197 | "escape": true 198 | } 199 | ``` 200 | -------------------------------------------------------------------------------- /build_scripts/PKGBUILD: -------------------------------------------------------------------------------- 1 | pkgname=notiflut 2 | pkgver=1.1.0 3 | pkgrel=1 4 | pkgdesc="A simple notificaion daemon with a notification center panel for checking previous notifications like other DEs" 5 | _pkgfoldername=Notiflut-Land 6 | url="https://github.com/LucaCoduriV/$_pkgfoldername" 7 | arch=( 8 | 'x86_64' 9 | 'aarch64' # ARM v8 64-bit 10 | 'armv7h' # ARM v7 hardfloat 11 | ) 12 | license=('Apache-2.0') 13 | depends=("gtk3" "gtk-layer-shell" "dbus" "glib2" "gobject-introspection" "libgee" "json-glib" "libhandy" "libpulse") 14 | conflicts=("notiflut" "notiflut_ctl") 15 | provides=("notiflut" "notiflut_ctl") 16 | makedepends=('flutter' 'clang' 'cmake' 'ninja' 'gtk3' 'pkgconf' 'xz' 'rust' 'protobuf') 17 | source=('git+https://github.com/LucaCoduriV/Notiflut-Land.git') 18 | sha256sums=('SKIP') 19 | 20 | build() { 21 | cd "$srcdir/$_pkgfoldername/notiflut_daemon" 22 | mkdir -p ./lib/messages 23 | echo 'Running rift message' 24 | dart run rinf message 25 | echo 'Running protoc google' 26 | protoc --dart_out=./lib/messages google/protobuf/timestamp.proto 27 | echo 'Running flutter build' 28 | flutter build linux --release 29 | 30 | cd "$srcdir/$_pkgfoldername/notiflut_ctl" 31 | echo 'Running cargo build' 32 | cargo build --release 33 | } 34 | 35 | package() { 36 | echo 'Installing notiflut_daemon' 37 | install -dm 755 "${pkgdir}/opt/${pkgname}" 38 | cp -a "$_pkgfoldername/notiflut_daemon/build/linux/x64/release/bundle/." -t "${pkgdir}/opt/${pkgname}" 39 | 40 | echo 'Creating link to notiflut' 41 | install -d "$pkgdir/usr/bin/" 42 | ln -s /opt/${pkgname}/notiflutland ${pkgdir}/usr/bin/notiflut 43 | 44 | #install service 45 | echo 'Installing service' 46 | mv $_pkgfoldername/notiflut_daemon/notiflut.systemd.service $_pkgfoldername/notiflut_daemon/notiflut.service 47 | install -Dm 644 $_pkgfoldername/notiflut_daemon/notiflut.service -t ${pkgdir}/etc/systemd/user/ 48 | install -Dm 644 $_pkgfoldername/notiflut_daemon/org.lucacoduriv.notiflut.service -t ${pkgdir}/usr/share/dbus-1/services/ 49 | 50 | #install ctl (TODO CHECK PERMISSION) 51 | echo 'Installing notiflut_ctl' 52 | install -Dm 777 "$_pkgfoldername/notiflut_ctl/target/release/notiflut_ctl" -t "${pkgdir}/usr/bin/" 53 | } 54 | -------------------------------------------------------------------------------- /images/NotificationCenterExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucaCoduriV/Notiflut-Land/be25040656c15121022eee68ec9e8444fff36434/images/NotificationCenterExample.png -------------------------------------------------------------------------------- /images/notification_center_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucaCoduriV/Notiflut-Land/be25040656c15121022eee68ec9e8444fff36434/images/notification_center_screenshot.png -------------------------------------------------------------------------------- /images/notification_popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucaCoduriV/Notiflut-Land/be25040656c15121022eee68ec9e8444fff36434/images/notification_popup.png -------------------------------------------------------------------------------- /images/notiflut-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucaCoduriV/Notiflut-Land/be25040656c15121022eee68ec9e8444fff36434/images/notiflut-logo.png -------------------------------------------------------------------------------- /images/popup_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LucaCoduriV/Notiflut-Land/be25040656c15121022eee68ec9e8444fff36434/images/popup_screenshot.png -------------------------------------------------------------------------------- /notiflut_ctl/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /notiflut_ctl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "notiflut_ctl" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | dbus = "0.9.7" 10 | dbus-crossroads = "0.5.2" 11 | thiserror = "1.0.40" 12 | anyhow = "1" 13 | serde = { version = "1.0.188", features = ["derive"] } 14 | serde_json = "1.0.105" 15 | clap = { version = "4.2.1", features = ["cargo", "derive"] } 16 | notify = "6.1.1" 17 | -------------------------------------------------------------------------------- /notiflut_ctl/src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand, ValueEnum}; 2 | 3 | #[derive(Parser)] 4 | #[command(author, version, about, long_about = None)] 5 | #[command(propagate_version = true)] 6 | pub struct Cli { 7 | #[command(subcommand)] 8 | pub command: Commands, 9 | } 10 | 11 | #[derive(Subcommand)] 12 | pub enum Commands { 13 | /// Shows the notification center 14 | Show, 15 | /// Hides the notification center 16 | Hide, 17 | /// Toggle the notification center 18 | Toggle, 19 | /// Get notifications status 20 | Status, 21 | /// get notifications count 22 | Count, 23 | /// reload configs 24 | Reload { 25 | #[arg(short, long)] 26 | watch: bool, 27 | }, 28 | /// Change the current selected theme 29 | Theme { variante: ThemeArgs }, 30 | } 31 | 32 | #[derive(ValueEnum, Clone)] 33 | pub enum ThemeArgs { 34 | Dark, 35 | Light, 36 | } 37 | -------------------------------------------------------------------------------- /notiflut_ctl/src/dbus_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use dbus::blocking::Connection; 3 | use dbus::blocking::Proxy; 4 | use std::rc::Rc; 5 | use std::time::Duration; 6 | 7 | /// D-Bus interface for desktop notifications. 8 | pub const NOTIFICATION_INTERFACE: &str = "org.freedesktop.Notifications"; 9 | 10 | /// D-Bus path for desktop notifications. 11 | pub const NOTIFICATION_PATH: &str = "/org/freedesktop/Notifications"; 12 | 13 | pub struct DbusClient { 14 | proxy: Proxy<'static, Rc>, 15 | } 16 | 17 | impl DbusClient { 18 | pub fn init() -> Result { 19 | let connection = Connection::new_session()?; 20 | let ref_counter = Rc::new(connection); 21 | let dc: DbusClient = Self { 22 | proxy: Proxy::new( 23 | NOTIFICATION_INTERFACE, 24 | format!("{NOTIFICATION_PATH}/ctl"), 25 | Duration::from_millis(1000), 26 | ref_counter, 27 | ), 28 | }; 29 | 30 | Ok(dc) 31 | } 32 | 33 | pub fn show_nc(&self) -> Result { 34 | let result: (String,) = self 35 | .proxy 36 | .method_call(NOTIFICATION_INTERFACE, "OpenNC", ())?; 37 | 38 | Ok(result.0) 39 | } 40 | 41 | pub fn toggle_nc(&self) -> Result { 42 | let result: (String,) = self 43 | .proxy 44 | .method_call(NOTIFICATION_INTERFACE, "ToggleNC", ())?; 45 | 46 | Ok(result.0) 47 | } 48 | 49 | pub fn hide_nc(&self) -> Result { 50 | let result: (String,) = self 51 | .proxy 52 | .method_call(NOTIFICATION_INTERFACE, "CloseNC", ())?; 53 | 54 | Ok(result.0) 55 | } 56 | 57 | pub fn reload(&self) -> Result { 58 | let result: (String,) = self 59 | .proxy 60 | .method_call(NOTIFICATION_INTERFACE, "Reload", ())?; 61 | 62 | Ok(result.0) 63 | } 64 | 65 | pub fn set_light_theme(&self) -> Result { 66 | let result: (String,) = self 67 | .proxy 68 | .method_call(NOTIFICATION_INTERFACE, "ThemeLight", ())?; 69 | 70 | Ok(result.0) 71 | } 72 | 73 | pub fn set_dark_theme(&self) -> Result { 74 | let result: (String,) = self 75 | .proxy 76 | .method_call(NOTIFICATION_INTERFACE, "ThemeDark", ())?; 77 | 78 | Ok(result.0) 79 | } 80 | 81 | pub fn get_notification_count(&self) -> Result { 82 | let result: (u64,) = 83 | self.proxy 84 | .method_call(NOTIFICATION_INTERFACE, "notificationCount", ())?; 85 | 86 | Ok(result.0) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /notiflut_ctl/src/dto.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug)] 4 | pub struct Status<'a> { 5 | pub text: &'a str, 6 | pub alt: &'a str, 7 | pub tooltip: bool, 8 | pub class: &'a str, 9 | pub percentage: u32, 10 | } 11 | -------------------------------------------------------------------------------- /notiflut_ctl/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, sync::mpsc::channel}; 2 | 3 | use clap::Parser; 4 | use cli::Commands; 5 | use notify::{RecursiveMode, Watcher}; 6 | 7 | use crate::cli::ThemeArgs; 8 | 9 | mod cli; 10 | mod dbus_client; 11 | mod dto; 12 | 13 | fn main() -> anyhow::Result<()> { 14 | let cli = cli::Cli::parse(); 15 | let dbus_client = dbus_client::DbusClient::init()?; 16 | 17 | let result = match &cli.command { 18 | Commands::Show => dbus_client.show_nc()?, 19 | Commands::Hide => dbus_client.hide_nc()?, 20 | Commands::Toggle => dbus_client.toggle_nc()?, 21 | Commands::Status => { 22 | let notification_count = dbus_client.get_notification_count()?; 23 | let alt = match notification_count { 24 | n if n < 10 => n.to_string(), 25 | _ => "more".to_string(), 26 | }; 27 | let percentage = notification_count.saturating_mul(10).clamp(0, 100) as u32; 28 | 29 | let status = dto::Status { 30 | text: ¬ification_count.to_string(), 31 | alt: &alt, 32 | tooltip: false, 33 | class: if notification_count > 0 { "active" } else { "" }, 34 | percentage, 35 | }; 36 | serde_json::to_string(&status)? 37 | } 38 | Commands::Count => dbus_client.get_notification_count()?.to_string(), 39 | Commands::Reload { watch: true } => { 40 | let (sndr, recv) = channel::<()>(); 41 | let mut watcher = notify::recommended_watcher(move |res| match res { 42 | Ok(_event) => { 43 | sndr.send(()).unwrap(); 44 | } 45 | Err(e) => println!("watch error: {:?}", e), 46 | })?; 47 | watcher.watch( 48 | Path::new("/home/luca/.config/notiflut"), 49 | RecursiveMode::Recursive, 50 | )?; 51 | for _ in recv.iter() { 52 | println!("{}", dbus_client.reload().unwrap()); 53 | } 54 | 55 | "Watch end".to_string() 56 | } 57 | Commands::Reload { watch: false } => dbus_client.reload()?.to_string(), 58 | Commands::Theme { 59 | variante: ThemeArgs::Dark, 60 | } => dbus_client.set_dark_theme()?.to_string(), 61 | Commands::Theme { 62 | variante: ThemeArgs::Light, 63 | } => dbus_client.set_light_theme()?.to_string(), 64 | }; 65 | 66 | println!("{}", result); 67 | 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /notiflut_daemon/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | # Rust related 47 | .cargo/ 48 | target/ 49 | 50 | # Generated messages 51 | */**/messages/ 52 | -------------------------------------------------------------------------------- /notiflut_daemon/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "e1e47221e86272429674bec4f1bd36acc4fc7b77" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 17 | base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 18 | - platform: android 19 | create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 20 | base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 21 | - platform: ios 22 | create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 23 | base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 24 | - platform: linux 25 | create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 26 | base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 27 | - platform: macos 28 | create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 29 | base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 30 | - platform: web 31 | create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 32 | base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 33 | - platform: windows 34 | create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 35 | base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /notiflut_daemon/Cargo.toml: -------------------------------------------------------------------------------- 1 | # This file is used for telling Rust-related tools 2 | # where various Rust crates are. 3 | # This also unifies `./target` output folder and 4 | # various Rust configurations. 5 | 6 | [workspace] 7 | members = ["./native/*"] 8 | resolver = "2" 9 | -------------------------------------------------------------------------------- /notiflut_daemon/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:notiflut/services/events_handlers.dart'; 6 | import 'package:notiflut/services/mainwindow_service.dart'; 7 | import 'package:notiflut/services/mediaplayer_service.dart'; 8 | import 'package:notiflut/services/event_dispatcher.dart'; 9 | import 'package:notiflut/services/subwindow_service.dart'; 10 | import 'package:notiflut/widgets/notification_center.dart'; 11 | import 'package:notiflut/widgets/popups_list.dart'; 12 | import 'package:notiflut/window_utils.dart'; 13 | import 'package:rinf/rinf.dart'; 14 | import 'package:watch_it/watch_it.dart'; 15 | import 'package:wayland_multi_window/wayland_multi_window.dart'; 16 | 17 | import 'services/theme_service.dart'; 18 | 19 | void main(List args) async { 20 | WidgetsFlutterBinding.ensureInitialized(); 21 | final isMainWindow = args.firstOrNull != 'multi_window'; 22 | 23 | if (isMainWindow) { 24 | await Rinf.ensureInitialized(); 25 | setupMainWindow(); 26 | di.registerSingleton(MediaPlayerService()).init(); 27 | di.registerSingleton(ThemeService()); 28 | final dispatcher = EventDispatcher( 29 | rustBroadcaster.stream 30 | .map((event) => StreamAdapter.fromRustSignal(event)), 31 | propagateToWindowId: 1, 32 | ); 33 | di.registerSingleton(dispatcher); 34 | di.registerSingleton(MainWindowService()); 35 | await setupSubWindow(); 36 | dispatcher.styleUpdateStream.listen(handleThemeChanged); 37 | dispatcher.themeChangedStram.listen(handleThemeTypeChanged); 38 | runApp(const MainWindow()); 39 | } else { 40 | final windowId = int.parse(args[1]); 41 | di.registerSingleton(ThemeService()); 42 | 43 | StreamController mainWindowEventsStream = 44 | StreamController(); 45 | WaylandMultiWindow.setMethodHandler( 46 | (MethodCall method, int windowsId) async { 47 | mainWindowEventsStream.add(StreamAdapter( 48 | eventId: int.parse(method.method), 49 | data: method.arguments as List)); 50 | }); 51 | final dispatcher = EventDispatcher(mainWindowEventsStream.stream); 52 | di.registerSingleton(dispatcher); 53 | di.registerSingleton(SubWindowService(windowId)); 54 | dispatcher.styleUpdateStream.listen(handleThemeChanged); 55 | dispatcher.themeChangedStram.listen(handleThemeTypeChanged); 56 | runApp(const SubWindow()); 57 | } 58 | } 59 | 60 | class MainWindow extends StatefulWidget with WatchItStatefulWidgetMixin { 61 | const MainWindow({super.key}); 62 | 63 | @override 64 | State createState() => _MainWindowState(); 65 | } 66 | 67 | class _MainWindowState extends State { 68 | @override 69 | void initState() { 70 | di.get().init(); 71 | super.initState(); 72 | } 73 | 74 | @override 75 | void dispose() { 76 | di.get().dispose(); 77 | super.dispose(); 78 | } 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | final theme = watchIt().theme; 83 | return MaterialApp( 84 | debugShowCheckedModeBanner: false, 85 | title: 'Notiflut-Land', 86 | theme: ThemeData( 87 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), 88 | useMaterial3: true, 89 | ), 90 | home: Scaffold( 91 | backgroundColor: theme != null 92 | ? Color(theme.notificationCenterStyle.backgroundColor) 93 | : Colors.black.withOpacity(0.1), 94 | body: NotificationCenter(), 95 | ), 96 | ); 97 | } 98 | } 99 | 100 | class SubWindow extends StatefulWidget with WatchItStatefulWidgetMixin { 101 | const SubWindow({super.key}); 102 | 103 | @override 104 | State createState() => _SubWindowState(); 105 | } 106 | 107 | class _SubWindowState extends State { 108 | @override 109 | void initState() { 110 | di.get().init(); 111 | super.initState(); 112 | } 113 | 114 | @override 115 | dispose() { 116 | di.get().dispose(); 117 | super.dispose(); 118 | } 119 | 120 | @override 121 | Widget build(BuildContext context) { 122 | final theme = watchIt().theme; 123 | return MaterialApp( 124 | debugShowCheckedModeBanner: false, 125 | title: 'Notiflut-Land', 126 | theme: ThemeData( 127 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), 128 | useMaterial3: true, 129 | ), 130 | home: Scaffold( 131 | backgroundColor: theme != null 132 | ? Color(theme.popupStyle.backgroundColor) 133 | : Colors.transparent, 134 | body: PopupsList(), 135 | ), 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/services/cache_service.dart: -------------------------------------------------------------------------------- 1 | // TODO FIND A WAY TO CLEAR RAM 2 | class CacheService{ 3 | final Map _cache = {}; 4 | 5 | S? get(T key){ 6 | return _cache[key]; 7 | } 8 | 9 | void put(T key, S data){ 10 | _cache[key] = data; 11 | } 12 | 13 | S? getOrPut(T key, S? Function() provider){ 14 | final oldCache = _cache[key]; 15 | if(oldCache != null){ 16 | return oldCache; 17 | } 18 | 19 | final newCache = provider(); 20 | if(newCache != null){ 21 | _cache[key] = newCache; 22 | } 23 | return newCache; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/services/event_dispatcher.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:rinf/rinf.dart'; 5 | import 'package:notiflut/messages/daemon_event.pb.dart' as daemon_event; 6 | import 'package:notiflut/messages/theme_event.pb.dart' as theme_event; 7 | import 'package:notiflut/messages/settings_event.pb.dart' as settings_event; 8 | import 'package:watch_it/watch_it.dart'; 9 | import 'package:wayland_multi_window/wayland_multi_window.dart'; 10 | 11 | import '../messages/daemon_event.pb.dart'; 12 | import 'theme_service.dart'; 13 | 14 | class StreamAdapter { 15 | final int eventId; 16 | final List data; 17 | 18 | StreamAdapter({required this.eventId, required this.data}); 19 | 20 | StreamAdapter.fromRustSignal(RustSignal signal) 21 | : eventId = signal.resource, 22 | data = signal.message as List; 23 | } 24 | 25 | class EventDispatcher { 26 | final int? propagateToWindowId; 27 | final StreamController _notificationsStream = 28 | StreamController(); 29 | Stream get notificationsStream => _notificationsStream.stream; 30 | 31 | final StreamController _styleUpdateStream = 32 | StreamController(); 33 | Stream get styleUpdateStream => _styleUpdateStream.stream; 34 | 35 | final StreamController _themeChangedStream = StreamController(); 36 | Stream get themeChangedStram => _themeChangedStream.stream; 37 | 38 | EventDispatcher(Stream stream, {this.propagateToWindowId}) { 39 | stream.listen(_handleEvents); 40 | } 41 | 42 | _handleEvents(StreamAdapter event) async { 43 | if (propagateToWindowId != null) { 44 | WaylandMultiWindow.invokeMethod( 45 | propagateToWindowId!, // 1 is the id of the popup subwindow 46 | event.eventId.toString(), 47 | event.data, 48 | ); 49 | } 50 | 51 | switch (event.eventId) { 52 | case daemon_event.ID: 53 | final appEvent = 54 | await compute(SignalAppEvent.fromBuffer, event.data.toList()); 55 | _notificationsStream.add(appEvent); 56 | break; 57 | case theme_event.ID: 58 | final style = 59 | await compute(theme_event.Style.fromBuffer, event.data.toList()); 60 | _styleUpdateStream.add(style); 61 | break; 62 | case settings_event.ID: 63 | _handleSettingsEvents(event); 64 | break; 65 | 66 | default: 67 | return; 68 | } 69 | } 70 | 71 | _handleSettingsEvents(StreamAdapter event) async { 72 | final settingsEvent = await compute( 73 | settings_event.SettingsSignal.fromBuffer, event.data.toList()); 74 | 75 | final operation = settingsEvent.whichOperation(); 76 | switch (operation) { 77 | case settings_event.SettingsSignal_Operation.theme: 78 | final themeService = di(); 79 | final theme = switch (settingsEvent.theme) { 80 | settings_event.ThemeVariante.Light => ThemeType.light, 81 | settings_event.ThemeVariante.Dark => ThemeType.dark, 82 | _ => null, 83 | }; 84 | 85 | if (theme != null) { 86 | _themeChangedStream.add(theme); 87 | themeService.type = theme; 88 | } else { 89 | print("Error theme variante not existing"); 90 | } 91 | 92 | case settings_event.SettingsSignal_Operation.notSet: 93 | break; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/services/events_handlers.dart: -------------------------------------------------------------------------------- 1 | import 'package:notiflut/messages/theme_event.pb.dart' as theme_event; 2 | import 'package:watch_it/watch_it.dart'; 3 | import 'theme_service.dart'; 4 | 5 | void handleThemeChanged(theme_event.Style style) async { 6 | final themeService = di(); 7 | themeService.style = style; 8 | print("STYLE UPDATED"); 9 | } 10 | 11 | void handleThemeTypeChanged(ThemeType themeType) async { 12 | final themeService = di(); 13 | themeService.type = themeType; 14 | } 15 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/services/mainwindow_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:notiflut/messages/daemon_event.pb.dart'; 8 | import 'package:notiflut/messages/daemon_event.pb.dart' as daemon_event; 9 | import 'package:notiflut/messages/app_event.pb.dart' as app_event; 10 | import 'package:notiflut/services/event_dispatcher.dart'; 11 | import 'package:notiflut/services/subwindow_service.dart'; 12 | import 'package:watch_it/watch_it.dart'; 13 | import 'package:window_manager/window_manager.dart'; 14 | import 'package:rinf/rinf.dart'; 15 | import 'package:wayland_multi_window/wayland_multi_window.dart'; 16 | 17 | enum MainWindowEvents { 18 | newNotification, 19 | styleUpdate, 20 | settingsUpdate; 21 | 22 | factory MainWindowEvents.fromString(String value) { 23 | return MainWindowEvents.values.firstWhere((e) => e.toString() == value, 24 | orElse: () => throw Exception("Not an element of MainWindowEvents")); 25 | } 26 | } 27 | 28 | // TODO create an other class to dispatch messages from rust 29 | class MainWindowService extends ChangeNotifier { 30 | List notifications = []; 31 | bool isHidden = true; 32 | 33 | MainWindowService() { 34 | di().notificationsStream.listen(_handleNotificationEvents); 35 | } 36 | 37 | void init() { 38 | WaylandMultiWindow.setMethodHandler(_handleSubWindowEvents); 39 | } 40 | 41 | @override 42 | void dispose() { 43 | WaylandMultiWindow.setMethodHandler(null); 44 | super.dispose(); 45 | } 46 | 47 | Future _handleSubWindowEvents( 48 | MethodCall call, 49 | int fromWindowId, 50 | ) async { 51 | final event = SubWindowEvents.fromString(call.method); 52 | final args = jsonDecode(call.arguments) as Map; 53 | 54 | switch (event) { 55 | case SubWindowEvents.invokeAction: 56 | invokeAction(args["id"] as int, args["action"] as String); 57 | break; 58 | case SubWindowEvents.notificationClosed: 59 | final notificationId = args["id"]; 60 | closeNotification(notificationId); 61 | } 62 | } 63 | 64 | _handleNotificationEvents(SignalAppEvent appEvent) async { 65 | switch (appEvent.type) { 66 | case SignalAppEvent_AppEventType.ToggleNotificationCenter: 67 | isHidden = !isHidden; 68 | if (isHidden) { 69 | Future.delayed(const Duration(milliseconds: 300), () { 70 | hideWindow(); 71 | }); 72 | } else { 73 | showWindow(); 74 | } 75 | break; 76 | case SignalAppEvent_AppEventType.HideNotificationCenter: 77 | isHidden = true; 78 | hideWindow(); 79 | notifyListeners(); 80 | break; 81 | case SignalAppEvent_AppEventType.ShowNotificationCenter: 82 | isHidden = false; 83 | notifyListeners(); 84 | Future.delayed(const Duration(milliseconds: 300), () { 85 | showWindow(); 86 | }); 87 | break; 88 | case SignalAppEvent_AppEventType.NewNotification: 89 | final notification = appEvent.notification; 90 | 91 | if (isHidden) { 92 | appEvent.type = SignalAppEvent_AppEventType.PopupNotification; 93 | WaylandMultiWindow.invokeMethod( 94 | 1, // 1 is the id of the popup subwindow 95 | daemon_event.ID.toString(), 96 | appEvent.writeToBuffer(), 97 | ); 98 | } 99 | 100 | // If urgency is low we don't store it in the notification center 101 | if (notification.hints.urgency == Hints_Urgency.Low) { 102 | return; 103 | } 104 | 105 | if (notification.id != 0) { 106 | notifications.removeWhere((element) => element.id == notification.id); 107 | } 108 | 109 | notifications.add(notification); 110 | notifications.sort((a, b) => 111 | b.createdAt.toDateTime().compareTo(a.createdAt.toDateTime())); 112 | 113 | notifyListeners(); 114 | break; 115 | case SignalAppEvent_AppEventType.CloseNotification: 116 | notifications.removeWhere( 117 | (element) => element.id == appEvent.notificationId.toInt()); 118 | break; 119 | case SignalAppEvent_AppEventType.PopupNotification: 120 | break; 121 | } 122 | } 123 | 124 | Future invokeAction(int id, String action) async { 125 | final event = app_event.AppEvent( 126 | type: app_event.AppEventType.ActionInvoked, 127 | data: action, 128 | notificationId: id, 129 | ); 130 | final request = RustRequest( 131 | resource: app_event.ID, 132 | operation: RustOperation.Create, 133 | message: event.writeToBuffer(), 134 | ); 135 | final response = await requestToRust(request); 136 | if (response.successful) { 137 | print("Action invoked with success"); 138 | } else { 139 | print("Action invoked with error"); 140 | } 141 | } 142 | 143 | void closeNotification(int id) async { 144 | final event = app_event.AppEvent( 145 | type: app_event.AppEventType.Close, 146 | data: null, 147 | notificationId: id, 148 | ); 149 | final request = RustRequest( 150 | resource: app_event.ID, 151 | operation: RustOperation.Create, 152 | message: event.writeToBuffer(), 153 | ); 154 | final response = await requestToRust(request); 155 | if (!response.successful) { 156 | print("Error while closing notification $id"); 157 | } 158 | 159 | notifications.removeWhere(((n) => n.id == id)); 160 | notifyListeners(); 161 | } 162 | 163 | void closeAllAppNotifications(String appName) async { 164 | final event = app_event.AppEvent( 165 | type: app_event.AppEventType.CloseAllApp, 166 | data: appName, 167 | notificationId: null, 168 | ); 169 | final request = RustRequest( 170 | resource: app_event.ID, 171 | operation: RustOperation.Create, 172 | message: event.writeToBuffer(), 173 | ); 174 | final response = await requestToRust(request); 175 | if (!response.successful) { 176 | print("Error while closing all $appName notifications"); 177 | } 178 | 179 | notifications.removeWhere(((n) => n.appName == appName)); 180 | notifyListeners(); 181 | } 182 | 183 | void closeAllNotifications() async { 184 | final event = app_event.AppEvent( 185 | type: app_event.AppEventType.CloseAll, 186 | data: null, 187 | notificationId: null, 188 | ); 189 | final request = RustRequest( 190 | resource: app_event.ID, 191 | operation: RustOperation.Create, 192 | message: event.writeToBuffer(), 193 | ); 194 | final response = await requestToRust(request); 195 | if (!response.successful) { 196 | print("Error while closing all notifications"); 197 | } 198 | 199 | notifications = []; 200 | notifyListeners(); 201 | } 202 | 203 | Future setWindowSize(Size size) async { 204 | if (size.height <= 0 || size.width <= 0) { 205 | return; 206 | } 207 | await windowManager.setLayerSize(size); 208 | } 209 | 210 | Future hideWindow() async { 211 | await windowManager.hide(); 212 | } 213 | 214 | Future showWindow() async { 215 | await windowManager.show(); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/services/mediaplayer_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:mpris/mpris.dart'; 5 | 6 | import '../utils.dart'; 7 | 8 | class MediaPlayerService extends ChangeNotifier { 9 | static Map bestTextColorCache = {}; 10 | 11 | Metadata? metadata; 12 | bool? shuffle; 13 | PlaybackStatus? playbackStatus; 14 | LoopStatus? loopStatus; 15 | double? volume; 16 | Color? bestTextColor; 17 | 18 | List<(MPRISPlayer, String)> players = []; 19 | (MPRISPlayer, String)? currentPlayer; 20 | StreamSubscription? _currentPlayerEventSub; 21 | StreamSubscription? _playerEventSub; 22 | 23 | get showMediaPlayerWidget => players.isNotEmpty; 24 | 25 | void init() { 26 | _getPlayerWithId().then((_) { 27 | _getAllCurrentPlayerData(); 28 | _playerEventSub = MPRIS().playerChanged().listen((e) { 29 | switch (e) { 30 | case PlayerMountEvent(:final player): 31 | player.getIdentity().then((id) { 32 | players.add((player, id)); 33 | notifyListeners(); 34 | }); 35 | case PlayerUnmountEvent(:final playerName): 36 | players.removeWhere((t) => t.$1.name == playerName); 37 | final nextPlayer = players.firstOrNull; 38 | if (nextPlayer != null) { 39 | selectPlayer(nextPlayer.$2); 40 | } 41 | notifyListeners(); 42 | case PlayerUnknownEvent(:final event): 43 | print(event); 44 | } 45 | }); 46 | }); 47 | } 48 | 49 | Future _computeBestTextColor(String url) async { 50 | if (bestTextColorCache[url] != null) { 51 | return bestTextColorCache[url]!; 52 | } 53 | final dominantColor = await getDominantColor(NetworkImage(url)); 54 | if (dominantColor == null) { 55 | return null; 56 | } 57 | final textColor = getContrastingTextColor(dominantColor); 58 | bestTextColorCache[url] = textColor; 59 | 60 | return textColor; 61 | } 62 | 63 | void _getAllCurrentPlayerData() { 64 | currentPlayer?.$1.getVolume().then((value) { 65 | volume = value; 66 | notifyListeners(); 67 | }).catchError((e) { 68 | volume = null; 69 | notifyListeners(); 70 | }); 71 | currentPlayer?.$1.getShuffle().then((value) { 72 | shuffle = value; 73 | notifyListeners(); 74 | }).catchError((e) { 75 | shuffle = null; 76 | notifyListeners(); 77 | }); 78 | currentPlayer?.$1.getLoopStatus().then((value) { 79 | loopStatus = value; 80 | notifyListeners(); 81 | }).catchError((e) { 82 | loopStatus = null; 83 | notifyListeners(); 84 | }); 85 | 86 | currentPlayer?.$1.getMetadata().then((value) { 87 | metadata = value; 88 | notifyListeners(); 89 | return value; 90 | }).then((value) { 91 | if (value.trackArtUrl != null) { 92 | _computeBestTextColor(value.trackArtUrl!).then((color) { 93 | bestTextColor = color; 94 | notifyListeners(); 95 | }); 96 | } else { 97 | bestTextColor = null; 98 | } 99 | }).catchError((e) { 100 | metadata = null; 101 | notifyListeners(); 102 | }); 103 | currentPlayer?.$1.getPlaybackStatus().then((value) { 104 | playbackStatus = value; 105 | notifyListeners(); 106 | }).catchError((e) { 107 | playbackStatus = null; 108 | notifyListeners(); 109 | }); 110 | _currentPlayerEventSub?.cancel(); 111 | _currentPlayerEventSub = 112 | currentPlayer?.$1.propertiesChanged().listen(_propertiesChanged); 113 | } 114 | 115 | void deinit() { 116 | _currentPlayerEventSub?.cancel(); 117 | _playerEventSub?.cancel(); 118 | } 119 | 120 | @override 121 | void dispose() { 122 | deinit(); 123 | super.dispose(); 124 | } 125 | 126 | Future _getPlayerWithId() async { 127 | final players = await MPRIS().getPlayers(); 128 | 129 | for (final player in players) { 130 | this.players.add((player, await player.getIdentity())); 131 | } 132 | currentPlayer = this.players.firstOrNull; 133 | notifyListeners(); 134 | } 135 | 136 | List getPlayersId() { 137 | return players.map((e) => e.$2).toList(); 138 | } 139 | 140 | void _propertiesChanged(PropertyChangedEvent event) { 141 | switch (event) { 142 | case UnsuportedEvent(:final value): 143 | print(value); 144 | case MetaDataChanged(:final metadata): 145 | this.metadata = metadata; 146 | 147 | if (metadata.trackArtUrl != null) { 148 | print("TRACK URL ${metadata.trackArtUrl}"); 149 | _computeBestTextColor(metadata.trackArtUrl!).then((color) { 150 | bestTextColor = color; 151 | notifyListeners(); 152 | }); 153 | } else { 154 | bestTextColor = null; 155 | } 156 | case PlaybackStatusChanged(:final playbackStatus): 157 | this.playbackStatus = playbackStatus; 158 | case LoopStatusChanged(:final loopStatus): 159 | this.loopStatus = loopStatus; 160 | case ShuffleChanged(:final shuffle): 161 | this.shuffle = shuffle; 162 | case VolumeChanged(:final volume): 163 | this.volume = volume; 164 | } 165 | notifyListeners(); 166 | } 167 | 168 | void selectPlayer(String id) { 169 | _currentPlayerEventSub?.cancel(); 170 | final tuple = players.where((player) => player.$2 == id).firstOrNull; 171 | final player = tuple?.$1; 172 | _currentPlayerEventSub = 173 | player?.propertiesChanged().listen(_propertiesChanged); 174 | if (player != null) { 175 | currentPlayer = (player, id); 176 | _getAllCurrentPlayerData(); 177 | } 178 | notifyListeners(); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/services/subwindow_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:notiflut/services/event_dispatcher.dart'; 6 | import 'package:watch_it/watch_it.dart'; 7 | import 'package:wayland_multi_window/wayland_multi_window.dart'; 8 | 9 | import 'package:notiflut/messages/daemon_event.pb.dart' as daemon_event 10 | show Notification; 11 | 12 | import '../messages/daemon_event.pb.dart'; 13 | import '../messages/google/protobuf/timestamp.pb.dart'; 14 | 15 | enum SubWindowEvents { 16 | invokeAction, 17 | notificationClosed; 18 | 19 | factory SubWindowEvents.fromString(String value) { 20 | return SubWindowEvents.values.firstWhere((e) => e.toString() == value, 21 | orElse: () => throw Exception("Not an element of SubWindowEvents")); 22 | } 23 | } 24 | 25 | class SubWindowService extends ChangeNotifier { 26 | List<(daemon_event.Notification, Timer?)> popups = []; 27 | bool isHidden = true; 28 | final int windowId; 29 | final LayerShellController layerController; 30 | 31 | SubWindowService(this.windowId) 32 | : layerController = LayerShellController.fromWindowId(windowId); 33 | 34 | void init() { 35 | di().notificationsStream.listen(_handleEvents); 36 | print("init"); 37 | } 38 | 39 | @override 40 | void dispose() { 41 | super.dispose(); 42 | } 43 | 44 | Future _handleEvents(SignalAppEvent appEvent) async { 45 | switch (appEvent.type) { 46 | case SignalAppEvent_AppEventType.CloseNotification: 47 | break; 48 | case SignalAppEvent_AppEventType.HideNotificationCenter: 49 | break; 50 | case SignalAppEvent_AppEventType.PopupNotification: 51 | final notification = appEvent.notification; 52 | print("new popup"); 53 | 54 | if (popups.isEmpty) { 55 | layerController.show(); 56 | } 57 | 58 | final timer = switch (notification.timeout) { 59 | -1 => schedulePopupCleanUp(notification.id, notification.createdAt), 60 | 0 => null, 61 | _ => schedulePopupCleanUp( 62 | notification.id, 63 | notification.createdAt, 64 | duration: Duration(milliseconds: notification.timeout), 65 | ), 66 | }; 67 | 68 | popups.removeWhere((e) => e.$1.id == notification.id); 69 | popups.insert(0, (notification, timer)); 70 | popups = List.from(popups); 71 | 72 | popups.sort((a, b) => 73 | b.$1.createdAt.toDateTime().compareTo(a.$1.createdAt.toDateTime())); 74 | 75 | notifyListeners(); 76 | break; 77 | case SignalAppEvent_AppEventType.ShowNotificationCenter: 78 | break; 79 | case SignalAppEvent_AppEventType.ToggleNotificationCenter: 80 | break; 81 | case SignalAppEvent_AppEventType.NewNotification: 82 | break; 83 | } 84 | } 85 | 86 | Future invokeAction(int id, String action) async { 87 | WaylandMultiWindow.invokeMethod( 88 | 0, 89 | SubWindowEvents.invokeAction.toString(), 90 | jsonEncode({"id": id, "action": action}), 91 | ); 92 | } 93 | 94 | Future sendCloseEvent(int id) async { 95 | WaylandMultiWindow.invokeMethod( 96 | 0, 97 | SubWindowEvents.notificationClosed.toString(), 98 | jsonEncode({"id": id}), 99 | ); 100 | } 101 | 102 | Timer schedulePopupCleanUp( 103 | int id, 104 | Timestamp date, { 105 | Duration duration = const Duration(seconds: 4), 106 | }) { 107 | return Timer(duration, () { 108 | closePopupWithDate(id, date); 109 | 110 | if (popups.isEmpty) { 111 | // TODO Understand why [PopupsList] does not resize automatically. 112 | if (isHidden) { 113 | layerController.hide(); 114 | print("POPUP CLEAN UP HIDE WINDOW"); 115 | } 116 | } 117 | }); 118 | } 119 | 120 | void updateTimer(int id, Timer timer) { 121 | final index = popups.indexWhere((tuple) => tuple.$1.id == id); 122 | if (index != -1) { 123 | final tuple = popups[index]; 124 | popups[index] = (tuple.$1, timer); 125 | } 126 | } 127 | 128 | void closePopupWithDate(int id, Timestamp date) { 129 | popups = List.from( 130 | popups..removeWhere(((n) => n.$1.id == id && n.$1.createdAt == date))); 131 | notifyListeners(); 132 | 133 | if (popups.isEmpty) { 134 | layerController.hide(); 135 | } 136 | } 137 | 138 | void closePopup(int id) { 139 | popups = List.from(popups..removeWhere(((n) => n.$1.id == id))); 140 | notifyListeners(); 141 | 142 | if (popups.isEmpty) { 143 | layerController.hide(); 144 | } 145 | } 146 | 147 | void cancelClosePopupTimer(int id) { 148 | final timer = popups.firstWhere((tuple) => tuple.$1.id == id).$2; 149 | timer?.cancel(); 150 | } 151 | 152 | Future setLayerSize(Size size) async { 153 | if (size.height <= 0 || size.width <= 0) { 154 | return; 155 | } 156 | await layerController.setLayerSize(size); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/services/theme_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:notiflut/messages/theme_event.pb.dart' as proto; 3 | 4 | enum ThemeType { 5 | light, 6 | dark, 7 | } 8 | 9 | class ThemeService extends ChangeNotifier { 10 | proto.Style? _style; 11 | ThemeType _type = ThemeType.light; 12 | 13 | set type(ThemeType type) { 14 | _type = type; 15 | notifyListeners(); 16 | } 17 | 18 | set style(proto.Style? s) { 19 | _style = s; 20 | notifyListeners(); 21 | } 22 | 23 | proto.Theme? get theme { 24 | return switch (_type) { 25 | ThemeType.light => _style?.light, 26 | ThemeType.dark => _style?.dark, 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:image/image.dart' as img; 6 | import 'package:notiflut/messages/daemon_event.pb.dart' as daemon_event; 7 | import 'package:palette_generator/palette_generator.dart'; 8 | 9 | import 'messages/daemon_event.pbenum.dart'; 10 | 11 | /// Returns an Image from raw data 12 | Image createImageIiibiiay( 13 | int width, int height, Uint8List bytes, int channels, int rowStride) { 14 | final image = img.Image.fromBytes( 15 | width: width, 16 | height: height, 17 | bytes: bytes.buffer, 18 | numChannels: channels, 19 | rowStride: rowStride, 20 | order: img.ChannelOrder.rgb, 21 | ); 22 | final encodedImage = img.encodeBmp(image); 23 | return Image.memory( 24 | encodedImage, 25 | height: 100, 26 | width: 100, 27 | ); 28 | } 29 | 30 | ImageProvider? imageRawToProvider(daemon_event.ImageSource source) { 31 | if(!source.hasPath() && !source.hasImageData()){ 32 | return null; 33 | } 34 | if(source.hasPath() && !File(source.path).existsSync()){ 35 | return null; 36 | } 37 | return switch (source.type.value) { 38 | final value when value == ImageSource_ImageSourceType.Data.value => 39 | createImageIiibiiay( 40 | source.imageData.width, 41 | source.imageData.height, 42 | Uint8List.fromList(source.imageData.data), 43 | source.imageData.onePointTwoBitAlpha ? 4 : 3, 44 | source.imageData.rowstride, 45 | ).image, 46 | final value 47 | when value == ImageSource_ImageSourceType.Path.value && 48 | source.path.isNotEmpty => 49 | Image.file(File(source.path)).image, 50 | _ => null, 51 | }; 52 | } 53 | 54 | extension StringExtension on String { 55 | String capitalize() { 56 | return "${this[0].toUpperCase()}${substring(1)}"; 57 | } 58 | } 59 | 60 | Future getDominantColor(ImageProvider provider) async { 61 | PaletteGenerator paletteGenerator = 62 | await PaletteGenerator.fromImageProvider(provider); 63 | 64 | return paletteGenerator.dominantColor?.color; 65 | } 66 | 67 | Color getContrastingColor(Color color) { 68 | // Adjust this threshold based on my preference 69 | const double threshold = 128.0; 70 | 71 | // Calculate the relative luminance of the color 72 | double luminance = 0.299 * color.red + 0.587 * color.green + 0.114 * color.blue; 73 | 74 | // Choose white or black text based on luminance 75 | return luminance > threshold ? Colors.black : Colors.white; 76 | } 77 | 78 | Color getContrastingTextColor(Color color) { 79 | // Adjust this target lightness based on my preference 80 | const double targetLightness = 0.5; 81 | 82 | HSLColor hslColor = HSLColor.fromColor(color); 83 | double currentLightness = hslColor.lightness; 84 | 85 | // Calculate the difference between the target and current lightness 86 | double lightnessDifference = targetLightness - currentLightness; 87 | 88 | // Adjust the lightness of the color 89 | HSLColor adjustedColor = hslColor.withLightness(currentLightness + lightnessDifference); 90 | 91 | return adjustedColor.toColor(); 92 | } 93 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/widgets/category.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart' hide BoxDecoration, BoxShadow; 2 | import 'package:flutter_inset_box_shadow/flutter_inset_box_shadow.dart'; 3 | import 'package:notiflut/utils.dart'; 4 | import 'package:notiflut/widgets/notification.dart'; 5 | 6 | class NotificationCategory extends StatefulWidget { 7 | final String appName; 8 | final List children; 9 | final Function()? onClose; 10 | final bool defaultState; 11 | final Color? backgroundColor; 12 | final Color? borderColor; 13 | final double? borderRadius; 14 | final double? borderWidth; 15 | 16 | const NotificationCategory({ 17 | required this.appName, 18 | this.defaultState = false, 19 | this.children = const [], 20 | this.onClose, 21 | super.key, 22 | this.backgroundColor, 23 | this.borderColor, 24 | this.borderRadius, 25 | this.borderWidth, 26 | }); 27 | 28 | @override 29 | State createState() => _NotificationCategoryState(); 30 | } 31 | 32 | class _NotificationCategoryState extends State { 33 | bool _open = false; 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | _open = widget.defaultState; 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return Column( 44 | children: [ 45 | Row( 46 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 47 | children: [ 48 | Padding( 49 | padding: const EdgeInsets.fromLTRB(10, 0, 0, 0), 50 | child: Text( 51 | widget.appName.capitalize(), 52 | style: const TextStyle(color: Colors.white), 53 | ), 54 | ), 55 | if (widget.children.length > 1) 56 | Row( 57 | children: [ 58 | ElevatedButton( 59 | style: ButtonStyle( 60 | surfaceTintColor: 61 | MaterialStateProperty.all(Colors.transparent), 62 | backgroundColor: MaterialStatePropertyAll( 63 | widget.backgroundColor != null 64 | ? widget.backgroundColor! 65 | : const Color(0xFFE0E0E0)), 66 | shape: const MaterialStatePropertyAll( 67 | RoundedRectangleBorder( 68 | borderRadius: 69 | BorderRadius.all(Radius.circular(10))), 70 | )), 71 | onPressed: () { 72 | setState(() { 73 | _open = !_open; 74 | }); 75 | }, 76 | child: Text(_open ? "Show less" : "See more", 77 | style: const TextStyle(color: Colors.black)), 78 | ), 79 | const SizedBox(width: 20), 80 | CircleAvatar( 81 | radius: 15, 82 | backgroundColor: const Color(0xBBE0E0E0), 83 | child: IconButton( 84 | iconSize: 15, 85 | style: ButtonStyle( 86 | backgroundColor: MaterialStateProperty.all( 87 | widget.backgroundColor)), 88 | icon: const Icon( 89 | Icons.close, 90 | color: Colors.black, 91 | ), 92 | onPressed: widget.onClose, 93 | ), 94 | ), 95 | const SizedBox(width: 5), 96 | ], 97 | ), 98 | ], 99 | ), 100 | AnimatedCrossFade( 101 | firstChild: widget.children.isEmpty 102 | ? NotificationTile.empty() 103 | : NotificationTileStack( 104 | widget.children[0], 105 | backgroundColor: widget.backgroundColor, 106 | borderSide: BorderSide( 107 | color: widget.borderColor ?? Colors.black, 108 | width: widget.borderWidth ?? 0, 109 | ), 110 | ), 111 | secondChild: Column(children: widget.children), 112 | crossFadeState: widget.children.length == 1 113 | ? CrossFadeState.showSecond 114 | : _open 115 | ? CrossFadeState.showSecond 116 | : CrossFadeState.showFirst, 117 | duration: const Duration(milliseconds: 300), 118 | ), 119 | ], 120 | ); 121 | } 122 | } 123 | 124 | class NotificationTileStack extends StatelessWidget { 125 | final NotificationTile tile; 126 | final Color? backgroundColor; 127 | final BorderSide borderSide; 128 | final double? borderRadius; 129 | const NotificationTileStack( 130 | this.tile, { 131 | super.key, 132 | this.backgroundColor, 133 | required this.borderSide, 134 | this.borderRadius, 135 | }); 136 | 137 | Widget buildFakeNotificationBottomTile(BuildContext context, int lvl) { 138 | return Container( 139 | margin: EdgeInsets.fromLTRB(5 + lvl * 5, 0, 5 + lvl * 5, 0), 140 | decoration: BoxDecoration( 141 | color: backgroundColor ?? const Color(0xFFE0E0E0), 142 | borderRadius: BorderRadius.only( 143 | bottomLeft: Radius.circular(borderRadius ?? 30), 144 | bottomRight: Radius.circular(borderRadius ?? 30), 145 | ), 146 | border: Border(left: borderSide, right: borderSide, bottom: borderSide), 147 | boxShadow: const [ 148 | BoxShadow( 149 | color: Colors.black54, 150 | blurRadius: 1.0, 151 | offset: Offset(0.0, 0.75), 152 | inset: true, 153 | ) 154 | ], 155 | ), 156 | height: 10, 157 | ); 158 | } 159 | 160 | @override 161 | Widget build(BuildContext context) { 162 | return Column( 163 | children: [ 164 | Stack( 165 | children: [ 166 | tile, 167 | Column( 168 | children: [ 169 | Visibility( 170 | visible: false, 171 | maintainSize: true, 172 | maintainAnimation: true, 173 | maintainState: true, 174 | child: NotificationTile( 175 | tile.id, 176 | tile.appName, 177 | tile.title, 178 | tile.body, 179 | actions: tile.actions, 180 | margin: const EdgeInsets.fromLTRB(4, 4, 4, 0), 181 | backgroundColor: backgroundColor, 182 | ), 183 | ), 184 | buildFakeNotificationBottomTile(context, 1), 185 | buildFakeNotificationBottomTile(context, 2), 186 | ], 187 | ) 188 | ], 189 | ), 190 | ], 191 | ); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/widgets/mediaPlayer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:mpris/mpris.dart'; 5 | import 'package:watch_it/watch_it.dart'; 6 | import '../services/mediaplayer_service.dart'; 7 | 8 | class MediaPlayer extends StatefulWidget with WatchItStatefulWidgetMixin { 9 | final BorderRadius? borderRadius; 10 | final Color? backgroundColor; 11 | final Color? titleTextColor; 12 | final Color? subtitleTextColor; 13 | final Color? bodyTextColor; 14 | final Color? borderColor; 15 | final double? borderWidth; 16 | 17 | MediaPlayer({ 18 | super.key, 19 | this.borderRadius, 20 | this.backgroundColor, 21 | this.bodyTextColor, 22 | this.titleTextColor, 23 | this.subtitleTextColor, 24 | this.borderWidth, 25 | this.borderColor, 26 | }); 27 | 28 | @override 29 | State createState() => _MediaPlayerState(); 30 | } 31 | 32 | class _MediaPlayerState extends State { 33 | late MediaPlayerService mpService; 34 | 35 | @override 36 | void initState() { 37 | mpService = di(); 38 | super.initState(); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | final mpService = watchIt(); 44 | final metadata = mpService.metadata; 45 | final bodyColor = 46 | mpService.bestTextColor ?? widget.bodyTextColor ?? Colors.black; 47 | final titleColor = 48 | mpService.bestTextColor ?? widget.titleTextColor ?? Colors.black; 49 | final subtitleColor = 50 | mpService.bestTextColor ?? widget.subtitleTextColor ?? Colors.black; 51 | 52 | final bodyTextStyle = TextStyle( 53 | color: bodyColor, 54 | fontSize: 16, 55 | fontWeight: FontWeight.w500, 56 | ); 57 | final subtitleTextStyle = TextStyle( 58 | color: subtitleColor, 59 | fontSize: 16, 60 | fontWeight: FontWeight.w500, 61 | ); 62 | 63 | return Card( 64 | surfaceTintColor: Colors.transparent, 65 | color: widget.backgroundColor ?? const Color(0xBBE0E0E0), 66 | clipBehavior: Clip.antiAliasWithSaveLayer, 67 | shape: RoundedRectangleBorder( 68 | borderRadius: widget.borderRadius ?? BorderRadius.circular(20), 69 | side: BorderSide( 70 | color: widget.borderColor ?? Colors.black, 71 | width: widget.borderWidth ?? 0), 72 | ), 73 | child: Container( 74 | decoration: metadata?.trackArtUrl != null 75 | ? BoxDecoration( 76 | image: DecorationImage( 77 | image: NetworkImage(metadata!.trackArtUrl!), 78 | fit: BoxFit.fitWidth, 79 | alignment: Alignment.center, 80 | ), 81 | ) 82 | : null, 83 | child: BackdropFilter( 84 | filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), 85 | child: Padding( 86 | padding: const EdgeInsets.all(15.0), 87 | child: Stack( 88 | children: [ 89 | Column( 90 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 91 | children: [ 92 | CardTitle(color: titleColor), 93 | if (metadata?.trackArtists != null) 94 | Row( 95 | mainAxisAlignment: MainAxisAlignment.center, 96 | children: [ 97 | Text( 98 | metadata!.trackArtists!.join(" - "), 99 | style: subtitleTextStyle, 100 | ) 101 | ], 102 | ), 103 | if (metadata?.trackTitle != null) 104 | Row( 105 | mainAxisAlignment: MainAxisAlignment.center, 106 | children: [ 107 | SizedBox( 108 | width: 400, 109 | child: Text( 110 | metadata!.trackTitle!, 111 | style: bodyTextStyle, 112 | overflow: TextOverflow.fade, 113 | softWrap: false, 114 | textAlign: TextAlign.center, 115 | ), 116 | ) 117 | ], 118 | ), 119 | PlayerButtons(color: bodyColor), 120 | ], 121 | ), 122 | ], 123 | ), 124 | ), 125 | ), 126 | ), 127 | ); 128 | } 129 | } 130 | 131 | class PlayerButtons extends StatefulWidget with WatchItStatefulWidgetMixin { 132 | final Color color; 133 | 134 | const PlayerButtons({ 135 | required this.color, 136 | super.key, 137 | }); 138 | 139 | @override 140 | State createState() => _PlayerButtonsState(); 141 | } 142 | 143 | class _PlayerButtonsState extends State { 144 | @override 145 | Widget build(BuildContext context) { 146 | final mpService = watchIt(); 147 | final players = mpService.players; 148 | final currentPlayer = mpService.currentPlayer; 149 | final color = widget.color; 150 | return Row( 151 | mainAxisAlignment: MainAxisAlignment.center, 152 | children: [ 153 | Expanded( 154 | flex: 1, 155 | child: DropdownButton( 156 | style: TextStyle(color: color), 157 | value: currentPlayer?.$2, 158 | items: players 159 | .map((e) => DropdownMenuItem(value: e.$2, child: Text(e.$2))) 160 | .toList(), 161 | onChanged: (String? playerId) { 162 | if (playerId == null) { 163 | return; 164 | } 165 | setState(() { 166 | mpService.selectPlayer(playerId); 167 | }); 168 | }, 169 | ), 170 | ), 171 | Expanded( 172 | flex: 3, 173 | child: Row( 174 | mainAxisAlignment: MainAxisAlignment.center, 175 | children: [ 176 | if (mpService.shuffle != null) 177 | ShuffleButton(mpService: mpService, color: color), 178 | PreviousButton(mpService: mpService, color: color), 179 | PlayPauseButton(mpService: mpService, color: color), 180 | NextButton(mpService: mpService, color: color), 181 | if (mpService.loopStatus != null) 182 | LoopButton(mpService: mpService, color: color), 183 | ], 184 | ), 185 | ), 186 | Expanded(flex: 1, child: Container()), 187 | ], 188 | ); 189 | } 190 | } 191 | 192 | class LoopButton extends StatelessWidget { 193 | const LoopButton({ 194 | super.key, 195 | required this.mpService, 196 | required this.color, 197 | }); 198 | 199 | final MediaPlayerService mpService; 200 | final Color color; 201 | 202 | @override 203 | Widget build(BuildContext context) { 204 | return IconButton( 205 | onPressed: () => 206 | mpService.currentPlayer?.$1.setLoopStatus(LoopStatus.none), 207 | icon: switch (mpService.loopStatus) { 208 | LoopStatus.none => Icon(Icons.repeat_outlined, color: color), 209 | LoopStatus.track => Icon(Icons.repeat_one_outlined, color: color), 210 | LoopStatus.playlist || 211 | null => 212 | Icon(Icons.repeat_on_outlined, color: color), 213 | }, 214 | ); 215 | } 216 | } 217 | 218 | class NextButton extends StatelessWidget { 219 | const NextButton({ 220 | super.key, 221 | required this.mpService, 222 | required this.color, 223 | }); 224 | 225 | final MediaPlayerService mpService; 226 | final Color color; 227 | 228 | @override 229 | Widget build(BuildContext context) { 230 | return IconButton( 231 | onPressed: () => mpService.currentPlayer?.$1.next(), 232 | icon: Icon(Icons.skip_next_outlined, color: color)); 233 | } 234 | } 235 | 236 | class PlayPauseButton extends StatelessWidget { 237 | const PlayPauseButton({ 238 | super.key, 239 | required this.mpService, 240 | required this.color, 241 | }); 242 | 243 | final MediaPlayerService mpService; 244 | final Color color; 245 | 246 | @override 247 | Widget build(BuildContext context) { 248 | return IconButton( 249 | onPressed: () => mpService.currentPlayer?.$1.toggle(), 250 | icon: mpService.playbackStatus == PlaybackStatus.playing 251 | ? Icon(Icons.pause_outlined, color: color) 252 | : Icon(Icons.play_arrow_outlined, color: color)); 253 | } 254 | } 255 | 256 | class PreviousButton extends StatelessWidget { 257 | const PreviousButton({ 258 | super.key, 259 | required this.mpService, 260 | required this.color, 261 | }); 262 | 263 | final MediaPlayerService mpService; 264 | final Color color; 265 | 266 | @override 267 | Widget build(BuildContext context) { 268 | return IconButton( 269 | onPressed: () => mpService.currentPlayer?.$1.previous(), 270 | icon: Icon(Icons.skip_previous_outlined, color: color)); 271 | } 272 | } 273 | 274 | class ShuffleButton extends StatelessWidget { 275 | const ShuffleButton({ 276 | super.key, 277 | required this.mpService, 278 | required this.color, 279 | }); 280 | 281 | final MediaPlayerService mpService; 282 | final Color color; 283 | 284 | @override 285 | Widget build(BuildContext context) { 286 | return IconButton( 287 | onPressed: () => 288 | mpService.currentPlayer?.$1.setShuffle(!mpService.shuffle!), 289 | icon: switch (mpService.shuffle) { 290 | true => Icon(Icons.shuffle_on_outlined, color: color), 291 | _ => Icon(Icons.shuffle_outlined, color: color), 292 | }, 293 | ); 294 | } 295 | } 296 | 297 | class CardTitle extends StatelessWidget with WatchItMixin { 298 | final Color color; 299 | const CardTitle({required this.color, super.key}); 300 | 301 | @override 302 | Widget build(BuildContext context) { 303 | final title = watchPropertyValue( 304 | (MediaPlayerService s) => s.currentPlayer?.$2 ?? "Title not found"); 305 | return Row( 306 | children: [ 307 | Icon( 308 | Icons.music_note, 309 | color: color, 310 | ), 311 | Text(title, style: TextStyle(color: color)), 312 | ], 313 | ); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/widgets/notification.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:open_url/open_url.dart'; 6 | import 'package:flutter_html/flutter_html.dart'; 7 | 8 | class NotificationAction { 9 | final Function() action; 10 | final String label; 11 | 12 | const NotificationAction(this.label, this.action); 13 | } 14 | 15 | /// Transforms the list of string to list of tuple 16 | /// We do this beacause the data received by rust is organized like that. 17 | List<(String, String)> actionsListToMap(List actionsList) { 18 | List<(String, String)> newList = []; 19 | 20 | for (int i = 0; i < actionsList.length; i += 2) { 21 | newList.add((actionsList[i], actionsList[i + 1])); 22 | } 23 | return newList; 24 | } 25 | 26 | class NotificationTile extends StatelessWidget { 27 | final int id; 28 | final String appName; 29 | final String title; 30 | final String body; 31 | final ImageProvider? iconProvider; 32 | final ImageProvider? imageProvider; 33 | final Function()? onTileTap; 34 | final Function()? closeAction; 35 | final Function(PointerEnterEvent)? onHover; 36 | final Function(PointerExitEvent)? onHoverExit; 37 | final List? actions; 38 | final DateTime? createdAt; 39 | final EdgeInsetsGeometry? margin; 40 | final Color? backgroundColor; 41 | final BorderRadius? borderRadius; 42 | final Color? bodyTextColor; 43 | final Color? titleTextColor; 44 | final Color? subtitleTextColor; 45 | final Color? buttonTextColor; 46 | final Color? borderColor; 47 | final double? borderWidth; 48 | 49 | factory NotificationTile.empty() { 50 | return const NotificationTile(0, "", "", ""); 51 | } 52 | 53 | const NotificationTile(this.id, this.appName, this.title, this.body, 54 | {super.key, 55 | this.imageProvider, 56 | this.iconProvider, 57 | this.closeAction, 58 | this.onTileTap, 59 | this.onHover, 60 | this.onHoverExit, 61 | this.actions, 62 | this.createdAt, 63 | this.margin, 64 | this.backgroundColor, 65 | this.borderRadius, 66 | this.bodyTextColor, 67 | this.titleTextColor, 68 | this.subtitleTextColor, 69 | this.buttonTextColor, 70 | this.borderColor, 71 | this.borderWidth}); 72 | // This widget is the root of your application. 73 | @override 74 | Widget build(BuildContext context) { 75 | final buttons = actions 76 | ?.map((v) => TextButton( 77 | onPressed: v.action, 78 | child: Text( 79 | v.label, 80 | style: TextStyle(color: buttonTextColor ?? Colors.black), 81 | ))) 82 | .toList(); 83 | String time = ""; 84 | if (createdAt != null) { 85 | final Duration duration = DateTime.now().difference(createdAt!); 86 | if (duration.inSeconds < 60) { 87 | time = "Now"; 88 | } else if (duration.inHours < 1) { 89 | time = "${duration.inMinutes} min. ago"; 90 | } else if (duration.inDays < 1) { 91 | time = "${duration.inHours} hours ago"; 92 | } else { 93 | time = "${createdAt!.day}/${createdAt!.month}"; 94 | } 95 | } 96 | return MouseRegion( 97 | onEnter: onHover, 98 | onExit: onHoverExit, 99 | child: Card( 100 | surfaceTintColor: Colors.transparent, 101 | margin: margin, 102 | color: backgroundColor ?? const Color(0xBBE0E0E0), 103 | shape: RoundedRectangleBorder( 104 | borderRadius: borderWidth == null || borderWidth == 0 105 | ? borderRadius ?? BorderRadius.circular(20) 106 | : BorderRadius.circular(0), 107 | side: BorderSide( 108 | color: borderColor ?? Colors.black, width: borderWidth ?? 0), 109 | ), 110 | child: Column( 111 | children: [ 112 | Padding( 113 | padding: const EdgeInsets.fromLTRB(15, 15, 15, 0), 114 | child: Row( 115 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 116 | children: [ 117 | Row( 118 | children: [ 119 | CircleAvatar( 120 | backgroundColor: Colors.transparent, 121 | backgroundImage: iconProvider, 122 | radius: 13, 123 | ), 124 | const SizedBox(width: 10), 125 | Text(appName, 126 | style: 127 | TextStyle(fontSize: 13, color: titleTextColor)), 128 | ], 129 | ), 130 | Row( 131 | mainAxisAlignment: MainAxisAlignment.end, 132 | children: [ 133 | Text(time, style: TextStyle(color: titleTextColor)), 134 | InkWell( 135 | borderRadius: BorderRadius.circular(20), 136 | onTap: closeAction, 137 | child: Icon(Icons.close, color: titleTextColor), 138 | ), 139 | ], 140 | ), 141 | ], 142 | ), 143 | ), 144 | ListTile( 145 | title: Text(title, style: TextStyle(color: subtitleTextColor)), 146 | onTap: onTileTap, 147 | subtitle: Html( 148 | data: body.replaceAll("\\n", "
"), 149 | extensions: [ 150 | ImageExtension( 151 | networkSchemas: {"file"}, 152 | builder: (extensionContext) { 153 | final element = extensionContext.styledElement; 154 | String src = element!.attributes["src"]!; 155 | if (src.contains("file://")) { 156 | src = src.replaceAll("file://", ""); 157 | 158 | final alt = element.attributes["alt"]!; 159 | if (!File(src).existsSync()) { 160 | return Text(alt); 161 | } 162 | } 163 | return Image.file( 164 | File(src), 165 | ); 166 | }), 167 | ], 168 | onLinkTap: (link, context, element) { 169 | openUrl(link!); 170 | }, 171 | style: { 172 | "*": Style( 173 | color: bodyTextColor ?? Colors.black, 174 | ), 175 | // "body": Style( 176 | // margin: Margins.all(0.0), 177 | // padding: HtmlPaddings.all(0.0), 178 | // ), 179 | // "img": Style( 180 | // display: Display.block, 181 | // width: Width.auto(), 182 | // margin: Margins.all(0.0), 183 | // padding: HtmlPaddings.all(0.0), 184 | // ), 185 | }, 186 | ), 187 | leading: CircleAvatar( 188 | backgroundColor: Colors.transparent, 189 | backgroundImage: imageProvider, 190 | ), 191 | ), 192 | const SizedBox(height: 10), 193 | Row( 194 | mainAxisAlignment: MainAxisAlignment.center, 195 | children: buttons ?? [], 196 | ), 197 | const SizedBox(height: 5), 198 | ], 199 | ), 200 | ), 201 | ); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/widgets/notification_center.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:notiflut/messages/daemon_event.pb.dart' as daemon_event 5 | show Notification; 6 | import 'package:notiflut/services/cache_service.dart'; 7 | import 'package:notiflut/widgets/mediaPlayer.dart'; 8 | import 'package:watch_it/watch_it.dart'; 9 | 10 | import '../services/mainwindow_service.dart'; 11 | import '../services/mediaplayer_service.dart'; 12 | import '../services/theme_service.dart'; 13 | import '../utils.dart'; 14 | import 'category.dart'; 15 | import 'notification.dart'; 16 | 17 | class NotificationCenter extends StatefulWidget 18 | with WatchItStatefulWidgetMixin { 19 | NotificationCenter({super.key}); 20 | 21 | @override 22 | State createState() => _NotificationCenterState(); 23 | } 24 | 25 | class _NotificationCenterState extends State { 26 | Timer? notificationUpTimeTimer; 27 | final CacheService> _imageCache = 28 | CacheService(); 29 | 30 | @override 31 | void dispose() { 32 | notificationUpTimeTimer?.cancel(); 33 | super.dispose(); 34 | } 35 | 36 | @override 37 | void initState() { 38 | super.initState(); 39 | 40 | notificationUpTimeTimer = 41 | Timer.periodic(const Duration(seconds: 30), (timer) { 42 | setState(() {}); 43 | }); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | final mainWindowService = watchIt(); 49 | final notifications = mainWindowService.notifications; 50 | final themeService = watchIt(); 51 | final theme = themeService.theme; 52 | final showMediaPlayer = 53 | watchPropertyValue((MediaPlayerService s) => s.showMediaPlayerWidget); 54 | 55 | final notificationByCategory = notifications 56 | .fold(>{}, (map, notification) { 57 | final key = notification.appName; 58 | 59 | map.putIfAbsent(key, () => []); 60 | map[key]!.add(notification); 61 | return map; 62 | }); 63 | 64 | final keys = notificationByCategory.keys; 65 | final categoryWidgets = keys.map((appName) { 66 | final notifications = notificationByCategory[appName]!; 67 | 68 | final notificationTiles = notifications.map((n) { 69 | ImageProvider? imageProvider = _imageCache.getOrPut( 70 | n.summary, () => imageRawToProvider(n.appImage)); 71 | ImageProvider? iconeProvider = _imageCache.getOrPut( 72 | n.appName, () => imageRawToProvider(n.appIcon)); 73 | return NotificationTile( 74 | n.id, 75 | n.appName, 76 | n.summary, 77 | n.body, 78 | iconProvider: iconeProvider, 79 | imageProvider: imageProvider, 80 | createdAt: n.createdAt.toDateTime(), 81 | actions: actionsListToMap(n.actions) 82 | .where((element) => element.$1 != "default") 83 | .map((e) => NotificationAction(e.$2, () { 84 | mainWindowService.invokeAction(n.id, e.$1); 85 | mainWindowService.closeNotification(n.id); 86 | })) 87 | .toList(), 88 | onTileTap: () { 89 | mainWindowService.invokeAction(n.id, "default"); 90 | mainWindowService.closeNotification(n.id); 91 | }, 92 | closeAction: () { 93 | mainWindowService.closeNotification(n.id); 94 | }, 95 | backgroundColor: theme != null 96 | ? Color(theme.notificationStyle.backgroundColor) 97 | : null, 98 | borderRadius: theme != null 99 | ? BorderRadius.circular( 100 | theme.notificationStyle.borderRadius.toDouble()) 101 | : null, 102 | bodyTextColor: theme != null 103 | ? Color(theme.notificationStyle.bodyTextColor) 104 | : null, 105 | titleTextColor: theme != null 106 | ? Color(theme.notificationStyle.titleTextColor) 107 | : null, 108 | subtitleTextColor: theme != null 109 | ? Color(theme.notificationStyle.subtitleTextColor) 110 | : null, 111 | buttonTextColor: theme != null 112 | ? Color(theme.notificationStyle.buttonTextColor) 113 | : null, 114 | borderWidth: theme?.notificationStyle.borderWidth.toDouble(), 115 | borderColor: 116 | theme != null ? Color(theme.notificationStyle.borderColor) : null, 117 | ); 118 | }).toList(); 119 | 120 | return NotificationCategory( 121 | key: Key(appName), 122 | appName: appName, 123 | backgroundColor: theme != null 124 | ? Color(theme.notificationStyle.backgroundColor) 125 | : null, 126 | onClose: () { 127 | mainWindowService.closeAllAppNotifications(appName); 128 | }, 129 | borderColor: 130 | theme != null ? Color(theme.notificationStyle.borderColor) : null, 131 | borderWidth: theme?.notificationStyle.borderWidth.toDouble(), 132 | borderRadius: theme?.notificationStyle.borderRadius.toDouble(), 133 | children: notificationTiles, 134 | ); 135 | }).toList(); 136 | 137 | // TODO find why there is a warning on runtime 138 | return Row( 139 | mainAxisAlignment: MainAxisAlignment.end, 140 | mainAxisSize: MainAxisSize.max, 141 | children: [ 142 | Container( 143 | width: 500, 144 | color: Colors.transparent, 145 | child: ListView( 146 | children: [ 147 | if (showMediaPlayer) 148 | MediaPlayer( 149 | borderRadius: theme != null 150 | ? BorderRadius.circular( 151 | theme.notificationStyle.borderRadius.toDouble()) 152 | : null, 153 | backgroundColor: theme != null 154 | ? Color(theme.notificationStyle.backgroundColor) 155 | : null, 156 | bodyTextColor: theme != null 157 | ? Color(theme.notificationStyle.bodyTextColor) 158 | : null, 159 | titleTextColor: theme != null 160 | ? Color(theme.notificationStyle.titleTextColor) 161 | : null, 162 | subtitleTextColor: theme != null 163 | ? Color(theme.notificationStyle.subtitleTextColor) 164 | : null, 165 | borderWidth: theme?.notificationStyle.borderWidth.toDouble(), 166 | borderColor: theme != null 167 | ? Color(theme.notificationStyle.borderColor) 168 | : null, 169 | ), 170 | ...categoryWidgets 171 | ], 172 | ), 173 | ), 174 | ], 175 | ); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/widgets/popups_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:notiflut/services/subwindow_service.dart'; 3 | import 'package:notiflut/utils.dart'; 4 | import 'package:notiflut/widgets/notification.dart'; 5 | import 'package:watch_it/watch_it.dart'; 6 | 7 | import '../services/theme_service.dart'; 8 | 9 | class PopupsList extends StatefulWidget with WatchItStatefulWidgetMixin { 10 | PopupsList({super.key}); 11 | 12 | @override 13 | State createState() => _PopupsListState(); 14 | } 15 | 16 | class _PopupsListState extends State { 17 | final ScrollController scrollController = ScrollController(); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final notifications = watchIt().popups; 22 | final notificationService = di(); 23 | final theme = watchIt().theme; 24 | resizeWindowAfterBuild(); 25 | return ListView( 26 | shrinkWrap: true, 27 | controller: scrollController, 28 | children: notifications.reversed.map((tuple) { 29 | final n = tuple.$1; 30 | ImageProvider? imageProvider = imageRawToProvider(n.appImage); 31 | ImageProvider? iconeProvider = imageRawToProvider(n.appIcon); 32 | return NotificationTile( 33 | n.id, 34 | n.appName, 35 | n.summary, 36 | n.body, 37 | iconProvider: iconeProvider, 38 | imageProvider: imageProvider, 39 | createdAt: n.createdAt.toDateTime(), 40 | actions: actionsListToMap(n.actions) 41 | .where((element) => element.$1 != "default") 42 | .map((e) => NotificationAction(e.$2, () { 43 | di().invokeAction(n.id, e.$1); 44 | di() 45 | .closePopupWithDate(n.id, n.createdAt); 46 | })) 47 | .toList(), 48 | onTileTap: () { 49 | notificationService.invokeAction(n.id, "default"); 50 | notificationService.closePopupWithDate(n.id, n.createdAt); 51 | }, 52 | closeAction: () { 53 | notificationService.closePopupWithDate(n.id, n.createdAt); 54 | notificationService.sendCloseEvent(n.id); 55 | }, 56 | onHover: (pointer) { 57 | notificationService.cancelClosePopupTimer(n.id); 58 | }, 59 | onHoverExit: (pointer) { 60 | final timer = notificationService.schedulePopupCleanUp( 61 | n.id, 62 | n.createdAt, 63 | duration: const Duration(seconds: 2), 64 | ); 65 | notificationService.updateTimer(n.id, timer); 66 | }, 67 | backgroundColor: theme != null 68 | ? Color(theme.notificationStyle.backgroundColor) 69 | : null, 70 | borderRadius: theme != null 71 | ? BorderRadius.circular( 72 | theme.notificationStyle.borderRadius.toDouble()) 73 | : null, 74 | bodyTextColor: theme != null 75 | ? Color(theme.notificationStyle.bodyTextColor) 76 | : null, 77 | titleTextColor: theme != null 78 | ? Color(theme.notificationStyle.titleTextColor) 79 | : null, 80 | subtitleTextColor: theme != null 81 | ? Color(theme.notificationStyle.subtitleTextColor) 82 | : null, 83 | buttonTextColor: theme != null 84 | ? Color(theme.notificationStyle.buttonTextColor) 85 | : null, 86 | borderWidth: theme?.notificationStyle.borderWidth.toDouble(), 87 | borderColor: 88 | theme != null ? Color(theme.notificationStyle.borderColor) : null, 89 | ); 90 | }).toList(), 91 | ); 92 | } 93 | 94 | Future resizeWindowAfterBuild() async { 95 | //This delay is used to be sure that the build function is completed 96 | await Future.delayed(const Duration(milliseconds: 500)); 97 | if (scrollController.hasClients) { 98 | final size = scrollController.position.extentAfter + 99 | scrollController.position.extentInside; 100 | if (size > 1) { 101 | print("New size: $size"); 102 | await di().setLayerSize(Size(500, size)); 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /notiflut_daemon/lib/window_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:wayland_multi_window/wayland_multi_window.dart' as w; 5 | import 'package:window_manager/window_manager.dart'; 6 | 7 | const _smallWindowSize = Size(500, 200); 8 | 9 | Future setupMainWindow() async { 10 | WindowOptions windowOptions = const WindowOptions( 11 | size: Size(800, 600), 12 | center: true, 13 | backgroundColor: Colors.transparent, 14 | skipTaskbar: false, 15 | titleBarStyle: TitleBarStyle.hidden, 16 | windowButtonVisibility: false, 17 | ); 18 | windowManager.waitUntilReadyToShow(windowOptions, () async { 19 | await windowManager.setLayer(LayerSurface.top); 20 | await windowManager.setAnchor(LayerEdge.top, true); 21 | await windowManager.setAnchor(LayerEdge.right, true); 22 | await windowManager.setAnchor(LayerEdge.left, true); 23 | await windowManager.setAnchor(LayerEdge.bottom, true); 24 | }); 25 | } 26 | 27 | Future setupSubWindow() async{ 28 | final window = await w.WaylandMultiWindow.createLayerShell(""); 29 | window 30 | ..setTitle('Notification popup') 31 | ..setLayer(w.LayerSurface.top) 32 | ..setAnchor(w.LayerEdge.top, true) 33 | ..setAnchor(w.LayerEdge.right, true) 34 | ..setAnchor(w.LayerEdge.left, false) 35 | ..setAnchor(w.LayerEdge.bottom, false) 36 | ..setLayerSize(_smallWindowSize) 37 | ..hide(); 38 | 39 | return window; 40 | } 41 | 42 | -------------------------------------------------------------------------------- /notiflut_daemon/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /notiflut_daemon/linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.10) 3 | project(runner LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "notiflutland") 8 | # The unique GTK application identifier for this application. See: 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID 10 | set(APPLICATION_ID "com.example.notiflutland") 11 | 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 13 | # versions of CMake. 14 | cmake_policy(SET CMP0063 NEW) 15 | 16 | # Load bundled libraries from the lib/ directory relative to the binary. 17 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 18 | 19 | # Root filesystem for cross-building. 20 | if(FLUTTER_TARGET_PLATFORM_SYSROOT) 21 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) 22 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 26 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 27 | endif() 28 | 29 | # Define build configuration options. 30 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 31 | set(CMAKE_BUILD_TYPE "Debug" CACHE 32 | STRING "Flutter build mode" FORCE) 33 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 34 | "Debug" "Profile" "Release") 35 | endif() 36 | 37 | # Compilation settings that should be applied to most targets. 38 | # 39 | # Be cautious about adding new options here, as plugins use this function by 40 | # default. In most cases, you should add new options to specific targets instead 41 | # of modifying this function. 42 | function(APPLY_STANDARD_SETTINGS TARGET) 43 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 44 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 45 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 46 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 47 | endfunction() 48 | 49 | # Flutter library and tool build rules. 50 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 51 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 52 | 53 | # System-level dependencies. 54 | find_package(PkgConfig REQUIRED) 55 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 56 | pkg_check_modules(GLS REQUIRED IMPORTED_TARGET gtk-layer-shell-0) 57 | 58 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 59 | 60 | # Define the application target. To change its name, change BINARY_NAME above, 61 | # not the value here, or `flutter run` will no longer work. 62 | # 63 | # Any new source files that you add to the application should be added here. 64 | add_executable(${BINARY_NAME} 65 | "main.cc" 66 | "my_application.cc" 67 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 68 | ) 69 | 70 | # Apply the standard set of build settings. This can be removed for applications 71 | # that need different build settings. 72 | apply_standard_settings(${BINARY_NAME}) 73 | 74 | # Add dependency libraries. Add any application-specific dependencies here. 75 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 76 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 77 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GLS) 78 | 79 | # Run the Flutter tool portions of the build. This must not be removed. 80 | add_dependencies(${BINARY_NAME} flutter_assemble) 81 | 82 | # Only the install-generated bundle's copy of the executable will launch 83 | # correctly, since the resources must in the right relative locations. To avoid 84 | # people trying to run the unbundled copy, put it in a subdirectory instead of 85 | # the default top-level location. 86 | set_target_properties(${BINARY_NAME} 87 | PROPERTIES 88 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 89 | ) 90 | 91 | # Generated plugin build rules, which manage building the plugins and adding 92 | # them to the application. 93 | include(flutter/generated_plugins.cmake) 94 | 95 | # === Installation === 96 | # By default, "installing" just makes a relocatable bundle in the build 97 | # directory. 98 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 99 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 100 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 101 | endif() 102 | 103 | # Start with a clean build bundle directory every time. 104 | install(CODE " 105 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 106 | " COMPONENT Runtime) 107 | 108 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 109 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 110 | 111 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 112 | COMPONENT Runtime) 113 | 114 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 115 | COMPONENT Runtime) 116 | 117 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 118 | COMPONENT Runtime) 119 | 120 | foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) 121 | install(FILES "${bundled_library}" 122 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 123 | COMPONENT Runtime) 124 | endforeach(bundled_library) 125 | 126 | # Fully re-copy the assets directory on each build to avoid having stale files 127 | # from a previous install. 128 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 129 | install(CODE " 130 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 131 | " COMPONENT Runtime) 132 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 133 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 134 | 135 | # Install the AOT library on non-Debug builds only. 136 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 137 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 138 | COMPONENT Runtime) 139 | endif() 140 | -------------------------------------------------------------------------------- /notiflut_daemon/linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /notiflut_daemon/linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | void fl_register_plugins(FlPluginRegistry* registry) { 14 | g_autoptr(FlPluginRegistrar) screen_retriever_registrar = 15 | fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); 16 | screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); 17 | g_autoptr(FlPluginRegistrar) wayland_multi_window_registrar = 18 | fl_plugin_registry_get_registrar_for_plugin(registry, "WaylandMultiWindowPlugin"); 19 | wayland_multi_window_plugin_register_with_registrar(wayland_multi_window_registrar); 20 | g_autoptr(FlPluginRegistrar) window_manager_registrar = 21 | fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); 22 | window_manager_plugin_register_with_registrar(window_manager_registrar); 23 | } 24 | -------------------------------------------------------------------------------- /notiflut_daemon/linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /notiflut_daemon/linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | screen_retriever 7 | wayland_multi_window 8 | window_manager 9 | ) 10 | 11 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 12 | rinf 13 | ) 14 | 15 | set(PLUGIN_BUNDLED_LIBRARIES) 16 | 17 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 18 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 19 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 21 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 22 | endforeach(plugin) 23 | 24 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 25 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 26 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 27 | endforeach(ffi_plugin) 28 | -------------------------------------------------------------------------------- /notiflut_daemon/linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /notiflut_daemon/linux/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | #include 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | gtk_layer_init_for_window(window); 23 | gtk_layer_set_layer (window, GTK_LAYER_SHELL_LAYER_TOP); 24 | gtk_layer_set_namespace(window, "NotiFlut-Land"); 25 | gtk_layer_set_anchor(window, GtkLayerShellEdge::GTK_LAYER_SHELL_EDGE_TOP, true); 26 | gtk_layer_set_anchor(window, GtkLayerShellEdge::GTK_LAYER_SHELL_EDGE_LEFT, true); 27 | gtk_layer_set_anchor(window, GtkLayerShellEdge::GTK_LAYER_SHELL_EDGE_RIGHT, true); 28 | gtk_layer_set_anchor(window, GtkLayerShellEdge::GTK_LAYER_SHELL_EDGE_BOTTOM, true); 29 | 30 | // Use a header bar when running in GNOME as this is the common style used 31 | // by applications and is the setup most users will be using (e.g. Ubuntu 32 | // desktop). 33 | // If running on X and not using GNOME then just use a traditional title bar 34 | // in case the window manager does more exotic layout, e.g. tiling. 35 | // If running on Wayland assume the header bar will work (may need changing 36 | // if future cases occur). 37 | gboolean use_header_bar = TRUE; 38 | #ifdef GDK_WINDOWING_X11 39 | GdkScreen* screen = gtk_window_get_screen(window); 40 | if (GDK_IS_X11_SCREEN(screen)) { 41 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 42 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 43 | use_header_bar = FALSE; 44 | } 45 | } 46 | #endif 47 | if (use_header_bar) { 48 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 49 | gtk_widget_show(GTK_WIDGET(header_bar)); 50 | gtk_header_bar_set_title(header_bar, "notiflutland"); 51 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 52 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 53 | } else { 54 | gtk_window_set_title(window, "notiflutland"); 55 | } 56 | 57 | gtk_window_set_default_size(window, 500, 2); 58 | gtk_widget_realize(GTK_WIDGET(window)); 59 | // gtk_widget_show(GTK_WIDGET(window)); 60 | 61 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 62 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 63 | 64 | FlView* view = fl_view_new(project); 65 | gtk_widget_show(GTK_WIDGET(view)); 66 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 67 | 68 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 69 | 70 | gtk_widget_grab_focus(GTK_WIDGET(view)); 71 | gtk_widget_set_size_request (GTK_WIDGET(view), 500, 2); 72 | } 73 | 74 | // Implements GApplication::local_command_line. 75 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 76 | MyApplication* self = MY_APPLICATION(application); 77 | // Strip out the first argument as it is the binary name. 78 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 79 | 80 | g_autoptr(GError) error = nullptr; 81 | if (!g_application_register(application, nullptr, &error)) { 82 | g_warning("Failed to register: %s", error->message); 83 | *exit_status = 1; 84 | return TRUE; 85 | } 86 | 87 | g_application_activate(application); 88 | *exit_status = 0; 89 | 90 | return TRUE; 91 | } 92 | 93 | // Implements GObject::dispose. 94 | static void my_application_dispose(GObject* object) { 95 | MyApplication* self = MY_APPLICATION(object); 96 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 97 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 98 | } 99 | 100 | static void my_application_class_init(MyApplicationClass* klass) { 101 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 102 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 103 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 104 | } 105 | 106 | static void my_application_init(MyApplication* self) {} 107 | 108 | MyApplication* my_application_new() { 109 | return MY_APPLICATION(g_object_new(my_application_get_type(), 110 | "application-id", APPLICATION_ID, 111 | "flags", G_APPLICATION_NON_UNIQUE, 112 | nullptr)); 113 | } 114 | -------------------------------------------------------------------------------- /notiflut_daemon/linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /notiflut_daemon/messages/app_event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package app_event; 3 | 4 | 5 | enum AppEventType { 6 | Close = 0; 7 | CloseAll = 1; 8 | CloseAllApp = 2; 9 | ActionInvoked = 3; 10 | } 11 | 12 | message AppEvent { 13 | AppEventType type = 1; 14 | optional uint32 notification_id = 2; 15 | optional string data = 3; 16 | } 17 | -------------------------------------------------------------------------------- /notiflut_daemon/messages/daemon_event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package daemon_event; 3 | 4 | import "google/protobuf/timestamp.proto"; 5 | import "google/protobuf/empty.proto"; 6 | 7 | message SignalAppEvent { 8 | enum AppEventType { 9 | NewNotification = 0; 10 | CloseNotification = 1; 11 | ShowNotificationCenter = 2; 12 | HideNotificationCenter = 3; 13 | ToggleNotificationCenter = 4; 14 | PopupNotification = 5; 15 | } 16 | AppEventType type = 1; 17 | oneof data { 18 | Notification notification = 2; 19 | uint64 notificationId = 3; 20 | } 21 | } 22 | 23 | message Notification { 24 | uint32 id = 1; 25 | string app_name = 2; 26 | uint32 replaces_id = 3; 27 | string summary = 4; 28 | string body = 5; 29 | repeated string actions = 6; 30 | int32 timeout = 7; 31 | google.protobuf.Timestamp created_at = 8; 32 | Hints hints = 9; 33 | optional ImageSource app_icon = 10; 34 | optional ImageSource app_image = 11; 35 | } 36 | 37 | 38 | message Hints { 39 | enum Urgency { 40 | Low = 0; 41 | Normal = 1; 42 | Critical = 2; 43 | } 44 | optional bool actions_icon = 1; 45 | optional string category = 2; 46 | optional string desktop_entry = 3; 47 | optional bool resident = 4; 48 | optional string sound_file = 5; 49 | optional string sound_name = 6; 50 | optional bool suppress_sound = 7; 51 | optional bool transient = 8; 52 | optional int32 x = 9; 53 | optional int32 y = 10; 54 | optional Urgency urgency = 11; 55 | } 56 | 57 | 58 | message ImageSource { 59 | enum ImageSourceType { 60 | Data = 0; 61 | Path = 1; 62 | } 63 | ImageSourceType type = 1; 64 | optional ImageData image_data = 2; 65 | optional string path = 3; 66 | } 67 | 68 | message ImageData { 69 | int32 width = 1; 70 | int32 height = 2; 71 | int32 rowstride = 3; 72 | bool one_point_two_bit_alpha = 4; 73 | int32 bits_per_sample = 5; 74 | int32 channels = 6; 75 | repeated uint32 data = 7; 76 | } 77 | -------------------------------------------------------------------------------- /notiflut_daemon/messages/settings_event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package settings_event; 3 | message SettingsSignal { 4 | oneof operation { 5 | ThemeVariante theme = 2; 6 | } 7 | } 8 | 9 | enum ThemeVariante { 10 | Light = 0; 11 | Dark = 1; 12 | } 13 | -------------------------------------------------------------------------------- /notiflut_daemon/messages/theme_event.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package theme_event; 3 | 4 | message NotificationStyle { 5 | int32 backgroundColor = 1; 6 | int32 borderRadius = 2; 7 | int32 bodyTextColor = 3; 8 | int32 titleTextColor = 4; 9 | int32 subtitleTextColor = 5; 10 | int32 buttonTextColor = 6; 11 | int32 borderColor = 7; 12 | int32 borderWidth = 8; 13 | } 14 | 15 | message NotificationCenterStyle { 16 | int32 backgroundColor = 1; 17 | } 18 | 19 | message PopupStyle { 20 | int32 backgroundColor = 1; 21 | } 22 | 23 | message Theme{ 24 | NotificationStyle notification_style = 1; 25 | NotificationCenterStyle notification_center_style = 2; 26 | PopupStyle popup_style = 3; 27 | } 28 | 29 | message Style { 30 | Theme light = 1; 31 | Theme dark = 2; 32 | } 33 | -------------------------------------------------------------------------------- /notiflut_daemon/native/README.md: -------------------------------------------------------------------------------- 1 | # Rust Crates 2 | 3 | This folder contains Rust crates. Entry point of the Rust logic is the `hub` library crate. These crates are integrated and compiled into the Flutter app by the [Rinf](https://github.com/cunarist/rinf) framework. 4 | 5 | - Do NOT change the name of the `hub` crate. Compilation presets expect the entry library crate to be located at `./native/hub`. 6 | - Do NOT modify the `bridge` module inside `./native/hub/src` unless you know what you're doing. 7 | - You CAN name crates other than `hub` as you want. 8 | -------------------------------------------------------------------------------- /notiflut_daemon/native/hub/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | # Do not change the name of this crate. 3 | name = "hub" 4 | version = "1.0.0" 5 | edition = "2021" 6 | 7 | [lib] 8 | # `lib` is required for non-library targets, 9 | # such as tests and benchmarks. 10 | # `cdylib` is for Linux, Android, Windows, and web. 11 | # `staticlib` is for iOS and macOS. 12 | crate-type = ["lib", "cdylib", "staticlib"] 13 | 14 | [dependencies] 15 | rinf = "4.16.3" 16 | tokio_with_wasm = "0.3.2" 17 | wasm-bindgen = "0.2.87" 18 | prost = "0.12.1" 19 | prost-types = "0.12.1" 20 | notification_server = { path = "../notification_server" } 21 | tracing = "0.1.40" 22 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 23 | -------------------------------------------------------------------------------- /notiflut_daemon/native/hub/src/bridge/generated.io.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | // Section: wire functions 3 | 4 | #[no_mangle] 5 | pub extern "C" fn wire_prepare_rust_signal_stream(port_: i64) { 6 | wire_prepare_rust_signal_stream_impl(port_) 7 | } 8 | 9 | #[no_mangle] 10 | pub extern "C" fn wire_prepare_rust_response_stream(port_: i64) { 11 | wire_prepare_rust_response_stream_impl(port_) 12 | } 13 | 14 | #[no_mangle] 15 | pub extern "C" fn wire_prepare_rust_report_stream(port_: i64) { 16 | wire_prepare_rust_report_stream_impl(port_) 17 | } 18 | 19 | #[no_mangle] 20 | pub extern "C" fn wire_prepare_channels(port_: i64) { 21 | wire_prepare_channels_impl(port_) 22 | } 23 | 24 | #[no_mangle] 25 | pub extern "C" fn wire_check_rust_streams(port_: i64) { 26 | wire_check_rust_streams_impl(port_) 27 | } 28 | 29 | #[no_mangle] 30 | pub extern "C" fn wire_start_rust_logic(port_: i64) { 31 | wire_start_rust_logic_impl(port_) 32 | } 33 | 34 | #[no_mangle] 35 | pub extern "C" fn wire_stop_rust_logic(port_: i64) { 36 | wire_stop_rust_logic_impl(port_) 37 | } 38 | 39 | #[no_mangle] 40 | pub extern "C" fn wire_request_to_rust(port_: i64, request_unique: *mut wire_RustRequestUnique) { 41 | wire_request_to_rust_impl(port_, request_unique) 42 | } 43 | 44 | // Section: allocate functions 45 | 46 | #[no_mangle] 47 | pub extern "C" fn new_box_autoadd_rust_request_unique_0() -> *mut wire_RustRequestUnique { 48 | support::new_leak_box_ptr(wire_RustRequestUnique::new_with_null_ptr()) 49 | } 50 | 51 | #[no_mangle] 52 | pub extern "C" fn new_uint_8_list_0(len: i32) -> *mut wire_uint_8_list { 53 | let ans = wire_uint_8_list { 54 | ptr: support::new_leak_vec_ptr(Default::default(), len), 55 | len, 56 | }; 57 | support::new_leak_box_ptr(ans) 58 | } 59 | 60 | // Section: related functions 61 | 62 | // Section: impl Wire2Api 63 | 64 | impl Wire2Api for *mut wire_RustRequestUnique { 65 | fn wire2api(self) -> RustRequestUnique { 66 | let wrap = unsafe { support::box_from_leak_ptr(self) }; 67 | Wire2Api::::wire2api(*wrap).into() 68 | } 69 | } 70 | 71 | impl Wire2Api for wire_RustRequest { 72 | fn wire2api(self) -> RustRequest { 73 | RustRequest { 74 | resource: self.resource.wire2api(), 75 | operation: self.operation.wire2api(), 76 | message: self.message.wire2api(), 77 | blob: self.blob.wire2api(), 78 | } 79 | } 80 | } 81 | impl Wire2Api for wire_RustRequestUnique { 82 | fn wire2api(self) -> RustRequestUnique { 83 | RustRequestUnique { 84 | id: self.id.wire2api(), 85 | request: self.request.wire2api(), 86 | } 87 | } 88 | } 89 | 90 | impl Wire2Api> for *mut wire_uint_8_list { 91 | fn wire2api(self) -> Vec { 92 | unsafe { 93 | let wrap = support::box_from_leak_ptr(self); 94 | support::vec_from_leak_ptr(wrap.ptr, wrap.len) 95 | } 96 | } 97 | } 98 | // Section: wire structs 99 | 100 | #[repr(C)] 101 | #[derive(Clone)] 102 | pub struct wire_RustRequest { 103 | resource: i32, 104 | operation: i32, 105 | message: *mut wire_uint_8_list, 106 | blob: *mut wire_uint_8_list, 107 | } 108 | 109 | #[repr(C)] 110 | #[derive(Clone)] 111 | pub struct wire_RustRequestUnique { 112 | id: i32, 113 | request: wire_RustRequest, 114 | } 115 | 116 | #[repr(C)] 117 | #[derive(Clone)] 118 | pub struct wire_uint_8_list { 119 | ptr: *mut u8, 120 | len: i32, 121 | } 122 | 123 | // Section: impl NewWithNullPtr 124 | 125 | pub trait NewWithNullPtr { 126 | fn new_with_null_ptr() -> Self; 127 | } 128 | 129 | impl NewWithNullPtr for *mut T { 130 | fn new_with_null_ptr() -> Self { 131 | std::ptr::null_mut() 132 | } 133 | } 134 | 135 | impl NewWithNullPtr for wire_RustRequest { 136 | fn new_with_null_ptr() -> Self { 137 | Self { 138 | resource: Default::default(), 139 | operation: Default::default(), 140 | message: core::ptr::null_mut(), 141 | blob: core::ptr::null_mut(), 142 | } 143 | } 144 | } 145 | 146 | impl Default for wire_RustRequest { 147 | fn default() -> Self { 148 | Self::new_with_null_ptr() 149 | } 150 | } 151 | 152 | impl NewWithNullPtr for wire_RustRequestUnique { 153 | fn new_with_null_ptr() -> Self { 154 | Self { 155 | id: Default::default(), 156 | request: Default::default(), 157 | } 158 | } 159 | } 160 | 161 | impl Default for wire_RustRequestUnique { 162 | fn default() -> Self { 163 | Self::new_with_null_ptr() 164 | } 165 | } 166 | 167 | // Section: sync execution mode utility 168 | 169 | #[no_mangle] 170 | pub extern "C" fn free_WireSyncReturn(ptr: support::WireSyncReturn) { 171 | unsafe { 172 | let _ = support::box_from_leak_ptr(ptr); 173 | }; 174 | } 175 | -------------------------------------------------------------------------------- /notiflut_daemon/native/hub/src/bridge/generated.rs: -------------------------------------------------------------------------------- 1 | #![allow( 2 | non_camel_case_types, 3 | unused, 4 | clippy::redundant_closure, 5 | clippy::useless_conversion, 6 | clippy::unit_arg, 7 | clippy::double_parens, 8 | non_snake_case, 9 | clippy::too_many_arguments 10 | )] 11 | // AUTO GENERATED FILE, DO NOT EDIT. 12 | // Generated by flutter_rust_bridge_codegen@ 1.79.0. 13 | 14 | use crate::bridge::interface::*; 15 | use core::panic::UnwindSafe; 16 | use rinf::engine::rust2dart::IntoIntoDart; 17 | use rinf::engine::*; 18 | use std::ffi::c_void; 19 | use std::sync::Arc; 20 | 21 | // Section: imports 22 | 23 | // Section: wire functions 24 | 25 | fn wire_prepare_rust_signal_stream_impl(port_: MessagePort) { 26 | BRIDGE_HANDLER.wrap::<_, _, _, ()>( 27 | WrapInfo { 28 | debug_name: "prepare_rust_signal_stream", 29 | port: Some(port_), 30 | mode: FfiCallMode::Stream, 31 | }, 32 | move || { 33 | move |task_callback| { 34 | Ok(prepare_rust_signal_stream( 35 | task_callback.stream_sink::<_, RustSignal>(), 36 | )) 37 | } 38 | }, 39 | ) 40 | } 41 | fn wire_prepare_rust_response_stream_impl(port_: MessagePort) { 42 | BRIDGE_HANDLER.wrap::<_, _, _, ()>( 43 | WrapInfo { 44 | debug_name: "prepare_rust_response_stream", 45 | port: Some(port_), 46 | mode: FfiCallMode::Stream, 47 | }, 48 | move || { 49 | move |task_callback| { 50 | Ok(prepare_rust_response_stream( 51 | task_callback.stream_sink::<_, RustResponseUnique>(), 52 | )) 53 | } 54 | }, 55 | ) 56 | } 57 | fn wire_prepare_rust_report_stream_impl(port_: MessagePort) { 58 | BRIDGE_HANDLER.wrap::<_, _, _, ()>( 59 | WrapInfo { 60 | debug_name: "prepare_rust_report_stream", 61 | port: Some(port_), 62 | mode: FfiCallMode::Stream, 63 | }, 64 | move || { 65 | move |task_callback| { 66 | Ok(prepare_rust_report_stream( 67 | task_callback.stream_sink::<_, String>(), 68 | )) 69 | } 70 | }, 71 | ) 72 | } 73 | fn wire_prepare_channels_impl(port_: MessagePort) { 74 | BRIDGE_HANDLER.wrap::<_, _, _, ()>( 75 | WrapInfo { 76 | debug_name: "prepare_channels", 77 | port: Some(port_), 78 | mode: FfiCallMode::Normal, 79 | }, 80 | move || move |task_callback| Ok(prepare_channels()), 81 | ) 82 | } 83 | fn wire_check_rust_streams_impl(port_: MessagePort) { 84 | BRIDGE_HANDLER.wrap::<_, _, _, bool>( 85 | WrapInfo { 86 | debug_name: "check_rust_streams", 87 | port: Some(port_), 88 | mode: FfiCallMode::Normal, 89 | }, 90 | move || move |task_callback| Ok(check_rust_streams()), 91 | ) 92 | } 93 | fn wire_start_rust_logic_impl(port_: MessagePort) { 94 | BRIDGE_HANDLER.wrap::<_, _, _, ()>( 95 | WrapInfo { 96 | debug_name: "start_rust_logic", 97 | port: Some(port_), 98 | mode: FfiCallMode::Normal, 99 | }, 100 | move || move |task_callback| Ok(start_rust_logic()), 101 | ) 102 | } 103 | fn wire_stop_rust_logic_impl(port_: MessagePort) { 104 | BRIDGE_HANDLER.wrap::<_, _, _, ()>( 105 | WrapInfo { 106 | debug_name: "stop_rust_logic", 107 | port: Some(port_), 108 | mode: FfiCallMode::Normal, 109 | }, 110 | move || move |task_callback| Ok(stop_rust_logic()), 111 | ) 112 | } 113 | fn wire_request_to_rust_impl( 114 | port_: MessagePort, 115 | request_unique: impl Wire2Api + UnwindSafe, 116 | ) { 117 | BRIDGE_HANDLER.wrap::<_, _, _, ()>( 118 | WrapInfo { 119 | debug_name: "request_to_rust", 120 | port: Some(port_), 121 | mode: FfiCallMode::Normal, 122 | }, 123 | move || { 124 | let api_request_unique = request_unique.wire2api(); 125 | move |task_callback| Ok(request_to_rust(api_request_unique)) 126 | }, 127 | ) 128 | } 129 | // Section: wrapper structs 130 | 131 | // Section: static checks 132 | 133 | // Section: allocate functions 134 | 135 | // Section: related functions 136 | 137 | // Section: impl Wire2Api 138 | 139 | pub trait Wire2Api { 140 | fn wire2api(self) -> T; 141 | } 142 | 143 | impl Wire2Api> for *mut S 144 | where 145 | *mut S: Wire2Api, 146 | { 147 | fn wire2api(self) -> Option { 148 | (!self.is_null()).then(|| self.wire2api()) 149 | } 150 | } 151 | 152 | impl Wire2Api for i32 { 153 | fn wire2api(self) -> i32 { 154 | self 155 | } 156 | } 157 | 158 | impl Wire2Api for i32 { 159 | fn wire2api(self) -> RustOperation { 160 | match self { 161 | 0 => RustOperation::Create, 162 | 1 => RustOperation::Read, 163 | 2 => RustOperation::Update, 164 | 3 => RustOperation::Delete, 165 | _ => unreachable!("Invalid variant for RustOperation: {}", self), 166 | } 167 | } 168 | } 169 | 170 | impl Wire2Api for u8 { 171 | fn wire2api(self) -> u8 { 172 | self 173 | } 174 | } 175 | 176 | // Section: impl IntoDart 177 | 178 | impl support::IntoDart for RustResponse { 179 | fn into_dart(self) -> support::DartAbi { 180 | vec![ 181 | self.successful.into_into_dart().into_dart(), 182 | self.message.into_dart(), 183 | self.blob.into_dart(), 184 | ] 185 | .into_dart() 186 | } 187 | } 188 | impl support::IntoDartExceptPrimitive for RustResponse {} 189 | impl rust2dart::IntoIntoDart for RustResponse { 190 | fn into_into_dart(self) -> Self { 191 | self 192 | } 193 | } 194 | 195 | impl support::IntoDart for RustResponseUnique { 196 | fn into_dart(self) -> support::DartAbi { 197 | vec![ 198 | self.id.into_into_dart().into_dart(), 199 | self.response.into_into_dart().into_dart(), 200 | ] 201 | .into_dart() 202 | } 203 | } 204 | impl support::IntoDartExceptPrimitive for RustResponseUnique {} 205 | impl rust2dart::IntoIntoDart for RustResponseUnique { 206 | fn into_into_dart(self) -> Self { 207 | self 208 | } 209 | } 210 | 211 | impl support::IntoDart for RustSignal { 212 | fn into_dart(self) -> support::DartAbi { 213 | vec![ 214 | self.resource.into_into_dart().into_dart(), 215 | self.message.into_dart(), 216 | self.blob.into_dart(), 217 | ] 218 | .into_dart() 219 | } 220 | } 221 | impl support::IntoDartExceptPrimitive for RustSignal {} 222 | impl rust2dart::IntoIntoDart for RustSignal { 223 | fn into_into_dart(self) -> Self { 224 | self 225 | } 226 | } 227 | 228 | // Section: executor 229 | 230 | support::lazy_static! { 231 | pub static ref BRIDGE_HANDLER: support::DefaultHandler = Default::default(); 232 | } 233 | 234 | /// cbindgen:ignore 235 | #[cfg(target_family = "wasm")] 236 | #[path = "generated.web.rs"] 237 | mod web; 238 | #[cfg(target_family = "wasm")] 239 | pub use web::*; 240 | 241 | #[cfg(not(target_family = "wasm"))] 242 | #[path = "generated.io.rs"] 243 | mod io; 244 | #[cfg(not(target_family = "wasm"))] 245 | pub use io::*; 246 | -------------------------------------------------------------------------------- /notiflut_daemon/native/hub/src/bridge/generated.web.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | // Section: wire functions 3 | 4 | #[wasm_bindgen] 5 | pub fn wire_prepare_rust_signal_stream(port_: MessagePort) { 6 | wire_prepare_rust_signal_stream_impl(port_) 7 | } 8 | 9 | #[wasm_bindgen] 10 | pub fn wire_prepare_rust_response_stream(port_: MessagePort) { 11 | wire_prepare_rust_response_stream_impl(port_) 12 | } 13 | 14 | #[wasm_bindgen] 15 | pub fn wire_prepare_rust_report_stream(port_: MessagePort) { 16 | wire_prepare_rust_report_stream_impl(port_) 17 | } 18 | 19 | #[wasm_bindgen] 20 | pub fn wire_prepare_channels(port_: MessagePort) { 21 | wire_prepare_channels_impl(port_) 22 | } 23 | 24 | #[wasm_bindgen] 25 | pub fn wire_check_rust_streams(port_: MessagePort) { 26 | wire_check_rust_streams_impl(port_) 27 | } 28 | 29 | #[wasm_bindgen] 30 | pub fn wire_start_rust_logic(port_: MessagePort) { 31 | wire_start_rust_logic_impl(port_) 32 | } 33 | 34 | #[wasm_bindgen] 35 | pub fn wire_stop_rust_logic(port_: MessagePort) { 36 | wire_stop_rust_logic_impl(port_) 37 | } 38 | 39 | #[wasm_bindgen] 40 | pub fn wire_request_to_rust(port_: MessagePort, request_unique: JsValue) { 41 | wire_request_to_rust_impl(port_, request_unique) 42 | } 43 | 44 | // Section: allocate functions 45 | 46 | // Section: related functions 47 | 48 | // Section: impl Wire2Api 49 | 50 | impl Wire2Api>> for Option> { 51 | fn wire2api(self) -> Option> { 52 | self.map(Wire2Api::wire2api) 53 | } 54 | } 55 | 56 | impl Wire2Api for JsValue { 57 | fn wire2api(self) -> RustRequest { 58 | let self_ = self.dyn_into::().unwrap(); 59 | assert_eq!( 60 | self_.length(), 61 | 4, 62 | "Expected 4 elements, got {}", 63 | self_.length() 64 | ); 65 | RustRequest { 66 | resource: self_.get(0).wire2api(), 67 | operation: self_.get(1).wire2api(), 68 | message: self_.get(2).wire2api(), 69 | blob: self_.get(3).wire2api(), 70 | } 71 | } 72 | } 73 | impl Wire2Api for JsValue { 74 | fn wire2api(self) -> RustRequestUnique { 75 | let self_ = self.dyn_into::().unwrap(); 76 | assert_eq!( 77 | self_.length(), 78 | 2, 79 | "Expected 2 elements, got {}", 80 | self_.length() 81 | ); 82 | RustRequestUnique { 83 | id: self_.get(0).wire2api(), 84 | request: self_.get(1).wire2api(), 85 | } 86 | } 87 | } 88 | 89 | impl Wire2Api> for Box<[u8]> { 90 | fn wire2api(self) -> Vec { 91 | self.into_vec() 92 | } 93 | } 94 | // Section: impl Wire2Api for JsValue 95 | 96 | impl Wire2Api for JsValue { 97 | fn wire2api(self) -> i32 { 98 | self.unchecked_into_f64() as _ 99 | } 100 | } 101 | impl Wire2Api>> for JsValue { 102 | fn wire2api(self) -> Option> { 103 | (!self.is_undefined() && !self.is_null()).then(|| self.wire2api()) 104 | } 105 | } 106 | impl Wire2Api for JsValue { 107 | fn wire2api(self) -> RustOperation { 108 | (self.unchecked_into_f64() as i32).wire2api() 109 | } 110 | } 111 | impl Wire2Api for JsValue { 112 | fn wire2api(self) -> u8 { 113 | self.unchecked_into_f64() as _ 114 | } 115 | } 116 | impl Wire2Api> for JsValue { 117 | fn wire2api(self) -> Vec { 118 | self.unchecked_into::() 119 | .to_vec() 120 | .into() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /notiflut_daemon/native/hub/src/bridge/interface.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use rinf::engine::StreamSink; 4 | use rinf::externs::lazy_static::lazy_static; 5 | use std::cell::RefCell; 6 | use std::sync::Arc; 7 | use std::sync::Mutex; 8 | use tokio::sync::mpsc::channel; 9 | use tokio::sync::mpsc::Receiver; 10 | use tokio::sync::mpsc::Sender; 11 | use tokio_with_wasm::tokio; 12 | 13 | /// Available operations that a `RustRequest` object can hold. 14 | /// There are 4 options, `Create`,`Read`,`Update`, and `Delete`. 15 | pub enum RustOperation { 16 | Create, 17 | Read, 18 | Update, 19 | Delete, 20 | } 21 | 22 | /// Holds the data that Rust streams to Dart. 23 | #[derive(Clone)] 24 | pub struct RustSignal { 25 | pub resource: i32, 26 | pub message: Option>, 27 | pub blob: Option>, 28 | } 29 | 30 | /// Request object that is sent from Dart to Rust. 31 | pub struct RustRequest { 32 | pub resource: i32, 33 | pub operation: RustOperation, 34 | pub message: Option>, 35 | pub blob: Option>, 36 | } 37 | 38 | /// Wrapper for `RustRequest` with a unique ID. 39 | pub struct RustRequestUnique { 40 | pub id: i32, 41 | pub request: RustRequest, 42 | } 43 | 44 | /// Response object that is sent from Rust to Dart. 45 | #[derive(Clone)] 46 | pub struct RustResponse { 47 | pub successful: bool, 48 | pub message: Option>, 49 | pub blob: Option>, 50 | } 51 | 52 | impl Default for RustResponse { 53 | /// Empty response with the successful value of false. 54 | fn default() -> RustResponse { 55 | RustResponse { 56 | successful: false, 57 | message: None, 58 | blob: None, 59 | } 60 | } 61 | } 62 | 63 | /// Wrapper for `RustResponse` with a unique ID. 64 | #[derive(Clone)] 65 | pub struct RustResponseUnique { 66 | pub id: i32, 67 | pub response: RustResponse, 68 | } 69 | 70 | type Cell = RefCell>; 71 | type SharedCell = Arc>>; 72 | 73 | type RustSignalStream = StreamSink; 74 | type RustResponseStream = StreamSink; 75 | type RustReportStream = StreamSink; 76 | type RustRequestSender = Sender; 77 | type RustRequestReceiver = Receiver; 78 | 79 | // Native: Main thread 80 | // Web: Worker thread 81 | thread_local! { 82 | pub static REQUEST_SENDER: Cell = RefCell::new(None); 83 | } 84 | 85 | // Native: `tokio` runtime threads 86 | // Web: Worker thread 87 | thread_local! { 88 | pub static SIGNAL_STREAM: Cell = RefCell::new(None); 89 | pub static RESPONSE_STREAM: Cell = RefCell::new(None); 90 | pub static REPORT_STREAM: Cell = RefCell::new(None); 91 | } 92 | 93 | // Native: All threads 94 | // Web: Worker thread 95 | lazy_static! { 96 | pub static ref SIGNAL_STREAM_SHARED: SharedCell = 97 | Arc::new(Mutex::new(RefCell::new(None))); 98 | pub static ref RESPONSE_STREAM_SHARED: SharedCell = 99 | Arc::new(Mutex::new(RefCell::new(None))); 100 | pub static ref REPORT_STREAM_SHARED: SharedCell = 101 | Arc::new(Mutex::new(RefCell::new(None))); 102 | pub static ref REQUST_RECEIVER_SHARED: SharedCell = 103 | Arc::new(Mutex::new(RefCell::new(None))); 104 | } 105 | 106 | #[cfg(not(target_family = "wasm"))] 107 | lazy_static! { 108 | pub static ref TOKIO_RUNTIME: rinf::externs::os_thread_local::ThreadLocal> = 109 | rinf::externs::os_thread_local::ThreadLocal::new(|| RefCell::new(None)); 110 | } 111 | 112 | #[cfg(target_family = "wasm")] 113 | thread_local! { 114 | pub static IS_MAIN_STARTED: RefCell = RefCell::new(false); 115 | } 116 | 117 | /// Returns a stream object in Dart that listens to Rust. 118 | pub fn prepare_rust_signal_stream(signal_stream: StreamSink) { 119 | let cell = SIGNAL_STREAM_SHARED.lock().unwrap(); 120 | cell.replace(Some(signal_stream)); 121 | } 122 | 123 | /// Returns a stream object in Dart that gives responses from Rust. 124 | pub fn prepare_rust_response_stream(response_stream: StreamSink) { 125 | let cell = RESPONSE_STREAM_SHARED.lock().unwrap(); 126 | cell.replace(Some(response_stream)); 127 | } 128 | 129 | /// Returns a stream object in Dart that gives strings to print from Rust. 130 | pub fn prepare_rust_report_stream(print_stream: StreamSink) { 131 | let cell = REPORT_STREAM_SHARED.lock().unwrap(); 132 | cell.replace(Some(print_stream)); 133 | } 134 | 135 | /// Prepare channels that are used in the Rust world. 136 | pub fn prepare_channels() { 137 | let (request_sender, request_receiver) = channel(1024); 138 | REQUEST_SENDER.with(move |inner| { 139 | inner.replace(Some(request_sender)); 140 | }); 141 | let cell = REQUST_RECEIVER_SHARED.lock().unwrap(); 142 | cell.replace(Some(request_receiver)); 143 | } 144 | 145 | /// Check if the streams are ready in Rust. 146 | /// This should be done before starting the Rust logic. 147 | pub fn check_rust_streams() -> bool { 148 | let mut are_all_ready = true; 149 | let cell = SIGNAL_STREAM_SHARED.lock().unwrap(); 150 | if cell.borrow().is_none() { 151 | are_all_ready = false; 152 | }; 153 | let cell = RESPONSE_STREAM_SHARED.lock().unwrap(); 154 | if cell.borrow().is_none() { 155 | are_all_ready = false; 156 | }; 157 | #[cfg(debug_assertions)] 158 | { 159 | let cell = REPORT_STREAM_SHARED.lock().unwrap(); 160 | if cell.borrow().is_none() { 161 | are_all_ready = false; 162 | }; 163 | } 164 | are_all_ready 165 | } 166 | 167 | /// Start the main function of Rust. 168 | pub fn start_rust_logic() { 169 | #[cfg(not(target_family = "wasm"))] 170 | { 171 | use rinf::externs::backtrace; 172 | #[cfg(debug_assertions)] 173 | std::panic::set_hook(Box::new(|panic_info| { 174 | let mut frames_filtered = Vec::new(); 175 | backtrace::trace(|frame| { 176 | // Filter some backtrace frames 177 | // as those from infrastructure functions are not needed. 178 | let mut should_keep_tracing = true; 179 | backtrace::resolve_frame(frame, |symbol| { 180 | if let Some(symbol_name) = symbol.name() { 181 | let name = symbol_name.to_string(); 182 | let name_trimmed = name.trim_start_matches('_'); 183 | if name_trimmed.starts_with("rust_begin_unwind") { 184 | frames_filtered.clear(); 185 | return; 186 | } 187 | if name_trimmed.starts_with("rust_try") { 188 | should_keep_tracing = false; 189 | return; 190 | } 191 | } 192 | let backtrace_frame = backtrace::BacktraceFrame::from(frame.to_owned()); 193 | frames_filtered.push(backtrace_frame); 194 | }); 195 | should_keep_tracing 196 | }); 197 | let mut backtrace_filtered = backtrace::Backtrace::from(frames_filtered); 198 | backtrace_filtered.resolve(); 199 | crate::debug_print!( 200 | "A panic occurred in Rust.\n{}\n{:?}", 201 | panic_info, 202 | backtrace_filtered 203 | ); 204 | })); 205 | TOKIO_RUNTIME.with(move |inner| { 206 | let tokio_runtime = tokio::runtime::Builder::new_multi_thread() 207 | .enable_all() 208 | .build() 209 | .unwrap(); 210 | tokio_runtime.spawn(crate::main()); 211 | inner.replace(Some(tokio_runtime)); 212 | }); 213 | } 214 | #[cfg(target_family = "wasm")] 215 | { 216 | #[cfg(debug_assertions)] 217 | std::panic::set_hook(Box::new(|panic_info| { 218 | crate::debug_print!("A panic occurred in Rust.\n{panic_info}"); 219 | })); 220 | IS_MAIN_STARTED.with(move |ref_cell| { 221 | let is_started = *ref_cell.borrow(); 222 | if !is_started { 223 | tokio::spawn(crate::main()); 224 | ref_cell.replace(true); 225 | } 226 | }); 227 | } 228 | } 229 | 230 | /// Stop and terminate all Rust tasks. 231 | pub fn stop_rust_logic() { 232 | #[cfg(not(target_family = "wasm"))] 233 | TOKIO_RUNTIME.with(move |ref_cell| { 234 | ref_cell.replace(None); 235 | }); 236 | } 237 | 238 | /// Send a request to Rust and receive a response in Dart. 239 | pub fn request_to_rust(request_unique: RustRequestUnique) { 240 | REQUEST_SENDER.with(move |inner| { 241 | let borrowed = inner.borrow(); 242 | let sender = borrowed.as_ref().unwrap(); 243 | sender.try_send(request_unique).ok(); 244 | }); 245 | } 246 | -------------------------------------------------------------------------------- /notiflut_daemon/native/hub/src/bridge/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module supports communication with Dart. 2 | //! More specifically, sending responses and 3 | //! stream signals to Dart are supported. 4 | //! DO NOT EDIT. 5 | 6 | #![allow(dead_code)] 7 | 8 | pub use interface::*; 9 | use tokio::sync::mpsc::Receiver; 10 | use tokio_with_wasm::tokio; 11 | 12 | mod generated; 13 | mod interface; 14 | 15 | /// This function is expected to be used only once 16 | /// during the initialization of the Rust logic. 17 | pub fn get_request_receiver() -> Receiver { 18 | let cell = REQUST_RECEIVER_SHARED.lock().unwrap(); 19 | let option = cell.replace(None); 20 | option.unwrap() 21 | } 22 | 23 | /// Sending the signal will notify the Flutter widgets 24 | /// and trigger the rebuild. 25 | /// No memory copy is involved as the bytes are moved directly to Dart. 26 | pub fn send_rust_signal(rust_signal: RustSignal) { 27 | SIGNAL_STREAM.with(|inner| { 28 | let mut borrowed = inner.borrow_mut(); 29 | let option = borrowed.as_ref(); 30 | if let Some(stream) = option { 31 | stream.add(rust_signal); 32 | } else { 33 | let cell = SIGNAL_STREAM_SHARED.lock().unwrap(); 34 | let stream = cell.borrow().as_ref().unwrap().clone(); 35 | stream.add(rust_signal); 36 | borrowed.replace(stream); 37 | } 38 | }); 39 | } 40 | 41 | /// Sends a response to Dart with a unique interaction ID 42 | /// to remember which request that response corresponds to. 43 | /// No memory copy is involved as the bytes are moved directly to Dart. 44 | pub fn respond_to_dart(response_unique: RustResponseUnique) { 45 | RESPONSE_STREAM.with(|inner| { 46 | let mut borrowed = inner.borrow_mut(); 47 | let option = borrowed.as_ref(); 48 | if let Some(stream) = option { 49 | stream.add(response_unique); 50 | } else { 51 | let cell = RESPONSE_STREAM_SHARED.lock().unwrap(); 52 | let stream = cell.borrow().as_ref().unwrap().clone(); 53 | stream.add(response_unique); 54 | borrowed.replace(stream); 55 | } 56 | }); 57 | } 58 | 59 | /// Delegates the printing operation to Flutter, 60 | /// which excels at handling various platforms 61 | /// including web and mobile emulators. 62 | /// When debugging, using this macro is recommended over `println!()`, 63 | /// as it seamlessly adapts to different environments. 64 | /// Note that this macro does nothing in release mode. 65 | #[macro_export] 66 | macro_rules! debug_print { 67 | ( $( $t:tt )* ) => { 68 | let rust_report = format!( $( $t )* ); 69 | #[cfg(debug_assertions)] 70 | $crate::bridge::send_rust_report(rust_report.into()); 71 | #[cfg(not(debug_assertions))] 72 | let _ = rust_report; 73 | } 74 | } 75 | 76 | /// Sends a string to Dart that should be printed in the CLI. 77 | /// Do NOT use this function directly in the code. 78 | /// Use `debug_print!` macro instead. 79 | #[cfg(debug_assertions)] 80 | pub fn send_rust_report(rust_report: String) { 81 | REPORT_STREAM.with(|inner| { 82 | let mut borrowed = inner.borrow_mut(); 83 | let option = borrowed.as_ref(); 84 | if let Some(stream) = option { 85 | stream.add(rust_report); 86 | } else { 87 | let cell = REPORT_STREAM_SHARED.lock().unwrap(); 88 | let stream = cell.borrow().as_ref().unwrap().clone(); 89 | stream.add(rust_report); 90 | borrowed.replace(stream); 91 | } 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /notiflut_daemon/native/hub/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use bridge::RustSignal; 4 | use bridge::{respond_to_dart, send_rust_signal}; 5 | use messages::daemon_event::ID as DAEMON_EVENT_ID; 6 | use messages::daemon_event::{signal_app_event, SignalAppEvent}; 7 | use messages::settings_event::settings_signal::Operation; 8 | use messages::settings_event::SettingsSignal; 9 | use messages::settings_event::ThemeVariante; 10 | use messages::settings_event::ID as SETTINGS_EVENT_ID; 11 | use messages::theme_event::Style; 12 | use messages::theme_event::ID as THEME_EVENT_ID; 13 | use notification_server::{NotificationCenterCommand, ThemeSettings}; 14 | use prost::Message; 15 | use tokio_with_wasm::tokio; 16 | use tracing::Level; 17 | use tracing_subscriber::EnvFilter; 18 | use tracing_subscriber::FmtSubscriber; 19 | use with_request::handle_request; 20 | 21 | mod bridge; 22 | mod messages; 23 | mod utils; 24 | mod with_request; 25 | 26 | /// This `hub` crate is the entry point for the Rust logic. 27 | /// Always use non-blocking async functions such as `tokio::fs::File::open`. 28 | async fn main() { 29 | let subscriber = FmtSubscriber::builder() 30 | .with_max_level(Level::TRACE) 31 | .with_level(true) 32 | .with_file(true) 33 | .with_line_number(true) 34 | .with_env_filter(EnvFilter::new("hub=trace,notification_server=trace")) 35 | .pretty() 36 | .finish(); 37 | 38 | tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); 39 | 40 | let mut request_receiver = bridge::get_request_receiver(); 41 | let mut server = notification_server::NotificationServer::new().await; 42 | let mut recv = server.run().await.unwrap(); 43 | let server = Arc::new(server); 44 | 45 | tokio::spawn(async move { 46 | while let Some(event) = recv.recv().await { 47 | match event { 48 | notification_server::NotificationServerEvent::ToggleNotificationCenter => { 49 | on_notification_center_state_change(NotificationCenterCommand::Toggle) 50 | } 51 | notification_server::NotificationServerEvent::CloseNotificationCenter => { 52 | on_notification_center_state_change(NotificationCenterCommand::Close) 53 | } 54 | notification_server::NotificationServerEvent::OpenNotificationCenter => { 55 | on_notification_center_state_change(NotificationCenterCommand::Open) 56 | } 57 | notification_server::NotificationServerEvent::CloseNotification(id) => { 58 | on_notification_close(id) 59 | } 60 | notification_server::NotificationServerEvent::NewNotification(n) => { 61 | on_notification(&n) 62 | } 63 | notification_server::NotificationServerEvent::StyleUpdate(style) => { 64 | on_style_change(&style) 65 | } 66 | notification_server::NotificationServerEvent::ThemeSelected(theme) => { 67 | on_theme_selected(theme) 68 | } 69 | } 70 | } 71 | }); 72 | 73 | // This is `tokio::sync::mpsc::Reciver` that receives the requests from Dart. 74 | while let Some(request_unique) = request_receiver.recv().await { 75 | let clone = server.clone(); 76 | tokio::spawn(async move { 77 | let response_unique = handle_request(request_unique, &clone); 78 | respond_to_dart(response_unique); 79 | }); 80 | } 81 | } 82 | fn on_notification(n: ¬ification_server::Notification) { 83 | let signal_message = SignalAppEvent { 84 | r#type: signal_app_event::AppEventType::NewNotification.into(), 85 | data: Some(signal_app_event::Data::Notification(n.into())), 86 | }; 87 | 88 | let rust_signal = RustSignal { 89 | resource: DAEMON_EVENT_ID, 90 | message: Some(signal_message.encode_to_vec()), 91 | blob: None, 92 | }; 93 | send_rust_signal(rust_signal); 94 | } 95 | 96 | fn on_style_change(s: ¬ification_server::Style) { 97 | let signal_message: Style = s.into(); 98 | let rust_signal = RustSignal { 99 | resource: THEME_EVENT_ID, 100 | message: Some(signal_message.encode_to_vec()), 101 | blob: None, 102 | }; 103 | send_rust_signal(rust_signal); 104 | } 105 | 106 | fn on_notification_close(n_id: u32) { 107 | let signal_message = SignalAppEvent { 108 | r#type: signal_app_event::AppEventType::CloseNotification.into(), 109 | data: Some(signal_app_event::Data::NotificationId(n_id.into())), 110 | }; 111 | 112 | let rust_signal = RustSignal { 113 | resource: DAEMON_EVENT_ID, 114 | message: Some(signal_message.encode_to_vec()), 115 | blob: None, 116 | }; 117 | send_rust_signal(rust_signal); 118 | } 119 | 120 | fn on_notification_center_state_change(state: NotificationCenterCommand) { 121 | let event_type = match state { 122 | NotificationCenterCommand::Open => signal_app_event::AppEventType::ShowNotificationCenter, 123 | NotificationCenterCommand::Close => signal_app_event::AppEventType::HideNotificationCenter, 124 | NotificationCenterCommand::Toggle => { 125 | signal_app_event::AppEventType::ToggleNotificationCenter 126 | } 127 | }; 128 | let signal_message = SignalAppEvent { 129 | r#type: event_type.into(), 130 | ..Default::default() 131 | }; 132 | let rust_signal = RustSignal { 133 | resource: DAEMON_EVENT_ID, 134 | message: Some(signal_message.encode_to_vec()), 135 | blob: None, 136 | }; 137 | send_rust_signal(rust_signal); 138 | } 139 | 140 | fn on_theme_selected(theme_settings: ThemeSettings) { 141 | let variante: ThemeVariante = theme_settings.into(); 142 | let signal_message = SettingsSignal { 143 | operation: Some(Operation::Theme(variante.into())), 144 | }; 145 | let rust_signal = RustSignal { 146 | resource: SETTINGS_EVENT_ID, 147 | message: Some(signal_message.encode_to_vec()), 148 | blob: None, 149 | }; 150 | send_rust_signal(rust_signal); 151 | } 152 | -------------------------------------------------------------------------------- /notiflut_daemon/native/hub/src/utils.rs: -------------------------------------------------------------------------------- 1 | use notification_server::{ 2 | Hints, ImageData, ImageSource, Notification, NotificationCenterStyle, NotificationStyle, 3 | PopupStyle, Style, Theme, ThemeSettings, Urgency, 4 | }; 5 | use prost_types::Timestamp; 6 | use std::convert::Into; 7 | 8 | use crate::messages; 9 | 10 | impl From for messages::daemon_event::Notification { 11 | fn from(val: Notification) -> Self { 12 | let created_at = Timestamp { 13 | seconds: val.created_at.timestamp(), 14 | nanos: val.created_at.timestamp_subsec_nanos() as i32, 15 | }; 16 | messages::daemon_event::Notification { 17 | id: val.n_id, 18 | app_name: val.app_name, 19 | replaces_id: val.replaces_id, 20 | summary: val.summary, 21 | body: val.body, 22 | actions: val.actions, 23 | timeout: val.timeout, 24 | created_at: Some(created_at), 25 | hints: Some(val.hints.into()), 26 | app_icon: val.app_icon.map(|a| a.into()), 27 | app_image: val.app_image.map(|a| a.into()), 28 | } 29 | } 30 | } 31 | 32 | impl From<&Notification> for messages::daemon_event::Notification { 33 | fn from(val: &Notification) -> Self { 34 | let created_at = Timestamp { 35 | seconds: val.created_at.timestamp(), 36 | nanos: val.created_at.timestamp_subsec_nanos() as i32, 37 | }; 38 | messages::daemon_event::Notification { 39 | id: val.n_id, 40 | app_name: val.app_name.clone(), 41 | replaces_id: val.replaces_id, 42 | summary: val.summary.clone(), 43 | body: val.body.clone(), 44 | actions: val.actions.clone(), 45 | timeout: val.timeout, 46 | created_at: Some(created_at), 47 | hints: Some(val.hints.clone().into()), 48 | app_icon: val.app_icon.as_ref().map(|a| a.into()), 49 | app_image: val.app_image.as_ref().map(|a| a.into()), 50 | } 51 | } 52 | } 53 | 54 | impl From for messages::daemon_event::Hints { 55 | fn from(val: Hints) -> Self { 56 | let urgency: Option = val.urgency.map(|u| u.into()); 57 | messages::daemon_event::Hints { 58 | actions_icon: val.actions_icon, 59 | category: val.category, 60 | desktop_entry: val.desktop_entry, 61 | resident: val.resident, 62 | sound_file: val.sound_file, 63 | sound_name: val.sound_name, 64 | suppress_sound: val.suppress_sound, 65 | transient: val.transient, 66 | x: val.x, 67 | y: val.y, 68 | urgency: urgency.map(|u| u.into()), 69 | } 70 | } 71 | } 72 | 73 | impl From<&Hints> for messages::daemon_event::Hints { 74 | fn from(val: &Hints) -> Self { 75 | let urgency: Option = 76 | val.urgency.clone().map(|u| u.into()); 77 | messages::daemon_event::Hints { 78 | actions_icon: val.actions_icon, 79 | category: val.category.clone(), 80 | desktop_entry: val.desktop_entry.clone(), 81 | resident: val.resident, 82 | sound_file: val.sound_file.clone(), 83 | sound_name: val.sound_name.clone(), 84 | suppress_sound: val.suppress_sound, 85 | transient: val.transient, 86 | x: val.x, 87 | y: val.y, 88 | urgency: urgency.map(|u| u.into()), 89 | } 90 | } 91 | } 92 | 93 | impl From for messages::daemon_event::hints::Urgency { 94 | fn from(val: Urgency) -> Self { 95 | match val { 96 | Urgency::Low => messages::daemon_event::hints::Urgency::Low, 97 | Urgency::Normal => messages::daemon_event::hints::Urgency::Normal, 98 | Urgency::Critical => messages::daemon_event::hints::Urgency::Critical, 99 | } 100 | } 101 | } 102 | 103 | impl From for messages::daemon_event::ImageSource { 104 | fn from(val: ImageSource) -> Self { 105 | match val { 106 | ImageSource::Data(d) => messages::daemon_event::ImageSource { 107 | r#type: messages::daemon_event::image_source::ImageSourceType::Data.into(), 108 | image_data: Some(d.into()), 109 | path: None, 110 | }, 111 | ImageSource::Path(p) => messages::daemon_event::ImageSource { 112 | r#type: messages::daemon_event::image_source::ImageSourceType::Path.into(), 113 | image_data: None, 114 | path: Some(p), 115 | }, 116 | } 117 | } 118 | } 119 | 120 | impl From<&ImageSource> for messages::daemon_event::ImageSource { 121 | fn from(val: &ImageSource) -> Self { 122 | match val { 123 | ImageSource::Data(ref d) => Self { 124 | r#type: messages::daemon_event::image_source::ImageSourceType::Data.into(), 125 | image_data: Some(d.into()), 126 | path: None, 127 | }, 128 | ImageSource::Path(p) => Self { 129 | r#type: messages::daemon_event::image_source::ImageSourceType::Path.into(), 130 | image_data: None, 131 | path: Some(p.clone()), 132 | }, 133 | } 134 | } 135 | } 136 | 137 | impl From for messages::daemon_event::ImageData { 138 | fn from(val: ImageData) -> Self { 139 | Self { 140 | width: val.width, 141 | height: val.height, 142 | rowstride: val.rowstride, 143 | one_point_two_bit_alpha: val.one_point_two_bit_alpha, 144 | bits_per_sample: val.bits_per_sample, 145 | channels: val.channels, 146 | data: val.data.into_iter().map(|d| d.into()).collect(), 147 | } 148 | } 149 | } 150 | 151 | impl From<&ImageData> for messages::daemon_event::ImageData { 152 | fn from(val: &ImageData) -> Self { 153 | Self { 154 | width: val.width, 155 | height: val.height, 156 | rowstride: val.rowstride, 157 | one_point_two_bit_alpha: val.one_point_two_bit_alpha, 158 | bits_per_sample: val.bits_per_sample, 159 | channels: val.channels, 160 | data: val.data.iter().map(|d| *d as u32).collect(), 161 | } 162 | } 163 | } 164 | 165 | impl From