cargo fmt
This commit is contained in:
parent
f639d350b9
commit
871d6317ff
12 changed files with 362 additions and 179 deletions
18
src/app.rs
18
src/app.rs
|
@ -1,15 +1,15 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crossterm::event::{Event, self, KeyCode};
|
use crossterm::event::{self, Event, KeyCode};
|
||||||
use tokio;
|
use tokio;
|
||||||
|
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
|
||||||
use crate::db::DB;
|
|
||||||
use crate::db::data_cache::DataCache;
|
use crate::db::data_cache::DataCache;
|
||||||
|
use crate::db::DB;
|
||||||
use crate::uis::history::HistoryState;
|
use crate::uis::history::HistoryState;
|
||||||
use crate::uis::new_transaction::NewTransactionTabState;
|
|
||||||
use crate::uis::navigation_frame::NavigationState;
|
use crate::uis::navigation_frame::NavigationState;
|
||||||
|
use crate::uis::new_transaction::NewTransactionTabState;
|
||||||
|
|
||||||
pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
@ -80,21 +80,19 @@ impl<'a> App<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh(&mut self) {
|
pub fn refresh(&mut self) {
|
||||||
|
|
||||||
let fut = Arc::clone(&self.db);
|
let fut = Arc::clone(&self.db);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let res = fut.lock().await.get_all_records().await;
|
let res = fut.lock().await.get_all_records().await;
|
||||||
match res {
|
match res {
|
||||||
Ok(_) => {},
|
Ok(_) => {}
|
||||||
Err(e) => warn!("{}", e)
|
Err(e) => warn!("{}", e),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
|
|
||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
use sqlx::Error;
|
use sqlx::Error;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use sqlx::postgres::PgPoolOptions;
|
|
||||||
|
|
||||||
use super::data_cache::DataCache;
|
use super::data_cache::DataCache;
|
||||||
use super::tables::Transaction;
|
use super::tables::Transaction;
|
||||||
|
@ -13,12 +12,12 @@ pub struct DB {
|
||||||
data_cache: DataCache,
|
data_cache: DataCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl DB {
|
impl DB {
|
||||||
pub async fn new(data_cache: DataCache) -> Result<DB, sqlx::Error> {
|
pub async fn new(data_cache: DataCache) -> Result<DB, sqlx::Error> {
|
||||||
let connection_pool = PgPoolOptions::new()
|
let connection_pool = PgPoolOptions::new()
|
||||||
.max_connections(3)
|
.max_connections(3)
|
||||||
.connect("postgres://rcntuser:Devel@pmentPa$$w0rd@10.0.0.183/Borealis").await?;
|
.connect("postgres://rcntuser:Devel@pmentPa$$w0rd@10.0.0.183/Borealis")
|
||||||
|
.await?;
|
||||||
Ok(DB {
|
Ok(DB {
|
||||||
conn_pool: connection_pool,
|
conn_pool: connection_pool,
|
||||||
data_cache,
|
data_cache,
|
||||||
|
@ -35,7 +34,6 @@ impl DB {
|
||||||
temp_transactions.push(row);
|
temp_transactions.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut transactions = self.data_cache.transactions.lock().unwrap();
|
let mut transactions = self.data_cache.transactions.lock().unwrap();
|
||||||
transactions.clear();
|
transactions.clear();
|
||||||
|
@ -43,7 +41,5 @@ impl DB {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use super::tables;
|
use super::tables::{self, Account};
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DataCache {
|
pub struct DataCache {
|
||||||
|
|
||||||
pub accounts: Arc<Mutex<Vec<tables::Account>>>,
|
pub accounts: Arc<Mutex<Vec<tables::Account>>>,
|
||||||
|
|
||||||
pub buckets: Arc<Mutex<Vec<tables::Buckets>>>,
|
pub buckets: Arc<Mutex<Vec<tables::Buckets>>>,
|
||||||
|
@ -13,15 +11,13 @@ pub struct DataCache {
|
||||||
pub transaction_catagories: Arc<Mutex<Vec<tables::TransactionCatagories>>>,
|
pub transaction_catagories: Arc<Mutex<Vec<tables::TransactionCatagories>>>,
|
||||||
|
|
||||||
pub transaction_breakdowns: Arc<Mutex<Vec<tables::TransactionBreakdown>>>,
|
pub transaction_breakdowns: Arc<Mutex<Vec<tables::TransactionBreakdown>>>,
|
||||||
|
|
||||||
pub transactions: Arc<Mutex<Vec<tables::Transaction>>>,
|
pub transactions: Arc<Mutex<Vec<tables::Transaction>>>,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl DataCache {
|
impl DataCache {
|
||||||
pub fn new() -> DataCache {
|
pub fn new() -> DataCache {
|
||||||
DataCache {
|
DataCache {
|
||||||
accounts: Arc::new(Mutex::new(Vec::new())),
|
accounts: Arc::new(Mutex::new(Vec::new())),
|
||||||
buckets: Arc::new(Mutex::new(Vec::new())),
|
buckets: Arc::new(Mutex::new(Vec::new())),
|
||||||
transaction_catagories: Arc::new(Mutex::new(Vec::new())),
|
transaction_catagories: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
@ -29,4 +25,16 @@ impl DataCache {
|
||||||
transactions: Arc::new(Mutex::new(Vec::new())),
|
transactions: Arc::new(Mutex::new(Vec::new())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn find_account_by_id(&self, id: i32) -> Option<Account> {
|
||||||
|
{
|
||||||
|
let accounts = self.accounts.lock().unwrap();
|
||||||
|
for i in accounts.iter() {
|
||||||
|
if i.acnt_id == id {
|
||||||
|
return Some(i.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
|
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
pub use self::connection::DB;
|
pub use self::connection::DB;
|
||||||
|
|
||||||
pub mod tables;
|
|
||||||
pub mod data_cache;
|
pub mod data_cache;
|
||||||
|
pub mod tables;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use sqlx::{FromRow, postgres::types::PgMoney};
|
use sqlx::{postgres::types::PgMoney, FromRow};
|
||||||
use time::Date;
|
use time::Date;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, FromRow)]
|
#[derive(Clone, FromRow)]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
pub acnt_id: i32,
|
pub acnt_id: i32,
|
||||||
|
@ -11,7 +10,6 @@ pub struct Account {
|
||||||
pub acnt_description: String,
|
pub acnt_description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, FromRow)]
|
#[derive(Clone, FromRow)]
|
||||||
pub struct Buckets {
|
pub struct Buckets {
|
||||||
pub bkt_id: i32,
|
pub bkt_id: i32,
|
||||||
|
@ -20,7 +18,6 @@ pub struct Buckets {
|
||||||
pub bkt_description: String,
|
pub bkt_description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, FromRow)]
|
#[derive(Clone, FromRow)]
|
||||||
pub struct TransactionCatagories {
|
pub struct TransactionCatagories {
|
||||||
pub trns_ctgry_id: i32,
|
pub trns_ctgry_id: i32,
|
||||||
|
@ -29,7 +26,6 @@ pub struct TransactionCatagories {
|
||||||
pub trns_ctgry_description: String,
|
pub trns_ctgry_description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, FromRow)]
|
#[derive(Clone, FromRow)]
|
||||||
pub struct TransactionBreakdown {
|
pub struct TransactionBreakdown {
|
||||||
pub trns_brkdwn_id: i32,
|
pub trns_brkdwn_id: i32,
|
||||||
|
@ -39,27 +35,60 @@ pub struct TransactionBreakdown {
|
||||||
pub trns_brkdwn_bucket: i32,
|
pub trns_brkdwn_bucket: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PartialTransactionBreakdown {
|
||||||
|
pub trns_brkdwn_id: Option<i32>,
|
||||||
|
pub trns_brkdwn_amount: Option<PgMoney>,
|
||||||
|
pub trns_brkdwn_parent_transaction: Option<i32>,
|
||||||
|
pub trns_brkdwn_catagory: Option<i32>,
|
||||||
|
pub trns_brkdwn_bucket: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialTransactionBreakdown {
|
||||||
|
pub fn new_empty() -> PartialTransactionBreakdown {
|
||||||
|
PartialTransactionBreakdown {
|
||||||
|
trns_brkdwn_id: None,
|
||||||
|
trns_brkdwn_amount: None,
|
||||||
|
trns_brkdwn_parent_transaction: None,
|
||||||
|
trns_brkdwn_catagory: None,
|
||||||
|
trns_brkdwn_bucket: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, FromRow)]
|
#[derive(Clone, FromRow)]
|
||||||
pub struct Transaction{
|
pub struct Transaction {
|
||||||
pub trns_id: i32,
|
pub trns_id: i32,
|
||||||
pub trns_amount: PgMoney,
|
pub trns_amount: PgMoney,
|
||||||
pub trns_description: String,
|
pub trns_description: String,
|
||||||
pub trns_bucket: i32,
|
pub trns_bucket: i32,
|
||||||
pub trns_date: Date
|
pub trns_date: Date,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PartialTransaction {
|
||||||
|
pub trns_id: Option<i32>,
|
||||||
|
pub trns_amount: Option<PgMoney>,
|
||||||
|
pub trns_description: Option<String>,
|
||||||
|
pub trns_bucket: Option<i32>,
|
||||||
|
pub trns_date: Option<Date>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Transaction {
|
impl Transaction {
|
||||||
|
|
||||||
pub fn get_header() -> String {
|
pub fn get_header() -> String {
|
||||||
return format!(" {:<7} | {:<9} | {:<10}", "Id", "Amount", "Date")
|
return format!(" {:<7} | {:<9} | {:<10}", "Id", "Amount", "Date");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_string(&self) -> String {
|
pub fn to_string(&self) -> String {
|
||||||
return format!(" T{:0>6} | {:>9} | {:>10}", self.trns_id, self.trns_amount.to_decimal(2).to_string(), self.trns_date.to_string())
|
return format!(
|
||||||
|
" T{:0>6} | {:>9} | {:>10}",
|
||||||
|
self.trns_id,
|
||||||
|
self.trns_amount.to_decimal(2).to_string(),
|
||||||
|
self.trns_date.to_string()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_str(&self) -> Cow<str> {
|
pub fn as_str(&self) -> Cow<str> {
|
||||||
return self.to_string().into()
|
return self.to_string().into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
|
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
pub mod uis;
|
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod uis;
|
||||||
|
|
23
src/main.rs
23
src/main.rs
|
@ -1,22 +1,18 @@
|
||||||
use std::io;
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
backend::CrosstermBackend,
|
|
||||||
Terminal,
|
|
||||||
};
|
|
||||||
|
|
||||||
use recount::db::DB;
|
|
||||||
use recount::db::data_cache::DataCache;
|
|
||||||
use recount::app::{App, AppResult};
|
use recount::app::{App, AppResult};
|
||||||
|
use recount::db::data_cache::DataCache;
|
||||||
|
use recount::db::DB;
|
||||||
use recount::tui::Tui;
|
use recount::tui::Tui;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> AppResult<()> {
|
async fn main() -> AppResult<()> {
|
||||||
|
|
||||||
let log_file = "testing_log.txt".to_string();
|
let log_file = "testing_log.txt".to_string();
|
||||||
init_logger(log_file);
|
init_logger(log_file);
|
||||||
|
|
||||||
|
@ -49,18 +45,17 @@ async fn main() -> AppResult<()> {
|
||||||
tui.exit()
|
tui.exit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn init_logger(output_file: String) {
|
pub fn init_logger(output_file: String) {
|
||||||
// TODO: configure the log levels to something appropriate
|
// TODO: configure the log levels to something appropriate
|
||||||
CombinedLogger::init(vec![
|
CombinedLogger::init(vec![
|
||||||
// TermLogger::new(
|
// TermLogger::new(
|
||||||
// LevelFilter::Info,
|
// LevelFilter::Info,
|
||||||
// Config::default(),
|
// Config::default(),
|
||||||
// TerminalMode::Mixed,
|
// TerminalMode::Mixed,
|
||||||
// ColorChoice::Auto,
|
// ColorChoice::Auto,
|
||||||
// ),
|
// ),
|
||||||
WriteLogger::new(
|
WriteLogger::new(
|
||||||
LevelFilter::Info,
|
LevelFilter::Debug,
|
||||||
Config::default(),
|
Config::default(),
|
||||||
File::create(output_file).unwrap(),
|
File::create(output_file).unwrap(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use crate::ui;
|
|
||||||
use crate::app::{App, AppResult};
|
use crate::app::{App, AppResult};
|
||||||
|
use crate::ui;
|
||||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||||
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen};
|
||||||
use std::io;
|
|
||||||
use ratatui::backend::Backend;
|
use ratatui::backend::Backend;
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
/// Representation of a terminal user interface.
|
/// Representation of a terminal user interface.
|
||||||
///
|
///
|
||||||
|
|
39
src/ui.rs
39
src/ui.rs
|
@ -1,32 +1,39 @@
|
||||||
use crate::{app::{App, ActiveFrame}, uis::render_history_tab};
|
use crate::{
|
||||||
|
app::{ActiveFrame, App},
|
||||||
|
uis::render_history_tab,
|
||||||
|
};
|
||||||
|
use log;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
backend::Backend,
|
backend::Backend,
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
|
style::Style,
|
||||||
widgets::{Block, Borders},
|
widgets::{Block, Borders},
|
||||||
Frame, style::Style,
|
Frame,
|
||||||
};
|
};
|
||||||
use log;
|
|
||||||
|
|
||||||
use crate::uis::{render_navigation_frame, render_new_transaction_tab};
|
use crate::uis::{render_navigation_frame, render_new_transaction_tab};
|
||||||
|
|
||||||
|
pub fn render<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||||
pub fn render<B: Backend> (f: &mut Frame<B>, app: &mut App) {
|
|
||||||
|
|
||||||
let size = f.size();
|
let size = f.size();
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Length(3), Constraint::Min(0), Constraint::Length(3)].as_ref())
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(3),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
.split(size);
|
.split(size);
|
||||||
|
|
||||||
let bottom_block = Block::default()
|
let bottom_block = Block::default().borders(Borders::ALL).border_style({
|
||||||
.borders(Borders::ALL)
|
if let ActiveFrame::Navigation = app.states.active_frame {
|
||||||
.border_style({
|
Style::default()
|
||||||
if let ActiveFrame::Navigation = app.states.active_frame {
|
} else {
|
||||||
Style::default()
|
Style::default().fg(ratatui::style::Color::Green)
|
||||||
} else {
|
}
|
||||||
Style::default().fg(ratatui::style::Color::Green)
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
f.render_widget(bottom_block, chunks[1]);
|
f.render_widget(bottom_block, chunks[1]);
|
||||||
|
|
||||||
render_navigation_frame(f, chunks[0], chunks[2], app);
|
render_navigation_frame(f, chunks[0], chunks[2], app);
|
||||||
|
|
|
@ -1,22 +1,26 @@
|
||||||
use crossterm::event::{KeyEvent, KeyEventKind, KeyCode};
|
|
||||||
use ratatui::{backend::Backend, Frame, layout::{Rect, Layout, Direction, Constraint}, widgets::{Block, Borders, List, ListItem, Paragraph}, text::{Text, Line, Span}, style::{Style, Color}};
|
|
||||||
use crate::{app::App, db::tables::Transaction};
|
use crate::{app::App, db::tables::Transaction};
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||||
|
use ratatui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
text::{Line, Span, Text},
|
||||||
|
widgets::{Block, Borders, List, ListItem, Paragraph},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct HistoryState {
|
pub struct HistoryState {
|
||||||
pub selected_index: Option<usize>,
|
pub selected_index: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryState {
|
impl HistoryState {
|
||||||
|
|
||||||
pub fn new() -> HistoryState {
|
pub fn new() -> HistoryState {
|
||||||
HistoryState {
|
HistoryState {
|
||||||
selected_index: None,
|
selected_index: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_event(event: KeyEvent, app: &mut App) {
|
pub fn handle_event(event: KeyEvent, app: &mut App) {
|
||||||
|
|
||||||
if app.states.history.selected_index.is_none() {
|
if app.states.history.selected_index.is_none() {
|
||||||
app.states.history.selected_index = Some(0);
|
app.states.history.selected_index = Some(0);
|
||||||
}
|
}
|
||||||
|
@ -26,14 +30,13 @@ impl HistoryState {
|
||||||
if event.kind == KeyEventKind::Press {
|
if event.kind == KeyEventKind::Press {
|
||||||
match event.code {
|
match event.code {
|
||||||
KeyCode::Up | KeyCode::Tab => app.states.history.next(max_index),
|
KeyCode::Up | KeyCode::Tab => app.states.history.next(max_index),
|
||||||
KeyCode::Down => app.states.history.previous(),
|
KeyCode::Down => app.states.history.previous(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(&mut self, max_index: usize) {
|
pub fn next(&mut self, max_index: usize) {
|
||||||
|
|
||||||
if self.selected_index.is_some() {
|
if self.selected_index.is_some() {
|
||||||
let cur_index = self.selected_index.unwrap();
|
let cur_index = self.selected_index.unwrap();
|
||||||
if cur_index + 1 < max_index {
|
if cur_index + 1 < max_index {
|
||||||
|
@ -43,19 +46,16 @@ impl HistoryState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous(&mut self) {
|
pub fn previous(&mut self) {
|
||||||
|
|
||||||
if self.selected_index.is_some() {
|
if self.selected_index.is_some() {
|
||||||
let cur_index = self.selected_index.unwrap();
|
let cur_index = self.selected_index.unwrap();
|
||||||
if cur_index > 0 {
|
if cur_index > 0 {
|
||||||
self.selected_index = Some(cur_index - 1);
|
self.selected_index = Some(cur_index - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_history_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect, app: &mut App) {
|
pub fn render_history_tab<B: Backend>(f: &mut Frame<B>, body_rect: Rect, app: &mut App) {
|
||||||
|
|
||||||
let split_body = Layout::default()
|
let split_body = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
|
@ -65,45 +65,51 @@ pub fn render_history_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect, app: &
|
||||||
{
|
{
|
||||||
let transacts_list = app.data_cache.transactions.lock().unwrap();
|
let transacts_list = app.data_cache.transactions.lock().unwrap();
|
||||||
if app.states.history.selected_index.is_some() {
|
if app.states.history.selected_index.is_some() {
|
||||||
let selected_item: &Transaction = &transacts_list[app.states.history.selected_index.unwrap()];
|
let selected_item: &Transaction =
|
||||||
|
&transacts_list[app.states.history.selected_index.unwrap()];
|
||||||
|
|
||||||
let ident_style: Style = Style::default().fg(Color::Yellow);
|
let ident_style: Style = Style::default().fg(Color::Yellow);
|
||||||
let value_style: Style = Style::default().fg(Color::LightBlue);
|
let value_style: Style = Style::default().fg(Color::LightBlue);
|
||||||
|
|
||||||
lines.push(
|
lines.push(Line::from(vec![
|
||||||
Line::from(vec![
|
Span::styled("Transaction Id: ", ident_style),
|
||||||
Span::styled("Transaction Id: ", ident_style),
|
Span::styled(format!("T{:0>6}", selected_item.trns_id), value_style),
|
||||||
Span::styled(format!("T{:0>6}", selected_item.trns_id), value_style),
|
]));
|
||||||
])
|
lines.push(Line::from(vec![
|
||||||
);
|
Span::styled("Amount: ", ident_style),
|
||||||
lines.push(
|
Span::styled(
|
||||||
Line::from(vec![
|
format!("{}", selected_item.trns_amount.to_decimal(2).to_string()),
|
||||||
Span::styled("Amount: ", ident_style),
|
value_style,
|
||||||
Span::styled(format!("{}", selected_item.trns_amount.to_decimal(2).to_string()), value_style)
|
),
|
||||||
])
|
]));
|
||||||
);
|
lines.push(Line::from(vec![
|
||||||
lines.push(
|
Span::styled("Transaction Date: ", ident_style),
|
||||||
Line::from(vec![
|
Span::styled(
|
||||||
Span::styled("Transaction Date: ", ident_style),
|
format!("{}", selected_item.trns_date.to_string()),
|
||||||
Span::styled(format!("{}", selected_item.trns_date.to_string()), value_style)
|
value_style,
|
||||||
])
|
),
|
||||||
);
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let paragraph = Paragraph::new(lines.clone())
|
let paragraph = Paragraph::new(lines.clone()).block(
|
||||||
.block(Block::default().borders(Borders::ALL).title("Details").title_alignment(ratatui::layout::Alignment::Left));
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title("Details")
|
||||||
|
.title_alignment(ratatui::layout::Alignment::Left),
|
||||||
|
);
|
||||||
|
|
||||||
f.render_widget(paragraph, split_body[1]);
|
f.render_widget(paragraph, split_body[1]);
|
||||||
|
|
||||||
|
|
||||||
let list_chunks = Layout::default()
|
let list_chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Length(1), Constraint::Min(3)])
|
.constraints([Constraint::Length(1), Constraint::Min(3)])
|
||||||
.split(split_body[0]);
|
.split(split_body[0]);
|
||||||
|
|
||||||
let list_header = Paragraph::new(
|
let list_header = Paragraph::new(Text::styled(
|
||||||
Text::styled(Transaction::get_header(), Style::default().fg(Color::Black).bg(Color::Cyan))
|
Transaction::get_header(),
|
||||||
).block(Block::default().borders(Borders::NONE));
|
Style::default().fg(Color::Black).bg(Color::Cyan),
|
||||||
|
))
|
||||||
|
.block(Block::default().borders(Borders::NONE));
|
||||||
|
|
||||||
f.render_widget(list_header, list_chunks[0]);
|
f.render_widget(list_header, list_chunks[0]);
|
||||||
|
|
||||||
|
@ -117,19 +123,17 @@ pub fn render_history_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect, app: &
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::Black).bg(Color::White)
|
Style::default().fg(Color::Black).bg(Color::White)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
None => Style::default().fg(Color::Black).bg(Color::White),
|
None => Style::default().fg(Color::Black).bg(Color::White),
|
||||||
};
|
};
|
||||||
|
|
||||||
items.push(ListItem::new(
|
items.push(
|
||||||
Text::from(transacts_list.get(i).unwrap().as_str())
|
ListItem::new(Text::from(transacts_list.get(i).unwrap().as_str())).style(style),
|
||||||
).style(style));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let history_items = List::new(items).block(Block::default().borders(Borders::NONE));
|
||||||
|
|
||||||
let history_items = List::new(items)
|
f.render_widget(history_items, list_chunks[1]);
|
||||||
.block(Block::default().borders(Borders::NONE));
|
|
||||||
|
|
||||||
f.render_widget(history_items, list_chunks[1]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
use crossterm::event::{KeyEvent, KeyEventKind, KeyCode};
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||||
use ratatui::{widgets::{Borders, Paragraph, Block, Tabs}, backend::Backend, Frame, style::{Style, Color, Modifier}, text::{Text, Line}, layout::{Rect, Layout, Direction, Constraint}};
|
use ratatui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
text::{Line, Text},
|
||||||
|
widgets::{Block, Borders, Paragraph, Tabs},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::app::{App, ActiveFrame};
|
use crate::app::{ActiveFrame, App};
|
||||||
|
|
||||||
pub struct NavigationState<'a> {
|
pub struct NavigationState<'a> {
|
||||||
pub tabs: Vec<&'a str>,
|
pub tabs: Vec<&'a str>,
|
||||||
|
@ -37,7 +44,7 @@ impl<'a> NavigationState<'a> {
|
||||||
match self.tab_index {
|
match self.tab_index {
|
||||||
0 => Some(ActiveFrame::History),
|
0 => Some(ActiveFrame::History),
|
||||||
1 => Some(ActiveFrame::NewTransaction),
|
1 => Some(ActiveFrame::NewTransaction),
|
||||||
_ => todo!()
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,34 +52,39 @@ impl<'a> NavigationState<'a> {
|
||||||
if event.kind == KeyEventKind::Press {
|
if event.kind == KeyEventKind::Press {
|
||||||
match event.code {
|
match event.code {
|
||||||
KeyCode::Tab => app.states.nav_state.next_tab(),
|
KeyCode::Tab => app.states.nav_state.next_tab(),
|
||||||
KeyCode::Enter => app.states.active_frame = app.states.nav_state.get_active_tab_frametype().unwrap(),
|
KeyCode::Enter => {
|
||||||
|
app.states.active_frame =
|
||||||
|
app.states.nav_state.get_active_tab_frametype().unwrap()
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_navigation_frame<B: Backend> (f: &mut Frame<B>, status_rect: Rect, navbar_rect: Rect, app: &App) {
|
pub fn render_navigation_frame<B: Backend>(
|
||||||
|
f: &mut Frame<B>,
|
||||||
|
status_rect: Rect,
|
||||||
|
navbar_rect: Rect,
|
||||||
|
app: &App,
|
||||||
|
) {
|
||||||
let status_bar_chunks = Layout::default()
|
let status_bar_chunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints([Constraint::Percentage(75), Constraint::Min(10)])
|
.constraints([Constraint::Percentage(75), Constraint::Min(10)])
|
||||||
.split(status_rect);
|
.split(status_rect);
|
||||||
|
|
||||||
let left_block = Block::default()
|
let left_block = Block::default().borders(Borders::ALL).border_style({
|
||||||
.borders(Borders::ALL)
|
if let ActiveFrame::Navigation = app.states.active_frame {
|
||||||
.border_style({
|
Style::default().fg(ratatui::style::Color::Green)
|
||||||
if let ActiveFrame::Navigation = app.states.active_frame {
|
} else {
|
||||||
Style::default().fg(ratatui::style::Color::Green)
|
Style::default()
|
||||||
} else {
|
}
|
||||||
Style::default()
|
});
|
||||||
}
|
let right_block = Block::default().borders(Borders::ALL);
|
||||||
});
|
|
||||||
let right_block = Block::default()
|
|
||||||
.borders(Borders::ALL);
|
|
||||||
|
|
||||||
|
let titles = app
|
||||||
let titles = app.states.nav_state
|
.states
|
||||||
|
.nav_state
|
||||||
.tabs
|
.tabs
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
|
@ -93,11 +105,9 @@ pub fn render_navigation_frame<B: Backend> (f: &mut Frame<B>, status_rect: Rect,
|
||||||
|
|
||||||
f.render_widget(tabs, status_bar_chunks[0]);
|
f.render_widget(tabs, status_bar_chunks[0]);
|
||||||
|
|
||||||
let connection_paragraph = Paragraph::new(
|
let connection_paragraph =
|
||||||
Text::styled("Aurora",
|
Paragraph::new(Text::styled("Aurora", Style::default().fg(Color::Green)))
|
||||||
Style::default().fg(Color::Green)
|
.block(right_block);
|
||||||
)
|
|
||||||
).block(right_block);
|
|
||||||
|
|
||||||
f.render_widget(connection_paragraph, status_bar_chunks[1]);
|
f.render_widget(connection_paragraph, status_bar_chunks[1]);
|
||||||
|
|
||||||
|
@ -112,9 +122,7 @@ pub fn render_navigation_frame<B: Backend> (f: &mut Frame<B>, status_rect: Rect,
|
||||||
app.states.nav_state.message.clone().unwrap()
|
app.states.nav_state.message.clone().unwrap()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let bottom_navbar = Paragraph::new(
|
let bottom_navbar = Paragraph::new(Text::styled(navbar, Style::default().fg(Color::White)))
|
||||||
Text::styled(navbar, Style::default().fg(Color::White))
|
|
||||||
)
|
|
||||||
.block(Block::default().borders(Borders::ALL));
|
.block(Block::default().borders(Borders::ALL));
|
||||||
f.render_widget(bottom_navbar, navbar_rect);
|
f.render_widget(bottom_navbar, navbar_rect);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,72 @@
|
||||||
use crate::app::App;
|
use crate::{
|
||||||
use crossterm::event::{KeyEvent, KeyEventKind, KeyCode};
|
app::App,
|
||||||
use ratatui::{backend::Backend, Frame, layout::{Rect, Layout, Direction, Constraint}, widgets::{Paragraph, Borders, Block}, text::Text, style::{Style, Color}};
|
db::tables::{Account, Buckets, PartialTransactionBreakdown},
|
||||||
|
};
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
|
||||||
|
use ratatui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
text::Text,
|
||||||
|
widgets::{Block, Borders, Paragraph},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
use time::Date;
|
||||||
|
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ManualDataFocus {
|
||||||
|
Account,
|
||||||
|
Amount,
|
||||||
|
Date,
|
||||||
|
Bucket,
|
||||||
|
Description,
|
||||||
|
Breakdowns,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ManualData {
|
||||||
|
pub focus: Option<ManualDataFocus>,
|
||||||
|
|
||||||
|
pub account: Option<Account>,
|
||||||
|
pub amount: Option<Decimal>,
|
||||||
|
pub date: Option<Date>,
|
||||||
|
pub bucket: Option<Buckets>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
pub editing_breakdown: PartialTransactionBreakdown,
|
||||||
|
pub breakdowns: Vec<PartialTransactionBreakdown>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ManualData {
|
||||||
|
pub fn new() -> ManualData {
|
||||||
|
ManualData {
|
||||||
|
focus: None,
|
||||||
|
account: None,
|
||||||
|
amount: None,
|
||||||
|
date: None,
|
||||||
|
bucket: None,
|
||||||
|
description: None,
|
||||||
|
editing_breakdown: PartialTransactionBreakdown::new_empty(),
|
||||||
|
breakdowns: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct NewTransactionTabState<'a> {
|
pub struct NewTransactionTabState<'a> {
|
||||||
pub cur_tab_index: usize,
|
pub cur_tab_index: usize,
|
||||||
pub tabs: Vec<&'a str>,
|
pub tabs: Vec<&'a str>,
|
||||||
|
|
||||||
|
manual_data: ManualData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> NewTransactionTabState<'a> {
|
impl<'a> NewTransactionTabState<'a> {
|
||||||
pub fn new() -> NewTransactionTabState<'a> {
|
pub fn new() -> NewTransactionTabState<'a> {
|
||||||
NewTransactionTabState {
|
NewTransactionTabState {
|
||||||
cur_tab_index: 0,
|
cur_tab_index: 0,
|
||||||
tabs: vec!["Quick Entry", "Manual Entry"]
|
tabs: vec!["Quick Entry", "Manual Entry"],
|
||||||
|
manual_data: ManualData::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,8 +84,7 @@ impl<'a> NewTransactionTabState<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_new_transaction_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect, app: &App) {
|
pub fn render_new_transaction_tab<B: Backend>(f: &mut Frame<B>, body_rect: Rect, app: &App) {
|
||||||
|
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
||||||
|
@ -39,7 +93,7 @@ pub fn render_new_transaction_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect
|
||||||
// Render the custom tab bar
|
// Render the custom tab bar
|
||||||
let mut constraints: Vec<Constraint> = vec![];
|
let mut constraints: Vec<Constraint> = vec![];
|
||||||
let tab_percent: u16 = (100 / app.states.transactions.tabs.len()) as u16;
|
let tab_percent: u16 = (100 / app.states.transactions.tabs.len()) as u16;
|
||||||
for _ in 0..app.states.transactions.tabs.len() {
|
for _ in 0..app.states.transactions.tabs.len() {
|
||||||
constraints.push(Constraint::Percentage(tab_percent));
|
constraints.push(Constraint::Percentage(tab_percent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,41 +102,127 @@ pub fn render_new_transaction_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect
|
||||||
.constraints(constraints)
|
.constraints(constraints)
|
||||||
.split(chunks[0]);
|
.split(chunks[0]);
|
||||||
|
|
||||||
for i in 0..app.states.transactions.tabs.len() {
|
for i in 0..app.states.transactions.tabs.len() {
|
||||||
|
let tab = Paragraph::new(Text::styled(
|
||||||
let tab = Paragraph::new(
|
app.states.transactions.tabs[i],
|
||||||
Text::styled(app.states.transactions.tabs[i],
|
Style::default().fg(Color::White),
|
||||||
Style::default().fg(Color::White)
|
))
|
||||||
)
|
.alignment(ratatui::layout::Alignment::Center)
|
||||||
)
|
.block(Block::default().borders(Borders::ALL).style({
|
||||||
.alignment(ratatui::layout::Alignment::Center)
|
if app.states.transactions.cur_tab_index == i {
|
||||||
.block(
|
Style::default().bg(Color::Blue)
|
||||||
Block::default()
|
} else {
|
||||||
.borders(Borders::ALL)
|
Style::default()
|
||||||
.style({
|
}
|
||||||
if app.states.transactions.cur_tab_index == i {
|
}));
|
||||||
Style::default().bg(Color::Blue)
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
f.render_widget(tab, tab_chunks[i])
|
f.render_widget(tab, tab_chunks[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
match app.states.transactions.tabs[app.states.transactions.cur_tab_index] {
|
match app.states.transactions.tabs[app.states.transactions.cur_tab_index] {
|
||||||
"Quick Entry" => render_quick_entry(f, chunks[1], app),
|
"Quick Entry" => render_quick_entry(f, chunks[1], app),
|
||||||
"Manual Entry" => render_manual_entry(f, chunks[1], app),
|
"Manual Entry" => render_manual_entry(f, chunks[1], app),
|
||||||
_ => return
|
_ => return,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_manual_entry<B: Backend>(f: &mut Frame<B>, area: Rect, app: &App) {
|
||||||
|
let num_can_show = (area.height / 3) as usize;
|
||||||
|
let mut constraints: Vec<Constraint> = Vec::new();
|
||||||
|
for _ in 0..num_can_show {
|
||||||
|
constraints.push(Constraint::Length(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Can show: {}", num_can_show);
|
||||||
|
debug!("saved {} constraints", constraints.len());
|
||||||
|
|
||||||
|
let split_body = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(constraints)
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut first_found = false;
|
||||||
|
let mut num_rendered = 0;
|
||||||
|
let manual_state = &app.states.transactions.manual_data;
|
||||||
|
|
||||||
|
if manual_state.focus.is_none()
|
||||||
|
|| matches!(manual_state.focus.unwrap(), ManualDataFocus::Account)
|
||||||
|
{
|
||||||
|
let right_text = match manual_state.account.clone() {
|
||||||
|
Some(val) => val.acnt_dsply_name,
|
||||||
|
None => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
render_manual_row(
|
||||||
|
f,
|
||||||
|
split_body[num_rendered * 2],
|
||||||
|
"Account: ",
|
||||||
|
right_text,
|
||||||
|
manual_state.focus,
|
||||||
|
ManualDataFocus::Account
|
||||||
|
);
|
||||||
|
|
||||||
|
first_found = true;
|
||||||
|
num_rendered = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first_found && num_rendered < num_can_show)
|
||||||
|
|| matches!(manual_state.focus.unwrap(), ManualDataFocus::Amount)
|
||||||
|
{
|
||||||
|
let right_text = match manual_state.amount {
|
||||||
|
Some(val) => val.to_string(),
|
||||||
|
None => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
render_manual_row(
|
||||||
|
f,
|
||||||
|
split_body[num_rendered * 2],
|
||||||
|
"Amount: ",
|
||||||
|
right_text,
|
||||||
|
manual_state.focus,
|
||||||
|
ManualDataFocus::Amount
|
||||||
|
);
|
||||||
|
|
||||||
|
first_found = true;
|
||||||
|
num_rendered += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_manual_row<B: Backend>(
|
||||||
|
f: &mut Frame<B>,
|
||||||
|
row_area: Rect,
|
||||||
|
left_text: &str,
|
||||||
|
right_text: String,
|
||||||
|
focus: Option<ManualDataFocus>,
|
||||||
|
matching: ManualDataFocus,
|
||||||
|
) {
|
||||||
|
let is_active = focus.map(|focus| focus == matching).unwrap_or(false);
|
||||||
|
|
||||||
|
let horizontal_pieces = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
|
.split(row_area);
|
||||||
|
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(Text::styled(left_text, Style::default().fg(Color::Yellow))),
|
||||||
|
horizontal_pieces[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
let right_bg_color = if is_active {
|
||||||
|
Color::Yellow
|
||||||
|
} else {
|
||||||
|
Color::White
|
||||||
};
|
};
|
||||||
|
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(Text::styled(right_text, Style::default().fg(Color::Black))).block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::NONE)
|
||||||
|
.style(Style::default().bg(right_bg_color)),
|
||||||
|
),
|
||||||
|
horizontal_pieces[1],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_manual_entry <B: Backend> (f: &mut Frame<B>, area: Rect, app: &App) {
|
pub fn render_quick_entry<B: Backend>(f: &mut Frame<B>, area: Rect, app: &App) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn render_quick_entry<B: Backend> (f: &mut Frame<B>, area: Rect, app: &App) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue