oooo it workin

This commit is contained in:
asonix 2021-05-04 20:17:55 -05:00
commit c16ed01fbe
32 changed files with 1687 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.flatpak-builder/
build/

6
build.sh Executable file
View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -xe
flatpak-builder build dog.asonix.git.asonix.owo.yml --user --install --force-clean
flatpak run dog.asonix.git.asonix.owo

33
data/gschema.xml Normal file
View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema path="/dog/asonix/git/asonix/owo/obs/" id="dog.asonix.git.asonix.owo.obs">
<key name="host" type="s">
<default>"localhost"</default>
<summary>OBS Host</summary>
<description>The hostname used to connect to OBS</description>
</key>
<key name="port" type="q">
<default>4444</default>
<summary>OBS Port</summary>
<description>The port used to connect to OBS</description>
</key>
</schema>
<schema path="/dog/asonix/git/asonix/owo/saved-state/" id="dog.asonix.git.asonix.owo.saved-state">
<key type="(ii)" name="window-position">
<default>(-1, -1)</default>
<summary>Window position</summary>
<description>Most recent window position (x, y)</description>
</key>
<key type="(ii)" name="window-size">
<default>(900, 600)</default>
<summary>Most recent window size</summary>
<description>Most recent window size (width, height)</description>
</key>
<key type="b" name="window-maximized">
<default>false</default>
<summary>Open window maximized.</summary>
<description>Whether the main window of the application should open maximized or not.</description>
</key>
</schema>
</schemalist>

6
data/meson.build Normal file
View file

@ -0,0 +1,6 @@
# Install our gschema.xml file so that we can write stateful information to GSettings
install_data (
'gschema.xml',
install_dir: join_paths (get_option ('datadir'), 'glib-2.0', 'schemas'),
rename: meson.project_name () + '.gschema.xml'
)

15
data/owo.appdata.xml.in Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>dog.asonix.git.asonix.owo</id>
<metadata_license>AGPL3</metadata_license>
<name>OwO</name>
<summary>OwO: The Experience</summary>
<description>
<p>Oh this is html isn't it, isn't it, isn't it html</p>
</description>
<custom>
<value key="x-appcenter-color-primary">#e0005c</value>
<value key="x-appcenter-color-primary-text">rgb(255, 255, 255)</value>
<value key="x-appcenter-suggested-price">0</value>
</custom>
</component>

9
data/owo.desktop.in Normal file
View file

@ -0,0 +1,9 @@
[Desktop Entry]
Name=OwO
GenericName=OwO UwU
Comment=Playing with GTK and elementary 6
Categories=Utility;Education;
Exec=dog.asonix.git.asonix.owo
Terminal=false
Type=Application
Keywords=Hello;World;Example;

View file

@ -0,0 +1,20 @@
app-id: dog.asonix.git.asonix.owo
runtime: io.elementary.Platform
runtime-version: 'daily'
sdk: io.elementary.Sdk
command: dog.asonix.git.asonix.owo
finish-args:
- '--share=ipc'
- '--socket=fallback-x11'
- '--socket=wayland'
- '--talk-name=dog.asonix.git.asonix.Streamdeck'
modules:
- name: owo
buildsystem: meson
sources:
- type: dir
path: .

64
meson.build Normal file
View file

@ -0,0 +1,64 @@
project('dog.asonix.git.asonix.owo', 'vala', 'c')
i18n = import('i18n')
add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format (meson.project_name()), language:'c')
app_files = files(
'src/Application.vala',
'src/Daemon.vala',
'src/MainWindow.vala',
'src/Data/Command.vala',
'src/Data/SwitchScene.vala',
'src/Dialogs/EditCommandDialog.vala',
'src/Dialogs/NewCommandDialog.vala',
'src/Views/ConfigCommand.vala',
'src/Views/DeckStack.vala',
'src/Views/DeckView.vala',
'src/Views/ObsView.vala',
'src/Widgets/CommandComboBox.vala',
'src/Widgets/CommandList.vala',
'src/Widgets/CommandRow.vala',
'src/Widgets/DeckList.vala',
'src/Widgets/DeckItem.vala',
'src/Widgets/DisconnectedPage.vala',
'src/Widgets/EmptyConfigPane.vala'
)
executable(
meson.project_name(),
app_files,
dependencies: [
dependency('gdk-3.0'),
dependency('gee-0.8'),
dependency('gio-2.0'),
dependency('granite'),
dependency('gtk+-3.0'),
dependency('json-glib-1.0'),
dependency('libhandy-1'),
dependency('pango')
],
install: true
)
i18n.merge_file(
input: join_paths('data', 'owo.desktop.in'),
output: meson.project_name() + '.desktop',
po_dir: join_paths(meson.source_root(), 'po'),
type: 'desktop',
install: true,
install_dir: join_paths(get_option('datadir'), 'applications')
)
i18n.merge_file(
input: join_paths('data', 'owo.appdata.xml.in'),
output: meson.project_name() + '.appdata.xml',
po_dir: join_paths(meson.source_root(), 'po'),
install: true,
install_dir: join_paths(get_option('datadir'), 'metainfo')
)
meson.add_install_script('meson/post_install.py')
subdir('po')
subdir('data')

10
meson/post_install.py Normal file
View file

@ -0,0 +1,10 @@
#!/usr/bin/env python3
import os
import subprocess
schemadir = os.path.join(os.environ['MESON_INSTALL_PREFIX'], 'share', 'glib-2.0', 'schemas')
if not os.environ.get('DESTDIR'):
print('Compiling gsettings schemas...')
subprocess.call(['glib-compile-schemas', schemadir])

1
po/LINGUAS Normal file
View file

@ -0,0 +1 @@
en

3
po/POTFILES Normal file
View file

@ -0,0 +1,3 @@
src/Application.vala
data/owo.desktop.in
data/owo.appdata.xml.in

View file

@ -0,0 +1,58 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the dog.asonix.git.asonix.owo package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: dog.asonix.git.asonix.owo\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-30 21:33-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: src/Application.vala:14
msgid "Label 1"
msgstr ""
#: src/Application.vala:15
msgid "Label 2"
msgstr ""
#: src/Application.vala:20
msgid "Hewwo owo"
msgstr ""
#: data/owo.desktop.in:3 data/owo.appdata.xml.in:5
msgid "OwO"
msgstr ""
#: data/owo.desktop.in:4
msgid "OwO UwU"
msgstr ""
#: data/owo.desktop.in:5
msgid "Playing with GTK and elementary 6"
msgstr ""
#: data/owo.desktop.in:8
msgid "dog.asonix.git.asonix.owo"
msgstr ""
#: data/owo.desktop.in:11
msgid "Hello;World;Example;"
msgstr ""
#: data/owo.appdata.xml.in:6
msgid "OwO: The Experience"
msgstr ""
#: data/owo.appdata.xml.in:8
msgid "Oh this is html isn't it, isn't it, isn't it html"
msgstr ""

58
po/en.po Normal file
View file

@ -0,0 +1,58 @@
# English translations for dog.asonix.git.asonix.owo package.
# Copyright (C) 2021 THE dog.asonix.git.asonix.owo'S COPYRIGHT HOLDER
# This file is distributed under the same license as the dog.asonix.git.asonix.owo package.
# Automatically generated, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: dog.asonix.git.asonix.owo\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-30 21:33-0500\n"
"PO-Revision-Date: 2021-04-30 21:32-0500\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=ASCII\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/Application.vala:14
msgid "Label 1"
msgstr "Label 1"
#: src/Application.vala:15
msgid "Label 2"
msgstr "Label 2"
#: src/Application.vala:20
msgid "Hewwo owo"
msgstr ""
#: data/owo.desktop.in:3 data/owo.appdata.xml.in:5
msgid "OwO"
msgstr "OwO"
#: data/owo.desktop.in:4
msgid "OwO UwU"
msgstr "OwO UwU"
#: data/owo.desktop.in:5
msgid "Playing with GTK and elementary 6"
msgstr "Playing with GTK and elementary 6"
#: data/owo.desktop.in:8
msgid "dog.asonix.git.asonix.owo"
msgstr "dog.asonix.git.asonix.owo"
#: data/owo.desktop.in:11
msgid "Hello;World;Example;"
msgstr "Hello;World;Example;"
#: data/owo.appdata.xml.in:6
msgid "OwO: The Experience"
msgstr "OwO: The Experience"
#: data/owo.appdata.xml.in:8
msgid "Oh this is html isn't it, isn't it, isn't it html"
msgstr "Oh this is html isn't it, isn't it, isn't it html"

4
po/meson.build Normal file
View file

@ -0,0 +1,4 @@
i18n.gettext(meson.project_name(),
args: '--directory=' + meson.source_root(),
preset: 'glib'
)

38
src/Application.vala Normal file
View file

@ -0,0 +1,38 @@
public class Streamdeck.App : Gtk.Application {
public static GLib.Settings obs_settings { get; private set; }
public static GLib.Settings saved_state { get; private set; }
public static MainWindow main_window { get; private set; }
static construct {
obs_settings = new GLib.Settings ("dog.asonix.git.asonix.owo.obs");
saved_state = new GLib.Settings ("dog.asonix.git.asonix.owo.saved-state");
}
construct {
flags = ApplicationFlags.FLAGS_NONE;
application_id = "dog.asonix.git.asonix.owo";
var present_action = new SimpleAction ("app.present", null);
present_action.activate.connect (() => {
if (main_window != null) {
main_window.present_with_time ((uint32) GLib.get_monotonic_time ());
}
});
add_action (present_action);
}
protected override void activate () {
if (main_window == null) {
main_window = new MainWindow (this);
main_window.build_ui ();
add_window (main_window);
}
main_window.present ();
}
public static int main (string[] args) {
return new Streamdeck.App ().run (args);
}
}

284
src/Daemon.vala Normal file
View file

@ -0,0 +1,284 @@
public struct Streamdeck.ReadInput {
public uint8 key;
public string serial_number;
}
public struct Streamdeck.DeckInfo {
public string serial_number;
public string device_name;
public string port_name;
}
public struct Streamdeck.CommandInfo {
public uint8 key;
public string command;
}
public class Streamdeck.Daemon : Object {
[DBus (name = "dog.asonix.git.asonix.Streamdeck")]
private interface StreamdeckBackend : Object {
public async abstract string[] get_scenes () throws GLib.Error;
public async abstract void enable_discovery () throws GLib.Error;
public async abstract void disable_discovery () throws GLib.Error;
public async abstract DeckInfo[] get_decks () throws GLib.Error;
public async abstract string connect (string host, uint16 port) throws GLib.Error;
public async abstract string disconnect () throws GLib.Error;
public async abstract string get_state () throws GLib.Error;
public async abstract string login (string password) throws GLib.Error;
public async abstract CommandInfo[] get_commands (string serial_number) throws GLib.Error;
public async abstract ReadInput[] read_input () throws GLib.Error;
public async abstract void set_input (string serial_number, uint8 key, string command) throws GLib.Error;
public async abstract void unset_input (string serial_number, uint8 key) throws GLib.Error;
}
private static Daemon? _instance;
public static Daemon instance {
get {
if (_instance == null) {
_instance = new Daemon ();
}
return _instance;
}
}
private StreamdeckBackend? backend_object;
private string? obs_state;
private DeckInfo[] decks;
private string[] scenes;
private Gee.HashMap<string, Gee.ArrayList<CommandInfo?>> commands = new Gee.HashMap<string, Gee.ArrayList<CommandInfo?>> ();
public signal void dbus_connection_signal ();
public signal void obs_state_signal (string state);
public signal void decks_signal (DeckInfo[] decks);
public signal void on_decks_added (DeckInfo[] decks);
public signal void on_decks_removed (DeckInfo[] decks);
public signal void scenes_signal (string[] scenes);
public signal void commands_signal (string serial_number, CommandInfo[] commands);
public signal void key_press_signal (ReadInput key_press);
construct {
dbus_connection_signal.connect ((_obj) => {
get_state.begin ((_obj, res) => {
try {
var state = get_state.end (res);
obs_state_signal (state);
} catch (Error e) {
print ("Get state error: %s\n", e.message);
}
});
load_decks.begin ((_obj, res) => {
try {
var new_decks = load_decks.end (res);
decks_signal (new_decks);
} catch (Error e) {
print ("Get decks error: %s\n", e.message);
}
});
});
obs_state_signal.connect ((_obj, state) => {
on_state_change.begin (state, (_obj, res) => {
try {
on_state_change.end (res);
} catch (Error e) {
print ("State handler error: %s\n", e.message);
}
});
});
decks_signal.connect ((_obj, deck_list) => {
var new_decks = diff_decks (decks, deck_list);
var removed_decks = diff_decks (deck_list, decks);
decks = deck_list;
on_decks_added (new_decks);
on_decks_removed (removed_decks);
foreach (DeckInfo deck_info in new_decks) {
update_command_cache (deck_info.serial_number);
}
});
scenes_signal.connect ((_obj, new_scenes) => {
scenes = new_scenes;
});
commands_signal.connect ((_obj, serial_number, new_commands) => {
var array_list = new Gee.ArrayList<CommandInfo?> ((a, b) => {
return a.key == b.key && a.command == b.command;
});
foreach (CommandInfo info in new_commands) {
array_list.add (info);
}
commands.set (serial_number, array_list);
});
Bus.get_proxy.begin<StreamdeckBackend> (
BusType.SESSION,
"dog.asonix.git.asonix.Streamdeck",
"/dog/asonix/git/asonix/Streamdeck",
0,
null,
(_obj, res) => {
try {
backend_object = Bus.get_proxy.end (res);
dbus_connection_signal ();
} catch (Error e) {
error ("Streamdeck error: %s", e.message);
}
}
);
}
public string get_obs_state () {
if (obs_state == null) {
return "Disconnected";
} else {
return obs_state;
}
}
public DeckInfo[] get_decks () {
return decks;
}
public string[] get_scenes () {
return scenes;
}
public Gee.ArrayList<CommandInfo?> get_commands (string serial_number) {
return commands.get (serial_number);
}
public async void add_command (string serial_number, uint8 key, string command) throws GLib.Error {
disconnected_err ();
yield backend_object.set_input (serial_number, key, command);
}
public async void remove_command (string serial_number, uint8 key) throws GLib.Error {
disconnected_err ();
yield backend_object.unset_input (serial_number, key);
}
public async void connect_obs () throws GLib.Error {
disconnected_err ();
string host;
uint16 port;
App.obs_settings.get ("host", "s", out host);
App.obs_settings.get ("port", "q", out port);
var state = yield backend_object.connect (host, port);
obs_state_signal (state);
}
public async void authenticate_obs () throws GLib.Error {
disconnected_err ();
// TODO: secure passw0rt storage
var passw0rt = "passw0rt";
var state = yield backend_object.login (passw0rt);
obs_state_signal (state);
}
public async void disconnect_obs () throws GLib.Error {
disconnected_err ();
var state = yield backend_object.disconnect ();
obs_state_signal (state);
}
public async void key_press () throws GLib.Error {
disconnected_err ();
var read_inputs = yield backend_object.read_input ();
foreach (ReadInput input in read_inputs) {
key_press_signal (input);
}
}
public void update_command_cache (string serial_number) {
load_commands.begin (serial_number, (_obj, res) => {
try {
var new_commands = load_commands.end (res);
commands_signal (serial_number, new_commands);
} catch (Error e) {
print ("Command fetch error: %s\n", e.message);
}
});
}
private async string[] load_scenes () throws GLib.Error {
disconnected_err ();
return yield backend_object.get_scenes ();
}
private async DeckInfo[] load_decks () throws GLib.Error {
disconnected_err ();
return yield backend_object.get_decks ();
}
private async CommandInfo[] load_commands (string serial_number) throws GLib.Error {
disconnected_err ();
return yield backend_object.get_commands (serial_number);
}
private async void on_state_change (string state) throws GLib.Error {
if (obs_state == state) {
return;
}
obs_state = state;
if (state == "Disconnected") {
yield connect_obs ();
} else if (state == "Unauthenticated") {
yield authenticate_obs ();
} else if (state == "Connected") {
var new_scenes = yield load_scenes ();
scenes_signal (new_scenes);
}
}
private async string get_state () throws GLib.Error {
disconnected_err ();
return yield backend_object.get_state ();
}
private void disconnected_err () throws GLib.Error {
if (backend_object == null) {
throw new GLib.IOError.FAILED ("Not connected to streamdeck daemon");
}
}
private DeckInfo[] diff_decks(DeckInfo[] lhs, DeckInfo[] rhs) {
DeckInfo[] new_decks = new DeckInfo[0];
var exists = false;
foreach (DeckInfo deck_info in rhs) {
exists = false;
foreach (DeckInfo existing_info in lhs) {
exists = exists
|| deck_info.serial_number == existing_info.serial_number;
}
if (!exists) {
new_decks += deck_info;
}
}
return new_decks;
}
}

