mirror of
https://github.com/YGGverse/Yoda.git
synced 2026-04-02 01:25:27 +00:00
implement permanent storage for profile history
This commit is contained in:
parent
7803aa1c44
commit
a6107bf1bb
8 changed files with 310 additions and 73 deletions
80
src/app.rs
80
src/app.rs
|
|
@ -23,7 +23,7 @@ impl App {
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
/// Build new `Self`
|
/// Build new `Self`
|
||||||
pub fn build(profile: Profile) -> Self {
|
pub fn build(profile: Profile) -> Result<Self> {
|
||||||
// Init GTK
|
// Init GTK
|
||||||
let application = Application::builder()
|
let application = Application::builder()
|
||||||
.application_id(APPLICATION_ID)
|
.application_id(APPLICATION_ID)
|
||||||
|
|
@ -76,54 +76,56 @@ impl App {
|
||||||
let browser = browser.clone();
|
let browser = browser.clone();
|
||||||
let profile = profile.clone();
|
let profile = profile.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
// Init writable connection
|
match profile.save() {
|
||||||
match profile.database.connection.write() {
|
Ok(_) => match profile.database.connection.write() {
|
||||||
Ok(mut connection) => {
|
Ok(mut connection) => {
|
||||||
// Create transaction
|
// Create transaction
|
||||||
match connection.transaction() {
|
match connection.transaction() {
|
||||||
Ok(transaction) => {
|
Ok(transaction) => {
|
||||||
match database::select(&transaction) {
|
match database::select(&transaction) {
|
||||||
Ok(records) => {
|
Ok(records) => {
|
||||||
// Cleanup previous session records
|
// Cleanup previous session records
|
||||||
for record in records {
|
for record in records {
|
||||||
match database::delete(&transaction, record.id) {
|
match database::delete(&transaction, record.id) {
|
||||||
|
Ok(_) => {
|
||||||
|
// Delegate clean action to childs
|
||||||
|
if let Err(e) =
|
||||||
|
browser.clean(&transaction, record.id)
|
||||||
|
{
|
||||||
|
todo!("{e}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => todo!("{e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current session to DB
|
||||||
|
match database::insert(&transaction) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Delegate clean action to childs
|
// Delegate save action to childs
|
||||||
if let Err(e) =
|
if let Err(e) = browser.save(
|
||||||
browser.clean(&transaction, record.id)
|
&transaction,
|
||||||
{
|
database::last_insert_id(&transaction),
|
||||||
|
) {
|
||||||
todo!("{e}")
|
todo!("{e}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => todo!("{e}"),
|
Err(e) => todo!("{e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => todo!("{e}"),
|
||||||
// Save current session to DB
|
|
||||||
match database::insert(&transaction) {
|
|
||||||
Ok(_) => {
|
|
||||||
// Delegate save action to childs
|
|
||||||
if let Err(e) = browser.save(
|
|
||||||
&transaction,
|
|
||||||
database::last_insert_id(&transaction),
|
|
||||||
) {
|
|
||||||
todo!("{e}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => todo!("{e}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => todo!("{e}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm changes
|
// Confirm changes
|
||||||
if let Err(e) = transaction.commit() {
|
if let Err(e) = transaction.commit() {
|
||||||
todo!("{e}")
|
todo!("{e}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => todo!("{e}"),
|
||||||
}
|
}
|
||||||
Err(e) => todo!("{e}"),
|
|
||||||
}
|
}
|
||||||
}
|
Err(e) => todo!("{e}"),
|
||||||
|
},
|
||||||
Err(e) => todo!("{e}"),
|
Err(e) => todo!("{e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -254,10 +256,10 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return activated App struct
|
// Return activated App struct
|
||||||
Self {
|
Ok(Self {
|
||||||
profile: profile.clone(),
|
profile: profile.clone(),
|
||||||
application,
|
application,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,12 @@ fn main() -> ExitCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
match Profile::init() {
|
match Profile::init() {
|
||||||
Ok(profile) => match App::build(profile).run() {
|
Ok(profile) => match App::build(profile) {
|
||||||
Ok(app) => return app,
|
Ok(app) => match app.run() {
|
||||||
Err(e) => eprintln!("Failed to initialize application: {e}"),
|
Ok(run) => return run,
|
||||||
|
Err(e) => eprintln!("Failed to run application: {e}"),
|
||||||
|
},
|
||||||
|
Err(e) => eprintln!("Failed to build application: {e}"),
|
||||||
},
|
},
|
||||||
Err(e) => eprintln!("Failed to initialize profile: {e}"),
|
Err(e) => eprintln!("Failed to initialize profile: {e}"),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ impl Profile {
|
||||||
|
|
||||||
// Init components
|
// Init components
|
||||||
let bookmark = Rc::new(Bookmark::build(&connection, &profile_id)?);
|
let bookmark = Rc::new(Bookmark::build(&connection, &profile_id)?);
|
||||||
let history = Rc::new(History::build(&connection, &profile_id));
|
let history = Rc::new(History::build(&connection, &profile_id)?);
|
||||||
let search = Rc::new(Search::build(&connection, &profile_id)?);
|
let search = Rc::new(Search::build(&connection, &profile_id)?);
|
||||||
let identity = Rc::new(Identity::build(&connection, &profile_id)?);
|
let identity = Rc::new(Identity::build(&connection, &profile_id)?);
|
||||||
|
|
||||||
|
|
@ -93,6 +93,12 @@ impl Profile {
|
||||||
config_path,
|
config_path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
pub fn save(&self) -> Result<()> {
|
||||||
|
self.history.save()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn migrate(tx: &Transaction) -> Result<()> {
|
pub fn migrate(tx: &Transaction) -> Result<()> {
|
||||||
|
|
@ -103,8 +109,7 @@ pub fn migrate(tx: &Transaction) -> Result<()> {
|
||||||
bookmark::migrate(tx)?;
|
bookmark::migrate(tx)?;
|
||||||
identity::migrate(tx)?;
|
identity::migrate(tx)?;
|
||||||
search::migrate(tx)?;
|
search::migrate(tx)?;
|
||||||
// @TODO not in use yet
|
history::migrate(tx)?;
|
||||||
// history::migrate(tx)?;
|
|
||||||
|
|
||||||
// Success
|
// Success
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,8 @@ pub fn init(tx: &Transaction) -> Result<usize> {
|
||||||
`request` TEXT NOT NULL,
|
`request` TEXT NOT NULL,
|
||||||
`title` TEXT NULL,
|
`title` TEXT NULL,
|
||||||
|
|
||||||
FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`)
|
FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`),
|
||||||
|
UNIQUE (`profile_id`, `request`)
|
||||||
)",
|
)",
|
||||||
[],
|
[],
|
||||||
)?)
|
)?)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
// mod database;
|
mod database;
|
||||||
mod item;
|
mod item;
|
||||||
mod memory;
|
mod memory;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use database::Database;
|
||||||
use gtk::glib::GString;
|
use gtk::glib::GString;
|
||||||
use item::Item;
|
use item::{Event, Item};
|
||||||
use memory::Memory;
|
use memory::Memory;
|
||||||
use sqlite::Connection;
|
use sqlite::{Connection, Transaction};
|
||||||
use std::{cell::RefCell, rc::Rc, sync::RwLock};
|
use std::{cell::RefCell, rc::Rc, sync::RwLock};
|
||||||
|
|
||||||
pub struct History {
|
pub struct History {
|
||||||
|
database: Database, // permanent storage
|
||||||
memory: RefCell<Memory>, // fast search index
|
memory: RefCell<Memory>, // fast search index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -16,12 +19,35 @@ impl History {
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
/// Create new `Self`
|
/// Create new `Self`
|
||||||
pub fn build(_connection: &Rc<RwLock<Connection>>, _profile_id: &Rc<i64>) -> Self {
|
pub fn build(connection: &Rc<RwLock<Connection>>, profile_id: &Rc<i64>) -> Result<Self> {
|
||||||
// Init children components
|
// Init children components
|
||||||
|
let database = Database::build(connection, profile_id);
|
||||||
let memory = RefCell::new(Memory::new());
|
let memory = RefCell::new(Memory::new());
|
||||||
|
|
||||||
|
for item in database.records(None, None)? {
|
||||||
|
memory.borrow_mut().add(item)
|
||||||
|
}
|
||||||
|
|
||||||
// Return new `Self`
|
// Return new `Self`
|
||||||
Self { memory }
|
Ok(Self { database, memory })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
pub fn save(&self) -> Result<()> {
|
||||||
|
for item in self.memory.borrow().items() {
|
||||||
|
if !item.is_saved {
|
||||||
|
match item.id {
|
||||||
|
Some(_) => {
|
||||||
|
self.database.update(item)?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.database.add(item)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
@ -30,7 +56,14 @@ impl History {
|
||||||
pub fn open(&self, request: GString, title: Option<GString>) {
|
pub fn open(&self, request: GString, title: Option<GString>) {
|
||||||
let mut memory = self.memory.borrow_mut();
|
let mut memory = self.memory.borrow_mut();
|
||||||
if !memory.open(&request) {
|
if !memory.open(&request) {
|
||||||
memory.add(Item::init(request, title))
|
memory.add(Item {
|
||||||
|
id: None,
|
||||||
|
request,
|
||||||
|
title,
|
||||||
|
opened: Event::new(),
|
||||||
|
closed: None,
|
||||||
|
is_saved: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,3 +89,16 @@ impl History {
|
||||||
self.memory.borrow().contains_request(request, limit)
|
self.memory.borrow().contains_request(request, limit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
|
||||||
|
pub fn migrate(tx: &Transaction) -> Result<()> {
|
||||||
|
// Migrate self components
|
||||||
|
database::init(tx)?;
|
||||||
|
|
||||||
|
// Delegate migration to childs
|
||||||
|
// nothing yet..
|
||||||
|
|
||||||
|
// Success
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
183
src/profile/history/database.rs
Normal file
183
src/profile/history/database.rs
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
use super::{item::Event, Item};
|
||||||
|
use anyhow::Result;
|
||||||
|
use gtk::glib::DateTime;
|
||||||
|
use sqlite::{Connection, Transaction};
|
||||||
|
use std::{rc::Rc, sync::RwLock};
|
||||||
|
|
||||||
|
pub struct Database {
|
||||||
|
connection: Rc<RwLock<Connection>>,
|
||||||
|
profile_id: Rc<i64>, // multi-profile relationship
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
/// Create new `Self`
|
||||||
|
pub fn build(connection: &Rc<RwLock<Connection>>, profile_id: &Rc<i64>) -> Self {
|
||||||
|
Self {
|
||||||
|
connection: connection.clone(),
|
||||||
|
profile_id: profile_id.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
|
||||||
|
/// Get history records from database with optional filter by `request`
|
||||||
|
pub fn records(&self, request: Option<&str>, title: Option<&str>) -> Result<Vec<Item>> {
|
||||||
|
let readable = self.connection.read().unwrap(); // @TODO
|
||||||
|
let tx = readable.unchecked_transaction()?;
|
||||||
|
select(&tx, *self.profile_id, request, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
/// Create new history record in database
|
||||||
|
/// * return last insert ID on success
|
||||||
|
pub fn add(&self, item: &Item) -> Result<i64> {
|
||||||
|
let mut writable = self.connection.write().unwrap(); // @TODO
|
||||||
|
let tx = writable.transaction()?;
|
||||||
|
let id = insert(&tx, *self.profile_id, item)?;
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&self, item: &Item) -> Result<usize> {
|
||||||
|
let mut writable = self.connection.write().unwrap(); // @TODO
|
||||||
|
let tx = writable.transaction()?;
|
||||||
|
let affected = update(&tx, *self.profile_id, item)?;
|
||||||
|
tx.commit()?;
|
||||||
|
Ok(affected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Low-level DB API
|
||||||
|
|
||||||
|
pub fn init(tx: &Transaction) -> Result<usize> {
|
||||||
|
Ok(tx.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `profile_history`
|
||||||
|
(
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
`profile_id` INTEGER NOT NULL,
|
||||||
|
`opened_time` INTEGER NOT NULL,
|
||||||
|
`opened_count` INTEGER NOT NULL,
|
||||||
|
`closed_time` INTEGER NULL,
|
||||||
|
`closed_count` INTEGER NULL,
|
||||||
|
`request` TEXT NOT NULL,
|
||||||
|
`title` TEXT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (`profile_id`) REFERENCES `profile` (`id`),
|
||||||
|
UNIQUE (`profile_id`, `request`)
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(tx: &Transaction, profile_id: i64, item: &Item) -> Result<i64> {
|
||||||
|
tx.execute(
|
||||||
|
"INSERT INTO `profile_history` (
|
||||||
|
`profile_id`,
|
||||||
|
`opened_time`,
|
||||||
|
`opened_count`,
|
||||||
|
`closed_time`,
|
||||||
|
`closed_count`,
|
||||||
|
`request`,
|
||||||
|
`title`
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
(
|
||||||
|
profile_id,
|
||||||
|
item.opened.time.to_unix(),
|
||||||
|
item.opened.count as i64,
|
||||||
|
item.closed.as_ref().map(|closed| closed.time.to_unix()),
|
||||||
|
item.closed.as_ref().map(|closed| closed.count as i64),
|
||||||
|
item.request.as_str(),
|
||||||
|
item.title.as_deref(),
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
Ok(tx.last_insert_rowid())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(tx: &Transaction, profile_id: i64, item: &Item) -> Result<usize> {
|
||||||
|
Ok(tx.execute(
|
||||||
|
"UPDATE `profile_history`
|
||||||
|
SET `opened_time` = ?,
|
||||||
|
`opened_count` = ?,
|
||||||
|
`closed_time` = ?,
|
||||||
|
`closed_count` = ?,
|
||||||
|
`request` = ?,
|
||||||
|
`title` = ?
|
||||||
|
WHERE `id` = ? AND `profile_id` = ?",
|
||||||
|
(
|
||||||
|
item.opened.time.to_unix(),
|
||||||
|
item.opened.count as i64,
|
||||||
|
item.closed.as_ref().map(|closed| closed.time.to_unix()),
|
||||||
|
item.closed.as_ref().map(|closed| closed.count as i64),
|
||||||
|
item.request.as_str(),
|
||||||
|
item.title.as_deref(),
|
||||||
|
item.id.unwrap(),
|
||||||
|
profile_id,
|
||||||
|
),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select(
|
||||||
|
tx: &Transaction,
|
||||||
|
profile_id: i64,
|
||||||
|
request: Option<&str>,
|
||||||
|
title: Option<&str>,
|
||||||
|
) -> Result<Vec<Item>> {
|
||||||
|
let mut stmt = tx.prepare(
|
||||||
|
"SELECT
|
||||||
|
`id`,
|
||||||
|
`profile_id`,
|
||||||
|
`opened_time`,
|
||||||
|
`opened_count`,
|
||||||
|
`closed_time`,
|
||||||
|
`closed_count`,
|
||||||
|
`request`,
|
||||||
|
`title`
|
||||||
|
FROM `profile_history`
|
||||||
|
WHERE `profile_id` = ? AND (`request` LIKE ? OR `title` LIKE ?)",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let result = stmt.query_map(
|
||||||
|
(profile_id, request.unwrap_or("%"), title.unwrap_or("%")),
|
||||||
|
|row| {
|
||||||
|
Ok(Item {
|
||||||
|
id: row.get(0)?,
|
||||||
|
//profile_id: row.get(1)?,
|
||||||
|
opened: Event {
|
||||||
|
time: DateTime::from_unix_local(row.get(2)?).unwrap(),
|
||||||
|
count: row.get(3)?,
|
||||||
|
},
|
||||||
|
closed: closed(row.get(4)?, row.get(5)?),
|
||||||
|
request: row.get::<_, String>(6)?.into(),
|
||||||
|
title: row.get::<_, Option<String>>(7)?.map(|s| s.into()),
|
||||||
|
is_saved: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
for record in result {
|
||||||
|
let item = record?;
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
|
||||||
|
fn closed(time: Option<i64>, count: Option<i64>) -> Option<Event> {
|
||||||
|
if let Some(t) = time {
|
||||||
|
if let Some(c) = count {
|
||||||
|
return Some(Event {
|
||||||
|
time: DateTime::from_unix_local(t).unwrap(),
|
||||||
|
count: c as usize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
mod event;
|
pub mod event;
|
||||||
|
pub use event::Event;
|
||||||
|
|
||||||
use event::Event;
|
|
||||||
use gtk::glib::GString;
|
use gtk::glib::GString;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -18,25 +18,17 @@ pub struct Item {
|
||||||
/// Collect `Item` close events
|
/// Collect `Item` close events
|
||||||
/// * used in recently closed pages menu and history page
|
/// * used in recently closed pages menu and history page
|
||||||
pub closed: Option<Event>,
|
pub closed: Option<Event>,
|
||||||
|
/// Mark in-memory `Item` as saved
|
||||||
|
/// * used for database update (e.g. on app close)
|
||||||
|
pub is_saved: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item {
|
impl Item {
|
||||||
// Constructors
|
|
||||||
|
|
||||||
pub fn init(request: GString, title: Option<GString>) -> Self {
|
|
||||||
Self {
|
|
||||||
id: None,
|
|
||||||
request,
|
|
||||||
title,
|
|
||||||
opened: Event::new(),
|
|
||||||
closed: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
|
|
||||||
pub fn open(&mut self) {
|
pub fn open(&mut self) {
|
||||||
self.opened.pulse()
|
self.opened.pulse();
|
||||||
|
self.is_saved = false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close(&mut self) {
|
pub fn close(&mut self) {
|
||||||
|
|
@ -44,5 +36,6 @@ impl Item {
|
||||||
Some(ref mut closed) => closed.pulse(),
|
Some(ref mut closed) => closed.pulse(),
|
||||||
None => self.closed = Some(Event::new()),
|
None => self.closed = Some(Event::new()),
|
||||||
}
|
}
|
||||||
|
self.is_saved = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@ impl Memory {
|
||||||
/// Update `opened` time for given `request`
|
/// Update `opened` time for given `request`
|
||||||
/// * return `false` if the `request` not found in memory index
|
/// * return `false` if the `request` not found in memory index
|
||||||
pub fn open(&mut self, request: &str) -> bool {
|
pub fn open(&mut self, request: &str) -> bool {
|
||||||
for record in &mut self.0 {
|
for item in &mut self.0 {
|
||||||
if record.request == request {
|
if item.request == request {
|
||||||
record.open();
|
item.open();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -32,9 +32,9 @@ impl Memory {
|
||||||
|
|
||||||
/// Update `closed` time for given `request`
|
/// Update `closed` time for given `request`
|
||||||
pub fn close(&mut self, request: &str) {
|
pub fn close(&mut self, request: &str) {
|
||||||
for record in &mut self.0 {
|
for item in &mut self.0 {
|
||||||
if record.request == request {
|
if item.request == request {
|
||||||
record.close();
|
item.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +102,10 @@ impl Memory {
|
||||||
}
|
}
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn items(&self) -> &Vec<Item> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Memory {
|
impl Default for Memory {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue