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:
parent
f657208860
commit
0b7b9eb0a1
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue