Compare commits
5 Commits
b9425af234
...
62b8e90c85
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 62b8e90c85 | |
Savanni D'Gerinel | e630c062c2 | |
Savanni D'Gerinel | f28a64d9a6 | |
Savanni D'Gerinel | d94dd5245c | |
Savanni D'Gerinel | b2ba257cac |
|
@ -3,4 +3,3 @@ target
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
result
|
result
|
||||||
*.tgz
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,8 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"changeset",
|
"changeset",
|
||||||
"config",
|
|
||||||
"config-derive",
|
|
||||||
"coordinates",
|
"coordinates",
|
||||||
"cyberpunk-splash",
|
"cyberpunk-splash",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
|
@ -10,7 +8,6 @@ members = [
|
||||||
"flow",
|
"flow",
|
||||||
"fluent-ergonomics",
|
"fluent-ergonomics",
|
||||||
"geo-types",
|
"geo-types",
|
||||||
"gm-control-panel",
|
|
||||||
"hex-grid",
|
"hex-grid",
|
||||||
"ifc",
|
"ifc",
|
||||||
"kifu/core",
|
"kifu/core",
|
||||||
|
|
17
build.sh
17
build.sh
|
@ -4,8 +4,6 @@ set -euo pipefail
|
||||||
|
|
||||||
RUST_ALL_TARGETS=(
|
RUST_ALL_TARGETS=(
|
||||||
"changeset"
|
"changeset"
|
||||||
"config"
|
|
||||||
"config-derive"
|
|
||||||
"coordinates"
|
"coordinates"
|
||||||
"cyberpunk-splash"
|
"cyberpunk-splash"
|
||||||
"dashboard"
|
"dashboard"
|
||||||
|
@ -13,7 +11,6 @@ RUST_ALL_TARGETS=(
|
||||||
"flow"
|
"flow"
|
||||||
"fluent-ergonomics"
|
"fluent-ergonomics"
|
||||||
"geo-types"
|
"geo-types"
|
||||||
"gm-control-panel"
|
|
||||||
"hex-grid"
|
"hex-grid"
|
||||||
"ifc"
|
"ifc"
|
||||||
"kifu-core"
|
"kifu-core"
|
||||||
|
@ -32,16 +29,6 @@ build_rust_targets() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
build_dist() {
|
|
||||||
local TARGETS=${@/$CMD}
|
|
||||||
|
|
||||||
for target in $TARGETS; do
|
|
||||||
if [ -f $target/dist.sh ]; then
|
|
||||||
cd $target && ./dist.sh
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
export CARGO=`which cargo`
|
export CARGO=`which cargo`
|
||||||
|
|
||||||
if [ -z "${TARGET-}" ]; then
|
if [ -z "${TARGET-}" ]; then
|
||||||
|
@ -58,9 +45,7 @@ if [ "${CMD}" == "clean" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for cmd in $CMD; do
|
for cmd in $CMD; do
|
||||||
if [ "${CMD}" == "dist" ]; then
|
if [ "${TARGET}" == "all" ]; then
|
||||||
build_dist $TARGET
|
|
||||||
elif [ "${TARGET}" == "all" ]; then
|
|
||||||
build_rust_targets $cmd ${RUST_ALL_TARGETS[*]}
|
build_rust_targets $cmd ${RUST_ALL_TARGETS[*]}
|
||||||
else
|
else
|
||||||
build_rust_targets $cmd $TARGET
|
build_rust_targets $cmd $TARGET
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "config-derive"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
proc-macro = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
quote = { version = "1" }
|
|
||||||
syn = { version = "1", features = [ "extra-traits" ] }
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
extern crate proc_macro;
|
|
||||||
|
|
||||||
use proc_macro::TokenStream;
|
|
||||||
use quote::quote;
|
|
||||||
|
|
||||||
use syn::{parse_macro_input, DeriveInput};
|
|
||||||
|
|
||||||
#[proc_macro_derive(ConfigOption)]
|
|
||||||
pub fn derive(input: TokenStream) -> TokenStream {
|
|
||||||
let DeriveInput { ident, .. } = parse_macro_input!(input as DeriveInput);
|
|
||||||
|
|
||||||
let result = quote! {
|
|
||||||
impl From<&Config> for Option<#ident> {
|
|
||||||
fn from(config: &Config) -> Self {
|
|
||||||
match config.values.get(&ConfigName::#ident) {
|
|
||||||
Some(ConfigOption::#ident(val)) => Some(val.clone()),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
result.into()
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "config"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
config-derive = { path = "../config-derive" }
|
|
||||||
serde_json = { version = "1" }
|
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
|
||||||
thiserror = { version = "1" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
cool_asserts = { version = "2" }
|
|
|
@ -1,160 +0,0 @@
|
||||||
/*
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
fs::File,
|
|
||||||
hash::Hash,
|
|
||||||
io::{ErrorKind, Read},
|
|
||||||
path::PathBuf,
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub use config_derive::ConfigOption;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum ConfigReadError {
|
|
||||||
#[error("Cannot read the configuration file: {0}")]
|
|
||||||
CannotRead(std::io::Error),
|
|
||||||
#[error("Cannot open the configuration file for reading: {0}")]
|
|
||||||
CannotOpen(std::io::Error),
|
|
||||||
#[error("Invalid json data found in the configurationfile: {0}")]
|
|
||||||
InvalidJSON(serde_json::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! define_config {
|
|
||||||
($($name:ident($struct:ident),)+) => (
|
|
||||||
#[derive(Clone, Debug, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
||||||
pub enum ConfigName {
|
|
||||||
$($name),+
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum ConfigOption {
|
|
||||||
$($name($struct)),+
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Config {
|
|
||||||
values: std::collections::HashMap<ConfigName, ConfigOption>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
values: std::collections::HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_path(config_path: std::path::PathBuf) -> Result<Self, $crate::ConfigReadError> {
|
|
||||||
let mut settings = config_path.clone();
|
|
||||||
settings.push("config");
|
|
||||||
|
|
||||||
match std::fs::File::open(settings) {
|
|
||||||
Ok(mut file) => {
|
|
||||||
let mut buf = String::new();
|
|
||||||
std::io::Read::read_to_string(&mut file, &mut buf)
|
|
||||||
.map_err(|err| $crate::ConfigReadError::CannotRead(err))?;
|
|
||||||
let values = serde_json::from_str(buf.as_ref())
|
|
||||||
.map_err(|err| $crate::ConfigReadError::InvalidJSON(err))?;
|
|
||||||
Ok(Self {
|
|
||||||
values,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Err(io_err) => {
|
|
||||||
match io_err.kind() {
|
|
||||||
std::io::ErrorKind::NotFound => {
|
|
||||||
/* create the path and an empty file */
|
|
||||||
Ok(Self {
|
|
||||||
values: std::collections::HashMap::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => Err($crate::ConfigReadError::CannotOpen(io_err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set(&mut self, val: ConfigOption) {
|
|
||||||
let _ = match val {
|
|
||||||
$(ConfigOption::$struct(_) => self.values.insert(ConfigName::$name, val)),+
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get<'a, T>(&'a self) -> Option<T>
|
|
||||||
where
|
|
||||||
Option<T>: From<&'a Self>,
|
|
||||||
{
|
|
||||||
self.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use cool_asserts::assert_matches;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
define_config! {
|
|
||||||
DatabasePath(DatabasePath),
|
|
||||||
Me(Me),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)]
|
|
||||||
pub struct DatabasePath(PathBuf);
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
enum Rank {
|
|
||||||
Kyu(i8),
|
|
||||||
Dan(i8),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)]
|
|
||||||
pub struct Me {
|
|
||||||
name: String,
|
|
||||||
rank: Option<Rank>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_can_set_and_get_options() {
|
|
||||||
let mut config: Config = Config::new();
|
|
||||||
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
|
|
||||||
"./fixtures/five_games",
|
|
||||||
))));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Some(DatabasePath(PathBuf::from("./fixtures/five_games"))),
|
|
||||||
config.get()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_can_serialize_and_deserialize() {
|
|
||||||
let mut config = Config::new();
|
|
||||||
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
|
|
||||||
"fixtures/five_games",
|
|
||||||
))));
|
|
||||||
config.set(ConfigOption::Me(Me {
|
|
||||||
name: "Savanni".to_owned(),
|
|
||||||
rank: Some(Rank::Kyu(10)),
|
|
||||||
}));
|
|
||||||
let s = serde_json::to_string(&config.values).unwrap();
|
|
||||||
println!("{}", s);
|
|
||||||
let values: HashMap<ConfigName, ConfigOption> = serde_json::from_str(s.as_ref()).unwrap();
|
|
||||||
println!("options: {:?}", values);
|
|
||||||
|
|
||||||
assert_matches!(values.get(&ConfigName::DatabasePath),
|
|
||||||
Some(ConfigOption::DatabasePath(ref db_path)) =>
|
|
||||||
assert_eq!(Some(db_path.clone()), config.get())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_matches!(values.get(&ConfigName::Me), Some(ConfigOption::Me(val)) =>
|
|
||||||
assert_eq!(Some(val.clone()), config.get())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
[Desktop Entry]
|
|
||||||
Type=Application
|
|
||||||
Version=1.0
|
|
||||||
Name=dashboard
|
|
||||||
Comment=My personal system dashboard
|
|
||||||
Exec=dashboard
|
|
|
@ -1,11 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
set -x
|
|
||||||
|
|
||||||
mkdir -p dist
|
|
||||||
cp dashboard.desktop dist
|
|
||||||
cp ../target/release/dashboard dist
|
|
||||||
strip dist/dashboard
|
|
||||||
tar -cf dashboard.tgz dist/
|
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
[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"
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
fn main() {
|
|
||||||
glib_build_tools::compile_resources(
|
|
||||||
"resources",
|
|
||||||
"resources/gresources.xml",
|
|
||||||
"com.luminescent-dreams.gm-control-panel.gresource",
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<gresources>
|
|
||||||
<gresource prefix="/com/luminescent-dreams/gm-control-panel/">
|
|
||||||
<file>style.css</file>
|
|
||||||
</gresource>
|
|
||||||
</gresources>
|
|
|
@ -1,6 +0,0 @@
|
||||||
.playlist-card {
|
|
||||||
margin: 8px;
|
|
||||||
padding: 8px;
|
|
||||||
min-width: 100px;
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
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<PlaylistCard>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<PlaylistCard> = 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
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<RwLock<Option<Sender<Message>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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::<Message>(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<String> = env::args().collect();
|
|
||||||
ApplicationExtManual::run_with_args(&app, &args);
|
|
||||||
runtime.shutdown_background();
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
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<PlaylistCardPrivate>) @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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
197
ifc/src/lib.rs
197
ifc/src/lib.rs
|
@ -250,12 +250,8 @@ impl From<chrono::NaiveDate> for IFC {
|
||||||
{
|
{
|
||||||
days = days - 1;
|
days = days - 1;
|
||||||
}
|
}
|
||||||
let mut month: u8 = (days / 28).try_into().unwrap();
|
let month: u8 = (days / 28).try_into().unwrap();
|
||||||
let mut day: u8 = (days % 28).try_into().unwrap();
|
let day: u8 = (days % 28).try_into().unwrap();
|
||||||
if day == 0 {
|
|
||||||
month = month - 1;
|
|
||||||
day = 28;
|
|
||||||
}
|
|
||||||
Self::Day(Day {
|
Self::Day(Day {
|
||||||
year: date.year(),
|
year: date.year(),
|
||||||
month: month + 1,
|
month: month + 1,
|
||||||
|
@ -272,6 +268,167 @@ impl From<IFC> for chrono::NaiveDate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
impl IFC {
|
||||||
|
pub fn year_day(year: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
year,
|
||||||
|
ordinal: if is_leap_year(year) { 366 } else { 365 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
impl From<chrono::Date<chrono::Utc>> for IFC {
|
||||||
|
fn from(d: chrono::Date<chrono::Utc>) -> IFC {
|
||||||
|
IFC::from(d.naive_utc())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<chrono::Date<chrono_tz::Tz>> for IFC {
|
||||||
|
fn from(d: chrono::Date<chrono_tz::Tz>) -> IFC {
|
||||||
|
IFC::from(d.naive_utc())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<chrono::NaiveDate> for IFC {
|
||||||
|
fn from(d: NaiveDate) -> IFC {
|
||||||
|
//println!("d: {} [{}]", d.format("%Y-%m-%d"), d.ordinal());
|
||||||
|
IFC {
|
||||||
|
year: (d.year() + 10000) as u32,
|
||||||
|
ordinal: d.ordinal0(),
|
||||||
|
leap_year: is_leap_year(d.year()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
impl Datelike for IFC {
|
||||||
|
fn year(&self) -> i32 {
|
||||||
|
// self.year as i32
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn month(&self) -> u32 {
|
||||||
|
// self.month0() + 1
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn month0(&self) -> u32 {
|
||||||
|
/*
|
||||||
|
if self.leap_year && self.ordinal == 365 {
|
||||||
|
12
|
||||||
|
} else if self.leap_year && self.ordinal == 168 {
|
||||||
|
5
|
||||||
|
} else {
|
||||||
|
self.ordinal / 28
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn day(&self) -> u32 {
|
||||||
|
// self.day0() + 1
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn day0(&self) -> u32 {
|
||||||
|
/*
|
||||||
|
if self.leap_year {
|
||||||
|
if self.ordinal == 365 {
|
||||||
|
28
|
||||||
|
} else if self.ordinal == 168 {
|
||||||
|
28
|
||||||
|
} else if self.ordinal > 168 {
|
||||||
|
(self.ordinal - 1).rem_euclid(28) as u32
|
||||||
|
} else {
|
||||||
|
self.ordinal.rem_euclid(28) as u32
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if self.ordinal == 364 {
|
||||||
|
28
|
||||||
|
} else {
|
||||||
|
self.ordinal.rem_euclid(28) as u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn ordinal(&self) -> u32 {
|
||||||
|
// self.ordinal + 1
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn ordinal0(&self) -> u32 {
|
||||||
|
// self.ordinal
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn weekday(&self) -> chrono::Weekday {
|
||||||
|
if self.day0() == 28 {
|
||||||
|
chrono::Weekday::Sat
|
||||||
|
} else {
|
||||||
|
match self.day0().rem_euclid(7) {
|
||||||
|
0 => chrono::Weekday::Sun,
|
||||||
|
1 => chrono::Weekday::Mon,
|
||||||
|
2 => chrono::Weekday::Tue,
|
||||||
|
3 => chrono::Weekday::Wed,
|
||||||
|
4 => chrono::Weekday::Thu,
|
||||||
|
5 => chrono::Weekday::Fri,
|
||||||
|
6 => chrono::Weekday::Sat,
|
||||||
|
_ => panic!("rem_euclid should not return anything outside the 0..6 range"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn iso_week(&self) -> chrono::IsoWeek {
|
||||||
|
panic!("iso_week is not implemented because chrono does not expose any constructors for IsoWeek!");
|
||||||
|
}
|
||||||
|
fn with_year(&self, year: i32) -> Option<IFC> {
|
||||||
|
/*
|
||||||
|
Some(IFC {
|
||||||
|
year: (year as u32) + 10000,
|
||||||
|
ordinal: self.ordinal,
|
||||||
|
leap_year: is_leap_year(year),
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn with_month(&self, month: u32) -> Option<IFC> {
|
||||||
|
// Some(IFC::ymd(self.year, month as u8, self.day() as u8))
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn with_month0(&self, month: u32) -> Option<IFC> {
|
||||||
|
// Some(IFC::ymd(self.year, month as u8 + 1, self.day() as u8))
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn with_day(&self, day: u32) -> Option<IFC> {
|
||||||
|
// Some(IFC::ymd(self.year, self.month() as u8, day as u8))
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn with_day0(&self, day: u32) -> Option<IFC> {
|
||||||
|
// Some(IFC::ymd(self.year, self.month() as u8, day as u8 + 1))
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn with_ordinal(&self, ordinal: u32) -> Option<IFC> {
|
||||||
|
/*
|
||||||
|
Some(IFC {
|
||||||
|
year: self.year,
|
||||||
|
ordinal,
|
||||||
|
leap_year: self.leap_year,
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn with_ordinal0(&self, ordinal: u32) -> Option<IFC> {
|
||||||
|
/*
|
||||||
|
Some(IFC {
|
||||||
|
year: self.year,
|
||||||
|
ordinal: ordinal + 1,
|
||||||
|
leap_year: self.leap_year,
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -455,34 +612,10 @@ mod tests {
|
||||||
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 1).unwrap()),
|
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 1).unwrap()),
|
||||||
IFC::ymd(12022, 1, 1).unwrap()
|
IFC::ymd(12022, 1, 1).unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 2).unwrap()),
|
|
||||||
IFC::ymd(12022, 1, 2).unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 3).unwrap()),
|
|
||||||
IFC::ymd(12022, 1, 3).unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
IFC::from(chrono::NaiveDate::from_ymd_opt(2023, 01, 26).unwrap()),
|
|
||||||
IFC::ymd(2023, 1, 26).unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
IFC::from(chrono::NaiveDate::from_ymd_opt(2023, 01, 27).unwrap()),
|
|
||||||
IFC::ymd(2023, 1, 27).unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
IFC::from(chrono::NaiveDate::from_ymd_opt(2023, 01, 28).unwrap()),
|
|
||||||
IFC::ymd(2023, 1, 28).unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 29).unwrap()),
|
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 29).unwrap()),
|
||||||
IFC::ymd(12022, 2, 1).unwrap()
|
IFC::ymd(12022, 2, 1).unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 30).unwrap()),
|
|
||||||
IFC::ymd(12022, 2, 2).unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
IFC::from(NaiveDate::from_ymd_opt(12022, 2, 26).unwrap()),
|
IFC::from(NaiveDate::from_ymd_opt(12022, 2, 26).unwrap()),
|
||||||
IFC::ymd(12022, 3, 1).unwrap()
|
IFC::ymd(12022, 3, 1).unwrap()
|
||||||
|
@ -511,10 +644,6 @@ mod tests {
|
||||||
IFC::from(NaiveDate::from_ymd_opt(12022, 8, 13).unwrap()),
|
IFC::from(NaiveDate::from_ymd_opt(12022, 8, 13).unwrap()),
|
||||||
IFC::ymd(12022, 9, 1).unwrap()
|
IFC::ymd(12022, 9, 1).unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
IFC::from(chrono::NaiveDate::from_ymd_opt(2023, 08, 12).unwrap()),
|
|
||||||
IFC::ymd(2023, 8, 28).unwrap()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
IFC::from(NaiveDate::from_ymd_opt(12022, 9, 10).unwrap()),
|
IFC::from(NaiveDate::from_ymd_opt(12022, 9, 10).unwrap()),
|
||||||
IFC::ymd(12022, 10, 1).unwrap()
|
IFC::ymd(12022, 10, 1).unwrap()
|
||||||
|
|
|
@ -7,8 +7,6 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = { version = "0.4" }
|
chrono = { version = "0.4" }
|
||||||
config = { path = "../../config" }
|
|
||||||
config-derive = { path = "../../config-derive" }
|
|
||||||
sgf = { path = "../../sgf" }
|
sgf = { path = "../../sgf" }
|
||||||
grid = { version = "0.9" }
|
grid = { version = "0.9" }
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
|
|
|
@ -1,34 +1,23 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank},
|
types::{AppState, GameState, Player, Rank},
|
||||||
ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView},
|
ui::{home, playing_field, HomeView, PlayingFieldView},
|
||||||
|
Config, DatabasePath,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::sync::{Arc, RwLock};
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[serde(tag = "type", content = "content")]
|
#[serde(tag = "type", content = "content")]
|
||||||
pub enum CoreRequest {
|
pub enum CoreRequest {
|
||||||
ChangeSetting(ChangeSettingRequest),
|
|
||||||
CreateGame(CreateGameRequest),
|
CreateGame(CreateGameRequest),
|
||||||
Home,
|
Home,
|
||||||
OpenConfiguration,
|
|
||||||
PlayingField,
|
PlayingField,
|
||||||
PlayStone(PlayStoneRequest),
|
PlayStone(PlayStoneRequest),
|
||||||
StartGame,
|
StartGame,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[typeshare]
|
|
||||||
#[serde(tag = "type", content = "content")]
|
|
||||||
pub enum ChangeSettingRequest {
|
|
||||||
LibraryPath(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
pub struct PlayStoneRequest {
|
pub struct PlayStoneRequest {
|
||||||
|
@ -69,42 +58,46 @@ impl From<HotseatPlayerRequest> for Player {
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[serde(tag = "type", content = "content")]
|
#[serde(tag = "type", content = "content")]
|
||||||
pub enum CoreResponse {
|
pub enum CoreResponse {
|
||||||
ConfigurationView(ConfigurationView),
|
|
||||||
HomeView(HomeView),
|
HomeView(HomeView),
|
||||||
PlayingFieldView(PlayingFieldView),
|
PlayingFieldView(PlayingFieldView),
|
||||||
UpdatedConfigurationView(ConfigurationView),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CoreApp {
|
pub struct CoreApp {
|
||||||
config: Arc<RwLock<Config>>,
|
config: Config,
|
||||||
state: Arc<RwLock<AppState>>,
|
state: Arc<RwLock<AppState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoreApp {
|
impl CoreApp {
|
||||||
pub fn new(config_path: std::path::PathBuf) -> Self {
|
pub fn new(config_path: std::path::PathBuf) -> Self {
|
||||||
|
println!("config_path: {:?}", config_path);
|
||||||
let config = Config::from_path(config_path).expect("configuration to open");
|
let config = Config::from_path(config_path).expect("configuration to open");
|
||||||
|
|
||||||
let db_path: DatabasePath = config.get().unwrap();
|
let db_path: DatabasePath = config.get();
|
||||||
let state = Arc::new(RwLock::new(AppState::new(db_path)));
|
let state = Arc::new(RwLock::new(AppState::new(db_path)));
|
||||||
|
|
||||||
Self {
|
println!("config: {:?}", config);
|
||||||
config: Arc::new(RwLock::new(config)),
|
println!("games database: {:?}", state.read().unwrap().database.len());
|
||||||
state,
|
|
||||||
}
|
Self { config, state }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
||||||
match request {
|
match request {
|
||||||
CoreRequest::ChangeSetting(request) => match request {
|
/*
|
||||||
ChangeSettingRequest::LibraryPath(path) => {
|
CoreRequest::LaunchScreen => {
|
||||||
let mut config = self.config.write().unwrap();
|
let app_state = self.state.read().unwrap();
|
||||||
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
|
|
||||||
path,
|
At launch, I want to either show a list of games in progress, the current game, or the game creation screen.
|
||||||
))));
|
- if a live game is in progress, immmediately go to that game. Such a game will be classified at game creation, so it should be persisted to the state.
|
||||||
CoreResponse::UpdatedConfigurationView(configuration(&config))
|
- if no live games are in progress, but there are slow games in progress, show a list of the slow games and let the player choose which one to jump into.
|
||||||
}
|
- if no games are in progress, show only the game creation screen
|
||||||
},
|
- game creation menu should be present both when there are only slow games and when there are no games
|
||||||
|
- the UI returned here will always be available in other places, such as when the user is viewing a game and wants to return to this page
|
||||||
|
|
||||||
|
For the initial version, I want only to show the game creation screen. Then I will backtrack record application state so that the only decisions can be made.
|
||||||
|
}
|
||||||
|
*/
|
||||||
CoreRequest::CreateGame(create_request) => {
|
CoreRequest::CreateGame(create_request) => {
|
||||||
let mut app_state = self.state.write().unwrap();
|
let mut app_state = self.state.write().unwrap();
|
||||||
let white_player = {
|
let white_player = {
|
||||||
|
@ -128,9 +121,6 @@ impl CoreApp {
|
||||||
CoreRequest::Home => {
|
CoreRequest::Home => {
|
||||||
CoreResponse::HomeView(home(self.state.read().unwrap().database.all_games()))
|
CoreResponse::HomeView(home(self.state.read().unwrap().database.all_games()))
|
||||||
}
|
}
|
||||||
CoreRequest::OpenConfiguration => {
|
|
||||||
CoreResponse::ConfigurationView(configuration(&self.config.read().unwrap()))
|
|
||||||
}
|
|
||||||
CoreRequest::PlayingField => {
|
CoreRequest::PlayingField => {
|
||||||
let app_state = self.state.read().unwrap();
|
let app_state = self.state.read().unwrap();
|
||||||
let game = app_state.game.as_ref().unwrap();
|
let game = app_state.game.as_ref().unwrap();
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
use crate::types::Player;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs::File,
|
||||||
|
io::{ErrorKind, Read},
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub trait ConfigOption {
|
||||||
|
type Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DatabasePath(PathBuf);
|
||||||
|
|
||||||
|
impl ConfigOption for DatabasePath {
|
||||||
|
type Value = PathBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigOption for Player {
|
||||||
|
type Value = Player;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Config {
|
||||||
|
// fn set_option(option: ConfigOption);
|
||||||
|
fn get_option<N, C: ConfigOption>(name: Name) -> C<Name = Name>
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
enum OptionNames {
|
||||||
|
DatabasePath,
|
||||||
|
Me,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum ConfigOption {
|
||||||
|
DatabasePath(DatabasePath),
|
||||||
|
Me(Me),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ConfigReadError {
|
||||||
|
#[error("Cannot read the configuration file: {0}")]
|
||||||
|
CannotRead(std::io::Error),
|
||||||
|
#[error("Cannot open the configuration file for reading: {0}")]
|
||||||
|
CannotOpen(std::io::Error),
|
||||||
|
#[error("Invalid json data found in the configurationfile: {0}")]
|
||||||
|
InvalidJSON(serde_json::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Config {
|
||||||
|
config_path: PathBuf,
|
||||||
|
values: HashMap<OptionNames, ConfigOption>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn new(config_path: PathBuf) -> Self {
|
||||||
|
Self {
|
||||||
|
config_path,
|
||||||
|
values: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_path(config_path: PathBuf) -> Result<Self, ConfigReadError> {
|
||||||
|
let mut settings = config_path.clone();
|
||||||
|
settings.push("config");
|
||||||
|
|
||||||
|
match File::open(settings) {
|
||||||
|
Ok(mut file) => {
|
||||||
|
let mut buf = String::new();
|
||||||
|
file.read_to_string(&mut buf)
|
||||||
|
.map_err(|err| ConfigReadError::CannotRead(err))?;
|
||||||
|
let values = serde_json::from_str(buf.as_ref())
|
||||||
|
.map_err(|err| ConfigReadError::InvalidJSON(err))?;
|
||||||
|
Ok(Self {
|
||||||
|
config_path,
|
||||||
|
values,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(io_err) => {
|
||||||
|
match io_err.kind() {
|
||||||
|
ErrorKind::NotFound => {
|
||||||
|
/* create the path and an empty file */
|
||||||
|
Ok(Self {
|
||||||
|
config_path,
|
||||||
|
values: HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(ConfigReadError::CannotOpen(io_err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&mut self, val: ConfigOption) {
|
||||||
|
let _ = match val {
|
||||||
|
ConfigOption::DatabasePath(_) => self.values.insert(OptionNames::DatabasePath, val),
|
||||||
|
ConfigOption::Me(_) => self.values.insert(OptionNames::Me, val),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<'a, T>(&'a self) -> T
|
||||||
|
where
|
||||||
|
T: From<&'a Self>,
|
||||||
|
{
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct DatabasePath(PathBuf);
|
||||||
|
|
||||||
|
impl std::ops::Deref for DatabasePath {
|
||||||
|
type Target = PathBuf;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Config> for DatabasePath {
|
||||||
|
fn from(config: &Config) -> Self {
|
||||||
|
match config.values.get(&OptionNames::DatabasePath) {
|
||||||
|
Some(ConfigOption::DatabasePath(path)) => path.clone(),
|
||||||
|
_ => DatabasePath(config.config_path.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Me(Player);
|
||||||
|
|
||||||
|
impl From<&Config> for Option<Me> {
|
||||||
|
fn from(config: &Config) -> Self {
|
||||||
|
config
|
||||||
|
.values
|
||||||
|
.get(&OptionNames::Me)
|
||||||
|
.and_then(|val| match val {
|
||||||
|
ConfigOption::Me(me) => Some(me.clone()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for Me {
|
||||||
|
type Target = Player;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::types::Rank;
|
||||||
|
use cool_asserts::assert_matches;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_set_and_get_options() {
|
||||||
|
let mut config = Config::new(PathBuf::from("."));
|
||||||
|
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
|
||||||
|
"fixtures/five_games",
|
||||||
|
))));
|
||||||
|
config.set(ConfigOption::Me(Me(Player {
|
||||||
|
name: "Savanni".to_owned(),
|
||||||
|
rank: Some(Rank::Kyu(10)),
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_can_serialize_and_deserialize() {
|
||||||
|
let mut config = Config::new(PathBuf::from("."));
|
||||||
|
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
|
||||||
|
"fixtures/five_games",
|
||||||
|
))));
|
||||||
|
config.set(ConfigOption::Me(Me(Player {
|
||||||
|
name: "Savanni".to_owned(),
|
||||||
|
rank: Some(Rank::Kyu(10)),
|
||||||
|
})));
|
||||||
|
let s = serde_json::to_string(&config.values).unwrap();
|
||||||
|
println!("{}", s);
|
||||||
|
let values: HashMap<OptionNames, ConfigOption> = serde_json::from_str(s.as_ref()).unwrap();
|
||||||
|
println!("options: {:?}", values);
|
||||||
|
|
||||||
|
assert_matches!(values.get(&OptionNames::DatabasePath),
|
||||||
|
Some(ConfigOption::DatabasePath(db_path)) =>
|
||||||
|
assert_eq!(*db_path, config.get())
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(values.get(&OptionNames::Me), Some(ConfigOption::Me(val)) =>
|
||||||
|
assert_eq!(Some(val.clone()), config.get())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,16 +39,11 @@ impl Database {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.read_to_string(&mut buffer)
|
.read_to_string(&mut buffer)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
match parse_sgf(&buffer) {
|
for sgf in parse_sgf(&buffer).unwrap() {
|
||||||
Ok(sgfs) => {
|
match sgf {
|
||||||
for sgf in sgfs {
|
Game::Go(game) => games.push(game),
|
||||||
match sgf {
|
Game::Unsupported(_) => {}
|
||||||
Game::Go(game) => games.push(game),
|
|
||||||
Game::Unsupported(_) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,13 @@
|
||||||
#[macro_use]
|
|
||||||
extern crate config_derive;
|
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
pub use api::{
|
pub use api::{
|
||||||
ChangeSettingRequest, CoreApp, CoreRequest, CoreResponse, CreateGameRequest,
|
CoreApp, CoreRequest, CoreResponse, CreateGameRequest, HotseatPlayerRequest, PlayerInfoRequest,
|
||||||
HotseatPlayerRequest, PlayerInfoRequest,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mod board;
|
mod board;
|
||||||
pub use board::*;
|
pub use board::*;
|
||||||
|
|
||||||
/*
|
|
||||||
mod config;
|
mod config;
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
*/
|
|
||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
|
|
||||||
|
|
|
@ -1,40 +1,14 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
api::PlayStoneRequest,
|
api::PlayStoneRequest,
|
||||||
board::{Board, Coordinate},
|
board::{Board, Coordinate},
|
||||||
|
config::DatabasePath,
|
||||||
database::Database,
|
database::Database,
|
||||||
};
|
};
|
||||||
use config::define_config;
|
|
||||||
use config_derive::ConfigOption;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{path::PathBuf, time::Duration};
|
use std::{path::PathBuf, time::Duration};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
define_config! {
|
|
||||||
DatabasePath(DatabasePath),
|
|
||||||
Me(Me),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
|
|
||||||
pub struct DatabasePath(pub PathBuf);
|
|
||||||
|
|
||||||
impl std::ops::Deref for DatabasePath {
|
|
||||||
type Target = PathBuf;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
|
|
||||||
pub struct Me(Player);
|
|
||||||
|
|
||||||
impl std::ops::Deref for Me {
|
|
||||||
type Target = Player;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Error)]
|
#[derive(Debug, PartialEq, Error)]
|
||||||
pub enum BoardError {
|
pub enum BoardError {
|
||||||
#[error("Position is invalid")]
|
#[error("Position is invalid")]
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
use crate::{
|
|
||||||
types::{Config, DatabasePath},
|
|
||||||
ui::Field,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use typeshare::typeshare;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[typeshare]
|
|
||||||
pub struct ConfigurationView {
|
|
||||||
pub library: Field<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn configuration(config: &Config) -> ConfigurationView {
|
|
||||||
let path: Option<DatabasePath> = config.get();
|
|
||||||
ConfigurationView {
|
|
||||||
library: Field {
|
|
||||||
id: "library-path-field".to_owned(),
|
|
||||||
label: "Library".to_owned(),
|
|
||||||
value: path.map(|path| path.to_string_lossy().into_owned()),
|
|
||||||
action: (),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use typeshare::typeshare;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct Action<A> {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub action: A,
|
||||||
|
}
|
|
@ -1,70 +1,36 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sgf::go::{Game, GameResult, Win};
|
use sgf::{
|
||||||
|
go::{Game, Rank},
|
||||||
|
Date,
|
||||||
|
};
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
pub struct GamePreviewElement {
|
pub struct GamePreviewElement {
|
||||||
pub date: String,
|
pub date: Vec<Date>,
|
||||||
pub name: String,
|
|
||||||
pub black_player: String,
|
pub black_player: String,
|
||||||
|
pub black_rank: Option<Rank>,
|
||||||
pub white_player: String,
|
pub white_player: String,
|
||||||
pub result: String,
|
pub white_rank: Option<Rank>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GamePreviewElement {
|
impl GamePreviewElement {
|
||||||
pub fn new(game: &Game) -> GamePreviewElement {
|
pub fn new(game: &Game) -> GamePreviewElement {
|
||||||
let black_player = match game.info.black_player {
|
|
||||||
Some(ref black_player) => black_player.clone(),
|
|
||||||
None => "unknown".to_owned(),
|
|
||||||
};
|
|
||||||
let white_player = match game.info.white_player {
|
|
||||||
Some(ref white_player) => white_player.clone(),
|
|
||||||
None => "unknown".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let black_player = match game.info.black_rank {
|
|
||||||
Some(rank) => format!("{} ({})", black_player, rank.to_string()),
|
|
||||||
None => black_player,
|
|
||||||
};
|
|
||||||
|
|
||||||
let white_player = match game.info.white_rank {
|
|
||||||
Some(rank) => format!("{} ({})", white_player, rank.to_string()),
|
|
||||||
None => white_player,
|
|
||||||
};
|
|
||||||
|
|
||||||
let name = match game.info.game_name {
|
|
||||||
Some(ref name) => name.clone(),
|
|
||||||
None => format!("{} vs. {}", black_player, white_player),
|
|
||||||
};
|
|
||||||
|
|
||||||
let format_win = |win: &Win| match win {
|
|
||||||
Win::Resignation => "Resignation".to_owned(),
|
|
||||||
Win::Time => "Timeout".to_owned(),
|
|
||||||
Win::Forfeit => "Forfeit".to_owned(),
|
|
||||||
Win::Score(score) => format!("{:.1}", score),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = match game.info.result {
|
|
||||||
Some(GameResult::Annulled) => "Annulled".to_owned(),
|
|
||||||
Some(GameResult::Draw) => "Draw".to_owned(),
|
|
||||||
Some(GameResult::Black(ref win)) => format!("Black by {}", format_win(win)),
|
|
||||||
Some(GameResult::White(ref win)) => format!("White by {}", format_win(win)),
|
|
||||||
Some(GameResult::Unknown(ref text)) => format!("Unknown: {}", text),
|
|
||||||
None => "".to_owned(),
|
|
||||||
};
|
|
||||||
|
|
||||||
GamePreviewElement {
|
GamePreviewElement {
|
||||||
date: game
|
date: game.info.date.clone(),
|
||||||
|
black_player: game
|
||||||
.info
|
.info
|
||||||
.date
|
.black_player
|
||||||
.first()
|
.clone()
|
||||||
.map(|dt| dt.to_string())
|
.unwrap_or("black_player".to_owned()),
|
||||||
.unwrap_or("".to_owned()),
|
black_rank: game.info.black_rank.clone(),
|
||||||
name,
|
white_player: game
|
||||||
black_player,
|
.info
|
||||||
white_player,
|
.white_player
|
||||||
result,
|
.clone()
|
||||||
|
.unwrap_or("white_player".to_owned()),
|
||||||
|
white_rank: game.info.white_rank.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,3 @@
|
||||||
use serde::{Deserialize, Serialize};
|
pub mod action;
|
||||||
use typeshare::typeshare;
|
|
||||||
|
|
||||||
pub mod game_preview;
|
pub mod game_preview;
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[typeshare]
|
|
||||||
pub struct Action<A> {
|
|
||||||
pub id: String,
|
|
||||||
pub label: String,
|
|
||||||
pub action: A,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[typeshare]
|
|
||||||
pub struct Toggle<A> {
|
|
||||||
pub id: String,
|
|
||||||
pub label: String,
|
|
||||||
pub value: bool,
|
|
||||||
pub action: A,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
#[typeshare]
|
|
||||||
pub struct Field<A> {
|
|
||||||
pub id: String,
|
|
||||||
pub label: String,
|
|
||||||
pub value: Option<String>,
|
|
||||||
pub action: A,
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
use crate::{
|
||||||
|
ui::types;
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use typeshare::typeshare;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum LaunchScreenView {
|
||||||
|
CreateGame(CreateGameView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will be called when the Kifu application starts.
|
||||||
|
pub fn launch_screen() -> LaunchScreenView {
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
mod configuration;
|
|
||||||
pub use configuration::{configuration, ConfigurationView};
|
|
||||||
|
|
||||||
mod elements;
|
mod elements;
|
||||||
pub use elements::{game_preview::GamePreviewElement, menu::Menu, Action, Field, Toggle};
|
pub use elements::{action::Action, game_preview::GamePreviewElement, menu::Menu};
|
||||||
|
|
||||||
mod playing_field;
|
mod playing_field;
|
||||||
pub use playing_field::{playing_field, PlayingFieldView};
|
pub use playing_field::{playing_field, PlayingFieldView};
|
||||||
|
|
||||||
|
// mod launch_screen;
|
||||||
|
// pub use launch_screen::{launch_screen, LaunchScreenView};
|
||||||
|
|
||||||
mod home;
|
mod home;
|
||||||
pub use home::{home, HomeView, HotseatPlayerElement, PlayerElement};
|
pub use home::{home, HomeView, HotseatPlayerElement, PlayerElement};
|
||||||
|
|
||||||
|
|
|
@ -9,16 +9,14 @@ screenplay = []
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
adw = { version = "0.4", package = "libadwaita", features = [ "v1_2" ] }
|
|
||||||
cairo-rs = { version = "0.17" }
|
cairo-rs = { version = "0.17" }
|
||||||
gio = { version = "0.17" }
|
gio = { version = "0.17" }
|
||||||
glib = { version = "0.17" }
|
glib = { version = "0.17" }
|
||||||
gtk = { version = "0.6", package = "gtk4", features = [ "v4_8" ] }
|
gtk = { version = "0.6", package = "gtk4" }
|
||||||
image = { version = "0.24" }
|
image = { version = "0.24" }
|
||||||
kifu-core = { path = "../core" }
|
kifu-core = { path = "../core" }
|
||||||
pango = { version = "*" }
|
|
||||||
sgf = { path = "../../sgf" }
|
|
||||||
tokio = { version = "1.26", features = [ "full" ] }
|
tokio = { version = "1.26", features = [ "full" ] }
|
||||||
|
screenplay = { path = "../../screenplay" }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
glib-build-tools = "0.17"
|
glib-build-tools = "0.17"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
glib_build_tools::compile_resources(
|
glib_build_tools::compile_resources(
|
||||||
&["resources"],
|
&["resources"],
|
||||||
"resources/gresources.xml",
|
"resources/resources.gresources.xml",
|
||||||
"com.luminescent-dreams.kifu-gtk.gresource",
|
"com.luminescent-dreams.kifu-gtk.gresource",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1 @@
|
||||||
<<<<<<< HEAD
|
{"Me":{"name":"Savanni","rank":{"Kyu":10}},"DatabasePath":"kifu/core/fixtures/five_games"}
|
||||||
{
|
|
||||||
"Me":{
|
|
||||||
"name":"Savanni",
|
|
||||||
"rank":{"Kyu":10}
|
|
||||||
},
|
|
||||||
"DatabasePath": "kifu/core/fixtures/five_games/"
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,6 +2,5 @@
|
||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="/com/luminescent-dreams/kifu-gtk/">
|
<gresource prefix="/com/luminescent-dreams/kifu-gtk/">
|
||||||
<file>wood_texture.jpg</file>
|
<file>wood_texture.jpg</file>
|
||||||
<file>style.css</file>
|
|
||||||
</gresource>
|
</gresource>
|
||||||
</gresources>
|
</gresources>
|
|
@ -1,3 +0,0 @@
|
||||||
.content {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
|
@ -1,28 +1,20 @@
|
||||||
use adw::prelude::*;
|
use gtk::prelude::*;
|
||||||
use kifu_core::{CoreApp, CoreRequest, CoreResponse};
|
use kifu_core::{CoreApp, CoreRequest, CoreResponse};
|
||||||
use kifu_gtk::{
|
use kifu_gtk::{
|
||||||
perftrace,
|
perftrace,
|
||||||
ui::{AppWindow, ConfigurationPage, Home, PlayingField},
|
ui::{Home, PlayingField},
|
||||||
CoreApi,
|
CoreApi,
|
||||||
};
|
};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse) {
|
fn handle_response(api: CoreApi, window: gtk::ApplicationWindow, message: CoreResponse) {
|
||||||
let playing_field = Arc::new(RwLock::new(None));
|
let playing_field = Arc::new(RwLock::new(None));
|
||||||
match message {
|
match message {
|
||||||
CoreResponse::ConfigurationView(view) => perftrace("ConfigurationView", || {
|
|
||||||
let config_page = ConfigurationPage::new(api, view);
|
|
||||||
|
|
||||||
let window = adw::PreferencesWindow::new();
|
|
||||||
window.add(&config_page);
|
|
||||||
window.set_visible_page(&config_page);
|
|
||||||
window.present();
|
|
||||||
}),
|
|
||||||
CoreResponse::HomeView(view) => perftrace("HomeView", || {
|
CoreResponse::HomeView(view) => perftrace("HomeView", || {
|
||||||
let api = api.clone();
|
let api = api.clone();
|
||||||
|
|
||||||
let home = Home::new(api, view);
|
let new_game = Home::new(api, view);
|
||||||
app_window.set_content(&home);
|
window.set_child(Some(&new_game));
|
||||||
}),
|
}),
|
||||||
CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || {
|
CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || {
|
||||||
let api = api.clone();
|
let api = api.clone();
|
||||||
|
@ -31,16 +23,13 @@ fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse)
|
||||||
if playing_field.is_none() {
|
if playing_field.is_none() {
|
||||||
perftrace("creating a new playing field", || {
|
perftrace("creating a new playing field", || {
|
||||||
let field = PlayingField::new(api, view);
|
let field = PlayingField::new(api, view);
|
||||||
app_window.set_content(&field);
|
window.set_child(Some(&field));
|
||||||
*playing_field = Some(field);
|
*playing_field = Some(field);
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
playing_field.as_ref().map(|field| field.update_view(view));
|
playing_field.as_ref().map(|field| field.update_view(view));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
CoreResponse::UpdatedConfigurationView(view) => perftrace("UpdatedConfiguration", || {
|
|
||||||
println!("updated configuration: {:?}", view);
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,9 +65,8 @@ fn main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = adw::Application::builder()
|
let app = gtk::Application::builder()
|
||||||
.application_id("com.luminescent-dreams.kifu-gtk")
|
.application_id("com.luminescent-dreams.kifu-gtk")
|
||||||
.resource_base_path("/com/luminescent-dreams/kifu-gtk")
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
app.connect_activate({
|
app.connect_activate({
|
||||||
|
@ -87,30 +75,20 @@ fn main() {
|
||||||
let (gtk_tx, gtk_rx) =
|
let (gtk_tx, gtk_rx) =
|
||||||
gtk::glib::MainContext::channel::<CoreResponse>(gtk::glib::PRIORITY_DEFAULT);
|
gtk::glib::MainContext::channel::<CoreResponse>(gtk::glib::PRIORITY_DEFAULT);
|
||||||
|
|
||||||
let app_window = AppWindow::new(&app);
|
|
||||||
|
|
||||||
let api = CoreApi {
|
let api = CoreApi {
|
||||||
gtk_tx,
|
gtk_tx,
|
||||||
rt: runtime.clone(),
|
rt: runtime.clone(),
|
||||||
core: core.clone(),
|
core: core.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let action_config = gio::SimpleAction::new("show-config", None);
|
let window = gtk::ApplicationWindow::new(app);
|
||||||
action_config.connect_activate({
|
window.present();
|
||||||
let api = api.clone();
|
|
||||||
move |_, _| {
|
|
||||||
api.dispatch(CoreRequest::OpenConfiguration);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.add_action(&action_config);
|
|
||||||
|
|
||||||
app_window.window.present();
|
|
||||||
|
|
||||||
gtk_rx.attach(None, {
|
gtk_rx.attach(None, {
|
||||||
let api = api.clone();
|
let api = api.clone();
|
||||||
move |message| {
|
move |message| {
|
||||||
perftrace("handle_response", || {
|
perftrace("handle_response", || {
|
||||||
handle_response(api.clone(), &app_window, message)
|
handle_response(api.clone(), window.clone(), message)
|
||||||
});
|
});
|
||||||
Continue(true)
|
Continue(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
use crate::CoreApi;
|
|
||||||
use adw::{prelude::*, subclass::prelude::*};
|
|
||||||
use glib::Object;
|
|
||||||
use kifu_core::{ui::ConfigurationView, ChangeSettingRequest, CoreRequest};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ConfigurationPagePrivate {}
|
|
||||||
|
|
||||||
#[glib::object_subclass]
|
|
||||||
impl ObjectSubclass for ConfigurationPagePrivate {
|
|
||||||
const NAME: &'static str = "Configuration";
|
|
||||||
type Type = ConfigurationPage;
|
|
||||||
type ParentType = adw::PreferencesPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectImpl for ConfigurationPagePrivate {}
|
|
||||||
impl WidgetImpl for ConfigurationPagePrivate {}
|
|
||||||
impl PreferencesPageImpl for ConfigurationPagePrivate {}
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct ConfigurationPage(ObjectSubclass<ConfigurationPagePrivate>)
|
|
||||||
@extends adw::PreferencesPage, gtk::Widget,
|
|
||||||
@implements gtk::Orientable;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigurationPage {
|
|
||||||
pub fn new(api: CoreApi, view: ConfigurationView) -> Self {
|
|
||||||
let s: Self = Object::builder().build();
|
|
||||||
|
|
||||||
let group = adw::PreferencesGroup::builder().build();
|
|
||||||
|
|
||||||
let library_entry = &adw::EntryRow::builder()
|
|
||||||
.name("library-path")
|
|
||||||
.title(view.library.label)
|
|
||||||
.show_apply_button(true)
|
|
||||||
.build();
|
|
||||||
if let Some(path) = view.library.value {
|
|
||||||
library_entry.set_text(&path);
|
|
||||||
}
|
|
||||||
library_entry.connect_apply(move |entry| {
|
|
||||||
api.dispatch(CoreRequest::ChangeSetting(
|
|
||||||
ChangeSettingRequest::LibraryPath(entry.text().into()),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
group.add(library_entry);
|
|
||||||
|
|
||||||
s.add(&group);
|
|
||||||
|
|
||||||
s
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,13 +3,7 @@ use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||||
use kifu_core::ui::GamePreviewElement;
|
use kifu_core::ui::GamePreviewElement;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GamePreviewPrivate {
|
pub struct GamePreviewPrivate;
|
||||||
date: gtk::Label,
|
|
||||||
title: gtk::Label,
|
|
||||||
black_player: gtk::Label,
|
|
||||||
white_player: gtk::Label,
|
|
||||||
result: gtk::Label,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
impl ObjectSubclass for GamePreviewPrivate {
|
impl ObjectSubclass for GamePreviewPrivate {
|
||||||
|
@ -27,26 +21,22 @@ glib::wrapper! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GamePreview {
|
impl GamePreview {
|
||||||
pub fn new() -> GamePreview {
|
pub fn new(element: GamePreviewElement) -> GamePreview {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
s.set_orientation(gtk::Orientation::Horizontal);
|
s.set_orientation(gtk::Orientation::Horizontal);
|
||||||
s.set_homogeneous(true);
|
|
||||||
s.set_hexpand(false);
|
|
||||||
|
|
||||||
s.append(&s.imp().date);
|
println!("game_preview: {:?}", element);
|
||||||
s.append(&s.imp().title);
|
let black_player = match element.black_rank {
|
||||||
s.append(&s.imp().black_player);
|
Some(rank) => format!("{} ({})", element.black_player, rank.to_string()),
|
||||||
s.append(&s.imp().white_player);
|
None => element.black_player,
|
||||||
s.append(&s.imp().result);
|
};
|
||||||
|
let white_player = match element.white_rank {
|
||||||
|
Some(rank) => format!("{} ({})", element.white_player, rank.to_string()),
|
||||||
|
None => element.white_player,
|
||||||
|
};
|
||||||
|
s.append(>k::Label::new(Some(&black_player)));
|
||||||
|
s.append(>k::Label::new(Some(&white_player)));
|
||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_game(&self, element: GamePreviewElement) {
|
|
||||||
self.imp().black_player.set_text(&element.black_player);
|
|
||||||
self.imp().white_player.set_text(&element.white_player);
|
|
||||||
self.imp().title.set_text(&element.name);
|
|
||||||
self.imp().date.set_text(&element.date);
|
|
||||||
self.imp().result.set_text(&element.result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{ui::Library, CoreApi};
|
use crate::ui::GamePreview;
|
||||||
|
use crate::CoreApi;
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||||
use kifu_core::{
|
use kifu_core::{
|
||||||
|
@ -100,54 +101,31 @@ impl Default for HomePrivate {
|
||||||
impl ObjectSubclass for HomePrivate {
|
impl ObjectSubclass for HomePrivate {
|
||||||
const NAME: &'static str = "Home";
|
const NAME: &'static str = "Home";
|
||||||
type Type = Home;
|
type Type = Home;
|
||||||
type ParentType = gtk::Box;
|
type ParentType = gtk::Grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectImpl for HomePrivate {}
|
impl ObjectImpl for HomePrivate {}
|
||||||
impl WidgetImpl for HomePrivate {}
|
impl WidgetImpl for HomePrivate {}
|
||||||
impl BoxImpl for HomePrivate {}
|
impl GridImpl for HomePrivate {}
|
||||||
|
|
||||||
glib::wrapper! {
|
glib::wrapper! {
|
||||||
pub struct Home(ObjectSubclass<HomePrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
|
pub struct Home(ObjectSubclass<HomePrivate>) @extends gtk::Grid, gtk::Widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Home {
|
impl Home {
|
||||||
pub fn new(api: CoreApi, view: HomeView) -> Home {
|
pub fn new(api: CoreApi, view: HomeView) -> Home {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
s.set_spacing(4);
|
|
||||||
s.set_homogeneous(false);
|
|
||||||
s.set_orientation(gtk::Orientation::Vertical);
|
|
||||||
|
|
||||||
let players = gtk::Box::builder()
|
|
||||||
.spacing(4)
|
|
||||||
.orientation(gtk::Orientation::Horizontal)
|
|
||||||
.build();
|
|
||||||
s.append(&players);
|
|
||||||
|
|
||||||
let black_player = PlayerDataEntry::new(view.black_player);
|
let black_player = PlayerDataEntry::new(view.black_player);
|
||||||
players.append(&black_player);
|
s.attach(&black_player, 1, 1, 1, 1);
|
||||||
*s.imp().black_player.borrow_mut() = Some(black_player.clone());
|
*s.imp().black_player.borrow_mut() = Some(black_player.clone());
|
||||||
|
|
||||||
let white_player = PlayerDataEntry::new(view.white_player);
|
let white_player = PlayerDataEntry::new(view.white_player);
|
||||||
players.append(&white_player);
|
s.attach(&white_player, 2, 1, 1, 1);
|
||||||
*s.imp().white_player.borrow_mut() = Some(white_player.clone());
|
*s.imp().white_player.borrow_mut() = Some(white_player.clone());
|
||||||
|
|
||||||
let new_game_button = gtk::Button::builder()
|
let new_game_button = gtk::Button::builder().label(&view.start_game.label).build();
|
||||||
.css_classes(vec!["suggested-action"])
|
s.attach(&new_game_button, 2, 2, 1, 1);
|
||||||
.label(&view.start_game.label)
|
|
||||||
.build();
|
|
||||||
s.append(&new_game_button);
|
|
||||||
|
|
||||||
let library = Library::new();
|
|
||||||
let library_view = gtk::ScrolledWindow::builder()
|
|
||||||
.hscrollbar_policy(gtk::PolicyType::Never)
|
|
||||||
.min_content_width(360)
|
|
||||||
.vexpand(true)
|
|
||||||
.hexpand(true)
|
|
||||||
.child(&library)
|
|
||||||
.build();
|
|
||||||
s.append(&library_view);
|
|
||||||
|
|
||||||
library.set_games(view.games);
|
|
||||||
|
|
||||||
new_game_button.connect_clicked({
|
new_game_button.connect_clicked({
|
||||||
move |_| {
|
move |_| {
|
||||||
|
@ -161,6 +139,12 @@ impl Home {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let game_list = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
|
s.attach(&game_list, 1, 3, 2, 1);
|
||||||
|
view.games
|
||||||
|
.iter()
|
||||||
|
.for_each(|game_preview| game_list.append(&GamePreview::new(game_preview.clone())));
|
||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,166 +0,0 @@
|
||||||
use crate::ui::GamePreview;
|
|
||||||
use adw::{prelude::*, subclass::prelude::*};
|
|
||||||
use glib::Object;
|
|
||||||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
|
||||||
use kifu_core::ui::GamePreviewElement;
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct GameObjectPrivate {
|
|
||||||
game: Rc<RefCell<Option<GamePreviewElement>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[glib::object_subclass]
|
|
||||||
impl ObjectSubclass for GameObjectPrivate {
|
|
||||||
const NAME: &'static str = "GameObject";
|
|
||||||
type Type = GameObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectImpl for GameObjectPrivate {}
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct GameObject(ObjectSubclass<GameObjectPrivate>);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GameObject {
|
|
||||||
pub fn new(game: GamePreviewElement) -> Self {
|
|
||||||
let s: Self = Object::builder().build();
|
|
||||||
*s.imp().game.borrow_mut() = Some(game);
|
|
||||||
s
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn game(&self) -> Option<GamePreviewElement> {
|
|
||||||
self.imp().game.borrow().clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LibraryPrivate {
|
|
||||||
model: gio::ListStore,
|
|
||||||
list_view: gtk::ColumnView,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LibraryPrivate {
|
|
||||||
fn default() -> Self {
|
|
||||||
let vector: Vec<GameObject> = vec![];
|
|
||||||
let model = gio::ListStore::new(glib::types::Type::OBJECT);
|
|
||||||
model.extend_from_slice(&vector);
|
|
||||||
|
|
||||||
/*
|
|
||||||
let factory = gtk::SignalListItemFactory::new();
|
|
||||||
|
|
||||||
factory.connect_setup(move |_, list_item| {
|
|
||||||
let preview = GamePreview::new();
|
|
||||||
list_item
|
|
||||||
.downcast_ref::<gtk::ListItem>()
|
|
||||||
.expect("Needs to be a ListItem")
|
|
||||||
.set_child(Some(&preview));
|
|
||||||
});
|
|
||||||
factory.connect_bind(move |_, list_item| {
|
|
||||||
let game_element = list_item
|
|
||||||
.downcast_ref::<gtk::ListItem>()
|
|
||||||
.expect("Needs to be ListItem")
|
|
||||||
.item()
|
|
||||||
.and_downcast::<GameObject>()
|
|
||||||
.expect("The item has to be a GameObject.");
|
|
||||||
|
|
||||||
let preview = list_item
|
|
||||||
.downcast_ref::<gtk::ListItem>()
|
|
||||||
.expect("Needs to be ListItem")
|
|
||||||
.child()
|
|
||||||
.and_downcast::<GamePreview>()
|
|
||||||
.expect("The child has to be a GamePreview object.");
|
|
||||||
|
|
||||||
match game_element.game() {
|
|
||||||
Some(game) => preview.set_game(game),
|
|
||||||
None => (),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
let selection_model = gtk::NoSelection::new(Some(model.clone()));
|
|
||||||
let list_view = gtk::ColumnView::builder().model(&selection_model).build();
|
|
||||||
|
|
||||||
fn make_factory<F>(bind: F) -> gtk::SignalListItemFactory
|
|
||||||
where
|
|
||||||
F: Fn(GamePreviewElement) -> String + 'static,
|
|
||||||
{
|
|
||||||
let factory = gtk::SignalListItemFactory::new();
|
|
||||||
factory.connect_setup(|_, list_item| {
|
|
||||||
list_item
|
|
||||||
.downcast_ref::<gtk::ListItem>()
|
|
||||||
.unwrap()
|
|
||||||
.set_child(Some(
|
|
||||||
>k::Label::builder()
|
|
||||||
.halign(gtk::Align::Start)
|
|
||||||
.ellipsize(pango::EllipsizeMode::End)
|
|
||||||
.build(),
|
|
||||||
))
|
|
||||||
});
|
|
||||||
factory.connect_bind(move |_, list_item| {
|
|
||||||
let list_item = list_item.downcast_ref::<gtk::ListItem>().unwrap();
|
|
||||||
let game = list_item.item().and_downcast::<GameObject>().unwrap();
|
|
||||||
let preview = list_item.child().and_downcast::<gtk::Label>().unwrap();
|
|
||||||
match game.game() {
|
|
||||||
Some(game) => preview.set_text(&bind(game)),
|
|
||||||
None => (),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
factory
|
|
||||||
}
|
|
||||||
|
|
||||||
list_view.append_column(>k::ColumnViewColumn::new(
|
|
||||||
Some("date"),
|
|
||||||
Some(make_factory(|g| g.date)),
|
|
||||||
));
|
|
||||||
list_view.append_column(>k::ColumnViewColumn::new(
|
|
||||||
Some("title"),
|
|
||||||
Some(make_factory(|g| g.name)),
|
|
||||||
));
|
|
||||||
list_view.append_column(>k::ColumnViewColumn::new(
|
|
||||||
Some("black"),
|
|
||||||
Some(make_factory(|g| g.black_player)),
|
|
||||||
));
|
|
||||||
list_view.append_column(>k::ColumnViewColumn::new(
|
|
||||||
Some("white"),
|
|
||||||
Some(make_factory(|g| g.white_player)),
|
|
||||||
));
|
|
||||||
list_view.append_column(>k::ColumnViewColumn::new(
|
|
||||||
Some("result"),
|
|
||||||
Some(make_factory(|g| g.result)),
|
|
||||||
));
|
|
||||||
|
|
||||||
Self { model, list_view }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[glib::object_subclass]
|
|
||||||
impl ObjectSubclass for LibraryPrivate {
|
|
||||||
const NAME: &'static str = "Library";
|
|
||||||
type Type = Library;
|
|
||||||
type ParentType = adw::Bin;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectImpl for LibraryPrivate {}
|
|
||||||
impl WidgetImpl for LibraryPrivate {}
|
|
||||||
impl BinImpl for LibraryPrivate {}
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct Library(ObjectSubclass<LibraryPrivate>) @extends adw::Bin, gtk::Widget;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Library {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let s: Self = Object::builder().build();
|
|
||||||
|
|
||||||
s.set_child(Some(&s.imp().list_view));
|
|
||||||
s
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_games(&self, games: Vec<GamePreviewElement>) {
|
|
||||||
let games = games
|
|
||||||
.into_iter()
|
|
||||||
.map(|g| GameObject::new(g))
|
|
||||||
.collect::<Vec<GameObject>>();
|
|
||||||
self.imp().model.extend_from_slice(&games);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +1,9 @@
|
||||||
use adw::prelude::*;
|
|
||||||
use gio::resources_lookup_data;
|
|
||||||
use glib::IsA;
|
|
||||||
use gtk::{prelude::*, STYLE_PROVIDER_PRIORITY_USER};
|
|
||||||
|
|
||||||
mod chat;
|
mod chat;
|
||||||
pub use chat::Chat;
|
pub use chat::Chat;
|
||||||
|
|
||||||
mod config;
|
|
||||||
pub use config::ConfigurationPage;
|
|
||||||
|
|
||||||
mod game_preview;
|
mod game_preview;
|
||||||
pub use game_preview::GamePreview;
|
pub use game_preview::GamePreview;
|
||||||
|
|
||||||
mod library;
|
|
||||||
pub use library::Library;
|
|
||||||
|
|
||||||
mod player_card;
|
mod player_card;
|
||||||
pub use player_card::PlayerCard;
|
pub use player_card::PlayerCard;
|
||||||
|
|
||||||
|
@ -29,72 +18,3 @@ pub use board::Board;
|
||||||
|
|
||||||
#[cfg(feature = "screenplay")]
|
#[cfg(feature = "screenplay")]
|
||||||
pub use playing_field::playing_field_view;
|
pub use playing_field::playing_field_view;
|
||||||
|
|
||||||
pub struct AppWindow {
|
|
||||||
pub window: adw::ApplicationWindow,
|
|
||||||
pub header: adw::HeaderBar,
|
|
||||||
pub content: adw::Bin,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppWindow {
|
|
||||||
pub fn new(app: &adw::Application) -> Self {
|
|
||||||
let window = adw::ApplicationWindow::builder()
|
|
||||||
.application(app)
|
|
||||||
.width_request(800)
|
|
||||||
.height_request(500)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let stylesheet = String::from_utf8(
|
|
||||||
resources_lookup_data(
|
|
||||||
"/com/luminescent-dreams/kifu-gtk/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 header = adw::HeaderBar::builder()
|
|
||||||
.title_widget(>k::Label::new(Some("Kifu")))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let app_menu = gio::Menu::new();
|
|
||||||
let menu_item = gio::MenuItem::new(Some("Configuration"), Some("app.show-config"));
|
|
||||||
app_menu.append_item(&menu_item);
|
|
||||||
|
|
||||||
let hamburger = gtk::MenuButton::builder()
|
|
||||||
.icon_name("open-menu-symbolic")
|
|
||||||
.build();
|
|
||||||
hamburger.set_menu_model(Some(&app_menu));
|
|
||||||
|
|
||||||
header.pack_end(&hamburger);
|
|
||||||
|
|
||||||
let content = adw::Bin::builder().css_classes(vec!["content"]).build();
|
|
||||||
content.set_child(Some(
|
|
||||||
&adw::StatusPage::builder().title("Nothing here").build(),
|
|
||||||
));
|
|
||||||
|
|
||||||
let layout = gtk::Box::builder()
|
|
||||||
.orientation(gtk::Orientation::Vertical)
|
|
||||||
.build();
|
|
||||||
layout.append(&header);
|
|
||||||
layout.append(&content);
|
|
||||||
|
|
||||||
window.set_content(Some(&layout));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
window,
|
|
||||||
header,
|
|
||||||
content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_content(&self, content: &impl IsA<gtk::Widget>) {
|
|
||||||
self.content.set_child(Some(content));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.71.1"
|
channel = "1.68.2"
|
||||||
targets = [ "wasm32-unknown-unknown" ]
|
targets = [ "wasm32-unknown-unknown" ]
|
||||||
|
|
|
@ -24,16 +24,6 @@ pub enum Date {
|
||||||
Date(chrono::NaiveDate),
|
Date(chrono::NaiveDate),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Date {
|
|
||||||
pub fn to_string(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Date::Year(y) => format!("{}", y),
|
|
||||||
Date::YearMonth(y, m) => format!("{}-{}", y, m),
|
|
||||||
Date::Date(date) => format!("{}-{}-{}", date.year(), date.month(), date.day()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
impl TryFrom<&str> for Date {
|
impl TryFrom<&str> for Date {
|
||||||
type Error = String;
|
type Error = String;
|
||||||
|
|
|
@ -105,9 +105,6 @@ impl TryFrom<Tree> for Game {
|
||||||
};
|
};
|
||||||
let mut info = GameInfo::default();
|
let mut info = GameInfo::default();
|
||||||
info.app_name = tree.root.find_prop("AP").map(|prop| prop.values[0].clone());
|
info.app_name = tree.root.find_prop("AP").map(|prop| prop.values[0].clone());
|
||||||
|
|
||||||
info.game_name = tree.root.find_prop("GN").map(|prop| prop.values[0].clone());
|
|
||||||
|
|
||||||
info.black_player = tree.root.find_prop("PB").map(|prop| prop.values.join(", "));
|
info.black_player = tree.root.find_prop("PB").map(|prop| prop.values.join(", "));
|
||||||
|
|
||||||
info.black_rank = tree
|
info.black_rank = tree
|
||||||
|
@ -239,7 +236,6 @@ pub enum GameResult {
|
||||||
Draw,
|
Draw,
|
||||||
Black(Win),
|
Black(Win),
|
||||||
White(Win),
|
White(Win),
|
||||||
Unknown(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for GameResult {
|
impl TryFrom<&str> for GameResult {
|
||||||
|
@ -254,7 +250,7 @@ impl TryFrom<&str> for GameResult {
|
||||||
let res = match parts[0].to_ascii_lowercase().as_str() {
|
let res = match parts[0].to_ascii_lowercase().as_str() {
|
||||||
"b" => GameResult::Black,
|
"b" => GameResult::Black,
|
||||||
"w" => GameResult::White,
|
"w" => GameResult::White,
|
||||||
_ => return Ok(GameResult::Unknown(parts[0].to_owned())),
|
_ => panic!("unknown result format"),
|
||||||
};
|
};
|
||||||
match parts[1].to_ascii_lowercase().as_str() {
|
match parts[1].to_ascii_lowercase().as_str() {
|
||||||
"r" | "resign" => Ok(res(Win::Resignation)),
|
"r" | "resign" => Ok(res(Win::Resignation)),
|
||||||
|
|
Loading…
Reference in New Issue