├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── COPYING ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── images │ ├── center.png │ ├── left.png │ └── overskride.png ├── build-flatpak.sh ├── data ├── icons │ ├── hicolor │ │ ├── scalable │ │ │ └── apps │ │ │ │ └── io.github.kaii_lb.Overskride.svg │ │ └── symbolic │ │ │ └── apps │ │ │ ├── io.github.kaii_lb.Overskride-symbolic.svg │ │ │ └── io.github.kaii_lb.Overskride.svg │ └── meson.build ├── io.github.kaii_lb.Overskride.appdata.xml.in ├── io.github.kaii_lb.Overskride.desktop.in ├── io.github.kaii_lb.Overskride.gschema.xml └── meson.build ├── io.github.kaii_lb.Overskride.json ├── make-pkg.sh ├── meson.build ├── po ├── LINGUAS ├── POTFILES └── meson.build ├── src ├── application.rs ├── bluetooth │ ├── agent.rs │ ├── audio_profiles.rs │ ├── battery.rs │ ├── bluetooth_settings.rs │ ├── device.rs │ ├── message.rs │ └── services.rs ├── config.rs ├── config.rs.in ├── gtk │ ├── battery-indicator.blp │ ├── connected-switch-row.blp │ ├── device-action-row.blp │ ├── help-overlay.blp │ ├── more-info-page.blp │ ├── preferences-page.blp │ ├── receiving-popover.blp │ ├── receiving-row.blp │ ├── selectable-row.blp │ ├── startup-error-message.blp │ ├── style.css │ └── window.blp ├── icons │ └── symbolic │ │ └── actions │ │ ├── bell-outline-symbolic.svg │ │ ├── bluetooth-symbolic.svg │ │ ├── chain-link-symbolic.svg │ │ ├── check-plain-symbolic.svg │ │ ├── cross-large-symbolic.svg │ │ ├── dock-left-symbolic.svg │ │ ├── folder-symbolic.svg │ │ ├── headphones-symbolic.svg │ │ ├── heart-broken-symbolic.svg │ │ ├── image-missing-symbolic.svg │ │ ├── menu-symbolic.svg │ │ ├── no-bluetooth-symbolic.svg │ │ ├── refresh-large-symbolic.svg │ │ ├── right-symbolic.svg │ │ ├── rssi-dead-symbolic.svg │ │ ├── rssi-high-symbolic.svg │ │ ├── rssi-low-symbolic.svg │ │ ├── rssi-medium-symbolic.svg │ │ ├── rssi-none-symbolic.svg │ │ ├── rssi-not-found-symbolic.svg │ │ ├── skull-symbolic.svg │ │ └── smartphone-symbolic.svg ├── main.rs ├── meson.build ├── obex │ ├── obex.rs │ └── obex_utils.rs ├── overskride.gresource.xml ├── widgets │ ├── battery_indicator.rs │ ├── connected_switch_row.rs │ ├── device_action_row.rs │ ├── more_info_page.rs │ ├── receiving_popover.rs │ ├── receiving_row.rs │ ├── selectable_row.rs │ └── startup_error_message.rs └── window.rs └── subprojects └── blueprint-compiler.wrap /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - v* 5 | pull_request: 6 | workflow_dispatch: 7 | name: CI 8 | jobs: 9 | flatpak: 10 | name: "Flatpak" 11 | runs-on: ubuntu-latest 12 | container: 13 | image: bilelmoussaoui/flatpak-github-actions:gnome-45 14 | options: --privileged 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: flatpak/flatpak-github-actions/flatpak-builder@v6.3 18 | with: 19 | bundle: overskride-nightly.flatpak 20 | manifest-path: io.github.kaii_lb.Overskride.json 21 | cache-key: flatpak-builder-${{ github.sha }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | /subprojects/blueprint-compiler 3 | target/ 4 | .git/ 5 | .gitignore 6 | .vscode/ 7 | .flatpak-builder/ 8 | overskride.flatpak 9 | overskride.zip 10 | overskride-flatpak 11 | flatpak-build 12 | buildflatpak/ 13 | flatpakexport/ 14 | data/appdata 15 | previousrelease.txt 16 | overskride.tar.xz 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Overskride Version 0.5.5 2 | - changed the way window titles work 3 | - added a distance approximation in more info page 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "overskride" 3 | version = "0.6.1" 4 | edition = "2021" 5 | authors = ["kaii_lb 2 | 3 | 4 | 101 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/apps/io.github.kaii_lb.Overskride-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/icons/hicolor/symbolic/apps/io.github.kaii_lb.Overskride.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /data/icons/meson.build: -------------------------------------------------------------------------------- 1 | application_id = 'io.github.kaii_lb.Overskride' 2 | 3 | scalable_dir = join_paths('hicolor', 'scalable', 'apps') 4 | install_data( 5 | join_paths(scalable_dir, ('@0@.svg').format(application_id)), 6 | install_dir: join_paths(get_option('datadir'), 'icons', scalable_dir) 7 | ) 8 | 9 | symbolic_dir = join_paths('hicolor', 'symbolic', 'apps') 10 | install_data( 11 | join_paths(symbolic_dir, ('@0@-symbolic.svg').format(application_id)), 12 | install_dir: join_paths(get_option('datadir'), 'icons', symbolic_dir) 13 | ) 14 | -------------------------------------------------------------------------------- /data/io.github.kaii_lb.Overskride.appdata.xml.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | io.github.kaii_lb.Overskride 4 | 5 | Overskride 6 | A simple but powerful bluetooth app 7 | 8 | CC-BY-4.0 9 | GPL-3.0-or-later 10 | 11 | 12 | 13 | 14 | 310 15 | 16 | 17 | pointing 18 | keyboard 19 | touch 20 | tablet 21 | 22 | 23 | 24 |

25 | A Bluetooth and Obex client that is straight to the point, DE/WM agnostic, and beautiful :D 26 |

27 |

28 | The main features are: - Dynamically enumerate and list all devices known/in range - Authenticating with devices (aka passkey confirmation) - Sending/receiving files - Multiple adapter support - Audio Profile support - Sorting devices by RSSI (signal strength) - Battery polling over Bluetooth (enable experimental Bluetooth options) - Distance approximation - ...and many more 29 |

30 |
31 | 32 | 33 | 34 | 35 |

Better Device Distance Approximation

36 |
37 | 38 | https://github.com/kaii-lb/overskride/actions/runs/6881017303 39 |
40 |
41 | 42 | io.github.kaii_lb.Overskride.desktop 43 | 44 | 45 | https://raw.githubusercontent.com/kaii-lb/overskride/main/assets/images/overskride.png 46 | multi-window screenshot of app 47 | 48 | 49 | https://raw.githubusercontent.com/kaii-lb/overskride/main/assets/images/center.png 50 | main screen of app 51 | 52 | 53 | https://raw.githubusercontent.com/kaii-lb/overskride/main/assets/images/left.png 54 | minimized form of connected device 55 | 56 | 57 |
58 | -------------------------------------------------------------------------------- /data/io.github.kaii_lb.Overskride.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Type=Application 4 | 5 | Name=Overskride 6 | Comment=A simple yet powerful bluetooth app! 7 | Categories=Utility; 8 | Keywords=blue;bluetooth;transfer;send;audio;share; 9 | 10 | Icon=io.github.kaii_lb.Overskride 11 | Terminal=false 12 | Exec=overskride 13 | StartupNotify=true 14 | -------------------------------------------------------------------------------- /data/io.github.kaii_lb.Overskride.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -1 6 | default window width 7 | 8 | 9 | -1 10 | default window height 11 | 12 | 13 | false 14 | default window maximized 15 | 16 | 17 | "" 18 | default adapter name which the app chooses at startup 19 | 20 | 21 | "" 22 | original adapter name incase everything else fails 23 | 24 | 25 | "" 26 | where the received files are stored 27 | 28 | 29 | true 30 | one time auto accept indicator 31 | 32 | 33 | false 34 | hides unknown devices from the device list 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /data/meson.build: -------------------------------------------------------------------------------- 1 | desktop_file = i18n.merge_file( 2 | input: 'io.github.kaii_lb.Overskride.desktop.in', 3 | output: 'io.github.kaii_lb.Overskride.desktop', 4 | type: 'desktop', 5 | po_dir: '../po', 6 | install: true, 7 | install_dir: join_paths(get_option('datadir'), 'applications') 8 | ) 9 | 10 | desktop_utils = find_program('desktop-file-validate', required: false) 11 | if desktop_utils.found() 12 | test('Validate desktop file', desktop_utils, args: [desktop_file]) 13 | endif 14 | 15 | appstream_file = i18n.merge_file( 16 | input: 'io.github.kaii_lb.Overskride.appdata.xml.in', 17 | output: 'io.github.kaii_lb.Overskride.appdata.xml', 18 | po_dir: '../po', 19 | install: true, 20 | install_dir: join_paths(get_option('datadir'), 'appdata') 21 | ) 22 | 23 | appstream_util = find_program('appstream-util', required: false) 24 | if appstream_util.found() 25 | test('Validate appstream file', appstream_util, args: ['validate', appstream_file]) 26 | endif 27 | 28 | install_data('io.github.kaii_lb.Overskride.gschema.xml', 29 | install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') 30 | ) 31 | 32 | compile_schemas = find_program('glib-compile-schemas', required: false) 33 | if compile_schemas.found() 34 | test('Validate schema file', 35 | compile_schemas, 36 | args: ['--strict', '--dry-run', meson.current_source_dir()]) 37 | endif 38 | 39 | subdir('icons') 40 | -------------------------------------------------------------------------------- /io.github.kaii_lb.Overskride.json: -------------------------------------------------------------------------------- 1 | { 2 | "app-id" : "io.github.kaii_lb.Overskride", 3 | "runtime" : "org.gnome.Platform", 4 | "runtime-version" : "45", 5 | "sdk" : "org.gnome.Sdk", 6 | "sdk-extensions" : [ 7 | "org.freedesktop.Sdk.Extension.rust-stable" 8 | ], 9 | "command" : "overskride", 10 | "finish-args" : [ 11 | "--share=ipc", 12 | "--socket=fallback-x11", 13 | "--device=dri", 14 | "--socket=wayland", 15 | "--allow=bluetooth", 16 | "--socket=system-bus", 17 | "--socket=session-bus", 18 | "--filesystem=xdg-run/gvfsd", 19 | "--talk-name=org.gtk.vfs.*" 20 | ], 21 | "build-options" : { 22 | "append-path" : "/usr/lib/sdk/rust-stable/bin", 23 | "build-args" : [ 24 | "--share=network" 25 | ], 26 | "env" : { 27 | "RUST_BACKTRACE" : "1", 28 | "RUST_LOG" : "overskride=debug" 29 | } 30 | }, 31 | "cleanup" : [ 32 | "/include", 33 | "/lib/pkgconfig", 34 | "/man", 35 | "/share/doc", 36 | "/share/gtk-doc", 37 | "/share/man", 38 | "/share/pkgconfig", 39 | "*.la", 40 | "*.a" 41 | ], 42 | "modules" : [ 43 | { 44 | "name": "blueprint-compiler", 45 | "buildsystem": "meson", 46 | "cleanup": ["*"], 47 | "sources": [ 48 | { 49 | "type": "git", 50 | "url": "https://gitlab.gnome.org/jwestman/blueprint-compiler.git", 51 | "tag": "v0.10.0" 52 | } 53 | ] 54 | }, 55 | { 56 | "name" : "overskride", 57 | "builddir" : true, 58 | "buildsystem" : "meson", 59 | "config-opts": [ 60 | "-Dbuildtype=release" 61 | ], 62 | "sources" : [ 63 | { 64 | "type" : "git", 65 | "url" : "https://github.com/kaii-lb/overskride.git", 66 | "branch" : "main" 67 | } 68 | ] 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /make-pkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd /home/$USER/Projects/overskride/build/package 4 | 5 | cp ../src/overskride overskride/usr/bin/overskride 6 | cp ../src/overskride.gresource overskride/usr/share/overskride/ 7 | cp ../data/io.github.kaii_lb.Overskride.desktop overskride/usr/share/applications/ 8 | cp ../data/io.github.kaii_lb.Overskride.appdata.xml overskride/usr/share/appdata/ 9 | cp /home/kaii/Projects/overskride/data/io.github.kaii_lb.Overskride.gschema.xml overskride/usr/share/glib-2.0/schemas/ 10 | cp /home/kaii/Projects/overskride/data/icons/hicolor/scalable/apps/io.github.kaii_lb.Overskride.svg overskride/usr/share/icons/hicolor/scalable/apps/ 11 | cp /home/kaii/Projects/overskride/data/icons/hicolor/symbolic/apps/io.github.kaii_lb.Overskride-symbolic.svg overskride/usr/share/icons/hicolor/symbolic/apps/ 12 | 13 | tar -cf - overskride/ | xz -9 -T0 > overskride.tar.xz 14 | cp overskride.tar.xz ../../ 15 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('overskride', 'rust', 2 | version: '0.6.1', 3 | meson_version: '>= 0.62.0', 4 | default_options : 'prefix=/usr', 5 | default_options: [ 'warning_level=2', 'werror=false', ], 6 | ) 7 | 8 | i18n = import('i18n') 9 | gnome = import('gnome') 10 | 11 | subdir('data') 12 | subdir('src') 13 | subdir('po') 14 | 15 | gnome.post_install( 16 | glib_compile_schemas: true, 17 | gtk_update_icon_cache: true, 18 | update_desktop_database: true, 19 | ) 20 | -------------------------------------------------------------------------------- /po/LINGUAS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaii-lb/overskride/aa796b71253edfe083edbd68325ea21b8728ebc5/po/LINGUAS -------------------------------------------------------------------------------- /po/POTFILES: -------------------------------------------------------------------------------- 1 | data/io.github.kaii_lb.Overskride.desktop.in 2 | data/io.github.kaii_lb.Overskride.appdata.xml.in 3 | data/io.github.kaii_lb.Overskride.gschema.xml 4 | src/window.ui 5 | -------------------------------------------------------------------------------- /po/meson.build: -------------------------------------------------------------------------------- 1 | i18n.gettext('overskride', preset: 'glib') 2 | -------------------------------------------------------------------------------- /src/application.rs: -------------------------------------------------------------------------------- 1 | /* application.rs 2 | * 3 | * Copyright 2023 kaii 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | use gtk::prelude::*; 22 | use adw::subclass::prelude::*; 23 | use gtk::{gio, glib}; 24 | 25 | use crate::config::VERSION; 26 | use crate::OverskrideWindow; 27 | 28 | mod imp { 29 | use super::*; 30 | 31 | #[derive(Debug, Default)] 32 | pub struct OverskrideApplication {} 33 | 34 | #[glib::object_subclass] 35 | impl ObjectSubclass for OverskrideApplication { 36 | const NAME: &'static str = "OverskrideApplication"; 37 | type Type = super::OverskrideApplication; 38 | type ParentType = adw::Application; 39 | } 40 | 41 | impl ObjectImpl for OverskrideApplication { 42 | fn constructed(&self) { 43 | self.parent_constructed(); 44 | let obj = self.obj(); 45 | obj.setup_gactions(); 46 | obj.set_accels_for_action("app.quit", &["q"]); 47 | obj.set_accels_for_action("win.refresh-devices", &["r"]) 48 | } 49 | } 50 | 51 | impl ApplicationImpl for OverskrideApplication { 52 | // We connect to the activate callback to create a window when the application 53 | // has been launched. Additionally, this callback notifies us when the user 54 | // tries to launch a "second instance" of the application. When they try 55 | // to do that, we'll just present any existing window. 56 | fn activate(&self) { 57 | let application = self.obj(); 58 | // Get the current window or create one if necessary 59 | let window = if let Some(window) = application.active_window() { 60 | window 61 | } else { 62 | let window = OverskrideWindow::new(&*application); 63 | window.upcast() 64 | }; 65 | 66 | // Ask the window manager/compositor to present the window 67 | window.present(); 68 | } 69 | } 70 | 71 | impl GtkApplicationImpl for OverskrideApplication {} 72 | impl AdwApplicationImpl for OverskrideApplication {} 73 | } 74 | 75 | glib::wrapper! { 76 | pub struct OverskrideApplication(ObjectSubclass) 77 | @extends gio::Application, gtk::Application, adw::Application, 78 | @implements gio::ActionGroup, gio::ActionMap; 79 | } 80 | 81 | impl OverskrideApplication { 82 | pub fn new(application_id: &str, flags: &gio::ApplicationFlags) -> Self { 83 | glib::Object::builder() 84 | .property("application-id", application_id) 85 | .property("flags", flags) 86 | .build() 87 | } 88 | 89 | fn setup_gactions(&self) { 90 | let quit_action = gio::ActionEntry::builder("quit") 91 | .activate(move |app: &Self, _, _| app.quit()) 92 | .build(); 93 | let about_action = gio::ActionEntry::builder("about") 94 | .activate(move |app: &Self, _, _| app.show_about()) 95 | .build(); 96 | self.add_action_entries([quit_action, about_action]); 97 | } 98 | 99 | fn show_about(&self) { 100 | let window = self.active_window().unwrap(); 101 | let about = adw::AboutWindow::builder() 102 | .transient_for(&window) 103 | .application_name("Overskride") 104 | .application_icon("io.github.kaii_lb.Overskride") 105 | .developer_name("kaii") 106 | .version(VERSION) 107 | .developers(vec!["kaii", "Email: imkaiilb@gmail.com", "Github: kaii-lb https://github.com/kaii-lb"]) 108 | .copyright("© 2023 kaii") 109 | .license_type(gtk::License::Gpl30) 110 | .release_notes(" 111 |
    112 |
  1. changed the way window titles work
  2. 113 |
  3. added a distance approximation in more info page
  4. 114 |
115 | ") 116 | .issue_url("https://github.com/kaii-lb/overskride/issues") 117 | .website("https://github.com/kaii-lb/overskride") 118 | .build(); 119 | 120 | about.present(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/bluetooth/agent.rs: -------------------------------------------------------------------------------- 1 | use futures::FutureExt; 2 | use gtk::glib::Sender; 3 | 4 | use crate::{message::Message, window::{DISPLAYING_DIALOG, PIN_CODE, PASS_KEY, CONFIRMATION_AUTHORIZATION}}; 5 | 6 | async fn request_pin_code(request: bluer::agent::RequestPinCode, sender: Sender) -> bluer::agent::ReqResult { 7 | println!("request pincode incoming"); 8 | let address = request.device; 9 | 10 | sender.send(Message::RequestPinCode(request)).expect("cannot send message"); 11 | unsafe { 12 | DISPLAYING_DIALOG = true; 13 | } 14 | 15 | wait_for_dialog_exit().await; 16 | 17 | let final_pin_code = unsafe { 18 | PIN_CODE.clone() 19 | }; 20 | println!("pin code is: {:?}", final_pin_code); 21 | if final_pin_code.is_empty() { 22 | Err(bluer::agent::ReqError::Rejected) 23 | } 24 | else { 25 | sender.send(Message::SwitchActive(true, address, true)).expect("cannot send message"); 26 | Ok(final_pin_code) 27 | } 28 | } 29 | 30 | async fn display_pin_code(request: bluer::agent::DisplayPinCode, sender: Sender) -> bluer::agent::ReqResult<()> { 31 | println!("display pincode incoming"); 32 | 33 | sender.send(Message::DisplayPinCode(request)).expect("cannot send message"); 34 | unsafe { 35 | DISPLAYING_DIALOG = true 36 | } 37 | 38 | wait_for_dialog_exit().await; 39 | 40 | println!("displaying pin code finished"); 41 | Ok(()) 42 | } 43 | 44 | async fn request_pass_key(request: bluer::agent::RequestPasskey, sender: Sender) -> bluer::agent::ReqResult { 45 | println!("request passkey incoming"); 46 | let address = request.device; 47 | 48 | sender.send(Message::RequestPassKey(request)).expect("cannot send message"); 49 | unsafe { 50 | DISPLAYING_DIALOG = true; 51 | } 52 | 53 | wait_for_dialog_exit().await; 54 | 55 | let pass_key = unsafe { 56 | PASS_KEY 57 | }; 58 | println!("pass key is: {}", pass_key); 59 | if pass_key == 0 { 60 | Err(bluer::agent::ReqError::Rejected) 61 | } 62 | else { 63 | sender.send(Message::SwitchActive(true, address, true)).expect("cannot send message"); 64 | Ok(pass_key) 65 | } 66 | } 67 | 68 | async fn display_pass_key(request: bluer::agent::DisplayPasskey, sender: Sender) -> bluer::agent::ReqResult<()> { 69 | println!("display passkey incoming"); 70 | 71 | sender.send(Message::DisplayPassKey(request)).expect("cannot send message"); 72 | unsafe { 73 | DISPLAYING_DIALOG = true; 74 | } 75 | 76 | wait_for_dialog_exit().await; 77 | 78 | Ok(()) 79 | } 80 | 81 | async fn request_confirmation(request: bluer::agent::RequestConfirmation, _: bluer::Session, _: bool, sender: Sender) -> bluer::agent::ReqResult<()> { 82 | println!("pairing confirmation incoming"); 83 | let address = request.device; 84 | 85 | sender.send(Message::RequestConfirmation(request)).expect("cannot send message"); 86 | unsafe { 87 | DISPLAYING_DIALOG = true; 88 | } 89 | 90 | wait_for_dialog_exit().await; 91 | 92 | let confirmed = unsafe { 93 | CONFIRMATION_AUTHORIZATION 94 | }; 95 | if confirmed { 96 | println!("allowed pairing with device"); 97 | sender.send(Message::SwitchActive(true, address, true)).expect("cannot send message"); 98 | Ok(()) 99 | } 100 | else { 101 | println!("rejected pairing with device"); 102 | Err(bluer::agent::ReqError::Rejected) 103 | } 104 | } 105 | 106 | async fn request_authorization(request: bluer::agent::RequestAuthorization, _: bluer::Session, _: bool, sender: Sender) -> bluer::agent::ReqResult<()> { 107 | println!("pairing authorization incoming"); 108 | let address = request.device; 109 | 110 | sender.send(Message::RequestAuthorization(request)).expect("cannot send message"); 111 | unsafe{ 112 | DISPLAYING_DIALOG = true; 113 | } 114 | 115 | wait_for_dialog_exit().await; 116 | 117 | let confirmed = unsafe { 118 | CONFIRMATION_AUTHORIZATION 119 | }; 120 | if confirmed { 121 | println!("allowed pairing with device"); 122 | sender.send(Message::SwitchActive(true, address, true)).expect("cannot send message"); 123 | Ok(()) 124 | } 125 | else { 126 | println!("rejected pairing with device"); 127 | Err(bluer::agent::ReqError::Rejected) 128 | } 129 | 130 | } 131 | 132 | async fn authorize_service(request: bluer::agent::AuthorizeService, sender: Sender) -> bluer::agent::ReqResult<()> { 133 | println!("service authorization incoming"); 134 | let address = request.device; 135 | 136 | sender.send(Message::AuthorizeService(request)).expect("cannot send message"); 137 | unsafe{ 138 | DISPLAYING_DIALOG = true; 139 | } 140 | 141 | wait_for_dialog_exit().await; 142 | 143 | let confirmed = unsafe { 144 | CONFIRMATION_AUTHORIZATION 145 | }; 146 | 147 | if confirmed { 148 | println!("allowed pairing with device"); 149 | sender.send(Message::SwitchActive(true, address, true)).expect("cannot send message"); 150 | Ok(()) 151 | } 152 | else { 153 | println!("rejected pairing with device"); 154 | Err(bluer::agent::ReqError::Rejected) 155 | } 156 | } 157 | 158 | pub async fn register_agent(session: &bluer::Session, request_default: bool, set_trust: bool, sender_to_be_sent: Sender) -> bluer::Result { 159 | let session1 = session.clone(); 160 | let session2 = session.clone(); 161 | 162 | // IDK if this is the best way, but its a way. 163 | let sender1 = sender_to_be_sent.clone(); 164 | let sender2 = sender_to_be_sent.clone(); 165 | let sender3 = sender_to_be_sent.clone(); 166 | let sender4 = sender_to_be_sent.clone(); 167 | let sender5 = sender_to_be_sent.clone(); 168 | let sender6 = sender_to_be_sent.clone(); 169 | let sender7 = sender_to_be_sent.clone(); 170 | 171 | let agent = bluer::agent::Agent { 172 | request_default, 173 | request_pin_code: Some(Box::new(move |req| request_pin_code(req, sender1.clone()).boxed())), 174 | display_pin_code: Some(Box::new(move |req| display_pin_code(req, sender2.clone()).boxed())), 175 | request_passkey: Some(Box::new(move |req| request_pass_key(req, sender3.clone()).boxed())), 176 | display_passkey: Some(Box::new(move |req| display_pass_key(req, sender4.clone()).boxed())), 177 | request_confirmation: Some(Box::new(move |req| { 178 | request_confirmation(req, session1.clone(), set_trust, sender5.clone()).boxed() 179 | })), 180 | request_authorization: Some(Box::new(move |req| { 181 | request_authorization(req, session2.clone(), set_trust, sender6.clone()).boxed() 182 | })), 183 | authorize_service: Some(Box::new(move |req| authorize_service(req, sender7.clone()).boxed())), 184 | ..Default::default() 185 | }; 186 | 187 | let handle = session.register_agent(agent).await.expect("unable to register agent, fuck-"); 188 | 189 | Ok(handle) 190 | } 191 | 192 | pub async fn wait_for_dialog_exit() { 193 | unsafe { 194 | loop { 195 | if !DISPLAYING_DIALOG { 196 | std::thread::sleep(std::time::Duration::from_secs(1)); 197 | break; 198 | } 199 | } 200 | } 201 | } 202 | 203 | #[tokio::main] 204 | pub async fn register_bluetooth_agent(sender: Sender) -> bluer::Result<()> { 205 | let session = bluer::Session::new().await?; 206 | let agent = register_agent(&session, true, false, sender.clone()).await?; 207 | println!("registered agent standalone {:?}", agent); 208 | 209 | loop { 210 | std::thread::sleep(std::time::Duration::from_secs(1)); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/bluetooth/audio_profiles.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use bluer::Error; 3 | use std::{cell::RefCell, rc::Rc, ops::Deref}; 4 | 5 | use pulseaudio::{mainloop::standard::{IterateResult, Mainloop}, context::Context, proplist::Proplist, def::Retval}; 6 | use pulseaudio::context::FlagSet as ContextFlagSet; 7 | 8 | pub struct AudioProfiles { 9 | pub active_profile: String, 10 | pub profiles: HashMap, 11 | } 12 | 13 | impl AudioProfiles { 14 | /// create a new pulse audio connection to a certain device, then returns the current active profile and the other profiles this device supports 15 | pub fn new(address: String) -> Result { 16 | // bla bla make new connection yes very fancy 17 | let mut proplist = Proplist::new().unwrap(); 18 | proplist.set_str(pulseaudio::proplist::properties::APPLICATION_NAME, "Overskride") 19 | .unwrap(); 20 | 21 | let mainloop = Rc::new(RefCell::new(Mainloop::new() 22 | .expect("Failed to create mainloop"))); 23 | 24 | let context = Rc::new(RefCell::new(Context::new_with_proplist( 25 | mainloop.borrow().deref(), 26 | "OverskrideContext", 27 | &proplist 28 | ).expect("Failed to create new context"))); 29 | 30 | context.borrow_mut().connect(None, ContextFlagSet::NOFLAGS, None) 31 | .expect("Failed to connect context"); 32 | 33 | // Wait for context to be ready 34 | loop { 35 | match mainloop.borrow_mut().iterate(false) { 36 | IterateResult::Quit(_) | 37 | IterateResult::Err(_) => { 38 | eprintln!("Iterate state was not success, quitting..."); 39 | return Err(Error { kind: bluer::ErrorKind::Failed, message: "iterate state was not a success".to_string() }); 40 | }, 41 | IterateResult::Success(_) => {}, 42 | } 43 | match context.borrow().get_state() { 44 | pulseaudio::context::State::Ready => { break; }, 45 | pulseaudio::context::State::Failed | 46 | pulseaudio::context::State::Terminated => { 47 | eprintln!("Context state failed/terminated, quitting..."); 48 | return Err(Error { kind: bluer::ErrorKind::Failed, message: "context state failed".to_string() }); 49 | }, 50 | _ => {}, 51 | } 52 | } 53 | 54 | // pulse audio bluetooth names start with "bluez_card." and instead of : its _ 55 | // so like bluez_card.XX_XX_XX_XX_XX_XX 56 | let card_name = "bluez_card.".to_string() + &address.replace(':', "_"); 57 | 58 | let clonable_map = Rc::new(RefCell::new(HashMap::::new())); 59 | let cloned_map = clonable_map.clone(); 60 | let clonable_active_profile = Rc::new(RefCell::new(String::new())); 61 | let cloned_active_profile = clonable_active_profile.clone(); 62 | 63 | let error = Rc::new(RefCell::new(false)); 64 | let error_clone = error.clone(); 65 | 66 | // gets the active profile and available profiles of this "card" (its really a device but wtv) 67 | let state = context.borrow().introspect().get_card_info_by_name(&card_name, move |card_info_result| { 68 | match card_info_result { 69 | pulseaudio::callbacks::ListResult::Item(item) => { 70 | let card_profiles = &item.profiles; 71 | for card_profile in card_profiles { 72 | // println!("\nprofile: {:?}", card_profile.name); 73 | // println!("profile description: {:?}", card_profile.description); 74 | 75 | if let Some(profile) = &card_profile.name { 76 | if let Some(description) = &card_profile.description { 77 | cloned_map.borrow_mut().insert(profile.to_string(), description.to_string()); 78 | } 79 | } 80 | } 81 | 82 | *cloned_active_profile.borrow_mut() = if let Some(active) = &item.active_profile { 83 | if let Some(name) = &active.name { 84 | name.to_string() 85 | } 86 | else { 87 | "".to_string() 88 | } 89 | } 90 | else { 91 | "".to_string() 92 | }; 93 | 94 | // println!("\ncard active profile: {:?}", item.active_profile.as_ref().unwrap()); 95 | }, 96 | pulseaudio::callbacks::ListResult::End => { 97 | println!("device's audio profiles enumerated"); 98 | }, 99 | pulseaudio::callbacks::ListResult::Error => { 100 | println!("could not get audio profiles for device"); 101 | *error_clone.borrow_mut() = true; 102 | } 103 | } 104 | }); 105 | 106 | // process pulse audio requests until the done, if error then return an error 107 | loop { 108 | mainloop.borrow_mut().iterate(false); 109 | std::thread::sleep(std::time::Duration::from_secs(1)); 110 | 111 | if state.get_state() == pulseaudio::operation::State::Done { 112 | if *error.borrow() { 113 | return Err(Error { kind: bluer::ErrorKind::Failed, message: "context state failed".to_string() }); 114 | } 115 | else { 116 | break; 117 | } 118 | } 119 | } 120 | mainloop.borrow_mut().quit(Retval(0)); 121 | context.borrow_mut().disconnect(); 122 | 123 | let active = clonable_active_profile.borrow().clone(); 124 | let mut profiles = clonable_map.borrow_mut().clone(); 125 | 126 | // remove the "off" profile as thats what the expander switch is for 127 | profiles.remove("off"); 128 | 129 | // return the active profile with the rest of the profiles 130 | Ok(AudioProfiles { active_profile: active, profiles }) 131 | } 132 | } 133 | 134 | /// sets the profile for a given device 135 | pub fn device_set_profile(address: String, profile: String) { 136 | // more connection shit 137 | let mut proplist = Proplist::new().unwrap(); 138 | proplist.set_str(pulseaudio::proplist::properties::APPLICATION_NAME, "Overskride") 139 | .unwrap(); 140 | 141 | let mainloop = Rc::new(RefCell::new(Mainloop::new() 142 | .expect("Failed to create mainloop"))); 143 | 144 | let context = Rc::new(RefCell::new(Context::new_with_proplist( 145 | mainloop.borrow().deref(), 146 | "OverskrideContext", 147 | &proplist 148 | ).expect("Failed to create new context"))); 149 | 150 | context.borrow_mut().connect(None, ContextFlagSet::NOFLAGS, None) 151 | .expect("Failed to connect context"); 152 | 153 | // Wait for context to be ready 154 | loop { 155 | match mainloop.borrow_mut().iterate(false) { 156 | IterateResult::Quit(_) | 157 | IterateResult::Err(_) => { 158 | eprintln!("Iterate state was not success, quitting..."); 159 | return; 160 | }, 161 | IterateResult::Success(_) => {}, 162 | } 163 | match context.borrow().get_state() { 164 | pulseaudio::context::State::Ready => { break; }, 165 | pulseaudio::context::State::Failed | 166 | pulseaudio::context::State::Terminated => { 167 | eprintln!("Context state failed/terminated, quitting..."); 168 | return; 169 | }, 170 | _ => {}, 171 | } 172 | } 173 | 174 | let card_name = "bluez_card.".to_string() + &address.replace(':', "_"); 175 | 176 | let clonable_state = Rc::new(RefCell::new(false)); 177 | let clone = clonable_state.clone(); 178 | let done = Rc::new(RefCell::new(false)); 179 | let done_clone = clonable_state.clone(); 180 | 181 | println!("{} {}", &card_name, &profile); 182 | 183 | // sets the card profile, then updates the state and the done-ness of this function 184 | // should move to using the returned "Operation" value instead of weird ass borrows 185 | context.borrow().introspect().set_card_profile_by_name(&card_name, &profile, Some(Box::new(move |state| { 186 | *clone.borrow_mut() = state; 187 | *done_clone.borrow_mut() = true; 188 | }))); 189 | 190 | loop { 191 | mainloop.borrow_mut().iterate(false); 192 | std::thread::sleep(std::time::Duration::from_secs(1)); 193 | 194 | if *done.borrow() { 195 | break; 196 | } 197 | } 198 | mainloop.borrow_mut().quit(Retval(0)); 199 | context.borrow_mut().disconnect(); 200 | } 201 | -------------------------------------------------------------------------------- /src/bluetooth/battery.rs: -------------------------------------------------------------------------------- 1 | // This code was autogenerated with `dbus-codegen-rust --file test.xml`, see https://github.com/diwic/dbus-rs 2 | use dbus::{self as dbus, blocking::{Connection, stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged}}; 3 | #[allow(unused_imports)] 4 | use dbus::arg; 5 | use dbus::blocking; 6 | use gtk::glib::Sender; 7 | 8 | use crate::message::Message; 9 | 10 | pub static mut CANCEL_BATTERY_CHECK: bool = false; 11 | static mut LAST_ADDRESS: String = String::new(); 12 | static mut PATH: String = String::new(); 13 | 14 | pub trait OrgBluezBattery1 { 15 | fn percentage(&self) -> Result; 16 | } 17 | 18 | impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> OrgBluezBattery1 for blocking::Proxy<'a, C> { 19 | fn percentage(&self) -> Result { 20 | ::get(self, "org.bluez.Battery1", "Percentage") 21 | } 22 | } 23 | 24 | // if battery state changes, then update the UI 25 | fn handle_properties_updated(sender: Sender) { 26 | let conn = Connection::new_system().unwrap(); 27 | let proxy = conn.with_proxy("org.bluez", unsafe {&PATH}, std::time::Duration::from_millis(5000)); 28 | 29 | let reported = if let Ok(val) = proxy.percentage() { 30 | val as i8 31 | } 32 | else { 33 | -1 34 | }; 35 | sender.send(Message::UpdateBatteryLevel(reported)).expect("cannot send message"); 36 | } 37 | 38 | /// Gets (and continues getting) the battery level of a device until it is canceled 39 | pub fn get_battery_for_device(address: String, adapter: String, sender: Sender) { 40 | unsafe { 41 | if address == LAST_ADDRESS { 42 | println!("stopped checking battery, last addressed matched"); 43 | return; 44 | } 45 | else { 46 | LAST_ADDRESS = address.clone(); 47 | } 48 | } 49 | 50 | // to get the path to this device make it so the full path is 51 | // something like "/org/bluez/dev_XX_XX_XX_XX_XX_XX" 52 | let fixed_address = "dev_".to_string() + &address.replace(':', "_"); 53 | let path = "/org/bluez/".to_string() + &adapter + "/" + &fixed_address; 54 | unsafe { 55 | PATH = path.clone(); 56 | } 57 | // println!("full path to device is: {}", path); 58 | 59 | let conn = Connection::new_system().unwrap(); 60 | let proxy = conn.with_proxy("org.bluez", path, std::time::Duration::from_millis(5000)); 61 | 62 | let sender_clone = sender.clone(); 63 | proxy.match_signal(move |_: PropertiesPropertiesChanged, _: &Connection, _: &dbus::Message| { 64 | let clone = sender_clone.clone(); 65 | handle_properties_updated(clone); 66 | true 67 | }).expect("can't match signal"); 68 | 69 | // send a first reported battery to the UI as we don't want to wait for the battery to change to let the user know what it is 70 | // as that could take an unreasonably long time with the dbus implementation 71 | // should add a fallback (or maybe make this the fallback) with a better get battery method 72 | let first_reported = if let Ok(val) = proxy.percentage() { 73 | val as i8 74 | } 75 | else { 76 | -1 77 | }; 78 | 79 | sender.send(Message::UpdateBatteryLevel(first_reported)).expect("cannot send message"); 80 | 81 | // get battery till canceled 82 | loop { 83 | conn.process(std::time::Duration::from_millis(1000)).expect("cannot process battery check request"); 84 | unsafe { 85 | if CANCEL_BATTERY_CHECK && address != LAST_ADDRESS { 86 | println!("stopped checking battery"); 87 | CANCEL_BATTERY_CHECK = false; 88 | break; 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/bluetooth/bluetooth_settings.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use gtk::glib::Sender; 3 | 4 | use crate::{message::Message, window::{ADAPTERS_LUT, DISPLAYING_DIALOG, SEND_FILES_PATH}, agent::wait_for_dialog_exit}; 5 | 6 | /// sets the current adapter's powered state, updating the UI 7 | #[tokio::main] 8 | pub async fn set_adapter_powered(adapter_name: String, sender: Sender) -> bluer::Result<()> { 9 | let adapter = bluer::Session::new().await?.adapter(adapter_name.as_str())?; 10 | 11 | let current = adapter.is_powered().await?; 12 | 13 | adapter.set_powered(!current).await?; 14 | 15 | let powered = adapter.is_powered().await?; 16 | 17 | if powered { 18 | sender.send(Message::RefreshDevicesList()).expect("can't send message"); 19 | sender.send(Message::PopupError("br-adapter-refreshed".to_string(), adw::ToastPriority::Normal)).expect("can't send message"); 20 | } 21 | else { 22 | sender.send(Message::SwitchActive(false, bluer::Address::any(), true)).expect("can't send message"); 23 | } 24 | 25 | sender.send(Message::SwitchAdapterPowered(powered)).expect("can't send message"); 26 | Ok(()) 27 | } 28 | 29 | /// Makes or un-makes this adapter visible to other devices 30 | #[tokio::main] 31 | pub async fn set_adapter_discoverable(adapter_name: String, sender: Sender) -> bluer::Result<()> { 32 | let adapter = bluer::Session::new().await?.adapter(adapter_name.as_str())?; 33 | 34 | let current = adapter.is_discoverable().await?; 35 | adapter.set_discoverable(!current).await?; 36 | 37 | std::thread::sleep(std::time::Duration::from_secs_f32(0.5)); 38 | let discoverable = adapter.is_discoverable().await?; 39 | sender.send(Message::SwitchAdapterDiscoverable(discoverable)).expect("can't send message"); 40 | 41 | // println!("discoverable is: {}", discoverable); 42 | 43 | Ok(()) 44 | } 45 | 46 | /// get the adapter properties, updating the UI heavily 47 | #[tokio::main] 48 | pub async fn get_adapter_properties(adapters_hashmap: HashMap, sender: Sender, adapter_name: String) -> bluer::Result<()> { 49 | let adapter = bluer::Session::new().await?.adapter(adapter_name.as_str())?; 50 | 51 | let is_powered = adapter.is_powered().await?; 52 | let is_discoverable = adapter.is_discoverable().await?; 53 | let alias = adapter.alias().await?; 54 | let timeout = adapter.discoverable_timeout().await? / 60; 55 | 56 | sender.send(Message::PopulateAdapterExpander(adapters_hashmap)).expect("cannot send message {}"); 57 | //println!("sent populate adapters message"); 58 | sender.send(Message::SwitchAdapterPowered(is_powered)).expect("cannot get adapter powered."); 59 | sender.send(Message::SwitchAdapterDiscoverable(is_discoverable)).expect("cannot get adapter discoverable."); 60 | sender.send(Message::SwitchAdapterName(alias.clone().to_string(), alias.to_string())).expect("cannot get adapter name."); 61 | sender.send(Message::SwitchAdapterTimeout(timeout)).expect("cannot get adapter timeout."); 62 | 63 | Ok(()) 64 | } 65 | 66 | /// set the adapter name, (its actually the alias, name is hardcoded) 67 | /// alias: "laptop 1", name: "hci0" 68 | /// don't change name, thats bad, change alias instead 69 | #[tokio::main] 70 | pub async fn set_adapter_name(alias: String, adapter_name: String, sender: Sender) -> bluer::Result<()> { 71 | let adapter = bluer::Session::new().await?.adapter(adapter_name.as_str())?; 72 | 73 | let old_alias = adapter.alias().await?; 74 | //println!("old alias is: {}", old_alias.to_string()); 75 | 76 | adapter.set_alias(alias.clone()).await?; 77 | 78 | // wait for alias to change 79 | std::thread::sleep(std::time::Duration::from_secs(1)); 80 | 81 | let new_alias = adapter.alias().await?; 82 | println!("new adapter alias is: {} compared to {}", new_alias, alias); 83 | 84 | // update the lut with the new info 85 | unsafe { 86 | let mut lut = ADAPTERS_LUT.clone().unwrap(); 87 | let bluetooth_name = adapter.name().to_string(); 88 | 89 | lut.remove(&old_alias.clone()); 90 | lut.insert(new_alias.clone(), bluetooth_name); 91 | ADAPTERS_LUT = Some(lut); 92 | } 93 | sender.send(Message::SwitchAdapterName(new_alias, old_alias)).expect("cannot change adapter name."); 94 | 95 | //println!("name is: {}", name.clone()); 96 | Ok(()) 97 | } 98 | 99 | /// sets the discoverable timeout of this adapter 100 | #[tokio::main] 101 | pub async fn set_timeout_duration(timeout: u32, adapter_name: String, sender: Sender) -> bluer::Result<()> { 102 | let adapter = bluer::Session::new().await?.adapter(adapter_name.as_str())?; 103 | 104 | adapter.set_discoverable_timeout(timeout * 60).await?; 105 | 106 | // std::thread::sleep(std::time::Duration::from_millis(100)); 107 | 108 | let new_timeout = adapter.discoverable_timeout().await? / 60; 109 | sender.send(Message::SwitchAdapterTimeout(new_timeout)).expect("cannot set timeout."); 110 | 111 | Ok(()) 112 | } 113 | /// adds every adapter with its alias and name to a hashmap, returning that hashmap 114 | #[tokio::main] 115 | pub async fn populate_adapter_expander() -> bluer::Result> { 116 | let current_session = bluer::Session::new().await?; 117 | let adapter_names = current_session.adapter_names().await?; 118 | let mut alias_name_hashmap: HashMap = HashMap::new(); 119 | 120 | for name in adapter_names.clone() { 121 | let adapter = current_session.adapter(name.as_str())?; 122 | 123 | let alias = adapter.alias().await?; 124 | 125 | alias_name_hashmap.insert(alias.clone().to_string(), name.clone().to_string()); 126 | //println!("adapter alias is: {}", alias) 127 | } 128 | 129 | unsafe { 130 | ADAPTERS_LUT = Some(alias_name_hashmap.clone()); 131 | } 132 | 133 | Ok(alias_name_hashmap) 134 | } 135 | 136 | /// wrapper to get the file save location from a file picker 137 | #[tokio::main] 138 | pub async fn get_store_location_from_dialog(sender: Sender) { 139 | unsafe { 140 | DISPLAYING_DIALOG = true; 141 | } 142 | sender.send(Message::GetFile(gtk::FileChooserAction::SelectFolder)).expect("cannot send message"); 143 | 144 | wait_for_dialog_exit().await; 145 | 146 | let path = unsafe { 147 | (SEND_FILES_PATH[0]).to_string() 148 | }; 149 | 150 | sender.send(Message::SetFileStorageLocation(path)).expect("cannot send message"); 151 | } 152 | -------------------------------------------------------------------------------- /src/bluetooth/device.rs: -------------------------------------------------------------------------------- 1 | use bluer::{AdapterEvent, AdapterProperty, DeviceEvent, DeviceProperty}; 2 | use futures::{pin_mut, stream::SelectAll, StreamExt}; 3 | use gtk::glib::Sender; 4 | use tokio_util::sync::CancellationToken; 5 | use uuid::uuid; 6 | 7 | use crate::{message::Message, window::{DEVICES_LUT, CURRENT_ADDRESS, CONFIRMATION_AUTHORIZATION, DISPLAYING_DIALOG}, agent::wait_for_dialog_exit, audio_profiles::AudioProfiles, battery::CANCEL_BATTERY_CHECK, services}; 8 | 9 | static mut CANCELLATION_TOKEN: Option = None; 10 | 11 | /// Set the associated with `address` device's state, between connected and not 12 | /// connected depending on what was already the case. 13 | /// A little funky and needs fixing but works for now. 14 | #[tokio::main] 15 | pub async fn set_device_active(address: bluer::Address, sender: Sender, adapter_name: String) -> bluer::Result<()> { 16 | let address_string = address.clone().to_string(); 17 | let adapter_string = adapter_name.clone(); 18 | 19 | let adapter = bluer::Session::new().await?.adapter(adapter_name.as_str())?; 20 | let device = adapter.device(address)?; 21 | 22 | sender.send(Message::SwitchActiveSpinner(true)).expect("cannot set spinner to show."); 23 | 24 | let state = device.is_connected().await?; 25 | 26 | if state { 27 | device.disconnect().await?; 28 | } 29 | else if !device.is_paired().await? { 30 | // let agent = register_agent(¤t_session, true, true).await?; 31 | // println!("agent is: {:?}\n", agent); 32 | 33 | device.pair().await?; 34 | 35 | device.connect().await?; 36 | device.connect().await?; 37 | // drop(agent); 38 | } 39 | else { 40 | device.connect().await?; 41 | } 42 | 43 | let updated_state = device.is_connected().await?; 44 | 45 | 46 | println!("set state {} for device {}\n", updated_state, device.address()); 47 | sender.send(Message::SwitchActiveSpinner(false)).expect("cannot set spinner to show."); 48 | sender.send(Message::SwitchActive(updated_state, address, true)).expect("cannot send message"); 49 | sender.send(Message::InvalidateSort()).expect("cannot set device name."); 50 | 51 | // sender.send(Message::SwitchActiveSpinner(false)).expect("cannot set spinner to show."); 52 | // connected_switch_row.set_active(!connected_switch_row.active()); 53 | 54 | let sender_clone = sender.clone(); 55 | std::thread::spawn(move || { 56 | let clone = sender_clone.clone(); 57 | unsafe { 58 | CANCEL_BATTERY_CHECK = true; 59 | } 60 | crate::battery::get_battery_for_device(address_string, adapter_string, clone); 61 | }); 62 | 63 | sender.send(Message::SwitchAudioProfileExpanded(false)).expect("cannot send message"); 64 | sender.send(Message::SwitchAudioProfilesList(false)).expect("cannot send message"); 65 | 66 | if let Ok(profiles) = AudioProfiles::new(address.to_string()) { 67 | let active = profiles.active_profile; 68 | let profiles_map = profiles.profiles; 69 | 70 | if !profiles_map.is_empty() { 71 | sender.send(Message::PopulateAudioProfilesList(profiles_map)).expect("cannot send message"); 72 | sender.send(Message::SwitchAudioProfilesList(true)).expect("cannot send message"); 73 | sender.send(Message::SetActiveAudioProfile(active)).expect("cannot send message"); 74 | } 75 | else { 76 | sender.send(Message::SwitchAudioProfilesList(false)).expect("cannot send message"); 77 | sender.send(Message::SwitchAudioProfileExpanded(false)).expect("cannot send message"); 78 | } 79 | } 80 | else { 81 | sender.send(Message::SwitchAudioProfilesList(false)).expect("cannot send message"); 82 | sender.send(Message::SwitchAudioProfileExpanded(false)).expect("cannot send message"); 83 | } 84 | 85 | if let Ok(()) = has_service(uuid!("00001105-0000-1000-8000-00805f9b34fb"), device).await { 86 | sender.send(Message::SwitchHasObexService(true)).expect("cannot send message"); 87 | sender.send(Message::SwitchSendFileActive(updated_state)).expect("cannot send message"); 88 | } 89 | else { 90 | sender.send(Message::SwitchHasObexService(false)).expect("cannot send message"); 91 | sender.send(Message::SwitchSendFileActive(false)).expect("cannot send message"); 92 | } 93 | 94 | 95 | 96 | Ok(()) 97 | } 98 | 99 | /// Set's the device's blocked state based on what was already the case. 100 | /// Basically stops all connections and requests if the device is blocked. 101 | #[tokio::main] 102 | pub async fn set_device_blocked(address: bluer::Address, sender: Sender, adapter_name: String) -> bluer::Result<()> { 103 | let adapter = bluer::Session::new().await?.adapter(adapter_name.as_str())?; 104 | let device = adapter.device(address)?; 105 | 106 | let blocked = !device.is_blocked().await?; 107 | 108 | device.set_blocked(blocked).await?; 109 | 110 | sender.send(Message::SwitchBlocked(blocked)).expect("cannot set device blocked."); 111 | 112 | // println!("setting blocked {} for device {}", new_blocked, device.address()); 113 | Ok(()) 114 | } 115 | 116 | /// Sets the device's trusted state depending on what was already the case. 117 | /// If trusted, connections to the device won't need pin/passkey everytime. 118 | #[tokio::main] 119 | pub async fn set_device_trusted(address: bluer::Address, sender: Sender, adapter_name: String) -> bluer::Result<()> { 120 | let adapter = bluer::Session::new().await?.adapter(adapter_name.as_str())?; 121 | let device = adapter.device(address)?; 122 | 123 | let trusted = !device.is_trusted().await?; 124 | 125 | device.set_trusted(trusted).await?; 126 | 127 | sender.send(Message::SwitchTrusted(trusted)).expect("cannot set device trusted."); 128 | // println!("setting trusted {} for device {}", new_trusted, device.address()); 129 | 130 | Ok(()) 131 | } 132 | 133 | /// Sets the currently selected device's name, updating the entry and listboxrow accordingly. 134 | #[tokio::main] 135 | pub async fn set_device_name(address: bluer::Address, name: String, sender: Sender, adapter_name: String) -> bluer::Result<()> { 136 | let adapter = bluer::Session::new().await?.adapter(adapter_name.as_str())?; 137 | let device = adapter.device(address)?; 138 | let mut lut = unsafe { 139 | DEVICES_LUT.clone().unwrap() 140 | }; 141 | 142 | let set_name = name.trim().to_string(); 143 | 144 | for key in lut.keys() { 145 | if let Some(pair) = lut.get_key_value(key) { 146 | if pair.1.trim() == set_name && pair.0 != &address { 147 | sender.send(Message::SetNameValid(false)).expect("cannot send message"); 148 | return Err(bluer::Error { kind: bluer::ErrorKind::AlreadyExists, message: "device-name-exists".to_string() }); 149 | } 150 | } 151 | } 152 | 153 | 154 | device.set_alias(set_name).await?; 155 | let current_alias = device.alias().await?; 156 | 157 | unsafe { 158 | lut.remove(&address); 159 | lut.insert(address, current_alias.clone()); 160 | DEVICES_LUT = Some(lut); 161 | } 162 | 163 | sender.send(Message::SwitchName(current_alias, None, address)).expect("cannot set device name."); 164 | sender.send(Message::SetNameValid(true)).expect("cannot send message"); 165 | 166 | std::thread::sleep(std::time::Duration::from_millis(500)); 167 | sender.send(Message::InvalidateSort()).expect("cannot set device name."); 168 | Ok(()) 169 | } 170 | 171 | /// Gets the the device associates with `address`, and then retrieves the properties of that device. 172 | #[tokio::main] 173 | pub async fn get_device_properties(address: bluer::Address, sender: Sender, adapter_name: String) -> bluer::Result<()> { 174 | let adapter_string = adapter_name.clone(); 175 | let address_string = address.clone().to_string(); 176 | 177 | let adapter = bluer::Session::new().await?.adapter(&adapter_name)?; 178 | let device = adapter.device(address)?; 179 | 180 | let is_active = device.is_connected().await?; 181 | let is_blocked = device.is_blocked().await?; 182 | let is_trusted = device.is_trusted().await?; 183 | let alias = device.alias().await?; 184 | let icon_name = match device.icon().await? { 185 | Some(icon) => { 186 | icon 187 | }, 188 | None => { 189 | "image-missing-symbolic".to_string() 190 | }, 191 | }; 192 | 193 | sender.send(Message::SwitchPage(Some(alias), Some(icon_name))).expect("cannot set device alias and icon in page."); 194 | sender.send(Message::SwitchActive(is_active, address, true)).expect("cannot set device active in page."); 195 | sender.send(Message::SwitchBlocked(is_blocked)).expect("cannot set device blocked in page."); 196 | sender.send(Message::SwitchTrusted(is_trusted)).expect("cannot set device trusted in page."); 197 | sender.send(Message::SetNameValid(true)).expect("cannot send message"); 198 | sender.send(Message::SwitchAudioProfileExpanded(false)).expect("cannot send message"); 199 | sender.send(Message::SwitchAudioProfilesList(false)).expect("cannot send message"); 200 | 201 | let sender_clone = sender.clone(); 202 | std::thread::spawn(move || { 203 | let clone = sender_clone.clone(); 204 | unsafe { 205 | CANCEL_BATTERY_CHECK = true; 206 | } 207 | crate::battery::get_battery_for_device(address_string, adapter_string, clone); 208 | }); 209 | 210 | 211 | if let Ok(profiles) = AudioProfiles::new(address.to_string()) { 212 | let active = profiles.active_profile; 213 | let profiles_map = profiles.profiles; 214 | 215 | if !profiles_map.is_empty() { 216 | sender.send(Message::PopulateAudioProfilesList(profiles_map)).expect("cannot send message"); 217 | sender.send(Message::SwitchAudioProfilesList(true)).expect("cannot send message"); 218 | sender.send(Message::SetActiveAudioProfile(active)).expect("cannot send message"); 219 | } 220 | else { 221 | sender.send(Message::SwitchAudioProfilesList(false)).expect("cannot send message"); 222 | sender.send(Message::SwitchAudioProfileExpanded(false)).expect("cannot send message"); 223 | } 224 | } 225 | else { 226 | sender.send(Message::SwitchAudioProfilesList(false)).expect("cannot send message"); 227 | sender.send(Message::SwitchAudioProfileExpanded(false)).expect("cannot send message"); 228 | } 229 | 230 | if let Ok(()) = has_service(uuid!("00001105-0000-1000-8000-00805f9b34fb"), device).await { 231 | sender.send(Message::SwitchHasObexService(true)).expect("cannot send message"); 232 | sender.send(Message::SwitchSendFileActive(is_active)).expect("cannot send message"); 233 | } 234 | else { 235 | sender.send(Message::SwitchHasObexService(false)).expect("cannot send message"); 236 | sender.send(Message::SwitchSendFileActive(false)).expect("cannot send message"); 237 | } 238 | 239 | // println!("the devices properties have been gotten with state: {}", is_active); 240 | 241 | Ok(()) 242 | } 243 | 244 | #[tokio::main] 245 | pub async fn remove_device(address: bluer::Address, sender: Sender, adapter_name: String) -> bluer::Result<()> { 246 | let adapter = bluer::Session::new().await?.adapter(adapter_name.as_str())?; 247 | let device = adapter.device(address)?; 248 | 249 | let title = "Remove Device?".to_string(); 250 | let subtitle = "Are you sure you want to remove `".to_string() + &device.alias().await? + "`?"; 251 | let confirm = "Remove".to_string(); 252 | 253 | unsafe{ 254 | DISPLAYING_DIALOG = true; 255 | } 256 | sender.send(Message::RequestYesNo(title, subtitle, confirm, adw::ResponseAppearance::Destructive)).expect("can't send message"); 257 | 258 | wait_for_dialog_exit().await; 259 | 260 | let confirmed = unsafe { 261 | CONFIRMATION_AUTHORIZATION 262 | }; 263 | 264 | if confirmed { 265 | println!("removing device..."); 266 | let name = device.alias().await?; 267 | adapter.remove_device(address).await?; 268 | unsafe { 269 | let mut devices_lut = DEVICES_LUT.clone().unwrap(); 270 | if devices_lut.contains_key(&address) { 271 | devices_lut.remove(&address); 272 | DEVICES_LUT = Some(devices_lut); 273 | } 274 | } 275 | 276 | sender.send(Message::RemoveDevice(name, address)).expect("can't send message"); 277 | sender.send(Message::UpdateListBoxImage()).expect("can't send message"); 278 | } 279 | 280 | Ok(()) 281 | } 282 | 283 | pub async fn has_service(service: bluer::Uuid, device: bluer::Device) -> bluer::Result<()> { 284 | if device.uuids().await?.unwrap_or_default().contains(&service) { 285 | return Ok(()); 286 | } 287 | 288 | Err(bluer::Error { kind: bluer::ErrorKind::DoesNotExist, message: "wanted service doesn't exist.".to_string()}) 289 | } 290 | 291 | #[tokio::main] 292 | pub async fn stop_searching() { 293 | unsafe { 294 | if let Some(token) = CANCELLATION_TOKEN.clone() { 295 | token.cancel(); 296 | } 297 | } 298 | } 299 | 300 | 301 | #[tokio::main] 302 | pub async fn get_devices_continuous(sender: Sender, adapter_name: String) -> bluer::Result<()> { 303 | let session = bluer::Session::new().await?; 304 | let adapter = &session.adapter(adapter_name.as_str())?; 305 | 306 | let filter = bluer::DiscoveryFilter { 307 | transport: bluer::DiscoveryTransport::Auto, 308 | ..Default::default() 309 | }; 310 | adapter.set_discovery_filter(filter).await?; 311 | 312 | let device_events = adapter.discover_devices().await?; 313 | pin_mut!(device_events); 314 | 315 | let mut all_change_events = SelectAll::new(); 316 | 317 | let sender_clone = sender.clone(); 318 | 319 | let cancellation_token = CancellationToken::new(); 320 | unsafe { 321 | CANCELLATION_TOKEN = Some(cancellation_token.clone()); 322 | } 323 | 324 | while adapter.is_powered().await? { 325 | tokio::select! { 326 | Some(device_event) = device_events.next() => { 327 | match device_event { 328 | AdapterEvent::DeviceAdded(addr) => { 329 | if adapter.is_powered().await? { 330 | let supposed_device = adapter.device(addr); 331 | 332 | let devices_lut = unsafe { 333 | DEVICES_LUT.clone().unwrap() 334 | }; 335 | 336 | if !devices_lut.contains_key(&addr) { 337 | if let Ok(added_device) = supposed_device { 338 | sender.send(Message::AddRow(added_device)).expect("cannot send message {}"); 339 | sender.send(Message::UpdateListBoxImage()).expect("cannot send message {}"); 340 | //println!("supposedly sent"); 341 | 342 | let device = adapter.device(addr)?; 343 | let change_events = device.events().await?.map(move |evt| (addr, evt)); 344 | all_change_events.push(change_events); 345 | } 346 | else { 347 | println!("device isn't present, something went wrong."); 348 | } 349 | } 350 | else { 351 | println!("device already exists, not adding again."); 352 | } 353 | } 354 | } 355 | AdapterEvent::DeviceRemoved(addr) => { 356 | if adapter.is_powered().await? { 357 | let mut devices_lut = unsafe { 358 | DEVICES_LUT.clone().unwrap() 359 | }; 360 | 361 | let device_name = if devices_lut.contains_key(&addr) { 362 | let lut = devices_lut.get(&addr).unwrap().clone(); 363 | unsafe { 364 | devices_lut.remove(&addr); 365 | DEVICES_LUT = Some(devices_lut); 366 | } 367 | 368 | lut 369 | } 370 | else { 371 | String::new() 372 | }; 373 | 374 | sender_clone.send(Message::RemoveDevice(device_name.clone(), addr)).expect("cannot send message"); 375 | sender_clone.send(Message::UpdateListBoxImage()).expect("cannot send message"); 376 | println!("Device removed: {:?} {}\n", addr, device_name.clone()); 377 | } 378 | }, 379 | AdapterEvent::PropertyChanged(AdapterProperty::Powered(powered)) => { 380 | std::thread::sleep(std::time::Duration::from_secs_f32(0.5)); 381 | sender_clone.send(Message::SwitchAdapterPowered(powered)).expect("cannot send message {}"); 382 | println!("powered switch to {}", powered); 383 | }, 384 | AdapterEvent::PropertyChanged(AdapterProperty::Discoverable(discoverable)) => { 385 | std::thread::sleep(std::time::Duration::from_secs_f32(0.5)); 386 | sender_clone.send(Message::SwitchAdapterDiscoverable(discoverable)).expect("cannot send message {}"); 387 | println!("discoverable switch to {}", discoverable); 388 | }, 389 | AdapterEvent::PropertyChanged(AdapterProperty::Alias(alias)) => { 390 | std::thread::sleep(std::time::Duration::from_secs_f32(0.5)); 391 | sender_clone.send(Message::SwitchAdapterName(alias.clone(), alias.clone())).expect("cannot send message {}"); 392 | }, 393 | event => { 394 | println!("unhandled adapter event: {:?}", event); 395 | } 396 | } 397 | } 398 | Some((addr, DeviceEvent::PropertyChanged(property))) = all_change_events.next() => { 399 | match property { 400 | DeviceProperty::Connected(connected) => { 401 | let current_address = unsafe { 402 | CURRENT_ADDRESS 403 | }; 404 | 405 | std::thread::sleep(std::time::Duration::from_secs_f32(0.5)); 406 | sender_clone.send(Message::SwitchActive(connected, addr, addr == current_address)).expect("cannot send message"); 407 | }, 408 | DeviceProperty::Trusted(trusted) => { 409 | let current_address = unsafe { 410 | CURRENT_ADDRESS 411 | }; 412 | 413 | if addr == current_address { 414 | std::thread::sleep(std::time::Duration::from_secs_f32(0.5)); 415 | sender_clone.send(Message::SwitchTrusted(trusted)).expect("cannot send message"); 416 | } 417 | }, 418 | DeviceProperty::Blocked(blocked) => { 419 | let current_address = unsafe { 420 | CURRENT_ADDRESS 421 | }; 422 | 423 | if addr == current_address { 424 | std::thread::sleep(std::time::Duration::from_secs_f32(0.5)); 425 | sender_clone.send(Message::SwitchBlocked(blocked)).expect("cannot send message"); 426 | } 427 | }, 428 | DeviceProperty::Alias(name) => { 429 | let current_address = unsafe { 430 | CURRENT_ADDRESS 431 | }; 432 | 433 | if addr == current_address { 434 | std::thread::sleep(std::time::Duration::from_secs_f32(0.01)); 435 | sender_clone.send(Message::SwitchName(name.clone(), None, addr)).expect("cannot send message"); 436 | sender_clone.send(Message::SwitchPage(Some(name.clone()), None)).expect("cannot send message"); 437 | } 438 | else { 439 | let hashmap = unsafe { 440 | DEVICES_LUT.clone().unwrap() 441 | }; 442 | 443 | let empty = String::new(); 444 | let old_alias = hashmap.get(&addr).unwrap_or(&empty); 445 | 446 | sender_clone.send(Message::SwitchName(name.clone(), Some(old_alias.to_string()), addr)).expect("cannot send message"); 447 | } 448 | }, 449 | DeviceProperty::Icon(icon) => { 450 | let current_address = unsafe { 451 | CURRENT_ADDRESS 452 | }; 453 | 454 | if addr == current_address { 455 | std::thread::sleep(std::time::Duration::from_secs_f32(0.5)); 456 | sender_clone.send(Message::SwitchPage(None, Some(icon))).expect("cannot send message"); 457 | } 458 | }, 459 | DeviceProperty::Rssi(rssi) => { 460 | let device = unsafe { 461 | DEVICES_LUT.clone().unwrap().get(&addr).unwrap_or(&"Unknown Device".to_string()).to_string() 462 | }; 463 | sender_clone.send(Message::SwitchRssi(device, rssi as i32)).expect("cannot send message"); 464 | sender_clone.send(Message::InvalidateSort()).expect("cannot send message"); 465 | }, 466 | event => { 467 | println!("unhandled device event: {:?}", event); 468 | }, 469 | } 470 | } 471 | _ = cancellation_token.cancelled() => { 472 | // println!("exited loop from refresh"); 473 | break; 474 | } 475 | else => break 476 | } 477 | 478 | // if cancellation_token.is_cancelled() { 479 | // break; 480 | // } 481 | } 482 | 483 | println!("exited loop"); 484 | // drop(agent); 485 | if cancellation_token.is_cancelled() { 486 | Ok(()) 487 | } 488 | else { 489 | Err(bluer::Error { kind: bluer::ErrorKind::Failed, message: "Stopped searching for devices".to_string() }) 490 | } 491 | } 492 | 493 | #[tokio::main] 494 | pub async fn get_more_info(address: bluer::Address, adapter_name: String) -> bluer::Result<(String, String, String, String, String, Vec)> { 495 | let session = bluer::Session::new().await?; 496 | let adapter = &session.adapter(&adapter_name)?; 497 | 498 | let device = adapter.device(address)?; 499 | 500 | let name = device.alias().await?; 501 | let device_type = device.icon().await?.unwrap_or("Unknown".to_string()); 502 | let mut services_list = vec![]; 503 | 504 | let distance = async { 505 | // factor for if indoors outside etc, between 2 to 4 506 | let n = 3; 507 | let measured = device.tx_power().await?; 508 | let rssi = device.rssi().await?; 509 | 510 | println!("{:?} {:?}", measured, rssi); 511 | 512 | if rssi.is_none() { 513 | return bluer::Result::from(Ok("Unknown".to_string())); 514 | } 515 | 516 | // the -59 is an average fallback case (closest to current device) 517 | let ratio = (measured.unwrap_or(-59) - rssi.unwrap()) as f32; 518 | 519 | // basically reverse the logarithmic way or calculate TX power to get the distance 520 | // it is absolute fuckery and i have no idea how the hell anyone would come up with this but it works fairly well 521 | let dist = 10f32.powf(ratio / (10.0 * n as f32)); 522 | 523 | Ok(format!("≈ {:.1$} meters", dist, 2)) 524 | 525 | 526 | // needs testing but this may be more accurate???? 527 | // var ratio = rssi*1.0/txPower; 528 | // if (ratio < 1.0) { 529 | // return Math.pow(ratio,10); 530 | // } 531 | // else { 532 | // var distance = (0.89976)*Math.pow(ratio,7.7095) + 0.111; 533 | // return distance; 534 | // } 535 | }.await?; 536 | 537 | for uuid in device.uuids().await?.unwrap() { 538 | let service = services::get_name_from_service(uuid).unwrap_or("".to_string()); 539 | 540 | if !service.is_empty() { 541 | services_list.push(service); 542 | } 543 | } 544 | 545 | let mut manufacturer = String::from("Unknown"); 546 | 547 | if let Some(info) = device.manufacturer_data().await? { 548 | println!("{:?}", info); 549 | 550 | for key in info.keys() { 551 | manufacturer = match bluer::id::Manufacturer::try_from(*key) { 552 | Ok(val) => { 553 | val.to_string() 554 | }, 555 | Err(_) => { 556 | "Unknown".to_string() 557 | } 558 | }; 559 | } 560 | } 561 | 562 | Ok((name, address.to_string(), manufacturer, device_type, distance, services_list)) 563 | } 564 | -------------------------------------------------------------------------------- /src/bluetooth/message.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | pub enum Message { 4 | #[allow(dead_code)] 5 | /// Changes the trusted switch's active to `bool` 6 | SwitchTrusted(bool), 7 | /// Changes the blocked switch's active to `bool` 8 | SwitchBlocked(bool), 9 | /// Changes the connected switch's active to `bool` if the `is_current` is true, and sets the corresponding device action row's connected state 10 | SwitchActive(bool, bluer::Address, bool), 11 | /// Changes the connected switch's spinner spinning state to `bool` 12 | SwitchActiveSpinner(bool), 13 | /// Changes the active devices's name to [alias](String) if no [old_alias](Option) is provided, otherwise it looks for a matching row 14 | SwitchName(String, Option, bluer::Address), 15 | /// Changes the supplied device's (`name: String`) RSSI to the supplied value (`rssi: i32`) 16 | SwitchRssi(String, i32), 17 | /// Moves between pages, ie changes the values of the rows and icons to `page: Option` and `icon: Option` 18 | SwitchPage(Option, Option), 19 | /// Removes the device matching the supplied name (`name: String`) 20 | RemoveDevice(String, bluer::Address), 21 | /// Adds a new device from the properties of [device](bluer::Device) 22 | AddRow(bluer::Device), 23 | /// Changes the adapter's powered state to `bool` 24 | SwitchAdapterPowered(bool), 25 | /// Changes the adapter's discoverable timeout to [timeout](u32) 26 | SwitchAdapterTimeout(u32), 27 | /// Changes the adapter's discoverable state to `bool` 28 | SwitchAdapterDiscoverable(bool), 29 | /// Changes the adapter's name to [alias](String), taking in [old_alias](String) for reference 30 | SwitchAdapterName(String, String), 31 | /// Adds all currently discovered adapters from [adapters_lut](HashMap) to the adapter list in the settings 32 | PopulateAdapterExpander(HashMap), 33 | /// Displays an error (or a message) of [message](String) with a [priority](adw::ToastPriority) as a [toast](adw::Toast) 34 | PopupError(String, adw::ToastPriority), 35 | /// Checks if there are devices and changes the "no bluetooth devices found" image accordingly 36 | UpdateListBoxImage(), 37 | /// Requests a pairing pincode using [request](bluer::agent::RequestPinCode) as input 38 | RequestPinCode(bluer::agent::RequestPinCode), 39 | /// Displays a pairing pincode using [request](bluer::agent::DisplayPinCode) as input 40 | DisplayPinCode(bluer::agent::DisplayPinCode), 41 | /// Requests a pairing passkey using [request](bluer::agent::RequestPasskey) as input 42 | RequestPassKey(bluer::agent::RequestPasskey), 43 | /// Displays a pairing passkey using [request](bluer::agent::RequestPasskey) as input 44 | DisplayPassKey(bluer::agent::DisplayPasskey), 45 | /// Requests pairing confirmation using [request](bluer::agent::RequestConfirmation) as input 46 | RequestConfirmation(bluer::agent::RequestConfirmation), 47 | /// Requests pairing authorization using [request](bluer::agent::RequestAuthorization) as input 48 | RequestAuthorization(bluer::agent::RequestAuthorization), 49 | /// Requests service authorization using [request](bluer::agent::AuthorizeService) as input 50 | AuthorizeService(bluer::agent::AuthorizeService), 51 | /// Goes the the settings page or the last device depending on `bool` 52 | GoToBluetoothSettings(bool), 53 | /// Gets a `yes/no` answer from a dialog 54 | /// ### Arguments 55 | /// * `title` - a short [String](String) describing the request 56 | /// * `subtitle` - a [String](String) describing the request in more detail 57 | /// * `confirm name` - a [String](String) for the name of the confirmation option 58 | /// * `response type` - a [Response Type](adw::ResponseAppearance) detailing if the response is destructive, suggested, etc 59 | RequestYesNo(String, String, String, adw::ResponseAppearance), 60 | /// Invalidates the device list's sorting, forcing it to resort the devices according to various factors 61 | InvalidateSort(), 62 | /// Forcefully refreshes the device list (needs more work) 63 | RefreshDevicesList(), 64 | /// Starts a new transfer, displaying a progress bar popover with the filename 65 | /// ### Arguments 66 | /// * `transfer` - a [String](String) containing the transfer object 67 | /// * `filename` - a [String](String) ...which is the filename 68 | /// * `percent` - a [f32](f32) the starting completion percent (like 45 **not** 0.45) 69 | /// * `current mb` - a [f32](f32) the current transferred megabytes 70 | /// * `filesize` - a [f32](f32) the total filesize in megabytes 71 | /// * `outbound` - a [bool](bool) indicating if the transfer is sending or receiving 72 | StartTransfer(String, String, f32, f32, f32, bool), 73 | /// Updates the transfer's progression based on: 74 | /// ### Arguments 75 | /// * `transfer` - a [String](String) containing the transfer object 76 | /// * `filename` - a [String](String) containing the file name 77 | /// * `current mb` - a [String](String) the current transferred megabytes 78 | /// * `status` - a [String](String) which is the current status of the transfer (ie: complete, error, active...) 79 | UpdateTransfer(String, String, f32, u64, String), 80 | /// Removes a transfer via the supplied [transfer](String) object and the [filename](String) incase of multiple files in same transfer 81 | RemoveTransfer(String, String), 82 | /// Gets the path of a selected file or folder, based on [filetype](gtk::FileChooserAction) 83 | GetFile(gtk::FileChooserAction), 84 | /// Sets the sensitive state of the send file row, aka if it is interactable 85 | SwitchSendFileActive(bool), 86 | /// Sets the new [file storage location](String), doing some checks along the way 87 | SetFileStorageLocation(String), 88 | /// Changes whether the current device has obex capabilities or not 89 | SwitchHasObexService(bool), 90 | /// Sets the "valid" state of the device name 91 | SetNameValid(bool), 92 | /// Adds the given profiles to the profiles list selector for a device 93 | PopulateAudioProfilesList(HashMap), 94 | /// Toggles the audio profiles list according to the given `bool` 95 | SwitchAudioProfilesList(bool), 96 | /// Sets the active audio profile in the expander list, deselecting everything else 97 | SetActiveAudioProfile(String), 98 | /// Switches the "expanded" state of the audio profile expander 99 | SwitchAudioProfileExpanded(bool), 100 | /// Updates the `LeverBar` of the device to match the battery level reported 101 | UpdateBatteryLevel(i8), 102 | /// Sets the "hide unknown devices" settings according to the given `bool` 103 | SetHideUnknownDevices(bool) 104 | } 105 | -------------------------------------------------------------------------------- /src/bluetooth/services.rs: -------------------------------------------------------------------------------- 1 | use phf::phf_map; 2 | use bluer::Uuid; 3 | 4 | /// i don't like how the services are called in bluer 5 | const SERVICES: phf::Map<&'static str, &'static str> = phf_map! { 6 | "00001203-0000-1000-8000-00805f9b34fb" => "Generic Audio", 7 | "00001108-0000-1000-8000-00805f9b34fb" => "Hands Free", 8 | "0000111e-0000-1000-8000-00805f9b34fb" => "Hands Free Headset", 9 | "00001112-0000-1000-8000-00805f9b34fb" => "Hands Free Audio Gateway", 10 | "0000111f-0000-1000-8000-00805f9b34fb" => "Hands Free Audio Gateway", 11 | "0000110d-0000-1000-8000-00805f9b34fb" => "Advanced Audio", 12 | "0000110a-0000-1000-8000-00805f9b34fb" => "A2DP Source", 13 | "0000110b-0000-1000-8000-00805f9b34fb" => "A2DP Sink", 14 | "0000110e-0000-1000-8000-00805f9b34fb" => "Audio / Video Remote Control", 15 | "0000110c-0000-1000-8000-00805f9b34fb" => "Audio / Video Remote Control Target", 16 | "00001115-0000-1000-8000-00805f9b34fb" => "Personal Area Networking User", 17 | "00001116-0000-1000-8000-00805f9b34fb" => "Network Access Point", 18 | "00001117-0000-1000-8000-00805f9b34fb" => "Group ad-hoc Network", 19 | "0000000f-0000-1000-8000-00805f9b34fb" => "Bluetooth Network Encapsulation Protocol", 20 | "00002a50-0000-1000-8000-00805f9b34fb" => "Part Number and Product ID", 21 | "0000180a-0000-1000-8000-00805f9b34fb" => "Device Information", 22 | "00001801-0000-1000-8000-00805f9b34fb" => "Generic Attribute", 23 | "00001802-0000-1000-8000-00805f9b34fb" => "Immediate Alert", 24 | "00001803-0000-1000-8000-00805f9b34fb" => "Link Loss", 25 | "00001804-0000-1000-8000-00805f9b34fb" => "Transmit Power", 26 | "0000112D-0000-1000-8000-00805f9b34fb" => "SIM Access", 27 | "0000180d-0000-1000-8000-00805f9b34fb" => "Heart Rate", 28 | "00002a37-0000-1000-8000-00805f9b34fb" => "Heart Rate Measurement", 29 | "00002a38-0000-1000-8000-00805f9b34fb" => "Body Sensor Location", 30 | "00002a39-0000-1000-8000-00805f9b34fb" => "Heart Rate Control Point", 31 | "00001809-0000-1000-8000-00805f9b34fb" => "Health Thermometer", 32 | "00002a1c-0000-1000-8000-00805f9b34fb" => "Temperature Measurement", 33 | "00002a1d-0000-1000-8000-00805f9b34fb" => "Temperature Type", 34 | "00002a1e-0000-1000-8000-00805f9b34fb" => "Intermediate Temperature", 35 | "00002a21-0000-1000-8000-00805f9b34fb" => "Measurement Interval", 36 | "00001816-0000-1000-8000-00805f9b34fb" => "Cycling Speed and Cadence", 37 | "00002a5b-0000-1000-8000-00805f9b34fb" => "Cycling Speed and Cadence Measurement", 38 | "00002a5c-0000-1000-8000-00805f9b34fb" => "Cycling Speed and Cadence Feature", 39 | "00002a5d-0000-1000-8000-00805f9b34fb" => "Sensor Location", 40 | "00002a55-0000-1000-8000-00805f9b34fb" => "Speed and Cadence Control Point", 41 | "00000003-0000-1000-8000-00805f9b34fb" => "Serial port transport protocol (rfcomm)", 42 | "00001400-0000-1000-8000-00805f9b34fb" => "Health Device", 43 | "00001401-0000-1000-8000-00805f9b34fb" => "Health Device Source", 44 | "00001402-0000-1000-8000-00805f9b34fb" => "Health Device Sink", 45 | "00001124-0000-1000-8000-00805f9b34fb" => "Human Interface Device", 46 | "00001103-0000-1000-8000-00805f9b34fb" => "Dial-Up Networking Gateway", 47 | "00001800-0000-1000-8000-00805f9b34fb" => "Generic Access", 48 | "00001200-0000-1000-8000-00805f9b34fb" => "Plug and Play", 49 | "00001101-0000-1000-8000-00805f9b34fb" => "Serial Port", 50 | "00001104-0000-1000-8000-00805f9b34fb" => "Obex Sync", 51 | "00001105-0000-1000-8000-00805f9b34fb" => "Obex Object Push", 52 | "00001106-0000-1000-8000-00805f9b34fb" => "Obex File Transfer Protocol", 53 | "0000112e-0000-1000-8000-00805f9b34fb" => "Phone Book Client Equipment", 54 | "0000112f-0000-1000-8000-00805f9b34fb" => "Phone Book Server Equipment", 55 | "00001130-0000-1000-8000-00805f9b34fb" => "Phone Book Access", 56 | "00001132-0000-1000-8000-00805f9b34fb" => "Message Access Service", 57 | "00001133-0000-1000-8000-00805f9b34fb" => "Message Notification Service", 58 | "00001134-0000-1000-8000-00805f9b34fb" => "Message Access", 59 | }; 60 | 61 | 62 | /// Gets the service name from a uuid, looks it up from a custom table 63 | pub fn get_name_from_service(service: Uuid) -> Result { 64 | let uuid_slice = service.to_string(); 65 | 66 | let name = SERVICES.get(uuid_slice.as_str()); 67 | if let Some(service_name) = name { 68 | Ok(service_name.to_string() + " Profile") 69 | } 70 | else { 71 | Err(bluer::Error { kind: bluer::ErrorKind::Failed, message: "Failed to get name from UUID".to_string() }) 72 | } 73 | } -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | pub static VERSION: &str = "0.6.1"; 2 | pub static GETTEXT_PACKAGE: &str = "overskride"; 3 | pub static LOCALEDIR: &str = "/usr/share/locale"; 4 | pub static PKGDATADIR: &str = "/usr/share/overskride"; 5 | -------------------------------------------------------------------------------- /src/config.rs.in: -------------------------------------------------------------------------------- 1 | pub static VERSION: &str = @VERSION@; 2 | pub static GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@; 3 | pub static LOCALEDIR: &str = @LOCALEDIR@; 4 | pub static PKGDATADIR: &str = @PKGDATADIR@; 5 | -------------------------------------------------------------------------------- /src/gtk/battery-indicator.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $BatteryLevelIndicator : Adw.PreferencesRow { 5 | activatable: false; 6 | tooltip-text: "this is an approximation"; 7 | Box { 8 | orientation: vertical; 9 | halign: fill; 10 | valign: center; 11 | //spacing: 14; 12 | // margin-bottom: 24; 13 | 14 | Label battery_label { 15 | halign: start; 16 | margin-start: 14; 17 | margin-top: 8; 18 | label: _("Battery: 45%"); 19 | } 20 | 21 | LevelBar level_bar { 22 | mode: continuous; 23 | margin-start:14; 24 | margin-end:14; 25 | margin-bottom: 14; 26 | margin-top: 8; 27 | min-value:0; 28 | max-value:100; 29 | value: 45; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/gtk/connected-switch-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $ConnectedSwitchRow : Adw.ActionRow { 5 | activatable: true; 6 | activatable-widget: switch; 7 | 8 | styles [ "destructive-action" ] 9 | 10 | [suffix] 11 | Box { 12 | Spinner spinner { 13 | // spinning: true; 14 | margin-end: 12; 15 | } 16 | 17 | Switch switch { 18 | // active: true; 19 | valign: center; 20 | action-name: "toggle_row_active"; 21 | //can-focus: false; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/gtk/device-action-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $DeviceActionRow : Adw.ActionRow { 5 | activatable: true; 6 | 7 | [suffix] 8 | Box { 9 | spacing: 10; 10 | 11 | Image connected_icon { 12 | pixel-size: 18; 13 | margin-bottom: 2; 14 | icon-name: "chain-link-symbolic"; 15 | } 16 | 17 | Image rssi_icon { 18 | icon-name: "rssi-none-symbolic"; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/gtk/help-overlay.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | ShortcutsWindow help_overlay { 4 | modal: true; 5 | 6 | ShortcutsSection { 7 | section-name: "shortcuts"; 8 | max-height: 10; 9 | 10 | ShortcutsGroup { 11 | title: C_("shortcut window", "General"); 12 | 13 | ShortcutsShortcut { 14 | title: C_("shortcut window", "Show Shortcuts"); 15 | action-name: "win.show-help-overlay"; 16 | } 17 | 18 | ShortcutsShortcut { 19 | title: C_("shortcut window", "Quit"); 20 | action-name: "app.quit"; 21 | } 22 | 23 | ShortcutsShortcut { 24 | title: C_("shortcut window", "Refresh Devices List"); 25 | action-name: "win.refresh-devices"; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/gtk/more-info-page.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $MoreInfoPage : Adw.ApplicationWindow { 5 | height-request: 350; 6 | width-request: 400; 7 | 8 | Adw.ToolbarView { 9 | [top] 10 | Adw.HeaderBar { 11 | title-widget: Adw.WindowTitle { 12 | title: "More Info"; 13 | }; 14 | } 15 | 16 | content: ScrolledWindow { 17 | propagate-natural-height: true; 18 | 19 | Box { 20 | valign: center; 21 | orientation: vertical; 22 | //spacing: 28; 23 | 24 | 25 | ListBox { 26 | selection-mode: none; 27 | 28 | Adw.ActionRow name_row { 29 | title-selectable: true; 30 | } 31 | Adw.ActionRow address_row { 32 | title-selectable: true; 33 | } 34 | Adw.ActionRow manufacturer_row { 35 | title-selectable: true; 36 | } 37 | Adw.ActionRow type_row { 38 | title-selectable: true; 39 | } 40 | Adw.ActionRow distance_row { 41 | title-selectable: true; 42 | tooltip-text: "this is an approximation, depends on signal strength"; 43 | } 44 | Adw.ExpanderRow services_row { 45 | title-selectable: true; 46 | } 47 | } 48 | } 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/gtk/preferences-page.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | Adw.PreferencesWindow pref_window { 5 | default-width: 600; 6 | default-height: 500; 7 | title: _("Preferences"); 8 | 9 | Adw.PreferencesPage { 10 | title: _("Behavior"); 11 | icon-name: "settings-symbolic"; 12 | 13 | Adw.PreferencesGroup { 14 | title: _("Sharing Settings"); 15 | description: _("Change how the app behaves during file transfers."); 16 | 17 | Adw.SwitchRow auto_accept_after_first_row { 18 | title: _("Auto Accept After First File"); 19 | subtitle: _("when receiving files, auto accept every file after the first one"); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/gtk/receiving-popover.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $ReceivingPopover : Popover { 5 | ScrolledWindow { 6 | propagate-natural-height: true; 7 | propagate-natural-width: true; 8 | max-content-height: 280; 9 | hscrollbar-policy: never; 10 | 11 | ListBox listbox { 12 | selection-mode: none; 13 | margin-top: 6; 14 | margin-bottom: 6; 15 | margin-start: 6; 16 | margin-end: 6; 17 | activate-on-single-click: false; 18 | // show-separators: true; 19 | styles ["operations-list"] 20 | 21 | ListBoxRow default_row { 22 | // visible: false; 23 | width-request: 380; 24 | height-request: 55; 25 | 26 | [center] 27 | Label { 28 | valign: center; 29 | justify: center; 30 | use-markup: true; 31 | label: "No Transactions Ongoing"; 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/gtk/receiving-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | 3 | template $ReceivingRow : ListBoxRow { 4 | Box { 5 | CenterBox { 6 | orientation: vertical; 7 | valign: fill; 8 | halign: fill; 9 | width-request: 380; 10 | height-request: 55; 11 | 12 | start-widget: Label title_label { 13 | halign: start; 14 | label: "“this is a video.mp4”"; 15 | ellipsize: end; 16 | max-width-chars: 30; 17 | }; 18 | 19 | 20 | center-widget: ProgressBar progress_bar { 21 | halign: start; 22 | width-request: 360; 23 | fraction: 0.45; 24 | }; 25 | 26 | end-widget: Label extra_label { 27 | halign: start; 28 | use-markup: true; 29 | label: "45% | 13/423 MB"; 30 | styles ["dim-label"] 31 | }; 32 | } 33 | Box { 34 | Button cancel_button { 35 | styles ["circular"] 36 | valign: center; 37 | icon-name: "cross-large-symbolic"; 38 | clicked => $cancel_transfer() swapped; 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/gtk/selectable-row.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $SelectableRow : Adw.ActionRow { 5 | activatable: true; 6 | 7 | [suffix] 8 | Box { 9 | spacing: 16; 10 | 11 | Image check_icon { 12 | icon-name: "check-plain-symbolic"; 13 | visible: false; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/gtk/startup-error-message.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $StartupErrorMessage : Adw.ApplicationWindow { 5 | height-request: 400; 6 | width-request: 400; 7 | 8 | Adw.ToastOverlay error_toast_overlay { 9 | Adw.ToolbarView { 10 | [top] 11 | Adw.HeaderBar { 12 | title-widget: Adw.WindowTitle { 13 | title: "Error"; 14 | }; 15 | } 16 | 17 | content: Box { 18 | valign: center; 19 | orientation: vertical; 20 | //spacing: 28; 21 | 22 | Adw.StatusPage { 23 | valign: start; 24 | icon-name: "heart-broken-symbolic"; 25 | title: "An Error Occurred"; 26 | description: "this usually happens when the bluetooth service is disabled"; 27 | 28 | Button run_enable_bluetooth_button { 29 | //orientation: vertical; 30 | styles ["card"] 31 | valign: center; 32 | halign: center; 33 | 34 | Box { 35 | orientation: vertical; 36 | margin-top: 16; 37 | margin-bottom: 16; 38 | margin-start: 16; 39 | margin-end: 16; 40 | 41 | Label { 42 | label: "in order to fix it, you could try:"; 43 | use-markup: true; 44 | justify: center; 45 | } 46 | Label { 47 | margin-top: 4; 48 | margin-bottom: 4; 49 | 50 | label: "`sudo systemctl enable --now bluetooth`\n`sudo systemctl start bluetooth`"; 51 | use-markup: true; 52 | justify: center; 53 | } 54 | Label { 55 | label: "or install the bluez package for your distro\n and run the above commands, then restart Overskride"; 56 | use-markup: true; 57 | justify: center; 58 | } 59 | } 60 | } 61 | } 62 | }; 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/gtk/style.css: -------------------------------------------------------------------------------- 1 | .operations-list, 2 | .operations-list > :hover, 3 | .operations-list > :active { 4 | background: none; 5 | } 6 | 7 | levelbar block.full { 8 | background-color: #78aeed; 9 | } 10 | 11 | levelbar block.three-quarters { 12 | background-color: #3584e4; 13 | } 14 | 15 | levelbar block.half { 16 | background-color: #cd9309; 17 | } 18 | 19 | levelbar block.low { 20 | background-color: #c01c28; 21 | } 22 | -------------------------------------------------------------------------------- /src/gtk/window.blp: -------------------------------------------------------------------------------- 1 | using Gtk 4.0; 2 | using Adw 1; 3 | 4 | template $OverskrideWindow: Adw.ApplicationWindow { 5 | width-request: 475; 6 | height-request: 575; 7 | 8 | Adw.Breakpoint breakpoint { 9 | condition ("max-width: 700sp") 10 | setters { 11 | split_view.collapsed: true; 12 | split_view.show-sidebar: false; 13 | //headerbar.show-title: false; 14 | //navigation_page.title: "Device Settings"; 15 | show_sidebar_button.active: false; 16 | } 17 | } 18 | 19 | content: Adw.ToastOverlay toast_overlay { 20 | child: Adw.OverlaySplitView split_view { 21 | pin-sidebar: false; 22 | show-sidebar: bind show_sidebar_button.active; 23 | enable-hide-gesture: true; 24 | enable-show-gesture: true; 25 | halign: fill; 26 | valign: fill; 27 | sidebar-position: start; 28 | sidebar-width-fraction: 0.4; 29 | sidebar-width-unit: sp; 30 | min-sidebar-width: 300; 31 | max-sidebar-width: 350; 32 | styles ["view"] 33 | 34 | 35 | sidebar: Adw.ToolbarView toolbar_view { 36 | top-bar-style: flat; 37 | 38 | [top] 39 | Adw.HeaderBar { 40 | title-widget: Adw.WindowTitle title { 41 | halign: center; 42 | margin-start: 24; 43 | title: "Overskride"; 44 | }; 45 | 46 | [start] 47 | MenuButton show_popup_button { 48 | always-show-arrow: false; 49 | icon-name: "folder-download-symbolic"; 50 | popover: $ReceivingPopover receiving_popover {}; 51 | styles ["flat"] 52 | } 53 | } 54 | content: Box sidebar_content_box { 55 | //title: _("Sidebar"); 56 | styles ["compact"] 57 | margin-top: 12; 58 | margin-bottom: 12; 59 | margin-start: 12; 60 | margin-end: 12; 61 | 62 | Box { 63 | //width-request: 350; 64 | valign: fill; 65 | orientation: vertical; 66 | spacing: 28; 67 | 68 | Adw.PreferencesGroup { 69 | title: "Settings"; 70 | description: "General Bluetooth Settings"; 71 | 72 | ListBox secondary_listbox { 73 | styles ["boxed-list"] 74 | Adw.ActionRow bluetooth_settings_row { 75 | title: "Bluetooth Settings"; 76 | activatable: true; 77 | } 78 | } 79 | } 80 | 81 | Adw.PreferencesGroup { 82 | title: "Devices"; 83 | description: "All the devices you've connected to"; 84 | 85 | Box listbox_image_box { 86 | height-request: 250; 87 | width-request: 250; 88 | orientation: vertical; 89 | visible: true; 90 | 91 | Frame { 92 | height-request: 250; 93 | 94 | child: Box { 95 | valign: center; 96 | halign: center; 97 | orientation: vertical; 98 | spacing: 20; 99 | 100 | Image { 101 | valign: center; 102 | halign: center; 103 | 104 | icon-name: "no-bluetooth-symbolic"; 105 | pixel-size: 52; 106 | opacity: 0.4; 107 | 108 | } 109 | 110 | Label { 111 | label: "No devices in range"; 112 | opacity: 0.4; 113 | } 114 | }; 115 | } 116 | } 117 | 118 | ScrolledWindow { 119 | propagate-natural-height: true; 120 | styles ["flat"] 121 | kinetic-scrolling: true; 122 | overlay-scrolling: true; 123 | 124 | [start] 125 | ListBox main_listbox { 126 | margin-top: 12; 127 | margin-bottom: 12; 128 | margin-start: 1; 129 | margin-end: 1; 130 | styles [ "boxed-list", "separators" ] 131 | valign: fill; 132 | // height-request: 250; 133 | visible: false; 134 | 135 | // Adw.ActionRow { 136 | // title: "Bluetooth Headphones"; 137 | // subtitle: "dummy, doesn't work"; 138 | // } 139 | // Adw.ActionRow { 140 | // title: "Mom's Phone"; 141 | // subtitle: "dummy, doesn't work"; 142 | // } 143 | // Adw.ActionRow { 144 | // title: "HP Laptop"; 145 | // subtitle: "dummy, doesn't work"; 146 | // } 147 | } 148 | } 149 | } 150 | } 151 | }; 152 | }; 153 | content: Adw.ToolbarView { 154 | [top] 155 | Adw.HeaderBar headerbar { 156 | title-widget: Adw.WindowTitle window_title { 157 | halign: center; 158 | //margin-start: 16; 159 | title: "Settings"; 160 | }; 161 | 162 | Box { 163 | ToggleButton show_sidebar_button { 164 | icon-name: "dock-left-symbolic"; 165 | active: true; 166 | tooltip-text: "Hide Sidebar"; 167 | } 168 | } 169 | 170 | [end] 171 | MenuButton menu_drop_down { 172 | icon-name: "open-menu-symbolic"; 173 | menu-model: primary_menu; 174 | tooltip-text: "Open Main Menu"; 175 | } 176 | } 177 | 178 | Stack main_stack { 179 | valign: start; 180 | halign: fill; 181 | transition-type: slide_left_right; 182 | 183 | StackPage device_settings_page { 184 | child: ScrolledWindow device_status_page { 185 | valign: start; 186 | propagate-natural-height: true; 187 | styles ["flat"] 188 | kinetic-scrolling: true; 189 | overlay-scrolling: true; 190 | margin-bottom: 40; 191 | 192 | Adw.Clamp { 193 | orientation: horizontal; 194 | unit: sp; 195 | maximum-size: 500; 196 | margin-top: 32; 197 | margin-bottom: 32; 198 | margin-start: 32; 199 | margin-end: 32; 200 | 201 | Box { 202 | orientation: vertical; 203 | valign: center; 204 | spacing: 18; 205 | 206 | Box { 207 | valign: start; 208 | halign: center; 209 | orientation: vertical; 210 | spacing: 20; 211 | 212 | Image device_icon { 213 | icon-name: "bluetooth-symbolic"; 214 | icon-size: large; 215 | pixel-size: 80; 216 | } 217 | Label device_title { 218 | label: "Bluetooth Settings"; 219 | use-markup: true; 220 | } 221 | } 222 | 223 | Adw.PreferencesGroup { 224 | title: "Connection Properties"; 225 | description: "Bluetooth connection information about this device."; 226 | 227 | $ConnectedSwitchRow connected_switch_row { 228 | title: "Connected"; 229 | } 230 | Adw.ExpanderRow audio_profile_expander { 231 | title: "Audio Profile"; 232 | show-enable-switch: true; 233 | sensitive: false; 234 | 235 | // Adw.ActionRow audio_profile_1 { 236 | // styles ["flat"] 237 | // title: "A2DP Sink"; 238 | // activatable: true; 239 | // } 240 | // Adw.ActionRow audio_profile_2 { 241 | // styles ["flat"] 242 | // title: "Handsfree HSP/HFP"; 243 | // activatable: true; 244 | // } 245 | } 246 | Adw.ActionRow send_file_row { 247 | title: "Send File To Device"; 248 | 249 | [suffix] 250 | Box { 251 | margin-top: 6; 252 | margin-bottom: 6; 253 | Button choose_file_button { 254 | label: "Choose File"; 255 | } 256 | } 257 | } 258 | } 259 | 260 | Adw.PreferencesGroup { 261 | title: "Device Properties"; 262 | description: "Information about this bluetooth device."; 263 | 264 | Adw.EntryRow device_name_entry { 265 | title: "Device Name"; 266 | text: "Bluetooth Headset"; 267 | input-purpose: alpha; 268 | show-apply-button: true; 269 | } 270 | Adw.SwitchRow trusted_row { 271 | title: "Trusted"; 272 | } 273 | Adw.SwitchRow blocked_row { 274 | title: "Blocked"; 275 | } 276 | } 277 | 278 | Adw.PreferencesGroup { 279 | title: "Status Information"; 280 | description: "The current state of this device"; 281 | 282 | $BatteryLevelIndicator battery_level_indicator { 283 | styles ["linked"] 284 | } 285 | 286 | Adw.ActionRow more_info_row { 287 | title: "More Info"; 288 | activatable: true; 289 | // sensitive: false; 290 | 291 | [suffix] 292 | Box { 293 | Image { 294 | // styles ["dim-label"] 295 | icon-name: "right-symbolic"; 296 | } 297 | } 298 | } 299 | } 300 | 301 | Adw.PreferencesGroup { 302 | Button remove_device_button { 303 | styles ["destructive-action"] 304 | label: "Remove Device"; 305 | } 306 | } 307 | } 308 | } 309 | }; 310 | } 311 | 312 | StackPage bluetooth_settings_page { 313 | child: ScrolledWindow bluetooth_status_page { 314 | propagate-natural-height: true; 315 | styles ["flat"] 316 | kinetic-scrolling: true; 317 | overlay-scrolling: true; 318 | valign: start; 319 | margin-bottom: 40; 320 | 321 | child: Adw.Clamp { 322 | orientation: horizontal; 323 | unit: sp; 324 | maximum-size: 500; 325 | margin-top: 32; 326 | margin-bottom: 32; 327 | margin-start: 32; 328 | margin-end: 32; 329 | 330 | Box { 331 | orientation: vertical; 332 | valign: start; 333 | spacing: 20; 334 | 335 | Box { 336 | valign: start; 337 | halign: center; 338 | orientation: vertical; 339 | spacing: 20; 340 | 341 | Image { 342 | icon-name: "bluetooth-symbolic"; 343 | icon-size: large; 344 | pixel-size: 80; 345 | } 346 | Label { 347 | label: "Bluetooth Settings"; 348 | use-markup: true; 349 | } 350 | } 351 | 352 | Adw.PreferencesGroup { 353 | title: "Bluetooth Adapter Status"; 354 | description: "What's this adapter doing?"; 355 | 356 | Adw.SwitchRow powered_switch_row { 357 | title: "Powered"; 358 | } 359 | 360 | Adw.SwitchRow discoverable_switch_row { 361 | title: "Discoverable"; 362 | subtitle: "visible to others?"; 363 | } 364 | } 365 | 366 | Adw.PreferencesGroup { 367 | title: "Adapter Properties"; 368 | description: "Information about the current bluetooth adapter."; 369 | 370 | Adw.ExpanderRow default_controller_expander { 371 | title: "Current Bluetooth Adapter"; 372 | show-enable-switch: false; 373 | // sensitive: false; 374 | 375 | Adw.ActionRow adapter_1 { 376 | styles ["flat"] 377 | 378 | title: "Bluetooth Dongle"; 379 | activatable: true; 380 | 381 | [suffix] 382 | Box { 383 | Image { 384 | icon-name: "check-plain-symbolic"; 385 | } 386 | } 387 | } 388 | 389 | Adw.ActionRow adapter_2 { 390 | styles ["flat"] 391 | 392 | title: "Integrated Controller"; 393 | activatable: true; 394 | } 395 | } 396 | 397 | Adw.SpinRow timeout_row { 398 | title: "Discoverable Timeout"; 399 | enable-undo: true; 400 | subtitle: "in minutes"; 401 | climb-rate: 100; 402 | wrap: true; 403 | adjustment: timeout_time_adjustment; 404 | } 405 | 406 | Adw.EntryRow adapter_name_entry { 407 | title: "Adapter Name"; 408 | text: "Bluetooth Dongle"; 409 | input-purpose: alpha; 410 | show-apply-button: true; 411 | } 412 | } 413 | 414 | Adw.PreferencesGroup { 415 | title: "System Settings"; 416 | description: "Manage how your system is set up."; 417 | 418 | Adw.SwitchRow auto_accept_trusted_row { 419 | title: "Auto Accept"; 420 | subtitle: "auto accept files from trusted devices?"; 421 | // sensitive: false; 422 | } 423 | 424 | Adw.EntryRow file_save_location { 425 | title: "Received Files Location"; 426 | text: "/home/$USER/Downloads/Bluetooth/"; 427 | show-apply-button: true; 428 | // sensitive: false; 429 | 430 | [suffix] 431 | Box { 432 | Button choose_location_button { 433 | icon-name: "folder-symbolic"; 434 | margin-bottom: 8; 435 | margin-top: 8; 436 | margin-end: 8; 437 | margin-start: 8; 438 | } 439 | } 440 | } 441 | Adw.SwitchRow hide_unknowns_switch_row { 442 | title: "Hide Unknown Devices"; 443 | subtitle: "Stops Unknown Devices from showing up in device list"; 444 | } 445 | } 446 | } 447 | }; 448 | }; 449 | } 450 | } 451 | }; 452 | }; 453 | }; 454 | } 455 | 456 | Adjustment timeout_time_adjustment { 457 | step-increment: 1; 458 | lower: 0; 459 | upper: 60; 460 | value: 3; 461 | } 462 | 463 | menu primary_menu { 464 | section { 465 | item { 466 | label: _("_Refresh"); 467 | action: "win.refresh-devices"; 468 | } 469 | } 470 | section { 471 | item { 472 | label: _("_Keyboard Shortcuts"); 473 | action: "win.show-help-overlay"; 474 | } 475 | 476 | item { 477 | label: _("_About Overskride"); 478 | action: "app.about"; 479 | } 480 | } 481 | } 482 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/bell-outline-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/bluetooth-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/chain-link-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/check-plain-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/cross-large-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/dock-left-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/folder-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/headphones-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/heart-broken-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/image-missing-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/menu-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/no-bluetooth-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/refresh-large-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/right-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/rssi-dead-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/rssi-high-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/rssi-low-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/rssi-medium-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/rssi-none-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 41 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/rssi-not-found-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/skull-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/icons/symbolic/actions/smartphone-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | /* main.rs 2 | * 3 | * Copyright 2023 kaii 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | * 18 | * SPDX-License-Identifier: GPL-3.0-or-later 19 | */ 20 | 21 | mod application; 22 | mod config; 23 | mod window; 24 | #[path = "bluetooth/message.rs"] mod message; 25 | #[path = "bluetooth/bluetooth_settings.rs"] mod bluetooth_settings; 26 | #[path = "bluetooth/device.rs"] mod device; 27 | #[path = "bluetooth/agent.rs"] mod agent; 28 | #[path = "bluetooth/services.rs"] mod services; 29 | #[path = "bluetooth/audio_profiles.rs"] mod audio_profiles; 30 | #[path = "bluetooth/battery.rs"] mod battery; 31 | #[path = "obex/obex.rs"] mod obex; 32 | #[path = "obex/obex_utils.rs"] mod obex_utils; 33 | #[path = "widgets/connected_switch_row.rs"] mod connected_switch_row; 34 | #[path = "widgets/device_action_row.rs"] mod device_action_row; 35 | #[path = "widgets/receiving_popover.rs"] mod receiving_popover; 36 | #[path = "widgets/receiving_row.rs"] mod receiving_row; 37 | #[path = "widgets/startup_error_message.rs"] mod startup_error_message; 38 | #[path = "widgets/selectable_row.rs"] mod selectable_row; 39 | #[path = "widgets/battery_indicator.rs"] mod battery_indicator; 40 | #[path = "widgets/more_info_page.rs"] mod more_info_page; 41 | 42 | 43 | use self::application::OverskrideApplication; 44 | use self::window::OverskrideWindow; 45 | 46 | use config::{GETTEXT_PACKAGE, LOCALEDIR, PKGDATADIR}; 47 | use gettextrs::{bind_textdomain_codeset, bindtextdomain, textdomain}; 48 | use gtk::{gio, glib}; 49 | use gtk::prelude::*; 50 | use gtk::gdk::Display; 51 | 52 | fn main() -> glib::ExitCode { 53 | // Set up gettext translations 54 | bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain"); 55 | bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8") 56 | .expect("Unable to set the text domain encoding"); 57 | textdomain(GETTEXT_PACKAGE).expect("Unable to switch to the text domain"); 58 | 59 | // Load resources 60 | // let resources = gio::Resource::load(PKGDATADIR.to_owned() + "/overskride.gresource") 61 | // .expect("Could not load resources"); 62 | // gio::resources_register(&resources); 63 | 64 | let resources = match std::env::var("MESON_DEVENV") { 65 | Err(_) => gio::Resource::load(PKGDATADIR.to_owned() + "/overskride.gresource") 66 | .expect("Unable to find overskride.gresource"), 67 | Ok(_) => match std::env::current_exe() { 68 | Ok(path) => { 69 | let mut resource_path = path; 70 | resource_path.pop(); 71 | resource_path.push("overskride.gresource"); 72 | gio::Resource::load(&resource_path) 73 | .expect("Unable to find overskride.gresource in devenv") 74 | } 75 | Err(err) => { 76 | eprintln!("Unable to find the current path: {}", err); 77 | return 1.into(); 78 | } 79 | }, 80 | }; 81 | 82 | gio::resources_register(&resources); 83 | 84 | // Create a new GtkApplication. The application manages our main loop, 85 | // application windows, integration with the window manager/compositor, and 86 | // desktop features such as file opening and single-instance applications. 87 | let app = OverskrideApplication::new("io.github.kaii_lb.Overskride", &gio::ApplicationFlags::empty()); 88 | 89 | app.connect_startup(|_| { 90 | load_css() 91 | }); 92 | 93 | // Run the application. This function will block until the application 94 | // exits. Upon return, we have our exit code to return to the shell. (This 95 | // is the code you see when you do `echo $?` after running a command in a 96 | // terminal. 97 | app.run() 98 | } 99 | 100 | fn load_css() { 101 | let provider = gtk::CssProvider::new(); 102 | provider.load_from_resource("/io/github/kaii_lb/Overskride/gtk/style.css"); 103 | 104 | gtk::style_context_add_provider_for_display( 105 | &Display::default().expect("could not connect to a display"), 106 | &provider, 107 | gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, 108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | pkgdatadir = join_paths(get_option('prefix'), get_option('datadir'), meson.project_name()) 2 | gnome = import('gnome') 3 | 4 | blueprints = custom_target('blueprints', 5 | input: files( 6 | 'gtk/window.blp', 7 | 'gtk/help-overlay.blp', 8 | 'gtk/connected-switch-row.blp', 9 | 'gtk/device-action-row.blp', 10 | 'gtk/receiving-popover.blp', 11 | 'gtk/receiving-row.blp', 12 | 'gtk/startup-error-message.blp', 13 | 'gtk/selectable-row.blp', 14 | 'gtk/battery-indicator.blp', 15 | 'gtk/more-info-page.blp', 16 | ), 17 | output: '.', 18 | command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], 19 | ) 20 | 21 | gnome.compile_resources('overskride', 22 | 'overskride.gresource.xml', 23 | dependencies: blueprints, 24 | gresource_bundle: true, 25 | install: true, 26 | install_dir: pkgdatadir, 27 | ) 28 | 29 | conf = configuration_data() 30 | conf.set_quoted('VERSION', meson.project_version()) 31 | conf.set_quoted('GETTEXT_PACKAGE', 'overskride') 32 | conf.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir'))) 33 | conf.set_quoted('PKGDATADIR', pkgdatadir) 34 | 35 | configure_file( 36 | input: 'config.rs.in', 37 | output: 'config.rs', 38 | configuration: conf 39 | ) 40 | 41 | # Copy the config.rs output to the source directory. 42 | run_command( 43 | 'cp', 44 | join_paths(meson.project_build_root(), 'src', 'config.rs'), 45 | join_paths(meson.project_source_root(), 'src', 'config.rs'), 46 | check: true 47 | ) 48 | 49 | cargo_bin = find_program('cargo') 50 | cargo_opt = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ] 51 | cargo_opt += [ '--target-dir', meson.project_build_root() / 'src' ] 52 | cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ] 53 | 54 | if get_option('buildtype') == 'release' 55 | cargo_opt += [ '--release' ] 56 | rust_target = 'release' 57 | else 58 | rust_target = 'debug' 59 | endif 60 | 61 | cargo_build = custom_target( 62 | 'cargo-build', 63 | build_by_default: true, 64 | build_always_stale: true, 65 | output: meson.project_name(), 66 | console: true, 67 | install: true, 68 | install_dir: get_option('bindir'), 69 | command: [ 70 | 'env', cargo_env, 71 | cargo_bin, 'build', 72 | cargo_opt, '&&', 'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@', 73 | ] 74 | ) 75 | -------------------------------------------------------------------------------- /src/obex/obex.rs: -------------------------------------------------------------------------------- 1 | use dbus::{blocking::{Connection, 2 | stdintf::org_freedesktop_dbus::{ObjectManagerInterfacesAdded, PropertiesPropertiesChanged, Properties}}, 3 | Path, arg::{PropMap, RefArg, Variant}, MethodErr}; 4 | 5 | use dbus_crossroads::Crossroads; 6 | use gtk::glib::Sender; 7 | use std::{time::Duration, collections::HashMap, sync::Mutex}; 8 | use dbus::channel::MatchingReceiver; 9 | use std::str::FromStr; 10 | 11 | use crate::{message::Message, obex_utils::{ObexAgentManager1, ObexTransfer1, ObexClient1, ObexObjectPush1}, 12 | window::{DISPLAYING_DIALOG, CONFIRMATION_AUTHORIZATION, SEND_FILES_PATH, STORE_FOLDER, AUTO_ACCEPT_FROM_TRUSTED, CURRENT_ADAPTER}, 13 | agent::wait_for_dialog_exit}; 14 | 15 | const SESSION_INTERFACE: &str = "org.bluez.obex.Session1"; 16 | const TRANSFER_INTERFACE: &str = "org.bluez.obex.Transfer1"; 17 | 18 | static mut SESSION_BUS: Mutex> = Mutex::new(None); 19 | static mut CURRENT_SESSION: String = String::new(); 20 | static mut CURRENT_TRANSFER: String = String::new(); 21 | static mut CURRENT_FILE_SIZE: u64 = 0; 22 | static mut CURRENT_FILE_NAME: String = String::new(); 23 | static mut CURRENT_SENDER: Option> = None; 24 | static mut OUTBOUND: bool = false; 25 | static mut LAST_BYTES: u64 = 0; 26 | pub static mut BREAKING: bool = false; 27 | pub static mut CANCEL: bool = false; 28 | pub static mut AUTO_ACCEPT_AFTER_FIRST: bool = true; 29 | 30 | // fn approx_equal(a: f32, b: f32, decimal_places: u8) -> bool { 31 | // let factor = 10.0f32.powi(decimal_places as i32); 32 | // let a = (a * factor).trunc(); 33 | // let b = (b * factor).trunc(); 34 | // a == b 35 | // } 36 | 37 | /// Checks if the properties match the [transfer interface](TRANSFER_INTERFACE) and updates various UI elements accordingly 38 | fn handle_properties_updated(interface: String, changed_properties: PropMap, transfer: String) { 39 | if interface == TRANSFER_INTERFACE { 40 | let sender = unsafe { 41 | CURRENT_SENDER.clone().unwrap() 42 | }; 43 | let status = if let Some(status_holder) = &changed_properties.get_key_value("Status") { 44 | let dummy_status = status_holder.1.0.as_str().unwrap(); 45 | 46 | // self explanatory, but it tells the user about whats happening with the transfer 47 | match dummy_status { 48 | "active" => { 49 | if unsafe { OUTBOUND } { 50 | sender.send(Message::PopupError("obex-transfer-active-outbound".to_string(), adw::ToastPriority::Normal)).expect("cannot send message"); 51 | unsafe { BREAKING = false; } 52 | } 53 | else { 54 | sender.send(Message::PopupError("obex-transfer-active-inbound".to_string(), adw::ToastPriority::Normal)).expect("cannot send message"); 55 | } 56 | }, 57 | "complete" => { 58 | if unsafe { OUTBOUND } { 59 | sender.send(Message::PopupError("obex-transfer-complete-outbound".to_string(), adw::ToastPriority::Normal)).expect("cannot send message"); 60 | unsafe { BREAKING = true; } 61 | } 62 | else { 63 | sender.send(Message::PopupError("obex-transfer-complete-inbound".to_string(), adw::ToastPriority::Normal)).expect("cannot send message"); 64 | move_to_store_folder(&sender); 65 | } 66 | }, 67 | "error" => { 68 | if unsafe { OUTBOUND } { 69 | sender.send(Message::PopupError("obex-transfer-error-outbound".to_string(), adw::ToastPriority::Normal)).expect("cannot send message"); 70 | unsafe { BREAKING = true; } 71 | } 72 | else { 73 | sender.send(Message::PopupError("obex-transfer-error-inbound".to_string(), adw::ToastPriority::Normal)).expect("cannot send message"); 74 | } 75 | }, 76 | message => { 77 | sender.send(Message::PopupError(message.to_string(), adw::ToastPriority::Normal)).expect("cannot send message"); 78 | unsafe { BREAKING = false; } 79 | } 80 | } 81 | 82 | // println!("status: {:?}", dummy_status); 83 | 84 | dummy_status 85 | } 86 | else { 87 | "" 88 | }; 89 | 90 | // convert from bytes to megabytes 91 | let (mb, kb) = if let Some(val) = changed_properties.get_key_value("Transferred") { 92 | let transferred = val.1.0.as_u64(); 93 | 94 | // calculate the transfer speed by subtracting the current amount from the last 95 | let value_kb = unsafe { 96 | // println!("transferred: {}, last: {}", transferred.unwrap(), LAST_BYTES); 97 | (transferred.unwrap_or(0) / 1000).saturating_sub(LAST_BYTES / 1000) 98 | }; 99 | 100 | let value_mb = match transferred { 101 | Some(val) => { 102 | ((val as f32 / 1000000.0) * 1000.0).round() / 1000.0 103 | }, 104 | None => { 105 | 0.0 106 | } 107 | }; 108 | // println!("transferred: {}", value_mb); 109 | 110 | unsafe { 111 | LAST_BYTES = transferred.unwrap_or(0); 112 | } 113 | 114 | (value_mb, value_kb) 115 | } 116 | else { 117 | (0.0, 0) 118 | }; 119 | // updates the transfer with the specified values 120 | sender.send(Message::UpdateTransfer(transfer, unsafe { CURRENT_FILE_NAME.clone() }, mb, kb, status.to_string())).expect("cannot send message"); 121 | } 122 | } 123 | 124 | /// Is run when a new interface gets added, i.e. a new connection to dbus on the specified path or a new session 125 | fn handle_interface_added(path: &Path, interfaces: &HashMap) { 126 | for interface in interfaces { 127 | 128 | // if interface is a session interface then set those variables accordingly 129 | if interface.0 == SESSION_INTERFACE && path.contains("server") { 130 | println!("started session: {:?}", interface.0); 131 | unsafe { 132 | let session_name = path.clone().to_string(); 133 | let holder = session_name; 134 | CURRENT_SESSION = holder.clone(); 135 | } 136 | } 137 | // if the interface is a transfer then handle the properties updated signal 138 | else if interface.0 == TRANSFER_INTERFACE && path.contains("server") && path.contains("transfer") { 139 | let conn: &mut Connection; 140 | unsafe { 141 | conn = SESSION_BUS.get_mut().unwrap().as_mut().unwrap(); 142 | if CURRENT_TRANSFER != path.to_string() { 143 | CONFIRMATION_AUTHORIZATION = false 144 | } 145 | 146 | CURRENT_TRANSFER = path.to_string(); 147 | println!("path is {}", path); 148 | } 149 | let proxy = conn.with_proxy("org.bluez.obex", path, Duration::from_millis(1000)); 150 | proxy.match_signal(|signal: PropertiesPropertiesChanged, _: &Connection, message: &dbus::Message| { 151 | let transfer = if let Some(path) = message.path() { 152 | path.to_string() 153 | } 154 | else { 155 | "".to_string() 156 | }; 157 | 158 | handle_properties_updated(signal.interface_name, signal.changed_properties, transfer); 159 | true 160 | }).expect("can't match signal"); 161 | 162 | if let Some(session) = &interface.1.get_key_value("Session").unwrap().1.0.as_str() { 163 | println!("transfer started at {:?}", session); 164 | } 165 | } 166 | } 167 | } 168 | 169 | /// Register a new obex agent to dbus, allowing files to be received 170 | pub fn register_obex_agent(sender: Sender) -> Result<(), dbus::Error> { 171 | let conn: &mut Connection; 172 | unsafe { 173 | SESSION_BUS = Mutex::new(Some(Connection::new_session().unwrap())); 174 | conn = SESSION_BUS.get_mut().unwrap().as_mut().unwrap(); 175 | CURRENT_SENDER = Some(sender.clone()); 176 | } 177 | 178 | let proxy = conn.with_proxy("org.bluez.obex", "/", Duration::from_millis(5000)); 179 | 180 | // matches the signal of a new object getting added to the dbus interface (ie an agent) 181 | proxy.match_signal(|signal: ObjectManagerInterfacesAdded, _: &Connection, _: &dbus::Message| { 182 | handle_interface_added(&signal.object, &signal.interfaces); 183 | // println!("caught signal! {:?}", signal); 184 | true 185 | }).expect("cannot match signal"); 186 | 187 | drop(proxy); 188 | let proxy2 = conn.with_proxy("org.bluez.obex", "/org/bluez/obex", Duration::from_millis(5000)); 189 | 190 | let mut cr = Crossroads::new(); 191 | 192 | create_agent(&mut cr, sender.clone()); 193 | proxy2.register_agent(Path::from_slice("/overskride/agent").unwrap()).expect("cant create agent"); 194 | 195 | serve(conn, Some(cr))?; 196 | 197 | Ok(()) 198 | } 199 | 200 | /// Infinitely processes dbus requests until canceled 201 | fn serve(conn: &mut Connection, cr: Option) -> Result<(), dbus::Error> { 202 | if let Some(mut crossroads) = cr { 203 | conn.start_receive(dbus::message::MatchRule::new_method_call(), Box::new(move |msg, conn| { 204 | crossroads.handle_message(msg, conn).unwrap(); 205 | true 206 | })); 207 | } 208 | 209 | // Serve clients forever. 210 | unsafe { 211 | BREAKING = false; 212 | CANCEL = false; 213 | 214 | while !CANCEL && !BREAKING { 215 | // println!("serving"); 216 | conn.process(std::time::Duration::from_millis(1000))?; 217 | } 218 | 219 | let sender = CURRENT_SENDER.clone().unwrap(); 220 | 221 | let proxy2 = conn.with_proxy("org.bluez.obex", CURRENT_TRANSFER.clone(), Duration::from_millis(5000)); 222 | 223 | if CANCEL { 224 | if let Err(err) = proxy2.cancel() { 225 | println!("error while canceling transfer {:?}", err.message()); 226 | } 227 | println!("canceled"); 228 | CANCEL = false; 229 | } 230 | 231 | BREAKING = false; 232 | 233 | // update transfer UI with the filename and transferred amount 234 | let filename = proxy2.name().unwrap_or("Unknown File".to_string()); 235 | let transferred = (proxy2.transferred().unwrap_or(9999) as f32 / 1000000.0).round() / 100.0; 236 | sender.send(Message::UpdateTransfer(proxy2.path.to_string(), filename.clone(), transferred, 0, "error".to_string())).expect("cannot send message"); 237 | 238 | // remove the transfer from the list after 1 minute 239 | std::thread::spawn(move || { 240 | std::thread::sleep(std::time::Duration::from_secs(60)); 241 | sender.send(Message::RemoveTransfer(CURRENT_TRANSFER.clone(), filename)).expect("cannot send message"); 242 | }); 243 | } 244 | Ok(()) 245 | } 246 | 247 | /// This functions describes the methods an agent has, creates an object of that agent, and inserts it into a crossroads instance 248 | fn create_agent(cr: &mut Crossroads, sender: Sender) { 249 | let agent = cr.register("org.bluez.obex.Agent1", |b| { 250 | b.method("AuthorizePush", ("transfer",), ("filename",), move |_, _, (transfer,): (Path,)| { 251 | println!("authorizing..."); 252 | let conn = Connection::new_session().expect("cannot create connection."); 253 | let props = conn.with_proxy("org.bluez.obex", transfer.clone(), std::time::Duration::from_secs(5)).get_all(TRANSFER_INTERFACE); 254 | 255 | if let Ok(all_props) = props { 256 | // lots of fuckery but its self explanatory 257 | let filename = all_props.get("Name").expect("cannot get name of file.").0.as_str().unwrap().to_owned(); 258 | let filesize_holder = &*all_props.get("Size").expect("cannot get file size.").0; 259 | let filesize = filesize_holder.as_u64().unwrap_or(9999); 260 | let session = all_props.get("Session").expect("cannot get session for receive").0.as_str().unwrap_or(""); 261 | 262 | // println!("all props is: {:?}", all_props); 263 | 264 | unsafe { 265 | CURRENT_FILE_NAME = filename.clone(); 266 | CURRENT_FILE_SIZE = filesize; 267 | OUTBOUND = false; 268 | } 269 | let mb = ((filesize as f32 / 1000000.0) * 100.0).round() / 100.0; // to megabytes 270 | 271 | // get the target device, if it doesn't exist, panic ensues 272 | let sender_props = conn.with_proxy("org.bluez.obex", session, std::time::Duration::from_secs(5)).get_all(SESSION_INTERFACE).unwrap(); 273 | let device = sender_props.get("Destination").expect("cannot get sender device").0.as_str().unwrap_or("00:00:00:00:00:00"); 274 | 275 | let (device_name, device_trusted) = if let Ok(props) = get_device_props(device) { 276 | props 277 | } 278 | else { 279 | return Err(MethodErr::from(("org.bluez.obex.Error.Canceled", "Request Canceled"))); 280 | }; 281 | 282 | // if user sets auto accept from trusted, immediately accept the transfer without confirmation 283 | if unsafe { AUTO_ACCEPT_FROM_TRUSTED } && device_trusted { 284 | println!("transfer is: {:?}", transfer); 285 | sender.send(Message::StartTransfer(transfer.to_string(), filename.clone(), 0.0, 0.0, mb, false)).expect("cannot send message"); 286 | 287 | return Ok((filename,)); 288 | } 289 | 290 | // if the ~/.cache directory doesn't exist, return as we have no where to store the file 291 | if !gtk::glib::user_cache_dir().exists() { 292 | sender.clone().send(Message::PopupError("file-storage-cache-invalid".to_string(), adw::ToastPriority::High)).expect("cannot send message"); 293 | 294 | return Err(MethodErr::from(("org.bluez.obex.Error.Canceled", "Request Canceled"))); 295 | } 296 | 297 | // spawn a dialog returning the accepted bool, no accepted => reject transfer 298 | if unsafe { CONFIRMATION_AUTHORIZATION } || spawn_dialog(filename.clone(), &sender, device_name) { 299 | println!("transfer is: {:?}", transfer); 300 | sender.send(Message::StartTransfer(transfer.to_string(), filename.clone(), 0.0, 0.0, mb, false)).expect("cannot send message"); 301 | 302 | unsafe { 303 | if !AUTO_ACCEPT_AFTER_FIRST { 304 | CONFIRMATION_AUTHORIZATION = false; 305 | } 306 | } 307 | 308 | Ok((filename,)) 309 | } 310 | else { 311 | println!("rejected push"); 312 | let error = MethodErr::from(("org.bluez.obex.Error.Rejected", "Not Authorized")); 313 | unsafe { 314 | CONFIRMATION_AUTHORIZATION = false; 315 | } 316 | Err(error) 317 | } 318 | } 319 | else { 320 | unsafe { 321 | CONFIRMATION_AUTHORIZATION = false; 322 | } 323 | println!("failed to authorize push"); 324 | Err(MethodErr::from(("org.bluez.obex.Error.Canceled", "Request Canceled"))) 325 | } 326 | }); 327 | 328 | // these are never called, not sure why they exist 329 | b.method("Cancel", (), (), move |_, _, _: ()| { 330 | println!("Cancelling..."); 331 | Ok(()) 332 | }); 333 | 334 | b.method("Release", (), (), move |_, _, _: ()| { 335 | println!("Releasing..."); 336 | Ok(()) 337 | }); 338 | }); 339 | cr.insert("/overskride/agent", &[agent], ()); 340 | 341 | println!("created obex agent"); 342 | } 343 | 344 | /// Spawns a new dialog asking the user to allow or reject a file transfer from a device 345 | #[tokio::main] 346 | async fn spawn_dialog(filename: String, sender: &Sender, device_name: String) -> bool { 347 | println!("file receive request incoming!"); 348 | 349 | let title = "File Transfer Incoming".to_string(); 350 | let subtitle = "Accept ".to_string() + &filename + " from " + &device_name + "?"; 351 | let confirm = "Accept".to_string(); 352 | let response_type = adw::ResponseAppearance::Suggested; 353 | 354 | unsafe{ 355 | DISPLAYING_DIALOG = true; 356 | } 357 | sender.send(Message::RequestYesNo(title, subtitle, confirm, response_type)).expect("cannot send message"); 358 | 359 | wait_for_dialog_exit().await; 360 | 361 | std::thread::sleep(std::time::Duration::from_millis(500)); 362 | unsafe { 363 | CONFIRMATION_AUTHORIZATION 364 | } 365 | } 366 | 367 | /// Wrapper function handling the adapter and target device, looping over all the files needing to be sent and sending them on by one 368 | #[tokio::main] 369 | pub async fn start_send_file(destination: bluer::Address, source: bluer::Address, sender: Sender) { 370 | unsafe{ 371 | // horrible way but it works, this is for waiting to exit from the dialog 372 | DISPLAYING_DIALOG = true; 373 | } 374 | sender.send(Message::GetFile(gtk::FileChooserAction::Open)).expect("cannot send message"); 375 | 376 | wait_for_dialog_exit().await; 377 | 378 | let file_paths = unsafe { 379 | SEND_FILES_PATH.clone() 380 | }; 381 | 382 | // if calling on an empty transfer, get out 383 | if file_paths.is_empty() { 384 | return; 385 | } 386 | 387 | let conn = Connection::new_session().expect("cannot create send connection"); 388 | let proxy = conn.with_proxy("org.bluez.obex", "/org/bluez/obex", std::time::Duration::from_secs(5)); 389 | 390 | // describes the properties of the transfer, like the origin and target devices 391 | let mut hashmap = PropMap::new(); 392 | 393 | hashmap.insert("Target".to_string(),Variant(Box::new("OPP".to_string()))); 394 | 395 | hashmap.insert("Source".to_string(), Variant(Box::new(source.to_string()))); 396 | 397 | let send_session = if let Ok(sesh) = proxy.create_session(&destination.to_string(), hashmap) { 398 | sesh 399 | } 400 | else { 401 | sender.send(Message::PopupError("obex-transfer-connection-error".to_string(), adw::ToastPriority::Normal)).expect("cannot send message"); 402 | return; 403 | }; 404 | println!("send session is: {:?}, with filepaths {:?}", send_session, file_paths); 405 | 406 | // for every file, try to send it to the target device 407 | for file in file_paths { 408 | println!("file to be sent is {}", file); 409 | send_file(file.clone(), &send_session, sender.clone()); 410 | } 411 | println!("done sending files"); 412 | } 413 | 414 | /// Sends a specified file from the file path to a target device, updating the UI in the process 415 | fn send_file(source_file: String, session_path: &Path, sender: Sender) { 416 | let conn = Connection::new_session().expect("cannot create send session"); 417 | let proxy = conn.with_proxy("org.bluez.obex", session_path, std::time::Duration::from_secs(1)); 418 | 419 | // return the path and properties 420 | let output = proxy.send_file(source_file.as_str()).unwrap(); 421 | 422 | println!("send transfer path is: {:?}", output.0.clone()); 423 | println!("send properties are: {:?}\n", output.1); 424 | 425 | // create a new proxy to the transfer path for easier processing of properties 426 | let transfer_proxy = conn.with_proxy("org.bluez.obex", output.0.clone(), std::time::Duration::from_secs(5)); 427 | 428 | unsafe { 429 | CURRENT_TRANSFER = output.0.clone().to_string(); 430 | OUTBOUND = true; 431 | } 432 | 433 | // changes filesize from bytes(?) to megabytes, then starts a transfer with the filename and size 434 | // let mb = ((transfer_proxy.size().unwrap_or(9999) as f32 / 1000000.0) * 100.0).round() / 100.0; 435 | let mb = ((transfer_proxy.size().unwrap_or(9999) as f32 / 1000000.0) * 1000.0).round() / 1000.0; 436 | sender.send(Message::StartTransfer(output.0.clone().to_string(), transfer_proxy.name().unwrap_or("Unknown File".to_string()), 0.0, 0.0, mb, true)).expect("cannot send message"); 437 | 438 | transfer_proxy.match_signal(move |signal: PropertiesPropertiesChanged, _: &Connection, _: &dbus::Message| { 439 | handle_properties_updated(signal.interface_name, signal.changed_properties, output.0.to_string()); 440 | true 441 | }).expect("can't match signal"); 442 | 443 | unsafe { 444 | BREAKING = false; 445 | CANCEL = false; 446 | 447 | while !CANCEL && !BREAKING { 448 | // process dbus requests to that path 449 | conn.process(std::time::Duration::from_millis(1000)).expect("cannot process request"); 450 | } 451 | 452 | let sender = CURRENT_SENDER.clone().unwrap(); 453 | 454 | // stop sending this file 455 | if CANCEL { 456 | if let Err(err) = transfer_proxy.cancel() { 457 | // sender.send(Message::PopupError("obex-transfer-cancel-not-authorized".to_string(), adw::ToastPriority::Normal)).expect("cannot send message"); 458 | println!("error while canceling transfer {:?}", err.message()); 459 | } 460 | let transferred = (transfer_proxy.transferred().unwrap_or(9999) as f32 / 1000000.0).round() / 100.0; 461 | sender.send(Message::UpdateTransfer(CURRENT_TRANSFER.clone(), CURRENT_FILE_NAME.clone(), transferred, 0, "error".to_string())).expect("cannot send message"); 462 | drop(transfer_proxy); 463 | drop(proxy); 464 | CANCEL = false; 465 | } 466 | 467 | BREAKING = false; 468 | 469 | // remove the transfer from the list after 1 minute 470 | std::thread::spawn(move || { 471 | std::thread::sleep(std::time::Duration::from_secs(60)); 472 | sender.send(Message::RemoveTransfer(CURRENT_TRANSFER.clone(), CURRENT_FILE_NAME.clone())).expect("cannot send message"); 473 | }); 474 | } 475 | } 476 | 477 | /// Moves a received file to where the user needs it to be 478 | /// needed because returning a file path in the agent's "AuthorizePush" method won't work because bluetooth :D 479 | pub fn move_to_store_folder(sender: &Sender) { 480 | if unsafe { OUTBOUND } { 481 | return; 482 | } 483 | 484 | let filename = unsafe { 485 | CURRENT_FILE_NAME.clone() 486 | }; 487 | let store_folder = unsafe { 488 | STORE_FOLDER.clone() 489 | }; 490 | // default path of stored file by obexd 491 | let filepath = if let Some(cache_dir) = gtk::glib::user_cache_dir().to_str() { 492 | cache_dir.to_string() + "/obexd/" + &filename 493 | } 494 | else { 495 | sender.send(Message::PopupError("obex-tranfer-cant-move".to_string(), adw::ToastPriority::High)).expect("cannot send message"); 496 | println!("unable to save file to store folder, it should still remain in ~/.cache/obexd"); 497 | return; 498 | }; 499 | 500 | let new_filepath = store_folder + &filename; 501 | 502 | // move file to location and handle error 503 | match std::fs::rename(filepath, new_filepath) { 504 | Ok(()) => { 505 | println!("file moved to directory"); 506 | }, 507 | Err(err) => { 508 | sender.send(Message::PopupError("obex-tranfer-cant-move".to_string(), adw::ToastPriority::High)).expect("cannot send message"); 509 | println!("file was not moved due to {:?}", err); 510 | }, 511 | } 512 | } 513 | 514 | /// Gets the name and trusted value of a specified device 515 | #[tokio::main] 516 | async fn get_device_props(address_slice: &str) -> bluer::Result<(String, bool)> { 517 | let adapter = bluer::Session::new().await?.adapter(unsafe { &CURRENT_ADAPTER })?; 518 | let address = bluer::Address::from_str(address_slice).unwrap_or(bluer::Address::any()); 519 | let device = adapter.device(address)?; 520 | 521 | let trusted = device.is_trusted().await?; 522 | let name = device.alias().await?; 523 | 524 | Ok((name, trusted)) 525 | } 526 | -------------------------------------------------------------------------------- /src/obex/obex_utils.rs: -------------------------------------------------------------------------------- 1 | // This code was autogenerated with `dbus-codegen-rust -d org.bluez.obex -p /org/bluez/obex -o Projects/test/obex/src/obex.rs`, see https://github.com/diwic/dbus-rs 2 | use dbus::{self as dbus}; 3 | #[allow(unused_imports)] 4 | use dbus::arg; 5 | use dbus::blocking; 6 | 7 | pub trait OrgFreedesktopDBusIntrospectable { 8 | fn introspect(&self) -> Result; 9 | } 10 | 11 | impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> OrgFreedesktopDBusIntrospectable for blocking::Proxy<'a, C> { 12 | 13 | fn introspect(&self) -> Result { 14 | self.method_call("org.freedesktop.DBus.Introspectable", "Introspect", ()).map(|r: (String, )| r.0) 15 | } 16 | } 17 | 18 | pub trait ObexAgentManager1 { 19 | fn register_agent(&self, agent: dbus::Path) -> Result<(), dbus::Error>; 20 | fn unregister_agent(&self, agent: dbus::Path) -> Result<(), dbus::Error>; 21 | } 22 | 23 | impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> ObexAgentManager1 for blocking::Proxy<'a, C> { 24 | 25 | fn register_agent(&self, agent: dbus::Path) -> Result<(), dbus::Error> { 26 | self.method_call("org.bluez.obex.AgentManager1", "RegisterAgent", (agent, )) 27 | } 28 | 29 | fn unregister_agent(&self, agent: dbus::Path) -> Result<(), dbus::Error> { 30 | self.method_call("org.bluez.obex.AgentManager1", "UnregisterAgent", (agent, )) 31 | } 32 | } 33 | 34 | pub trait ObexClient1 { 35 | fn create_session(&self, destination: &str, args: arg::PropMap) -> Result, dbus::Error>; 36 | fn remove_session(&self, session: dbus::Path) -> Result<(), dbus::Error>; 37 | } 38 | 39 | impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> ObexClient1 for blocking::Proxy<'a, C> { 40 | 41 | fn create_session(&self, destination: &str, args: arg::PropMap) -> Result, dbus::Error> { 42 | self.method_call("org.bluez.obex.Client1", "CreateSession", (destination, args, )).map(|r: (dbus::Path<'static>, )| r.0) 43 | } 44 | 45 | fn remove_session(&self, session: dbus::Path) -> Result<(), dbus::Error> { 46 | self.method_call("org.bluez.obex.Client1", "RemoveSession", (session, )) 47 | } 48 | } 49 | 50 | pub trait ObexSession1 { 51 | fn get_capabilities(&self) -> Result; 52 | fn source(&self) -> Result; 53 | fn destination(&self) -> Result; 54 | fn target(&self) -> Result; 55 | fn root(&self) -> Result; 56 | } 57 | 58 | impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> ObexSession1 for blocking::Proxy<'a, C> { 59 | 60 | fn get_capabilities(&self) -> Result { 61 | self.method_call("org.bluez.obex.Session1", "GetCapabilities", ()).map(|r: (String, )| r.0) 62 | } 63 | 64 | fn source(&self) -> Result { 65 | ::get(self, "org.bluez.obex.Session1", "Source") 66 | } 67 | 68 | fn destination(&self) -> Result { 69 | ::get(self, "org.bluez.obex.Session1", "Destination") 70 | } 71 | 72 | fn target(&self) -> Result { 73 | ::get(self, "org.bluez.obex.Session1", "Target") 74 | } 75 | 76 | fn root(&self) -> Result { 77 | ::get(self, "org.bluez.obex.Session1", "Root") 78 | } 79 | } 80 | 81 | pub trait ObexTransfer1 { 82 | fn cancel(&self) -> Result<(), dbus::Error>; 83 | fn status(&self) -> Result; 84 | fn session(&self) -> Result, dbus::Error>; 85 | fn name(&self) -> Result; 86 | fn type_(&self) -> Result; 87 | fn size(&self) -> Result; 88 | fn time(&self) -> Result; 89 | fn filename(&self) -> Result; 90 | fn transferred(&self) -> Result; 91 | } 92 | 93 | impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> ObexTransfer1 for blocking::Proxy<'a, C> { 94 | 95 | fn cancel(&self) -> Result<(), dbus::Error> { 96 | self.method_call("org.bluez.obex.Transfer1", "Cancel", ()) 97 | } 98 | 99 | fn status(&self) -> Result { 100 | ::get(self, "org.bluez.obex.Transfer1", "Status") 101 | } 102 | 103 | fn session(&self) -> Result, dbus::Error> { 104 | ::get(self, "org.bluez.obex.Transfer1", "Session") 105 | } 106 | 107 | fn name(&self) -> Result { 108 | ::get(self, "org.bluez.obex.Transfer1", "Name") 109 | } 110 | 111 | fn type_(&self) -> Result { 112 | ::get(self, "org.bluez.obex.Transfer1", "Type") 113 | } 114 | 115 | fn size(&self) -> Result { 116 | ::get(self, "org.bluez.obex.Transfer1", "Size") 117 | } 118 | 119 | fn time(&self) -> Result { 120 | ::get(self, "org.bluez.obex.Transfer1", "Time") 121 | } 122 | 123 | fn filename(&self) -> Result { 124 | ::get(self, "org.bluez.obex.Transfer1", "Filename") 125 | } 126 | 127 | fn transferred(&self) -> Result { 128 | ::get(self, "org.bluez.obex.Transfer1", "Transferred") 129 | } 130 | } 131 | 132 | pub trait ObexObjectPush1 { 133 | fn send_file(&self, sourcefile: &str) -> Result<(dbus::Path<'static>, arg::PropMap), dbus::Error>; 134 | fn pull_business_card(&self, targetfile: &str) -> Result<(dbus::Path<'static>, arg::PropMap), dbus::Error>; 135 | fn exchange_business_cards(&self, clientfile: &str, targetfile: &str) -> Result<(dbus::Path<'static>, arg::PropMap), dbus::Error>; 136 | } 137 | 138 | impl<'a, T: blocking::BlockingSender, C: ::std::ops::Deref> ObexObjectPush1 for blocking::Proxy<'a, C> { 139 | 140 | fn send_file(&self, sourcefile: &str) -> Result<(dbus::Path<'static>, arg::PropMap), dbus::Error> { 141 | self.method_call("org.bluez.obex.ObjectPush1", "SendFile", (sourcefile, )) 142 | } 143 | 144 | fn pull_business_card(&self, targetfile: &str) -> Result<(dbus::Path<'static>, arg::PropMap), dbus::Error> { 145 | self.method_call("org.bluez.obex.ObjectPush1", "PullBusinessCard", (targetfile, )) 146 | } 147 | 148 | fn exchange_business_cards(&self, clientfile: &str, targetfile: &str) -> Result<(dbus::Path<'static>, arg::PropMap), dbus::Error> { 149 | self.method_call("org.bluez.obex.ObjectPush1", "ExchangeBusinessCards", (clientfile, targetfile, )) 150 | } 151 | } -------------------------------------------------------------------------------- /src/overskride.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | gtk/window.ui 5 | gtk/help-overlay.ui 6 | gtk/connected-switch-row.ui 7 | gtk/device-action-row.ui 8 | gtk/receiving-popover.ui 9 | gtk/receiving-row.ui 10 | gtk/startup-error-message.ui 11 | gtk/selectable-row.ui 12 | gtk/battery-indicator.ui 13 | gtk/more-info-page.ui 14 | gtk/style.css 15 | 16 | 17 | icons/symbolic/actions/check-plain-symbolic.svg 18 | icons/symbolic/actions/dock-left-symbolic.svg 19 | icons/symbolic/actions/bluetooth-symbolic.svg 20 | icons/symbolic/actions/headphones-symbolic.svg 21 | icons/symbolic/actions/folder-symbolic.svg 22 | icons/symbolic/actions/refresh-large-symbolic.svg 23 | icons/symbolic/actions/smartphone-symbolic.svg 24 | icons/symbolic/actions/image-missing-symbolic.svg 25 | icons/symbolic/actions/rssi-dead-symbolic.svg 26 | icons/symbolic/actions/rssi-high-symbolic.svg 27 | icons/symbolic/actions/rssi-low-symbolic.svg 28 | icons/symbolic/actions/rssi-medium-symbolic.svg 29 | icons/symbolic/actions/rssi-not-found-symbolic.svg 30 | icons/symbolic/actions/rssi-none-symbolic.svg 31 | icons/symbolic/actions/no-bluetooth-symbolic.svg 32 | icons/symbolic/actions/menu-symbolic.svg 33 | icons/symbolic/actions/bell-outline-symbolic.svg 34 | icons/symbolic/actions/cross-large-symbolic.svg 35 | icons/symbolic/actions/skull-symbolic.svg 36 | icons/symbolic/actions/heart-broken-symbolic.svg 37 | icons/symbolic/actions/right-symbolic.svg 38 | icons/symbolic/actions/chain-link-symbolic.svg 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/widgets/battery_indicator.rs: -------------------------------------------------------------------------------- 1 | use gtk::glib; 2 | use gtk::subclass::prelude::*; 3 | use adw::subclass::prelude::PreferencesRowImpl; 4 | use glib::{Object, Properties}; 5 | use gtk::{subclass::{widget::WidgetImpl, prelude::{ListBoxRowImpl, ObjectImpl}}, TemplateChild}; 6 | use gtk::prelude::ObjectExt; 7 | use std::cell::RefCell; 8 | use adw::prelude::WidgetExt; 9 | 10 | mod imp { 11 | use super::*; 12 | 13 | /// a preference row that is actually a level bar showing colored level and % of device battery 14 | #[derive(Properties, Default, gtk::CompositeTemplate)] 15 | #[template(resource = "/io/github/kaii_lb/Overskride/gtk/battery-indicator.ui")] 16 | #[properties(wrapper_type = super::BatteryLevelIndicator)] 17 | pub struct BatteryLevelIndicator { 18 | #[template_child] 19 | pub battery_label: TemplateChild, 20 | #[template_child] 21 | pub level_bar: TemplateChild, 22 | 23 | #[property(get, set = Self::set_battery_level_from_i8)] 24 | pub battery_level: RefCell, 25 | } 26 | 27 | #[glib::object_subclass] 28 | impl ObjectSubclass for BatteryLevelIndicator { 29 | const NAME: &'static str = "BatteryLevelIndicator"; 30 | type Type = super::BatteryLevelIndicator; 31 | type ParentType = adw::PreferencesRow; 32 | 33 | fn class_init(klass: &mut Self::Class) { 34 | klass.bind_template(); 35 | } 36 | 37 | fn instance_init(obj: &glib::subclass::InitializingObject) { 38 | obj.init_template(); 39 | } 40 | } 41 | 42 | #[glib::derived_properties] 43 | impl ObjectImpl for BatteryLevelIndicator { 44 | fn constructed(&self) { 45 | self.parent_constructed(); 46 | 47 | // this makes it so the level bar color changes depending on percentage 48 | self.level_bar.add_offset_value("full", 100.0); 49 | self.level_bar.add_offset_value("three-quarters", 75.0); 50 | self.level_bar.add_offset_value("half", 50.0); 51 | self.level_bar.add_offset_value("third", 25.0); 52 | } 53 | } 54 | 55 | // add offset 56 | impl WidgetImpl for BatteryLevelIndicator {} 57 | impl ListBoxRowImpl for BatteryLevelIndicator {} 58 | impl PreferencesRowImpl for BatteryLevelIndicator {} 59 | 60 | impl BatteryLevelIndicator { 61 | // set the battery level to either the percent or unavailable depending on the value 62 | fn set_battery_level_from_i8(&self, level: i8) { 63 | let level = level.clamp(-1, 100); 64 | let levelbar = self.level_bar.get(); 65 | let battery_label = self.battery_label.get(); 66 | 67 | if level == -1 { 68 | levelbar.set_value(100.0); 69 | levelbar.set_sensitive(false); 70 | battery_label.set_label(&("Battery: ".to_string() + "Unavailable")); 71 | } 72 | else { 73 | levelbar.set_sensitive(true); 74 | levelbar.set_value(level as f64); 75 | battery_label.set_label(&("Battery: ".to_string() + &level.to_string() + "%")); 76 | } 77 | 78 | self.battery_level.set(level); 79 | } 80 | } 81 | } 82 | 83 | glib::wrapper! { 84 | pub struct BatteryLevelIndicator(ObjectSubclass) 85 | @extends adw::PreferencesRow, gtk::Widget, gtk::ListBoxRow, 86 | @implements gtk::Accessible, gtk::Orientable, gtk::Buildable, gtk::ConstraintTarget; 87 | } 88 | 89 | impl BatteryLevelIndicator { 90 | pub fn new() -> Self { 91 | Object::builder() 92 | .build() 93 | } 94 | } 95 | 96 | impl Default for BatteryLevelIndicator { 97 | fn default() -> Self { 98 | Self::new() 99 | } 100 | } 101 | 102 | -------------------------------------------------------------------------------- /src/widgets/connected_switch_row.rs: -------------------------------------------------------------------------------- 1 | use glib::{Object, Properties}; 2 | use gtk::glib; 3 | use adw::subclass::prelude::{ActionRowImpl, PreferencesRowImpl}; 4 | use gtk::subclass::prelude::*; 5 | use gtk::prelude::ObjectExt; 6 | use std::cell::RefCell; 7 | 8 | mod imp { 9 | use gtk::prelude::WidgetExt; 10 | 11 | use super::*; 12 | 13 | /// an adw::SwitchRow but with the ability to show a spinning icon next to it 14 | #[derive(Properties, Default, gtk::CompositeTemplate)] 15 | #[template(resource = "/io/github/kaii_lb/Overskride/gtk/connected-switch-row.ui")] 16 | #[properties(wrapper_type = super::ConnectedSwitchRow)] 17 | pub struct ConnectedSwitchRow { 18 | #[template_child] 19 | pub switch: TemplateChild, 20 | #[template_child] 21 | pub spinner: TemplateChild, 22 | 23 | #[property(get, set = Self::set_row_active)] 24 | pub active: RefCell, 25 | #[property(get = Self::get_row_spinning, set = Self::set_row_spinning)] 26 | pub spinning: RefCell, 27 | #[property(set = Self::set_switch_active)] 28 | pub switch_active: RefCell, 29 | #[property(get, set)] 30 | pub has_obex: RefCell, 31 | } 32 | 33 | #[glib::object_subclass] 34 | impl ObjectSubclass for ConnectedSwitchRow { 35 | const NAME: &'static str = "ConnectedSwitchRow"; 36 | type Type = super::ConnectedSwitchRow; 37 | type ParentType = adw::ActionRow; 38 | 39 | fn class_init(klass: &mut Self::Class) { 40 | klass.bind_template(); 41 | } 42 | 43 | fn instance_init(obj: &glib::subclass::InitializingObject) { 44 | obj.init_template(); 45 | } 46 | } 47 | 48 | #[glib::derived_properties] 49 | impl ObjectImpl for ConnectedSwitchRow { 50 | fn constructed(&self) { 51 | self.parent_constructed(); 52 | self.switch.get().connect_activate(|meeee| { 53 | meeee.parent().unwrap().activate(); 54 | }); 55 | } 56 | } 57 | 58 | impl ActionRowImpl for ConnectedSwitchRow {} 59 | impl WidgetImpl for ConnectedSwitchRow {} 60 | impl ListBoxRowImpl for ConnectedSwitchRow {} 61 | impl PreferencesRowImpl for ConnectedSwitchRow {} 62 | 63 | impl ConnectedSwitchRow { 64 | /// sets the `ConnectedSwitchRow`'s state to `active`, make the spinning visible in the process. 65 | fn set_row_active(&self, active: bool) { 66 | let current_active = self.switch.get().is_active(); 67 | 68 | if current_active == active { 69 | return; 70 | } 71 | // println!("current active for custom row is: {}", current_active); 72 | 73 | *self.active.borrow_mut() = active; 74 | self.spinner.set_spinning(true); 75 | } 76 | 77 | /// return the current state of the row's spinner, ie: spinning, or not visible. 78 | fn get_row_spinning(&self) -> bool { 79 | self.spinner.is_spinning() 80 | } 81 | 82 | /// sets the row's spinner to `spinning`. 83 | fn set_row_spinning(&self, spinning: bool) { 84 | self.spinner.set_spinning(spinning); 85 | } 86 | 87 | fn set_switch_active(&self, active: bool) { 88 | self.switch.set_active(active); 89 | *self.active.borrow_mut() = active; 90 | } 91 | } 92 | } 93 | 94 | glib::wrapper! { 95 | pub struct ConnectedSwitchRow(ObjectSubclass) 96 | @extends adw::ActionRow, gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, 97 | @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget; 98 | } 99 | 100 | impl ConnectedSwitchRow { 101 | /// creates a new `ConnectedSwitchRow`, no values in, no values out. 102 | pub fn new() -> Self { 103 | Object::builder() 104 | .build() 105 | } 106 | } 107 | 108 | impl Default for ConnectedSwitchRow { 109 | fn default() -> Self { 110 | Self::new() 111 | } 112 | } 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/widgets/device_action_row.rs: -------------------------------------------------------------------------------- 1 | use glib::{Object, Properties}; 2 | use gtk::glib; 3 | use adw::subclass::prelude::{ActionRowImpl, PreferencesRowImpl}; 4 | use gtk::subclass::prelude::*; 5 | use gtk::prelude::{ObjectExt, WidgetExt}; 6 | use std::cell::RefCell; 7 | 8 | mod imp { 9 | use super::*; 10 | 11 | /// an action row adapted to hold a lot of info about this device and its adapter 12 | #[derive(Properties, Default, gtk::CompositeTemplate)] 13 | #[template(resource = "/io/github/kaii_lb/Overskride/gtk/device-action-row.ui")] 14 | #[properties(wrapper_type = super::DeviceActionRow)] 15 | pub struct DeviceActionRow { 16 | #[template_child] 17 | pub rssi_icon: TemplateChild, 18 | #[template_child] 19 | pub connected_icon: TemplateChild, 20 | 21 | #[property(get, set)] 22 | pub rssi: RefCell, 23 | #[property(get, set)] 24 | pub adapter_name: RefCell, 25 | #[property(get, set = Self::set_current_connected)] 26 | pub connected: RefCell, 27 | #[property(get, set)] 28 | pub trusted: RefCell, 29 | 30 | pub address: RefCell, 31 | pub adapter_address: RefCell, 32 | } 33 | 34 | #[glib::object_subclass] 35 | impl ObjectSubclass for DeviceActionRow { 36 | const NAME: &'static str = "DeviceActionRow"; 37 | type Type = super::DeviceActionRow; 38 | type ParentType = adw::ActionRow; 39 | 40 | fn class_init(klass: &mut Self::Class) { 41 | klass.bind_template(); 42 | } 43 | 44 | fn instance_init(obj: &glib::subclass::InitializingObject) { 45 | obj.init_template(); 46 | } 47 | } 48 | 49 | #[glib::derived_properties] 50 | impl ObjectImpl for DeviceActionRow { 51 | fn constructed(&self) { 52 | self.parent_constructed(); 53 | } 54 | } 55 | 56 | impl ActionRowImpl for DeviceActionRow {} 57 | impl WidgetImpl for DeviceActionRow {} 58 | impl ListBoxRowImpl for DeviceActionRow {} 59 | impl PreferencesRowImpl for DeviceActionRow {} 60 | 61 | impl DeviceActionRow { 62 | fn set_current_connected(&self, connected: bool) { 63 | let icon = self.connected_icon.get(); 64 | 65 | if connected { 66 | icon.show(); 67 | } 68 | else { 69 | icon.hide(); 70 | } 71 | 72 | self.connected.set(connected); 73 | } 74 | } 75 | } 76 | 77 | glib::wrapper! { 78 | pub struct DeviceActionRow(ObjectSubclass) 79 | @extends adw::ActionRow, gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, 80 | @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget; 81 | } 82 | 83 | impl DeviceActionRow { 84 | /// creates a new `DeviceActionRow`, no values in, no values out. 85 | pub fn new() -> Self { 86 | Object::builder() 87 | .build() 88 | } 89 | 90 | pub fn get_bluer_address(&self) -> bluer::Address { 91 | *self.imp().address.borrow() 92 | } 93 | 94 | pub fn set_bluer_address(&self, address: bluer::Address) { 95 | *self.imp().address.borrow_mut() = address; 96 | } 97 | 98 | pub fn get_bluer_adapter_address(&self) -> bluer::Address { 99 | *self.imp().adapter_address.borrow() 100 | } 101 | 102 | pub fn set_bluer_adapter_address(&self, address: bluer::Address) { 103 | *self.imp().adapter_address.borrow_mut() = address; 104 | } 105 | 106 | /// updates the rssi icon of this row to one of the preset icons depending on current rssi 107 | pub fn update_rssi_icon(&self) { 108 | let icon_name = match *self.imp().rssi.borrow() { 109 | 0 => { 110 | "rssi-none-symbolic" 111 | }, 112 | n if -n <= 60 => { 113 | "rssi-high-symbolic" 114 | } 115 | n if -n <= 70 => { 116 | "rssi-medium-symbolic" 117 | } 118 | n if -n <= 80 => { 119 | "rssi-low-symbolic" 120 | } 121 | n if -n <= 90 => { 122 | "rssi-dead-symbolic" 123 | } 124 | n if -n <= 100 => { 125 | "rssi-none-symbolic" 126 | } 127 | val => { 128 | println!("rssi unknown value: {}", val); 129 | "rssi-not-found-symbolic" 130 | } 131 | }; 132 | 133 | self.imp().rssi_icon.set_icon_name(Some(icon_name)); 134 | } 135 | } 136 | 137 | impl Default for DeviceActionRow { 138 | fn default() -> Self { 139 | Self::new() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/widgets/more_info_page.rs: -------------------------------------------------------------------------------- 1 | use glib::Object; 2 | use gtk::glib; 3 | use adw::subclass::prelude::AdwApplicationWindowImpl; 4 | use gtk::subclass::prelude::*; 5 | use adw::prelude::PreferencesRowExt; 6 | use adw::prelude::ExpanderRowExt; 7 | use gtk::prelude::WidgetExt; 8 | 9 | mod imp { 10 | use super::*; 11 | 12 | /// a custom error message if the startup failed, showing the user a way to fix the error 13 | #[derive(Default, gtk::CompositeTemplate)] 14 | #[template(resource = "/io/github/kaii_lb/Overskride/gtk/more-info-page.ui")] 15 | pub struct MoreInfoPage { 16 | #[template_child] 17 | pub name_row: TemplateChild, 18 | #[template_child] 19 | pub address_row: TemplateChild, 20 | #[template_child] 21 | pub manufacturer_row: TemplateChild, 22 | #[template_child] 23 | pub type_row: TemplateChild, 24 | #[template_child] 25 | pub distance_row: TemplateChild, 26 | #[template_child] 27 | pub services_row: TemplateChild, 28 | } 29 | 30 | #[glib::object_subclass] 31 | impl ObjectSubclass for MoreInfoPage { 32 | const NAME: &'static str = "MoreInfoPage"; 33 | type Type = super::MoreInfoPage; 34 | type ParentType = adw::ApplicationWindow; 35 | 36 | fn class_init(klass: &mut Self::Class) { 37 | klass.bind_template(); 38 | } 39 | 40 | fn instance_init(obj: &glib::subclass::InitializingObject) { 41 | obj.init_template(); 42 | } 43 | } 44 | 45 | impl ObjectImpl for MoreInfoPage { 46 | fn constructed(&self) { 47 | self.parent_constructed(); 48 | } 49 | } 50 | 51 | impl WidgetImpl for MoreInfoPage {} 52 | impl AdwApplicationWindowImpl for MoreInfoPage {} 53 | impl ApplicationWindowImpl for MoreInfoPage {} 54 | impl WindowImpl for MoreInfoPage {} 55 | 56 | impl MoreInfoPage {} 57 | } 58 | 59 | glib::wrapper! { 60 | pub struct MoreInfoPage(ObjectSubclass) 61 | @extends adw::ApplicationWindow, gtk::Widget, gtk::Window, gtk::ApplicationWindow, 62 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; 63 | } 64 | 65 | impl MoreInfoPage { 66 | /// creates a new `MoreInfoPage` 67 | pub fn new() -> Self { 68 | Object::builder() 69 | .build() 70 | } 71 | 72 | pub fn initialize_from_info(&self, name: String, address: String, manufactuer: String, device_type: String, distance: String, services_list: Vec) { 73 | self.imp().name_row.get().set_title(&("Name: ".to_string() + &name)); 74 | self.imp().address_row.get().set_title(&("Address: ".to_string() + &address)); 75 | self.imp().manufacturer_row.get().set_title(&("Manufacturer: ".to_string() + &manufactuer)); 76 | self.imp().type_row.get().set_title(&("Type: ".to_string() + &device_type)); 77 | self.imp().distance_row.get().set_title(&("Distance: ".to_string() + &distance)); 78 | 79 | let expander_row = self.imp().services_row.get(); 80 | expander_row.set_title("Available Services"); 81 | if services_list.is_empty() { 82 | expander_row.set_sensitive(false); 83 | } 84 | else { 85 | expander_row.set_sensitive(true); 86 | 87 | for service in services_list { 88 | let row = adw::ActionRow::new(); 89 | row.set_title(&service); 90 | row.set_title_selectable(true); 91 | 92 | expander_row.add_row(&row); 93 | } 94 | } 95 | } 96 | } 97 | 98 | impl Default for MoreInfoPage { 99 | fn default() -> Self { 100 | Self::new() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/widgets/receiving_popover.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::WidgetExt; 2 | use glib::Object; 3 | use gtk::glib; 4 | use gtk::prelude::{IsA, Cast}; 5 | use gtk::subclass::prelude::*; 6 | 7 | use crate::receiving_row::ReceivingRow; 8 | 9 | mod imp { 10 | use super::*; 11 | 12 | /// holds all the current transfer ongoing allowing the user easy management of them 13 | #[derive(Default, gtk::CompositeTemplate)] 14 | #[template(resource = "/io/github/kaii_lb/Overskride/gtk/receiving-popover.ui")] 15 | pub struct ReceivingPopover { 16 | #[template_child] 17 | pub listbox: TemplateChild, 18 | #[template_child] 19 | pub default_row: TemplateChild, 20 | } 21 | 22 | #[glib::object_subclass] 23 | impl ObjectSubclass for ReceivingPopover { 24 | const NAME: &'static str = "ReceivingPopover"; 25 | type Type = super::ReceivingPopover; 26 | type ParentType = gtk::Popover; 27 | 28 | fn class_init(klass: &mut Self::Class) { 29 | klass.bind_template(); 30 | } 31 | 32 | fn instance_init(obj: &glib::subclass::InitializingObject) { 33 | obj.init_template(); 34 | } 35 | } 36 | 37 | impl ObjectImpl for ReceivingPopover { 38 | fn constructed(&self) { 39 | self.parent_constructed(); 40 | } 41 | } 42 | 43 | impl WidgetImpl for ReceivingPopover {} 44 | impl PopoverImpl for ReceivingPopover {} 45 | } 46 | 47 | glib::wrapper! { 48 | pub struct ReceivingPopover(ObjectSubclass) 49 | @extends gtk::Popover, gtk::Widget, 50 | @implements gtk::Accessible, gtk::Native, gtk::Buildable, gtk::ConstraintTarget, gtk::ShortcutManager; 51 | } 52 | 53 | impl ReceivingPopover { 54 | /// creates a new `ReceivingPopover`, no values in, no values out. 55 | pub fn new() -> Self { 56 | Object::builder() 57 | .build() 58 | } 59 | 60 | /// adds a row, enabling or disabling the "no transfers" label as it sees fit 61 | pub fn add_row(&self, row: &impl IsA) { 62 | let listbox = self.imp().listbox.get(); 63 | listbox.append(row); 64 | 65 | println!("added row"); 66 | 67 | if listbox.row_at_index(2).is_some() { 68 | listbox.set_show_separators(true); 69 | } 70 | self.imp().default_row.get().set_visible(false); 71 | } 72 | 73 | /// remove the row from this popover, using the transfer and filename as guidance 74 | pub fn remove_row(&self, transfer: String, filename: String) { 75 | let listbox = self.imp().listbox.get(); 76 | 77 | let mut index = 0; 78 | while let Some(row) = listbox.row_at_index(index) { 79 | if let Ok(receiving_row) = row.clone().downcast::() { 80 | if receiving_row.transfer().contains(&transfer) && receiving_row.filename().contains(&filename) { 81 | listbox.remove(&row); 82 | println!("removed row"); 83 | } 84 | } 85 | 86 | index += 1; 87 | } 88 | 89 | if listbox.row_at_index(1).is_none() { 90 | self.imp().default_row.get().set_visible(true); 91 | listbox.set_show_separators(false); 92 | } 93 | else { 94 | self.imp().default_row.get().set_visible(false); 95 | 96 | if listbox.row_at_index(2).is_some() { 97 | listbox.set_show_separators(true); 98 | } 99 | } 100 | } 101 | 102 | pub fn get_row_by_transfer(&self, transfer: &String, filename: &String) -> Option { 103 | let listbox = self.imp().listbox.get(); 104 | 105 | let mut index = 0; 106 | while let Some(row) = listbox.row_at_index(index) { 107 | if let Ok(receiving_row) = row.clone().downcast::() { 108 | if receiving_row.transfer().contains(transfer) && receiving_row.filename().contains(filename) { 109 | return Some(receiving_row); 110 | } 111 | } 112 | 113 | index += 1; 114 | } 115 | println!("unknown row {} {}", transfer, filename); 116 | None 117 | } 118 | } 119 | 120 | impl Default for ReceivingPopover { 121 | fn default() -> Self { 122 | Self::new() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/widgets/receiving_row.rs: -------------------------------------------------------------------------------- 1 | use adw::prelude::ButtonExt; 2 | use glib::{Object, Properties}; 3 | use gtk::glib; 4 | use gtk::subclass::prelude::*; 5 | use gtk::prelude::ObjectExt; 6 | use std::cell::RefCell; 7 | use gtk::prelude::WidgetExt; 8 | 9 | use crate::obex::CANCEL; 10 | 11 | mod imp { 12 | use super::*; 13 | 14 | /// custom type for a transfer UI, many methods to allow easy manipulation of a transfers state 15 | #[derive(Properties, Default, gtk::CompositeTemplate)] 16 | #[template(resource = "/io/github/kaii_lb/Overskride/gtk/receiving-row.ui")] 17 | #[properties(wrapper_type = super::ReceivingRow)] 18 | pub struct ReceivingRow { 19 | #[template_child] 20 | pub title_label: TemplateChild, 21 | #[template_child] 22 | pub progress_bar: TemplateChild, 23 | #[template_child] 24 | pub extra_label: TemplateChild, 25 | #[template_child] 26 | pub cancel_button: TemplateChild, 27 | 28 | #[property(get, set)] 29 | pub transfer: RefCell, 30 | #[property(get = Self::get_filename_from_label, set = Self::set_filename_from_label)] 31 | pub filename: RefCell, 32 | #[property(get, set = Self::set_progress_bar_fraction)] 33 | pub percentage: RefCell, 34 | #[property(get, set)] 35 | pub filesize: RefCell, 36 | #[property(get, set)] 37 | pub outbound: RefCell, 38 | } 39 | 40 | #[glib::object_subclass] 41 | impl ObjectSubclass for ReceivingRow { 42 | const NAME: &'static str = "ReceivingRow"; 43 | type Type = super::ReceivingRow; 44 | type ParentType = gtk::ListBoxRow; 45 | 46 | fn class_init(klass: &mut Self::Class) { 47 | klass.bind_template(); 48 | klass.bind_template_callbacks(); 49 | } 50 | 51 | fn instance_init(obj: &glib::subclass::InitializingObject) { 52 | obj.init_template(); 53 | } 54 | } 55 | 56 | #[glib::derived_properties] 57 | impl ObjectImpl for ReceivingRow { 58 | fn constructed(&self) { 59 | self.parent_constructed(); 60 | } 61 | } 62 | 63 | impl WidgetImpl for ReceivingRow {} 64 | impl ListBoxRowImpl for ReceivingRow {} 65 | 66 | #[gtk::template_callbacks] 67 | impl ReceivingRow { 68 | fn get_filename_from_label(&self) -> String { 69 | self.title_label.get().label().to_string() 70 | } 71 | 72 | fn set_filename_from_label(&self, filename: String) { 73 | if *self.outbound.borrow() { 74 | self.title_label.get().set_label(&("Sending: “".to_string() + &filename + "”")); 75 | } 76 | else { 77 | self.title_label.get().set_label(&("Receiving: “".to_string() + &filename + "”")); 78 | } 79 | } 80 | 81 | fn set_progress_bar_fraction(&self, fraction: f32) { 82 | let holder = (fraction / 100.0) as f64; 83 | // println!("divved {}", holder); 84 | self.progress_bar.get().set_fraction(holder.clamp(0.0, 1.0)); 85 | } 86 | 87 | #[template_callback] 88 | fn cancel_transfer(&self, button: >k::Button) { 89 | unsafe { 90 | CANCEL = true; 91 | } 92 | button.set_sensitive(false); 93 | } 94 | } 95 | } 96 | 97 | glib::wrapper! { 98 | pub struct ReceivingRow(ObjectSubclass) 99 | @extends gtk::ListBoxRow, gtk::Widget, 100 | @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget; 101 | } 102 | 103 | impl ReceivingRow { 104 | /// creates a new `ReceivingRow` from a transfer, filename, size and if its sending or receiving 105 | pub fn new(transfer: String, filename: String, filesize: f32, outbound: bool) -> Self { 106 | Object::builder() 107 | .property("transfer", transfer) 108 | .property("outbound", outbound) 109 | .property("filename", filename) 110 | .property("filesize", filesize) 111 | .build() 112 | } 113 | 114 | pub fn get_extra(&self) -> String { 115 | self.imp().extra_label.get().label().to_string() 116 | } 117 | 118 | /// extra is the little text at the bottom of the transfer, this sets it 119 | pub fn set_extra(&self, percent: f32, current_mb: f32, filesize_mb: f32, transfer_rate: u64) { 120 | let percentage = percent.to_string() + "% | "; 121 | 122 | let formatted_mb = format!("{:.2}", current_mb); 123 | let size = formatted_mb + "/" + &filesize_mb.to_string(); 124 | let rate = " at ".to_string() + transfer_rate.to_string().trim() + "KB/s"; 125 | 126 | let extra = "".to_string() + &percentage + &size + &rate + ""; 127 | self.set_filesize(filesize_mb); 128 | self.set_percentage(percent); 129 | self.imp().extra_label.get().set_label(&extra); 130 | } 131 | 132 | // changes the extra to the error string 133 | pub fn set_error(&self, error: String) { 134 | let final_string = "".to_string() + &error + ""; 135 | self.imp().extra_label.get().set_label(&final_string); 136 | } 137 | 138 | /// changes the transfers icon based on if the transfer is done, error, or still running 139 | pub fn set_active_icon(&self, icon_name: String, filesize: f32) -> bool { 140 | let cancel_button = self.imp().cancel_button.get(); 141 | let self_destruct: bool; 142 | 143 | let icon = match icon_name.as_str() { 144 | // make icon an X and tell user its complete 145 | "complete" => { 146 | cancel_button.set_sensitive(false); 147 | self_destruct = true; 148 | 149 | let done = "File Transfer Completed (".to_string() + &filesize.to_string() + " MB)"; 150 | self.set_error(done); 151 | 152 | "check-plain-symbolic" 153 | }, 154 | // make icon a skull and tell user transfer got bent 155 | "error" => { 156 | cancel_button.set_sensitive(false); 157 | self_destruct = true; 158 | 159 | let done = "File Transfer Canceled (? MB)".to_string(); 160 | self.set_error(done); 161 | self.imp().progress_bar.get().set_sensitive(false); 162 | 163 | "skull-symbolic" 164 | }, 165 | // notify user of "special case", this is most likely its still running 166 | e => { 167 | if !e.is_empty() { 168 | println!("special icon case: {}", e); 169 | } 170 | self_destruct = false; 171 | "cross-large-symbolic" 172 | }, 173 | }; 174 | 175 | cancel_button.set_icon_name(icon); 176 | self_destruct 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/widgets/selectable_row.rs: -------------------------------------------------------------------------------- 1 | use glib::{Object, Properties}; 2 | use gtk::glib; 3 | use adw::subclass::prelude::{ActionRowImpl, PreferencesRowImpl}; 4 | use gtk::subclass::prelude::*; 5 | use gtk::prelude::ObjectExt; 6 | use std::cell::RefCell; 7 | use gtk::prelude::WidgetExt; 8 | 9 | mod imp { 10 | use super::*; 11 | 12 | /// a custom type that has a checkmark next to the row, showing it if the row is selected, hiding it if not 13 | #[derive(Properties, Default, gtk::CompositeTemplate)] 14 | #[template(resource = "/io/github/kaii_lb/Overskride/gtk/selectable-row.ui")] 15 | #[properties(wrapper_type = super::SelectableRow)] 16 | pub struct SelectableRow { 17 | #[template_child] 18 | pub check_icon: TemplateChild, 19 | 20 | #[property(get, set)] 21 | pub profile: RefCell, 22 | #[property(get, set = Self::set_row_selected)] 23 | pub selected: RefCell, 24 | } 25 | 26 | #[glib::object_subclass] 27 | impl ObjectSubclass for SelectableRow { 28 | const NAME: &'static str = "SelectableRow"; 29 | type Type = super::SelectableRow; 30 | type ParentType = adw::ActionRow; 31 | 32 | fn class_init(klass: &mut Self::Class) { 33 | klass.bind_template(); 34 | } 35 | 36 | fn instance_init(obj: &glib::subclass::InitializingObject) { 37 | obj.init_template(); 38 | } 39 | } 40 | 41 | #[glib::derived_properties] 42 | impl ObjectImpl for SelectableRow { 43 | fn constructed(&self) { 44 | self.parent_constructed(); 45 | } 46 | } 47 | 48 | impl ActionRowImpl for SelectableRow {} 49 | impl WidgetImpl for SelectableRow {} 50 | impl ListBoxRowImpl for SelectableRow {} 51 | impl PreferencesRowImpl for SelectableRow {} 52 | 53 | impl SelectableRow { 54 | /// adds a checkmark next to the row if selected, removes it if not 55 | pub fn set_row_selected(&self, active: bool) { 56 | *self.selected.borrow_mut() = active; 57 | let check_icon = self.check_icon.get(); 58 | 59 | if *self.selected.borrow() { 60 | check_icon.show(); 61 | } 62 | else { 63 | check_icon.hide(); 64 | } 65 | } 66 | } 67 | } 68 | 69 | glib::wrapper! { 70 | pub struct SelectableRow(ObjectSubclass) 71 | @extends adw::ActionRow, gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, 72 | @implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget; 73 | } 74 | 75 | impl SelectableRow { 76 | /// creates a new `SelectableRow`, no values in, no values out. 77 | pub fn new() -> Self { 78 | Object::builder() 79 | .build() 80 | } 81 | } 82 | 83 | impl Default for SelectableRow { 84 | fn default() -> Self { 85 | Self::new() 86 | } 87 | } 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/widgets/startup_error_message.rs: -------------------------------------------------------------------------------- 1 | use glib::Object; 2 | use gtk::glib; 3 | use adw::subclass::prelude::AdwApplicationWindowImpl; 4 | use gtk::subclass::prelude::*; 5 | 6 | mod imp { 7 | use adw::prelude::ButtonExt; 8 | 9 | use super::*; 10 | 11 | /// a custom error message if the startup failed, showing the user a way to fix the error 12 | #[derive(Default, gtk::CompositeTemplate)] 13 | #[template(resource = "/io/github/kaii_lb/Overskride/gtk/startup-error-message.ui")] 14 | pub struct StartupErrorMessage { 15 | #[template_child] 16 | pub run_enable_bluetooth_button: TemplateChild, 17 | #[template_child] 18 | pub error_toast_overlay: TemplateChild, 19 | } 20 | 21 | #[glib::object_subclass] 22 | impl ObjectSubclass for StartupErrorMessage { 23 | const NAME: &'static str = "StartupErrorMessage"; 24 | type Type = super::StartupErrorMessage; 25 | type ParentType = adw::ApplicationWindow; 26 | 27 | fn class_init(klass: &mut Self::Class) { 28 | klass.bind_template(); 29 | } 30 | 31 | fn instance_init(obj: &glib::subclass::InitializingObject) { 32 | obj.init_template(); 33 | } 34 | } 35 | 36 | impl ObjectImpl for StartupErrorMessage { 37 | fn constructed(&self) { 38 | self.parent_constructed(); 39 | let toast_overlay = self.error_toast_overlay.get(); 40 | self.run_enable_bluetooth_button.connect_clicked(move |_| { 41 | 42 | if std::env::var("container").is_err() { 43 | let argv = [std::ffi::OsStr::new("pkexec"), std::ffi::OsStr::new("systemctl"), std::ffi::OsStr::new("enable"), 44 | std::ffi::OsStr::new("--now"), std::ffi::OsStr::new("bluetooth")]; 45 | 46 | let argv2 = [std::ffi::OsStr::new("pkexec"), std::ffi::OsStr::new("systemctl"), std::ffi::OsStr::new("start"), 47 | std::ffi::OsStr::new("bluetooth")]; 48 | 49 | gtk::gio::Subprocess::newv(&argv, gtk::gio::SubprocessFlags::STDERR_PIPE).expect("cannot enable bluetooth by pkexec"); 50 | gtk::gio::Subprocess::newv(&argv2, gtk::gio::SubprocessFlags::STDERR_PIPE).expect("cannot enable bluetooth by pkexec"); 51 | 52 | let toast = adw::Toast::new("applying commands through pkexec"); 53 | toast.set_timeout(5); 54 | 55 | toast_overlay.add_toast(toast); 56 | } 57 | else { 58 | let display = gtk::gdk::Display::default().unwrap(); 59 | let clipboard = gtk::prelude::DisplayExt::clipboard(&display); 60 | clipboard.set_text("sudo systemctl enable --now bluetooth"); 61 | 62 | let toast = adw::Toast::new("copied command to clipboard"); 63 | toast.set_timeout(5); 64 | 65 | toast_overlay.add_toast(toast); 66 | } 67 | }); 68 | } 69 | } 70 | 71 | impl WidgetImpl for StartupErrorMessage {} 72 | impl AdwApplicationWindowImpl for StartupErrorMessage {} 73 | impl ApplicationWindowImpl for StartupErrorMessage {} 74 | impl WindowImpl for StartupErrorMessage {} 75 | 76 | impl StartupErrorMessage {} 77 | } 78 | 79 | glib::wrapper! { 80 | pub struct StartupErrorMessage(ObjectSubclass) 81 | @extends adw::ApplicationWindow, gtk::Widget, gtk::Window, gtk::ApplicationWindow, 82 | @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; 83 | } 84 | 85 | impl StartupErrorMessage { 86 | /// creates a new `StartupErrorMessage` 87 | pub fn new() -> Self { 88 | Object::builder() 89 | .build() 90 | } 91 | } 92 | 93 | impl Default for StartupErrorMessage { 94 | fn default() -> Self { 95 | Self::new() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /subprojects/blueprint-compiler.wrap: -------------------------------------------------------------------------------- 1 | [wrap-git] 2 | directory = blueprint-compiler 3 | url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git 4 | revision = v0.10.0 5 | depth = 1 6 | 7 | [provide] 8 | program_names = blueprint-compiler --------------------------------------------------------------------------------