Compare commits

...

3 Commits
main ... dev

Author SHA1 Message Date
4bc439feaf dev-2025-08-18T13:06:13+02:00 2025-08-18 13:06:13 +02:00
c9b07c92dc dev-2025-08-17T21:06:39+02:00 2025-08-17 21:06:39 +02:00
2fefc6ec65 dev-2025-08-16T20:05:59+02:00 2025-08-16 20:05:59 +02:00
7 changed files with 226 additions and 23 deletions

View File

@ -6,6 +6,7 @@ edition = "2021"
[dependencies] [dependencies]
gettext-rs = { version = "0.7", features = ["gettext-system"] } gettext-rs = { version = "0.7", features = ["gettext-system"] }
gtk = { version = "0.9", package = "gtk4", features = ["gnome_47"] } gtk = { version = "0.9", package = "gtk4", features = ["gnome_47"] }
pipewire = "0.8.0"
[dependencies.adw] [dependencies.adw]
package = "libadwaita" package = "libadwaita"

View File

@ -1,10 +1,11 @@
{ {
"id" : "de.AdaLouBaumann.AudioDeviceManager", "id" : "de.AdaLouBaumann.AudioDeviceManager",
"runtime" : "org.gnome.Platform", "runtime" : "org.gnome.Platform",
"runtime-version" : "master", "runtime-version" : "22.08",
"sdk" : "org.gnome.Sdk", "sdk" : "org.gnome.Sdk",
"sdk-extensions" : [ "sdk-extensions" : [
"org.freedesktop.Sdk.Extension.rust-stable" "org.freedesktop.Sdk.Extension.rust-stable",
"org.freedesktop.Sdk.Extension.llvm14"
], ],
"command" : "audio-device-manager", "command" : "audio-device-manager",
"finish-args" : [ "finish-args" : [
@ -21,7 +22,8 @@
], ],
"env" : { "env" : {
"RUST_BACKTRACE" : "1", "RUST_BACKTRACE" : "1",
"RUST_LOG" : "audio-device-manager=debug" "RUST_LOG" : "audio-device-manager=debug",
"INCLUDE" : "/lib/clang/14.0.6/include"
} }
}, },
"cleanup" : [ "cleanup" : [
@ -44,7 +46,7 @@
{ {
"type" : "git", "type" : "git",
"url" : "https://gitea.ada-baumann.de/ada/AudioDeviceManager", "url" : "https://gitea.ada-baumann.de/ada/AudioDeviceManager",
"branch" : "main" "branch" : "dev"
} }
] ]
} }

12
src/components.rs Normal file
View File

@ -0,0 +1,12 @@
use adw::gdk::pango;
use gtk::ListBoxRow;
pub(crate) fn new_device(name: String) -> ListBoxRow {
let device_label = gtk::Label::builder()
.ellipsize(pango::EllipsizeMode::End)
.xalign(0.0)
.label(&name)
.build();
ListBoxRow::builder().child(&device_label).build()
}

View File

@ -22,6 +22,8 @@
mod application; mod application;
mod config; mod config;
mod window; mod window;
mod components;
mod pipewire;
use self::application::AudioDeviceManagerApplication; use self::application::AudioDeviceManagerApplication;
use self::window::AudioDeviceManagerWindow; use self::window::AudioDeviceManagerWindow;

57
src/pipewire.rs Normal file
View File

@ -0,0 +1,57 @@
use std::thread;
use std::thread::JoinHandle;
use gtk::glib::ExitCode;
use pipewire::{context::Context, main_loop::MainLoop};
use pipewire::types::ObjectType;
pub fn spawn_pipewire_thread() -> JoinHandle<ExitCode> {
let pw_thread = thread::spawn(|| {
// Initialize PipeWire and run the main loop
// ...
let mainloop = MainLoop::new(None).expect("failed to get mail loop");
let context = Context::new(&mainloop).expect("failed to get context");
let core = context.connect(None).expect("failed to get core");
let registry = core.get_registry().expect("failed to get registry");
let _listener = registry
.add_listener_local()
.global(|global|
{
if global.type_ == ObjectType::Port {
let props = global.props.as_ref().unwrap();
let port_name = props.get("port.name");
let port_alias = props.get("port.alias");
let object_path = props.get("object.path");
let format_dsp = props.get("format.dsp");
let audio_channel = props.get("audio.channel");
let port_id = props.get("port.id");
let port_direction = props.get("port.direction");
println!("Port: Name: {:?} Alias: {:?} Id: {:?} Direction: {:?} AudioChannel: {:?} Object Path: {:?} FormatDsp: {:?}",
port_name,
port_alias,
port_id,port_direction,audio_channel,object_path,format_dsp
);
} else if global.type_ == ObjectType::Device {
let props = global.props.as_ref().unwrap();
let device_name = props.get("device.name");
let device_nick = props.get("device.nick");
let device_description = props.get("device.description");
let device_api = props.get("device.api");
let media_class = props.get("media.class");
println!("Device: Name: {:?} Nick: {:?} Desc: {:?} Api: {:?} MediaClass: {:?}",
device_name, device_nick, device_description, device_api, media_class);
}
}
)
.register();
// Calling the `destroy_global` method on the registry will destroy the object with the specified id on the remote.
// We don't have a specific object to destroy now, so this is commented out.
// registry.destroy_global(313).into_result()?;
mainloop.run();
return ExitCode::FAILURE; // not sure
});
return pw_thread;
}

View File

