Compare commits
40 Commits
62b8e90c85
...
b9425af234
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | b9425af234 | |
Savanni D'Gerinel | 4bd6388913 | |
Savanni D'Gerinel | 8dad470440 | |
Savanni D'Gerinel | e0fb7714d0 | |
Savanni D'Gerinel | b866249c9d | |
Savanni D'Gerinel | 2a6a5de5e1 | |
Savanni D'Gerinel | 1489121877 | |
Savanni D'Gerinel | 562d4871a1 | |
Savanni D'Gerinel | 16c8dcb682 | |
Savanni D'Gerinel | cc828c417a | |
Savanni D'Gerinel | 784f3ff7f4 | |
Savanni D'Gerinel | 5439e2ac04 | |
Savanni D'Gerinel | 0bf6e079a2 | |
Savanni D'Gerinel | 3998538e88 | |
Savanni D'Gerinel | 793cd67218 | |
Savanni D'Gerinel | ff13ff3c0e | |
Savanni D'Gerinel | cc3ad372e6 | |
Savanni D'Gerinel | 3c063af525 | |
Savanni D'Gerinel | aa64bf4c7e | |
Savanni D'Gerinel | f75e0d4d65 | |
Savanni D'Gerinel | d8534a08eb | |
Savanni D'Gerinel | e5d0b7d20f | |
Savanni D'Gerinel | e9ffab1187 | |
Savanni D'Gerinel | a584fb4de3 | |
Savanni D'Gerinel | e3f4ca246d | |
Savanni D'Gerinel | 07b7351501 | |
Savanni D'Gerinel | 70a295d4b1 | |
Savanni D'Gerinel | 5478d388cb | |
Savanni D'Gerinel | e203b17c8b | |
Savanni D'Gerinel | 69583dfd64 | |
Savanni D'Gerinel | d59c2585db | |
Savanni D'Gerinel | a6fcbfac71 | |
Savanni D'Gerinel | 4f940099da | |
Savanni D'Gerinel | f4735dd16b | |
Savanni D'Gerinel | b662ac519a | |
Savanni D'Gerinel | efec8dfe5a | |
Savanni D'Gerinel | 0765d94a5e | |
Savanni D'Gerinel | 40b33797f3 | |
Savanni D'Gerinel | 24e88da8e2 | |
Savanni D'Gerinel | 456d872b40 |
|
@ -3,3 +3,4 @@ target
|
|||
node_modules
|
||||
dist
|
||||
result
|
||||
*.tgz
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,8 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"changeset",
|
||||
"config",
|
||||
"config-derive",
|
||||
"coordinates",
|
||||
"cyberpunk-splash",
|
||||
"dashboard",
|
||||
|
@ -8,6 +10,7 @@ members = [
|
|||
"flow",
|
||||
"fluent-ergonomics",
|
||||
"geo-types",
|
||||
"gm-control-panel",
|
||||
"hex-grid",
|
||||
"ifc",
|
||||
"kifu/core",
|
||||
|
|
17
build.sh
17
build.sh
|
@ -4,6 +4,8 @@ set -euo pipefail
|
|||
|
||||
RUST_ALL_TARGETS=(
|
||||
"changeset"
|
||||
"config"
|
||||
"config-derive"
|
||||
"coordinates"
|
||||
"cyberpunk-splash"
|
||||
"dashboard"
|
||||
|
@ -11,6 +13,7 @@ RUST_ALL_TARGETS=(
|
|||
"flow"
|
||||
"fluent-ergonomics"
|
||||
"geo-types"
|
||||
"gm-control-panel"
|
||||
"hex-grid"
|
||||
"ifc"
|
||||
"kifu-core"
|
||||
|
@ -29,6 +32,16 @@ build_rust_targets() {
|
|||
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`
|
||||
|
||||
if [ -z "${TARGET-}" ]; then
|
||||
|
@ -45,7 +58,9 @@ if [ "${CMD}" == "clean" ]; then
|
|||
fi
|
||||
|
||||
for cmd in $CMD; do
|
||||
if [ "${TARGET}" == "all" ]; then
|
||||
if [ "${CMD}" == "dist" ]; then
|
||||
build_dist $TARGET
|
||||
elif [ "${TARGET}" == "all" ]; then
|
||||
build_rust_targets $cmd ${RUST_ALL_TARGETS[*]}
|
||||
else
|
||||
build_rust_targets $cmd $TARGET
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
[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" ] }
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
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()
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
[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" }
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
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())
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.0
|
||||
Name=dashboard
|
||||
Comment=My personal system dashboard
|
||||
Exec=dashboard
|
|
@ -0,0 +1,11 @@
|
|||
#!/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/
|
||||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fn main() {
|
||||
glib_build_tools::compile_resources(
|
||||
"resources",
|
||||
"resources/gresources.xml",
|
||||
"com.luminescent-dreams.gm-control-panel.gresource",
|
||||
);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/com/luminescent-dreams/gm-control-panel/">
|
||||
<file>style.css</file>
|
||||
</gresource>
|
||||
</gresources>
|
|
@ -0,0 +1,6 @@
|
|||
.playlist-card {
|
||||
margin: 8px;
|
||||
padding: 8px;
|
||||
min-width: 100px;
|
||||
min-height: 100px;
|
||||
}
|
|
@ -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<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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<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();
|
||||
}
|
|
@ -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<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))
|
||||
}
|
||||
}
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
197
ifc/src/lib.rs
197
ifc/src/lib.rs
|
@ -250,8 +250,12 @@ impl From<chrono::NaiveDate> for IFC {
|
|||
{
|
||||
days = days - 1;
|
||||
}
|
||||
let month: u8 = (days / 28).try_into().unwrap();
|
||||
let day: u8 = (days % 28).try_into().unwrap();
|
||||
let mut month: u8 = (days / 28).try_into().unwrap();
|
||||
let mut day: u8 = (days % 28).try_into().unwrap();
|
||||
if day == 0 {
|
||||
month = month - 1;
|
||||
day = 28;
|
||||
}
|
||||
Self::Day(Day {
|
||||
year: date.year(),
|
||||
month: month + 1,
|
||||
|
@ -268,167 +272,6 @@ 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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -612,10 +455,34 @@ mod tests {
|
|||
IFC::from(NaiveDate::from_ymd_opt(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!(
|
||||
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 29).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!(
|
||||
IFC::from(NaiveDate::from_ymd_opt(12022, 2, 26).unwrap()),
|
||||
IFC::ymd(12022, 3, 1).unwrap()
|
||||
|
@ -644,6 +511,10 @@ mod tests {
|
|||
IFC::from(NaiveDate::from_ymd_opt(12022, 8, 13).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!(
|
||||
IFC::from(NaiveDate::from_ymd_opt(12022, 9, 10).unwrap()),
|
||||
IFC::ymd(12022, 10, 1).unwrap()
|
||||
|
|
|
@ -7,6 +7,8 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4" }
|
||||
config = { path = "../../config" }
|
||||
config-derive = { path = "../../config-derive" }
|
||||
sgf = { path = "../../sgf" }
|
||||
grid = { version = "0.9" }
|
||||
serde_json = { version = "1" }
|
||||
|
|
|
@ -1,23 +1,34 @@
|
|||
use crate::{
|
||||
types::{AppState, GameState, Player, Rank},
|
||||
ui::{home, playing_field, HomeView, PlayingFieldView},
|
||||
Config, DatabasePath,
|
||||
types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank},
|
||||
ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use typeshare::typeshare;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[typeshare]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum CoreRequest {
|
||||
ChangeSetting(ChangeSettingRequest),
|
||||
CreateGame(CreateGameRequest),
|
||||
Home,
|
||||
OpenConfiguration,
|
||||
PlayingField,
|
||||
PlayStone(PlayStoneRequest),
|
||||
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)]
|
||||
#[typeshare]
|
||||
pub struct PlayStoneRequest {
|
||||
|
@ -58,46 +69,42 @@ impl From<HotseatPlayerRequest> for Player {
|
|||
#[typeshare]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
pub enum CoreResponse {
|
||||
ConfigurationView(ConfigurationView),
|
||||
HomeView(HomeView),
|
||||
PlayingFieldView(PlayingFieldView),
|
||||
UpdatedConfigurationView(ConfigurationView),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CoreApp {
|
||||
config: Config,
|
||||
config: Arc<RwLock<Config>>,
|
||||
state: Arc<RwLock<AppState>>,
|
||||
}
|
||||
|
||||
impl CoreApp {
|
||||
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 db_path: DatabasePath = config.get();
|
||||
let db_path: DatabasePath = config.get().unwrap();
|
||||
let state = Arc::new(RwLock::new(AppState::new(db_path)));
|
||||
|
||||
println!("config: {:?}", config);
|
||||
println!("games database: {:?}", state.read().unwrap().database.len());
|
||||
|
||||
Self { config, state }
|
||||
Self {
|
||||
config: Arc::new(RwLock::new(config)),
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
||||
match request {
|
||||
/*
|
||||
CoreRequest::LaunchScreen => {
|
||||
let app_state = self.state.read().unwrap();
|
||||
|
||||
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.
|
||||
- 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::ChangeSetting(request) => match request {
|
||||
ChangeSettingRequest::LibraryPath(path) => {
|
||||
let mut config = self.config.write().unwrap();
|
||||
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
|
||||
path,
|
||||
))));
|
||||
CoreResponse::UpdatedConfigurationView(configuration(&config))
|
||||
}
|
||||
},
|
||||
CoreRequest::CreateGame(create_request) => {
|
||||
let mut app_state = self.state.write().unwrap();
|
||||
let white_player = {
|
||||
|
@ -121,6 +128,9 @@ impl CoreApp {
|
|||
CoreRequest::Home => {
|
||||
CoreResponse::HomeView(home(self.state.read().unwrap().database.all_games()))
|
||||
}
|
||||
CoreRequest::OpenConfiguration => {
|
||||
CoreResponse::ConfigurationView(configuration(&self.config.read().unwrap()))
|
||||
}
|
||||
CoreRequest::PlayingField => {
|
||||
let app_state = self.state.read().unwrap();
|
||||
let game = app_state.game.as_ref().unwrap();
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
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,11 +39,16 @@ impl Database {
|
|||
.unwrap()
|
||||
.read_to_string(&mut buffer)
|
||||
.unwrap();
|
||||
for sgf in parse_sgf(&buffer).unwrap() {
|
||||
match sgf {
|
||||
Game::Go(game) => games.push(game),
|
||||
Game::Unsupported(_) => {}
|
||||
match parse_sgf(&buffer) {
|
||||
Ok(sgfs) => {
|
||||
for sgf in sgfs {
|
||||
match sgf {
|
||||
Game::Go(game) => games.push(game),
|
||||
Game::Unsupported(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
#[macro_use]
|
||||
extern crate config_derive;
|
||||
|
||||
mod api;
|
||||
pub use api::{
|
||||
CoreApp, CoreRequest, CoreResponse, CreateGameRequest, HotseatPlayerRequest, PlayerInfoRequest,
|
||||
ChangeSettingRequest, CoreApp, CoreRequest, CoreResponse, CreateGameRequest,
|
||||
HotseatPlayerRequest, PlayerInfoRequest,
|
||||
};
|
||||
|
||||
mod board;
|
||||
pub use board::*;
|
||||
|
||||
/*
|
||||
mod config;
|
||||
pub use config::*;
|
||||
*/
|
||||
|
||||
mod database;
|
||||
|
||||
|
|
|
@ -1,14 +1,40 @@
|
|||
use crate::{
|
||||
api::PlayStoneRequest,
|
||||
board::{Board, Coordinate},
|
||||
config::DatabasePath,
|
||||
database::Database,
|
||||
};
|
||||
use config::define_config;
|
||||
use config_derive::ConfigOption;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
use thiserror::Error;
|
||||
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)]
|
||||
pub enum BoardError {
|
||||
#[error("Position is invalid")]
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
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: (),
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
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,36 +1,70 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use sgf::{
|
||||
go::{Game, Rank},
|
||||
Date,
|
||||
};
|
||||
use sgf::go::{Game, GameResult, Win};
|
||||
use typeshare::typeshare;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[typeshare]
|
||||
pub struct GamePreviewElement {
|
||||
pub date: Vec<Date>,
|
||||
pub date: String,
|
||||
pub name: String,
|
||||
pub black_player: String,
|
||||
pub black_rank: Option<Rank>,
|
||||
pub white_player: String,
|
||||
pub white_rank: Option<Rank>,
|
||||
pub result: String,
|
||||
}
|
||||
|
||||
impl 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 {
|
||||
date: game.info.date.clone(),
|
||||
black_player: game
|
||||
date: game
|
||||
.info
|
||||
.black_player
|
||||
.clone()
|
||||
.unwrap_or("black_player".to_owned()),
|
||||
black_rank: game.info.black_rank.clone(),
|
||||
white_player: game
|
||||
.info
|
||||
.white_player
|
||||
.clone()
|
||||
.unwrap_or("white_player".to_owned()),
|
||||
white_rank: game.info.white_rank.clone(),
|
||||
.date
|
||||
.first()
|
||||
.map(|dt| dt.to_string())
|
||||
.unwrap_or("".to_owned()),
|
||||
name,
|
||||
black_player,
|
||||
white_player,
|
||||
result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,31 @@
|
|||
pub mod action;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typeshare::typeshare;
|
||||
|
||||
pub mod game_preview;
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
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;
|
||||
pub use elements::{action::Action, game_preview::GamePreviewElement, menu::Menu};
|
||||
pub use elements::{game_preview::GamePreviewElement, menu::Menu, Action, Field, Toggle};
|
||||
|
||||
mod playing_field;
|
||||
pub use playing_field::{playing_field, PlayingFieldView};
|
||||
|
||||
// mod launch_screen;
|
||||
// pub use launch_screen::{launch_screen, LaunchScreenView};
|
||||
|
||||
mod home;
|
||||
pub use home::{home, HomeView, HotseatPlayerElement, PlayerElement};
|
||||
|
||||
|
|
|
@ -9,14 +9,16 @@ screenplay = []
|
|||
# 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" ] }
|
||||
cairo-rs = { version = "0.17" }
|
||||
gio = { version = "0.17" }
|
||||
glib = { version = "0.17" }
|
||||
gtk = { version = "0.6", package = "gtk4" }
|
||||
gtk = { version = "0.6", package = "gtk4", features = [ "v4_8" ] }
|
||||
image = { version = "0.24" }
|
||||
kifu-core = { path = "../core" }
|
||||
pango = { version = "*" }
|
||||
sgf = { path = "../../sgf" }
|
||||
tokio = { version = "1.26", features = [ "full" ] }
|
||||
screenplay = { path = "../../screenplay" }
|
||||
|
||||
[build-dependencies]
|
||||
glib-build-tools = "0.17"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
fn main() {
|
||||
glib_build_tools::compile_resources(
|
||||
&["resources"],
|
||||
"resources/resources.gresources.xml",
|
||||
"resources/gresources.xml",
|
||||
"com.luminescent-dreams.kifu-gtk.gresource",
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1 +1,8 @@
|
|||
{"Me":{"name":"Savanni","rank":{"Kyu":10}},"DatabasePath":"kifu/core/fixtures/five_games"}
|
||||
<<<<<<< HEAD
|
||||
{
|
||||
"Me":{
|
||||
"name":"Savanni",
|
||||
"rank":{"Kyu":10}
|
||||
},
|
||||
"DatabasePath": "kifu/core/fixtures/five_games/"
|
||||
}
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
<gresources>
|
||||
<gresource prefix="/com/luminescent-dreams/kifu-gtk/">
|
||||
<file>wood_texture.jpg</file>
|
||||
<file>style.css</file>
|
||||
</gresource>
|
||||
</gresources>
|
|
@ -0,0 +1,3 @@
|
|||
.content {
|
||||
padding: 8px;
|
||||
}
|
|
@ -1,20 +1,28 @@
|
|||
use gtk::prelude::*;
|
||||
use adw::prelude::*;
|
||||
use kifu_core::{CoreApp, CoreRequest, CoreResponse};
|
||||
use kifu_gtk::{
|
||||
perftrace,
|
||||
ui::{Home, PlayingField},
|
||||
ui::{AppWindow, ConfigurationPage, Home, PlayingField},
|
||||
CoreApi,
|
||||
};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
fn handle_response(api: CoreApi, window: gtk::ApplicationWindow, message: CoreResponse) {
|
||||
fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse) {
|
||||
let playing_field = Arc::new(RwLock::new(None));
|
||||
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", || {
|
||||
let api = api.clone();
|
||||
|
||||
let new_game = Home::new(api, view);
|
||||
window.set_child(Some(&new_game));
|
||||
let home = Home::new(api, view);
|
||||
app_window.set_content(&home);
|
||||
}),
|
||||
CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || {
|
||||
let api = api.clone();
|
||||
|
@ -23,13 +31,16 @@ fn handle_response(api: CoreApi, window: gtk::ApplicationWindow, message: CoreRe
|
|||
if playing_field.is_none() {
|
||||
perftrace("creating a new playing field", || {
|
||||
let field = PlayingField::new(api, view);
|
||||
window.set_child(Some(&field));
|
||||
app_window.set_content(&field);
|
||||
*playing_field = Some(field);
|
||||
})
|
||||
} else {
|
||||
playing_field.as_ref().map(|field| field.update_view(view));
|
||||
}
|
||||
}),
|
||||
CoreResponse::UpdatedConfigurationView(view) => perftrace("UpdatedConfiguration", || {
|
||||
println!("updated configuration: {:?}", view);
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,8 +76,9 @@ fn main() {
|
|||
}
|
||||
});
|
||||
|
||||
let app = gtk::Application::builder()
|
||||
let app = adw::Application::builder()
|
||||
.application_id("com.luminescent-dreams.kifu-gtk")
|
||||
.resource_base_path("/com/luminescent-dreams/kifu-gtk")
|
||||
.build();
|
||||
|
||||
app.connect_activate({
|
||||
|
@ -75,20 +87,30 @@ fn main() {
|
|||
let (gtk_tx, gtk_rx) =
|
||||
gtk::glib::MainContext::channel::<CoreResponse>(gtk::glib::PRIORITY_DEFAULT);
|
||||
|
||||
let app_window = AppWindow::new(&app);
|
||||
|
||||
let api = CoreApi {
|
||||
gtk_tx,
|
||||
rt: runtime.clone(),
|
||||
core: core.clone(),
|
||||
};
|
||||
|
||||
let window = gtk::ApplicationWindow::new(app);
|
||||
window.present();
|
||||
let action_config = gio::SimpleAction::new("show-config", None);
|
||||
action_config.connect_activate({
|
||||
let api = api.clone();
|
||||
move |_, _| {
|
||||
api.dispatch(CoreRequest::OpenConfiguration);
|
||||
}
|
||||
});
|
||||
app.add_action(&action_config);
|
||||
|
||||
app_window.window.present();
|
||||
|
||||
gtk_rx.attach(None, {
|
||||
let api = api.clone();
|
||||
move |message| {
|
||||
perftrace("handle_response", || {
|
||||
handle_response(api.clone(), window.clone(), message)
|
||||
handle_response(api.clone(), &app_window, message)
|
||||
});
|
||||
Continue(true)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
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,7 +3,13 @@ use gtk::{glib, prelude::*, subclass::prelude::*};
|
|||
use kifu_core::ui::GamePreviewElement;
|
||||
|
||||
#[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]
|
||||
impl ObjectSubclass for GamePreviewPrivate {
|
||||
|
@ -21,22 +27,26 @@ glib::wrapper! {
|
|||
}
|
||||
|
||||
impl GamePreview {
|
||||
pub fn new(element: GamePreviewElement) -> GamePreview {
|
||||
pub fn new() -> GamePreview {
|
||||
let s: Self = Object::builder().build();
|
||||
s.set_orientation(gtk::Orientation::Horizontal);
|
||||
s.set_homogeneous(true);
|
||||
s.set_hexpand(false);
|
||||
|
||||
println!("game_preview: {:?}", element);
|
||||
let black_player = match element.black_rank {
|
||||
Some(rank) => format!("{} ({})", element.black_player, rank.to_string()),
|
||||
None => element.black_player,
|
||||
};
|
||||
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.append(&s.imp().date);
|
||||
s.append(&s.imp().title);
|
||||
s.append(&s.imp().black_player);
|
||||
s.append(&s.imp().white_player);
|
||||
s.append(&s.imp().result);
|
||||
|
||||
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,5 +1,4 @@
|
|||
use crate::ui::GamePreview;
|
||||
use crate::CoreApi;
|
||||
use crate::{ui::Library, CoreApi};
|
||||
use glib::Object;
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||
use kifu_core::{
|
||||
|
@ -101,31 +100,54 @@ impl Default for HomePrivate {
|
|||
impl ObjectSubclass for HomePrivate {
|
||||
const NAME: &'static str = "Home";
|
||||
type Type = Home;
|
||||
type ParentType = gtk::Grid;
|
||||
type ParentType = gtk::Box;
|
||||
}
|
||||
|
||||
impl ObjectImpl for HomePrivate {}
|
||||
impl WidgetImpl for HomePrivate {}
|
||||
impl GridImpl for HomePrivate {}
|
||||
impl BoxImpl for HomePrivate {}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Home(ObjectSubclass<HomePrivate>) @extends gtk::Grid, gtk::Widget;
|
||||
pub struct Home(ObjectSubclass<HomePrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
|
||||
}
|
||||
|
||||
impl Home {
|
||||
pub fn new(api: CoreApi, view: HomeView) -> Home {
|
||||
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);
|
||||
s.attach(&black_player, 1, 1, 1, 1);
|
||||
players.append(&black_player);
|
||||
*s.imp().black_player.borrow_mut() = Some(black_player.clone());
|
||||
|
||||
let white_player = PlayerDataEntry::new(view.white_player);
|
||||
s.attach(&white_player, 2, 1, 1, 1);
|
||||
players.append(&white_player);
|
||||
*s.imp().white_player.borrow_mut() = Some(white_player.clone());
|
||||
|
||||
let new_game_button = gtk::Button::builder().label(&view.start_game.label).build();
|
||||
s.attach(&new_game_button, 2, 2, 1, 1);
|
||||
let new_game_button = gtk::Button::builder()
|
||||
.css_classes(vec!["suggested-action"])
|
||||
.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({
|
||||
move |_| {
|
||||
|
@ -139,12 +161,6 @@ 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
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,9 +1,20 @@
|
|||
use adw::prelude::*;
|
||||
use gio::resources_lookup_data;
|
||||
use glib::IsA;
|
||||
use gtk::{prelude::*, STYLE_PROVIDER_PRIORITY_USER};
|
||||
|
||||
mod chat;
|
||||
pub use chat::Chat;
|
||||
|
||||
mod config;
|
||||
pub use config::ConfigurationPage;
|
||||
|
||||
mod game_preview;
|
||||
pub use game_preview::GamePreview;
|
||||
|
||||
mod library;
|
||||
pub use library::Library;
|
||||
|
||||
mod player_card;
|
||||
pub use player_card::PlayerCard;
|
||||
|
||||
|
@ -18,3 +29,72 @@ pub use board::Board;
|
|||
|
||||
#[cfg(feature = "screenplay")]
|
||||
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]
|
||||
channel = "1.68.2"
|
||||
channel = "1.71.1"
|
||||
targets = [ "wasm32-unknown-unknown" ]
|
||||
|
|
|
@ -24,6 +24,16 @@ pub enum Date {
|
|||
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 {
|
||||
type Error = String;
|
||||
|
|
|
@ -105,6 +105,9 @@ impl TryFrom<Tree> for Game {
|
|||
};
|
||||
let mut info = GameInfo::default();
|
||||
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_rank = tree
|
||||
|
@ -236,6 +239,7 @@ pub enum GameResult {
|
|||
Draw,
|
||||
Black(Win),
|
||||
White(Win),
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for GameResult {
|
||||
|
@ -250,7 +254,7 @@ impl TryFrom<&str> for GameResult {
|
|||
let res = match parts[0].to_ascii_lowercase().as_str() {
|
||||
"b" => GameResult::Black,
|
||||
"w" => GameResult::White,
|
||||
_ => panic!("unknown result format"),
|
||||
_ => return Ok(GameResult::Unknown(parts[0].to_owned())),
|
||||
};
|
||||
match parts[1].to_ascii_lowercase().as_str() {
|
||||
"r" | "resign" => Ok(res(Win::Resignation)),
|
||||
|
|
Loading…
Reference in New Issue