Added Location and location ownership/rights to gamenight.

This commit is contained in:
2025-12-24 14:48:54 +01:00
parent 8a48119c80
commit ff88029a4b
57 changed files with 3034 additions and 995 deletions

828
gamenight-cli/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ edition = "2024"
[dependencies]
gamenight-api-client-rs = { path = "../gamenight-api-client-rs" }
tokio = { version = "1", features = ["full"] }
inquire = { version = "0.7.5", features = ["date"] }
inquire = { version = "0.7.5", features = ["date", "editor"] }
async-trait = "0.1"
dyn-clone = "1.0"
chrono = "0.4"

View File

@@ -42,6 +42,9 @@ pub struct Config {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub last_instance: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
pub editor: Option<String>,
}
impl Config {
@@ -57,7 +60,8 @@ impl Config {
pub fn new() -> Config {
Config {
instances: vec![],
last_instance: None
last_instance: None,
editor: None
}
}

View File

@@ -0,0 +1,31 @@
use std::fmt::Display;
use gamenight_api_client_rs::models;
use uuid::Uuid;
#[derive(Clone)]
pub struct Location {
pub id: Uuid,
pub name: String,
pub address: Option<String>,
pub note: Option<String>,
}
impl From<models::Location> for Location {
fn from(location: models::Location) -> Self {
Self {
id: Uuid::parse_str(&location.id).unwrap(),
name: location.name,
address: location.address,
note: location.note
}
}
}
impl Display for Location {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, r#"name: {}
address: {}
note: {}"#, &self.name, &<std::option::Option<std::string::String> as Clone>::clone(&self.address).unwrap_or_default(), &<std::option::Option<std::string::String> as Clone>::clone(&self.note).unwrap_or_default())
}
}

View File

@@ -3,4 +3,5 @@ pub mod user;
pub mod config;
pub mod participants;
pub mod game;
pub mod owned_games;
pub mod owned_games;
pub mod location;

View File

@@ -0,0 +1,65 @@
use std::{ffi::OsStr, fmt::Display};
use async_trait::async_trait;
use gamenight_api_client_rs::{apis::default_api::{location_authorize_post, location_post}, models::{authorize_location_request_body::Op::Grant, AddLocationRequestBody, AuthorizeLocationRequestBody}};
use inquire::{Editor, Text};
use super::*;
#[derive(Clone)]
pub struct AddLocation {
}
impl AddLocation {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl<'a> Flow<'a> for AddLocation {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
if let Some(name) = Text::new("What is the name of the location you want to add?").prompt_skippable()? {
let address;
let note;
{
address = Text::new("Optional: What is the address?").prompt_skippable()?;
let mut editor_prompt = Editor::new("What is the name of the location you want to add?");
let editor_command;
if let Some(editor) = &state.gamenight_configuration.editor {
editor_command = editor.clone();
editor_prompt = editor_prompt.with_editor_command(OsStr::new(&editor_command))
}
note = editor_prompt.prompt_skippable()?;
}
let add_location_request = AddLocationRequestBody {
name,
address,
note
};
let location_id = location_post(&state.api_configuration, Some(add_location_request)).await?;
let add_authorize_request = AuthorizeLocationRequestBody {
location_id: location_id.location_id.to_string(),
user_id: state.get_user_id()?.to_string(),
op: Grant
};
location_authorize_post(&state.api_configuration, Some(add_authorize_request)).await?;
}
Ok((FlowOutcome::Cancelled, state))
}
}
impl Display for AddLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Add Location")
}
}

View File

@@ -1,7 +1,7 @@
use inquire::{ui::RenderConfig, Select};
use crate::flows::{add_gamenight::AddGamenight, exit::Exit, games::Games, list_gamenights::ListGamenights};
use crate::flows::{add_gamenight::AddGamenight, exit::Exit, games::Games, list_gamenights::ListGamenights, locations::Locations};
use super::*;
@@ -27,6 +27,7 @@ impl<'a> Flow<'a> for GamenightMenu {
Box::new(ListGamenights::new()),
Box::new(AddGamenight::new()),
Box::new(Games::new()),
Box::new(Locations::new()),
Box::new(Exit::new())
];

View File

@@ -0,0 +1,49 @@
use std::fmt::Display;
use async_trait::async_trait;
use gamenight_api_client_rs::apis::default_api::locations_get;
use inquire::{ui::RenderConfig, Select};
use crate::flows::{exit::Exit, view_location::ViewLocation};
use super::*;
#[derive(Clone)]
pub struct ListLocations {
}
impl ListLocations {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl<'a> Flow<'a> for ListLocations {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
let locations = locations_get(&state.api_configuration).await?;
let mut flows = locations.into_iter().map(|location| -> Box<dyn Flow + Send> {
Box::new(ViewLocation::new(location.into()))
}).collect::<Vec::<Box::<dyn Flow + Send>>>();
flows.push(Box::new(Exit::new()));
let choice = Select::new("What would you like to do?", flows)
.with_help_message("Select the action you want to take or quit the program")
.with_render_config(RenderConfig {
option_index_prefix: inquire::ui::IndexPrefix::Simple,
..Default::default()
})
.prompt_skippable()?;
self.continue_choice(state, &choice).await
}
}
impl Display for ListLocations {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "List all locations")
}
}

View File

