Merge pull request #75 from Xetibo/ina

fix big oopsie
This commit is contained in:
takotori 2023-12-11 13:12:51 +01:00 committed by GitHub
commit 6263ac59bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
98 changed files with 11878 additions and 650 deletions

21
.github/workflows/release-arch.yml vendored Normal file
View file

@ -0,0 +1,21 @@
on:
release:
types: [ created ]
jobs:
release:
runs-on: [self-hosted, arch]
steps:
- uses: actions/checkout@v3
- name: nightly-rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
- name: Build rust package
run: makepkg PKGBUILD
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
reset-${{github.ref_name}}-0-x86_64.pkg.tar.zst

View file

@ -1,21 +1,35 @@
on:
release:
types: [created]
types: [ created ]
jobs:
release:
runs-on: ubuntu-latest
runs-on: [self-hosted, ubuntu]
steps:
- uses: actions/checkout@v3
- name: nightly-rust
uses: actions-rs/toolchain@v1
with:
with:
profile: minimal
toolchain: nightly
- name: Build
- name: Build rust package
run: cargo build --release --verbose
- name: Build Flatpak
run: |
cd flatpak
python3 flatpak-generator.py ../Cargo.lock -o cargo-sources.json
flatpak-builder build org.xetibo.ReSet.json --force-clean
flatpak build-export export build
flatpak build-bundle export reset.flatpak org.xetibo.ReSet
- name: Build Ubuntu package
run: |
cp ./target/release/reset ./debian/.
dpkg-deb --build debian
mv debian.deb reset.deb
- name: Release
uses: softprops/action-gh-release@v1
with:
files: |
target/release/reset
flatpak/reset.flatpak
reset.deb

View file

@ -2,6 +2,7 @@ name: Rust
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
@ -11,32 +12,26 @@ env:
jobs:
build:
runs-on: ubuntu-latest
runs-on: [self-hosted, ubuntu]
steps:
- uses: actions/checkout@v3
- name: nightly-rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
- name: Cache
uses: actions/cache@v3
with:
path: target/debug
key: ${{ runner.os }}-cache
- name: run code coverage
uses: actions-rs/tarpaulin@v0.1
with:
version: '0.15.0'
args: '-- --test-threads 1'
- name: upload code coverage
uses: actions/upload-artifact@v1
with:
name: code-coverage-report
path: cobertura.xml
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- uses: actions/checkout@v3
- name: nightly-rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
- name: run code coverage
uses: actions-rs/tarpaulin@v0.1
with:
version: '0.15.0'
args: '-- --test-threads 1'
- name: upload code coverage
uses: actions/upload-artifact@v1
with:
name: code-coverage-report
path: cobertura.xml
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

8
.gitignore vendored
View file

@ -2,6 +2,12 @@
# will have compiled files and executables
debug/
target/
flatpak/build
flatpak/.flatpak-builder
flatpak/export
flatpak/reset.flatpak
pkg/
*.pkg.tar.zst
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
@ -17,4 +23,4 @@ Cargo.lock
# Added by cargo
/target
.idea/
.idea/

View file

@ -5,9 +5,22 @@ edition = "2021"
description = "A wip universal Linux settings application."
[dependencies]
ReSet-Lib = "0.1.0"
adw = { version = "0.5.3", package = "libadwaita" }
gtk = { version = "0.7.3", package = "gtk4" }
reset_daemon = "0.4.6"
re_set-lib = "0.6.5"
adw = { version = "0.5.3", package = "libadwaita", features = ["v1_4"] }
dbus = "0.9.7"
gtk = { version = "0.7.3", package = "gtk4", features = ["v4_12"] }
glib = "0.18.3"
tokio = { version = "1.33.0", features = [
"rt",
"time",
"net",
"macros",
"rt-multi-thread",
"sync",
] }
fork = "0.1.22"
ipnetwork = "0.20.0"
[build-dependencies]
glib-build-tools = "0.18.0"

20
PKGBUILD Normal file
View file

@ -0,0 +1,20 @@
# Maintainer: Fabio Lenherr <dashie@dashie.org>
pkgname=reset
pkgver=0.1
pkgrel=0
arch=('x86_64')
pkgdir="/usr/bin/${pkgname}"
pkgdesc="A wip universal Linux settings application."
depends=('rust' 'gtk4' 'dbus')
build() {
cargo build --release
}
package() {
cd ..
install -Dm755 target/release/"$pkgname" "$pkgdir"/usr/bin/"$pkgname"
install -Dm644 "$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop"
install -Dm644 "src/resources/icons/ReSet.svg" "$pkgdir/usr/share/pixmaps/ReSet.svg"
}

View file

@ -4,4 +4,14 @@ fn main() {
"src/resources/resources.gresource.xml",
"src.templates.gresource",
);
}
glib_build_tools::compile_resources(
&["src/resources/icons"],
"src/resources/icons/resources.gresource.xml",
"src.icons.gresource",
);
glib_build_tools::compile_resources(
&["src/resources/style"],
"src/resources/style/resources.gresource.xml",
"src.style.gresource",
);
}

5
debian/DEBIAN/control vendored Normal file
View file

@ -0,0 +1,5 @@
Package: ReSet
Version: 0.1
Maintainer: DashieTM
Architecture: all
Description: A wip universal Linux settings application.

17
flatpak/README.md Normal file
View file

@ -0,0 +1,17 @@
### instructions for building:
- `python3 flatpak-generator.py ../Cargo.lock -o cargo-sources.json`
- `flatpak-builder build org.xetibo.ReSet.json --force-clean`
- `flatpak build-export export build`
- `flatpak build-bundle export reset.flatpak org.xetibo.ReSet`
- you can also use the build.sh script provided
- note: if you are using a point release distribution(ubuntu, debian stable etc. please use the flatpak version of these commands -> flatpak run org.flatpak.Builder build...)
### instructions for installation:
`flatpak install --user reset.flatpak`
### permissions
currently ReSet uses permission on all devices, for some reason otherwise it can't access sound settings like volume changes etc.
This can likely be fixed by implementing portal integration later.

5
flatpak/build.sh Executable file
View file

@ -0,0 +1,5 @@
#! /bin/bash
python3 flatpak-generator.py ../Cargo.lock -o cargo-sources.json
flatpak-builder build org.xetibo.ReSet.json --force-clean
flatpak build-export export build
flatpak build-bundle export reset.flatpak org.xetibo.ReSet

1581
flatpak/cargo-sources.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,381 @@
#!/usr/bin/env python3
# provided by flatpak -> flatpak-builder-tools
# https://github.com/flatpak/flatpak-builder-tools/tree/master/cargo
__license__ = 'MIT'
import json
from urllib.parse import urlparse, ParseResult, parse_qs
import os
import contextlib
import glob
import subprocess
import argparse
import logging
import hashlib
import asyncio
import aiohttp
import toml
CRATES_IO = 'https://static.crates.io/crates'
CARGO_HOME = 'cargo'
CARGO_CRATES = f'{CARGO_HOME}/vendor'
VENDORED_SOURCES = 'vendored-sources'
GIT_CACHE = 'flatpak-cargo/git'
COMMIT_LEN = 7
@contextlib.contextmanager
def workdir(path: str):
oldpath = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(oldpath)
def canonical_url(url):
'Converts a string to a Cargo Canonical URL, as per https://github.com/rust-lang/cargo/blob/35c55a93200c84a4de4627f1770f76a8ad268a39/src/cargo/util/canonical_url.rs#L19'
# Hrm. The upstream cargo does not replace those URLs, but if we don't then it doesn't work too well :(
url = url.replace('git+https://', 'https://')
u = urlparse(url)
# It seems cargo drops query and fragment
u = ParseResult(u.scheme, u.netloc, u.path, None, None, None)
u = u._replace(path = u.path.rstrip('/'))
if u.netloc == 'github.com':
u = u._replace(scheme = 'https')
u = u._replace(path = u.path.lower())
if u.path.endswith('.git'):
u = u._replace(path = u.path[:-len('.git')])
return u
def get_git_tarball(repo_url, commit):
url = canonical_url(repo_url)
path = url.path.split('/')[1:]
assert len(path) == 2
owner = path[0]
if path[1].endswith('.git'):
repo = path[1].replace('.git', '')
else:
repo = path[1]
if url.hostname == 'github.com':
return f'https://codeload.{url.hostname}/{owner}/{repo}/tar.gz/{commit}'
elif url.hostname.split('.')[0] == 'gitlab':
return f'https://{url.hostname}/{owner}/{repo}/-/archive/{commit}/{repo}-{commit}.tar.gz'
elif url.hostname == 'bitbucket.org':
return f'https://{url.hostname}/{owner}/{repo}/get/{commit}.tar.gz'
else:
raise ValueError(f'Don\'t know how to get tarball for {repo_url}')
async def get_remote_sha256(url):
logging.info(f"started sha256({url})")
sha256 = hashlib.sha256()
async with aiohttp.ClientSession(raise_for_status=True) as http_session:
async with http_session.get(url) as response:
while True:
data = await response.content.read(4096)
if not data:
break
sha256.update(data)
logging.info(f"done sha256({url})")
return sha256.hexdigest()
def load_toml(tomlfile='Cargo.lock'):
with open(tomlfile, 'r') as f:
toml_data = toml.load(f)
return toml_data
def git_repo_name(git_url, commit):
name = canonical_url(git_url).path.split('/')[-1]
return f'{name}-{commit[:COMMIT_LEN]}'
def fetch_git_repo(git_url, commit):
repo_dir = git_url.replace('://', '_').replace('/', '_')
cache_dir = os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
clone_dir = os.path.join(cache_dir, 'flatpak-cargo', repo_dir)
if not os.path.isdir(os.path.join(clone_dir, '.git')):
subprocess.run(['git', 'clone', '--depth=1', git_url, clone_dir], check=True)
rev_parse_proc = subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=clone_dir, check=True,
stdout=subprocess.PIPE)
head = rev_parse_proc.stdout.decode().strip()
if head[:COMMIT_LEN] != commit[:COMMIT_LEN]:
subprocess.run(['git', 'fetch', 'origin', commit], cwd=clone_dir, check=True)
subprocess.run(['git', 'checkout', commit], cwd=clone_dir, check=True)
return clone_dir
async def get_git_repo_packages(git_url, commit):
logging.info('Loading packages from %s', git_url)
git_repo_dir = fetch_git_repo(git_url, commit)
packages = {}
with workdir(git_repo_dir):
if os.path.isfile('Cargo.toml'):
packages.update(await get_cargo_toml_packages(load_toml('Cargo.toml'), '.'))
else:
for toml_path in glob.glob('*/Cargo.toml'):
packages.update(await get_cargo_toml_packages(load_toml(toml_path),
os.path.dirname(toml_path)))
assert packages, f"No packages found in {git_repo_dir}"
logging.debug('Packages in %s:\n%s', git_url, json.dumps(packages, indent=4))
return packages
async def get_cargo_toml_packages(root_toml, root_dir):
assert not os.path.isabs(root_dir) and os.path.isdir(root_dir)
assert 'package' in root_toml or 'workspace' in root_toml
packages = {}
excluded_paths = None
def is_excluded(path):
if not excluded_paths:
return False
return any(os.path.commonpath([excluded_path, path]) == excluded_path
for excluded_path in excluded_paths)
async def get_dep_packages(entry, toml_dir):
assert not os.path.isabs(toml_dir)
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
if 'dependencies' in entry:
for dep_name, dep in entry['dependencies'].items():
if 'package' in dep:
dep_name = dep['package']
if 'path' not in dep:
continue
if dep_name in packages:
continue
dep_dir = os.path.normpath(os.path.join(toml_dir, dep['path']))
if is_excluded(dep_dir):
logging.warning("Excluded dependency at %s", dep_dir)
return
logging.debug("Loading dependency %s from %s", dep_name, dep_dir)
dep_toml = load_toml(os.path.join(dep_dir, 'Cargo.toml'))
assert dep_toml['package']['name'] == dep_name, toml_dir
await get_dep_packages(dep_toml, dep_dir)
packages[dep_name] = dep_dir
if 'target' in entry:
for _, target in entry['target'].items():
await get_dep_packages(target, toml_dir)
if 'package' in root_toml:
await get_dep_packages(root_toml, root_dir)
packages[root_toml['package']['name']] = root_dir
if 'workspace' in root_toml:
workspace = root_toml['workspace']
if 'exclude' in workspace:
excluded_paths = [os.path.normpath(os.path.join(root_dir, excluded))
for excluded in workspace['exclude']]
for member in workspace.get('members', []):
for subpkg_toml in glob.glob(os.path.join(root_dir, member, 'Cargo.toml')):
subpkg = os.path.normpath(os.path.dirname(subpkg_toml))
if is_excluded(subpkg):
logging.warning("Excluded member at %s", subpkg)
continue
logging.debug("Loading workspace member %s in %s", member, root_dir)
pkg_toml = load_toml(subpkg_toml)
await get_dep_packages(pkg_toml, subpkg)
packages[pkg_toml['package']['name']] = subpkg
return packages
async def get_git_repo_sources(url, commit, tarball=False):
name = git_repo_name(url, commit)
if tarball:
tarball_url = get_git_tarball(url, commit)
git_repo_sources = [{
'type': 'archive',
'archive-type': 'tar-gzip',
'url': tarball_url,
'sha256': await get_remote_sha256(tarball_url),
'dest': f'{GIT_CACHE}/{name}',
}]
else:
git_repo_sources = [{
'type': 'git',
'url': url,
'commit': commit,
'dest': f'{GIT_CACHE}/{name}',
}]
return git_repo_sources
async def get_git_package_sources(package, git_repos):
name = package['name']
source = package['source']
commit = urlparse(source).fragment
assert commit, 'The commit needs to be indicated in the fragement part'
canonical = canonical_url(source)
repo_url = canonical.geturl()
git_repo = git_repos.setdefault(repo_url, {
'commits': {},
'lock': asyncio.Lock(),
})
async with git_repo['lock']:
if commit not in git_repo['commits']:
git_repo['commits'][commit] = await get_git_repo_packages(repo_url, commit)
cargo_vendored_entry = {
repo_url: {
'git': repo_url,
'replace-with': VENDORED_SOURCES,
}
}
rev = parse_qs(urlparse(source).query).get('rev')
tag = parse_qs(urlparse(source).query).get('tag')
branch = parse_qs(urlparse(source).query).get('branch')
if rev:
assert len(rev) == 1
cargo_vendored_entry[repo_url]['rev'] = rev[0]
elif tag:
assert len(tag) == 1
cargo_vendored_entry[repo_url]['tag'] = tag[0]
elif branch:
assert len(branch) == 1
cargo_vendored_entry[repo_url]['branch'] = branch[0]
logging.info("Adding package %s from %s", name, repo_url)
pkg_subpath = git_repo['commits'][commit][name]
pkg_repo_dir = os.path.join(GIT_CACHE, git_repo_name(repo_url, commit), pkg_subpath)
git_sources = [
{
'type': 'shell',
'commands': [
f'cp -r --reflink=auto "{pkg_repo_dir}" "{CARGO_CRATES}/{name}"'
],
},
{
'type': 'inline',
'contents': json.dumps({'package': None, 'files': {}}),
'dest': f'{CARGO_CRATES}/{name}', #-{version}',
'dest-filename': '.cargo-checksum.json',
}
]
return (git_sources, cargo_vendored_entry)
async def get_package_sources(package, cargo_lock, git_repos):
metadata = cargo_lock.get('metadata')
name = package['name']
version = package['version']
if 'source' not in package:
logging.debug('%s has no source', name)
return
source = package['source']
if source.startswith('git+'):
return await get_git_package_sources(package, git_repos)
key = f'checksum {name} {version} ({source})'
if metadata is not None and key in metadata:
checksum = metadata[key]
elif 'checksum' in package:
checksum = package['checksum']
else:
logging.warning(f'{name} doesn\'t have checksum')
return
crate_sources = [
{
'type': 'archive',
'archive-type': 'tar-gzip',
'url': f'{CRATES_IO}/{name}/{name}-{version}.crate',
'sha256': checksum,
'dest': f'{CARGO_CRATES}/{name}-{version}',
},
{
'type': 'inline',
'contents': json.dumps({'package': checksum, 'files': {}}),
'dest': f'{CARGO_CRATES}/{name}-{version}',
'dest-filename': '.cargo-checksum.json',
},
]
return (crate_sources, {'crates-io': {'replace-with': VENDORED_SOURCES}})
async def generate_sources(cargo_lock, git_tarballs=False):
# {
# "git-repo-url": {
# "lock": asyncio.Lock(),
# "commits": {
# "commit-hash": {
# "package-name": "./relative/package/path"
# }
# }
# }
# }
git_repos = {}
sources = []
package_sources = []
cargo_vendored_sources = {
VENDORED_SOURCES: {'directory': f'{CARGO_CRATES}'},
}
pkg_coros = [get_package_sources(p, cargo_lock, git_repos) for p in cargo_lock['package']]
for pkg in await asyncio.gather(*pkg_coros):
if pkg is None:
continue
else:
pkg_sources, cargo_vendored_entry = pkg
package_sources.extend(pkg_sources)
cargo_vendored_sources.update(cargo_vendored_entry)
logging.debug('Adding collected git repos:\n%s', json.dumps(list(git_repos), indent=4))
git_repo_coros = []
for git_url, git_repo in git_repos.items():
for git_commit in git_repo['commits']:
git_repo_coros.append(get_git_repo_sources(git_url, git_commit, git_tarballs))
sources.extend(sum(await asyncio.gather(*git_repo_coros), []))
sources.extend(package_sources)
logging.debug('Vendored sources:\n%s', json.dumps(cargo_vendored_sources, indent=4))
sources.append({
'type': 'inline',
'contents': toml.dumps({
'source': cargo_vendored_sources,
}),
'dest': CARGO_HOME,
'dest-filename': 'config'
})
return sources
def main():
parser = argparse.ArgumentParser()
parser.add_argument('cargo_lock', help='Path to the Cargo.lock file')
parser.add_argument('-o', '--output', required=False, help='Where to write generated sources')
parser.add_argument('-t', '--git-tarballs', action='store_true', help='Download git repos as tarballs')
parser.add_argument('-d', '--debug', action='store_true')
args = parser.parse_args()
if args.output is not None:
outfile = args.output
else:
outfile = 'generated-sources.json'
if args.debug:
loglevel = logging.DEBUG
else:
loglevel = logging.INFO
logging.basicConfig(level=loglevel)
generated_sources = asyncio.run(generate_sources(load_toml(args.cargo_lock),
git_tarballs=args.git_tarballs))
with open(outfile, 'w') as out:
json.dump(generated_sources, out, indent=4, sort_keys=False)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,11 @@
[Desktop Entry]
Name=ReSet
GenericName=SettingsApplication
GenericName[de]=SettingsApplikation
Exec=reset
Terminal=false
Type=Application
Keywords=settings;gtk;
Icon=org.xetibo.ReSet
Categories=Utility;GTK;
StartupNotify=false

View file

@ -0,0 +1,49 @@
{
"app-id": "org.xetibo.ReSet",
"runtime": "org.gnome.Platform",
"runtime-version": "45",
"sdk": "org.gnome.Sdk",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable"
],
"command": "reset",
"finish-args": [
"--socket=session-bus",
"--socket=system-bus",
"--socket=pulseaudio",
"--share=ipc",
"--socket=fallback-x11",
"--socket=wayland",
"--device=dri",
"--device=all",
"--allow=bluetooth"
],
"build-options": {
"append-path": "/usr/lib/sdk/rust-stable/bin"
},
"modules": [
{
"name": "reset",
"buildsystem": "simple",
"build-options": {
"env": {
"CARGO_HOME": "/run/build/reset/cargo"
}
},
"build-commands": [
"cargo --offline fetch --manifest-path Cargo.toml --verbose",
"cargo --offline build --release --verbose",
"install -Dm755 ./target/release/reset -t /app/bin/",
"install -Dm644 ./src/resources/icons/ReSet.svg /app/share/icons/hicolor/scalable/apps/org.xetibo.ReSet.svg",
"install -Dm644 ./flatpak/org.xetibo.ReSet.desktop /app/share/applications/org.xetibo.ReSet.desktop"
],
"sources": [
{
"type": "dir",
"path": ".."
},
"cargo-sources.json"
]
}
]
}

11
reset.desktop Normal file
View file

@ -0,0 +1,11 @@
[Desktop Entry]
Name=ReSet
GenericName=SettingsApplication
GenericName[de]=SettingsApplikation
Exec=reset
Terminal=false
Type=Application
Keywords=settings;gtk;
Icon=ReSet
Categories=Utility;GTK;
StartupNotify=false

View file

@ -0,0 +1,80 @@
use std::time::Duration;
use adw::glib;
use adw::glib::Object;
use adw::prelude::{ComboRowExt, PreferencesRowExt};
use dbus::blocking::Connection;
use dbus::Error;
use glib::subclass::types::ObjectSubclassIsExt;
use glib::{clone, Cast};
use gtk::{gio, StringList, StringObject};
use components::utils::create_dropdown_label_factory;
use re_set_lib::audio::audio_structures::Card;
use crate::components;
use super::card_entry_impl;
glib::wrapper! {
pub struct CardEntry(ObjectSubclass<card_entry_impl::CardEntry>)
@extends adw::ComboRow, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable, adw::PreferencesRow;
}
impl CardEntry {
pub fn new(card: Card) -> Self {
let entry: CardEntry = Object::builder().build();
{
let imp = entry.imp();
let mut map = imp.reset_card_map.borrow_mut();
entry.set_title(&card.name);
let mut index: u32 = 0;
let list = StringList::new(&[]);
for (i, profile) in (0_u32..).zip(card.profiles.iter()) {
if profile.name == card.active_profile {
index = i;
}
list.append(&profile.description);
map.insert(
profile.description.clone(),
(card.index, profile.name.clone()),
);
}
entry.set_model(Some(&list));
entry.set_selected(index);
entry.set_use_subtitle(true);
entry.connect_selected_notify(clone!(@weak imp => move |dropdown| {
let selected = dropdown.selected_item();
if selected.is_none() {
return;
}
let selected = selected.unwrap();
let selected = selected.downcast_ref::<StringObject>().unwrap();
let selected = selected.string().to_string();
let map = imp.reset_card_map.borrow();
let (device_index, profile_name) = map.get(&selected).unwrap();
set_card_profile_of_device(*device_index, profile_name.clone());
}));
entry.set_factory(Some(&create_dropdown_label_factory()));
}
entry
}
}
fn set_card_profile_of_device(device_index: u32, profile_name: String) -> bool {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> = proxy.method_call(
"org.Xetibo.ReSetAudio",
"SetCardProfileOfDevice",
(device_index, profile_name),
);
});
true
}

View file

@ -0,0 +1,53 @@
use adw::subclass::action_row::ActionRowImpl;
use adw::subclass::preferences_row::PreferencesRowImpl;
use adw::subclass::prelude::ComboRowImpl;
use adw::ComboRow;
use std::cell::RefCell;
use std::collections::HashMap;
use gtk::subclass::prelude::*;
use gtk::{glib, CompositeTemplate};
use super::card_entry;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetCardEntry.ui")]
pub struct CardEntry {
// first string is the alias name, the first return string is the index of the adapter and the
// second the name of the profile
pub reset_card_map: RefCell<HashMap<String, (u32, String)>>,
}
#[glib::object_subclass]
impl ObjectSubclass for CardEntry {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetCardEntry";
type Type = card_entry::CardEntry;
type ParentType = ComboRow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ActionRowImpl for CardEntry {}
impl PreferencesRowImpl for CardEntry {}
impl ComboRowImpl for CardEntry {}
impl ObjectImpl for CardEntry {
fn constructed(&self) {}
}
impl ListBoxRowImpl for CardEntry {}
impl WidgetImpl for CardEntry {}
impl WindowImpl for CardEntry {}
impl ApplicationWindowImpl for CardEntry {}

View file

@ -0,0 +1,22 @@
use crate::components::base::list_entry_impl;
use adw::glib;
use adw::glib::{IsA, Object};
use gtk::prelude::ListBoxRowExt;
use gtk::Widget;
glib::wrapper! {
pub struct ListEntry(ObjectSubclass<list_entry_impl::ListEntry>)
@extends gtk::ListBoxRow, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Actionable;
}
unsafe impl Send for ListEntry {}
unsafe impl Sync for ListEntry {}
impl ListEntry {
pub fn new(child: &impl IsA<Widget>) -> Self {
let entry: ListEntry = Object::builder().build();
entry.set_child(Some(child));
entry
}
}

View file

@ -0,0 +1,37 @@
use crate::components::base::list_entry;
use gtk::subclass::prelude::*;
use gtk::{glib, CompositeTemplate};
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetListBoxRow.ui")]
pub struct ListEntry {}
#[glib::object_subclass]
impl ObjectSubclass for ListEntry {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetListBoxRow";
type Type = list_entry::ListEntry;
type ParentType = gtk::ListBoxRow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ListEntry {
fn constructed(&self) {
self.parent_constructed();
}
}
impl ListBoxRowImpl for ListEntry {}
impl WidgetImpl for ListEntry {}
impl WindowImpl for ListEntry {}
impl ApplicationWindowImpl for ListEntry {}

View file

@ -0,0 +1,9 @@
pub mod card_entry;
pub mod card_entry_impl;
pub mod list_entry;
pub mod list_entry_impl;
pub mod popup;
pub mod popup_impl;
pub mod setting_box;
pub mod setting_box_impl;
pub mod utils;

View file

@ -0,0 +1,24 @@
use adw::glib;
use adw::glib::Object;
use gtk::{gdk, Editable, Popover};
use super::popup_impl;
glib::wrapper! {
pub struct Popup(ObjectSubclass<popup_impl::Popup>)
@extends Popover, gtk::Widget,
@implements Editable,gdk::Popup, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl Popup {
pub fn new() -> Self {
let popup: Popup = Object::builder().build();
popup
}
}
impl Default for Popup {
fn default() -> Self {
Self::new()
}
}

View file

@ -0,0 +1,54 @@
use std::cell::RefCell;
use std::sync::Arc;
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate, Label, PasswordEntry, PasswordEntryBuffer, Popover};
use super::popup;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetPopup.ui")]
pub struct Popup {
#[template_child]
pub reset_popup_label: TemplateChild<Label>,
#[template_child]
pub reset_popup_entry: TemplateChild<PasswordEntry>,
#[template_child]
pub reset_popup_button: TemplateChild<Button>,
pub reset_popup_text: Arc<RefCell<PasswordEntryBuffer>>,
}
unsafe impl Send for Popup {}
unsafe impl Sync for Popup {}
#[glib::object_subclass]
impl ObjectSubclass for Popup {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetPopup";
type Type = popup::Popup;
type ParentType = Popover;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for Popup {
fn constructed(&self) {
self.parent_constructed();
}
}
impl WidgetImpl for Popup {}
impl WindowImpl for Popup {}
impl PopoverImpl for Popup {}
impl ApplicationWindowImpl for Popup {}
impl EditableImpl for Popup {}

View file

@ -0,0 +1,19 @@
use crate::components::base::setting_box_impl;
use adw::glib;
use adw::glib::{IsA, Object};
use gtk::prelude::BoxExt;
use gtk::Widget;
glib::wrapper! {
pub struct SettingBox(ObjectSubclass<setting_box_impl::SettingBox>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
impl SettingBox {
pub fn new(child: &impl IsA<Widget>) -> Self {
let entry: SettingBox = Object::builder().build();
entry.append(child);
entry
}
}

View file

@ -0,0 +1,37 @@
use crate::components::base::setting_box;
use gtk::subclass::prelude::*;
use gtk::{glib, CompositeTemplate};
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetSettingBox.ui")]
pub struct SettingBox {}
#[glib::object_subclass]
impl ObjectSubclass for SettingBox {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetSettingBox";
type Type = setting_box::SettingBox;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for SettingBox {
fn constructed(&self) {
self.parent_constructed();
}
}
impl BoxImpl for SettingBox {}
impl WidgetImpl for SettingBox {}
impl WindowImpl for SettingBox {}
impl ApplicationWindowImpl for SettingBox {}

View file

@ -0,0 +1,449 @@
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread,
time::Duration,
};
use dbus::{
arg::{self, Append},
blocking::Connection,
Error,
};
use gtk::gio;
use re_set_lib::{
audio::audio_structures::{InputStream, OutputStream, Sink, Source},
signals::GetVal,
};
use crate::components::{
input::source_box::{start_input_box_listener, SourceBox},
output::sink_box::{start_output_box_listener, SinkBox},
};
#[derive(Default, PartialEq, Eq)]
pub enum Position {
Connectivity,
Wifi,
Bluetooth,
Audio,
AudioOutput,
AudioInput,
#[default]
Home,
}
#[derive(Default)]
pub struct Listeners {
pub wifi_disabled: AtomicBool,
pub wifi_listener: AtomicBool,
pub bluetooth_listener: AtomicBool,
pub bluetooth_scan_requested: AtomicBool,
pub pulse_listener: AtomicBool,
}
impl Listeners {
pub fn stop_network_listener(&self) {
if !self.wifi_listener.load(Ordering::SeqCst) {
return;
}
self.wifi_listener.store(false, Ordering::SeqCst);
thread::spawn(|| {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(bool,), Error> =
proxy.method_call("org.Xetibo.ReSetWireless", "StopNetworkListener", ());
});
}
pub fn stop_audio_listener(&self) {
self.pulse_listener.store(false, Ordering::SeqCst);
}
pub fn stop_bluetooth_listener(&self) {
self.bluetooth_listener.store(false, Ordering::SeqCst);
}
}
#[derive(Debug)]
pub struct SinkAdded {
pub sink: Sink,
}
impl arg::AppendAll for SinkAdded {
fn append(&self, i: &mut arg::IterAppend) {
self.sink.append_by_ref(i);
}
}
impl arg::ReadAll for SinkAdded {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(SinkAdded { sink: i.read()? })
}
}
impl dbus::message::SignalArgs for SinkAdded {
const NAME: &'static str = "SinkAdded";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
impl GetVal<(Sink,)> for SinkAdded {
fn get_value(&self) -> (Sink,) {
(self.sink.clone(),)
}
}
#[derive(Debug)]
pub struct SinkChanged {
pub sink: Sink,
}
impl arg::AppendAll for SinkChanged {
fn append(&self, i: &mut arg::IterAppend) {
self.sink.append_by_ref(i);
}
}
impl arg::ReadAll for SinkChanged {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(SinkChanged { sink: i.read()? })
}
}
impl dbus::message::SignalArgs for SinkChanged {
const NAME: &'static str = "SinkChanged";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
impl GetVal<(Sink,)> for SinkChanged {
fn get_value(&self) -> (Sink,) {
(self.sink.clone(),)
}
}
#[derive(Debug)]
pub struct SinkRemoved {
pub index: u32,
}
impl arg::AppendAll for SinkRemoved {
fn append(&self, i: &mut arg::IterAppend) {
self.index.append_by_ref(i);
}
}
impl arg::ReadAll for SinkRemoved {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(SinkRemoved { index: i.read()? })
}
}
impl dbus::message::SignalArgs for SinkRemoved {
const NAME: &'static str = "SinkRemoved";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
impl GetVal<(u32,)> for SinkRemoved {
fn get_value(&self) -> (u32,) {
(self.index,)
}
}
#[derive(Debug)]
pub struct InputStreamAdded {
pub stream: InputStream,
}
impl arg::AppendAll for InputStreamAdded {
fn append(&self, i: &mut arg::IterAppend) {
self.stream.append_by_ref(i);
}
}
impl arg::ReadAll for InputStreamAdded {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(InputStreamAdded { stream: i.read()? })
}
}
impl dbus::message::SignalArgs for InputStreamAdded {
const NAME: &'static str = "InputStreamAdded";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
impl GetVal<(InputStream,)> for InputStreamAdded {
fn get_value(&self) -> (InputStream,) {
(self.stream.clone(),)
}
}
#[derive(Debug)]
pub struct InputStreamChanged {
pub stream: InputStream,
}
impl arg::AppendAll for InputStreamChanged {
fn append(&self, i: &mut arg::IterAppend) {
self.stream.append_by_ref(i);
}
}
impl arg::ReadAll for InputStreamChanged {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(InputStreamChanged { stream: i.read()? })
}
}
impl dbus::message::SignalArgs for InputStreamChanged {
const NAME: &'static str = "InputStreamChanged";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
#[derive(Debug)]
pub struct InputStreamRemoved {
pub index: u32,
}
impl arg::AppendAll for InputStreamRemoved {
fn append(&self, i: &mut arg::IterAppend) {
self.index.append_by_ref(i);
}
}
impl arg::ReadAll for InputStreamRemoved {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(InputStreamRemoved { index: i.read()? })
}
}
impl dbus::message::SignalArgs for InputStreamRemoved {
const NAME: &'static str = "InputStreamRemoved";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
impl GetVal<(u32,)> for InputStreamRemoved {
fn get_value(&self) -> (u32,) {
(self.index,)
}
}
#[derive(Debug)]
pub struct SourceAdded {
pub source: Source,
}
impl arg::AppendAll for SourceAdded {
fn append(&self, i: &mut arg::IterAppend) {
self.source.append_by_ref(i);
}
}
impl arg::ReadAll for SourceAdded {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(SourceAdded { source: i.read()? })
}
}
impl dbus::message::SignalArgs for SourceAdded {
const NAME: &'static str = "SourceAdded";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
impl GetVal<(Source,)> for SourceAdded {
fn get_value(&self) -> (Source,) {
(self.source.clone(),)
}
}
#[derive(Debug)]
pub struct SourceChanged {
pub source: Source,
}
impl arg::AppendAll for SourceChanged {
fn append(&self, i: &mut arg::IterAppend) {
self.source.append_by_ref(i);
}
}
impl arg::ReadAll for SourceChanged {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(SourceChanged { source: i.read()? })
}
}
impl dbus::message::SignalArgs for SourceChanged {
const NAME: &'static str = "SourceChanged";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
impl GetVal<(Source,)> for SourceChanged {
fn get_value(&self) -> (Source,) {
(self.source.clone(),)
}
}
#[derive(Debug)]
pub struct SourceRemoved {
pub index: u32,
}
impl arg::AppendAll for SourceRemoved {
fn append(&self, i: &mut arg::IterAppend) {
self.index.append_by_ref(i);
}
}
impl arg::ReadAll for SourceRemoved {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(SourceRemoved { index: i.read()? })
}
}
impl dbus::message::SignalArgs for SourceRemoved {
const NAME: &'static str = "SourceRemoved";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
impl GetVal<(u32,)> for SourceRemoved {
fn get_value(&self) -> (u32,) {
(self.index,)
}
}
#[derive(Debug)]
pub struct OutputStreamAdded {
pub stream: OutputStream,
}
impl arg::AppendAll for OutputStreamAdded {
fn append(&self, i: &mut arg::IterAppend) {
self.stream.append_by_ref(i);
}
}
impl arg::ReadAll for OutputStreamAdded {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(OutputStreamAdded { stream: i.read()? })
}
}
impl dbus::message::SignalArgs for OutputStreamAdded {
const NAME: &'static str = "OutputStreamAdded";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
impl GetVal<(OutputStream,)> for OutputStreamAdded {
fn get_value(&self) -> (OutputStream,) {
(self.stream.clone(),)
}
}
#[derive(Debug)]
pub struct OutputStreamChanged {
pub stream: OutputStream,
}
impl arg::AppendAll for OutputStreamChanged {
fn append(&self, i: &mut arg::IterAppend) {
self.stream.append_by_ref(i);
}
}
impl arg::ReadAll for OutputStreamChanged {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(OutputStreamChanged { stream: i.read()? })
}
}
impl dbus::message::SignalArgs for OutputStreamChanged {
const NAME: &'static str = "OutputStreamChanged";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
#[derive(Debug)]
pub struct OutputStreamRemoved {
pub index: u32,
}
impl arg::AppendAll for OutputStreamRemoved {
fn append(&self, i: &mut arg::IterAppend) {
self.index.append_by_ref(i);
}
}
impl arg::ReadAll for OutputStreamRemoved {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(OutputStreamRemoved { index: i.read()? })
}
}
impl dbus::message::SignalArgs for OutputStreamRemoved {
const NAME: &'static str = "OutputStreamRemoved";
const INTERFACE: &'static str = "org.Xetibo.ReSetAudio";
}
impl GetVal<(u32,)> for OutputStreamRemoved {
fn get_value(&self) -> (u32,) {
(self.index,)
}
}
pub fn start_audio_listener(
listeners: Arc<Listeners>,
sink_box: Option<Arc<SinkBox>>,
source_box: Option<Arc<SourceBox>>,
) {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
if listeners.pulse_listener.load(Ordering::SeqCst) {
return;
}
let mut conn = start_dbus_audio_listener(conn);
if let Some(sink_box) = sink_box {
conn = start_output_box_listener(conn, sink_box);
}
if let Some(source_box) = source_box {
conn = start_input_box_listener(conn, source_box);
}
listeners.pulse_listener.store(true, Ordering::SeqCst);
loop {
let _ = conn.process(Duration::from_millis(1000));
if !listeners.pulse_listener.load(Ordering::SeqCst) {
stop_dbus_audio_listener(conn);
break;
}
// thread::sleep(Duration::from_millis(1000));
// TODO is this really how we should do this?
}
});
}
fn start_dbus_audio_listener(conn: Connection) -> Connection {
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> = proxy.method_call("org.Xetibo.ReSetAudio", "StartAudioListener", ());
conn
}
fn stop_dbus_audio_listener(conn: Connection) {
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> = proxy.method_call("org.Xetibo.ReSetAudio", "StopAudioListener", ());
}

View file

@ -0,0 +1,448 @@
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, SystemTime};
use adw::glib;
use adw::glib::Object;
use adw::prelude::{ComboRowExt, ListModelExtManual, PreferencesGroupExt};
use adw::subclass::prelude::ObjectSubclassIsExt;
use dbus::blocking::Connection;
use dbus::message::SignalArgs;
use dbus::{Error, Path};
use glib::{clone, Cast};
use gtk::glib::Variant;
use gtk::prelude::{ActionableExt, ButtonExt, ListBoxRowExt, WidgetExt};
use gtk::{gio, StringObject, Widget};
use re_set_lib::bluetooth::bluetooth_structures::{BluetoothAdapter, BluetoothDevice};
use re_set_lib::signals::{BluetoothDeviceAdded, BluetoothDeviceChanged, BluetoothDeviceRemoved};
use crate::components::base::utils::Listeners;
use crate::components::bluetooth::bluetooth_box_impl;
use crate::components::bluetooth::bluetooth_entry::BluetoothEntry;
glib::wrapper! {
pub struct BluetoothBox(ObjectSubclass<bluetooth_box_impl::BluetoothBox>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
unsafe impl Send for BluetoothBox {}
unsafe impl Sync for BluetoothBox {}
impl BluetoothBox {
pub fn new(listeners: Arc<Listeners>) -> Arc<Self> {
let obj: Arc<Self> = Arc::new(Object::builder().build());
setup_callbacks(listeners, obj)
}
}
// TODO
// handle bonded -> this means saved but not connected
// handle rssi below x -> don't show device
fn setup_callbacks(
listeners: Arc<Listeners>,
bluetooth_box: Arc<BluetoothBox>,
) -> Arc<BluetoothBox> {
let bluetooth_box_ref = bluetooth_box.clone();
let listeners_ref = listeners.clone();
let imp = bluetooth_box.imp();
imp.reset_visibility.set_activatable(true);
imp.reset_visibility
.set_action_name(Some("navigation.push"));
imp.reset_visibility
.set_action_target_value(Some(&Variant::from("visibility")));
imp.reset_bluetooth_main_tab
.set_action_name(Some("navigation.pop"));
imp.reset_bluetooth_refresh_button.set_sensitive(false);
imp.reset_bluetooth_refresh_button
.connect_clicked(move |button| {
button.set_sensitive(false);
listeners
.bluetooth_scan_requested
.store(true, Ordering::SeqCst);
});
imp.reset_bluetooth_discoverable_switch
.connect_state_set(clone!(@weak imp => @default-return glib::Propagation::Proceed,move |_, state| {
set_bluetooth_adapter_visibility(imp.reset_current_bluetooth_adapter.borrow().path.clone(), state);
glib::Propagation::Proceed
}));
imp.reset_bluetooth_pairable_switch
.connect_state_set(clone!(@weak imp => @default-return glib::Propagation::Proceed,move |_, state| {
set_bluetooth_adapter_pairability(imp.reset_current_bluetooth_adapter.borrow().path.clone(), state);
glib::Propagation::Proceed
}));
imp.reset_bluetooth_switch
.connect_state_set(move |_, state| {
if !state {
let imp = bluetooth_box_ref.imp();
for x in imp
.reset_bluetooth_connected_devices
.observe_children()
.iter::<Object>()
.flatten()
{
// todo test this
if let Some(item) = x.downcast_ref::<Widget>() {
imp.reset_bluetooth_available_devices.remove(item);
}
}
listeners_ref
.bluetooth_listener
.store(false, Ordering::SeqCst);
set_adapter_enabled(
imp.reset_current_bluetooth_adapter.borrow().path.clone(),
false,
);
} else {
let imp = bluetooth_box_ref.imp();
set_adapter_enabled(
imp.reset_current_bluetooth_adapter.borrow().path.clone(),
true,
);
start_bluetooth_listener(listeners_ref.clone(), bluetooth_box_ref.clone());
}
glib::Propagation::Proceed
});
bluetooth_box
}
pub fn populate_conntected_bluetooth_devices(bluetooth_box: Arc<BluetoothBox>) {
// TODO handle saved devices -> they also exist
gio::spawn_blocking(move || {
let ref_box = bluetooth_box.clone();
let devices = get_connected_devices();
let adapters = get_bluetooth_adapters();
{
let imp = bluetooth_box.imp();
let list = imp.reset_model_list.write().unwrap();
let mut model_index = imp.reset_model_index.write().unwrap();
let mut map = imp.reset_bluetooth_adapters.write().unwrap();
imp.reset_current_bluetooth_adapter
.replace(adapters.last().unwrap().clone());
for (index, adapter) in adapters.into_iter().enumerate() {
list.append(&adapter.alias);
map.insert(adapter.alias.clone(), (adapter, index as u32));
*model_index += 1;
}
}
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = ref_box.imp();
let list = imp.reset_model_list.read().unwrap();
imp.reset_bluetooth_adapter.set_model(Some(&*list));
let map = imp.reset_bluetooth_adapters.read().unwrap();
let device = imp.reset_current_bluetooth_adapter.borrow();
if let Some(index) = map.get(&device.alias) {
imp.reset_bluetooth_adapter.set_selected(index.1);
}
{
let current_adapter = imp.reset_current_bluetooth_adapter.borrow();
imp.reset_bluetooth_switch
.set_state(current_adapter.powered);
imp.reset_bluetooth_switch
.set_active(current_adapter.powered);
imp.reset_bluetooth_discoverable_switch
.set_state(current_adapter.discoverable);
imp.reset_bluetooth_discoverable_switch
.set_active(current_adapter.discoverable);
imp.reset_bluetooth_pairable_switch
.set_state(current_adapter.pairable);
imp.reset_bluetooth_pairable_switch
.set_active(current_adapter.pairable);
}
imp.reset_bluetooth_adapter.connect_selected_notify(
clone!(@weak imp => move |dropdown| {
let selected = dropdown.selected_item();
if selected.is_none() {
return;
}
let selected = selected.unwrap();
let selected = selected.downcast_ref::<StringObject>().unwrap();
let selected = selected.string().to_string();
let device = imp.reset_bluetooth_adapters.read().unwrap();
let device = device.get(&selected);
if device.is_none() {
return;
}
set_bluetooth_adapter(device.unwrap().0.path.clone());
}),
);
for device in devices {
let path = device.path.clone();
let connected = device.connected;
let bluetooth_entry = BluetoothEntry::new(device);
imp.available_devices
.borrow_mut()
.insert(path, bluetooth_entry.clone());
if connected {
imp.reset_bluetooth_connected_devices.add(&*bluetooth_entry);
} else {
imp.reset_bluetooth_available_devices.add(&*bluetooth_entry);
}
}
});
});
});
}
pub fn start_bluetooth_listener(listeners: Arc<Listeners>, bluetooth_box: Arc<BluetoothBox>) {
gio::spawn_blocking(move || {
if listeners.bluetooth_listener.load(Ordering::SeqCst) {
return;
}
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> =
proxy.method_call("org.Xetibo.ReSetBluetooth", "StartBluetoothListener", ());
let device_added = BluetoothDeviceAdded::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let device_removed = BluetoothDeviceRemoved::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let device_changed = BluetoothDeviceChanged::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let device_added_box = bluetooth_box.clone();
let device_removed_box = bluetooth_box.clone();
let device_changed_box = bluetooth_box.clone();
let loop_box = bluetooth_box.clone();
let res = conn.add_match(device_added, move |ir: BluetoothDeviceAdded, _, _| {
let bluetooth_box = device_added_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = bluetooth_box.imp();
let path = ir.bluetooth_device.path.clone();
let connected = ir.bluetooth_device.connected;
let bluetooth_entry = BluetoothEntry::new(ir.bluetooth_device);
imp.available_devices
.borrow_mut()
.insert(path, bluetooth_entry.clone());
if connected {
imp.reset_bluetooth_connected_devices.add(&*bluetooth_entry);
} else {
imp.reset_bluetooth_available_devices.add(&*bluetooth_entry);
}
});
});
true
});
if res.is_err() {
println!("fail on bluetooth device add event");
return;
}
let res = conn.add_match(device_removed, move |ir: BluetoothDeviceRemoved, _, _| {
let bluetooth_box = device_removed_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = bluetooth_box.imp();
let mut map = imp.available_devices.borrow_mut();
if let Some(list_entry) = map.remove(&ir.bluetooth_device) {
if list_entry.imp().bluetooth_device.borrow().connected {
imp.reset_bluetooth_connected_devices.remove(&*list_entry);
} else {
imp.reset_bluetooth_available_devices.remove(&*list_entry);
}
}
});
});
true
});
if res.is_err() {
println!("fail on bluetooth device remove event");
return;
}
let res = conn.add_match(device_changed, move |ir: BluetoothDeviceChanged, _, _| {
let bluetooth_box = device_changed_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = bluetooth_box.imp();
let mut map = imp.available_devices.borrow_mut();
if let Some(list_entry) = map.get_mut(&ir.bluetooth_device.path) {
let mut existing_bluetooth_device =
list_entry.imp().bluetooth_device.borrow_mut();
if existing_bluetooth_device.connected != ir.bluetooth_device.connected {
if ir.bluetooth_device.connected {
imp.reset_bluetooth_connected_devices.add(&**list_entry);
imp.reset_bluetooth_available_devices.remove(&**list_entry);
} else {
imp.reset_bluetooth_available_devices.add(&**list_entry);
imp.reset_bluetooth_connected_devices.remove(&**list_entry);
}
}
if existing_bluetooth_device.bonded != ir.bluetooth_device.bonded {
if ir.bluetooth_device.bonded {
list_entry
.imp()
.remove_device_button
.borrow()
.set_sensitive(true);
} else {
list_entry
.imp()
.remove_device_button
.borrow()
.set_sensitive(false);
}
}
*existing_bluetooth_device = ir.bluetooth_device;
}
});
});
true
});
if res.is_err() {
println!("fail on bluetooth device remove event");
return;
}
listeners.bluetooth_listener.store(true, Ordering::SeqCst);
let mut time = SystemTime::now();
let mut listener_active = true;
loop {
let _ = conn.process(Duration::from_millis(1000));
if !listeners.bluetooth_listener.load(Ordering::SeqCst) {
let _: Result<(), Error> =
proxy.method_call("org.Xetibo.ReSetBluetooth", "StopBluetoothListener", ());
break;
}
if listener_active && time.elapsed().unwrap() > Duration::from_millis(10000) {
listener_active = false;
let instance_ref = loop_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
instance_ref
.imp()
.reset_bluetooth_refresh_button
.set_sensitive(true);
});
});
let _: Result<(), Error> =
proxy.method_call("org.Xetibo.ReSetBluetooth", "StopBluetoothScan", ());
}
if !listener_active && listeners.bluetooth_scan_requested.load(Ordering::SeqCst) {
listeners
.bluetooth_scan_requested
.store(false, Ordering::SeqCst);
listener_active = true;
let _: Result<(), Error> =
proxy.method_call("org.Xetibo.ReSetBluetooth", "StartBluetoothListener", ());
time = SystemTime::now();
}
thread::sleep(Duration::from_millis(100));
}
});
}
fn get_connected_devices() -> Vec<BluetoothDevice> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Vec<BluetoothDevice>,), Error> = proxy.method_call(
"org.Xetibo.ReSetBluetooth",
"GetConnectedBluetoothDevices",
(),
);
if res.is_err() {
return Vec::new();
}
res.unwrap().0
}
fn get_bluetooth_adapters() -> Vec<BluetoothAdapter> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Vec<BluetoothAdapter>,), Error> =
proxy.method_call("org.Xetibo.ReSetBluetooth", "GetBluetoothAdapters", ());
if res.is_err() {
return Vec::new();
}
res.unwrap().0
}
fn set_bluetooth_adapter(path: Path<'static>) {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(Vec<BluetoothAdapter>,), Error> =
proxy.method_call("org.Xetibo.ReSetBluetooth", "SetBluetoothAdapter", (path,));
}
fn set_bluetooth_adapter_visibility(path: Path<'static>, visible: bool) {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(bool,), Error> = proxy.method_call(
"org.Xetibo.ReSetBluetooth",
"SetBluetoothAdapterDiscoverability",
(path, visible),
);
}
fn set_bluetooth_adapter_pairability(path: Path<'static>, visible: bool) {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(bool,), Error> = proxy.method_call(
"org.Xetibo.ReSetBluetooth",
"SetBluetoothAdapterPairability",
(path, visible),
);
}
fn set_adapter_enabled(path: Path<'static>, enabled: bool) {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(Vec<BluetoothAdapter>,), Error> = proxy.method_call(
"org.Xetibo.ReSetBluetooth",
"SetBluetoothAdapterEnabled",
(path, enabled),
);
}

View file

@ -0,0 +1,76 @@
use adw::{ActionRow, ComboRow, PreferencesGroup};
use dbus::Path;
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate, Switch};
use gtk::{prelude::*, StringList};
use re_set_lib::bluetooth::bluetooth_structures::BluetoothAdapter;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use crate::components::base::list_entry::ListEntry;
use crate::components::bluetooth::bluetooth_box;
use crate::components::bluetooth::bluetooth_entry::BluetoothEntry;
type BluetoothMap = RefCell<HashMap<Path<'static>, Arc<BluetoothEntry>>>;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetBluetooth.ui")]
pub struct BluetoothBox {
#[template_child]
pub reset_bluetooth_switch: TemplateChild<Switch>,
#[template_child]
pub reset_bluetooth_available_devices: TemplateChild<PreferencesGroup>,
#[template_child]
pub reset_bluetooth_refresh_button: TemplateChild<Button>,
#[template_child]
pub reset_bluetooth_adapter: TemplateChild<ComboRow>,
#[template_child]
pub reset_bluetooth_connected_devices: TemplateChild<PreferencesGroup>,
#[template_child]
pub reset_visibility: TemplateChild<ActionRow>,
#[template_child]
pub reset_bluetooth_main_tab: TemplateChild<ListEntry>,
#[template_child]
pub reset_bluetooth_discoverable_switch: TemplateChild<Switch>,
#[template_child]
pub reset_bluetooth_pairable_switch: TemplateChild<Switch>,
pub available_devices: BluetoothMap,
pub connected_devices: BluetoothMap,
pub reset_bluetooth_adapters: Arc<RwLock<HashMap<String, (BluetoothAdapter, u32)>>>,
pub reset_current_bluetooth_adapter: Arc<RefCell<BluetoothAdapter>>,
pub reset_model_list: Arc<RwLock<StringList>>,
pub reset_model_index: Arc<RwLock<u32>>,
}
#[glib::object_subclass]
impl ObjectSubclass for BluetoothBox {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetBluetooth";
type Type = bluetooth_box::BluetoothBox;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
BluetoothEntry::ensure_type();
ListEntry::ensure_type();
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for BluetoothBox {
fn constructed(&self) {
self.parent_constructed();
}
}
impl BoxImpl for BluetoothBox {}
impl WidgetImpl for BluetoothBox {}
impl WindowImpl for BluetoothBox {}
impl ApplicationWindowImpl for BluetoothBox {}

View file

@ -0,0 +1,174 @@
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use crate::components::bluetooth::bluetooth_entry_impl;
use adw::glib::Object;
use adw::prelude::{ActionRowExt, PreferencesRowExt};
use adw::{glib, ActionRow};
use dbus::blocking::Connection;
use dbus::{Error, Path};
use glib::subclass::prelude::ObjectSubclassIsExt;
use gtk::prelude::{ButtonExt, ListBoxRowExt, WidgetExt};
use gtk::{gio, Align, Button, GestureClick, Image, Label};
use re_set_lib::bluetooth::bluetooth_structures::BluetoothDevice;
glib::wrapper! {
pub struct BluetoothEntry(ObjectSubclass<bluetooth_entry_impl::BluetoothEntry>)
@extends ActionRow, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::Actionable, gtk::ConstraintTarget, gtk::ListBoxRow, adw::PreferencesRow;
}
unsafe impl Send for BluetoothEntry {}
unsafe impl Sync for BluetoothEntry {}
impl BluetoothEntry {
pub fn new(device: BluetoothDevice) -> Arc<Self> {
let entry: Arc<BluetoothEntry> = Arc::new(Object::builder().build());
let entry_imp = entry.imp();
let entry_ref = entry.clone();
let entry_ref_remove = entry.clone();
entry.set_title(&device.alias);
entry.set_subtitle(&device.address);
entry.set_activatable(true);
entry_imp.remove_device_button.replace(
Button::builder()
.icon_name("user-trash-symbolic")
.valign(Align::Center)
.build(),
);
entry_imp
.connecting_label
.replace(Label::builder().label("").build());
entry.add_suffix(entry_imp.remove_device_button.borrow().deref());
if device.icon.is_empty() {
entry.add_prefix(&Image::from_icon_name("dialog-question-symbolic"));
} else {
entry.add_prefix(&Image::from_icon_name(&device.icon));
}
if device.connected || device.bonded {
entry_imp.remove_device_button.borrow().set_sensitive(true);
} else {
entry_imp.remove_device_button.borrow().set_sensitive(false);
}
entry_imp.bluetooth_device.replace(device);
entry_imp
.remove_device_button
.borrow()
.connect_clicked(move |_| {
let imp = entry_ref_remove.imp();
remove_device_pairing(imp.bluetooth_device.borrow().path.clone());
});
let gesture = GestureClick::new();
// paired is not what we think
// TODO implement paired
gesture.connect_released(move |_, _, _, _| {
let imp = entry_ref.imp();
let borrow = imp.bluetooth_device.borrow();
if borrow.connected {
let imp = entry_ref.imp();
imp.remove_device_button.borrow().set_sensitive(false);
imp.connecting_label.borrow().set_text("Disconnecting...");
disconnect_from_device(entry_ref.clone(), borrow.path.clone());
} else {
entry_ref.set_sensitive(false);
imp.connecting_label.borrow().set_text("Connecting...");
connect_to_device(entry_ref.clone(), borrow.path.clone());
}
});
entry.add_controller(gesture);
entry
}
}
fn connect_to_device(entry: Arc<BluetoothEntry>, path: Path<'static>) {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(bool,), Error> = proxy.method_call(
"org.Xetibo.ReSetBluetooth",
"ConnectToBluetoothDevice",
(path,),
);
glib::spawn_future(async move {
glib::idle_add_once(move || {
if res.is_err() {
entry.set_sensitive(true);
entry
.imp()
.connecting_label
.borrow()
.set_text("Error on connecting");
} else {
entry.set_sensitive(true);
entry.imp().connecting_label.borrow().set_text("");
}
});
});
});
}
// fn pair_with_device(path: Path<'static>) {
// gio::spawn_blocking(move || {
// let conn = Connection::new_session().unwrap();
// let proxy = conn.with_proxy(
// "org.Xetibo.ReSetDaemon",
// "/org/Xetibo/ReSetDaemon",
// Duration::from_millis(1000),
// );
// let _: Result<(bool,), Error> = proxy.method_call(
// "org.Xetibo.ReSetBluetooth",
// "PairWithBluetoothDevice",
// (path,),
// );
// });
// }
fn disconnect_from_device(entry: Arc<BluetoothEntry>, path: Path<'static>) {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(bool,), Error> = proxy.method_call(
"org.Xetibo.ReSetBluetooth",
"DisconnectFromBluetoothDevice",
(path,),
);
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = entry.imp();
if res.is_err() {
imp.remove_device_button.borrow().set_sensitive(true);
imp.connecting_label
.borrow()
.set_text("Error on disconnecting");
} else {
imp.remove_device_button.borrow().set_sensitive(true);
imp.connecting_label.borrow().set_text("");
}
});
});
});
}
fn remove_device_pairing(path: Path<'static>) {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(bool,), Error> =
proxy.method_call("org.Xetibo.ReSetBluetooth", "RemoveDevicePairing", (path,));
});
}

View file

@ -0,0 +1,51 @@
use crate::components::bluetooth::bluetooth_entry;
use adw::subclass::action_row::ActionRowImpl;
use adw::subclass::preferences_row::PreferencesRowImpl;
use adw::ActionRow;
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate, Label};
use re_set_lib::bluetooth::bluetooth_structures::BluetoothDevice;
use std::cell::RefCell;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetBluetoothEntry.ui")]
pub struct BluetoothEntry {
pub remove_device_button: RefCell<Button>,
pub connecting_label: RefCell<Label>,
pub device_name: RefCell<String>,
pub bluetooth_device: RefCell<BluetoothDevice>
}
#[glib::object_subclass]
impl ObjectSubclass for BluetoothEntry {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetBluetoothEntry";
type Type = bluetooth_entry::BluetoothEntry;
type ParentType = ActionRow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for BluetoothEntry {
fn constructed(&self) {
self.parent_constructed();
}
}
impl ActionRowImpl for BluetoothEntry {}
impl PreferencesRowImpl for BluetoothEntry {}
impl ListBoxRowImpl for BluetoothEntry {}
impl WidgetImpl for BluetoothEntry {}
impl WindowImpl for BluetoothEntry {}
impl ApplicationWindowImpl for BluetoothEntry {}

View file

@ -0,0 +1,4 @@
pub mod bluetooth_box;
pub mod bluetooth_box_impl;
pub mod bluetooth_entry;
pub mod bluetooth_entry_impl;

View file

@ -0,0 +1,6 @@
pub mod output_stream_entry;
pub mod output_stream_entry_impl;
pub mod source_box;
pub mod source_box_impl;
pub mod source_entry;
pub mod source_entry_impl;

View file

@ -0,0 +1,196 @@
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use crate::components::utils::{create_dropdown_label_factory, set_combo_row_ellipsis};
use adw::glib;
use adw::glib::Object;
use adw::prelude::{ButtonExt, ComboRowExt, PreferencesRowExt, RangeExt};
use dbus::blocking::Connection;
use dbus::Error;
use glib::subclass::types::ObjectSubclassIsExt;
use glib::{clone, Cast, Propagation};
use gtk::{gio, StringObject};
use re_set_lib::audio::audio_structures::OutputStream;
use super::output_stream_entry_impl;
use super::source_box::SourceBox;
glib::wrapper! {
pub struct OutputStreamEntry(ObjectSubclass<output_stream_entry_impl::OutputStreamEntry>)
@extends adw::PreferencesGroup, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
unsafe impl Send for OutputStreamEntry {}
unsafe impl Sync for OutputStreamEntry {}
impl OutputStreamEntry {
pub fn new(source_box: Arc<SourceBox>, stream: OutputStream) -> Self {
let obj: Self = Object::builder().build();
// TODO use event callback for progress bar -> this is the "im speaking" indicator
{
let box_imp = source_box.imp();
let imp = obj.imp();
let name = stream.application_name.clone() + ": " + stream.name.as_str();
imp.reset_source_selection.set_title(name.as_str());
imp.reset_source_selection
.set_factory(Some(&create_dropdown_label_factory()));
set_combo_row_ellipsis(imp.reset_source_selection.get());
let volume = stream.volume.first().unwrap_or(&0_u32);
let fraction = (*volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
imp.reset_volume_slider.set_value(*volume as f64);
imp.stream.replace(stream);
imp.reset_volume_slider.connect_change_value(
clone!(@weak imp => @default-return Propagation::Stop, move |_, _, value| {
let fraction = (value / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
let mut stream = imp.stream.try_borrow();
while stream.is_err() {
stream = imp.stream.try_borrow();
}
let stream = stream.unwrap();
let index = stream.index;
let channels = stream.channels;
{
let mut time = imp.volume_time_stamp.borrow_mut();
if time.is_some()
&& time.unwrap().elapsed().unwrap() < Duration::from_millis(50)
{
return Propagation::Proceed;
}
*time = Some(SystemTime::now());
}
set_outputstream_volume(value, index, channels);
Propagation::Proceed
}),
);
{
let list = box_imp.reset_model_list.read().unwrap();
imp.reset_source_selection.set_model(Some(&*list));
let map = box_imp.reset_source_map.write().unwrap();
let mut name = box_imp.reset_default_source.try_borrow();
while name.is_err() {
name = box_imp.reset_default_source.try_borrow();
}
let name = name.unwrap();
let name = &name.alias;
let index = map.get(name);
if let Some(index) = index {
imp.reset_source_selection.set_selected(index.1);
}
}
imp.reset_source_selection.connect_selected_notify(
clone!(@weak imp, @weak box_imp => move |dropdown| {
let selected = dropdown.selected_item();
if selected.is_none() {
return;
}
let selected = selected.unwrap();
let selected = selected.downcast_ref::<StringObject>().unwrap();
let selected = selected.string().to_string();
let source = box_imp.reset_source_map.write().unwrap();
let source = source.get(&selected);
if source.is_none() {
return;
}
let mut stream = imp.stream.try_borrow();
while stream.is_err() {
stream = imp.stream.try_borrow();
}
let stream = stream.unwrap();
let source = source.unwrap().0;
set_source_of_output_stream(stream.index, source);
}),
);
imp.reset_source_mute
.connect_clicked(clone!(@weak imp => move |_| {
let stream = imp.stream.clone();
let mut stream = stream.try_borrow_mut();
while stream.is_err() {
stream = imp.stream.try_borrow_mut();
}
let mut stream = stream.unwrap();
stream.muted = !stream.muted;
let muted = stream.muted;
let index = stream.index;
if muted {
imp.reset_source_mute
.set_icon_name("microphone-disabled-symbolic");
} else {
imp.reset_source_mute
.set_icon_name("audio-input-microphone-symbolic");
}
toggle_output_stream_mute(index, muted);
}));
}
obj
}
}
fn set_outputstream_volume(value: f64, index: u32, channels: u16) -> bool {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> = proxy.method_call(
"org.Xetibo.ReSetAudio",
"SetOutputStreamVolume",
(index, channels, value as u32),
);
// if res.is_err() {
// return false;
// }
// res.unwrap().0
});
true
}
fn toggle_output_stream_mute(index: u32, muted: bool) -> bool {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> = proxy.method_call(
"org.Xetibo.ReSetAudio",
"SetOutputStreamMute",
(index, muted),
);
// if res.is_err() {
// return false;
// }
// res.unwrap().0
});
true
}
fn set_source_of_output_stream(stream: u32, source: u32) -> bool {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(bool,), Error> = proxy.method_call(
"org.Xetibo.ReSetAudio",
"SetSourceOfOutputStream",
(stream, source),
);
// if res.is_err() {
// return false;
// }
// res.unwrap().0
});
true
}
// TODO propagate error from dbus

View file

@ -0,0 +1,50 @@
use adw::subclass::prelude::PreferencesGroupImpl;
use adw::{ComboRow, PreferencesGroup};
use re_set_lib::audio::audio_structures::OutputStream;
use std::cell::RefCell;
use std::sync::Arc;
use std::time::SystemTime;
use crate::components::input::output_stream_entry;
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate, Label, ProgressBar, Scale};
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetOutputStreamEntry.ui")]
pub struct OutputStreamEntry {
#[template_child]
pub reset_source_selection: TemplateChild<ComboRow>,
#[template_child]
pub reset_source_mute: TemplateChild<Button>,
#[template_child]
pub reset_volume_slider: TemplateChild<Scale>,
#[template_child]
pub reset_volume_percentage: TemplateChild<Label>,
#[template_child]
pub reset_volume_meter: TemplateChild<ProgressBar>,
pub stream: Arc<RefCell<OutputStream>>,
pub associated_source: Arc<RefCell<(u32, String)>>,
pub volume_time_stamp: RefCell<Option<SystemTime>>,
}
#[glib::object_subclass]
impl ObjectSubclass for OutputStreamEntry {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetOutputStreamEntry";
type Type = output_stream_entry::OutputStreamEntry;
type ParentType = PreferencesGroup;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl PreferencesGroupImpl for OutputStreamEntry {}
impl ObjectImpl for OutputStreamEntry {}
impl WidgetImpl for OutputStreamEntry {}

View file

@ -0,0 +1,657 @@
use adw::prelude::PreferencesRowExt;
use re_set_lib::audio::audio_structures::{Card, OutputStream, Source};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use adw::glib;
use adw::glib::Object;
use adw::prelude::{
BoxExt, ButtonExt, CheckButtonExt, ComboRowExt, ListBoxRowExt, PreferencesGroupExt, RangeExt,
};
use dbus::blocking::Connection;
use dbus::message::SignalArgs;
use dbus::{Error, Path};
use glib::subclass::prelude::ObjectSubclassIsExt;
use glib::{clone, Cast, Propagation, Variant};
use gtk::prelude::ActionableExt;
use gtk::{gio, StringObject};
use crate::components::base::card_entry::CardEntry;
use crate::components::base::list_entry::ListEntry;
use crate::components::base::utils::{
OutputStreamAdded, OutputStreamChanged, OutputStreamRemoved, SourceAdded, SourceChanged,
SourceRemoved,
};
use crate::components::input::source_box_impl;
use crate::components::input::source_entry::set_source_volume;
use crate::components::utils::{create_dropdown_label_factory, set_combo_row_ellipsis};
use super::output_stream_entry::OutputStreamEntry;
use super::source_entry::{set_default_source, toggle_source_mute, SourceEntry};
glib::wrapper! {
pub struct SourceBox(ObjectSubclass<source_box_impl::SourceBox>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
unsafe impl Send for SourceBox {}
unsafe impl Sync for SourceBox {}
impl SourceBox {
pub fn new() -> Self {
let obj: Self = Object::builder().build();
{
let imp = obj.imp();
let mut model_index = imp.reset_model_index.write().unwrap();
*model_index = 0;
}
obj
}
pub fn setup_callbacks(&self) {
let self_imp = self.imp();
self_imp.reset_source_row.set_activatable(true);
self_imp
.reset_source_row
.set_action_name(Some("navigation.push"));
self_imp
.reset_source_row
.set_action_target_value(Some(&Variant::from("sources")));
self_imp.reset_cards_row.set_activatable(true);
self_imp
.reset_cards_row
.set_action_name(Some("navigation.push"));
self_imp
.reset_cards_row
.set_action_target_value(Some(&Variant::from("profileConfiguration")));
self_imp.reset_output_stream_button.set_activatable(true);
self_imp
.reset_output_stream_button
.set_action_name(Some("navigation.pop"));
self_imp.reset_input_cards_back_button.set_activatable(true);
self_imp
.reset_input_cards_back_button
.set_action_name(Some("navigation.pop"));
self_imp
.reset_source_dropdown
.set_factory(Some(&create_dropdown_label_factory()));
set_combo_row_ellipsis(self_imp.reset_source_dropdown.get());
}
}
impl Default for SourceBox {
fn default() -> Self {
Self::new()
}
}
pub fn populate_sources(input_box: Arc<SourceBox>) {
gio::spawn_blocking(move || {
let sources = get_sources();
{
let input_box_imp = input_box.imp();
let list = input_box_imp.reset_model_list.write().unwrap();
let mut map = input_box_imp.reset_source_map.write().unwrap();
let mut model_index = input_box_imp.reset_model_index.write().unwrap();
input_box_imp
.reset_default_source
.replace(get_default_source());
for (i, source) in (0_u32..).zip(sources.iter()) {
list.append(&source.alias);
map.insert(source.alias.clone(), (source.index, i, source.name.clone()));
*model_index += 1;
}
}
populate_outputstreams(input_box.clone());
populate_cards(input_box.clone());
glib::spawn_future(async move {
glib::idle_add_once(move || {
let input_box_ref_slider = input_box.clone();
let input_box_ref_toggle = input_box.clone();
let input_box_ref_mute = input_box.clone();
let input_box_ref = input_box.clone();
{
let input_box_imp = input_box_ref.imp();
let default_sink = input_box_imp.reset_default_source.clone();
let source = default_sink.borrow();
let volume = source.volume.first().unwrap_or(&0_u32);
let fraction = (*volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
input_box_imp.reset_volume_percentage.set_text(&percentage);
input_box_imp.reset_volume_slider.set_value(*volume as f64);
let mut list = input_box_imp.reset_source_list.write().unwrap();
for source in sources {
let index = source.index;
let alias = source.alias.clone();
let mut is_default = false;
if input_box_imp.reset_default_source.borrow().name == source.name {
is_default = true;
}
let source_entry = Arc::new(SourceEntry::new(
is_default,
input_box_imp.reset_default_check_button.clone(),
source,
input_box.clone(),
));
let source_clone = source_entry.clone();
let entry = Arc::new(ListEntry::new(&*source_entry));
entry.set_activatable(false);
list.insert(index, (entry.clone(), source_clone, alias));
input_box_imp.reset_sources.append(&*entry);
}
let list = input_box_imp.reset_model_list.read().unwrap();
input_box_imp.reset_source_dropdown.set_model(Some(&*list));
let map = input_box_imp.reset_source_map.read().unwrap();
let name = input_box_imp.reset_default_source.borrow();
if let Some(index) = map.get(&name.alias) {
input_box_imp.reset_source_dropdown.set_selected(index.1);
}
input_box_imp.reset_source_dropdown.connect_selected_notify(
clone!(@weak input_box_imp => move |dropdown| {
let input_box = input_box_ref_toggle.clone();
let selected = dropdown.selected_item();
if selected.is_none() {
return;
}
let selected = selected.unwrap();
let selected = selected.downcast_ref::<StringObject>().unwrap();
let selected = selected.string().to_string();
let source = input_box_imp.reset_source_map.read().unwrap();
let source = source.get(&selected);
if source.is_none() {
return;
}
let source = Arc::new(source.unwrap().2.clone());
gio::spawn_blocking(move || {
let result = set_default_source(source);
if result.is_none(){
return;
}
refresh_default_source(result.unwrap(), input_box.clone(), false);
});
}),
);
}
input_box_ref
.imp()
.reset_volume_slider
.connect_change_value(move |_, _, value| {
let imp = input_box_ref_slider.imp();
let fraction = (value / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
let source = imp.reset_default_source.borrow();
let index = source.index;
let channels = source.channels;
{
let mut time = imp.volume_time_stamp.borrow_mut();
if time.is_some()
&& time.unwrap().elapsed().unwrap() < Duration::from_millis(50)
{
return Propagation::Proceed;
}
*time = Some(SystemTime::now());
}
set_source_volume(value, index, channels);
Propagation::Proceed
});
input_box_ref
.imp()
.reset_source_mute
.connect_clicked(move |_| {
let imp = input_box_ref_mute.imp();
let mut source = imp.reset_default_source.borrow_mut();
source.muted = !source.muted;
if source.muted {
imp.reset_source_mute
.set_icon_name("microphone-disabled-symbolic");
} else {
imp.reset_source_mute
.set_icon_name("audio-input-microphone-symbolic");
}
toggle_source_mute(source.index, source.muted);
});
});
});
});
}
pub fn refresh_default_source(new_source: Source, input_box: Arc<SourceBox>, entry: bool) {
let volume = *new_source.volume.first().unwrap_or(&0_u32);
let fraction = (volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = input_box.imp();
if !entry {
let list = imp.reset_source_list.read().unwrap();
let entry = list.get(&new_source.index);
if entry.is_none() {
return;
}
let entry_imp = entry.unwrap().1.imp();
entry_imp.reset_selected_source.set_active(true);
} else {
let map = imp.reset_source_map.read().unwrap();
let entry = map.get(&new_source.alias);
if entry.is_none() {
return;
}
imp.reset_source_dropdown.set_selected(entry.unwrap().1);
}
imp.reset_volume_percentage.set_text(&percentage);
imp.reset_volume_slider.set_value(volume as f64);
if new_source.muted {
imp.reset_source_mute
.set_icon_name("microphone-disabled-symbolic");
} else {
imp.reset_source_mute
.set_icon_name("audio-input-microphone-symbolic");
}
imp.reset_default_source.replace(new_source);
});
});
}
pub fn populate_outputstreams(input_box: Arc<SourceBox>) {
let input_box_ref = input_box.clone();
gio::spawn_blocking(move || {
let streams = get_output_streams();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let input_box_imp = input_box_ref.imp();
let mut list = input_box_imp.reset_output_stream_list.write().unwrap();
for stream in streams {
let index = stream.index;
let input_stream = Arc::new(OutputStreamEntry::new(input_box.clone(), stream));
let input_stream_clone = input_stream.clone();
let entry = Arc::new(ListEntry::new(&*input_stream));
entry.set_activatable(false);
list.insert(index, (entry.clone(), input_stream_clone));
input_box_imp.reset_output_streams.append(&*entry);
}
});
});
});
}
pub fn populate_cards(input_box: Arc<SourceBox>) {
gio::spawn_blocking(move || {
let input_box_ref = input_box.clone();
let cards = get_cards();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = input_box_ref.imp();
for card in cards {
imp.reset_cards.add(&CardEntry::new(card));
}
});
});
});
}
fn get_output_streams() -> Vec<OutputStream> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Vec<OutputStream>,), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "ListOutputStreams", ());
if res.is_err() {
return Vec::new();
}
res.unwrap().0
}
fn get_sources() -> Vec<Source> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Vec<Source>,), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "ListSources", ());
if res.is_err() {
return Vec::new();
}
res.unwrap().0
}
fn get_cards() -> Vec<Card> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Vec<Card>,), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "ListCards", ());
if res.is_err() {
return Vec::new();
}
res.unwrap().0
}
fn get_default_source_name() -> String {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(String,), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "GetDefaultSourceName", ());
if res.is_err() {
return String::from("");
}
res.unwrap().0
}
fn get_default_source() -> Source {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Source,), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "GetDefaultSource", ());
if res.is_err() {
return Source::default();
}
res.unwrap().0
}
pub fn start_input_box_listener(conn: Connection, source_box: Arc<SourceBox>) -> Connection {
let source_added = SourceAdded::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let source_removed = SourceRemoved::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let source_changed = SourceChanged::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let output_stream_added = OutputStreamAdded::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let output_stream_removed = OutputStreamRemoved::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let output_stream_changed = OutputStreamChanged::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let source_added_box = source_box.clone();
let source_removed_box = source_box.clone();
let source_changed_box = source_box.clone();
let output_stream_added_box = source_box.clone();
let output_stream_removed_box = source_box.clone();
let output_stream_changed_box = source_box.clone();
let res = conn.add_match(source_added, move |ir: SourceAdded, _, _| {
let source_box = source_added_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let input_box = source_box.clone();
let input_box_imp = input_box.imp();
let mut list = input_box_imp.reset_source_list.write().unwrap();
let source_index = ir.source.index;
let alias = ir.source.alias.clone();
let name = ir.source.name.clone();
let mut is_default = false;
if input_box_imp.reset_default_source.borrow().name == ir.source.name {
is_default = true;
}
let source_entry = Arc::new(SourceEntry::new(
is_default,
input_box_imp.reset_default_check_button.clone(),
ir.source,
input_box.clone(),
));
let source_clone = source_entry.clone();
let entry = Arc::new(ListEntry::new(&*source_entry));
entry.set_activatable(false);
list.insert(source_index, (entry.clone(), source_clone, alias.clone()));
input_box_imp.reset_sources.append(&*entry);
let mut map = input_box_imp.reset_source_map.write().unwrap();
let mut index = input_box_imp.reset_model_index.write().unwrap();
input_box_imp
.reset_model_list
.write()
.unwrap()
.append(&alias);
map.insert(alias, (source_index, *index, name));
*index += 1;
});
});
true
});
if res.is_err() {
println!("fail on source add event");
return conn;
}
let res = conn.add_match(source_removed, move |ir: SourceRemoved, _, _| {
let source_box = source_removed_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let input_box = source_box.clone();
let input_box_imp = input_box.imp();
let mut list = input_box_imp.reset_source_list.write().unwrap();
let entry = list.remove(&ir.index);
if entry.is_none() {
return;
}
input_box_imp
.reset_sources
.remove(&*entry.clone().unwrap().0);
let mut map = input_box_imp.reset_source_map.write().unwrap();
let entry_index = map.remove(&entry.unwrap().2);
if let Some(entry_index) = entry_index {
input_box_imp
.reset_model_list
.write()
.unwrap()
.remove(entry_index.1);
}
let mut index = input_box_imp.reset_model_index.write().unwrap();
if *index != 0 {
*index -= 1;
}
});
});
true
});
if res.is_err() {
println!("fail on source remove event");
return conn;
}
let res = conn.add_match(source_changed, move |ir: SourceChanged, _, _| {
let source_box = source_changed_box.clone();
let default_source = get_default_source_name();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let input_box = source_box.clone();
let input_box_imp = input_box.imp();
let is_default = ir.source.name == default_source;
let volume = ir.source.volume.first().unwrap_or(&0_u32);
let fraction = (*volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
let list = input_box_imp.reset_source_list.read().unwrap();
let entry = list.get(&ir.source.index);
if entry.is_none() {
return;
}
let imp = entry.unwrap().1.imp();
if is_default {
input_box_imp.reset_volume_percentage.set_text(&percentage);
input_box_imp.reset_volume_slider.set_value(*volume as f64);
input_box_imp
.reset_default_source
.replace(ir.source.clone());
if ir.source.muted {
input_box_imp
.reset_source_mute
.set_icon_name("microphone-disabled-symbolic");
} else {
input_box_imp
.reset_source_mute
.set_icon_name("audio-input-microphone-symbolic");
}
imp.reset_selected_source.set_active(true);
} else {
imp.reset_selected_source.set_active(false);
}
imp.reset_source_name
.set_title(ir.source.alias.clone().as_str());
imp.reset_volume_percentage.set_text(&percentage);
imp.reset_volume_slider.set_value(*volume as f64);
if ir.source.muted {
imp.reset_source_mute
.set_icon_name("microphone-disabled-symbolic");
} else {
imp.reset_source_mute
.set_icon_name("audio-input-microphone-symbolic");
}
});
});
true
});
if res.is_err() {
println!("fail on source change event");
return conn;
}
let res = conn.add_match(output_stream_added, move |ir: OutputStreamAdded, _, _| {
let source_box = output_stream_added_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let input_box = source_box.clone();
let input_box_imp = input_box.imp();
let mut list = input_box_imp.reset_output_stream_list.write().unwrap();
let index = ir.stream.index;
let output_stream = Arc::new(OutputStreamEntry::new(input_box.clone(), ir.stream));
let entry = Arc::new(ListEntry::new(&*output_stream));
entry.set_activatable(false);
list.insert(index, (entry.clone(), output_stream.clone()));
input_box_imp.reset_output_streams.append(&*entry);
});
});
true
});
if res.is_err() {
println!("fail on output stream add event");
return conn;
}
let res = conn.add_match(
output_stream_changed,
move |ir: OutputStreamChanged, _, _| {
let imp = output_stream_changed_box.imp();
let alias: String;
{
let source_list = imp.reset_source_list.read().unwrap();
if let Some(alias_opt) = source_list.get(&ir.stream.source_index) {
alias = alias_opt.2.clone();
} else {
alias = String::from("");
}
}
let source_box = output_stream_changed_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let input_box = source_box.clone();
let input_box_imp = input_box.imp();
let entry: Arc<OutputStreamEntry>;
{
let list = input_box_imp.reset_output_stream_list.read().unwrap();
let entry_opt = list.get(&ir.stream.index);
if entry_opt.is_none() {
return;
}
entry = entry_opt.unwrap().1.clone();
}
let imp = entry.imp();
if ir.stream.muted {
imp.reset_source_mute
.set_icon_name("microphone-disabled-symbolic");
} else {
imp.reset_source_mute
.set_icon_name("audio-input-microphone-symbolic");
}
let name = ir.stream.application_name.clone() + ": " + ir.stream.name.as_str();
imp.reset_source_selection.set_title(name.as_str());
let volume = ir.stream.volume.first().unwrap_or(&0_u32);
let fraction = (*volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
imp.reset_volume_slider.set_value(*volume as f64);
let map = input_box_imp.reset_source_map.read().unwrap();
if let Some(index) = map.get(&alias) {
imp.reset_source_selection.set_selected(index.1);
}
});
});
true
},
);
if res.is_err() {
println!("fail on output stream change event");
return conn;
}
let res = conn.add_match(
output_stream_removed,
move |ir: OutputStreamRemoved, _, _| {
let source_box = output_stream_removed_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let input_box = source_box.clone();
let input_box_imp = input_box.imp();
let mut list = input_box_imp.reset_output_stream_list.write().unwrap();
let entry = list.remove(&ir.index);
if entry.is_none() {
return;
}
input_box_imp
.reset_output_streams
.remove(&*entry.unwrap().0);
});
});
true
},
);
if res.is_err() {
println!("fail on output stream remove event");
return conn;
}
conn
}

View file

@ -0,0 +1,94 @@
use adw::{ActionRow, ComboRow, PreferencesGroup};
use re_set_lib::audio::audio_structures::Source;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::SystemTime;
use crate::components::base::list_entry::ListEntry;
use crate::components::input::source_box;
use gtk::subclass::prelude::*;
use gtk::{glib, CheckButton, CompositeTemplate, StringList, TemplateChild};
use gtk::{prelude::*, Button, Label, ProgressBar, Scale};
use super::output_stream_entry::OutputStreamEntry;
use super::source_entry::SourceEntry;
type SourceEntryMap = Arc<RwLock<HashMap<u32, (Arc<ListEntry>, Arc<SourceEntry>, String)>>>;
type OutputStreamEntryMap = Arc<RwLock<HashMap<u32, (Arc<ListEntry>, Arc<OutputStreamEntry>)>>>;
type SourceMap = Arc<RwLock<HashMap<String, (u32, u32, String)>>>;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetAudioInput.ui")]
pub struct SourceBox {
#[template_child]
pub reset_source_row: TemplateChild<ActionRow>,
#[template_child]
pub reset_cards_row: TemplateChild<ActionRow>,
#[template_child]
pub reset_source_dropdown: TemplateChild<ComboRow>,
#[template_child]
pub reset_source_mute: TemplateChild<Button>,
#[template_child]
pub reset_volume_slider: TemplateChild<Scale>,
#[template_child]
pub reset_volume_percentage: TemplateChild<Label>,
#[template_child]
pub reset_volume_meter: TemplateChild<ProgressBar>,
#[template_child]
pub reset_sources: TemplateChild<gtk::Box>,
#[template_child]
pub reset_output_stream_button: TemplateChild<ActionRow>,
#[template_child]
pub reset_output_streams: TemplateChild<gtk::Box>,
#[template_child]
pub reset_input_cards_back_button: TemplateChild<ActionRow>,
#[template_child]
pub reset_cards: TemplateChild<PreferencesGroup>,
pub reset_default_check_button: Arc<CheckButton>,
pub reset_default_source: Arc<RefCell<Source>>,
pub reset_source_list: SourceEntryMap,
pub reset_output_stream_list: OutputStreamEntryMap,
pub reset_model_list: Arc<RwLock<StringList>>,
pub reset_model_index: Arc<RwLock<u32>>,
// first u32 is the index of the source, the second the index in the model list and the third is
// the full name
pub reset_source_map: SourceMap,
pub volume_time_stamp: RefCell<Option<SystemTime>>,
}
#[glib::object_subclass]
impl ObjectSubclass for SourceBox {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetAudioInput";
type Type = source_box::SourceBox;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
OutputStreamEntry::ensure_type();
SourceEntry::ensure_type();
ListEntry::ensure_type();
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl BoxImpl for SourceBox {}
impl ObjectImpl for SourceBox {
fn constructed(&self) {
let obj = self.obj();
obj.setup_callbacks();
}
}
impl ListBoxRowImpl for SourceBox {}
impl WidgetImpl for SourceBox {}
impl WindowImpl for SourceBox {}
impl ApplicationWindowImpl for SourceBox {}

View file

@ -0,0 +1,159 @@
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use adw::glib;
use adw::glib::Object;
use adw::prelude::{ButtonExt, CheckButtonExt, PreferencesRowExt, RangeExt};
use dbus::blocking::Connection;
use dbus::Error;
use glib::subclass::types::ObjectSubclassIsExt;
use glib::{clone, Propagation};
use gtk::{gio, CheckButton};
use re_set_lib::audio::audio_structures::Source;
use super::source_box::{refresh_default_source, SourceBox};
use super::source_entry_impl;
glib::wrapper! {
pub struct SourceEntry(ObjectSubclass<source_entry_impl::SourceEntry>)
@extends adw::PreferencesGroup, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
unsafe impl Send for SourceEntry {}
unsafe impl Sync for SourceEntry {}
impl SourceEntry {
pub fn new(
is_default: bool,
check_group: Arc<CheckButton>,
source: Source,
input_box: Arc<SourceBox>,
) -> Self {
let obj: Self = Object::builder().build();
// TODO use event callback for progress bar -> this is the "im speaking" indicator
{
let imp = obj.imp();
imp.reset_source_name
.set_title(source.alias.clone().as_str());
let name = Arc::new(source.name.clone());
let volume = source.volume.first().unwrap_or(&0_u32);
let fraction = (*volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
imp.reset_volume_slider.set_value(*volume as f64);
imp.source.replace(source);
imp.reset_volume_slider.connect_change_value(
clone!(@weak imp => @default-return Propagation::Stop, move |_, _, value| {
let fraction = (value / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
let source = imp.source.borrow();
let index = source.index;
let channels = source.channels;
{
let mut time = imp.volume_time_stamp.borrow_mut();
if time.is_some()
&& time.unwrap().elapsed().unwrap() < Duration::from_millis(50)
{
return Propagation::Proceed;
}
*time = Some(SystemTime::now());
}
set_source_volume(value, index, channels);
Propagation::Proceed
}),
);
imp.reset_selected_source.set_group(Some(&*check_group));
if is_default {
imp.reset_selected_source.set_active(true);
} else {
imp.reset_selected_source.set_active(false);
}
imp.reset_selected_source.connect_toggled(move |button| {
let input_box = input_box.clone();
if button.is_active() {
let name = name.clone();
gio::spawn_blocking(move || {
let result = set_default_source(name);
if result.is_none() {
return;
}
refresh_default_source(result.unwrap(), input_box, true);
});
}
});
imp.reset_source_mute
.connect_clicked(clone!(@weak imp => move |_| {
let mut source = imp.source.borrow_mut();
source.muted = !source.muted;
if source.muted {
imp.reset_source_mute
.set_icon_name("microphone-disabled-symbolic");
} else {
imp.reset_source_mute
.set_icon_name("audio-input-microphone-symbolic");
}
toggle_source_mute(source.index, source.muted);
}));
}
obj
}
}
pub fn set_source_volume(value: f64, index: u32, channels: u16) -> bool {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> = proxy.method_call(
"org.Xetibo.ReSetAudio",
"SetSourceVolume",
(index, channels, value as u32),
);
// if res.is_err() {
// return false;
// }
// res.unwrap().0
});
true
}
pub fn toggle_source_mute(index: u32, muted: bool) -> bool {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "SetSourceMute", (index, muted));
// if res.is_err() {
// return false;
// }
// res.unwrap().0
});
true
}
pub fn set_default_source(name: Arc<String>) -> Option<Source> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Source,), Error> = proxy.method_call(
"org.Xetibo.ReSetAudio",
"SetDefaultSource",
(name.as_str(),),
);
if res.is_err() {
return None;
}
Some(res.unwrap().0)
}

View file

@ -0,0 +1,52 @@
use adw::subclass::prelude::PreferencesGroupImpl;
use adw::{ActionRow, PreferencesGroup};
use re_set_lib::audio::audio_structures::Source;
use std::cell::RefCell;
use std::sync::Arc;
use std::time::SystemTime;
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CheckButton, CompositeTemplate, Label, ProgressBar, Scale};
use super::source_entry;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetSourceEntry.ui")]
pub struct SourceEntry {
#[template_child]
pub reset_source_name: TemplateChild<ActionRow>,
#[template_child]
pub reset_selected_source: TemplateChild<CheckButton>,
#[template_child]
pub reset_source_mute: TemplateChild<Button>,
#[template_child]
pub reset_volume_slider: TemplateChild<Scale>,
#[template_child]
pub reset_volume_percentage: TemplateChild<Label>,
#[template_child]
pub reset_volume_meter: TemplateChild<ProgressBar>,
pub source: Arc<RefCell<Source>>,
pub volume_time_stamp: RefCell<Option<SystemTime>>,
}
#[glib::object_subclass]
impl ObjectSubclass for SourceEntry {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetSourceEntry";
type Type = source_entry::SourceEntry;
type ParentType = PreferencesGroup;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl PreferencesGroupImpl for SourceEntry {}
impl ObjectImpl for SourceEntry {}
impl WidgetImpl for SourceEntry {}

7
src/components/mod.rs Normal file
View file

@ -0,0 +1,7 @@
mod base;
pub mod bluetooth;
mod input;
pub mod output;
pub mod utils;
pub mod wifi;
pub mod window;

View file

View file

@ -0,0 +1,223 @@
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use crate::components::utils::{create_dropdown_label_factory, set_combo_row_ellipsis};
use adw::glib;
use adw::glib::Object;
use adw::prelude::{ButtonExt, ComboRowExt, PreferencesRowExt, RangeExt};
use dbus::blocking::Connection;
use dbus::Error;
use glib::subclass::types::ObjectSubclassIsExt;
use glib::{clone, Cast, Propagation};
use gtk::{gio, StringObject};
use re_set_lib::audio::audio_structures::InputStream;
use super::input_stream_entry_impl;
use super::sink_box::SinkBox;
glib::wrapper! {
pub struct InputStreamEntry(ObjectSubclass<input_stream_entry_impl::InputStreamEntry>)
@extends adw::PreferencesGroup, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
unsafe impl Send for InputStreamEntry {}
unsafe impl Sync for InputStreamEntry {}
impl InputStreamEntry {
pub fn new(sink_box: Arc<SinkBox>, stream: InputStream) -> Self {
let obj: Self = Object::builder().build();
// TODO use event callback for progress bar -> this is the "im speaking" indicator
{
let index = stream.sink_index;
let box_imp = sink_box.imp();
let imp = obj.imp();
if stream.muted {
imp.reset_sink_mute
.set_icon_name("audio-volume-muted-symbolic");
} else {
imp.reset_sink_mute
.set_icon_name("audio-volume-high-symbolic");
}
let name = stream.application_name.clone() + ": " + stream.name.as_str();
imp.reset_sink_selection.set_title(name.as_str());
imp.reset_sink_selection
.set_factory(Some(&create_dropdown_label_factory()));
set_combo_row_ellipsis(imp.reset_sink_selection.get());
let volume = stream.volume.first().unwrap_or(&0_u32);
let fraction = (*volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
imp.reset_volume_slider.set_value(*volume as f64);
imp.stream.replace(stream);
{
let sink = box_imp.reset_default_sink.borrow();
imp.associated_sink.replace((sink.index, sink.name.clone()));
}
imp.reset_volume_slider.connect_change_value(
clone!(@weak imp => @default-return Propagation::Stop, move |_, _, value| {
let fraction = (value / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
let mut stream = imp.stream.try_borrow();
while stream.is_err() {
stream = imp.stream.try_borrow();
}
let stream = stream.unwrap();
let index = stream.index;
let channels = stream.channels;
{
let mut time = imp.volume_time_stamp.borrow_mut();
if time.is_some() && time.unwrap().elapsed().unwrap() < Duration::from_millis(50) {
return Propagation::Proceed;
}
*time = Some(SystemTime::now());
}
set_inputstream_volume(value, index, channels);
Propagation::Proceed
}),
);
{
let list = box_imp.reset_model_list.read().unwrap();
// while list.is_err() {
// list = box_imp.resetModelList.try_borrow();
// }
// let list = list.unwrap();
imp.reset_sink_selection.set_model(Some(&*list));
let map = box_imp.reset_sink_map.read().unwrap();
let sink_list = box_imp.reset_sink_list.read().unwrap();
let name = sink_list.get(&index);
if let Some(name) = name {
let name = &name.2;
let index = map.get(name);
if let Some(index) = index {
imp.reset_sink_selection.set_selected(index.1);
}
} else {
let mut name = box_imp.reset_default_sink.try_borrow();
while name.is_err() {
name = box_imp.reset_default_sink.try_borrow();
}
let name = &name.unwrap().alias;
let index = map.get(name);
if let Some(index) = index {
imp.reset_sink_selection.set_selected(index.1);
}
}
}
imp.reset_sink_selection.connect_selected_notify(
clone!(@weak imp, @weak box_imp => move |dropdown| {
let selected = dropdown.selected_item();
if selected.is_none() {
return;
}
let selected = selected.unwrap();
let selected = selected.downcast_ref::<StringObject>().unwrap();
let selected = selected.string().to_string();
let sink = box_imp.reset_sink_map.read().unwrap();
// if sink.is_err() {
// return;
// }
// let sink = sink.unwrap();
let sink = sink.get(&selected);
if sink.is_none() {
return;
}
let mut stream = imp.stream.try_borrow();
while stream.is_err() {
stream = imp.stream.try_borrow();
}
let stream = stream.unwrap();
let sink = sink.unwrap().0;
set_sink_of_input_stream(stream.index, sink);
}),
);
imp.reset_sink_mute
.connect_clicked(clone!(@weak imp => move |_| {
let stream = imp.stream.clone();
let mut stream = stream.try_borrow_mut();
while stream.is_err() {
stream = imp.stream.try_borrow_mut();
}
let mut stream = stream.unwrap();
stream.muted = !stream.muted;
let muted = stream.muted;
let index = stream.index;
if muted {
imp.reset_sink_mute
.set_icon_name("audio-volume-muted-symbolic");
} else {
imp.reset_sink_mute
.set_icon_name("audio-volume-high-symbolic");
}
toggle_input_stream_mute(index, muted);
}));
}
obj
}
}
fn set_inputstream_volume(value: f64, index: u32, channels: u16) -> bool {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> = proxy.method_call(
"org.Xetibo.ReSetAudio",
"SetInputStreamVolume",
(index, channels, value as u32),
);
// if res.is_err() {
// return false;
// }
// res.unwrap().0
});
true
}
fn toggle_input_stream_mute(index: u32, muted: bool) -> bool {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> = proxy.method_call(
"org.Xetibo.ReSetAudio",
"SetInputStreamMute",
(index, muted),
);
// if res.is_err() {
// return false;
// }
// res.unwrap().0
});
true
}
fn set_sink_of_input_stream(stream: u32, sink: u32) -> bool {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> = proxy.method_call(
"org.Xetibo.ReSetAudio",
"SetSinkOfInputStream",
(stream, sink),
);
// if res.is_err() {
// return false;
// }
// res.unwrap().0
});
true
}
// TODO propagate error from dbus

View file

@ -0,0 +1,51 @@
use adw::subclass::prelude::PreferencesGroupImpl;
use adw::{ComboRow, PreferencesGroup};
use re_set_lib::audio::audio_structures::InputStream;
use std::cell::RefCell;
use std::sync::Arc;
use std::time::SystemTime;
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate, Label, ProgressBar, Scale};
use super::input_stream_entry;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetInputStreamEntry.ui")]
pub struct InputStreamEntry {
#[template_child]
pub reset_sink_selection: TemplateChild<ComboRow>,
#[template_child]
pub reset_sink_mute: TemplateChild<Button>,
#[template_child]
pub reset_volume_slider: TemplateChild<Scale>,
#[template_child]
pub reset_volume_percentage: TemplateChild<Label>,
#[template_child]
pub reset_volume_meter: TemplateChild<ProgressBar>,
pub stream: Arc<RefCell<InputStream>>,
pub associated_sink: Arc<RefCell<(u32, String)>>,
pub volume_time_stamp: RefCell<Option<SystemTime>>,
}
#[glib::object_subclass]
impl ObjectSubclass for InputStreamEntry {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetInputStreamEntry";
type Type = input_stream_entry::InputStreamEntry;
type ParentType = PreferencesGroup;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl PreferencesGroupImpl for InputStreamEntry {}
impl ObjectImpl for InputStreamEntry {}
impl WidgetImpl for InputStreamEntry {}

View file

@ -0,0 +1,7 @@
pub mod input_stream_entry;
pub mod input_stream_entry_impl;
pub mod sink_box;
pub mod sink_box_impl;
pub mod sink_entry;
pub mod sink_entry_impl;
pub mod audio_box;

View file

@ -0,0 +1,647 @@
use adw::prelude::PreferencesGroupExt;
use adw::prelude::PreferencesRowExt;
use re_set_lib::audio::audio_structures::Card;
use re_set_lib::audio::audio_structures::InputStream;
use re_set_lib::audio::audio_structures::Sink;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use adw::glib::Object;
use adw::prelude::{BoxExt, ButtonExt, CheckButtonExt, ComboRowExt, RangeExt};
use adw::{glib, prelude::ListBoxRowExt};
use dbus::blocking::Connection;
use dbus::message::SignalArgs;
use dbus::{Error, Path};
use glib::subclass::prelude::ObjectSubclassIsExt;
use glib::{clone, Cast, Propagation, Variant};
use gtk::prelude::ActionableExt;
use gtk::{gio, StringObject};
use crate::components::base::card_entry::CardEntry;
use crate::components::base::list_entry::ListEntry;
use crate::components::base::utils::{
InputStreamAdded, InputStreamChanged, InputStreamRemoved, SinkAdded, SinkChanged, SinkRemoved,
};
use crate::components::output::sink_entry::set_sink_volume;
use crate::components::utils::{create_dropdown_label_factory, set_combo_row_ellipsis};
use super::input_stream_entry::InputStreamEntry;
use super::sink_box_impl;
use super::sink_entry::{set_default_sink, toggle_sink_mute, SinkEntry};
glib::wrapper! {
pub struct SinkBox(ObjectSubclass<sink_box_impl::SinkBox>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
unsafe impl Send for SinkBox {}
unsafe impl Sync for SinkBox {}
impl SinkBox {
pub fn new() -> Self {
let obj: Self = Object::builder().build();
{
let imp = obj.imp();
let mut model_index = imp.reset_model_index.write().unwrap();
*model_index = 0;
}
obj
}
pub fn setup_callbacks(&self) {
let self_imp = self.imp();
self_imp.reset_sinks_row.set_activatable(true);
self_imp
.reset_sinks_row
.set_action_name(Some("navigation.push"));
self_imp
.reset_sinks_row
.set_action_target_value(Some(&Variant::from("outputDevices")));
self_imp.reset_cards_row.set_activatable(true);
self_imp
.reset_cards_row
.set_action_name(Some("navigation.push"));
self_imp
.reset_cards_row
.set_action_target_value(Some(&Variant::from("profileConfiguration")));
self_imp.reset_input_stream_button.set_activatable(true);
self_imp
.reset_input_stream_button
.set_action_name(Some("navigation.pop"));
self_imp.reset_input_cards_back_button.set_activatable(true);
self_imp
.reset_input_cards_back_button
.set_action_name(Some("navigation.pop"));
self_imp
.reset_sink_dropdown
.set_factory(Some(&create_dropdown_label_factory()));
set_combo_row_ellipsis(self_imp.reset_sink_dropdown.get());
}
}
impl Default for SinkBox {
fn default() -> Self {
Self::new()
}
}
pub fn populate_sinks(output_box: Arc<SinkBox>) {
gio::spawn_blocking(move || {
let sinks = get_sinks();
{
let output_box_imp = output_box.imp();
let list = output_box_imp.reset_model_list.write().unwrap();
let mut map = output_box_imp.reset_sink_map.write().unwrap();
let mut model_index = output_box_imp.reset_model_index.write().unwrap();
output_box_imp
.reset_default_sink
.replace(get_default_sink());
for (i, sink) in (0_u32..).zip(sinks.iter()) {
list.append(&sink.alias);
map.insert(sink.alias.clone(), (sink.index, i, sink.name.clone()));
*model_index += 1;
}
}
populate_inputstreams(output_box.clone());
populate_cards(output_box.clone());
glib::spawn_future(async move {
glib::idle_add_once(move || {
let output_box_ref_select = output_box.clone();
let output_box_ref_slider = output_box.clone();
let output_box_ref_mute = output_box.clone();
let output_box_ref = output_box.clone();
{
let output_box_imp = output_box_ref.imp();
let default_sink = output_box_imp.reset_default_sink.clone();
let sink = default_sink.borrow();
let volume = sink.volume.first().unwrap_or(&0);
let fraction = (*volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
output_box_imp.reset_volume_percentage.set_text(&percentage);
output_box_imp.reset_volume_slider.set_value(*volume as f64);
let mut list = output_box_imp.reset_sink_list.write().unwrap();
for sink in sinks {
let index = sink.index;
let alias = sink.alias.clone();
let mut is_default = false;
if output_box_imp.reset_default_sink.borrow().name == sink.name {
is_default = true;
}
let sink_entry = Arc::new(SinkEntry::new(
is_default,
output_box_imp.reset_default_check_button.clone(),
sink,
output_box.clone(),
));
let sink_clone = sink_entry.clone();
let entry = Arc::new(ListEntry::new(&*sink_entry));
entry.set_activatable(false);
list.insert(index, (entry.clone(), sink_clone, alias));
output_box_imp.reset_sinks.append(&*entry);
}
let list = output_box_imp.reset_model_list.read().unwrap();
output_box_imp.reset_sink_dropdown.set_model(Some(&*list));
let map = output_box_imp.reset_sink_map.read().unwrap();
let name = output_box_imp.reset_default_sink.borrow();
if let Some(index) = map.get(&name.alias) {
output_box_imp.reset_sink_dropdown.set_selected(index.1);
}
output_box_imp.reset_sink_dropdown.connect_selected_notify(
clone!(@weak output_box_imp => move |dropdown| {
let output_box_ref = output_box_ref_select.clone();
let selected = dropdown.selected_item();
if selected.is_none() {
return;
}
let selected = selected.unwrap();
let selected = selected.downcast_ref::<StringObject>().unwrap();
let selected = selected.string().to_string();
let sink = output_box_imp.reset_sink_map.read().unwrap();
let sink = sink.get(&selected);
if sink.is_none() {
return;
}
let new_sink_name = Arc::new(sink.unwrap().2.clone());
gio::spawn_blocking(move || {
let result = set_default_sink(new_sink_name);
if result.is_none() {
return;
}
let new_sink = result.unwrap();
refresh_default_sink(new_sink, output_box_ref, false);
});
}),
);
}
output_box_ref
.imp()
.reset_volume_slider
.connect_change_value(move |_, _, value| {
let imp = output_box_ref_slider.imp();
let fraction = (value / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
let sink = imp.reset_default_sink.borrow();
let index = sink.index;
let channels = sink.channels;
{
let mut time = imp.volume_time_stamp.borrow_mut();
if time.is_some()
&& time.unwrap().elapsed().unwrap() < Duration::from_millis(50)
{
return Propagation::Proceed;
}
*time = Some(SystemTime::now());
}
set_sink_volume(value, index, channels);
Propagation::Proceed
});
output_box_ref
.imp()
.reset_sink_mute
.connect_clicked(move |_| {
let imp = output_box_ref_mute.imp();
let mut stream = imp.reset_default_sink.borrow_mut();
stream.muted = !stream.muted;
if stream.muted {
imp.reset_sink_mute
.set_icon_name("audio-volume-muted-symbolic");
} else {
imp.reset_sink_mute
.set_icon_name("audio-volume-high-symbolic");
}
toggle_sink_mute(stream.index, stream.muted);
});
});
});
});
}
pub fn refresh_default_sink(new_sink: Sink, output_box: Arc<SinkBox>, entry: bool) {
let volume = *new_sink.volume.first().unwrap_or(&0_u32);
let fraction = (volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = output_box.imp();
if !entry {
let list = imp.reset_sink_list.read().unwrap();
let entry = list.get(&new_sink.index);
if entry.is_none() {
return;
}
let entry_imp = entry.unwrap().1.imp();
entry_imp.reset_selected_sink.set_active(true);
} else {
let map = imp.reset_sink_map.read().unwrap();
let entry = map.get(&new_sink.alias);
if entry.is_none() {
return;
}
imp.reset_sink_dropdown.set_selected(entry.unwrap().1);
}
imp.reset_volume_percentage.set_text(&percentage);
imp.reset_volume_slider.set_value(volume as f64);
if new_sink.muted {
imp.reset_sink_mute
.set_icon_name("audio-volume-muted-symbolic");
} else {
imp.reset_sink_mute
.set_icon_name("audio-volume-high-symbolic");
}
imp.reset_default_sink.replace(new_sink);
});
});
}
pub fn populate_inputstreams(output_box: Arc<SinkBox>) {
let output_box_ref = output_box.clone();
gio::spawn_blocking(move || {
let streams = get_input_streams();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let output_box_imp = output_box_ref.imp();
let mut list = output_box_imp.reset_input_stream_list.write().unwrap();
for stream in streams {
let index = stream.index;
let input_stream = Arc::new(InputStreamEntry::new(output_box.clone(), stream));
let entry = Arc::new(ListEntry::new(&*input_stream));
entry.set_activatable(false);
list.insert(index, (entry.clone(), input_stream.clone()));
output_box_imp.reset_input_streams.append(&*entry);
}
});
});
});
}
pub fn populate_cards(output_box: Arc<SinkBox>) {
gio::spawn_blocking(move || {
let output_box_ref = output_box.clone();
let cards = get_cards();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = output_box_ref.imp();
for card in cards {
imp.reset_cards.add(&CardEntry::new(card));
}
});
});
});
}
fn get_input_streams() -> Vec<InputStream> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Vec<InputStream>,), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "ListInputStreams", ());
if res.is_err() {
return Vec::new();
}
res.unwrap().0
}
fn get_sinks() -> Vec<Sink> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Vec<Sink>,), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "ListSinks", ());
if res.is_err() {
return Vec::new();
}
res.unwrap().0
}
fn get_cards() -> Vec<Card> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Vec<Card>,), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "ListCards", ());
if res.is_err() {
return Vec::new();
}
res.unwrap().0
}
fn get_default_sink_name() -> String {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(String,), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "GetDefaultSinkName", ());
if res.is_err() {
return String::from("");
}
res.unwrap().0
}
fn get_default_sink() -> Sink {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Sink,), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "GetDefaultSink", ());
if res.is_err() {
return Sink::default();
}
res.unwrap().0
}
pub fn start_output_box_listener(conn: Connection, sink_box: Arc<SinkBox>) -> Connection {
let sink_added = SinkAdded::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let sink_removed = SinkRemoved::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let sink_changed = SinkChanged::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let input_stream_added = InputStreamAdded::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let input_stream_removed = InputStreamRemoved::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let input_stream_changed = InputStreamChanged::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let sink_added_box = sink_box.clone();
let sink_removed_box = sink_box.clone();
let sink_changed_box = sink_box.clone();
let input_stream_added_box = sink_box.clone();
let input_stream_removed_box = sink_box.clone();
let input_stream_changed_box = sink_box.clone();
let res = conn.add_match(sink_added, move |ir: SinkAdded, _, _| {
let sink_box = sink_added_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let output_box = sink_box.clone();
let output_box_imp = output_box.imp();
let mut list = output_box_imp.reset_sink_list.write().unwrap();
let sink_index = ir.sink.index;
let alias = ir.sink.alias.clone();
let name = ir.sink.name.clone();
let mut is_default = false;
if output_box_imp.reset_default_sink.borrow().name == ir.sink.name {
is_default = true;
}
let sink_entry = Arc::new(SinkEntry::new(
is_default,
output_box_imp.reset_default_check_button.clone(),
ir.sink,
output_box.clone(),
));
let sink_clone = sink_entry.clone();
let entry = Arc::new(ListEntry::new(&*sink_entry));
entry.set_activatable(false);
list.insert(sink_index, (entry.clone(), sink_clone, alias.clone()));
output_box_imp.reset_sinks.append(&*entry);
let mut map = output_box_imp.reset_sink_map.write().unwrap();
let mut index = output_box_imp.reset_model_index.write().unwrap();
output_box_imp
.reset_model_list
.write()
.unwrap()
.append(&alias);
map.insert(alias, (sink_index, *index, name));
*index += 1;
});
});
true
});
if res.is_err() {
println!("fail on sink add event");
return conn;
}
let res = conn.add_match(sink_removed, move |ir: SinkRemoved, _, _| {
let sink_box = sink_removed_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let output_box = sink_box.clone();
let output_box_imp = output_box.imp();
let mut list = output_box_imp.reset_sink_list.write().unwrap();
let entry = list.remove(&ir.index);
if entry.is_none() {
return;
}
output_box_imp
.reset_sinks
.remove(&*entry.clone().unwrap().0);
let mut map = output_box_imp.reset_sink_map.write().unwrap();
let entry_index = map.remove(&entry.unwrap().2);
if let Some(entry_index) = entry_index {
output_box_imp
.reset_model_list
.write()
.unwrap()
.remove(entry_index.1);
}
let mut index = output_box_imp.reset_model_index.write().unwrap();
if *index != 0 {
*index -= 1;
}
});
});
true
});
if res.is_err() {
println!("fail on sink remove event");
return conn;
}
let res = conn.add_match(sink_changed, move |ir: SinkChanged, _, _| {
let sink_box = sink_changed_box.clone();
let default_sink = get_default_sink_name();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let output_box = sink_box.clone();
let output_box_imp = output_box.imp();
let is_default = ir.sink.name == default_sink;
let volume = ir.sink.volume.first().unwrap_or(&0_u32);
let fraction = (*volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
let list = output_box_imp.reset_sink_list.read().unwrap();
let entry = list.get(&ir.sink.index);
if entry.is_none() {
return;
}
let imp = entry.unwrap().1.imp();
if is_default {
output_box_imp.reset_volume_percentage.set_text(&percentage);
output_box_imp.reset_volume_slider.set_value(*volume as f64);
output_box_imp.reset_default_sink.replace(ir.sink.clone());
if ir.sink.muted {
output_box_imp
.reset_sink_mute
.set_icon_name("audio-volume-muted-symbolic");
} else {
output_box_imp
.reset_sink_mute
.set_icon_name("audio-volume-high-symbolic");
}
imp.reset_selected_sink.set_active(true);
} else {
imp.reset_selected_sink.set_active(false);
}
imp.reset_sink_name
.set_title(ir.sink.alias.clone().as_str());
imp.reset_volume_percentage.set_text(&percentage);
imp.reset_volume_slider.set_value(*volume as f64);
if ir.sink.muted {
imp.reset_sink_mute
.set_icon_name("audio-volume-muted-symbolic");
} else {
imp.reset_sink_mute
.set_icon_name("audio-volume-high-symbolic");
}
});
});
true
});
if res.is_err() {
println!("fail on sink change event");
return conn;
}
let res = conn.add_match(input_stream_added, move |ir: InputStreamAdded, _, _| {
let sink_box = input_stream_added_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let output_box = sink_box.clone();
let output_box_imp = output_box.imp();
let mut list = output_box_imp.reset_input_stream_list.write().unwrap();
let index = ir.stream.index;
let input_stream = Arc::new(InputStreamEntry::new(output_box.clone(), ir.stream));
let entry = Arc::new(ListEntry::new(&*input_stream));
entry.set_activatable(false);
list.insert(index, (entry.clone(), input_stream.clone()));
output_box_imp.reset_input_streams.append(&*entry);
});
});
true
});
if res.is_err() {
println!("fail on input stream add event");
return conn;
}
let res = conn.add_match(input_stream_changed, move |ir: InputStreamChanged, _, _| {
let imp = input_stream_changed_box.imp();
let alias: String;
{
let sink_list = imp.reset_sink_list.read().unwrap();
if let Some(alias_opt) = sink_list.get(&ir.stream.sink_index) {
alias = alias_opt.2.clone();
} else {
alias = String::from("");
}
}
let sink_box = input_stream_changed_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let output_box = sink_box.clone();
let output_box_imp = output_box.imp();
let entry: Arc<InputStreamEntry>;
{
let list = output_box_imp.reset_input_stream_list.read().unwrap();
let entry_opt = list.get(&ir.stream.index);
if entry_opt.is_none() {
return;
}
entry = entry_opt.unwrap().1.clone();
}
let imp = entry.imp();
if ir.stream.muted {
imp.reset_sink_mute
.set_icon_name("audio-volume-muted-symbolic");
} else {
imp.reset_sink_mute
.set_icon_name("audio-volume-high-symbolic");
}
let name = ir.stream.application_name.clone() + ": " + ir.stream.name.as_str();
imp.reset_sink_selection.set_title(name.as_str());
let volume = ir.stream.volume.first().unwrap_or(&0_u32);
let fraction = (*volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
imp.reset_volume_slider.set_value(*volume as f64);
let map = output_box_imp.reset_sink_map.read().unwrap();
if let Some(index) = map.get(&alias) {
imp.reset_sink_selection.set_selected(index.1);
}
});
});
true
});
if res.is_err() {
println!("fail on input stream change event");
return conn;
}
let res = conn.add_match(input_stream_removed, move |ir: InputStreamRemoved, _, _| {
let sink_box = input_stream_removed_box.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let output_box = sink_box.clone();
let output_box_imp = output_box.imp();
let mut list = output_box_imp.reset_input_stream_list.write().unwrap();
let entry = list.remove(&ir.index);
if entry.is_none() {
return;
}
output_box_imp
.reset_input_streams
.remove(&*entry.unwrap().0);
});
});
true
});
if res.is_err() {
println!("fail on input stream remove event");
return conn;
}
conn
}

View file

@ -0,0 +1,94 @@
use adw::{ActionRow, ComboRow, PreferencesGroup};
use re_set_lib::audio::audio_structures::Sink;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::SystemTime;
use crate::components::base::list_entry::ListEntry;
use crate::components::output::input_stream_entry::InputStreamEntry;
use gtk::subclass::prelude::*;
use gtk::{glib, Box, Button, CheckButton, CompositeTemplate, Label, StringList, TemplateChild};
use gtk::{prelude::*, ProgressBar, Scale};
use super::sink_box;
use super::sink_entry::SinkEntry;
type SinkEntryMap = Arc<RwLock<HashMap<u32, (Arc<ListEntry>, Arc<SinkEntry>, String)>>>;
type InputStreamEntryMap = Arc<RwLock<HashMap<u32, (Arc<ListEntry>, Arc<InputStreamEntry>)>>>;
// key is model name -> alias, first u32 is the index of the sink, the second the index in the model list and the third is
// the detailed name
type SinkMap = Arc<RwLock<HashMap<String, (u32, u32, String)>>>;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetAudioOutput.ui")]
pub struct SinkBox {
#[template_child]
pub reset_sinks_row: TemplateChild<ActionRow>,
#[template_child]
pub reset_cards_row: TemplateChild<ActionRow>,
#[template_child]
pub reset_sink_dropdown: TemplateChild<ComboRow>,
#[template_child]
pub reset_sink_mute: TemplateChild<Button>,
#[template_child]
pub reset_volume_slider: TemplateChild<Scale>,
#[template_child]
pub reset_volume_percentage: TemplateChild<Label>,
#[template_child]
pub reset_volume_meter: TemplateChild<ProgressBar>,
#[template_child]
pub reset_sinks: TemplateChild<Box>,
#[template_child]
pub reset_input_stream_button: TemplateChild<ActionRow>,
#[template_child]
pub reset_input_streams: TemplateChild<Box>,
#[template_child]
pub reset_input_cards_back_button: TemplateChild<ActionRow>,
#[template_child]
pub reset_cards: TemplateChild<PreferencesGroup>,
pub reset_default_check_button: Arc<CheckButton>,
pub reset_default_sink: Arc<RefCell<Sink>>,
pub reset_sink_list: SinkEntryMap,
pub reset_input_stream_list: InputStreamEntryMap,
pub reset_model_list: Arc<RwLock<StringList>>,
pub reset_model_index: Arc<RwLock<u32>>,
pub reset_sink_map: SinkMap,
pub volume_time_stamp: RefCell<Option<SystemTime>>,
}
#[glib::object_subclass]
impl ObjectSubclass for SinkBox {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetAudioOutput";
type Type = sink_box::SinkBox;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
InputStreamEntry::ensure_type();
SinkEntry::ensure_type();
ListEntry::ensure_type();
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl BoxImpl for SinkBox {}
impl ObjectImpl for SinkBox {
fn constructed(&self) {
let obj = self.obj();
obj.setup_callbacks();
}
}
impl ListBoxRowImpl for SinkBox {}
impl WidgetImpl for SinkBox {}
impl WindowImpl for SinkBox {}
impl ApplicationWindowImpl for SinkBox {}

View file

@ -0,0 +1,154 @@
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use adw::glib;
use adw::glib::Object;
use adw::prelude::{ButtonExt, CheckButtonExt, PreferencesRowExt, RangeExt};
use dbus::blocking::Connection;
use dbus::Error;
use glib::subclass::types::ObjectSubclassIsExt;
use glib::{clone, Propagation};
use gtk::{gio, CheckButton};
use re_set_lib::audio::audio_structures::Sink;
use super::sink_box::{refresh_default_sink, SinkBox};
use super::sink_entry_impl;
glib::wrapper! {
pub struct SinkEntry(ObjectSubclass<sink_entry_impl::SinkEntry>)
@extends adw::PreferencesGroup, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
unsafe impl Send for SinkEntry {}
unsafe impl Sync for SinkEntry {}
impl SinkEntry {
pub fn new(
is_default: bool,
check_group: Arc<CheckButton>,
stream: Sink,
output_box: Arc<SinkBox>,
) -> Self {
let obj: Self = Object::builder().build();
// TODO use event callback for progress bar -> this is the "im speaking" indicator
{
let imp = obj.imp();
imp.reset_sink_name.set_title(stream.alias.clone().as_str());
let name = Arc::new(stream.name.clone());
let volume = stream.volume.first().unwrap_or(&0_u32);
let fraction = (*volume as f64 / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
imp.reset_volume_slider.set_value(*volume as f64);
imp.stream.replace(stream);
imp.reset_volume_slider.connect_change_value(
clone!(@weak imp => @default-return Propagation::Stop, move |_, _, value| {
let fraction = (value / 655.36).round();
let percentage = (fraction).to_string() + "%";
imp.reset_volume_percentage.set_text(&percentage);
let sink = imp.stream.borrow();
let index = sink.index;
let channels = sink.channels;
{
let mut time = imp.volume_time_stamp.borrow_mut();
if time.is_some() && time.unwrap().elapsed().unwrap() < Duration::from_millis(50) {
return Propagation::Proceed;
}
*time = Some(SystemTime::now());
}
set_sink_volume(value, index, channels);
Propagation::Proceed
}),
);
imp.reset_selected_sink.set_group(Some(&*check_group));
if is_default {
imp.reset_selected_sink.set_active(true);
} else {
imp.reset_selected_sink.set_active(false);
}
imp.reset_selected_sink.connect_toggled(move |button| {
let output_box_ref = output_box.clone();
if button.is_active() {
let name = name.clone();
gio::spawn_blocking(move || {
let result = set_default_sink(name);
if result.is_none() {
return;
}
refresh_default_sink(result.unwrap(), output_box_ref, true);
});
}
});
imp.reset_sink_mute
.connect_clicked(clone!(@weak imp => move |_| {
let stream = imp.stream.clone();
let mut stream = stream.borrow_mut();
stream.muted = !stream.muted;
if stream.muted {
imp.reset_sink_mute
.set_icon_name("audio-volume-muted-symbolic");
} else {
imp.reset_sink_mute
.set_icon_name("audio-volume-high-symbolic");
}
toggle_sink_mute(stream.index, stream.muted);
}));
}
obj
}
}
pub fn set_sink_volume(value: f64, index: u32, channels: u16) -> bool {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> = proxy.method_call(
"org.Xetibo.ReSetAudio",
"SetSinkVolume",
(index, channels, value as u32),
);
// if res.is_err() {
// return false;
// }
// res.unwrap().0
});
true
}
pub fn toggle_sink_mute(index: u32, muted: bool) -> bool {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "SetSinkMute", (index, muted));
// if res.is_err() {
// return false;
// }
// res.unwrap().0
});
true
}
pub fn set_default_sink(name: Arc<String>) -> Option<Sink> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Sink,), Error> =
proxy.method_call("org.Xetibo.ReSetAudio", "SetDefaultSink", (name.as_str(),));
if res.is_err() {
return None;
}
Some(res.unwrap().0)
}

View file

@ -0,0 +1,51 @@
use adw::subclass::prelude::PreferencesGroupImpl;
use adw::{ActionRow, PreferencesGroup};
use re_set_lib::audio::audio_structures::Sink;
use std::cell::RefCell;
use std::sync::Arc;
use std::time::SystemTime;
use crate::components::output::sink_entry;
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CheckButton, CompositeTemplate, Label, ProgressBar, Scale};
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetSinkEntry.ui")]
pub struct SinkEntry {
#[template_child]
pub reset_sink_name: TemplateChild<ActionRow>,
#[template_child]
pub reset_selected_sink: TemplateChild<CheckButton>,
#[template_child]
pub reset_sink_mute: TemplateChild<Button>,
#[template_child]
pub reset_volume_slider: TemplateChild<Scale>,
#[template_child]
pub reset_volume_percentage: TemplateChild<Label>,
#[template_child]
pub reset_volume_meter: TemplateChild<ProgressBar>,
pub stream: Arc<RefCell<Sink>>,
pub volume_time_stamp: RefCell<Option<SystemTime>>,
}
#[glib::object_subclass]
impl ObjectSubclass for SinkEntry {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetSinkEntry";
type Type = sink_entry::SinkEntry;
type ParentType = PreferencesGroup;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl PreferencesGroupImpl for SinkEntry {}
impl ObjectImpl for SinkEntry {}
impl WidgetImpl for SinkEntry {}

43
src/components/utils.rs Normal file
View file

@ -0,0 +1,43 @@
use adw::gdk::pango::EllipsizeMode;
use adw::prelude::ListModelExtManual;
use adw::ComboRow;
use glib::{Cast, Object};
use gtk::prelude::{GObjectPropertyExpressionExt, ListBoxRowExt, ListItemExt, WidgetExt};
use gtk::{Align, SignalListItemFactory, StringObject};
pub fn create_dropdown_label_factory() -> SignalListItemFactory {
let factory = SignalListItemFactory::new();
factory.connect_setup(|_, item| {
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
let label = gtk::Label::new(None);
label.set_halign(Align::Start);
item.property_expression("item")
.chain_property::<StringObject>("string")
.bind(&label, "label", gtk::Widget::NONE);
item.set_child(Some(&label));
});
factory
}
pub fn set_combo_row_ellipsis(element: ComboRow) {
for (i, child) in element
.child()
.unwrap()
.observe_children()
.iter::<Object>()
.enumerate()
{
if i == 2 {
if let Ok(object) = child {
if let Some(item) = object.downcast_ref::<gtk::Box>() {
if let Some(widget) = item.first_child() {
if let Some(label) = widget.downcast_ref::<gtk::Label>() {
label.set_ellipsize(EllipsizeMode::End);
label.set_max_width_chars(1);
}
}
}
}
}
}
}

View file

@ -0,0 +1,13 @@
pub mod saved_wifi_entry;
pub mod saved_wifi_entry_impl;
pub mod utils;
pub mod wifi_address_entry;
pub mod wifi_address_entry_impl;
pub mod wifi_box;
pub mod wifi_box_impl;
pub mod wifi_entry;
pub mod wifi_entry_impl;
pub mod wifi_options;
pub mod wifi_options_impl;
pub mod wifi_route_entry;
pub mod wifi_route_entry_impl;

View file

@ -0,0 +1,61 @@
use std::time::Duration;
use crate::components::wifi::saved_wifi_entry_impl;
use crate::components::wifi::wifi_box_impl::WifiBox;
use adw::glib;
use adw::glib::Object;
use adw::prelude::{ButtonExt, WidgetExt};
use dbus::blocking::Connection;
use dbus::{Error, Path};
use glib::subclass::types::ObjectSubclassIsExt;
use glib::{clone, PropertySet};
use gtk::gio;
use gtk::prelude::ListBoxRowExt;
glib::wrapper! {
pub struct SavedWifiEntry(ObjectSubclass<saved_wifi_entry_impl::SavedWifiEntry>)
@extends adw::ActionRow, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::Actionable, gtk::ConstraintTarget, gtk::ListBoxRow;
}
impl SavedWifiEntry {
pub fn new(name: &str, path: Path<'static>, wifi_box: &WifiBox) -> Self {
let entry: SavedWifiEntry = Object::builder().build();
entry.set_activatable(false);
let entry_imp = entry.imp();
entry_imp.reset_edit_saved_wifi_button.connect_clicked(
clone!(@ weak entry_imp, @ weak wifi_box => move |_| {
// TODO accesspoint has to be saved somewhere i guess
// let _option = getConnectionSettings(entryImp.accessPoint.borrow().associated_connection.clone());
// wifiBox.resetWifiNavigation.push(&*WifiOptions::new(_option));
}),
);
entry_imp.reset_saved_wifi_label.set_text(name);
entry_imp.reset_connection_path.set(path);
entry_imp.reset_delete_saved_wifi_button.connect_clicked(
clone!(@weak entry as entry => move |_| {
delete_connection(entry.imp().reset_connection_path.take());
// TODO handle error
let parent = entry.parent().unwrap();
parent.set_visible(false);
parent.unparent();
}),
);
entry
}
}
fn delete_connection(path: Path<'static>) {
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> =
proxy.method_call("org.Xetibo.ReSetWireless", "DeleteConnection", (path,));
});
}

View file

