desktop: add `bookmarks.toml` file for storing bookmark data

This is in a separate file to not create clutter in the main preferences.toml file.
This commit is contained in:
sleepycatcoding 2024-03-27 20:57:41 +02:00 committed by Nathan Adams
parent f657208860
commit 0b7b9eb0a1
3 changed files with 303 additions and 6 deletions

View File

@ -3,8 +3,8 @@ mod write;
use crate::cli::Opt;
use crate::log::FilenamePattern;
use crate::preferences::read::read_preferences;
use crate::preferences::write::PreferencesWriter;
use crate::preferences::read::{read_bookmarks, read_preferences};
use crate::preferences::write::{BookmarksWriter, PreferencesWriter};
use anyhow::{Context, Error};
use ruffle_core::backend::ui::US_ENGLISH;
use ruffle_render_wgpu::clap::{GraphicsBackend, PowerPreference};
@ -12,6 +12,7 @@ use std::sync::{Arc, Mutex};
use sys_locale::get_locale;
use toml_edit::DocumentMut;
use unic_langid::LanguageIdentifier;
use url::Url;
/// The preferences that relate to the application itself.
///
@ -34,6 +35,8 @@ pub struct GlobalPreferences {
/// The actual, mutable user preferences that are persisted to disk.
preferences: Arc<Mutex<PreferencesAndDocument>>,
bookmarks: Arc<Mutex<BookmarksAndDocument>>,
}
impl GlobalPreferences {
@ -56,9 +59,26 @@ impl GlobalPreferences {
Default::default()
};
let bookmarks_path = cli.config.join("bookmarks.toml");
let bookmarks = if bookmarks_path.exists() {
let contents = std::fs::read_to_string(&bookmarks_path)
.context("Failed to read saved bookmarks")?;
let (result, document) = read_bookmarks(&contents);
for warning in result.warnings {
tracing::warn!("{warning}");
}
BookmarksAndDocument {
toml_document: document,
values: result.result,
}
} else {
Default::default()
};
Ok(Self {
cli,
preferences: Arc::new(Mutex::new(preferences)),
bookmarks: Arc::new(Mutex::new(bookmarks)),
})
}
@ -127,6 +147,14 @@ impl GlobalPreferences {
.filename_pattern
}
pub fn bookmarks(&self, fun: impl FnOnce(&Vec<Bookmark>)) {
fun(&self
.bookmarks
.lock()
.expect("Bookmarks is no reentrant")
.values)
}
pub fn write_preferences(&self, fun: impl FnOnce(&mut PreferencesWriter)) -> Result<(), Error> {
let mut preferences = self
.preferences
@ -140,6 +168,17 @@ impl GlobalPreferences {
std::fs::write(self.cli.config.join("preferences.toml"), serialized)
.context("Could not write preferences to disk")
}
pub fn write_bookmarks(&self, fun: impl FnOnce(&mut BookmarksWriter)) -> Result<(), Error> {
let mut bookmarks = self.bookmarks.lock().expect("Bookmarks is not reentrant");
let mut writer = BookmarksWriter::new(&mut bookmarks);
fun(&mut writer);
let serialized = bookmarks.toml_document.to_string();
std::fs::write(self.cli.config.join("bookmarks.toml"), serialized)
.context("Could not write bookmarks to disk")
}
}
/// The actual preferences that are persisted to disk, and mutable at runtime.
@ -160,6 +199,14 @@ struct PreferencesAndDocument {
values: SavedGlobalPreferences,
}
#[derive(Default)]
struct BookmarksAndDocument {
/// The original toml document
toml_document: DocumentMut,
values: Vec<Bookmark>,
}
#[derive(PartialEq, Debug)]
pub struct SavedGlobalPreferences {
pub graphics_backend: GraphicsBackend,
@ -193,3 +240,8 @@ impl Default for SavedGlobalPreferences {
pub struct LogPreferences {
pub filename_pattern: FilenamePattern,
}
#[derive(Debug, PartialEq)]
pub struct Bookmark {
pub url: Url,
}

View File

@ -1,7 +1,7 @@
use crate::preferences::SavedGlobalPreferences;
use crate::preferences::{Bookmark, SavedGlobalPreferences};
use std::fmt;
use std::str::FromStr;
use toml_edit::{DocumentMut, Item, TableLike};
use toml_edit::{ArrayOfTables, DocumentMut, Item, Table, TableLike};
#[derive(Debug, PartialEq)]
pub struct ParseResult<T: PartialEq + fmt::Debug> {
@ -66,6 +66,29 @@ pub trait ReadExt<'a> {
}
}
fn get_array_of_tables(
&'a self,
cx: &mut ParseContext,
key: &'static str,
fun: impl FnOnce(&mut ParseContext, &ArrayOfTables),
) {
if let Some(item) = self.get_impl(key) {
cx.push_key(key);
if let Some(array) = item.as_array_of_tables() {
fun(cx, array);
} else {
cx.add_warning(format!(
"Invalid {}: expected array of tables but found {}",
cx.path(),
item.type_name()
));
}
cx.pop_key();
}
}
fn parse_from_str<T: FromStr>(&'a self, cx: &mut ParseContext, key: &'static str) -> Option<T> {
cx.push_key(key);
@ -155,6 +178,12 @@ impl<'a> ReadExt<'a> for Item {
}
}
impl<'a> ReadExt<'a> for Table {
fn get_impl(&'a self, key: &str) -> Option<&'a Item> {
self.get(key)
}
}
impl<'a> ReadExt<'a> for dyn TableLike + 'a {
fn get_impl(&'a self, key: &str) -> Option<&'a Item> {
self.get(key)
@ -219,6 +248,40 @@ pub fn read_preferences(input: &str) -> (ParseResult<SavedGlobalPreferences>, Do
(result, document)
}
pub fn read_bookmarks(input: &str) -> (ParseResult<Vec<Bookmark>>, DocumentMut) {
let mut result = ParseResult {
result: Default::default(),
warnings: vec![],
};
let document = match input.parse::<DocumentMut>() {
Ok(document) => document,
Err(e) => {
result.add_warning(format!("Invalid TOML: {e}"));
return (result, DocumentMut::default());
}
};
let mut cx = ParseContext::default();
document.get_array_of_tables(&mut cx, "bookmark", |cx, bookmarks| {
for bookmark in bookmarks.iter() {
let url = match bookmark.parse_from_str(cx, "url") {
Some(value) => value,
None => {
cx.add_warning("Missing bookmark.url".to_string());
continue;
}
};
result.result.push(Bookmark { url });
}
});
result.warnings = cx.warnings;
(result, document)
}
#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
use super::*;
@ -226,6 +289,7 @@ mod tests {
use crate::preferences::LogPreferences;
use fluent_templates::loader::langid;
use ruffle_render_wgpu::clap::{GraphicsBackend, PowerPreference};
use url::Url;
#[test]
fn invalid_toml() {
@ -535,4 +599,93 @@ mod tests {
read_preferences("log = \"yes\"").0
);
}
#[test]
fn bookmark() {
assert_eq!(
ParseResult {
result: vec![],
warnings: vec![
"Invalid bookmark: expected array of tables but found table".to_string()
]
},
read_bookmarks("[bookmark]").0
);
assert_eq!(
ParseResult {
result: vec![],
warnings: vec!["Missing bookmark.url".to_string()],
},
read_bookmarks("[[bookmark]]").0
);
assert_eq!(
ParseResult {
result: vec![],
warnings: vec!["Invalid bookmark.url: unsupported value \"invalid\"".to_string()],
},
read_bookmarks("[[bookmark]]\nurl = \"invalid\"").0,
);
}
#[test]
fn multiple_bookmarks() {
assert_eq!(
ParseResult {
result: vec![
Bookmark {
url: Url::from_str("file:///home/user/example.swf").unwrap(),
},
Bookmark {
url: Url::from_str("https://ruffle.rs/logo-anim.swf").unwrap(),
}
],
warnings: vec![],
},
read_bookmarks(
r#"
[[bookmark]]
url = "file:///home/user/example.swf"
[[bookmark]]
url = "https://ruffle.rs/logo-anim.swf"
"#
)
.0
);
assert_eq!(
ParseResult {
result: vec![
Bookmark {
url: Url::from_str("file:///home/user/example.swf").unwrap(),
},
Bookmark {
url: Url::from_str("https://ruffle.rs/logo-anim.swf").unwrap(),
}
],
warnings: vec![
"Invalid bookmark.url: unsupported value \"invalid\"".to_string(),
"Missing bookmark.url".to_string()
],
},
read_bookmarks(
r#"
[[bookmark]]
url = "file:///home/user/example.swf"
[[bookmark]]
url = "invalid"
[[bookmark]]
[[bookmark]]
url = "https://ruffle.rs/logo-anim.swf"
"#
)
.0
);
}
}

View File

@ -1,7 +1,7 @@
use crate::log::FilenamePattern;
use crate::preferences::PreferencesAndDocument;
use crate::preferences::{Bookmark, BookmarksAndDocument, PreferencesAndDocument};
use ruffle_render_wgpu::clap::{GraphicsBackend, PowerPreference};
use toml_edit::value;
use toml_edit::{array, value, Table};
use unic_langid::LanguageIdentifier;
pub struct PreferencesWriter<'a>(&'a mut PreferencesAndDocument);
@ -51,6 +51,57 @@ impl<'a> PreferencesWriter<'a> {
}
}
pub struct BookmarksWriter<'a>(&'a mut BookmarksAndDocument);
impl<'a> BookmarksWriter<'a> {
pub(super) fn new(bookmarks: &'a mut BookmarksAndDocument) -> Self {
Self(bookmarks)
}
pub fn add(&mut self, bookmark: Bookmark) {
// TODO: if more fields are added, this should use URL matching (e.g. other properties are ignored)
if !self.0.values.contains(&bookmark) {
if let Some(array) = self.0.toml_document["bookmark"]
.or_insert(array())
.as_array_of_tables_mut()
{
// TODO: If we add a BookmarkWriter use this here instead rather than duplicating the table write code.
let mut table = Table::new();
table["url"] = value(bookmark.url.to_string());
array.push(table);
self.0.values.push(bookmark);
} else {
// TODO: There is definitely a better way to handle this, then just logging a warning.
tracing::warn!("bookmark is not an array of tables, bookmarks will NOT be saved.");
}
}
}
pub fn remove(&mut self, index: usize) {
// We need to get the URL to find the bookmark in the TOML file, since index may not correspond to
// the same table entry (i.e. invalid tables and such that we want to keep intact for compatibility purposes)
let bookmark = self.0.values.remove(index);
// Remove the bookmark from the TOML file.
if let Some(array) = self.0.toml_document["bookmark"]
.or_insert(array())
.as_array_of_tables_mut()
{
let bookmark_url = bookmark.url.to_string();
array.retain(|x| {
if let Some(url) = x.get("url").and_then(|x| x.as_str()) {
return url != bookmark_url;
}
true
});
} else {
// TODO: We should add a way to return an error from write methods.
tracing::warn!("bookmark is not an array of tables, bookmarks will NOT be saved.");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -188,4 +239,45 @@ mod tests {
);
}
}
#[allow(clippy::unwrap_used)]
mod bookmarks {
use super::*;
use crate::preferences::read::read_bookmarks;
use std::str::FromStr;
define_serialization_test_helpers!(read_bookmarks, BookmarksAndDocument, BookmarksWriter);
#[test]
fn add_bookmark() {
test(
"",
|writer| {
writer.add(Bookmark {
url: url::Url::from_str("file:///home/user/example.swf").unwrap(),
})
},
"[[bookmark]]\nurl = \"file:///home/user/example.swf\"\n",
);
test("[[bookmark]]\nurl = \"file:///home/user/example.swf\"\n", |writer| writer.add(Bookmark {
url: url::Url::from_str("file:///home/user/another_file.swf").unwrap()
}), "[[bookmark]]\nurl = \"file:///home/user/example.swf\"\n\n[[bookmark]]\nurl = \"file:///home/user/another_file.swf\"\n");
}
#[test]
fn remove_bookmark() {
test(
"[[bookmark]]\nurl = \"file://home/user/example.swf\"\n\n[[bookmark]]\nurl = \"https://ruffle.rs/logo-anim.swf\"\n\n[[bookmark]]\nurl = \"file:///another_file.swf\"\n",
|writer| {
writer.remove(1);
},
"[[bookmark]]\nurl = \"file://home/user/example.swf\"\n\n[[bookmark]]\nurl = \"file:///another_file.swf\"\n",
);
// Test that we leave invalid bookmark tables intact when removing a bookmark.
test("[[bookmark]]\nurl = \"file://home/user/example.swf\"\n\n[[bookmark]]\n\n[[bookmark]]\nurl = \"https://ruffle.rs/logo-anim.swf\"\n\n[[bookmark]]\nurl = \"invalid\"\n", |writer| {
writer.remove(1);
}, "[[bookmark]]\nurl = \"file://home/user/example.swf\"\n\n[[bookmark]]\n\n[[bookmark]]\nurl = \"invalid\"\n")
}
}
}