71
src/Data/Command.vala Normal file
View file

@ -0,0 +1,71 @@
namespace Streamdeck.Data {
public enum CommandType {
SWITCH_SCENE,
}
public struct CommandDescription {
CommandType type;
string name;
string id;
}
public abstract class Command : GLib.Object {
private CommandType type;
private uint8 key;
protected Command (uint8 key, CommandType type) {
this.key = key;
this.type = type;
}
public static CommandDescription[] available() {
CommandDescription[] available = {
CommandDescription() {
type = CommandType.SWITCH_SCENE,
name = _("Switch Scene"),
id = "SwitchScene"
}
};
return available;
}
public static CommandType? type_string (string? type) {
switch (type) {
case "SwitchScene":
return CommandType.SWITCH_SCENE;
default:
return null;
}
}
public static Command? parse (uint8 key, string command) {
var parser = new Json.Parser ();
try {
parser.load_from_data (command);
var obj = parser.get_root ().get_object ();
switch (type_string(obj.get_string_member ("type"))) {
case CommandType.SWITCH_SCENE:
var scene_name = obj.get_string_member ("name");
return new SwitchScene (key, scene_name);
default:
return null;
}
} catch {
return null;
}
}
public abstract string to_json ();
public CommandType get_command_type () {
return type;
}
public uint8 get_command_key () {
return key;
}
}
}

26
src/Data/SwitchScene.vala Normal file
View file

@ -0,0 +1,26 @@
namespace Streamdeck.Data {
public class SwitchScene : Command {
public string scene_name;
public SwitchScene (uint8 key, string scene_name) {
base (key, CommandType.SWITCH_SCENE);
this.scene_name = scene_name;
}
public override string to_json () {
var gen = new Json.Generator ();
var root = new Json.Node (Json.NodeType.OBJECT);
var object = new Json.Object ();
root.set_object (object);
gen.set_root (root);
object.set_string_member ("type", "SwitchScene");
object.set_string_member ("name", scene_name);
size_t length;
string json = gen.to_data (out length);
return json;
}
}
}

View file

@ -0,0 +1,31 @@
namespace Streamdeck.Dialogs {
public class EditCommandDialog : Granite.Dialog {
private Gtk.Stack stack;
public EditCommandDialog (string serial_number, Data.Command initial_command) {
var disconnected_page = new Widgets.DisconnectedPage ();
var command_page = new Views.ConfigCommand.from_existing (serial_number, initial_command);
stack = new Gtk.Stack ();
stack.add_named (disconnected_page, "disconnected");
stack.add_named (command_page, "command");
get_content_area ().add (stack);
if (Daemon.instance.get_obs_state () != "Connected") {
add_button ("Close", Gtk.ResponseType.REJECT);
} else {
add_button ("Close", Gtk.ResponseType.CLOSE);
}
show_all ();
if (Daemon.instance.get_obs_state () != "Connected") {
stack.set_visible_child_name ("disconnected");
} else {
stack.set_visible_child_name ("command");
}
}
}
}

View file

@ -0,0 +1,61 @@
namespace Streamdeck.Dialogs {
public class NewCommandDialog : Granite.Dialog {
private string serial_number;
private Gtk.Stack stack;
private ReadInput key_info;
public NewCommandDialog (string serial_number) {
this.serial_number = serial_number;
var disconnected_page = new Widgets.DisconnectedPage ();
var press_label = new Gtk.Label (
_("Please press the button you wish to program on your stream deck")
);
press_label.valign = Gtk.Align.CENTER;
press_label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL);
var press_page = new Gtk.Grid ();
press_page.column_spacing = 12;
press_page.row_spacing = 12;
press_page.halign = Gtk.Align.CENTER;
press_page.margin = 24;
press_page.attach (press_label, 0, 0);
var command_page = new Views.ConfigCommand (serial_number);
stack = new Gtk.Stack ();
stack.add_named (disconnected_page, "disconnected");
stack.add_named (press_page, "keypress");
stack.add_named (command_page, "command");
get_content_area ().add (stack);
if (Daemon.instance.get_obs_state () != "Connected") {
add_button ("Close", Gtk.ResponseType.REJECT);
} else {
add_button ("Close", Gtk.ResponseType.CLOSE);
}
show_all ();
Daemon.instance.key_press_signal.connect ((_obj, key_info) => {
if (serial_number == key_info.serial_number) {
this.key_info = key_info;
command_page.key = key_info.key;
stack.set_visible_child_name ("command");
} else {
Daemon.instance.key_press.begin ();
}
});
if (Daemon.instance.get_obs_state () != "Connected") {
stack.set_visible_child_name ("disconnected");
} else {
stack.set_visible_child_name ("keypress");
Daemon.instance.key_press.begin ();
}
}
}
}

102
src/MainWindow.vala Normal file
View file