@ -0,0 +1,61 @@
use adw::subclass::preferences_row::PreferencesRowImpl;
use adw::subclass::prelude::ActionRowImpl;
use adw::ActionRow;
use re_set_lib::network::network_structures::AccessPoint;
use std::cell::RefCell;
use dbus::Path;
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate, Label};
use super::saved_wifi_entry;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetSavedWifiEntry.ui")]
pub struct SavedWifiEntry {
#[template_child]
pub reset_delete_saved_wifi_button: TemplateChild<Button>,
#[template_child]
pub reset_edit_saved_wifi_button: TemplateChild<Button>,
#[template_child]
pub reset_saved_wifi_label: TemplateChild<Label>,
pub reset_connection_path: RefCell<Path<'static>>,
pub access_point: RefCell<AccessPoint>,
}
unsafe impl Send for SavedWifiEntry {}
unsafe impl Sync for SavedWifiEntry {}
#[glib::object_subclass]
impl ObjectSubclass for SavedWifiEntry {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetSavedWifiEntry";
type Type = saved_wifi_entry::SavedWifiEntry;
type ParentType = ActionRow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for SavedWifiEntry {
fn constructed(&self) {
self.parent_constructed();
}
}
impl PreferencesRowImpl for SavedWifiEntry {}
impl ListBoxRowImpl for SavedWifiEntry {}
impl ActionRowImpl for SavedWifiEntry {}
impl WidgetImpl for SavedWifiEntry {}
impl WindowImpl for SavedWifiEntry {}
impl ApplicationWindowImpl for SavedWifiEntry {}

View file

@ -0,0 +1,37 @@
use dbus::arg::RefArg;
use dbus::blocking::Connection;
use dbus::Error;
use dbus::Path;
use re_set_lib::network::connection::Connection as ResetConnection;
use std::collections::HashMap;
use std::time::Duration;
#[derive(Default, Copy, Clone)]
pub enum IpProtocol {
#[default]
IPv4,
IPv6,
}
type ResultType =
Result<(HashMap<String, HashMap<String, dbus::arg::Variant<Box<dyn RefArg>>>>,), Error>;
pub fn get_connection_settings(path: Path<'static>) -> ResetConnection {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: ResultType =
proxy.method_call("org.Xetibo.ReSetWireless", "GetConnectionSettings", (path,));
if res.is_err() {
ResetConnection::default();
}
let (res,) = res.unwrap();
let res = ResetConnection::convert_from_propmap(res);
if res.is_err() {
ResetConnection::default();
}
res.unwrap()
}

View file

@ -0,0 +1,160 @@
use std::cell::RefCell;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::rc::Rc;
use std::str::FromStr;
use adw::glib;
use adw::glib::Object;
use adw::prelude::PreferencesRowExt;
use glib::clone;
use glib::subclass::prelude::ObjectSubclassIsExt;
use gtk::prelude::{ButtonExt, EditableExt, WidgetExt};
use re_set_lib::network::connection::{Address, Connection};
use crate::components::wifi::utils::IpProtocol;
use crate::components::wifi::wifi_address_entry_impl;
use crate::components::wifi::wifi_address_entry_impl::WifiAddressEntryImpl;
glib::wrapper! {
pub struct WifiAddressEntry(ObjectSubclass<wifi_address_entry_impl::WifiAddressEntryImpl>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl WifiAddressEntry {
pub fn new(
address: Option<usize>,
conn: Rc<RefCell<Connection>>,
protocol: IpProtocol,
) -> Self {
let entry: WifiAddressEntry = Object::builder().build();
let entry_imp = entry.imp();
if let Some(address) = address {
let conn = conn.borrow();
let address = unsafe { conn.ipv4.address_data.get_unchecked(address) };
entry_imp.reset_address_address.set_text(&address.address);
entry_imp
.reset_address_prefix
.set_text(&address.prefix_length.to_string());
entry_imp
.reset_address_row
.set_title(&format!("{}/{}", &*address.address, address.prefix_length));
}
entry_imp.protocol.set(protocol);
entry.setup_callbacks(conn);
entry
}
pub fn setup_callbacks(&self, connection: Rc<RefCell<Connection>>) {
let self_imp = self.imp();
let conn = connection.clone();
self_imp.reset_address_address.connect_changed(clone!(@weak self_imp => move |entry| {
let address_input = entry.text();
let mut conn = conn.borrow_mut();
if address_input.is_empty() {
self_imp.reset_address_address.remove_css_class("error");
self_imp.reset_address_row.set_title("Add new address");
return;
}
let result = match self_imp.protocol.get() {
IpProtocol::IPv4 => Ipv4Addr::from_str(address_input.as_str()).map(IpAddr::V4),
IpProtocol::IPv6 => Ipv6Addr::from_str(address_input.as_str()).map(IpAddr::V6),
};
match result {
Ok(ip_addr) => {
self_imp.reset_address_address.remove_css_class("error");
let address_data = match self_imp.protocol.get() {
IpProtocol::IPv4 => &mut conn.ipv4.address_data,
IpProtocol::IPv6 => &mut conn.ipv6.address_data,
};
address_data.push(Address::new_no_options(ip_addr.to_string(), self_imp.prefix.get().1 as u32));
*self_imp.address.borrow_mut() = (true, ip_addr.to_string());
}
Err(_) => {
self_imp.reset_address_address.add_css_class("error");
*self_imp.address.borrow_mut() = (false, String::default());
}
}
set_row_name(&self_imp);
}));
let conn = connection.clone();
self_imp.reset_address_prefix.connect_changed(clone!(@weak self_imp => move |entry| {
let prefix_input = entry.text();
let prefix = prefix_input.parse::<u8>();
let mut conn = conn.borrow_mut();
let handle_error = || {
if self_imp.reset_address_prefix.text().is_empty() {
self_imp.reset_address_prefix.remove_css_class("error");
} else {
self_imp.reset_address_prefix.add_css_class("error");
}
self_imp.prefix.set((false, 0));
set_row_name(&self_imp);
};
if prefix_input.is_empty() || prefix.is_err() {
handle_error();
return;
}
let prefix = prefix.unwrap();
match self_imp.protocol.get() {
IpProtocol::IPv4 if prefix <= 32 => {
self_imp.prefix.set((true, prefix as u32));
self_imp.reset_address_prefix.remove_css_class("error");
if let Ok(address2) = Ipv4Addr::from_str(self_imp.reset_address_address.text().as_str()) {
if let Some(addr) = conn.ipv4.address_data.iter_mut()
.find(|conn_addr| *conn_addr.address == address2.to_string()) {
addr.prefix_length = prefix as u32;
}
}
}
IpProtocol::IPv6 if prefix <= 128 => {
self_imp.prefix.set((true, prefix as u32));
self_imp.reset_address_prefix.remove_css_class("error");
if let Ok(address2) = Ipv6Addr::from_str(self_imp.reset_address_address.text().as_str()) {
if let Some(addr) = conn.ipv6.address_data.iter_mut()
.find(|conn_addr| *conn_addr.address == address2.to_string()) {
addr.prefix_length = prefix as u32;
}
}
}
_ => handle_error()
}
set_row_name(&self_imp);
}));
let conn = connection.clone();
self_imp.reset_address_remove.connect_clicked(
clone!(@weak self_imp, @weak self as what => move |_| {
let address = self_imp.reset_address_address.text();
let mut conn = conn.borrow_mut();
conn.ipv4.address_data.retain(|addr| addr.address != address);
what.unparent();
}),
);
}
}
fn set_row_name(self_imp: &WifiAddressEntryImpl) {
if self_imp.reset_address_address.text().is_empty() {
return;
}
let address = self_imp.address.borrow();
let prefix = self_imp.prefix.get();
let title = match (address.0, prefix.0) {
(true, true) => {
format!("{}/{}", address.1, prefix.1)
}
(true, false) => "Prefix wrong".to_string(),
(false, true) => "Address wrong".to_string(),
(false, false) => "Address and Prefix wrong".to_string(),
};
self_imp.reset_address_row.set_title(&title);
}

View file

@ -0,0 +1,52 @@
use crate::components::wifi::utils::IpProtocol;
use crate::components::wifi::wifi_address_entry;
use adw::{EntryRow, ExpanderRow};
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate};
use std::cell::{Cell, RefCell};
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetWifiAddressEntry.ui")]
pub struct WifiAddressEntryImpl {
#[template_child]
pub reset_address_row: TemplateChild<ExpanderRow>,
#[template_child]
pub reset_address_address: TemplateChild<EntryRow>,
#[template_child]
pub reset_address_prefix: TemplateChild<EntryRow>,
#[template_child]
pub reset_address_remove: TemplateChild<Button>,
pub address: RefCell<(bool, String)>,
pub prefix: Cell<(bool, u32)>,
pub protocol: Cell<IpProtocol>,
}
#[glib::object_subclass]
impl ObjectSubclass for WifiAddressEntryImpl {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetWifiAddressEntry";
type Type = wifi_address_entry::WifiAddressEntry;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for WifiAddressEntryImpl {
fn constructed(&self) {
self.parent_constructed();
}
}
impl BoxImpl for WifiAddressEntryImpl {}
impl WidgetImpl for WifiAddressEntryImpl {}
impl WindowImpl for WifiAddressEntryImpl {}
impl ApplicationWindowImpl for WifiAddressEntryImpl {}

View file

@ -0,0 +1,454 @@
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::Duration;
use crate::components::base::utils::Listeners;
use crate::components::utils::set_combo_row_ellipsis;
use adw::glib;
use adw::glib::Object;
use adw::prelude::{ComboRowExt, ListBoxRowExt, PreferencesGroupExt};
use adw::subclass::prelude::ObjectSubclassIsExt;
use dbus::blocking::Connection;
use dbus::message::SignalArgs;
use dbus::Error;
use dbus::Path;
use glib::{clone, Cast, PropertySet};
use gtk::glib::Variant;
use gtk::prelude::{ActionableExt, WidgetExt};
use gtk::{gio, StringObject};
use re_set_lib::network::network_structures::{AccessPoint, WifiDevice, WifiStrength};
use re_set_lib::signals::{AccessPointAdded, WifiDeviceChanged};
use re_set_lib::signals::{AccessPointChanged, AccessPointRemoved};
use crate::components::wifi::wifi_box_impl;
use crate::components::wifi::wifi_entry::WifiEntry;
use super::saved_wifi_entry::SavedWifiEntry;
glib::wrapper! {
pub struct WifiBox(ObjectSubclass<wifi_box_impl::WifiBox>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
type ResultMap = Result<(Vec<(Path<'static>, Vec<u8>)>,), Error>;
unsafe impl Send for WifiBox {}
unsafe impl Sync for WifiBox {}
impl WifiBox {
pub fn new(listeners: Arc<Listeners>) -> Arc<Self> {
let obj: Arc<WifiBox> = Arc::new(Object::builder().build());
setup_callbacks(listeners, obj)
}
pub fn setup_callbacks(&self) {}
}
fn setup_callbacks(listeners: Arc<Listeners>, wifi_box: Arc<WifiBox>) -> Arc<WifiBox> {
let wifi_status = get_wifi_status();
let imp = wifi_box.imp();
let wifibox_ref = wifi_box.clone();
imp.reset_saved_networks.set_activatable(true);
imp.reset_saved_networks
.set_action_name(Some("navigation.push"));
imp.reset_saved_networks
.set_action_target_value(Some(&Variant::from("saved")));
imp.reset_wifi_switch.set_active(wifi_status);
imp.reset_wifi_switch.set_state(wifi_status);
imp.reset_available_networks.set_activatable(true);
imp.reset_available_networks
.set_action_name(Some("navigation.pop"));
set_combo_row_ellipsis(imp.reset_wifi_device.get());
imp.reset_wifi_switch.connect_state_set(
clone!(@weak imp => @default-return glib::Propagation::Proceed, move |_, value| {
set_wifi_enabled(value);
if !value {
let mut map = imp.wifi_entries.lock().unwrap();
for entry in map.iter() {
imp.reset_wifi_list.remove(&*(*entry.1));
}
map.clear();
imp.wifi_entries_path.lock().unwrap().clear();
listeners.wifi_listener.store(false, Ordering::SeqCst);
} else {
start_event_listener(listeners.clone(), wifibox_ref.clone());
show_stored_connections(wifibox_ref.clone());
scan_for_wifi(wifibox_ref.clone());
}
glib::Propagation::Proceed
}),
);
wifi_box
}
pub fn scan_for_wifi(wifi_box: Arc<WifiBox>) {
let wifibox_ref = wifi_box.clone();
let _wifibox_ref_listener = wifi_box.clone();
let wifi_entries = wifi_box.imp().wifi_entries.clone();
let wifi_entries_path = wifi_box.imp().wifi_entries_path.clone();
gio::spawn_blocking(move || {
let access_points = get_access_points();
let devices = get_wifi_devices();
{
let imp = wifibox_ref.imp();
let list = imp.reset_model_list.write().unwrap();
let mut model_index = imp.reset_model_index.write().unwrap();
let mut map = imp.reset_wifi_devices.write().unwrap();
imp.reset_current_wifi_device
.replace(devices.last().unwrap().clone());
for (index, device) in devices.into_iter().enumerate() {
list.append(&device.name);
map.insert(device.name.clone(), (device, index as u32));
*model_index += 1;
}
}
let wifi_entries = wifi_entries.clone();
let wifi_entries_path = wifi_entries_path.clone();
dbus_start_network_events();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let mut wifi_entries = wifi_entries.lock().unwrap();
let mut wifi_entries_path = wifi_entries_path.lock().unwrap();
let imp = wifibox_ref.imp();
let list = imp.reset_model_list.read().unwrap();
imp.reset_wifi_device.set_model(Some(&*list));
let map = imp.reset_wifi_devices.read().unwrap();
{
let device = imp.reset_current_wifi_device.borrow();
if let Some(index) = map.get(&device.name) {
imp.reset_wifi_device.set_selected(index.1);
}
}
imp.reset_wifi_device.connect_selected_notify(
clone!(@weak imp => move |dropdown| {
let selected = dropdown.selected_item();
if selected.is_none() {
return;
}
let selected = selected.unwrap();
let selected = selected.downcast_ref::<StringObject>().unwrap();
let selected = selected.string().to_string();
let device = imp.reset_wifi_devices.read().unwrap();
let device = device.get(&selected);
if device.is_none() {
return;
}
set_wifi_device(device.unwrap().0.path.clone());
}),
);
for access_point in access_points {
if access_point.ssid.is_empty() {
continue;
}
let ssid = access_point.ssid.clone();
let path = access_point.dbus_path.clone();
let connected =
imp.reset_current_wifi_device.borrow().active_access_point == path;
let entry = WifiEntry::new(connected, access_point, imp);
wifi_entries.insert(ssid, entry.clone());
wifi_entries_path.insert(path, entry.clone());
imp.reset_wifi_list.add(&*entry);
}
});
});
});
}
pub fn show_stored_connections(wifi_box: Arc<WifiBox>) {
let wifibox_ref = wifi_box.clone();
gio::spawn_blocking(move || {
let connections = get_stored_connections();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let self_imp = wifibox_ref.imp();
for connection in connections {
// TODO include button for settings
let name =
&String::from_utf8(connection.1).unwrap_or_else(|_| String::from(""));
let entry = SavedWifiEntry::new(name, connection.0, self_imp);
self_imp.reset_stored_wifi_list.add(&entry);
}
});
});
});
}
pub fn dbus_start_network_events() {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(), Error> =
proxy.method_call("org.Xetibo.ReSetWireless", "StartNetworkListener", ());
}
pub fn get_access_points() -> Vec<AccessPoint> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Vec<AccessPoint>,), Error> =
proxy.method_call("org.Xetibo.ReSetWireless", "ListAccessPoints", ());
if res.is_err() {
return Vec::new();
}
let (access_points,) = res.unwrap();
access_points
}
pub fn set_wifi_device(path: Path<'static>) {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(bool,), Error> =
proxy.method_call("org.Xetibo.ReSetWireless", "SetWifiDevice", (path,));
}
pub fn get_wifi_devices() -> Vec<WifiDevice> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(Vec<WifiDevice>,), Error> =
proxy.method_call("org.Xetibo.ReSetWireless", "GetAllWifiDevices", ());
if res.is_err() {
return Vec::new();
}
let (devices,) = res.unwrap();
devices
}
pub fn get_wifi_status() -> bool {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: Result<(bool,), Error> =
proxy.method_call("org.Xetibo.ReSetWireless", "GetWifiStatus", ());
if res.is_err() {
return false;
}
res.unwrap().0
}
pub fn get_stored_connections() -> Vec<(Path<'static>, Vec<u8>)> {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let res: ResultMap = proxy.method_call("org.Xetibo.ReSetWireless", "ListStoredConnections", ());
if res.is_err() {
return Vec::new();
}
let (connections,) = res.unwrap();
connections
}
pub fn set_wifi_enabled(enabled: bool) {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(bool,), Error> =
proxy.method_call("org.Xetibo.ReSetWireless", "SetWifiEnabled", (enabled,));
}
pub fn start_event_listener(listeners: Arc<Listeners>, wifi_box: Arc<WifiBox>) {
gio::spawn_blocking(move || {
if listeners.wifi_disabled.load(Ordering::SeqCst)
|| listeners.wifi_listener.load(Ordering::SeqCst)
{
return;
}
listeners.wifi_listener.store(true, Ordering::SeqCst);
let conn = Connection::new_session().unwrap();
let added_ref = wifi_box.clone();
let removed_ref = wifi_box.clone();
let changed_ref = wifi_box.clone();
let wifi_changed_ref = wifi_box.clone();
let access_point_added = AccessPointAdded::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let access_point_removed = AccessPointRemoved::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let access_point_changed = AccessPointChanged::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let device_changed = WifiDeviceChanged::match_rule(
Some(&"org.Xetibo.ReSetDaemon".into()),
Some(&Path::from("/org/Xetibo/ReSetDaemon")),
)
.static_clone();
let res = conn.add_match(access_point_added, move |ir: AccessPointAdded, _, _| {
let wifi_box = added_ref.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = wifi_box.imp();
let mut wifi_entries = imp.wifi_entries.lock().unwrap();
let mut wifi_entries_path = imp.wifi_entries_path.lock().unwrap();
let ssid = ir.access_point.ssid.clone();
let path = ir.access_point.dbus_path.clone();
if wifi_entries.get(&ssid).is_some() || ssid.is_empty() {
return;
}
let connected = imp.reset_current_wifi_device.borrow().active_access_point
== ir.access_point.dbus_path;
let entry = WifiEntry::new(connected, ir.access_point, imp);
wifi_entries.insert(ssid, entry.clone());
wifi_entries_path.insert(path, entry.clone());
imp.reset_wifi_list.add(&*entry);
});
});
true
});
if res.is_err() {
println!("fail on access point add event");
return;
}
let res = conn.add_match(access_point_removed, move |ir: AccessPointRemoved, _, _| {
let wifi_box = removed_ref.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = wifi_box.imp();
let mut wifi_entries = imp.wifi_entries.lock().unwrap();
let mut wifi_entries_path = imp.wifi_entries_path.lock().unwrap();
let entry = wifi_entries_path.remove(&ir.access_point);
if entry.is_none() {
return;
}
let entry = entry.unwrap();
let ssid = entry.imp().access_point.borrow().ssid.clone();
wifi_entries.remove(&ssid);
imp.reset_wifi_list.remove(&*entry);
});
});
true
});
if res.is_err() {
println!("fail on access point remove event");
return;
}
let res = conn.add_match(access_point_changed, move |ir: AccessPointChanged, _, _| {
let wifi_box = changed_ref.clone();
glib::spawn_future(async move {
glib::idle_add_local_once(move || {
let imp = wifi_box.imp();
let wifi_entries = imp.wifi_entries.lock().unwrap();
let entry = wifi_entries.get(&ir.access_point.ssid);
if entry.is_none() {
return;
}
let entry = entry.unwrap();
let entry_imp = entry.imp();
let strength = WifiStrength::from_u8(ir.access_point.strength);
let ssid = ir.access_point.ssid.clone();
let name_opt = String::from_utf8(ssid).unwrap_or_else(|_| String::from(""));
let name = name_opt.as_str();
entry_imp.wifi_strength.set(strength);
entry_imp.reset_wifi_label.get().set_text(name);
entry_imp.reset_wifi_encrypted.set_visible(false);
// TODO handle encryption thing
entry_imp
.reset_wifi_strength
.get()
.set_from_icon_name(match strength {
WifiStrength::Excellent => {
Some("network-wireless-signal-excellent-symbolic")
}
WifiStrength::Ok => Some("network-wireless-signal-ok-symbolic"),
WifiStrength::Weak => Some("network-wireless-signal-weak-symbolic"),
WifiStrength::None => Some("network-wireless-signal-none-symbolic"),
});
if !ir.access_point.stored {
entry_imp.reset_wifi_edit_button.set_sensitive(false);
}
if ir.access_point.dbus_path
== imp.reset_current_wifi_device.borrow().active_access_point
{
entry_imp.reset_wifi_connected.set_text("Connected");
} else {
entry_imp.reset_wifi_connected.set_text("");
}
{
let mut wifi_name = entry_imp.wifi_name.borrow_mut();
*wifi_name = String::from(name);
}
});
});
true
});
if res.is_err() {
println!("fail on access point change event");
return;
}
let res = conn.add_match(device_changed, move |ir: WifiDeviceChanged, _, _| {
let wifi_box = wifi_changed_ref.clone();
glib::spawn_future(async move {
glib::idle_add_once(move || {
let imp = wifi_box.imp();
let mut current_device = imp.reset_current_wifi_device.borrow_mut();
if current_device.path == ir.wifi_device.path {
current_device.active_access_point = ir.wifi_device.active_access_point;
} else {
*current_device = ir.wifi_device;
}
let mut wifi_entries = imp.wifi_entries.lock().unwrap();
for entry in wifi_entries.iter_mut() {
let imp = entry.1.imp();
let mut connected = imp.connected.borrow_mut();
*connected = imp.access_point.borrow().dbus_path == current_device.path;
if *connected {
imp.reset_wifi_connected.set_text("Connected");
} else {
imp.reset_wifi_connected.set_text("");
}
}
});
});
true
});
if res.is_err() {
println!("fail on wifi device change event");
return;
}
loop {
let _ = conn.process(Duration::from_millis(1000));
if !listeners.wifi_listener.load(Ordering::SeqCst) {
break;
}
}
});
}

View file

@ -0,0 +1,79 @@
use crate::components::wifi::wifi_box;
use adw::{ActionRow, ComboRow, NavigationView, PreferencesGroup};
use dbus::Path;
use gtk::subclass::prelude::*;
use gtk::{glib, CompositeTemplate, Switch};
use gtk::{prelude::*, StringList};
use re_set_lib::network::network_structures::WifiDevice;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use crate::components::base::list_entry::ListEntry;
use crate::components::wifi::wifi_entry::WifiEntry;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetWiFi.ui")]
pub struct WifiBox {
#[template_child]
pub reset_wifi_navigation: TemplateChild<NavigationView>,
#[template_child]
pub reset_wifi_details: TemplateChild<PreferencesGroup>,
#[template_child]
pub reset_wifi_device: TemplateChild<ComboRow>,
#[template_child]
pub reset_saved_networks: TemplateChild<ActionRow>,
#[template_child]
pub reset_wifi_switch: TemplateChild<Switch>,
#[template_child]
pub reset_wifi_list: TemplateChild<PreferencesGroup>,
#[template_child]
pub reset_stored_wifi_list: TemplateChild<PreferencesGroup>,
#[template_child]
pub reset_available_networks: TemplateChild<ActionRow>,
pub wifi_entries: Arc<Mutex<HashMap<Vec<u8>, Arc<WifiEntry>>>>,
pub wifi_entries_path: Arc<Mutex<HashMap<Path<'static>, Arc<WifiEntry>>>>,
pub saved_wifi_entries: Arc<Mutex<Vec<ListEntry>>>,
pub reset_wifi_devices: Arc<RwLock<HashMap<String, (WifiDevice, u32)>>>,
pub reset_current_wifi_device: Arc<RefCell<WifiDevice>>,
pub reset_model_list: Arc<RwLock<StringList>>,
pub reset_model_index: Arc<RwLock<u32>>,
}
unsafe impl Send for WifiBox {}
unsafe impl Sync for WifiBox {}
#[glib::object_subclass]
impl ObjectSubclass for WifiBox {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetWifi";
type Type = wifi_box::WifiBox;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
WifiEntry::ensure_type();
ListEntry::ensure_type();
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for WifiBox {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.setup_callbacks();
}
}
impl BoxImpl for WifiBox {}
impl WidgetImpl for WifiBox {}
impl WindowImpl for WifiBox {}
impl ApplicationWindowImpl for WifiBox {}

View file

@ -0,0 +1,224 @@
use std::sync::Arc;
use std::time::Duration;
use crate::components::wifi::utils::get_connection_settings;
use adw::glib;
use adw::glib::{Object, PropertySet};
use adw::prelude::{ActionRowExt, ButtonExt, EditableExt, PopoverExt};
use adw::subclass::prelude::ObjectSubclassIsExt;
use dbus::blocking::Connection;
use dbus::Error;
use glib::clone;
use gtk::gio;
use gtk::prelude::{ListBoxRowExt, WidgetExt};
use re_set_lib::network::network_structures::{AccessPoint, WifiStrength};
use crate::components::wifi::wifi_box_impl::WifiBox;
use crate::components::wifi::wifi_entry_impl;
use crate::components::wifi::wifi_options::WifiOptions;
glib::wrapper! {
pub struct WifiEntry(ObjectSubclass<wifi_entry_impl::WifiEntry>)
@extends adw::ActionRow, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::Actionable, gtk::ConstraintTarget, gtk::ListBoxRow;
}
unsafe impl Send for WifiEntry {}
unsafe impl Sync for WifiEntry {}
impl WifiEntry {
pub fn new(connected: bool, access_point: AccessPoint, wifi_box: &WifiBox) -> Arc<Self> {
let entry: Arc<WifiEntry> = Arc::new(Object::builder().build());
let stored_entry = entry.clone();
let new_entry = entry.clone();
let entry_imp = entry.imp();
let strength = WifiStrength::from_u8(access_point.strength);
let ssid = access_point.ssid.clone();
let name_opt = String::from_utf8(ssid).unwrap_or_else(|_| String::from(""));
let name = name_opt.as_str();
entry_imp.wifi_strength.set(strength);
entry_imp.reset_wifi_label.get().set_text(name);
entry_imp.reset_wifi_encrypted.set_visible(false);
entry_imp.connected.set(connected);
// TODO handle encryption thing
entry_imp
.reset_wifi_strength
.get()
.set_from_icon_name(match strength {
WifiStrength::Excellent => Some("network-wireless-signal-excellent-symbolic"),
WifiStrength::Ok => Some("network-wireless-signal-ok-symbolic"),
WifiStrength::Weak => Some("network-wireless-signal-weak-symbolic"),
WifiStrength::None => Some("network-wireless-signal-none-symbolic"),
});
if !access_point.stored {
entry_imp.reset_wifi_edit_button.set_sensitive(false);
}
if connected {
entry_imp.reset_wifi_connected.set_text("Connected");
}
{
let mut wifi_name = entry_imp.wifi_name.borrow_mut();
*wifi_name = String::from(name);
}
entry_imp.access_point.set(access_point);
entry.set_activatable(true);
entry.connect_activated(clone!(@weak entry_imp => move |_| {
let access_point = entry_imp.access_point.borrow();
if *entry_imp.connected.borrow() {
click_disconnect(stored_entry.clone());
} else if access_point.stored {
click_stored_network(stored_entry.clone());
} else {
click_new_network(new_entry.clone());
}
}));
entry.setup_callbacks(wifi_box);
entry
}
pub fn setup_callbacks(&self, wifi_box: &WifiBox) {
let self_imp = self.imp();
self_imp.reset_wifi_edit_button.connect_clicked(clone!(@ weak self_imp, @ weak wifi_box => move |_| {
let _option = get_connection_settings(self_imp.access_point.borrow().associated_connection.clone());
wifi_box.reset_wifi_navigation.push(&*WifiOptions::new(_option, self_imp.access_point.borrow().dbus_path.clone()));
}));
}
}
pub fn click_disconnect(entry: Arc<WifiEntry>) {
let entry_ref = entry.clone();
entry.set_activatable(false);
gio::spawn_blocking(move || {
let imp = entry_ref.imp();
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(10000),
);
let res: Result<(bool,), Error> = proxy.method_call(
"org.Xetibo.ReSetWireless",
"DisconnectFromCurrentAccessPoint",
(),
);
if res.is_err() {
imp.connected.replace(false);
return;
}
imp.reset_wifi_connected.set_text("");
imp.connected.replace(false);
glib::spawn_future(async move {
glib::idle_add_once(move || {
entry.set_activatable(true);
});
});
});
}
pub fn click_stored_network(entry: Arc<WifiEntry>) {
let entry_imp = entry.imp();
let access_point = entry_imp.access_point.borrow().clone();
let entry_ref = entry.clone();
entry.set_activatable(false);
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(10000),
);
let res: Result<(bool,), Error> = proxy.method_call(
"org.Xetibo.ReSetWireless",
"ConnectToKnownAccessPoint",
(access_point,),
);
glib::spawn_future(async move {
glib::idle_add_once(move || {
entry.set_activatable(true);
let imp = entry_ref.imp();
if res.is_err() {
imp.connected.replace(false);
return;
}
if res.unwrap() == (false,) {
imp.connected.replace(false);
return;
}
let imp = entry_ref.imp();
imp.reset_wifi_connected.set_text("Connected");
imp.connected.replace(true);
});
});
});
// TODO crate spinner animation and block UI
}
pub fn click_new_network(entry: Arc<WifiEntry>) {
let connect_new_network =
|entry: Arc<WifiEntry>, access_point: AccessPoint, password: String| {
let entry_ref = entry.clone();
let popup = entry.imp().reset_wifi_popup.imp();
popup.reset_popup_label.set_text("Connecting...");
popup.reset_popup_label.set_visible(true);
popup.reset_popup_entry.set_sensitive(false);
popup.reset_popup_button.set_sensitive(false);
gio::spawn_blocking(move || {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(10000),
);
let res: Result<(bool,), Error> = proxy.method_call(
"org.Xetibo.ReSetWireless",
"ConnectToNewAccessPoint",
(access_point, password),
);
glib::spawn_future(async move {
glib::idle_add_once(move || {
if res.is_err() {
let imp = entry_ref.imp();
imp.reset_wifi_popup
.imp()
.reset_popup_label
.set_text("Could not connect to dbus.");
imp.connected.replace(false);
return;
}
if res.unwrap() == (false,) {
let imp = entry_ref.imp();
imp.reset_wifi_popup
.imp()
.reset_popup_label
.set_text("Could not connect to access point.");
imp.connected.replace(false);
return;
}
let imp = entry_ref.imp();
imp.reset_wifi_popup.popdown();
imp.reset_wifi_edit_button.set_sensitive(true);
imp.reset_wifi_connected.set_text("Connected");
imp.connected.replace(true);
});
});
});
// TODO crate spinner animation and block UI
};
let entry_imp = entry.imp();
let popup_imp = entry_imp.reset_wifi_popup.imp();
popup_imp
.reset_popup_entry
.connect_activate(clone!(@weak entry as orig_entry, @weak entry_imp => move |entry| {
connect_new_network(orig_entry, entry_imp.access_point.clone().take(), entry.text().to_string());
}));
popup_imp.reset_popup_button.connect_clicked(
clone!(@weak entry as orig_entry,@weak entry_imp, @weak popup_imp => move |_| {
let entry = entry_imp.reset_wifi_popup.imp().reset_popup_entry.text().to_string();
connect_new_network(orig_entry, entry_imp.access_point.clone().take(), entry);
}),
);
entry_imp.reset_wifi_popup.popup();
}

View file