@ -6,15 +6,24 @@ template $AudioDeviceManagerWindow: Adw.ApplicationWindow {
default-width: 800; default-width: 800;
default-height: 600; default-height: 600;
content: Adw.NavigationSplitView { Adw.Breakpoint {
condition ("max-width: 500sp")
setters {
split_view.collapsed: true;
}
}
content: Adw.NavigationSplitView split_view {
min-sidebar-width: 200;
sidebar: Adw.NavigationPage { sidebar: Adw.NavigationPage {
title: _("Sidebar"); title: _("Devices");
tag: "sidebar"; tag: "devices";
child: Adw.ToolbarView { child: Adw.ToolbarView {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
show-title: false; show-title: true;
[start] [start]
Gtk.ToggleButton { Gtk.ToggleButton {
icon-name: "list-add-symbolic"; icon-name: "list-add-symbolic";
@ -33,14 +42,14 @@ template $AudioDeviceManagerWindow: Adw.ApplicationWindow {
}; };
}; };
content: Adw.NavigationPage { content: Adw.NavigationPage device_navigation_page {
title: _("Content"); title: _("");
tag: "content"; tag: "device navigation page";
child: Adw.ToolbarView { child: Adw.ToolbarView {
[top] [top]
Adw.HeaderBar { Adw.HeaderBar {
show-title: false; show-title: true;
[end] [end]
MenuButton { MenuButton {
primary: true; primary: true;
@ -50,17 +59,19 @@ template $AudioDeviceManagerWindow: Adw.ApplicationWindow {
} }
} }
content: Adw.StatusPage { content: Gtk.ScrolledWindow {
title: _("Content"); child: Adw.Clamp device_page_clamp {
maximum-size: 640;
LinkButton { Box {
label: _("API Reference"); orientation: vertical;
uri: "https://ada-baumann.de"; spacing: 24;
} }
}; };
}; };
}; };
}; };
};
} }
menu primary_menu { menu primary_menu {

View File

@ -18,10 +18,13 @@
* *
* SPDX-License-Identifier: GPL-2.0-or-later * SPDX-License-Identifier: GPL-2.0-or-later
*/ */
use adw::glib::{clone, closure_local};
use adw::prelude::{AlertDialogExt, AlertDialogExtManual, NavigationPageExt};
use adw::ResponseAppearance;
use gtk::prelude::*; use gtk::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use gtk::{gio, glib}; use gtk::{gio, glib, Button, ListBoxRow};
use crate::components::new_device;
mod imp { mod imp {
use super::*; use super::*;
@ -32,6 +35,10 @@ mod imp {
// Template widgets // Template widgets
#[template_child] #[template_child]
pub devices_list: TemplateChild<gtk::ListBox>, pub devices_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub device_page_clamp: TemplateChild<adw::Clamp>,
#[template_child]
pub device_navigation_page: TemplateChild<adw::NavigationPage>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -42,6 +49,15 @@ mod imp {
fn class_init(klass: &mut Self::Class) { fn class_init(klass: &mut Self::Class) {
klass.bind_template(); klass.bind_template();
// Create async action to create new device and add to action group "win"
klass.install_action_async(
"win.new-device",
None,
|window, _, _| async move {
window.new_device().await;
}
);
} }
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) { fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
@ -58,13 +74,115 @@ mod imp {
glib::wrapper! { glib::wrapper! {
pub struct AudioDeviceManagerWindow(ObjectSubclass<imp::AudioDeviceManagerWindow>) pub struct AudioDeviceManagerWindow(ObjectSubclass<imp::AudioDeviceManagerWindow>)
@extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow, @implements gio::ActionGroup, gio::ActionMap; @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, adw::ApplicationWindow,
@implements gio::ActionGroup, gio::ActionMap;
} }
impl AudioDeviceManagerWindow { impl AudioDeviceManagerWindow {
pub fn new<P: IsA<gtk::Application>>(application: &P) -> Self { pub fn new<P: IsA<gtk::Application>>(application: &P) -> Self {
glib::Object::builder() let instance: AudioDeviceManagerWindow = glib::Object::builder()
.property("application", application) .property("application", application)
.build() .build();
instance.bind_signals();
instance
}
fn bind_signals(&self) {
self.imp().devices_list.connect_row_activated(clone!(
#[weak(rename_to = window)]
self,
move |_, row| {
println!("Row selected {}, {:?}", row.index(), row);
let label: gtk::Label = row
.child()
.and_downcast()
.expect("No Label in Row");
window.select_device(label.text().to_string());
}
));
}
async fn new_device(&self) {
let entry = gtk::Entry::builder()
.placeholder_text("Name")
.activates_default(true)
.build();
let cancel_response = "cancel";
let create_response = "create";
// Create new dialog
let dialog = adw::AlertDialog::builder()
.heading("New Device")
.close_response(cancel_response)
.default_response(create_response)
.extra_child(&entry)
.build();
dialog.add_responses(&[(cancel_response, "Cancel"), (create_response, "Create")]);
// Make the dialog button insensitive initially
dialog.set_response_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
// Set entry's css class to "error", when there is no text in it
entry.connect_changed(clone!(
#[weak]
dialog,
move |entry| {
let text = entry.text();
let empty = text.is_empty();
dialog.set_response_enabled(create_response, !empty);
if empty {
entry.add_css_class("error");
} else {
entry.remove_css_class("error");
}
}
));
let response = dialog.choose_future(self).await;
// Return if the user chose 'cancel_response'
if response == cancel_response {
println!("Cancel");
return;
}
let device = new_device(entry.text().to_string());
self.imp().devices_list.append(&device);
}
fn select_device(&self, name: String) {
self.imp().device_navigation_page.set_title(&name);
let device_page = self.build_device_page(name);
self.imp().device_page_clamp.set_child(Some(&device_page));
}
fn build_device_page(&self, name: String) -> gtk::Box {
let device_page = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.margin_start(12)
.margin_end(12)
.spacing(12)
.build();
let entry = gtk::Entry::builder()
.placeholder_text("Test")
.secondary_icon_name("list-add-symbolic")
.build();
device_page.append(&entry);
device_page
} }
} }