@ -0,0 +1,102 @@
public class Streamdeck.MainWindow : Hdy.ApplicationWindow {
public const string ACTION_PREFIX = "win.";
public const string ACTION_QUIT = "action_quit";
public Views.DeckStack deck_stack { get; private set; }
public Gtk.Stack main_stack { get; private set; }
private uint configure_id;
private const ActionEntry[] ACTION_ENTRIES = {
{ ACTION_QUIT, action_quit }
};
public MainWindow (Gtk.Application application) {
Object (application: application);
application.set_accels_for_action (
ACTION_PREFIX + ACTION_QUIT,
{"<Control>q", "<Control>w"}
);
}
construct {
Hdy.init ();
add_action_entries (ACTION_ENTRIES, this);
}
public void build_ui () {
height_request = 350;
width_request = 400;
title = _("Streamdeck");
int window_x, window_y, window_width, window_height;
App.saved_state.get ("window-position", "(ii)", out window_x, out window_y);
App.saved_state.get ("window-size", "(ii)", out window_width, out window_height);
set_default_size (window_width, window_height);
if (window_x != -1 || window_y != -1) {
move (window_x, window_y);
}
if (App.saved_state.get_boolean ("window-maximized")) {
maximize ();
}
main_stack = new Gtk.Stack ();
main_stack.add_titled (new Views.DeckView (), "deck-config", _("Streamdecks"));
main_stack.add_titled (new Views.ObsView (), "obs-config", _("OBS"));
var stack_switcher = new Gtk.StackSwitcher ();
stack_switcher.margin = 12;
stack_switcher.halign = Gtk.Align.CENTER;
stack_switcher.homogeneous = true;
stack_switcher.stack = main_stack;
var headerbar = new Hdy.HeaderBar ();
headerbar.show_close_button = true;
headerbar.set_title (_("Streamdeck"));
headerbar.show_all ();
var grid = new Gtk.Grid ();
grid.attach (headerbar, 0, 0);
grid.attach (stack_switcher, 0, 1);
grid.attach (main_stack, 0, 2);
grid.show_all ();
add (grid);
}
private void action_quit () {
destroy ();
}
public void to_obs () {
main_stack.set_visible_child_name ("obs-config");
}
public override bool configure_event (Gdk.EventConfigure event) {
if (configure_id == 0) {
configure_id = Timeout.add (200, () => {
configure_id = 0;
App.saved_state.set_boolean ("window-maximized", is_maximized);
if (!is_maximized) {
int width, height, root_x, root_y;
get_position (out root_x, out root_y);
get_size (out width, out height);
App.saved_state.set ("window-position", "(ii)", root_x, root_y);
App.saved_state.set ("window-size", "(ii)", width, height);
}
return GLib.Source.REMOVE;
});
}
return base.configure_event (event);
}
}

View file

@ -0,0 +1,128 @@
namespace Streamdeck.Views {
public class ConfigCommand : Gtk.Grid {
private Gtk.Stack command_stack;
private Gtk.ComboBoxText scenes_combobox;
private Widgets.CommandComboBox command_combobox;
private string serial_number;
public Data.Command? command;
public uint8? key;
public signal void changed (Data.Command command);
public ConfigCommand (string serial_number) {
this.serial_number = serial_number;
build ();
}
public ConfigCommand.from_existing (string serial_number, Data.Command command) {
this.serial_number = serial_number;
this.key = command.get_command_key ();
this.command = command;
build ();
switch (command.get_command_type ()) {
case Data.CommandType.SWITCH_SCENE:
var scene_name = ((Data.SwitchScene) command).scene_name;
scenes_combobox.set_active_id (scene_name);
command_combobox.set_active_id ("SwitchScene");
break;
default:
break;
}
}
private void build () {
var command_title = new Gtk.Label (
_("Configure the command you wish send")
);
command_title.valign = Gtk.Align.CENTER;
command_title.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL);
var command_label = new Gtk.Label (
_("Command:")
);
command_label.halign = Gtk.Align.START;
command_label.valign = Gtk.Align.CENTER;
command_combobox = new Widgets.CommandComboBox ();
var scenes_label = new Gtk.Label (_("to"));
scenes_combobox = new Gtk.ComboBoxText ();
scenes_combobox.id_column = 1;
foreach (var scene in Daemon.instance.get_scenes ()) {
scenes_combobox.append (scene, scene);
}
var switch_scene_grid = new Gtk.Grid ();
switch_scene_grid.attach (scenes_label, 0, 0);
switch_scene_grid.attach (scenes_combobox, 1, 0, 2);
switch_scene_grid.column_spacing = 12;
switch_scene_grid.row_spacing = 12;
switch_scene_grid.halign = Gtk.Align.CENTER;
command_stack = new Gtk.Stack ();
command_stack.add_named (new Gtk.Label ("..."), "Empty");
command_stack.add_named (switch_scene_grid, "SwitchScene");
column_spacing = 12;
row_spacing = 12;
halign = Gtk.Align.CENTER;
margin = 24;
attach (command_title, 0, 0, 3);
attach (command_label, 0, 1);
attach (command_combobox, 1, 1);
attach (command_stack, 2, 1);
show_all ();
scenes_combobox.changed.connect (() => {
var scene_name = scenes_combobox.get_active_text ();
if (command != null && command.get_command_type () == Data.CommandType.SWITCH_SCENE) {
unowned var switch_scene = (Data.SwitchScene) command;
switch_scene.scene_name = scene_name;
changed (command);
} else if (key != null) {
command = new Data.SwitchScene (key, scene_name);
changed (command);
}
});
changed.connect ((_obj, cmd) => {
Daemon.instance.add_command.begin (
serial_number,
cmd.get_command_key (),
cmd.to_json (),
(obj, res) => {
try {
Daemon.instance.add_command.end (res);
Daemon.instance.update_command_cache (serial_number);
} catch (Error e) {
print ("Error saving command %s\n", e.message);
}
}
);
});
command_combobox.selected.connect ((type) => {
if (key != null) {
handle_type_change (type);
}
});
}
private void handle_type_change (Data.CommandType type) {
switch (type) {
case Data.CommandType.SWITCH_SCENE:
command_stack.set_visible_child_name ("SwitchScene");
break;
default:
break;
}
}
}
}

31
src/Views/DeckStack.vala Normal file
View file

@ -0,0 +1,31 @@
namespace Streamdeck.Views {
public class DeckStack : Gtk.Stack {
construct {
add_named (new Widgets.EmptyConfigPane (), "empty-state");
show_all();
}
public void select_deck (string serial_number) {
if (get_child_by_name (serial_number) != null) {
set_visible_child_name (serial_number);
}
}
public void add_deck (DeckInfo deck_info) {
if (get_child_by_name (deck_info.serial_number) != null) {
return;
}
var pane = new Widgets.CommandList (deck_info);
add_named (pane, deck_info.serial_number);
}
public void remove_deck (DeckInfo deck_info) {
var child = get_child_by_name (deck_info.serial_number);
if (child != null) {
remove (child);
}
}
}
}

66
src/Views/DeckView.vala Normal file
View file

@ -0,0 +1,66 @@
namespace Streamdeck.Views {
public class DeckView : Gtk.Grid {
private DeckStack deck_stack;
construct {
column_homogeneous = true;
column_spacing = 12;
row_spacing = 12;
deck_stack = new Views.DeckStack ();
deck_stack.margin = 12;
deck_stack.margin_start = 0;
var deck_list = new Widgets.DeckList (deck_stack);
foreach (DeckInfo deck_info in Daemon.instance.get_decks()) {
deck_list.add_deck (deck_info);
}
if (!deck_list.any_selected ()) {
deck_list.select_first_item ();
}
deck_list.show_all ();
deck_stack.show_all ();
var scrolled_window = new Gtk.ScrolledWindow (null, null);
scrolled_window.add (deck_list);
scrolled_window.expand = true;
var sidebar = new Gtk.Grid ();
sidebar.orientation = Gtk.Orientation.VERTICAL;
sidebar.add(scrolled_window);
sidebar.margin = 12;
sidebar.margin_end = 0;
attach (sidebar, 0, 0);
attach (deck_stack, 1, 0, 2, 1);
Daemon.instance.on_decks_added.connect ((_obj, decks) => {
foreach (DeckInfo deck_info in decks) {
deck_list.add_deck (deck_info);
}
if (!deck_list.any_selected ()) {
deck_list.select_first_item ();
}
});
Daemon.instance.on_decks_removed.connect ((_obj, decks) => {
foreach (DeckInfo deck_info in decks) {
deck_list.remove_deck (deck_info);
}
if (!deck_list.any_selected ()) {
deck_list.select_first_item ();
}
deck_list.show_all ();
});
show_all ();
}
}
}

94
src/Views/ObsView.vala Normal file
View file

@ -0,0 +1,94 @@
namespace Streamdeck.Views {
public class ObsView : Gtk.Grid {
construct {
column_spacing = 12;
row_spacing = 12;
halign = Gtk.Align.CENTER;
margin = 24;
margin_top = 64;
var host_label = new Gtk.Label (_("Hostname:"));
var host_entry = new Gtk.Entry ();
host_entry.valign = Gtk.Align.CENTER;
host_entry.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL);
var host_buffer = host_entry.get_buffer ();
var port_label = new Gtk.Label (_("Port:"));
var port_entry = new Gtk.SpinButton.with_range (1, 65535, 1);
port_entry.valign = Gtk.Align.CENTER;
port_entry.snap_to_ticks = true;
var connect_button = new Gtk.Button ();
switch (Daemon.instance.get_obs_state ()) {
case "Disconnected":
connect_button.label = _("Connect");
break;
default:
connect_button.label = _("Reconnect");
break;
}
App.obs_settings.bind (
"host",
host_buffer,
"text",
GLib.SettingsBindFlags.DEFAULT
);
App.obs_settings.bind (
"port",
port_entry,
"value",
GLib.SettingsBindFlags.DEFAULT
);
connect_button.clicked.connect (() => {
connect_button.sensitive = false;
switch (Daemon.instance.get_obs_state ()) {
case "Disconnected":
Daemon.instance.connect_obs.begin ((_obj, res) => {
try {
Daemon.instance.connect_obs.end (res);
} catch (Error e) {
print ("Error connecting to obs: %s\n", e.message);
}
});
break;
default:
Daemon.instance.disconnect_obs.begin ((_obj, res) => {
try {
Daemon.instance.disconnect_obs.end (res);
} catch (Error e) {
print ("Error disconnecting from obs: %s\n", e.message);
}
});
break;
}
});
Daemon.instance.obs_state_signal.connect ((_obj, state) => {
if (!connect_button.sensitive) {
connect_button.sensitive = true;
}
switch (state) {
case "Disconnected":
connect_button.label = _("Connect");
break;
default:
connect_button.label = _("Reconnect");
break;
}
});
attach (host_label, 0, 0);
attach (host_entry, 1, 0);
attach (port_label, 0, 1);
attach (port_entry, 1, 1);
attach (connect_button, 1, 2);
show_all ();
}
}
}

View file

@ -0,0 +1,32 @@
namespace Streamdeck.Widgets {
public class CommandComboBox : Gtk.ComboBoxText {
private Data.CommandType? _selected;
public signal void selected (Data.CommandType type);
construct {
id_column = 1;
foreach (Data.CommandDescription cmd in Data.Command.available ()) {
append (cmd.id, cmd.name);
}
changed.connect ((_obj) => {
var id = get_active_id ();
var type = Data.Command.type_string (id);
if (type != null) {
selected (type);
}
_selected = type;
});
show_all ();
}
public Data.CommandType? get_selected_type () {
return _selected;
}
}
}

View file