@ -0,0 +1,67 @@
use crate::components::base::popup::Popup;
use crate::components::wifi::wifi_entry;
use adw::subclass::preferences_row::PreferencesRowImpl;
use adw::subclass::prelude::ActionRowImpl;
use adw::ActionRow;
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate, Image, Label};
use re_set_lib::network::network_structures::{AccessPoint, WifiStrength};
use std::cell::RefCell;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetWifiEntry.ui")]
pub struct WifiEntry {
#[template_child]
pub reset_wifi_strength: TemplateChild<Image>,
#[template_child]
pub reset_wifi_encrypted: TemplateChild<Image>,
#[template_child]
pub reset_wifi_label: TemplateChild<Label>,
#[template_child]
pub reset_wifi_edit_button: TemplateChild<Button>,
#[template_child]
pub reset_wifi_connected: TemplateChild<Label>,
#[template_child]
pub reset_wifi_popup: TemplateChild<Popup>,
pub wifi_name: RefCell<String>,
pub wifi_strength: RefCell<WifiStrength>,
pub access_point: RefCell<AccessPoint>,
pub connected: RefCell<bool>,
}
unsafe impl Send for WifiEntry {}
unsafe impl Sync for WifiEntry {}
#[glib::object_subclass]
impl ObjectSubclass for WifiEntry {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetWifiEntry";
type Type = wifi_entry::WifiEntry;
type ParentType = ActionRow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for WifiEntry {
fn constructed(&self) {
self.parent_constructed();
}
}
impl PreferencesRowImpl for WifiEntry {}
impl ListBoxRowImpl for WifiEntry {}
impl ActionRowImpl for WifiEntry {}
impl WidgetImpl for WifiEntry {}
impl WindowImpl for WifiEntry {}
impl ApplicationWindowImpl for WifiEntry {}

View file

@ -0,0 +1,392 @@
use std::net::{Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use adw::glib::Object;
use adw::prelude::{ActionRowExt, ComboRowExt, PreferencesGroupExt};
use adw::subclass::prelude::ObjectSubclassIsExt;
use adw::{gio, glib};
use dbus::arg::PropMap;
use dbus::{Error, Path};
use glib::{clone, PropertySet};
use gtk::prelude::{ButtonExt, EditableExt, WidgetExt};
use re_set_lib::network::connection::{Connection, DNSMethod4, DNSMethod6, Enum, TypeSettings};
use IpProtocol::{IPv4, IPv6};
use crate::components::wifi::utils::IpProtocol;
use crate::components::wifi::wifi_address_entry::WifiAddressEntry;
use crate::components::wifi::wifi_options_impl;
use crate::components::wifi::wifi_route_entry::WifiRouteEntry;
glib::wrapper! {
pub struct WifiOptions(ObjectSubclass<wifi_options_impl::WifiOptions>)
@extends adw::NavigationPage, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
unsafe impl Send for WifiOptions {}
unsafe impl Sync for WifiOptions {}
impl WifiOptions {
pub fn new(connection: Connection, access_point: Path<'static>) -> Arc<Self> {
let wifi_option: Arc<WifiOptions> = Arc::new(Object::builder().build());
wifi_option.imp().connection.set(connection);
wifi_option.initialize_ui();
setup_callbacks(&wifi_option, access_point);
wifi_option
}
pub fn initialize_ui(&self) {
let self_imp = self.imp();
let ip4_address_length;
let ip4_route_length;
let ip6_address_length;
let ip6_route_length;
{
let conn = self_imp.connection.borrow();
ip4_address_length = conn.ipv4.address_data.len();
ip4_route_length = conn.ipv4.route_data.len();
ip6_address_length = conn.ipv4.address_data.len();
ip6_route_length = conn.ipv4.route_data.len();
// General
self_imp
.reset_wifi_auto_connect
.set_active(conn.settings.autoconnect);
self_imp
.reset_wifi_metered
.set_active(conn.settings.metered != -1);
match &conn.device {
TypeSettings::WIFI(wifi) => {
self_imp.reset_wifi_link_speed.set_visible(false);
self_imp.reset_wifi_ip4_addr.set_visible(false);
self_imp.reset_wifi_ip6_addr.set_visible(false);
self_imp.reset_wifi_dns.set_visible(false);
self_imp.reset_wifi_gateway.set_visible(false);
self_imp.reset_wifi_last_used.set_visible(true);
self_imp
.reset_wifi_mac
.set_subtitle(&wifi.cloned_mac_address);
self_imp
.reset_wifi_name
.set_subtitle(&String::from_utf8(wifi.ssid.clone()).unwrap_or_default());
}
TypeSettings::ETHERNET(ethernet) => {
self_imp.reset_wifi_link_speed.set_visible(true);
self_imp.reset_wifi_ip4_addr.set_visible(true);
self_imp.reset_wifi_ip6_addr.set_visible(true);
self_imp.reset_wifi_dns.set_visible(true);
self_imp.reset_wifi_gateway.set_visible(true);
self_imp.reset_wifi_last_used.set_visible(false);
self_imp
.reset_wifi_mac
.set_subtitle(&ethernet.cloned_mac_address);
self_imp
.reset_wifi_link_speed
.set_subtitle(&ethernet.speed.to_string());
}
TypeSettings::VPN(_vpn) => {}
TypeSettings::None => {}
};
// IPv4
self_imp
.reset_ip4_method
.set_selected(conn.ipv4.dns_method.to_i32() as u32);
self.set_ip4_visibility(conn.ipv4.dns_method.to_i32() as u32);
let ipv4_dns: Vec<String> = conn
.ipv4
.dns
.iter()
.map(|addr| {
addr.iter()
.map(|octet| octet.to_string())
.collect::<Vec<String>>()
.join(".")
})
.collect();
self_imp.reset_ip4_dns.set_text(&ipv4_dns.join(", "));
self_imp.reset_ip4_gateway.set_text(&conn.ipv4.gateway);
// IPv6
self_imp
.reset_ip6_method
.set_selected(conn.ipv6.dns_method.to_i32() as u32);
self.set_ip6_visibility(conn.ipv6.dns_method.to_i32() as u32);
let ipv6_dns: Vec<String> = conn
.ipv6
.dns
.iter()
.map(|addr| {
addr.iter()
.map(|octet| octet.to_string())
.collect::<Vec<String>>()
.join(":")
})
.collect();
self_imp.reset_ip6_dns.set_text(&ipv6_dns.join(", "));
self_imp.reset_ip6_gateway.set_text(&conn.ipv6.gateway);
// Security
if let TypeSettings::WIFI(wifi) = &conn.device {
match wifi.security_settings.key_management.as_str() {
"none" => {
self_imp.reset_wifi_security_dropdown.set_selected(0);
self_imp.reset_wifi_password.set_visible(false);
self_imp.reset_wifi_password.set_text("");
}
"wpa-psk" => {
self_imp.reset_wifi_security_dropdown.set_selected(1);
self_imp.reset_wifi_password.set_visible(true);
self_imp
.reset_wifi_password
.set_text(&wifi.security_settings.psk);
}
_ => {}
}
}
}
// IPv4
for i in 0..ip4_address_length {
let address = &WifiAddressEntry::new(Some(i), self_imp.connection.clone(), IPv4);
self_imp.reset_ip4_address_group.add(address);
}
let address = &WifiAddressEntry::new(None, self_imp.connection.clone(), IPv4);
self_imp.reset_ip4_address_group.add(address);
for i in 0..ip4_route_length {
let route = &WifiRouteEntry::new(Some(i), self_imp.connection.clone(), IPv4);
self_imp.reset_ip4_routes_group.add(route)
}
let route = &WifiRouteEntry::new(None, self_imp.connection.clone(), IPv4);
self_imp.reset_ip4_routes_group.add(route);
// IPv6
for i in 0..ip6_address_length {
let address = &WifiAddressEntry::new(Some(i), self_imp.connection.clone(), IPv6);
self_imp.reset_ip6_address_group.add(address);
}
let address = &WifiAddressEntry::new(None, self_imp.connection.clone(), IPv6);
self_imp.reset_ip6_address_group.add(address);
for i in 0..ip6_route_length {
let route = &WifiRouteEntry::new(Some(i), self_imp.connection.clone(), IPv6);
self_imp.reset_ip6_routes_group.add(route);
}
let route = &WifiRouteEntry::new(None, self_imp.connection.clone(), IPv6);
self_imp.reset_ip6_routes_group.add(route);
// Security
}
pub fn set_ip4_visibility(&self, method: u32) {
let self_imp = self.imp();
match method {
0 => {
// auto
self_imp.reset_ip4_address_group.set_visible(false);
self_imp.reset_ip4_routes_group.set_visible(true);
self_imp.reset_ip4_gateway.set_visible(false);
}
1 => {
// manual
self_imp.reset_ip4_address_group.set_visible(true);
self_imp.reset_ip4_routes_group.set_visible(true);
self_imp.reset_ip4_gateway.set_visible(true);
}
_ => {
self_imp.reset_ip4_address_group.set_visible(false);
self_imp.reset_ip4_routes_group.set_visible(false);
self_imp.reset_ip4_gateway.set_visible(false);
}
}
}
pub fn set_ip6_visibility(&self, method: u32) {
let self_imp = self.imp();
match method {
0 | 1 => {
// auto, dhcp
self_imp.reset_ip6_address_group.set_visible(false);
self_imp.reset_ip6_routes_group.set_visible(true);
self_imp.reset_ip6_gateway.set_visible(false);
}
2 => {
// manual
self_imp.reset_ip6_address_group.set_visible(true);
self_imp.reset_ip6_routes_group.set_visible(true);
self_imp.reset_ip6_gateway.set_visible(true);
}
_ => {
self_imp.reset_ip6_address_group.set_visible(false);
self_imp.reset_ip6_routes_group.set_visible(false);
self_imp.reset_ip6_gateway.set_visible(false);
}
}
}
}
fn setup_callbacks(wifi_options: &Arc<WifiOptions>, path: Path<'static>) {
let imp = wifi_options.imp();
// General
imp.reset_wifi_auto_connect
.connect_active_notify(clone!(@weak imp => move |x| {
imp.connection.borrow_mut().settings.autoconnect = x.is_active();
}));
imp.reset_wifi_metered
.connect_active_notify(clone!(@weak imp => move |x| {
imp.connection.borrow_mut().settings.metered = if x.is_active() { 1 } else { 2 };
}));
imp.wifi_options_apply_button
.connect_clicked(clone!(@weak imp => move |_| {
let prop = imp.connection.borrow().convert_to_propmap();
set_connection_settings(path.clone(), prop);
}));
// IPv4
let wifi_options_ip4 = wifi_options.clone();
imp.reset_ip4_method
.connect_selected_notify(clone!(@weak imp => move |dropdown| {
let selected = dropdown.selected();
let mut conn = imp.connection.borrow_mut();
conn.ipv4.dns_method = DNSMethod4::from_i32(selected as i32);
wifi_options_ip4.set_ip4_visibility(selected);
}));
imp.reset_ip4_dns
.connect_changed(clone!(@weak imp => move |entry| {
let dns_input = entry.text();
let mut conn = imp.connection.borrow_mut();
conn.ipv4.dns.clear();
if dns_input.is_empty() {
imp.reset_ip4_dns.remove_css_class("error");
return;
}
for dns_entry in dns_input.as_str().split(',').map(|s| s.trim()) {
if let Ok(addr) = Ipv4Addr::from_str(dns_entry) {
imp.reset_ip4_dns.remove_css_class("error");
conn.ipv4.dns.push(addr.octets().to_vec());
} else {
imp.reset_ip4_dns.add_css_class("error");
}
}
}));
imp.reset_ip4_address_add_button
.connect_clicked(clone!(@weak imp => move |_| {
let address = &WifiAddressEntry::new(None, imp.connection.clone(), IpProtocol::IPv4);
imp.reset_ip4_address_group.add(address);
}));
imp.reset_ip4_gateway
.connect_changed(clone!(@weak imp => move |entry| {
let gateway_input = entry.text();
let mut conn = imp.connection.borrow_mut();
conn.ipv4.gateway.clear();
if gateway_input.is_empty() {
imp.reset_ip4_gateway.remove_css_class("error");
return;
}
if Ipv4Addr::from_str(gateway_input.as_str()).is_ok() {
imp.reset_ip4_gateway.remove_css_class("error");
conn.ipv4.gateway = gateway_input.to_string();
} else {
imp.reset_ip4_gateway.add_css_class("error");
}
}));
// IPv6
let wifi_options_ip6 = wifi_options.clone();
imp.reset_ip6_method
.connect_selected_notify(clone!(@weak imp => move |dropdown| {
let selected = dropdown.selected();
let mut conn = imp.connection.borrow_mut();
conn.ipv6.dns_method = DNSMethod6::from_i32(selected as i32);
wifi_options_ip6.set_ip6_visibility(selected);
}));
imp.reset_ip6_dns
.connect_changed(clone!(@weak imp => move |entry| {
let dns_input = entry.text();
let mut conn = imp.connection.borrow_mut();
conn.ipv6.dns.clear();
if dns_input.is_empty() {
imp.reset_ip6_dns.remove_css_class("error");
return;
}
for dns_entry in dns_input.as_str().split(',').map(|s| s.trim()) {
if let Ok(addr) = Ipv6Addr::from_str(dns_entry) {
imp.reset_ip6_dns.remove_css_class("error");
conn.ipv6.dns.push(addr.octets().to_vec());
} else {
imp.reset_ip6_dns.add_css_class("error");
}
}
}));
imp.reset_ip6_address_add_button
.connect_clicked(clone!(@weak imp => move |_| {
let address = &WifiAddressEntry::new(None, imp.connection.clone(), IpProtocol::IPv4);
imp.reset_ip6_address_group.add(address);
}));
imp.reset_ip6_gateway
.connect_changed(clone!(@weak imp => move |entry| {
let gateway_input = entry.text();
let mut conn = imp.connection.borrow_mut();
conn.ipv6.gateway.clear();
if gateway_input.is_empty() {
imp.reset_ip6_gateway.remove_css_class("error");
return;
}
if Ipv6Addr::from_str(gateway_input.as_str()).is_ok() {
imp.reset_ip6_gateway.remove_css_class("error");
conn.ipv6.gateway = gateway_input.to_string();
} else {
imp.reset_ip6_gateway.add_css_class("error");
}
}));
// Security
imp.reset_wifi_security_dropdown
.connect_selected_notify(clone!(@weak imp => move |dropdown| {
let selected = dropdown.selected();
let mut conn = imp.connection.borrow_mut();
match (selected, &mut conn.device) {
(0 , TypeSettings::WIFI(wifi)) => { // None
imp.reset_wifi_password.set_visible(false);
wifi.security_settings.key_management = String::from("none");
wifi.security_settings.authentication_algorithm = String::from("open");
},
(1 , TypeSettings::WIFI(wifi)) => { // WPA/WPA2 Personal
imp.reset_wifi_password.set_visible(true);
wifi.security_settings.key_management = String::from("wpa-psk");
wifi.security_settings.authentication_algorithm = String::from("");
},
(_, _) => {}
}
}));
imp.reset_wifi_password
.connect_changed(clone!(@weak imp => move |entry| {
let password_input = entry.text();
let mut conn = imp.connection.borrow_mut();
if let TypeSettings::WIFI(wifi) = &mut conn.device {
wifi.security_settings.psk = password_input.to_string();
}
}));
}
fn set_connection_settings(path: Path<'static>, prop: PropMap) {
gio::spawn_blocking(move || {
let conn = dbus::blocking::Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(1000),
);
let _: Result<(bool,), Error> = proxy.method_call(
"org.Xetibo.ReSetWireless",
"SetConnectionSettings",
(path, prop),
);
});
}

View file

@ -0,0 +1,107 @@
use crate::components::wifi::wifi_options;
use adw::subclass::prelude::NavigationPageImpl;
use adw::{
ActionRow, ComboRow, EntryRow, NavigationPage, PasswordEntryRow, PreferencesGroup, SwitchRow,
};
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate};
use re_set_lib::network::connection::Connection;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetWifiOptions.ui")]
pub struct WifiOptions {
// General
#[template_child]
pub reset_wifi_name: TemplateChild<ActionRow>,
#[template_child]
pub reset_wifi_mac: TemplateChild<ActionRow>,
#[template_child]
pub reset_wifi_link_speed: TemplateChild<ActionRow>,
#[template_child]
pub reset_wifi_ip4_addr: TemplateChild<ActionRow>,
#[template_child]
pub reset_wifi_ip6_addr: TemplateChild<ActionRow>,
#[template_child]
pub reset_wifi_gateway: TemplateChild<ActionRow>,
#[template_child]
pub reset_wifi_dns: TemplateChild<ActionRow>,
#[template_child]
pub reset_wifi_last_used: TemplateChild<ActionRow>,
#[template_child]
pub reset_wifi_auto_connect: TemplateChild<SwitchRow>,
#[template_child]
pub reset_wifi_metered: TemplateChild<SwitchRow>,
// IPv4
#[template_child]
pub reset_ip4_method: TemplateChild<ComboRow>,
#[template_child]
pub reset_ip4_dns: TemplateChild<EntryRow>,
#[template_child]
pub reset_ip4_gateway: TemplateChild<EntryRow>,
#[template_child]
pub reset_ip4_address_group: TemplateChild<PreferencesGroup>,
#[template_child]
pub reset_ip4_address_add_button: TemplateChild<Button>,
#[template_child]
pub reset_ip4_routes_group: TemplateChild<PreferencesGroup>,
#[template_child]
pub reset_ip4_route_add_button: TemplateChild<Button>,
// IPv6
#[template_child]
pub reset_ip6_method: TemplateChild<ComboRow>,
#[template_child]
pub reset_ip6_dns: TemplateChild<EntryRow>,
#[template_child]
pub reset_ip6_gateway: TemplateChild<EntryRow>,
#[template_child]
pub reset_ip6_address_group: TemplateChild<PreferencesGroup>,
#[template_child]
pub reset_ip6_address_add_button: TemplateChild<Button>,
#[template_child]
pub reset_ip6_routes_group: TemplateChild<PreferencesGroup>,
#[template_child]
pub reset_ip6_route_add_button: TemplateChild<Button>,
// Security
#[template_child]
pub reset_wifi_security_dropdown: TemplateChild<ComboRow>,
#[template_child]
pub reset_wifi_password: TemplateChild<PasswordEntryRow>,
// Misc
#[template_child]
pub wifi_options_apply_button: TemplateChild<Button>,
pub connection: Rc<RefCell<Connection>>,
}
#[glib::object_subclass]
impl ObjectSubclass for WifiOptions {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetWifiOptions";
type Type = wifi_options::WifiOptions;
type ParentType = NavigationPage;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl NavigationPageImpl for WifiOptions {}
impl ObjectImpl for WifiOptions {
fn constructed(&self) {
self.parent_constructed();
}
}
impl BoxImpl for WifiOptions {}
impl WidgetImpl for WifiOptions {}
impl WindowImpl for WifiOptions {}
impl ApplicationWindowImpl for WifiOptions {}

View file

@ -0,0 +1,242 @@
use crate::components::wifi::utils::IpProtocol;
use adw::glib;
use adw::glib::Object;
use adw::prelude::{ExpanderRowExt, PreferencesRowExt};
use glib::clone;
use glib::subclass::prelude::ObjectSubclassIsExt;
use gtk::prelude::{EditableExt, WidgetExt};
use re_set_lib::network::connection::{Address, Connection};
use std::cell::RefCell;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::rc::Rc;
use std::str::FromStr;
use crate::components::wifi::wifi_route_entry_impl;
use crate::components::wifi::wifi_route_entry_impl::WifiRouteEntryImpl;
glib::wrapper! {
pub struct WifiRouteEntry(ObjectSubclass<wifi_route_entry_impl::WifiRouteEntryImpl>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
impl WifiRouteEntry {
pub fn new(
address: Option<usize>,
conn: Rc<RefCell<Connection>>,
protocol: IpProtocol,
) -> Self {
let entry: WifiRouteEntry = Object::builder().build();
let entry_imp = entry.imp();
if let Some(address) = address {
let conn = conn.borrow();
let address = unsafe { conn.ipv4.route_data.get_unchecked(address) };
entry_imp.reset_route_address.set_text(&address.address);
entry_imp
.reset_route_prefix
.set_text(&address.prefix_length.to_string());
if let Some(gateway) = &address.gateway {
entry_imp.reset_route_gateway.set_text(gateway);
}
if let Some(metric) = address.metric {
entry_imp.reset_route_metric.set_text(&metric.to_string());
}
entry_imp
.reset_route_row
.set_title(&format!("{}/{}", &*address.address, address.prefix_length));
}
entry_imp.protocol.set(protocol);
entry.setup_callbacks(conn);
entry
}
fn setup_callbacks(&self, connection: Rc<RefCell<Connection>>) {
let self_imp = self.imp();
let conn = connection.clone();
self_imp.reset_route_address.connect_changed(clone!(@weak self_imp => move |entry| {
let address_input = entry.text();
let mut conn = conn.borrow_mut();
if address_input.is_empty() {
self_imp.reset_route_address.remove_css_class("error");
self_imp.reset_route_row.set_title("Add new address");
return;
}
let result = match self_imp.protocol.get() {
IpProtocol::IPv4 => Ipv4Addr::from_str(address_input.as_str()).map(IpAddr::V4),
IpProtocol::IPv6 => Ipv6Addr::from_str(address_input.as_str()).map(IpAddr::V6),
};
match result {
Ok(ip_addr) => {
self_imp.reset_route_address.remove_css_class("error");
let address_data = match self_imp.protocol.get() {
IpProtocol::IPv4 => &mut conn.ipv4.route_data,
IpProtocol::IPv6 => &mut conn.ipv6.route_data,
};
address_data.push(Address::new(ip_addr.to_string(), self_imp.prefix.get().1 as u32,self_imp.gateway.borrow().clone() ,self_imp.metric.get()));
*self_imp.address.borrow_mut() = (true, ip_addr.to_string());
}
Err(_) => {
self_imp.reset_route_address.add_css_class("error");
*self_imp.address.borrow_mut() = (false, String::default());
}
}
set_row_title(&self_imp);
}));
let conn = connection.clone();
self_imp.reset_route_prefix.connect_changed(clone!(@weak self_imp => move |entry| {
let prefix_input = entry.text();
let prefix = prefix_input.parse::<u8>();
let mut conn = conn.borrow_mut();
let handle_error = || {
if self_imp.reset_route_prefix.text().is_empty() {
self_imp.reset_route_prefix.remove_css_class("error");
} else {
self_imp.reset_route_prefix.add_css_class("error");
}
self_imp.prefix.set((false, 0));
set_row_title(&self_imp);
};
if prefix_input.is_empty() || prefix.is_err() {
handle_error();
return;
}
let prefix = prefix.unwrap();
match self_imp.protocol.get() {
IpProtocol::IPv4 if prefix <= 32 => {
self_imp.prefix.set((true, prefix as u32));
self_imp.reset_route_prefix.remove_css_class("error");
if let Ok(address2) = Ipv4Addr::from_str(self_imp.reset_route_address.text().as_str()) {
if let Some(addr) = conn.ipv4.route_data.iter_mut()
.find(|conn_addr| *conn_addr.address == address2.to_string()) {
addr.prefix_length = prefix as u32;
}
}
}
IpProtocol::IPv6 if prefix <= 128 => {
self_imp.prefix.set((true, prefix as u32));
self_imp.reset_route_prefix.remove_css_class("error");
if let Ok(address2) = Ipv6Addr::from_str(self_imp.reset_route_address.text().as_str()) {
if let Some(addr) = conn.ipv6.route_data.iter_mut()
.find(|conn_addr| *conn_addr.address == address2.to_string()) {
addr.prefix_length = prefix as u32;
}
}
}
_ => handle_error()
}
set_row_title(&self_imp);
}));
let conn = connection.clone();
self_imp
.reset_route_gateway
.connect_changed(clone!(@weak self_imp => move |entry| {
let gateway_input = entry.text();
let mut conn = conn.borrow_mut();
if gateway_input.is_empty() {
self_imp.reset_route_gateway.remove_css_class("error");
*self_imp.gateway.borrow_mut() = None;
set_row_subtitle(&self_imp);
return;
}
let result = match self_imp.protocol.get() {
IpProtocol::IPv4 => Ipv4Addr::from_str(gateway_input.as_str()).map(IpAddr::V4),
IpProtocol::IPv6 => Ipv6Addr::from_str(gateway_input.as_str()).map(IpAddr::V6),
};
match result {
Ok(ip_addr) => {
self_imp.reset_route_gateway.remove_css_class("error");
let address_data = match self_imp.protocol.get() {
IpProtocol::IPv4 => &mut conn.ipv4.route_data,
IpProtocol::IPv6 => &mut conn.ipv6.route_data,
};
if let Some(address) = address_data.iter_mut()
.find(|conn_addr| *conn_addr.address == self_imp.reset_route_address.text()) {
address.gateway = Some(ip_addr.to_string());
}
*self_imp.gateway.borrow_mut() = Some(ip_addr.to_string());
}
Err(_) => {
self_imp.reset_route_gateway.add_css_class("error");
*self_imp.gateway.borrow_mut() = None;
}
}
set_row_subtitle(&self_imp);
}));
let conn = connection.clone();
self_imp
.reset_route_metric
.connect_changed(clone!(@weak self_imp => move |entry| {
let metric_input = entry.text();
let mut conn = conn.borrow_mut();
if metric_input.is_empty() {
self_imp.reset_route_metric.remove_css_class("error");
self_imp.metric.set(None);
set_row_subtitle(&self_imp);
return;
}
let result = metric_input.parse::<u32>();
match result {
Ok(metric) => {
self_imp.reset_route_metric.remove_css_class("error");
let address_data = match self_imp.protocol.get() {
IpProtocol::IPv4 => &mut conn.ipv4.route_data,
IpProtocol::IPv6 => &mut conn.ipv6.route_data,
};
if let Some(address) = address_data.iter_mut()
.find(|conn_addr| *conn_addr.address == self_imp.reset_route_address.text()) {
address.metric = Some(metric);
}
self_imp.metric.set(Some(metric));
}
Err(_) => {
self_imp.reset_route_metric.add_css_class("error");
self_imp.metric.set(None);
}
}
set_row_subtitle(&self_imp);
}));
}
}
fn set_row_title(self_imp: &WifiRouteEntryImpl) {
if self_imp.reset_route_address.text().is_empty() {
return;
}
let address = self_imp.address.borrow();
let prefix = self_imp.prefix.get();
let title = match (address.0, prefix.0) {
(true, true) => {
format!("{}/{}", address.1, prefix.1)
}
(true, false) => "Prefix wrong".to_string(),
(false, true) => "Address wrong".to_string(),
(false, false) => "Address and Prefix wrong".to_string(),
};
self_imp.reset_route_row.set_title(&title);
}
fn set_row_subtitle(self_imp: &WifiRouteEntryImpl) {
let gateway = self_imp.gateway.borrow().clone();
let metric = self_imp.metric.get();
let title = match (gateway, metric) {
(Some(gateway), Some(metric)) => {
format!("{}, {}", gateway, metric)
}
(Some(gateway), None) => gateway,
(None, Some(metric)) => metric.to_string(),
(None, None) => String::default(),
};
self_imp.reset_route_row.set_subtitle(&title);
}

View file

@ -0,0 +1,58 @@
use crate::components::wifi::utils::IpProtocol;
use crate::components::wifi::wifi_route_entry;
use adw::{EntryRow, ExpanderRow};
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate};
use std::cell::{Cell, RefCell};
#[derive(Default, CompositeTemplate)]
#[template(resource = "/org/Xetibo/ReSet/resetWifiRouteEntry.ui")]
pub struct WifiRouteEntryImpl {
#[template_child]
pub reset_route_row: TemplateChild<ExpanderRow>,
#[template_child]
pub reset_route_address: TemplateChild<EntryRow>,
#[template_child]
pub reset_route_prefix: TemplateChild<EntryRow>,
#[template_child]
pub reset_route_gateway: TemplateChild<EntryRow>,
#[template_child]
pub reset_route_metric: TemplateChild<EntryRow>,
#[template_child]
pub reset_route_remove: TemplateChild<Button>,
pub address: RefCell<(bool, String)>,
pub prefix: Cell<(bool, u32)>,
pub gateway: RefCell<Option<String>>,
pub metric: Cell<Option<u32>>,
pub protocol: Cell<IpProtocol>,
}
#[glib::object_subclass]
impl ObjectSubclass for WifiRouteEntryImpl {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetWifiRouteEntry";
type Type = wifi_route_entry::WifiRouteEntry;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for WifiRouteEntryImpl {
fn constructed(&self) {
self.parent_constructed();
}
}
impl BoxImpl for WifiRouteEntryImpl {}
impl WidgetImpl for WifiRouteEntryImpl {}
impl WindowImpl for WifiRouteEntryImpl {}
impl ApplicationWindowImpl for WifiRouteEntryImpl {}

View file

@ -0,0 +1,221 @@
use gtk::prelude::FrameExt;
use std::cell::RefCell;
use std::hint::spin_loop;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use crate::components::base::setting_box::SettingBox;
use crate::components::base::utils::{start_audio_listener, Listeners, Position};
use crate::components::bluetooth::bluetooth_box::{
populate_conntected_bluetooth_devices, start_bluetooth_listener, BluetoothBox,
};
use crate::components::input::source_box::{populate_sources, SourceBox};
use crate::components::output::sink_box::{populate_sinks, SinkBox};
use crate::components::wifi::wifi_box::{
scan_for_wifi, show_stored_connections, start_event_listener, WifiBox,
};
use gtk::prelude::WidgetExt;
use gtk::{Align, FlowBox, FlowBoxChild, Frame};
pub const HANDLE_CONNECTIVITY_CLICK: fn(Arc<Listeners>, FlowBox, Rc<RefCell<Position>>) =
|listeners: Arc<Listeners>, reset_main: FlowBox, position: Rc<RefCell<Position>>| {
if handle_init(listeners.clone(), position, Position::Connectivity) {
return;
}
let wifi_box = WifiBox::new(listeners.clone());
start_event_listener(listeners.clone(), wifi_box.clone());
show_stored_connections(wifi_box.clone());
scan_for_wifi(wifi_box.clone());
let wifi_frame = wrap_in_flow_box_child(SettingBox::new(&*wifi_box));
let bluetooth_box = BluetoothBox::new(listeners.clone());
populate_conntected_bluetooth_devices(bluetooth_box.clone());
start_bluetooth_listener(listeners, bluetooth_box.clone());
let bluetooth_frame = wrap_in_flow_box_child(SettingBox::new(&*bluetooth_box));
reset_main.remove_all();
reset_main.insert(&wifi_frame, -1);
reset_main.insert(&bluetooth_frame, -1);
reset_main.set_max_children_per_line(2);
};
pub const HANDLE_WIFI_CLICK: fn(Arc<Listeners>, FlowBox, Rc<RefCell<Position>>) =
|listeners: Arc<Listeners>, reset_main: FlowBox, position: Rc<RefCell<Position>>| {
if handle_init(listeners.clone(), position, Position::Wifi) {
return;
}
let wifi_box = WifiBox::new(listeners.clone());
start_event_listener(listeners, wifi_box.clone());
show_stored_connections(wifi_box.clone());
scan_for_wifi(wifi_box.clone());
let wifi_frame = wrap_in_flow_box_child(SettingBox::new(&*wifi_box));
reset_main.remove_all();
reset_main.insert(&wifi_frame, -1);
reset_main.set_max_children_per_line(1);
};
pub const HANDLE_BLUETOOTH_CLICK: fn(Arc<Listeners>, FlowBox, Rc<RefCell<Position>>) =
|listeners: Arc<Listeners>, reset_main: FlowBox, position: Rc<RefCell<Position>>| {
if handle_init(listeners.clone(), position, Position::Bluetooth) {
return;
}
let bluetooth_box = BluetoothBox::new(listeners.clone());
start_bluetooth_listener(listeners, bluetooth_box.clone());
populate_conntected_bluetooth_devices(bluetooth_box.clone());
let bluetooth_frame = wrap_in_flow_box_child(SettingBox::new(&*bluetooth_box));
reset_main.remove_all();
reset_main.insert(&bluetooth_frame, -1);
reset_main.set_max_children_per_line(1);
};
pub const HANDLE_AUDIO_CLICK: fn(Arc<Listeners>, FlowBox, Rc<RefCell<Position>>) =
|listeners: Arc<Listeners>, reset_main: FlowBox, position: Rc<RefCell<Position>>| {
if handle_init(listeners.clone(), position, Position::Audio) {
return;
}
let audio_output = Arc::new(SinkBox::new());
let audio_input = Arc::new(SourceBox::new());
start_audio_listener(
listeners.clone(),
Some(audio_output.clone()),
Some(audio_input.clone()),
);
if !listeners.pulse_listener.load(Ordering::SeqCst) {
spin_loop();
}
populate_sinks(audio_output.clone());
populate_sources(audio_input.clone());
let sink_frame = wrap_in_flow_box_child(SettingBox::new(&*audio_output));
let source_frame = wrap_in_flow_box_child(SettingBox::new(&*audio_input));
reset_main.remove_all();
reset_main.insert(&sink_frame, -1);
reset_main.insert(&source_frame, -1);
reset_main.set_max_children_per_line(2);
};
pub const HANDLE_VOLUME_CLICK: fn(Arc<Listeners>, FlowBox, Rc<RefCell<Position>>) =
|listeners: Arc<Listeners>, reset_main: FlowBox, position: Rc<RefCell<Position>>| {
if handle_init(listeners.clone(), position, Position::AudioOutput) {
return;
}
let audio_output = Arc::new(SinkBox::new());
start_audio_listener(listeners.clone(), Some(audio_output.clone()), None);
if !listeners.pulse_listener.load(Ordering::SeqCst) {
spin_loop();
}
populate_sinks(audio_output.clone());
let audio_frame = wrap_in_flow_box_child(SettingBox::new(&*audio_output));
reset_main.remove_all();
reset_main.insert(&audio_frame, -1);
reset_main.set_max_children_per_line(1);
};
pub const HANDLE_MICROPHONE_CLICK: fn(Arc<Listeners>, FlowBox, Rc<RefCell<Position>>) =
|listeners: Arc<Listeners>, reset_main: FlowBox, position: Rc<RefCell<Position>>| {
if handle_init(listeners.clone(), position, Position::AudioInput) {
return;
}
let audio_input = Arc::new(SourceBox::new());
start_audio_listener(listeners.clone(), None, Some(audio_input.clone()));
if !listeners.pulse_listener.load(Ordering::SeqCst) {
spin_loop();
}
populate_sources(audio_input.clone());
let source_frame = wrap_in_flow_box_child(SettingBox::new(&*audio_input));
reset_main.remove_all();
reset_main.insert(&source_frame, -1);
reset_main.set_max_children_per_line(1);
};
pub const HANDLE_HOME: fn(Arc<Listeners>, FlowBox, Rc<RefCell<Position>>) =
|listeners: Arc<Listeners>, reset_main: FlowBox, position: Rc<RefCell<Position>>| {
if handle_init(listeners, position, Position::Home) {
return;
}
reset_main.remove_all();
};
fn wrap_in_flow_box_child(widget: SettingBox) -> FlowBoxChild {
let frame = Frame::new(None);
frame.set_child(Some(&widget));
frame.add_css_class("resetSettingFrame");
FlowBoxChild::builder()
.child(&frame)
.halign(Align::Fill)
.valign(Align::Start)
.build()
}
fn handle_init(
listeners: Arc<Listeners>,
position: Rc<RefCell<Position>>,
clicked_position: Position,
) -> bool {
{
let mut pos_borrow = position.borrow_mut();
if *pos_borrow == clicked_position {
return true;
}
*pos_borrow = clicked_position;
}
listeners.stop_network_listener();
listeners.stop_audio_listener();
listeners.stop_bluetooth_listener();
false
}
// for future implementations
// pub const HANDLE_VPN_CLICK: fn(Arc<Listeners>, FlowBox) =
// |listeners: Arc<Listeners>, resetMain: FlowBox| {
// listeners.stop_network_listener();
// listeners.stop_bluetooth_listener();
// listeners.stop_audio_listener();
// let label = Label::new(Some("not implemented yet"));
// resetMain.remove_all();
// resetMain.insert(&label, -1);
// resetMain.set_max_children_per_line(1);
// };
//
// pub const HANDLE_PERIPHERALS_CLICK: fn(Arc<Listeners>, FlowBox) =
// |listeners: Arc<Listeners>, resetMain: FlowBox| {
// listeners.stop_network_listener();
// listeners.stop_audio_listener();
// listeners.stop_bluetooth_listener();
// let label = Label::new(Some("not implemented yet"));
// resetMain.remove_all();
// resetMain.insert(&label, -1);
// resetMain.set_max_children_per_line(1);
// };
//
// pub const HANDLE_MONITOR_CLICK: fn(Arc<Listeners>, FlowBox) =
// |listeners: Arc<Listeners>, resetMain: FlowBox| {
// listeners.stop_network_listener();
// listeners.stop_audio_listener();
// listeners.stop_bluetooth_listener();
// let label = Label::new(Some("not implemented yet"));
// resetMain.remove_all();
// resetMain.insert(&label, -1);
// resetMain.set_max_children_per_line(1);
// };
//
// pub const HANDLE_MOUSE_CLICK: fn(Arc<Listeners>, FlowBox) =
// |listeners: Arc<Listeners>, resetMain: FlowBox| {
// listeners.stop_network_listener();
// listeners.stop_audio_listener();
// listeners.stop_bluetooth_listener();
// let label = Label::new(Some("not implemented yet"));
// resetMain.remove_all();
// resetMain.insert(&label, -1);
// resetMain.set_max_children_per_line(1);
// };
//
// pub const HANDLE_KEYBOARD_CLICK: fn(Arc<Listeners>, FlowBox) =
// |listeners: Arc<Listeners>, resetMain: FlowBox| {
// listeners.stop_network_listener();
// listeners.stop_audio_listener();
// listeners.stop_bluetooth_listener();
// let label = Label::new(Some("not implemented yet"));
// resetMain.remove_all();
// resetMain.insert(&label, -1);
// resetMain.set_max_children_per_line(1);
// };
//

View file

@ -0,0 +1,5 @@
pub mod handle_sidebar_click;
pub mod reset_window;
pub mod reset_window_impl;
pub mod sidebar_entry;
pub mod sidebar_entry_impl;

View file

@ -0,0 +1,293 @@
use adw::glib::clone;
use adw::subclass::prelude::ObjectSubclassIsExt;
use adw::BreakpointCondition;
use glib::Object;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ListBoxRow, Orientation};
use crate::components::window::handle_sidebar_click::*;
use crate::components::window::reset_window_impl;
use crate::components::window::sidebar_entry::SidebarEntry;
use crate::components::window::sidebar_entry_impl::Categories;
glib::wrapper! {
pub struct ReSetWindow(ObjectSubclass<reset_window_impl::ReSetWindow>)
@extends adw::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
unsafe impl Send for ReSetWindow {}
unsafe impl Sync for ReSetWindow {}
impl ReSetWindow {
pub fn new(app: &Application) -> Self {
Object::builder().property("application", app).build()
}
pub fn setup_callback(&self) {
let self_imp = self.imp();
self_imp.reset_search_entry.connect_search_changed(
clone!(@ weak self as window => move |_| {
window.filter_list();
}),
);
self_imp
.reset_sidebar_toggle
.connect_clicked(clone!(@ weak self as window => move |_| {
window.toggle_sidebar();
}));
self_imp.reset_sidebar_list.connect_row_activated(
clone!(@ weak self_imp as flowbox => move |_, y| {
let result = y.downcast_ref::<SidebarEntry>().unwrap();
let click_event = result.imp().on_click_event.borrow().on_click_event;
(click_event)(flowbox.listeners.clone(), flowbox.reset_main.get(), flowbox.position.clone());
}),
);
self_imp
.reset_close
.connect_clicked(clone!(@ weak self as window => move |_| {
window.close();
}));
}
pub fn handle_dynamic_sidebar(&self) {
let self_imp = self.imp();
self_imp
.reset_sidebar_breakpoint
.set_condition(BreakpointCondition::parse("max-width: 860sp").as_ref().ok());
self_imp.reset_sidebar_breakpoint.add_setter(
&Object::from(self_imp.reset_overlay_split_view.get()),
"collapsed",
&true.to_value(),
);
self_imp.reset_sidebar_breakpoint.add_setter(
&Object::from(self_imp.reset_sidebar_toggle.get()),
"visible",
&true.to_value(),
);
}
pub fn filter_list(&self) {
let text = self.imp().reset_search_entry.text().to_string();
for (main_entry, sub_entriess) in self.imp().sidebar_entries.borrow().iter() {
if text.is_empty() {
main_entry.set_visible(true);
for sub_entry in sub_entriess {
sub_entry.set_visible(true);
}
continue;
}
if main_entry
.imp()
.name
.borrow()
.to_lowercase()
.contains(&text.to_lowercase())
{
main_entry.set_visible(true);
} else {
main_entry.set_visible(false);
}
for sub_entry in sub_entriess {
if sub_entry
.imp()
.name
.borrow()
.to_lowercase()
.contains(&text.to_lowercase())
{
sub_entry.set_visible(true);
main_entry.set_visible(true);
} else {
sub_entry.set_visible(false);
}
}
}
}
pub fn toggle_sidebar(&self) {
if self.imp().reset_overlay_split_view.shows_sidebar() {
self.imp().reset_overlay_split_view.set_show_sidebar(false);
} else {
self.imp().reset_overlay_split_view.set_show_sidebar(true);
}
}
pub fn setup_sidebar_entries(&self) {
let self_imp = self.imp();
let mut sidebar_entries = self_imp.sidebar_entries.borrow_mut();
let connectivity_list = vec![
SidebarEntry::new(
"WiFi",
"network-wireless-symbolic",
Categories::Connectivity,
true,
HANDLE_WIFI_CLICK,
),
SidebarEntry::new(
"Bluetooth",
"bluetooth-symbolic",
Categories::Connectivity,
true,
HANDLE_BLUETOOTH_CLICK,
),
// uncommented when VPN is implemented
// SidebarEntry::new(
// "VPN",
// "network-vpn-symbolic",
// Categories::Connectivity,
// true,
// HANDLE_VPN_CLICK,
// ),
];
sidebar_entries.push((
SidebarEntry::new(
"Connectivity",
"network-wired-symbolic",
Categories::Connectivity,
false,
HANDLE_CONNECTIVITY_CLICK,
),
connectivity_list,
));
let audio_list = vec![
SidebarEntry::new(
"Output",
"audio-volume-high-symbolic",
Categories::Audio,
true,
HANDLE_VOLUME_CLICK,
),
SidebarEntry::new(
"Input",
"audio-input-microphone-symbolic",
Categories::Audio,
true,
HANDLE_MICROPHONE_CLICK,
),
];
sidebar_entries.push((
SidebarEntry::new(
"Audio",
"audio-headset-symbolic",
Categories::Audio,
false,
HANDLE_AUDIO_CLICK,
),
audio_list,
));
// uncommented when implemented
// let peripheralsList = vec![
// SidebarEntry::new(
// "Displays",
// "video-display-symbolic",
// Categories::Peripherals,
// true,
// HANDLE_MONITOR_CLICK,
// ),
// SidebarEntry::new(
// "Mouse",
// "input-mouse-symbolic",
// Categories::Peripherals,
// true,
// HANDLE_MOUSE_CLICK,
// ),
// SidebarEntry::new(
// "Keyboard",
// "input-keyboard-symbolic",
// Categories::Peripherals,
// true,
// HANDLE_KEYBOARD_CLICK,
// ),
// ];
// let home = SidebarEntry::new(
// "Home",
// "preferences-system-devices-symbolic",
// Categories::Peripherals,
// false,
// HANDLE_VOLUME_CLICK,
// );
//
// sidebar_entries.push((home, Vec::new()));
(HANDLE_VOLUME_CLICK)(
self_imp.listeners.clone(),
self_imp.reset_main.clone(),
self_imp.position.clone(),
);
self_imp
.reset_sidebar_list
.connect_row_activated(clone!(@ weak self_imp => move |_, _| {
self_imp.reset_search_entry.set_text("");
}));
for (main_entry, sub_entries) in sidebar_entries.iter() {
self_imp.reset_sidebar_list.append(main_entry);
for sub_entry in sub_entries {
self_imp.reset_sidebar_list.append(sub_entry);
}
let separator = gtk::Separator::builder()
.margin_bottom(3)
.margin_top(3)
.orientation(Orientation::Horizontal)
.build();
let separator_row = ListBoxRow::new();
separator_row.set_child(Some(&separator));
separator_row.set_selectable(false);
separator_row.set_activatable(false);
self_imp.reset_sidebar_list.append(&separator_row);
}
}
pub fn setup_popover_buttons(&self) {
let self_imp = self.imp();
self_imp
.reset_about_button
.connect_clicked(clone!(@ weak self as window => move |_| {
let dialog = adw::AboutWindow::builder()
.application_name("ReSet")
.application_icon("ReSet")
.developer_name("Xetibo")
.license("GPL-3.0")
.license_type(gtk::License::Gpl30)
.website("https://github.com/Xetibo/ReSet")
.issue_url("https://github.com/Xetibo/ReSet/issues")
.version("0.0.1")
.transient_for(&window)
.modal(true)
.copyright("© 2022-2023 Xetibo")
.developers(vec!["DashieTM".to_string(), "Takotori".to_string()])
.designers(vec!["DashieTM".to_string(), "Takotori".to_string()])
.build();
window.imp().reset_popover_menu.popdown();
dialog.present();
}));
self_imp
.reset_preference_button
.connect_clicked(clone!(@weak self as window => move |_| {
let preferences = adw::PreferencesWindow::builder().build();
window.imp().reset_popover_menu.popdown();
preferences.present();
}));
self_imp
.reset_shortcuts_button
.connect_clicked(clone!(@weak self as window => move |_| {
let shortcuts = gtk::ShortcutsWindow::builder().build();
window.imp().reset_popover_menu.popdown();
shortcuts.present();
}));
}
}

View file

@ -0,0 +1,103 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use adw::glib::StaticTypeExt;
use adw::subclass::prelude::AdwApplicationWindowImpl;
use adw::{Breakpoint, OverlaySplitView};
use glib::subclass::InitializingObject;
use gtk::prelude::WidgetExt;
use gtk::subclass::prelude::*;
use gtk::{glib, Button, CompositeTemplate, FlowBox, ListBox, PopoverMenu, SearchEntry};
use crate::components::base::utils::{Listeners, Position};
use crate::components::wifi::wifi_box::WifiBox;
use crate::components::window::reset_window;
use crate::components::window::sidebar_entry::SidebarEntry;
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/Xetibo/ReSet/resetMainWindow.ui")]
pub struct ReSetWindow {
#[template_child]
pub reset_main: TemplateChild<FlowBox>,
#[template_child]
pub reset_sidebar_breakpoint: TemplateChild<Breakpoint>,
#[template_child]
pub reset_overlay_split_view: TemplateChild<OverlaySplitView>,
#[template_child]
pub reset_search_entry: TemplateChild<SearchEntry>,
#[template_child]
pub reset_sidebar_list: TemplateChild<ListBox>,
#[template_child]
pub reset_sidebar_toggle: TemplateChild<Button>,
#[template_child]
pub reset_popover_menu: TemplateChild<PopoverMenu>,
#[template_child]
pub reset_close: TemplateChild<Button>,
#[template_child]
pub reset_about_button: TemplateChild<Button>,
#[template_child]
pub reset_preference_button: TemplateChild<Button>,
#[template_child]
pub reset_shortcuts_button: TemplateChild<Button>,
pub sidebar_entries: RefCell<Vec<(SidebarEntry, Vec<SidebarEntry>)>>,
pub listeners: Arc<Listeners>,
pub position: Rc<RefCell<Position>>,
}
unsafe impl Send for ReSetWindow {}
unsafe impl Sync for ReSetWindow {}
#[glib::object_subclass]
impl ObjectSubclass for ReSetWindow {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetUI";
type Type = reset_window::ReSetWindow;
type ParentType = adw::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
WifiBox::ensure_type();
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ReSetWindow {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.setup_callback();
obj.setup_popover_buttons();
obj.handle_dynamic_sidebar();
obj.setup_sidebar_entries();
}
}
impl WidgetImpl for ReSetWindow {
fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
self.parent_size_allocate(width, height, baseline);
if width > 658 {
self.reset_main.set_margin_start(60);
self.reset_main.set_margin_end(60);
} else {
let div = (width - 540) / 2;
if div > 1 {
self.reset_main.set_margin_start(div);
self.reset_main.set_margin_end(div);
} else {
self.reset_main.set_margin_start(0);
self.reset_main.set_margin_end(0);
}
}
}
}
impl WindowImpl for ReSetWindow {}
impl ApplicationWindowImpl for ReSetWindow {}
impl AdwApplicationWindowImpl for ReSetWindow {}

View file

@ -0,0 +1,53 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use crate::components::base::utils::{Listeners, Position};
use crate::components::window::sidebar_entry_impl;
use crate::components::window::sidebar_entry_impl::{Categories, SidebarAction};
use adw::subclass::prelude::ObjectSubclassIsExt;
use glib::Object;
use gtk::prelude::*;
use gtk::{glib, FlowBox};
glib::wrapper! {
pub struct SidebarEntry(ObjectSubclass<sidebar_entry_impl::SidebarEntry>)
@extends gtk::ListBoxRow, gtk::Widget,
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
}
impl SidebarEntry {
pub fn new(
entry_name: &str,
icon_name: &str,
category: Categories,
is_subcategory: bool,
click_event: fn(Arc<Listeners>, FlowBox, Rc<RefCell<Position>>),
) -> Self {
let entry: SidebarEntry = Object::builder().build();
let entry_imp = entry.imp();
entry_imp.reset_sidebar_label.get().set_text(entry_name);
entry_imp
.reset_sidebar_image
.set_from_icon_name(Some(icon_name));
entry_imp.category.set(category);
entry_imp.is_subcategory.set(is_subcategory);
{
let mut name = entry_imp.name.borrow_mut();
*name = String::from(entry_name);
let mut action = entry_imp.on_click_event.borrow_mut();
*action = SidebarAction {
on_click_event: click_event,
};
}
Self::set_margin(&entry);
entry
}
fn set_margin(entry: &SidebarEntry) {
if entry.imp().is_subcategory.get() {
let option = entry.child().unwrap();
option.set_margin_start(30);
}
}
}

View file

@ -0,0 +1,67 @@
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::Arc;
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{glib, CompositeTemplate, FlowBox, Image, Label, ListBoxRow};
use crate::components::base::utils::{Listeners, Position};
use crate::components::window::handle_sidebar_click::HANDLE_HOME;
use crate::components::window::sidebar_entry;
#[derive(Default)]
pub enum Categories {
Connectivity,
Audio,
Peripherals,
#[default]
Misc,
}
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/Xetibo/ReSet/resetSidebarEntry.ui")]
pub struct SidebarEntry {
#[template_child]
pub reset_sidebar_label: TemplateChild<Label>,
#[template_child]
pub reset_sidebar_image: TemplateChild<Image>,
pub category: Cell<Categories>,
pub is_subcategory: Cell<bool>,
pub on_click_event: RefCell<SidebarAction>,
pub name: RefCell<String>,
}
pub struct SidebarAction {
pub on_click_event: fn(Arc<Listeners>, FlowBox, Rc<RefCell<Position>>),
}
impl Default for SidebarAction {
fn default() -> Self {
Self {
on_click_event: HANDLE_HOME,
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for SidebarEntry {
const ABSTRACT: bool = false;
const NAME: &'static str = "resetSidebarEntry";
type Type = sidebar_entry::SidebarEntry;
type ParentType = ListBoxRow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for SidebarEntry {}
impl ListBoxRowImpl for SidebarEntry {}
impl WidgetImpl for SidebarEntry {}

View file

@ -1,29 +1,85 @@
mod window;
use std::thread;
use std::time::Duration;
use components::window::reset_window::ReSetWindow;
use dbus::blocking::Connection;
use dbus::Error;
use gtk::gdk::Display;
use gtk::prelude::*;
use gtk::{Application, gio};
use window::Window;
use gtk::{gio, Application, CssProvider};
use reset_daemon::run_daemon;
mod components;
const APP_ID: &str = "org.Xetibo.ReSet";
fn main() {
#[tokio::main]
async fn main() {
tokio::task::spawn(daemon_check());
gio::resources_register_include!("src.templates.gresource")
.expect("Failed to register resources.");
gio::resources_register_include!("src.icons.gresource").expect("Failed to register resources.");
gio::resources_register_include!("src.style.gresource").expect("Failed to register resources.");
// Create a new application
let app = Application::builder()
.application_id(APP_ID)
.build();
let app = Application::builder().application_id(APP_ID).build();
app.connect_startup(move |_| {
adw::init().unwrap();
load_css();
});
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.connect_shutdown(shutdown);
app.run();
}
fn load_css() {
let provider = CssProvider::new();
provider.load_from_resource("/org/Xetibo/ReSet/style/style.css");
gtk::style_context_add_provider_for_display(
&Display::default().expect("Could not connect to a display."),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
fn build_ui(app: &Application) {
// Create new window and present it
let window = Window::new(app);
let window = ReSetWindow::new(app);
window.present();
}
}
fn shutdown(_: &Application) {
thread::spawn(|| {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(100),
);
let res: Result<(), Error> =
proxy.method_call("org.Xetibo.ReSetDaemon", "UnregisterClient", ("ReSet",));
res
});
}
async fn daemon_check() {
let handle = thread::spawn(|| {
let conn = Connection::new_session().unwrap();
let proxy = conn.with_proxy(
"org.Xetibo.ReSetDaemon",
"/org/Xetibo/ReSetDaemon",
Duration::from_millis(100),
);
let res: Result<(), Error> =
proxy.method_call("org.Xetibo.ReSetDaemon", "RegisterClient", ("ReSet",));
res
});
let res = handle.join();
if res.unwrap().is_err() {
println!("Daemon was not running");
run_daemon().await;
} else {
println!("Daemon was running");
}
}

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#502100"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-settings"
version="1.1"
id="svg1"
sodipodi:docname="ReSet1.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="38.541667"
inkscape:cx="12"
inkscape:cy="11.987027"
inkscape:window-width="3440"
inkscape:window-height="1440"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<ellipse
cx="-12"
cy="-12.000001"
id="circle1"
transform="scale(-1)"
rx="2.25"
ry="2.2500007"
style="stroke-width:1.3;stroke-dasharray:none" />
<path
d="m 16.933331,14.000005 a 1.1000004,1.1000012 0 0 0 0.220001,1.213335 l 0.04,0.04 a 1.3333338,1.3333349 0 0 1 0,1.886671 1.3333338,1.3333349 0 0 1 -1.886668,0 l -0.04,-0.04 a 1.1000004,1.1000012 0 0 0 -1.213335,-0.219999 1.1000004,1.1000012 0 0 0 -0.666667,1.006667 v 0.113333 a 1.3333338,1.3333349 0 0 1 -1.333332,1.333326 1.3333338,1.3333349 0 0 1 -1.333334,-1.333336 v -0.06 a 1.1000004,1.1000012 0 0 0 -0.7200005,-1.006666 1.1000004,1.1000012 0 0 0 -1.213334,0.22 l -0.04,0.04 a 1.3333338,1.3333349 0 0 1 -1.8866673,0 1.3333338,1.3333349 0 0 1 0,-1.88667 l 0.04,-0.04001 A 1.1000004,1.1000012 0 0 0 7.1199943,14.053327 1.1000004,1.1000012 0 0 0 6.1133272,13.38666 H 5.9999939 A 1.3333338,1.3333349 0 0 1 4.66666,12.053336 1.3333338,1.3333349 0 0 1 5.9999939,10.720002 h 0.06 a 1.1000004,1.1000012 0 0 0 1.0066674,-0.7200024 1.1000004,1.1000012 0 0 0 -0.22,-1.2133349 l -0.04,-0.04 a 1.3333338,1.3333349 0 0 1 0,-1.8866689 1.3333338,1.3333349 0 0 1 1.8866674,0 l 0.04,0.040001 a 1.1000004,1.1000012 0 0 0 1.2133328,0.2199993 h 0.05333 A 1.1000004,1.1000012 0 0 0 10.666663,6.1133283 V 5.9999948 A 1.3333338,1.3333349 0 0 1 11.999996,4.66666 1.3333338,1.3333349 0 0 1 13.33333,5.9999948 v 0.06 a 1.1000004,1.1000012 0 0 0 0.666668,1.0066678 1.1000004,1.1000012 0 0 0 1.213333,-0.2200002 l 0.04,-0.04 a 1.3333338,1.3333349 0 0 1 1.886668,0 1.3333338,1.3333349 0 0 1 0,1.8866688 l -0.04,0.04 a 1.1000004,1.1000012 0 0 0 -0.220003,1.2133354 v 0.05333 a 1.1000004,1.1000012 0 0 0 1.006669,0.6666684 h 0.113334 A 1.3333338,1.3333349 0 0 1 19.333333,12 1.3333338,1.3333349 0 0 1 17.999999,13.333335 h -0.06 a 1.1000004,1.1000012 0 0 0 -1.006673,0.666667 z"
id="path1"
style="stroke-width:1.3;stroke-dasharray:none" />
<ellipse
style="fill:none;fill-opacity:1;stroke-width:1.3;stroke-dasharray:none"
id="path2"
cx="12.113708"
cy="12.113707"
rx="10.863708"
ry="10.863707" />
<rect
style="fill:none;fill-opacity:1;stroke-width:1.23083;stroke-dasharray:none"
id="rect2"
width="4.2691722"
height="0.06917306"
x="4.0028996"
y="4.8591166" />
<rect
style="fill:none;fill-opacity:1;stroke-width:1.23083;stroke-dasharray:none"
id="rect2-5"
width="0.069173172"
height="4.2691717"
x="3.849386"
y="0.64830494" />
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/Xetibo/ReSet/icons/scalable/actions">
<file preprocess="xml-stripblanks">ReSet.svg</file>
</gresource>
</gresources>

View file

@ -0,0 +1,207 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.4"/>
<template class="resetAudioInput" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="valign">start</property>
<child>
<object class="GtkLabel">
<property name="css-classes">resetSettingLabel</property>
<property name="halign">start</property>
<property name="label">Input</property>
<property name="margin-start">5</property>
</object>
</child>
<child>
<object class="AdwNavigationView">
<child>
<object class="AdwNavigationPage">
<property name="tag">output</property>
<property name="title">output</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow" id="reset_source_row">
<property name="title">Input Devices</property>
<child>
<object class="GtkImage">
<property name="icon-name">go-previous-symbolic-rtl</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow" id="reset_cards_row">
<property name="title">Profile Settings</property>
<child>
<object class="GtkImage">
<property name="icon-name">go-previous-symbolic-rtl</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwComboRow" id="reset_source_dropdown">
<property name="title">Default Input Device</property>
<property name="use-subtitle">True</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="margin-bottom">10</property>
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<property name="margin-top">5</property>
<child>
<object class="GtkButton" id="reset_source_mute">
<property name="icon-name">audio-input-microphone-symbolic</property>
<property name="margin-end">5</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkScale" id="reset_volume_slider">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="page-increment">2005.4016</property>
<property name="step-increment">2005.4016</property>
<property name="upper">100270.08</property>
</object>
</property>
<property name="hexpand">True</property>
<property name="valign">center</property>
<property name="value-pos">bottom</property>
<marks>
<mark position="bottom" value="65536.0">100%</mark>
</marks>
</object>
</child>
<child>
<object class="GtkLabel" id="reset_volume_percentage">
<property name="label">100%</property>
<property name="lines">1</property>
<property name="width-request">40</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkProgressBar" id="reset_volume_meter">
<property name="margin-top">5</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSeparator"/>
</child>
<child>
<object class="GtkBox" id="reset_output_streams">
<property name="margin-top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label">Input Streams</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwNavigationPage">
<property name="tag">sources</property>
<property name="title">sources</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow" id="reset_output_stream_button">
<property name="title">Input Streams</property>
<child>
<object class="GtkImage">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="icon-name">go-previous-symbolic</property>
<property name="margin-end">5</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="reset_sources">
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label">Input Devices</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwNavigationPage">
<property name="tag">profileConfiguration</property>
<property name="title">profileConfiguration</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow" id="reset_input_cards_back_button">
<property name="title">Input Streams</property>
<child>
<object class="GtkImage">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="icon-name">go-previous-symbolic</property>
<property name="margin-end">5</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel"/>
</child>
<child>
<object class="AdwPreferencesGroup" id="reset_cards">
<property name="title">Devices</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,210 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.4"/>
<template class="resetAudioOutput" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="valign">start</property>
<child>
<object class="GtkLabel">
<property name="css-classes">resetSettingLabel</property>
<property name="halign">start</property>
<property name="label">Output</property>
<property name="margin-start">5</property>
</object>
</child>
<child>
<object class="AdwNavigationView">
<child>
<object class="AdwNavigationPage">
<property name="tag">output</property>
<property name="title">output</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow" id="reset_sinks_row">
<property name="title">Output Devices</property>
<child>
<object class="GtkImage">
<property name="icon-name">go-previous-symbolic-rtl</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow" id="reset_cards_row">
<property name="title">Profile Settings</property>
<child>
<object class="GtkImage">
<property name="icon-name">go-previous-symbolic-rtl</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwComboRow" id="reset_sink_dropdown">
<property name="title">Default Output Device</property>
<property name="use-subtitle">True</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="margin-bottom">10</property>
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<property name="margin-top">5</property>
<child>
<object class="GtkButton" id="reset_sink_mute">
<property name="icon-name">audio-volume-high-symbolic</property>
<property name="margin-end">5</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkScale" id="reset_volume_slider">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="page-increment">2005.4016</property>
<property name="step-increment">2005.4016</property>
<property name="upper">100270.08</property>
</object>
</property>
<property name="hexpand">True</property>
<property name="valign">center</property>
<property name="value-pos">bottom</property>
<marks>
<mark position="bottom" value="65536.0">100%</mark>
</marks>
</object>
</child>
<child>
<object class="GtkLabel" id="reset_volume_percentage">
<property name="label">100%</property>
<property name="lines">1</property>
<property name="width-request">40</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkProgressBar" id="reset_volume_meter">
<property name="margin-top">5</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkSeparator"/>
</child>
<child>
<object class="GtkBox" id="reset_input_streams">
<property name="margin-top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label">Output Streams</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwNavigationPage">
<property name="tag">outputDevices</property>
<property name="title">outputDevices</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow" id="reset_input_stream_button">
<property name="title">Output Streams</property>
<child>
<object class="GtkImage">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="icon-name">go-previous-symbolic</property>
<property name="margin-end">5</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel"/>
</child>
<child>
<object class="GtkBox" id="reset_sinks">
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label">Output Devices</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwNavigationPage">
<property name="tag">profileConfiguration</property>
<property name="title">profileConfiguration</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow" id="reset_input_cards_back_button">
<property name="title">Output Streams</property>
<child>
<object class="GtkImage">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="icon-name">go-previous-symbolic</property>
<property name="margin-end">5</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel"/>
</child>
<child>
<object class="AdwPreferencesGroup" id="reset_cards">
<property name="title">Devices</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,62 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<template class="resetAudioSourceEntry" parent="GtkBox">
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<child>
<object class="GtkImage" id="resetSourceIcon">
<property name="icon-name">audio-volume-high-symbolic</property>
<property name="margin-end">5</property>
</object>
</child>
<child>
<object class="GtkLabel" id="resetSourceName">
<property name="label">Master Volume</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<property name="margin-top">5</property>
<child>
<object class="GtkButton" id="resetSourceMute">
<property name="icon-name">audio-volume-high-symbolic</property>
<property name="margin-end">5</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkScale" id="resetVolumeSlider">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="page-increment">5.0</property>
<property name="upper">100.0</property>
</object>
</property>
<property name="hexpand">True</property>
<property name="valign">center</property>
<property name="value-pos">bottom</property>
</object>
</child>
<child>
<object class="GtkLabel" id="resetVolumePercentage">
<property name="label">100%</property>
<property name="lines">1</property>
<property name="width-request">40</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkProgressBar" id="resetVolumeMeter"/>
</child>
</template>
</interface>

View file

@ -0,0 +1,177 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.4"/>
<template class="resetBluetooth" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="valign">start</property>
<child>
<object class="GtkBox">
<property name="height-request">40</property>
<child>
<object class="GtkLabel">
<property name="css-classes">resetSettingLabel</property>
<property name="label">Bluetooth</property>
<property name="margin-start">5</property>
</object>
</child>
<child>
<object class="GtkSwitch" id="reset_bluetooth_switch">
<property name="active">True</property>
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="margin-end">5</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwNavigationView">
<child>
<object class="AdwNavigationPage">
<property name="tag">main</property>
<property name="title">main</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup" id="reset_bluetooth_details">
<child>
<object class="AdwComboRow" id="reset_bluetooth_adapter">
<property name="title">Bluetooth Adapter</property>
</object>
</child>
<child>
<object class="AdwActionRow" id="reset_visibility">
<property name="title">Visibility Settings</property>
<child>
<object class="GtkImage">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="icon-name">go-previous-symbolic-rtl</property>
<property name="margin-end">5</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="reset_bluetooth_available_devices">
<property name="header-suffix">
<object class="GtkButton" id="reset_bluetooth_refresh_button">
<property name="icon-name">view-refresh-symbolic</property>
</object>
</property>
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">5</property>
<property name="title">Available Devices</property>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="reset_bluetooth_connected_devices">
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">5</property>
<property name="title">Connected Devices</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwNavigationPage">
<property name="tag">visibility</property>
<property name="title">visibility</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkListBox">
<property name="css-classes">boxed-list</property>
<property name="margin-bottom">10</property>
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">5</property>
<property name="show-separators">True</property>
<property name="valign">start</property>
<child>
<object class="resetListBoxRow" id="reset_bluetooth_main_tab">
<child>
<object class="GtkBox">
<property name="height-request">40</property>
<child>
<object class="GtkLabel">
<property name="label">Main</property>
<property name="margin-start">5</property>
</object>
</child>
<child>
<object class="GtkImage">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="icon-name">go-previous-symbolic</property>
<property name="margin-end">5</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label">Visibility Settings</property>
<property name="margin-start">5</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<property name="margin-start">5</property>
<property name="margin-top">5</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="hexpand-set">True</property>
<property name="label">Pairable</property>
</object>
</child>
<child>
<object class="GtkSwitch" id="reset_bluetooth_pairable_switch"/>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<property name="margin-start">5</property>
<property name="margin-top">5</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="hexpand-set">True</property>
<property name="label">Discoverable</property>
</object>
</child>
<child>
<object class="GtkSwitch" id="reset_bluetooth_discoverable_switch"/>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,9 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.0"/>
<template class="resetBluetoothEntry" parent="AdwActionRow">
<property name="margin-start">5</property>
</template>
</interface>

View file

@ -0,0 +1,8 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="libadwaita" version="1.4"/>
<object class="AdwComboRow" id="reset_card_entry">
<property name="use-subtitle">True</property>
</object>
</interface>

View file

@ -0,0 +1,20 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.0"/>
<template class="resetCards" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="valign">start</property>
<child>
<object class="GtkListBox" id="resetCardList">
<property name="css-classes">boxed-list</property>
<property name="margin-bottom">10</property>
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">5</property>
<property name="show-separators">True</property>
<property name="valign">start</property>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,74 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.4"/>
<template class="resetInputStreamEntry" parent="AdwPreferencesGroup">
<property name="margin-bottom">10</property>
<child>
<object class="GtkSeparator">
<property name="margin-top">10</property>
</object>
</child>
<child>
<object class="AdwComboRow" id="reset_sink_selection">
<property name="css-classes">audioRow</property>
<property name="title">asadf</property>
<property name="use-subtitle">True</property>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<property name="margin-top">5</property>
<child>
<object class="GtkButton" id="reset_sink_mute">
<property name="icon-name">audio-input-microphone-symbolic</property>
<property name="margin-end">5</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkScale" id="reset_volume_slider">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="page-increment">2005.4016</property>
<property name="step-increment">2005.4016</property>
<property name="upper">100270.08</property>
</object>
</property>
<property name="hexpand">True</property>
<property name="valign">center</property>
<property name="value-pos">bottom</property>
<marks>
<mark position="bottom" value="65536.0">100%</mark>
</marks>
</object>
</child>
<child>
<object class="GtkLabel" id="reset_volume_percentage">
<property name="label">100%</property>
<property name="lines">1</property>
<property name="width-request">40</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkProgressBar" id="reset_volume_meter">
<property name="margin-top">5</property>
</object>
</child>
</object>
</property>
<property name="css-classes">audioRow
</property>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,9 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.0"/>
<template class="resetListBoxRow" parent="GtkListBoxRow">
<property name="height-request">40</property>
<property name="selectable">False</property>
</template>
</interface>

View file

@ -4,398 +4,152 @@
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.4"/>
<template class="resetUI" parent="AdwApplicationWindow">
<property name="default-height">500</property>
<property name="default-width">951</property>
<property name="height-request">200</property>
<property name="width-request">540</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar">
<property name="centering-policy">strict</property>
<property name="title-widget">
<object class="AdwWindowTitle">
<property name="title">ReSet</property>
</object>
</property>
</object>
</child>
<child>
<object class="AdwNavigationSplitView">
<property name="content">
<object class="AdwNavigationPage">
<property name="child">
<object class="GtkFlowBox" id="resetMainWindow">
<object class="GtkWindowHandle"/>
</child>
<child>
<object class="AdwOverlaySplitView" id="reset_overlay_split_view">
<property name="content">
<object class="GtkBox">
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkWindowHandle">
<child>
<object class="GtkBox">
<property name="valign">start</property>
<child>
<object class="GtkBox" id="resetWifi_2">
<property name="orientation">vertical</property>
<child>
<object class="AdwClampScrollable">
<property name="orientation">vertical</property>
<child>
<object class="GtkButton" id="reset_sidebar_toggle">
<property name="icon-name">sidebar-show-symbolic</property>
<property name="visible">False</property>
</object>
</child>
<child>
<object class="GtkMenuButton">
<property name="can-shrink">True</property>
<property name="halign">end</property>
<property name="has-frame">False</property>
<property name="hexpand">True</property>
<property name="hexpand-set">True</property>
<property name="icon-name">open-menu-symbolic</property>
<property name="popover">
<object class="GtkPopoverMenu" id="reset_popover_menu">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="justify">right</property>
<property name="label">Available networks</property>
<object class="GtkButton" id="reset_shortcuts_button">
<property name="has-frame">False</property>
<property name="label">Shortcuts</property>
</object>
</child>
<child>
<object class="GtkListBox" id="resetWifiList">
<child>
<object class="GtkBox">
<child>
<object class="GtkImage" id="resetWifiStrength">
<property name="icon-name">network-wireless-symbolic</property>
<property name="margin-end">15</property>
</object>
</child>
<child>
<object class="GtkLabel" id="resetWifiLabel">
<property name="hexpand">True</property>
<property name="label">LoremIpsumInternet</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkButton" id="resetWifiButton">
<property name="halign">start</property>
<property name="icon-name">emblem-system-symbolic</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<child>
<object class="GtkImage" id="resetWifiStrength_2">
<property name="icon-name">network-wireless-symbolic</property>
<property name="margin-end">15</property>
</object>
</child>
<child>
<object class="GtkLabel" id="resetWifiLabel_2">
<property name="hexpand">True</property>
<property name="label">LoremIpsumInternet</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkButton" id="resetWifiButton_2">
<property name="halign">start</property>
<property name="icon-name">emblem-system-symbolic</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<child>
<object class="GtkImage" id="resetWifiStrength_3">
<property name="icon-name">network-wireless-symbolic</property>
<property name="margin-end">15</property>
</object>
</child>
<child>
<object class="GtkLabel" id="resetWifiLabel_3">
<property name="hexpand">True</property>
<property name="label">LoremIpsumInternet</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkButton" id="resetWifiButton_3">
<property name="halign">start</property>
<property name="icon-name">emblem-system-symbolic</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<child>
<object class="GtkImage" id="resetWifiStrength_4">
<property name="icon-name">network-wireless-symbolic</property>
<property name="margin-end">15</property>
</object>
</child>
<child>
<object class="GtkLabel" id="resetWifiLabel_4">
<property name="hexpand">True</property>
<property name="label">LoremIpsumInternet</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkButton" id="resetWifiButton_4">
<property name="halign">start</property>
<property name="icon-name">emblem-system-symbolic</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<child>
<object class="GtkImage" id="resetWifiStrength_5">
<property name="icon-name">network-wireless-symbolic</property>
<property name="margin-end">15</property>
</object>
</child>
<child>
<object class="GtkLabel" id="resetWifiLabel_5">
<property name="hexpand">True</property>
<property name="label">LoremIpsumInternet</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkButton" id="resetWifiButton_5">
<property name="halign">start</property>
<property name="icon-name">emblem-system-symbolic</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<child>
<object class="GtkImage" id="resetWifiStrength_6">
<property name="icon-name">network-wireless-symbolic</property>
<property name="margin-end">15</property>
</object>
</child>
<child>
<object class="GtkLabel" id="resetWifiLabel_6">
<property name="hexpand">True</property>
<property name="label">LoremIpsumInternet</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkButton" id="resetWifiButton_6">
<property name="halign">start</property>
<property name="icon-name">emblem-system-symbolic</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
<object class="GtkButton" id="reset_preference_button">
<property name="has-frame">False</property>
<property name="label">Preferences</property>
</object>
</child>
<child>
<object class="GtkButton" id="reset_about_button">
<property name="has-frame">False</property>
<property name="label">About</property>
</object>
</child>
</object>
</child>
</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="halign">start</property>
<property name="label">Advanced</property>
<property name="valign">start</property>
</object>
</child>
</property>
<property name="valign">end</property>
<property name="vexpand">True</property>
</object>
</child>
</object>
</property>
<property name="margin-bottom">20</property>
<property name="margin-end">20</property>
<property name="margin-start">20</property>
<property name="margin-top">20</property>
</object>
</property>
<property name="sidebar">
<object class="AdwNavigationPage">
<property name="child">
<object class="GtkViewport">
<property name="vscroll-policy">natural</property>
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="width-request">150</property>
<child>
<object class="GtkSearchEntry">
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="placeholder-text">Search</property>
</object>
</child>
<child>
<object class="GtkListBox">
<property name="css-name">resetList</property>
<property name="width-request">150</property>
<child>
<object class="GtkListBoxRow" id="resetConnectivity">
<child>
<object class="GtkBox">
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-top">10</property>
<child>
<object class="GtkImage">
<property name="icon-name">network-wired-symbolic</property>
<property name="margin-end">10</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Connectivity</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBoxRow" id="resetWifi">
<child>
<object class="GtkBox">
<property name="margin-bottom">10</property>
<property name="margin-start">30</property>
<property name="margin-top">10</property>
<child>
<object class="GtkImage">
<property name="icon-name">network-wireless-symbolic</property>
<property name="margin-end">10</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">WiFi</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBoxRow" id="resetBluetooth">
<child>
<object class="GtkBox">
<property name="margin-bottom">10</property>
<property name="margin-start">30</property>
<property name="margin-top">10</property>
<child>
<object class="GtkImage">
<property name="icon-name">bluetooth-symbolic</property>
<property name="margin-end">10</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Bluetooth</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBoxRow" id="resetVPN">
<child>
<object class="GtkBox">
<property name="margin-bottom">10</property>
<property name="margin-start">30</property>
<property name="margin-top">10</property>
<child>
<object class="GtkImage">
<property name="icon-name">network-vpn-symbolic</property>
<property name="margin-end">10</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">VPN</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBoxRow" id="resetAudio">
<child>
<object class="GtkBox">
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-top">10</property>
<child>
<object class="GtkImage">
<property name="icon-name">audio-headset-symbolic</property>
<property name="margin-end">10</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Audio</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBoxRow" id="resetVolume">
<child>
<object class="GtkBox">
<property name="margin-bottom">10</property>
<property name="margin-start">35</property>
<property name="margin-top">10</property>
<child>
<object class="GtkImage">
<property name="icon-name">audio-volume-high-symbolic</property>
<property name="margin-end">10</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Volume</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBoxRow" id="resetMicrophone">
<child>
<object class="GtkBox">
<property name="margin-bottom">10</property>
<property name="margin-start">30</property>
<property name="margin-top">10</property>
<child>
<object class="GtkImage">
<property name="icon-name">audio-input-microphone-symbolic</property>
<property name="margin-end">10</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label">Microphone</property>
</object>
</child>
</object>
</child>
</object>
</child>
<style>
<class name="b"/>
</style>
</object>
</child>
<object class="GtkButton" id="reset_close">
<property name="css-classes">resetClose</property>
<property name="halign">start</property>
<property name="has-frame">False</property>
<property name="icon-name">window-close-symbolic</property>
<property name="margin-start">5</property>
</object>
</child>
</object>
</property>
</child>
</object>
</property>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="propagate-natural-height">True</property>
<child>
<object class="GtkViewport">
<child>
<object class="GtkFlowBox" id="reset_main">
<property name="column-spacing">25</property>
<property name="hexpand">True</property>
<property name="hexpand-set">True</property>
<property name="margin-top">5</property>
<property name="row-spacing">25</property>
<property name="selection-mode">none</property>
<property name="valign">start</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</property>
<property name="max-sidebar-width">180.0</property>
<property name="sidebar">
<object class="GtkBox">
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkWindowHandle">
<property name="valign">start</property>
<child>
<object class="GtkSearchEntry" id="reset_search_entry">
<property name="placeholder-text">Search</property>
<property name="valign">start</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="margin-top">5</property>
<property name="propagate-natural-height">True</property>
<child>
<object class="GtkViewport">
<child>
<object class="GtkListBox" id="reset_sidebar_list">
<property name="css-name">resetList</property>
<property name="width-request">150</property>
<style>
<class name="b"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</property>
<!-- Custom fragments -->
</object>
</child>
<!-- Custom fragments -->
<child>
<object id="reset_sidebar_breakpoint" class="AdwBreakpoint">
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,73 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.4"/>
<template class="resetOutputStreamEntry" parent="AdwPreferencesGroup">
<child>
<object class="GtkSeparator">
<property name="margin-top">10</property>
</object>
</child>
<child>
<object class="AdwComboRow" id="reset_source_selection">
<property name="css-classes">audioRow</property>
<property name="title">asadf</property>
<property name="use-subtitle">True</property>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<property name="margin-top">5</property>
<child>
<object class="GtkButton" id="reset_source_mute">
<property name="icon-name">audio-input-microphone-symbolic</property>
<property name="margin-end">5</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkScale" id="reset_volume_slider">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="page-increment">2005.4016</property>
<property name="step-increment">2005.4016</property>
<property name="upper">100270.08</property>
</object>
</property>
<property name="hexpand">True</property>
<property name="valign">center</property>
<property name="value-pos">bottom</property>
<marks>
<mark position="bottom" value="65536.0">100%</mark>
</marks>
</object>
</child>
<child>
<object class="GtkLabel" id="reset_volume_percentage">
<property name="label">100%</property>
<property name="lines">1</property>
<property name="width-request">40</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkProgressBar" id="reset_volume_meter">
<property name="margin-top">5</property>
</object>
</child>
</object>
</property>
<property name="css-classes">audioRow
</property>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,42 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<template class="resetPopup" parent="GtkPopover">
<child>
<object class="GtkBox">
<property name="homogeneous">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="reset_popup_label">
<property name="visible">False</property>
</object>
</child>
<child>
<object class="GtkBox">
<child>
<object class="GtkPasswordEntry" id="reset_popup_entry">
<property name="activates-default">True</property>
<property name="margin-bottom">5</property>
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">5</property>
<property name="placeholder-text">Wifi Password</property>
<property name="show-peek-icon">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="reset_popup_button">
<property name="label">connect</property>
<property name="margin-bottom">5</property>
<property name="margin-end">5</property>
<property name="margin-start">5</property>
<property name="margin-top">5</property>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,40 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.0"/>
<template class="resetSavedWifiEntry" parent="AdwActionRow">
<property name="activatable">False</property>
<property name="child">
<object class="GtkBox">
<child>
<object class="GtkLabel" id="reset_saved_wifi_label">
<property name="ellipsize">end</property>
<property name="hexpand">True</property>
<property name="label">LoremIpsumInternet</property>
<property name="margin-end">10</property>
<property name="single-line-mode">True</property>
<property name="width-request">200</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkButton" id="reset_edit_saved_wifi_button">
<property name="halign">end</property>
<property name="has-frame">False</property>
<property name="hexpand">True</property>
<property name="icon-name">document-edit-symbolic</property>
</object>
</child>
<child>
<object class="GtkButton" id="reset_delete_saved_wifi_button">
<property name="halign">start</property>
<property name="has-frame">False</property>
<property name="icon-name">user-trash-symbolic</property>
<property name="valign">center</property>
</object>
</child>
</object>
</property>
</template>
</interface>

View file

@ -0,0 +1,10 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.0"/>
<template class="resetSettingBox" parent="GtkBox">
<property name="css-classes">settings-box</property>
<property name="orientation">vertical</property>
<property name="width-request">500</property>
</template>
</interface>

View file

@ -0,0 +1,25 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.0"/>
<template class="resetSidebarEntry" parent="GtkListBoxRow">
<property name="child">
<object class="GtkBox">
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-top">10</property>
<child>
<object class="GtkImage" id="reset_sidebar_image">
<property name="margin-end">10</property>
</object>
</child>
<child>
<object class="GtkLabel" id="reset_sidebar_label"/>
</child>
</object>
</property>
<property name="css-classes">resetSidebarEntry</property>
<property name="margin-bottom">2</property>
<property name="margin-top">2</property>
</template>
</interface>

View file

@ -0,0 +1,81 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.3"/>
<template class="resetSinkEntry" parent="AdwPreferencesGroup">
<child>
<object class="AdwActionRow" id="reset_sink_name">
<property name="css-classes">audioRow</property>
<property name="title">aaaaaaaaaaaaaaa</property>
<property name="title-lines">3</property>
<child>
<object class="GtkCheckButton" id="reset_selected_sink">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="hexpand-set">True</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<property name="margin-top">5</property>
<child>
<object class="GtkButton" id="reset_sink_mute">
<property name="icon-name">audio-input-microphone-symbolic</property>
<property name="margin-end">5</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkScale" id="reset_volume_slider">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="page-increment">2005.4016</property>
<property name="step-increment">2005.4016</property>
<property name="upper">100270.08</property>
</object>
</property>
<property name="hexpand">True</property>
<property name="valign">center</property>
<property name="value-pos">bottom</property>
<marks>
<mark position="bottom" value="65536.0">100%</mark>
</marks>
</object>
</child>
<child>
<object class="GtkLabel" id="reset_volume_percentage">
<property name="label">100%</property>
<property name="lines">1</property>
<property name="width-request">40</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkProgressBar" id="reset_volume_meter">
<property name="margin-top">5</property>
</object>
</child>
</object>
</property>
<property name="css-classes">audioRow
</property>
</object>
</child>
<child>
<object class="GtkSeparator">
<property name="margin-bottom">5</property>
<property name="margin-top">5</property>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,81 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.3"/>
<template class="resetSourceEntry" parent="AdwPreferencesGroup">
<child>
<object class="AdwActionRow" id="reset_source_name">
<property name="css-classes">audioRow</property>
<property name="title">text</property>
<property name="title-lines">3</property>
<child>
<object class="GtkCheckButton" id="reset_selected_source">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="hexpand-set">True</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwActionRow">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="margin-bottom">5</property>
<property name="margin-top">5</property>
<child>
<object class="GtkButton" id="reset_source_mute">
<property name="icon-name">audio-input-microphone-symbolic</property>
<property name="margin-end">5</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkScale" id="reset_volume_slider">
<property name="adjustment">
<object class="GtkAdjustment">
<property name="page-increment">2005.4016</property>
<property name="step-increment">2005.4016</property>
<property name="upper">100270.08</property>
</object>
</property>
<property name="hexpand">True</property>
<property name="valign">center</property>
<property name="value-pos">bottom</property>
<marks>
<mark position="bottom" value="65536.0">100%</mark>
</marks>
</object>
</child>
<child>
<object class="GtkLabel" id="reset_volume_percentage">
<property name="label">100%</property>
<property name="lines">1</property>
<property name="width-request">40</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkProgressBar" id="reset_volume_meter">
<property name="margin-top">5</property>
</object>
</child>
</object>
</property>
<property name="css-classes">audioRow
</property>
</object>
</child>
<child>
<object class="GtkSeparator">
<property name="margin-bottom">5</property>
<property name="margin-top">5</property>
</object>
</child>
</template>
</interface>

File diff suppressed because it is too large Load diff

View file

@ -1,61 +1,109 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<object class="GtkListBoxRow" id="resetWifiEntry">
<requires lib="gtk" version="4.6"/>
<requires lib="libadwaita" version="1.4"/>
<template class="resetWifi" parent="GtkBox">
<property name="hexpand">True</property>
<property name="hexpand-set">True</property>
<property name="orientation">vertical</property>
<property name="valign">start</property>
<child>
<object class="GtkBox">
<property name="height-request">40</property>
<child>
<object class="GtkImage" id="resetWifiStrength">
<property name="icon-name">network-wireless-symbolic</property>
<property name="margin-end">15</property>
<object class="GtkLabel">
<property name="css-classes">resetSettingLabel</property>
<property name="label">WiFi</property>
<property name="margin-start">5</property>
</object>
</child>
<child>
<object class="GtkLabel" id="resetWifiLabel">
<object class="GtkSwitch" id="reset_wifi_switch">
<property name="active">True</property>
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="label">LoremIpsumInternet</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkButton" id="resetWifiButton">
<property name="halign">start</property>
<property name="icon-name">emblem-system-symbolic</property>
<property name="margin-end">5</property>
<property name="valign">center</property>
</object>
</child>
</object>
</child>
</object>
<object class="GtkBox" id="resetWifi">
<property name="orientation">vertical</property>
<child>
<object class="AdwClampScrollable">
<property name="orientation">vertical</property>
<object class="AdwNavigationView" id="reset_wifi_navigation">
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<object class="AdwNavigationPage">
<property name="tag">main</property>
<property name="title">main</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="justify">right</property>
<property name="label">Available networks</property>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup" id="reset_wifi_details">
<child>
<object class="AdwComboRow" id="reset_wifi_device">
<property name="title">WiFi Device</property>
</object>
</child>
<child>
<object class="AdwActionRow" id="reset_saved_networks">
<property name="title">Saved Networks</property>
<child>
<object class="GtkImage">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="icon-name">go-previous-symbolic-rtl</property>
<property name="margin-end">5</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="reset_wifi_list">
<property name="margin-top">10</property>
<property name="title">Available networks</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwNavigationPage">
<property name="tag">saved</property>
<property name="title">saved</property>
<child>
<object class="GtkListBox" id="resetWifiList"/>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow" id="reset_available_networks">
<property name="title">Available Networks</property>
<child>
<object class="GtkImage">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="icon-name">go-previous-symbolic</property>
<property name="margin-end">5</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="reset_stored_wifi_list">
<property name="title">Saved Networks</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton">
<property name="halign">start</property>
<property name="label">Advanced</property>
<property name="valign">start</property>
</object>
</child>
</object>
<object class="AdwToast"/>
</template>
</interface>

View file

@ -0,0 +1,39 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.2"/>
<template class="resetWifiAddressEntry" parent="GtkBox">
<property name="margin-bottom">2</property>
<property name="margin-top">2</property>
<child>
<object class="AdwExpanderRow" id="reset_address_row">
<property name="hexpand">True</property>
<property name="title">Add new Address</property>
<child>
<object class="GtkBox">
<child>
<object class="AdwEntryRow" id="reset_address_address">
<property name="hexpand">True</property>
<property name="title">Address</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="reset_address_prefix">
<property name="max-width-chars">5</property>
<property name="title">Prefix</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="reset_address_remove">
<property name="height-request">55</property>
<property name="icon-name">edit-delete-symbolic</property>
<property name="valign">start</property>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,64 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.0"/>
<template class="resetWifiEntry" parent="AdwActionRow">
<property name="child">
<object class="GtkBox">
<property name="margin-start">5</property>
<child>
<object class="GtkBox">
<property name="width-request">35</property>
<child>
<object class="GtkImage" id="reset_wifi_strength">
<property name="icon-name">network-wireless-signal-excellent-symbolic</property>
</object>
</child>
<child>
<object class="GtkImage" id="reset_wifi_encrypted">
<property name="halign">start</property>
<property name="icon-name">system-lock-screen-symbolic</property>
<property name="margin-bottom">6</property>
<property name="pixel-size">9</property>
<property name="valign">end</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel" id="reset_wifi_label">
<property name="ellipsize">end</property>
<property name="hexpand">True</property>
<property name="label">LoremIpsumInternet</property>
<property name="margin-end">10</property>
<property name="single-line-mode">True</property>
<property name="width-request">200</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkLabel" id="reset_wifi_connected">
<property name="halign">end</property>
<property name="hexpand">True</property>
<property name="justify">right</property>
<property name="margin-end">10</property>
<property name="single-line-mode">True</property>
<property name="xalign">0.0</property>
</object>
</child>
<child>
<object class="GtkButton" id="reset_wifi_edit_button">
<property name="halign">start</property>
<property name="has-frame">False</property>
<property name="icon-name">document-edit-symbolic</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="resetPopup" id="reset_wifi_popup"/>
</child>
</object>
</property>
</template>
</interface>

View file

@ -0,0 +1,291 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.4"/>
<template class="resetWifiOptions" parent="AdwNavigationPage">
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkNotebook">
<property name="scrollable">True</property>
<property name="show-border">False</property>
<child>
<object class="GtkBox">
<property name="margin-top">5</property>
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwActionRow" id="reset_wifi_name">
<property name="css-classes">property</property>
<property name="title">WiFi Name</property>
</object>
</child>
<child>
<object class="AdwActionRow" id="reset_wifi_mac">
<property name="css-classes">property</property>
<property name="title">MAC-Address</property>
</object>
</child>
<child>
<object class="AdwActionRow" id="reset_wifi_link_speed">
<property name="css-classes">property</property>
<property name="title">Link Speed</property>
</object>
</child>
<child>
<object class="AdwActionRow" id="reset_wifi_ip4_addr">
<property name="css-classes">property</property>
<property name="title">IPv4 Address</property>
</object>
</child>
<child>
<object class="AdwActionRow" id="reset_wifi_ip6_addr">
<property name="css-classes">property</property>
<property name="title">IPv6 Address</property>
</object>
</child>
<child>
<object class="AdwActionRow" id="reset_wifi_gateway">
<property name="css-classes">property</property>
<property name="title">Gateway</property>
</object>
</child>
<child>
<object class="AdwActionRow" id="reset_wifi_dns">
<property name="css-classes">property</property>
<property name="title">DNS</property>
</object>
</child>
<child>
<object class="AdwActionRow" id="reset_wifi_last_used">
<property name="css-classes">property</property>
<property name="title">Last Used</property>
</object>
</child>
<child>
<object class="AdwSwitchRow" id="reset_wifi_auto_connect">
<property name="title">Connect automatically</property>
</object>
</child>
<child>
<object class="AdwSwitchRow" id="reset_wifi_metered">
<property name="title">Metered Connection</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="label">General</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="margin-top">5</property>
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwComboRow" id="reset_ip4_method">
<property name="model">
<object class="GtkStringList">
<items>
<item>Automatic (DHCP)</item>
<item>Manual</item>
<item>Link-Local Only</item>
<item>Shared to other computers</item>
<item>Disabled</item>
</items>
</object>
</property>
<property name="title">IPv4 Method</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="reset_ip4_dns">
<property name="title">DNS (separate IP by comma, empty for automatic)</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="reset_ip4_gateway">
<property name="title">Gateway</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="reset_ip4_address_group">
<property name="header-suffix">
<object class="GtkButton" id="reset_ip4_address_add_button">
<property name="icon-name">list-add-symbolic</property>
</object>
</property>
<property name="margin-top">10</property>
<property name="title">Addresses</property>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="reset_ip4_routes_group">
<property name="header-suffix">
<object class="GtkBox">
<child>
<object class="GtkLabel">
<property name="label">Automatic</property>
<property name="margin-end">5</property>
</object>
</child>
<child>
<object class="GtkSwitch">
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkButton" id="reset_ip4_route_add_button">
<property name="icon-name">list-add-symbolic</property>
<property name="margin-start">10</property>
</object>
</child>
</object>
</property>
<property name="margin-top">10</property>
<property name="title">Routes</property>
</object>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="label">IPv4</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="margin-top">5</property>
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwComboRow" id="reset_ip6_method">
<property name="model">
<object class="GtkStringList">
<items>
<item>Automatic</item>
<item>Automatic (DHCP)</item>
<item>Manual</item>
<item>Link-Local Only</item>
<item>Shared to other computers</item>
<item>Disabled</item>
</items>
</object>
</property>
<property name="title">IPv6 Method</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="reset_ip6_dns">
<property name="title">DNS (separate IP by comma, empty for automatic)</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="reset_ip6_gateway">
<property name="title">Gateway</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="reset_ip6_address_group">
<property name="header-suffix">
<object class="GtkButton" id="reset_ip6_address_add_button">
<property name="icon-name">list-add-symbolic</property>
</object>
</property>
<property name="margin-top">10</property>
<property name="title">Addresses</property>
</object>
</child>
<child>
<object class="AdwPreferencesGroup" id="reset_ip6_routes_group">
<property name="header-suffix">
<object class="GtkBox">
<child>
<object class="GtkLabel">
<property name="label">Automatic</property>
<property name="margin-end">5</property>
</object>
</child>
<child>
<object class="GtkSwitch">
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkButton" id="reset_ip6_route_add_button">
<property name="icon-name">list-add-symbolic</property>
<property name="margin-start">10</property>
</object>
</child>
</object>
</property>
<property name="margin-top">10</property>
<property name="title">Routes</property>
</object>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="label">IPv6</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="margin-top">5</property>
<property name="orientation">vertical</property>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="AdwComboRow" id="reset_wifi_security_dropdown">
<property name="model">
<object class="GtkStringList">
<items>
<item>None</item>
<item>WPA &amp; WPA2 Personal</item>
</items>
</object>
</property>
<property name="title">Security</property>
</object>
</child>
<child>
<object class="AdwPasswordEntryRow" id="reset_wifi_password">
<property name="title">Password</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="label">Security</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="wifi_options_apply_button">
<property name="halign">end</property>
<property name="label">Apply</property>
<property name="margin-top">10</property>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -0,0 +1,53 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.17.0 -->
<interface>
<requires lib="gtk" version="4.12"/>
<requires lib="libadwaita" version="1.2"/>
<template class="resetWifiRouteEntry" parent="GtkBox">
<child>
<object class="AdwExpanderRow" id="reset_route_row">
<property name="hexpand">True</property>
<property name="title">Add new Route</property>
<child>
<object class="GtkBox">
<child>
<object class="AdwEntryRow" id="reset_route_address">
<property name="hexpand">True</property>
<property name="title">Address</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="reset_route_prefix">
<property name="max-width-chars">5</property>
<property name="title">Prefix</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<child>
<object class="AdwEntryRow" id="reset_route_gateway">
<property name="hexpand">True</property>
<property name="title">Gateway</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="reset_route_metric">
<property name="max-width-chars">5</property>
<property name="title">Metric</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="reset_route_remove">
<property name="height-request">55</property>
<property name="icon-name">edit-delete-symbolic</property>
<property name="valign">start</property>
</object>
</child>
</template>
</interface>

View file

@ -1,7 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/xetibo/reset/">
<file compressed="true" preprocess="xml-stripblanks">resetMainWindow.ui</file>
<!-- <file compressed="true" preprocess="xml-stripblanks">resetWiFi.ui</file>-->
</gresource>
<gresource prefix="/org/Xetibo/ReSet/">
<!--Main window-->
<file compressed="true" preprocess="xml-stripblanks">resetMainWindow.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetSidebarEntry.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetSettingBox.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetListBoxRow.ui</file>
<!--WiFi-->
<file compressed="true" preprocess="xml-stripblanks">resetWiFi.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetWifiEntry.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetSavedWifiEntry.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetWifiOptions.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetWifiAddressEntry.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetWifiRouteEntry.ui</file>
<!--Bluetooth-->
<file compressed="true" preprocess="xml-stripblanks">resetBluetooth.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetBluetoothEntry.ui</file>
<!--Output-->
<file compressed="true" preprocess="xml-stripblanks">resetAudioOutput.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetOutputStreamEntry.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetSinkEntry.ui</file>
<!--Input-->
<file compressed="true" preprocess="xml-stripblanks">resetAudioInput.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetInputStreamEntry.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetSourceEntry.ui</file>
<!--Misc-->
<file compressed="true" preprocess="xml-stripblanks">resetPopup.ui</file>
<file compressed="true" preprocess="xml-stripblanks">resetCardEntry.ui</file>
</gresource>
</gresources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/Xetibo/ReSet/style">
<file compressed="true">style.css</file>
</gresource>
</gresources>

View file

@ -0,0 +1,27 @@
button.resetClose {
border-radius: 25px;
}
row.resetSidebarEntry {
border-radius: 5px;
}
label.resetSettingLabel {
font-size: 32px;
font-weight: bold;
}
frame.resetSettingFrame {
border-radius: 5px;
padding: 10px;
}
label.resetSettingLabel {
font-size: 32px;
font-weight: bold;
}
row.audioRow {
border-color: transparent;
}

View file

@ -1,37 +0,0 @@
use adw::subclass::prelude::AdwApplicationWindowImpl;
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{glib, CompositeTemplate};
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/xetibo/reset/resetMainWindow.ui")]
pub struct Window {
// todo i guess
}
#[glib::object_subclass]
impl ObjectSubclass for Window {
const NAME: &'static str = "resetUI";
type Type = super::Window;
type ParentType = adw::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for Window {}
impl WidgetImpl for Window {}
impl WindowImpl for Window {}
impl ApplicationWindowImpl for Window {}
impl AdwApplicationWindowImpl for Window {}

View file

@ -1,20 +0,0 @@
mod imp;
use glib::Object;
use gtk::{gio, glib, Application};
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder()
.property("application", app)
.build()
}
}