Show a list of games in the library #64

Merged
savanni merged 9 commits from kifu/game-library into main 2023-08-20 17:02:14 +00:00
15 changed files with 231 additions and 75 deletions

4
Cargo.lock generated
View File

@ -910,7 +910,7 @@ dependencies = [
"gdk4", "gdk4",
"gio", "gio",
"glib", "glib",
"glib-build-tools", "glib-build-tools 0.16.3",
"gtk4", "gtk4",
"libadwaita", "libadwaita",
"serde", "serde",
@ -1348,7 +1348,7 @@ dependencies = [
"gtk4", "gtk4",
"image", "image",
"kifu-core", "kifu-core",
"screenplay", "sgf",
"tokio", "tokio",
] ]

View File

@ -30,6 +30,7 @@ macro_rules! define_config {
} }
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum ConfigOption { pub enum ConfigOption {
$($name($struct)),+ $($name($struct)),+
} }

View File

@ -69,15 +69,11 @@ pub struct CoreApp {
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().unwrap();
let state = Arc::new(RwLock::new(AppState::new(db_path))); 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, state }
} }

View File

@ -1,36 +1,52 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sgf::{ use sgf::go::Game;
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: Vec<Date>, pub date: Vec<String>,
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 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),
};
GamePreviewElement { GamePreviewElement {
date: game.info.date.clone(), date: game
black_player: game
.info .info
.black_player .date
.clone() .iter()
.unwrap_or("black_player".to_owned()), .map(|dt| dt.to_string())
black_rank: game.info.black_rank.clone(), .collect::<Vec<String>>(),
white_player: game name,
.info black_player,
.white_player white_player,
.clone()
.unwrap_or("white_player".to_owned()),
white_rank: game.info.white_rank.clone(),
} }
} }
} }

View File

@ -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 {
}

View File

@ -4,9 +4,6 @@ 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};

View File

@ -12,11 +12,12 @@ screenplay = []
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" } gtk = { version = "0.6", package = "gtk4", features = ["v4_8"] }
image = { version = "0.24" } image = { version = "0.24" }
kifu-core = { path = "../core" } kifu-core = { path = "../core" }
tokio = { version = "1.26", features = [ "full" ] } tokio = { version = "1.26", features = [ "full" ] }
screenplay = { path = "../../screenplay" } # screenplay = { path = "../../screenplay" }
sgf = { path = "../../sgf" }
[build-dependencies] [build-dependencies]
glib-build-tools = "0.17" glib-build-tools = "0.17"

View File

@ -1 +1,7 @@
{"Me":{"name":"Savanni","rank":{"Kyu":10}},"DatabasePath":"../core/fixtures/five_games"} {
"Me":{
"name":"Savanni",
"rank":{"Kyu":10}
},
"DatabasePath": "kifu/core/fixtures/five_games"
}

View File

@ -13,8 +13,8 @@ fn handle_response(api: CoreApi, window: gtk::ApplicationWindow, message: CoreRe
CoreResponse::HomeView(view) => perftrace("HomeView", || { CoreResponse::HomeView(view) => perftrace("HomeView", || {
let api = api.clone(); let api = api.clone();
let new_game = Home::new(api, view); let home = Home::new(api, view);
window.set_child(Some(&new_game)); window.set_child(Some(&home));
}), }),
CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || { CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || {
let api = api.clone(); let api = api.clone();

View File

@ -3,7 +3,12 @@ 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 {
title: gtk::Label,
black_player: gtk::Label,
white_player: gtk::Label,
date: gtk::Label,
}
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for GamePreviewPrivate { impl ObjectSubclass for GamePreviewPrivate {
@ -21,22 +26,26 @@ glib::wrapper! {
} }
impl GamePreview { impl GamePreview {
pub fn new(element: GamePreviewElement) -> GamePreview { pub fn new() -> 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(true);
println!("game_preview: {:?}", element); s.append(&s.imp().date);
let black_player = match element.black_rank { s.append(&s.imp().title);
Some(rank) => format!("{} ({})", element.black_player, rank.to_string()), s.append(&s.imp().black_player);
None => element.black_player, s.append(&s.imp().white_player);
};
let white_player = match element.white_rank {
Some(rank) => format!("{} ({})", element.white_player, rank.to_string()),
None => element.white_player,
};
s.append(&gtk::Label::new(Some(&black_player)));
s.append(&gtk::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);
if let Some(date) = element.date.first() {
self.imp().date.set_text(&date);
}
}
} }

View File

@ -1,5 +1,4 @@
use crate::ui::GamePreview; use crate::{ui::Library, CoreApi};
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::{
@ -101,31 +100,51 @@ 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::Grid; type ParentType = gtk::Box;
} }
impl ObjectImpl for HomePrivate {} impl ObjectImpl for HomePrivate {}
impl WidgetImpl for HomePrivate {} impl WidgetImpl for HomePrivate {}
impl GridImpl for HomePrivate {} impl BoxImpl for HomePrivate {}
glib::wrapper! { 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 { 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);
s.attach(&black_player, 1, 1, 1, 1); players.append(&black_player);
*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);
s.attach(&white_player, 2, 1, 1, 1); players.append(&white_player);
*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().label(&view.start_game.label).build(); let new_game_button = gtk::Button::builder().label(&view.start_game.label).build();
s.attach(&new_game_button, 2, 2, 1, 1); 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 |_| {
@ -139,12 +158,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 s
} }
} }

113
kifu/gtk/src/ui/library.rs Normal file
View File

@ -0,0 +1,113 @@
use crate::ui::GamePreview;
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::ListView,
}
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::ListView::new(Some(selection_model), Some(factory));
list_view.set_hexpand(true);
Self { model, list_view }
}
}
#[glib::object_subclass]
impl ObjectSubclass for LibraryPrivate {
const NAME: &'static str = "Library";
type Type = Library;
type ParentType = gtk::Box;
}
impl ObjectImpl for LibraryPrivate {}
impl WidgetImpl for LibraryPrivate {}
impl BoxImpl for LibraryPrivate {}
glib::wrapper! {
pub struct Library(ObjectSubclass<LibraryPrivate>) @extends gtk::Widget, gtk::Box;
}
impl Library {
pub fn new() -> Self {
let s: Self = Object::builder().build();
s.set_hexpand(true);
s.append(&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);
}
}

View File

@ -4,6 +4,9 @@ pub use chat::Chat;
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;

View File

@ -24,6 +24,16 @@ 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;

View File

@ -99,6 +99,11 @@ impl TryFrom<Tree> for Game {
info.app_name = tree.sequence[0] info.app_name = tree.sequence[0]
.find_prop("AP") .find_prop("AP")
.map(|prop| prop.values[0].clone()); .map(|prop| prop.values[0].clone());
info.game_name = tree.sequence[0]
.find_prop("GN")
.map(|prop| prop.values[0].clone());
info.black_player = tree.sequence[0] info.black_player = tree.sequence[0]
.find_prop("PB") .find_prop("PB")
.map(|prop| prop.values.join(", ")); .map(|prop| prop.values.join(", "));