@ -0,0 +1,136 @@
namespace Streamdeck.Widgets {
public class CommandList : Gtk.Frame {
private DeckInfo info;
private Gtk.ListBox list_box;
private Dialogs.NewCommandDialog? new_command_dialog;
private Gee.HashMap<uint8, CommandRow> row_map = new Gee.HashMap<uint8, CommandRow> ();
public CommandList(DeckInfo info) {
this.info = info;
var alert = new Granite.Widgets.AlertView (
_("No commands registered"),
_("Try adding a new command."),
""
);
alert.show_all ();
list_box = new Gtk.ListBox ();
list_box.selection_mode = Gtk.SelectionMode.SINGLE;
list_box.activate_on_single_click = true;
list_box.set_placeholder (alert);
var add_button = new Gtk.Button.from_icon_name (
"list-add-symbolic",
Gtk.IconSize.BUTTON
);
add_button.tooltip_text = _("Add");
var remove_button = new Gtk.Button.from_icon_name (
"list-remove-symbolic",
Gtk.IconSize.BUTTON
);
remove_button.tooltip_text = _("Remove");
var actionbar = new Gtk.ActionBar ();
actionbar.get_style_context ().add_class (Gtk.STYLE_CLASS_INLINE_TOOLBAR);
actionbar.add (add_button);
actionbar.add (remove_button);
actionbar.show_all ();
var scrolled = new Gtk.ScrolledWindow (null, null);
scrolled.expand = true;
scrolled.add (list_box);
var commands = Daemon.instance.get_commands (info.serial_number);
if (commands != null) {
foreach (CommandInfo cmd_info in commands) {
var command = Data.Command.parse (cmd_info.key, cmd_info.command);
if (command != null) {
var existing = row_map.get (cmd_info.key);
if (existing != null) {
list_box.remove (existing);
}
var row = new CommandRow (info.serial_number, command);
row_map.set (cmd_info.key, row);
list_box.add (row);
}
}
}
var grid = new Gtk.Grid ();
grid.attach (scrolled, 0, 0);
grid.attach (actionbar, 0, 1);
add (grid);
remove_button.clicked.connect (() => {
var row = list_box.get_selected_row ();
if (row == null) {
return;
}
unowned var command_row = (CommandRow) row;
var key = command_row.get_key ();
Daemon.instance.remove_command.begin (info.serial_number, key, (_obj, res) => {
try {
Daemon.instance.remove_command.end (res);
row_map.unset (key);
list_box.remove (row);
} catch (Error e) {
print ("Error removing command: %s\n", e.message);
}
});
});
add_button.clicked.connect (() => {
if (new_command_dialog == null) {
new_command_dialog = new Dialogs.NewCommandDialog (info.serial_number);
new_command_dialog.transient_for = (Gtk.Window) get_toplevel ();
new_command_dialog.show_all ();
new_command_dialog.response.connect ((response_id) => {
if (response_id == Gtk.ResponseType.REJECT) {
unowned var app = (MainWindow) get_toplevel ();
app.to_obs ();
}
new_command_dialog.destroy ();
});
new_command_dialog.destroy.connect (() => {
new_command_dialog = null;
});
}
new_command_dialog.present ();
});
Daemon.instance.commands_signal.connect ((_obj, serial_number, commands) => {
if (serial_number == info.serial_number) {
foreach (CommandInfo cmd_info in commands) {
var command = Data.Command.parse (cmd_info.key, cmd_info.command);
if (command != null) {
var existing = row_map.get (cmd_info.key);
if (existing != null) {
list_box.remove (existing);
}
var row = new CommandRow (serial_number, command);
row_map.set (cmd_info.key, row);
list_box.add (row);
}
}
}
});
show_all ();
}
}
}

100
src/Widgets/CommandRow.vala Normal file
View file

@ -0,0 +1,100 @@
namespace Streamdeck.Widgets {
public class CommandRow : Gtk.ListBoxRow {
private string serial_number;
private Data.Command command;
private Dialogs.EditCommandDialog? edit_dialog;
public CommandRow (string serial_number, Data.Command command) {
this.serial_number = serial_number;
this.command = command;
var key = command.get_command_key ();
var row_key = new Gtk.Label (@"$(key)");
row_key.halign = Gtk.Align.START;
row_key.valign = Gtk.Align.START;
row_key.show_all ();
var row_command = new Gtk.Label (null);
row_command.halign = Gtk.Align.START;
row_command.valign = Gtk.Align.START;
row_command.show_all ();
var row_grid = new Gtk.Grid ();
row_grid.orientation = Gtk.Orientation.HORIZONTAL;
row_grid.margin = 6;
row_grid.margin_start = 3;
row_grid.column_spacing = 3;
row_grid.add (row_key);
row_grid.add (row_command);
switch (command.get_command_type ()) {
case Data.CommandType.SWITCH_SCENE:
row_command.label = _("Switch scene");
switch_scene_ui (row_grid);
break;
default:
row_command.label = _("Unknown Command");
break;
}
var row_edit = new Gtk.Button.from_icon_name (
"document-edit-symbolic",
Gtk.IconSize.BUTTON
);
row_edit.tooltip_text = _("Edit");
row_edit.halign = Gtk.Align.END;
row_edit.valign = Gtk.Align.END;
row_edit.expand = true;
row_edit.clicked.connect (() => {
if (edit_dialog == null) {
edit_dialog = new Dialogs.EditCommandDialog (serial_number, command);
edit_dialog.transient_for = (Gtk.Window) get_toplevel ();
edit_dialog.show_all ();
edit_dialog.response.connect ((response_id) => {
if (response_id == Gtk.ResponseType.REJECT) {
unowned var app = (MainWindow) get_toplevel ();
app.to_obs ();
}
edit_dialog.destroy ();
});
edit_dialog.destroy.connect (() => {
edit_dialog = null;
});
}
edit_dialog.present ();
});
row_grid.add (row_edit);
row_grid.show_all();
add (row_grid);
show_all ();
}
public uint8 get_key () {
return command.get_command_key ();
}
private void switch_scene_ui (Gtk.Grid row_grid) {
unowned var switch_scene = (Data.SwitchScene) command;
var to = new Gtk.Label (_("to"));
to.halign = Gtk.Align.START;
to.valign = Gtk.Align.START;
to.show_all ();
var scene_name = new Gtk.Label (switch_scene.scene_name);
scene_name.halign = Gtk.Align.START;
scene_name.valign = Gtk.Align.START;
scene_name.show_all ();
row_grid.add (to);
row_grid.add (scene_name);
}
}
}