@@ -0,0 +1,101 @@
use std::fmt::Display;
use async_trait::async_trait;
use gamenight_api_client_rs::{apis::default_api::{self, authorized_location_user_ids_get, users_get}, models::{AuthorizeLocationRequestBody, LocationId, User}};
use inquire::MultiSelect;
use uuid::Uuid;
use crate::flows::FlowError;
use super::{Flow, FlowOutcome, FlowResult, GamenightState};
#[derive(Clone)]
pub struct LocationAuthorize {
location_id: Uuid
}
impl LocationAuthorize {
pub fn new(location_id: Uuid) -> Self {
Self {
location_id
}
}
}
struct AuthorizeMultiSelectStruct<'a> {
id: Uuid,
username: &'a String
}
impl<'a> Display for AuthorizeMultiSelectStruct<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.username)
}
}
impl<'a> TryFrom<&'a User> for AuthorizeMultiSelectStruct<'a> {
type Error = FlowError;
fn try_from(value: &'a User) -> Result<Self, Self::Error> {
Ok(AuthorizeMultiSelectStruct{
id: Uuid::parse_str(&value.id)?,
username: &value.username
})
}
}
#[async_trait]
impl<'a> Flow<'a> for LocationAuthorize {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
let users = users_get(&state.api_configuration).await?;
let location_id = LocationId {
location_id: self.location_id.to_string()
};
let authorized_user_ids = authorized_location_user_ids_get(&state.api_configuration, Some(location_id)).await?;
let authorized_user_ids : Vec<String> = authorized_user_ids.into_iter().map(|x| x.user_id).collect();
let options: Vec<AuthorizeMultiSelectStruct> = users.iter().map(|x| {x.try_into()}).collect::<Result<Vec<AuthorizeMultiSelectStruct>, FlowError>>()?;
let (authorized_indices, authorized_users) : &(Vec<usize>, Vec<&User>) = &users.iter().enumerate().filter(|t| {
authorized_user_ids.contains(&t.1.id)
}).unzip();
let selections = MultiSelect::new("Which users should be able to host gamenights in this location?", options)
.with_default(&authorized_indices[..])
.prompt_skippable()?;
if let Some(selections) = &selections {
for selection in selections {
if authorized_users.iter().find(|x| {x.id == selection.id.to_string()}).is_none() {
default_api::location_authorize_post(&state.api_configuration, Some(
AuthorizeLocationRequestBody {
location_id: self.location_id.to_string(),
user_id: selection.id.to_string(),
op: gamenight_api_client_rs::models::authorize_location_request_body::Op::Grant
}
)).await?
}
}
for authorized_user in authorized_users {
if selections.iter().find(|x| {x.id.to_string() == authorized_user.id}).is_none() {
default_api::location_authorize_post(&state.api_configuration, Some(
AuthorizeLocationRequestBody {
location_id: self.location_id.to_string(),
user_id: authorized_user.id.to_string(),
op: gamenight_api_client_rs::models::authorize_location_request_body::Op::Revoke
}
)).await?
}
}
}
clear_screen::clear();
Ok((FlowOutcome::Successful, state))
}
}
impl Display for LocationAuthorize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Authorize users for location")
}
}

View File

@@ -0,0 +1,46 @@
use std::fmt::Display;
use async_trait::async_trait;
use inquire::{ui::RenderConfig, Select};
use crate::flows::{add_location::AddLocation, exit::Exit, list_locations::ListLocations};
use super::*;
#[derive(Clone)]
pub struct Locations {
}
impl Locations {
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl<'a> Flow<'a> for Locations {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
let flows: Vec<Box<dyn Flow + Send>> = vec![
Box::new(ListLocations::new()),
Box::new(AddLocation::new()),
Box::new(Exit::new())
];
let choice = Select::new("What would you like to do?", flows)
.with_help_message("Select the action you want to take or quit the program")
.with_render_config(RenderConfig {
option_index_prefix: inquire::ui::IndexPrefix::Simple,
..Default::default()
})
.prompt_skippable()?;
self.continue_choice(state, &choice).await
}
}
impl Display for Locations {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Locations")
}
}

View File

@@ -30,6 +30,11 @@ mod view_game;
mod rename_game;
mod own;
mod disown;
mod locations;
mod list_locations;
mod view_location;
mod add_location;
mod location_authorize;
pub struct GamenightState {
api_configuration: Configuration,
@@ -122,6 +127,14 @@ impl From<ParseIntError> for FlowError {
}
}
impl From<jsonwebtoken::errors::Error> for FlowError {
fn from(value: jsonwebtoken::errors::Error) -> Self {
Self {
error: value.to_string()
}
}
}
#[derive(PartialEq)]
pub enum FlowOutcome {
Successful,

View File

@@ -23,14 +23,6 @@ impl ViewGamenight {
}
}
impl From<jsonwebtoken::errors::Error> for FlowError {
fn from(value: jsonwebtoken::errors::Error) -> Self {
Self {
error: value.to_string()
}
}
}
#[async_trait]
impl<'a> Flow<'a> for ViewGamenight {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {

View File

@@ -0,0 +1,44 @@
use inquire::Select;
use crate::{domain::location::Location, flows::{exit::Exit, location_authorize::LocationAuthorize}};
use super::*;
#[derive(Clone)]
pub struct ViewLocation {
location: Location
}
impl ViewLocation {
pub fn new(location: Location) -> Self {
Self {
location
}
}
}
#[async_trait]
impl<'a> Flow<'a> for ViewLocation {
async fn run(&self, state: &'a mut GamenightState) -> FlowResult<'a> {
println!("{}", self.location);
let options: Vec<Box<dyn Flow<'a> + Send>> = vec![
Box::new(LocationAuthorize::new(self.location.id)),
Box::new(Exit::new())
];
let choice = Select::new("What do you want to do:", options)
.prompt_skippable()?;
self.continue_choice(state, &choice).await
}
}
impl Display for ViewLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.location.name)
}
}