diff --git a/Cargo.lock b/Cargo.lock
index 76e05d6..abd507e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -900,6 +900,24 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "gm-control-panel"
+version = "0.1.0"
+dependencies = [
+ "config",
+ "config-derive",
+ "futures",
+ "gdk4",
+ "gio",
+ "glib",
+ "glib-build-tools",
+ "gtk4",
+ "libadwaita",
+ "serde",
+ "serde_json",
+ "tokio",
+]
+
[[package]]
name = "gobject-sys"
version = "0.17.10"
diff --git a/Cargo.toml b/Cargo.toml
index 2d81067..fc88da2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ members = [
"flow",
"fluent-ergonomics",
"geo-types",
+ "gm-control-panel",
"hex-grid",
"ifc",
"kifu/core",
diff --git a/build.sh b/build.sh
index ebfc926..1d443b2 100755
--- a/build.sh
+++ b/build.sh
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
-set -x
RUST_ALL_TARGETS=(
"changeset"
@@ -14,6 +13,7 @@ RUST_ALL_TARGETS=(
"flow"
"fluent-ergonomics"
"geo-types"
+ "gm-control-panel"
"hex-grid"
"ifc"
"kifu-core"
diff --git a/gm-control-panel/Cargo.toml b/gm-control-panel/Cargo.toml
new file mode 100644
index 0000000..81029d6
--- /dev/null
+++ b/gm-control-panel/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "gm-control-panel"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+adw = { version = "0.4", package = "libadwaita", features = [ "v1_2", "gtk_v4_6" ] }
+config = { path = "../config" }
+config-derive = { path = "../config-derive" }
+futures = { version = "0.3" }
+gio = { version = "0.17" }
+glib = { version = "0.17" }
+gdk = { version = "0.6", package = "gdk4" }
+gtk = { version = "0.6", package = "gtk4", features = [ "v4_6" ] }
+serde = { version = "1" }
+serde_json = { version = "*" }
+tokio = { version = "1", features = ["full"] }
+
+[build-dependencies]
+glib-build-tools = "0.16"
+
diff --git a/gm-control-panel/build.rs b/gm-control-panel/build.rs
new file mode 100644
index 0000000..c83cc1e
--- /dev/null
+++ b/gm-control-panel/build.rs
@@ -0,0 +1,7 @@
+fn main() {
+ glib_build_tools::compile_resources(
+ "resources",
+ "resources/gresources.xml",
+ "com.luminescent-dreams.gm-control-panel.gresource",
+ );
+}
diff --git a/gm-control-panel/resources/gresources.xml b/gm-control-panel/resources/gresources.xml
new file mode 100644
index 0000000..1caf7fc
--- /dev/null
+++ b/gm-control-panel/resources/gresources.xml
@@ -0,0 +1,6 @@
+
+
+
+ style.css
+
+
diff --git a/gm-control-panel/resources/style.css b/gm-control-panel/resources/style.css
new file mode 100644
index 0000000..32b6d6a
--- /dev/null
+++ b/gm-control-panel/resources/style.css
@@ -0,0 +1,6 @@
+.playlist-card {
+ margin: 8px;
+ padding: 8px;
+ min-width: 100px;
+ min-height: 100px;
+}
diff --git a/gm-control-panel/src/app_window.rs b/gm-control-panel/src/app_window.rs
new file mode 100644
index 0000000..26574f2
--- /dev/null
+++ b/gm-control-panel/src/app_window.rs
@@ -0,0 +1,64 @@
+use crate::PlaylistCard;
+use adw::prelude::AdwApplicationWindowExt;
+use gio::resources_lookup_data;
+use gtk::{prelude::*, STYLE_PROVIDER_PRIORITY_USER};
+use std::iter::Iterator;
+
+#[derive(Clone)]
+pub struct ApplicationWindow {
+ pub window: adw::ApplicationWindow,
+ pub layout: gtk::FlowBox,
+ pub playlists: Vec,
+}
+
+impl ApplicationWindow {
+ pub fn new(app: &adw::Application) -> Self {
+ let window = adw::ApplicationWindow::builder()
+ .application(app)
+ .title("GM-control-panel")
+ .width_request(500)
+ .build();
+
+ let stylesheet = String::from_utf8(
+ resources_lookup_data(
+ "/com/luminescent-dreams/gm-control-panel/style.css",
+ gio::ResourceLookupFlags::NONE,
+ )
+ .expect("stylesheet should just be available")
+ .to_vec(),
+ )
+ .expect("to parse stylesheet");
+
+ let provider = gtk::CssProvider::new();
+ provider.load_from_data(&stylesheet);
+ let context = window.style_context();
+ context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER);
+
+ let layout = gtk::FlowBox::new();
+
+ let playlists: Vec = vec![
+ "Creepy Cathedral",
+ "Joyful Tavern",
+ "Exploring",
+ "Out on the streets",
+ "The North Abbey",
+ ]
+ .into_iter()
+ .map(|name| {
+ let playlist = PlaylistCard::new();
+ playlist.set_name(name);
+ playlist
+ })
+ .collect();
+
+ playlists.iter().for_each(|card| layout.append(card));
+
+ window.set_content(Some(&layout));
+
+ Self {
+ window,
+ layout,
+ playlists,
+ }
+ }
+}
diff --git a/gm-control-panel/src/config.rs b/gm-control-panel/src/config.rs
new file mode 100644
index 0000000..e8ea768
--- /dev/null
+++ b/gm-control-panel/src/config.rs
@@ -0,0 +1,40 @@
+use config::define_config;
+use config_derive::ConfigOption;
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+
+define_config! {
+ Language(Language),
+ MusicPath(MusicPath),
+ PlaylistDatabasePath(PlaylistDatabasePath),
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
+pub struct Language(String);
+
+impl std::ops::Deref for Language {
+ type Target = String;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
+pub struct MusicPath(PathBuf);
+
+impl std::ops::Deref for MusicPath {
+ type Target = PathBuf;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
+pub struct PlaylistDatabasePath(PathBuf);
+
+impl std::ops::Deref for PlaylistDatabasePath {
+ type Target = PathBuf;
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
diff --git a/gm-control-panel/src/main.rs b/gm-control-panel/src/main.rs
new file mode 100644
index 0000000..da86db8
--- /dev/null
+++ b/gm-control-panel/src/main.rs
@@ -0,0 +1,59 @@
+use glib::{Continue, Sender};
+use gtk::prelude::*;
+use std::{
+ env,
+ sync::{Arc, RwLock},
+};
+
+mod app_window;
+use app_window::ApplicationWindow;
+
+mod config;
+
+mod playlist_card;
+use playlist_card::PlaylistCard;
+
+mod types;
+use types::PlaybackState;
+
+#[derive(Clone, Debug)]
+pub enum Message {}
+
+#[derive(Clone)]
+pub struct Core {
+ tx: Arc>>>,
+}
+
+pub fn main() {
+ gio::resources_register_include!("com.luminescent-dreams.gm-control-panel.gresource")
+ .expect("Failed to register resource");
+
+ let app = adw::Application::builder()
+ .application_id("com.luminescent-dreams.gm-control-panel")
+ .build();
+
+ let runtime = tokio::runtime::Builder::new_multi_thread()
+ .enable_all()
+ .build()
+ .unwrap();
+
+ let core = Core {
+ tx: Arc::new(RwLock::new(None)),
+ };
+
+ app.connect_activate(move |app| {
+ let (gtk_tx, gtk_rx) =
+ gtk::glib::MainContext::channel::(gtk::glib::PRIORITY_DEFAULT);
+
+ *core.tx.write().unwrap() = Some(gtk_tx);
+
+ let window = ApplicationWindow::new(app);
+ window.window.present();
+
+ gtk_rx.attach(None, move |_msg| Continue(true));
+ });
+
+ let args: Vec = env::args().collect();
+ ApplicationExtManual::run_with_args(&app, &args);
+ runtime.shutdown_background();
+}
diff --git a/gm-control-panel/src/playlist_card.rs b/gm-control-panel/src/playlist_card.rs
new file mode 100644
index 0000000..d1d0007
--- /dev/null
+++ b/gm-control-panel/src/playlist_card.rs
@@ -0,0 +1,54 @@
+use crate::PlaybackState;
+use glib::Object;
+use gtk::{prelude::*, subclass::prelude::*};
+
+pub struct PlaylistCardPrivate {
+ name: gtk::Label,
+ playing: gtk::Label,
+}
+
+impl Default for PlaylistCardPrivate {
+ fn default() -> Self {
+ Self {
+ name: gtk::Label::new(None),
+ playing: gtk::Label::new(Some("Stopped")),
+ }
+ }
+}
+
+#[glib::object_subclass]
+impl ObjectSubclass for PlaylistCardPrivate {
+ const NAME: &'static str = "PlaylistCard";
+ type Type = PlaylistCard;
+ type ParentType = gtk::Box;
+}
+
+impl ObjectImpl for PlaylistCardPrivate {}
+impl WidgetImpl for PlaylistCardPrivate {}
+impl BoxImpl for PlaylistCardPrivate {}
+
+glib::wrapper! {
+ pub struct PlaylistCard(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
+}
+
+impl PlaylistCard {
+ pub fn new() -> Self {
+ let s: Self = Object::builder().build();
+ s.set_orientation(gtk::Orientation::Vertical);
+ s.add_css_class("playlist-card");
+ s.add_css_class("card");
+
+ s.append(&s.imp().name);
+ s.append(&s.imp().playing);
+
+ s
+ }
+
+ pub fn set_name(&self, s: &str) {
+ self.imp().name.set_text(s);
+ }
+
+ pub fn set_playback(&self, s: PlaybackState) {
+ self.imp().playing.set_text(&format!("{}", s))
+ }
+}
diff --git a/gm-control-panel/src/types.rs b/gm-control-panel/src/types.rs
new file mode 100644
index 0000000..f0a6349
--- /dev/null
+++ b/gm-control-panel/src/types.rs
@@ -0,0 +1,16 @@
+use std::fmt;
+
+#[derive(Clone, Debug)]
+pub enum PlaybackState {
+ Stopped,
+ Playing,
+}
+
+impl fmt::Display for PlaybackState {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Self::Stopped => write!(f, "Stopped"),
+ Self::Playing => write!(f, "Playing"),
+ }
+ }
+}