47
src/Widgets/DeckItem.vala Normal file
View file

@ -0,0 +1,47 @@
namespace Streamdeck.Widgets {
public class DeckItem : Gtk.ListBoxRow {
public string serial_number { get; set; }
public string device_name { get; set; }
public string port_name { get; set; }
public DeckItem (string serial_number, string device_name, string port_name) {
Object (
serial_number: serial_number,
device_name: device_name,
port_name: port_name
);
}
construct {
var row_title = new Gtk.Label (device_name);
row_title.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL);
row_title.halign = Gtk.Align.START;
row_title.valign = Gtk.Align.START;
row_title.ellipsize = Pango.EllipsizeMode.END;
row_title.show_all ();
var row_description = new Gtk.Label (port_name);
row_description.margin_top = 2;
row_description.use_markup = true;
row_description.halign = Gtk.Align.START;
row_description.valign = Gtk.Align.START;
row_description.ellipsize = Pango.EllipsizeMode.END;
row_description.show_all ();
var row_grid = new Gtk.Grid ();
row_grid.margin = 6;
row_grid.margin_start = 3;
row_grid.column_spacing = 3;
row_grid.attach (row_title, 0, 0);
row_grid.attach (row_description, 0, 1);
row_grid.show_all ();
add (row_grid);
bind_property ("device-name", row_title, "label");
bind_property ("port-name", row_description, "label");
show_all ();
}
}
}

110
src/Widgets/DeckList.vala Normal file
View file

@ -0,0 +1,110 @@
namespace Streamdeck.Widgets {
public class DeckList : Gtk.Frame {
private Gtk.Label usb_l;
private Views.DeckStack deck_stack;
private Gtk.ListBox list_box;
public DeckList (Views.DeckStack deck_stack) {
this.deck_stack = deck_stack;
}
construct {
var alert = new Granite.Widgets.AlertView (
_("No streamdecks found"),
_("Try plugging one in!"),
""
);
alert.show_all();
list_box = new Gtk.ListBox ();
list_box.selection_mode = Gtk.SelectionMode.SINGLE;
list_box.activate_on_single_click = true;
list_box.set_placeholder (alert);
usb_l = new Gtk.Label (_("Usb"));
usb_l.get_style_context ().add_class (Granite.STYLE_CLASS_H4_LABEL);
usb_l.halign = Gtk.Align.START;
list_box.set_header_func (update_headers);
list_box.row_selected.connect ((row) => {
row.activate ();
unowned var item = (DeckItem) row;
deck_stack.select_deck (item.serial_number);
});
var scroll = new Gtk.ScrolledWindow (null, null);
scroll.hscrollbar_policy = Gtk.PolicyType.NEVER;
scroll.expand = true;
scroll.add (list_box);
add (scroll);
show_all ();
}
public void add_deck (DeckInfo deck_info) {
var already_present = false;
foreach (Gtk.Widget child in list_box.get_children ()) {
var deck_item = (DeckItem) child;
already_present = already_present
|| deck_item.serial_number == deck_info.serial_number;
}
if (!already_present) {
var item = new DeckItem (
deck_info.serial_number,
deck_info.device_name,
deck_info.port_name
);
deck_stack.add_deck (deck_info);
list_box.add (item);
}
deck_stack.show_all ();
show_all ();
}
public void remove_deck (DeckInfo deck_info) {
foreach (Gtk.Widget child in list_box.get_children ()) {
var deck_item = (DeckItem) child;
if (deck_item.serial_number == deck_info.serial_number) {
deck_stack.remove_deck (deck_info);
list_box.remove (deck_item);
break;
}
}
deck_stack.show_all ();
show_all ();
}
private void update_headers (Gtk.ListBoxRow row, Gtk.ListBoxRow? before = null) {
if (before != null) {
row.set_header (null);
}
if (usb_l.get_parent () != null) {
usb_l.unparent ();
}
row.set_header (usb_l);
}
public bool any_selected () {
return list_box.get_selected_row () != null;
}
public void select_first_item () {
var row = list_box.get_row_at_index (0);
if (row == null) {
return;
}
list_box.select_row (row);
}
}
}

View file

@ -0,0 +1,20 @@
namespace Streamdeck.Widgets {
public class DisconnectedPage : Gtk.Grid {
construct {
var disconnected_label = new Gtk.Label (
_("OBS is currently disconnected, try connecting it before configuring commands")
);
disconnected_label.valign = Gtk.Align.CENTER;
disconnected_label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL);
column_spacing = 12;
row_spacing = 12;
halign = Gtk.Align.CENTER;
margin = 24;
attach (disconnected_label, 0, 0);
show_all ();
}
}
}

View file

@ -0,0 +1,21 @@
namespace Streamdeck.Widgets {
public class EmptyConfigPane: Gtk.Grid {
construct {
column_spacing = 12;
row_spacing = 12;
halign = Gtk.Align.CENTER;
margin = 24;
margin_top = 64;
var label = new Gtk.Label (
_("No streamdeck selected")
);
label.valign = Gtk.Align.CENTER;
label.get_style_context ().add_class (Granite.STYLE_CLASS_H3_LABEL);
attach (label, 0, 0);
show_all ();
}
}
}