Files
tools/publish/src/app.rs
T

1331 lines
52 KiB
Rust
Raw Normal View History

2026-08-05 11:21:25 -04:00
use crate::navigator::Navigator;
2026-09-02 23:05:29 -04:00
use crate::rdf::conversion::term_to_named_node;
2026-06-08 19:33:49 -04:00
use crate::rdf::term_helper::{TermHelper, TermHelperMut};
2026-09-02 23:05:29 -04:00
use crate::tasks;
2026-08-05 11:21:25 -04:00
use crate::widget::iri_input::iri_input;
use crate::widget::navigation_area::navigation_area;
2026-09-02 23:05:29 -04:00
use gl_graph::class::Class;
use gl_graph::ontology::{ConnectedOntology, OntologyBuilder, ReadOnlyEntity, ResourceDescription};
use gl_graph::{CurieHelper, language, vocab};
use gl_search::tantivy::schema::Value;
2026-08-12 14:11:47 -04:00
use gl_search::{Schema, SearchIndex};
2026-06-08 19:33:49 -04:00
use http::StatusCode;
2026-09-02 23:05:29 -04:00
use iced::advanced::text::Wrapping;
2026-06-08 19:33:49 -04:00
use iced::alignment::Horizontal;
2026-08-05 11:21:25 -04:00
use iced::keyboard::{Event, key};
2026-09-02 23:05:29 -04:00
use iced::task::sipper;
2026-06-08 19:33:49 -04:00
use iced::widget::button::Style;
2026-08-24 21:44:23 -04:00
use iced::widget::grid::Sizing;
2026-09-02 23:05:29 -04:00
use iced::widget::{
button, center, column, combo_box, container, grid, mouse_area, opaque, operation, pick_list,
row, scrollable, space, stack, table, text, text_input, toggler,
};
use iced::window::Settings;
use iced::{
Background, Color, Element, Font, Length, Subscription, Task, color, font, keyboard, window,
};
2026-06-08 19:33:49 -04:00
use ldp::middleware::BasicAuthMiddleware;
use ldp::model::{KeyedDataset, QuadKey};
use ldp::reqwest::{Client, Url};
use ldp::reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions};
use oxigraph::io::RdfFormat;
2026-09-02 23:05:29 -04:00
use oxigraph::model::{
BaseDirection, Graph, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Quad,
Term, TermRef, Triple, TripleRef,
};
use rfd::FileHandle;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::ops::Sub;
use std::path::PathBuf;
use tokio::io::AsyncWriteExt;
use tonic::codegen::tokio_stream::StreamExt;
2026-08-30 18:50:48 -04:00
use tracing::{debug, error, trace};
2026-06-08 19:33:49 -04:00
2026-06-30 19:25:15 -04:00
#[derive(Clone, Debug)]
2026-06-08 19:33:49 -04:00
pub(crate) enum Message {
None,
2026-08-20 20:43:18 -04:00
ConnectToOntologyService(String),
2026-08-22 23:31:14 -04:00
ConnectedToOntologyService(ConnectedOntology),
2026-08-20 20:43:18 -04:00
CacheDatatypes(HashMap<NamedNode, ResourceDescription>),
CacheReadOnlyEntities(HashSet<ReadOnlyEntity>),
PopulateCaches,
2026-08-26 20:43:01 -04:00
LookupResourceDescriptions(HashSet<NamedNode>),
2026-08-23 17:01:13 -04:00
CacheResourceDescriptions(HashMap<NamedNode, ResourceDescription>),
2026-06-08 19:33:49 -04:00
WindowClosed(window::Id),
URLInputChanged(String),
URLInputSubmitted,
2026-07-14 21:31:15 -04:00
NavigateTo(Url),
2026-06-08 19:33:49 -04:00
FetchDocument(Url),
LoadDocument(RdfSource<KeyedDataset<RowState>>),
2026-08-26 19:44:18 -04:00
ResourceNotFound,
2026-06-08 19:33:49 -04:00
ShowNewDocumentButtons,
HideNewDocumentButtons,
ShowError(String),
2026-08-07 19:28:08 -04:00
AddRow(Option<(QuadKey, RowState)>),
2026-06-08 19:33:49 -04:00
DeleteRow(QuadKey),
OpenQueryWindow(SearchResultClickAction),
HoverRow(QuadKey),
UnhoverRow(QuadKey),
QueryUpdated(String),
2026-08-20 20:43:18 -04:00
SetSearchResults(Vec<HashMap<gl_search::Field, gl_search::OwnedValue>>),
UpdateQueryClass(Class),
2026-06-08 19:33:49 -04:00
SearchResultClicked(NamedNode),
DatatypeUpdated(QuadKey, Option<String>),
LanguageUpdated(QuadKey, Option<String>),
2026-07-24 19:25:07 -04:00
SubjectUpdated(QuadKey, String),
2026-07-08 21:07:52 -04:00
PredicateUpdated(QuadKey, String),
ObjectUpdated(QuadKey, String),
2026-06-08 19:33:49 -04:00
DirectionToggled(QuadKey, BaseDirection),
SaveGraph(bool),
ShowOverwriteConfirmationModal,
HideOverwriteConfirmationModal,
ConfirmOverwrite,
2026-09-02 23:05:29 -04:00
DownloadResource,
DownloadLocationSelected(Option<PathBuf>),
DownloadProgress((usize, Option<usize>)),
2026-08-26 19:44:18 -04:00
NewDocument(Class, Option<Url>),
2026-07-12 13:52:13 -04:00
NavigateToPredicate(QuadKey),
NavigateToObject(QuadKey),
2026-07-14 21:31:15 -04:00
NavigateBack,
NavigateForward,
2026-06-08 19:33:49 -04:00
ResetState,
2026-08-07 19:28:08 -04:00
RunInference,
2026-08-20 20:43:18 -04:00
SetInferredTriples(Graph),
2026-08-30 18:50:48 -04:00
ShowInferencesWindow,
2026-08-31 13:45:15 -04:00
ShowReadOnlyWindow,
2026-08-05 11:21:25 -04:00
Event(Event),
2026-06-08 19:33:49 -04:00
}
#[derive(Default, Debug, Clone)]
pub(crate) struct RowState {
read_only: bool,
datatype_state: combo_box::State<String>,
}
#[derive(Debug, Clone)]
pub(crate) enum SearchResultClickAction {
URLInput,
Predicate(QuadKey),
Object(QuadKey),
}
struct SearchState {
window_id: window::Id,
action: SearchResultClickAction,
2026-08-20 20:43:18 -04:00
entity_class_selection: Option<Class>,
2026-06-08 19:33:49 -04:00
query: String,
2026-08-20 20:43:18 -04:00
results: Vec<HashMap<gl_search::Field, gl_search::OwnedValue>>,
2026-06-08 19:33:49 -04:00
}
2026-09-02 23:05:29 -04:00
enum DownloadState {
Idle,
InProgress(FileHandle, usize),
}
2026-06-08 19:33:49 -04:00
pub(crate) struct Publisher {
http_client: ClientWithMiddleware,
2026-07-13 14:05:40 -04:00
curie_helper: CurieHelper,
2026-08-22 23:31:14 -04:00
ontology: Option<ConnectedOntology>,
2026-08-20 20:43:18 -04:00
read_only_entities: HashSet<ReadOnlyEntity>,
2026-06-08 19:33:49 -04:00
abbreviated_datatypes: Vec<String>,
2026-08-20 20:43:18 -04:00
resource_descriptions: HashMap<NamedNode, ResourceDescription>,
2026-06-08 19:33:49 -04:00
window_id: window::Id,
2026-08-30 18:50:48 -04:00
inference_window_id: Option<window::Id>,
2026-08-31 13:45:15 -04:00
read_only_window_id: Option<window::Id>,
2026-06-08 19:33:49 -04:00
url_input: String,
2026-07-28 15:09:33 -04:00
url_input_valid: bool,
2026-07-14 21:31:15 -04:00
navigator: Navigator<Url>,
2026-06-08 19:33:49 -04:00
document: RdfSource<KeyedDataset<RowState>>,
2026-09-02 23:05:29 -04:00
download_state: DownloadState,
2026-08-07 19:28:08 -04:00
inferred_triples: Graph,
2026-06-08 19:33:49 -04:00
hovered_row: Option<QuadKey>,
search_state: Option<SearchState>,
index: SearchIndex,
show_overwrite_confirmation: bool,
modified: bool,
show_new_document_buttons: bool,
2026-08-26 19:44:18 -04:00
resource_not_found: bool,
2026-06-08 19:33:49 -04:00
}
impl Publisher {
2026-08-20 20:43:18 -04:00
fn is_read_only<'a>(&'a self, triple: impl Into<TripleRef<'a>>) -> bool {
let triple = triple.into();
2026-09-02 23:05:29 -04:00
if triple.predicate == vocab::rdf::TYPE
&& let TermRef::NamedNode(node) = triple.object
{
self.read_only_entities
.contains(&ReadOnlyEntity::Class(node.into_owned()))
2026-08-20 20:43:18 -04:00
} else {
2026-09-02 23:05:29 -04:00
self.read_only_entities
.contains(&ReadOnlyEntity::Property(triple.predicate.into_owned()))
2026-08-20 20:43:18 -04:00
}
}
2026-07-13 14:05:40 -04:00
2026-08-20 20:43:18 -04:00
pub(crate) fn new() -> (Self, Task<Message>) {
let curie_helper = CurieHelper::new(gl_graph::PREFIXES.clone());
2026-06-08 19:33:49 -04:00
2026-06-30 19:25:15 -04:00
let index = SearchIndex::builder()
2026-07-02 14:09:54 -04:00
.with_path("/home/alex/.local/share/org.graphofliberty.desktop/index")
2026-06-30 19:25:15 -04:00
.build()
.expect("Failed to build search index");
2026-06-08 19:33:49 -04:00
let (id, task) = window::open(Settings::default());
let client = Client::new();
let http_client = ClientBuilder::new(client.clone())
.with(BasicAuthMiddleware::new(
"fedoraAdmin".to_string(),
Some("fedoraAdmin".to_string()),
))
.build();
2026-07-14 21:31:15 -04:00
let starting_url = Url::parse("http://fedora.quill.lan/rest/").unwrap();
let navigator = Navigator::new(starting_url.clone());
let document = RdfSource::new(starting_url.clone());
2026-06-08 19:33:49 -04:00
(
Self {
http_client,
2026-07-13 14:05:40 -04:00
curie_helper,
2026-08-20 20:43:18 -04:00
ontology: None,
read_only_entities: HashSet::new(),
abbreviated_datatypes: Vec::new(),
resource_descriptions: HashMap::new(),
2026-06-08 19:33:49 -04:00
window_id: id,
2026-08-30 18:50:48 -04:00
inference_window_id: None,
2026-08-31 13:45:15 -04:00
read_only_window_id: None,
2026-07-14 21:31:15 -04:00
url_input: starting_url.to_string(),
2026-07-28 15:09:33 -04:00
url_input_valid: true,
2026-07-14 21:31:15 -04:00
navigator,
2026-06-08 19:33:49 -04:00
document,
2026-09-02 23:05:29 -04:00
download_state: DownloadState::Idle,
2026-08-07 19:28:08 -04:00
inferred_triples: Graph::new(),
2026-06-08 19:33:49 -04:00
hovered_row: None,
search_state: None,
index,
show_overwrite_confirmation: false,
modified: false,
show_new_document_buttons: false,
2026-08-26 19:44:18 -04:00
resource_not_found: false,
2026-06-08 19:33:49 -04:00
},
2026-08-20 20:43:18 -04:00
task.map(|_| Message::ConnectToOntologyService("http://[::1]:3000".to_string())),
2026-06-08 19:33:49 -04:00
)
}
pub(crate) fn update(&mut self, message: Message) -> Task<Message> {
let mut task = Task::none();
2026-06-30 19:25:15 -04:00
trace!(?message);
2026-06-08 19:33:49 -04:00
match message {
2026-08-20 20:43:18 -04:00
Message::ConnectToOntologyService(endpoint) => {
2026-09-02 23:05:29 -04:00
let builder =
OntologyBuilder::from_string(&endpoint, language::ENGLISH_OR_UNTAGGED.clone())
.expect("Unable to parse endpoint of ontology service");
2026-08-22 23:31:14 -04:00
task = tasks::connect_to_ontology_service(builder)
2026-08-20 20:43:18 -04:00
.chain(Task::done(Message::PopulateCaches));
}
Message::ConnectedToOntologyService(ontology) => {
self.ontology = Some(ontology);
}
Message::PopulateCaches => {
if let Some(ontology) = &mut self.ontology {
2026-09-02 23:05:29 -04:00
let classes = Class::ALL
.iter()
.map(|class| class.to_named_node().into_owned());
2026-08-24 21:44:23 -04:00
2026-08-20 20:43:18 -04:00
task = Task::batch([
tasks::list_datatypes(ontology.clone()),
tasks::list_read_only_entities(ontology.clone()),
2026-08-24 21:44:23 -04:00
tasks::lookup_resource_descriptions(ontology.clone(), classes),
2026-08-20 20:43:18 -04:00
])
}
}
Message::CacheDatatypes(datatypes) => {
2026-09-02 23:05:29 -04:00
self.abbreviated_datatypes = datatypes
.keys()
2026-08-20 20:43:18 -04:00
.map(|node| {
2026-09-02 23:05:29 -04:00
self.curie_helper
.abbreviate(None, node.as_str())
2026-08-20 20:43:18 -04:00
.unwrap_or(node.as_str().to_string())
2026-09-02 23:05:29 -04:00
})
.collect();
2026-08-20 20:43:18 -04:00
}
Message::CacheReadOnlyEntities(entities) => {
self.read_only_entities = entities;
}
2026-08-23 17:01:13 -04:00
Message::LookupResourceDescriptions(nodes) => {
let provided_nodes: HashSet<NamedNode> = HashSet::from_iter(nodes);
2026-09-02 23:05:29 -04:00
let nodes_with_known_descriptions =
HashSet::from_iter(self.resource_descriptions.keys().cloned());
2026-08-23 17:01:13 -04:00
let nodes_to_look_up = provided_nodes.sub(&nodes_with_known_descriptions);
if let Some(ontology) = &mut self.ontology {
task = tasks::lookup_resource_descriptions(ontology.clone(), nodes_to_look_up)
2026-08-20 20:43:18 -04:00
}
}
2026-08-23 17:01:13 -04:00
Message::CacheResourceDescriptions(descriptions) => {
self.resource_descriptions.extend(descriptions);
2026-08-20 20:43:18 -04:00
}
2026-06-08 19:33:49 -04:00
Message::WindowClosed(id) => {
if self.window_id == id {
task = iced::exit();
2026-09-02 23:05:29 -04:00
} else if let Some(search_state) = &self.search_state
&& search_state.window_id == id
{
2026-06-08 19:33:49 -04:00
self.search_state = None;
2026-09-02 23:05:29 -04:00
} else if let Some(infrence_window_id) = self.inference_window_id
&& infrence_window_id == id
{
2026-08-31 13:45:15 -04:00
self.inference_window_id = None;
2026-09-02 23:05:29 -04:00
} else if let Some(read_only_window_id) = self.read_only_window_id
&& read_only_window_id == id
{
self.read_only_window_id = None;
2026-06-08 19:33:49 -04:00
}
}
Message::URLInputChanged(value) => {
2026-08-28 15:57:59 -04:00
self.url_input_valid = Url::parse(&value).is_ok();
2026-06-08 19:33:49 -04:00
self.url_input = value;
}
Message::URLInputSubmitted => {
2026-07-30 12:40:22 -04:00
if let Ok(url) = Url::parse(&self.url_input) {
if &url != self.navigator.current() {
self.navigator.goto(url.clone());
}
task = Task::done(Message::NavigateTo(url));
2026-07-20 23:53:33 -04:00
}
2026-07-14 21:31:15 -04:00
}
Message::NavigateTo(url) => {
2026-09-02 23:05:29 -04:00
task =
Task::done(Message::ResetState).chain(Task::done(Message::FetchDocument(url)));
2026-06-08 19:33:49 -04:00
}
Message::FetchDocument(url) => {
let client = self.http_client.clone();
2026-09-02 23:05:29 -04:00
task = Task::perform(
async {
let request = ResourceRequestBuilder::with_client_and_url(client, url)
.accept_rdf_format(RdfFormat::Turtle)
.follow_described_by(true)
.build();
2026-06-08 19:33:49 -04:00
2026-09-02 23:05:29 -04:00
let response = request.send().await?;
response.into_rdf_source().await
},
|result| match result {
Ok(document) => Message::LoadDocument(document),
Err(ldp::Error::Reqwest(err))
if err.status() == Some(StatusCode::NOT_FOUND) =>
{
Message::ResourceNotFound
2026-06-08 19:33:49 -04:00
}
2026-09-02 23:05:29 -04:00
Err(err) => Message::ShowError(err.to_string()),
},
);
2026-06-08 19:33:49 -04:00
}
2026-08-26 19:44:18 -04:00
Message::ResourceNotFound => {
task = Task::done(Message::ShowNewDocumentButtons);
2026-09-02 23:05:29 -04:00
self.resource_not_found = true;
2026-08-26 19:44:18 -04:00
}
2026-06-08 19:33:49 -04:00
Message::LoadDocument(document) => {
2026-09-02 23:05:29 -04:00
let nodes_to_look_up = document
.dataset()
.quads
.iter()
.flat_map(|(_, quad)| {
let mut nodes = vec![quad.predicate.clone()];
if let Some(object) = term_to_named_node(&quad.object) {
nodes.push(object.clone());
}
nodes
})
.collect();
2026-08-24 21:44:23 -04:00
2026-08-20 20:43:18 -04:00
let add_row_tasks = document.dataset().quads.iter().map(|(key, quad)| {
2026-08-08 23:50:04 -04:00
let datatype_state = combo_box::State::new(self.abbreviated_datatypes.clone());
let state = RowState {
2026-08-20 20:43:18 -04:00
read_only: self.is_read_only(quad.as_ref()),
2026-08-08 23:50:04 -04:00
datatype_state,
};
2026-08-20 20:43:18 -04:00
Task::done(Message::AddRow(Some((key, state))))
2026-08-08 23:50:04 -04:00
});
2026-08-24 21:44:23 -04:00
task = Task::done(Message::LookupResourceDescriptions(nodes_to_look_up))
.chain(Task::batch(add_row_tasks))
2026-08-07 19:28:08 -04:00
.chain(Task::done(Message::RunInference))
.chain(Task::done(Message::ResetState));
2026-06-08 19:33:49 -04:00
self.document = document;
}
Message::ShowError(error) => {
error!(error);
}
Message::AddRow(None) => {
2026-07-10 23:14:50 -04:00
let mut quad = self.document.new_quad();
let empty_node = NamedNode::new_unchecked("");
quad.predicate = empty_node.clone();
quad.object = Term::NamedNode(empty_node);
2026-06-08 19:33:49 -04:00
let key = self.document.dataset_mut().quads.insert(quad);
2026-08-08 23:50:04 -04:00
let datatype_state = combo_box::State::new(self.abbreviated_datatypes.clone());
2026-06-08 19:33:49 -04:00
let state = RowState {
2026-08-07 19:28:08 -04:00
read_only: false,
2026-06-08 19:33:49 -04:00
datatype_state,
};
2026-08-07 19:28:08 -04:00
task = Task::done(Message::AddRow(Some((key, state))));
}
Message::AddRow(Some((key, state))) => {
2026-06-08 19:33:49 -04:00
self.document
.dataset_mut()
.associated_data
.insert(key, state);
2026-08-07 19:28:08 -04:00
2026-06-08 19:33:49 -04:00
self.modified = true;
}
Message::DeleteRow(key) => {
self.document.dataset_mut().remove(key);
self.modified = true;
}
Message::OpenQueryWindow(action) => {
2026-07-10 12:20:51 -04:00
let selected_entity_class = match action {
2026-08-24 21:44:23 -04:00
SearchResultClickAction::Predicate(_) => Some(vocab::rdf::PROPERTY),
2026-07-10 12:20:51 -04:00
SearchResultClickAction::Object(key) => {
2026-08-08 23:50:04 -04:00
self.document.dataset().quads.get(key).and_then(|quad| {
2026-08-24 21:44:23 -04:00
if quad.predicate == vocab::rdf::TYPE {
Some(vocab::rdfs::CLASS)
2026-08-08 23:50:04 -04:00
} else {
None
}
})
}
2026-07-10 12:20:51 -04:00
SearchResultClickAction::URLInput => None,
};
2026-07-12 13:52:13 -04:00
if let Some(search_state) = &mut self.search_state {
search_state.action = action;
} else {
let (id, window_task) = window::open(Settings::default());
self.search_state = Some(SearchState {
window_id: id,
action,
2026-08-20 20:43:18 -04:00
entity_class_selection: None,
2026-07-12 13:52:13 -04:00
query: String::new(),
results: Vec::new(),
});
2026-07-25 23:08:19 -04:00
task = window_task.then(|_| operation::focus("query"));
2026-07-12 13:52:13 -04:00
}
2026-07-10 12:20:51 -04:00
2026-09-02 23:05:29 -04:00
if let Some(selected_entity_class) = selected_entity_class
&& let Some(class) = Class::try_from_named_node(selected_entity_class)
{
task = task.chain(Task::done(Message::UpdateQueryClass(class)))
2026-06-17 20:50:26 -04:00
}
}
2026-08-20 20:43:18 -04:00
Message::UpdateQueryClass(type_) => {
2026-06-17 20:50:26 -04:00
if let Some(search_state) = &mut self.search_state {
2026-08-20 20:43:18 -04:00
search_state.entity_class_selection = Some(type_);
2026-07-01 21:28:12 -04:00
task = Task::done(Message::QueryUpdated(search_state.query.clone()));
2026-06-08 19:33:49 -04:00
}
}
Message::QueryUpdated(new_query) => {
if let Some(search_state) = &mut self.search_state {
2026-09-02 23:05:29 -04:00
let category_id = search_state
.entity_class_selection
2026-08-20 20:43:18 -04:00
.as_ref()
.map(|selection| selection.to_owned() as u64);
2026-06-09 20:09:16 -04:00
2026-06-30 19:25:15 -04:00
let query = new_query.clone();
search_state.query = new_query;
2026-08-08 23:50:04 -04:00
let results = self
.index
.query(category_id, query.as_str(), Schema::all_fields(), 25)
2026-07-22 21:41:57 -04:00
.expect("Unable to complete search");
2026-08-20 20:43:18 -04:00
task = Task::done(Message::SetSearchResults(results));
2026-06-08 19:33:49 -04:00
};
}
2026-06-29 23:10:22 -04:00
Message::SetSearchResults(results) => {
if let Some(search_state) = &mut self.search_state {
search_state.results = results;
}
}
2026-06-08 19:33:49 -04:00
Message::SearchResultClicked(node) => {
if let Some(search_state) = &self.search_state {
2026-09-02 23:05:29 -04:00
task = task.chain(Task::done(Message::LookupResourceDescriptions(
HashSet::from_iter([node.clone()]),
)));
2026-08-25 17:07:40 -04:00
2026-06-08 19:33:49 -04:00
match search_state.action {
SearchResultClickAction::URLInput => {
task = Task::done(Message::URLInputChanged(node.as_str().to_string()))
.chain(Task::done(Message::URLInputSubmitted));
}
SearchResultClickAction::Predicate(key) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
quad.predicate = node;
}
}
SearchResultClickAction::Object(key) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
quad.object = Term::NamedNode(node);
}
}
}
task = task.chain(window::close(search_state.window_id));
self.modified = true;
}
}
Message::HoverRow(index) => {
self.hovered_row = Some(index);
}
Message::UnhoverRow(index) if self.hovered_row == Some(index) => {
self.hovered_row = None;
}
Message::DatatypeUpdated(key, Some(maybe_prefixed_iri)) => {
let node = self
2026-07-13 14:05:40 -04:00
.curie_helper
2026-07-24 19:25:07 -04:00
.expand(None, &maybe_prefixed_iri)
2026-07-13 14:05:40 -04:00
.unwrap_or(maybe_prefixed_iri);
2026-06-08 19:33:49 -04:00
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
2026-07-13 14:05:40 -04:00
term.set_datatype(Some(NamedNode::new_unchecked(node)));
2026-06-08 19:33:49 -04:00
self.modified = true;
}
}
Message::DatatypeUpdated(key, None) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
term.set_datatype(None);
self.modified = true;
}
}
Message::LanguageUpdated(key, Some(language)) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
term.set_language(language.as_str());
self.modified = true;
}
}
Message::LanguageUpdated(key, None) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
term.set_language("en");
self.modified = true;
}
}
2026-07-24 19:25:07 -04:00
Message::SubjectUpdated(key, value) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
quad.subject = NamedOrBlankNode::NamedNode(NamedNode::new_unchecked(value));
self.modified = true;
}
}
2026-07-08 21:07:52 -04:00
Message::PredicateUpdated(key, value) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
quad.predicate = NamedNode::new_unchecked(value);
self.modified = true;
}
}
Message::ObjectUpdated(key, value) => {
2026-06-08 19:33:49 -04:00
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
term.set_value(value);
self.modified = true;
}
}
Message::DirectionToggled(key, direction) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let mut term = TermHelperMut::new(&mut quad.object);
term.set_direction(direction);
self.modified = true;
}
}
Message::SaveGraph(overwrite) => {
let client = self.http_client.clone();
2026-08-20 20:43:18 -04:00
let read_only_entities = self.read_only_entities.clone();
2026-09-02 23:05:29 -04:00
let options = SerializationOptions::from_format(RdfFormat::Turtle).with_filter(
move |triple| {
if triple.predicate == vocab::rdf::TYPE
&& let TermRef::NamedNode(node) = triple.object
{
2026-08-24 21:44:23 -04:00
!read_only_entities.contains(&ReadOnlyEntity::Class(node.into_owned()))
2026-08-20 20:43:18 -04:00
} else {
2026-09-02 23:05:29 -04:00
!read_only_entities
.contains(&ReadOnlyEntity::Property(triple.predicate.into_owned()))
2026-08-20 20:43:18 -04:00
}
2026-09-02 23:05:29 -04:00
},
);
2026-06-08 19:33:49 -04:00
let request = self
.document
.to_update(options)
.expect("Failed to generate PUT request");
let url = self.document.origin().clone();
task = Task::future(async move {
match request.send(client, overwrite).await {
Ok(RdfSourceUpdateResponse::Success) => Message::FetchDocument(url),
Ok(RdfSourceUpdateResponse::DocumentModified(_)) => {
Message::ShowOverwriteConfirmationModal
}
Err(err) => Message::ShowError(format!("Failed to save graph: {err}")),
}
});
}
Message::ShowOverwriteConfirmationModal => {
self.show_overwrite_confirmation = true;
}
Message::HideOverwriteConfirmationModal => {
self.show_overwrite_confirmation = false;
}
Message::ConfirmOverwrite => {
task = Task::done(Message::SaveGraph(true))
.chain(Task::done(Message::HideOverwriteConfirmationModal));
}
2026-09-02 23:05:29 -04:00
Message::DownloadResource => {
let suggested_file_name = self.document.origin_file_name().unwrap_or_default();
let destination_selector =
rfd::AsyncFileDialog::new().set_file_name(suggested_file_name);
task = Task::perform(destination_selector.save_file(), |result| {
Message::DownloadLocationSelected(result.map(|fh| fh.path().to_owned()))
});
}
Message::DownloadLocationSelected(None) => {
debug!("canceled");
}
Message::DownloadLocationSelected(Some(path)) => {
if let Ok(url) = Url::parse(&self.url_input) {
let client = self.http_client.clone();
task = Task::sip(
sipper(async move |mut sender| {
let request = ResourceRequestBuilder::with_client_and_url(client, url)
.follow_described_by(false)
.build();
let resource = request.send().await?;
let mut handle = tokio::fs::File::create(path).await?;
let mut progress = 0usize;
let total_size = resource.size();
let mut stream = resource.into_stream();
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result?;
handle.write_all(&chunk).await?;
progress += chunk.len();
sender.send((progress, total_size)).await;
}
Ok(())
}),
Message::DownloadProgress,
|result: crate::error::Result<()>| {
if let Err(err) = result {
Message::ShowError(err.to_string())
} else {
Message::None
}
},
);
}
}
Message::DownloadProgress((bytes_written, Some(bytes_total))) => {
let percentage = bytes_written as f32 / bytes_total as f32;
debug!(bytes_written, bytes_total, percentage);
}
2026-06-08 19:33:49 -04:00
Message::ShowNewDocumentButtons => {
self.show_new_document_buttons = true;
}
Message::HideNewDocumentButtons => {
self.show_new_document_buttons = false;
}
2026-08-26 19:44:18 -04:00
Message::NewDocument(class, None) => {
let client = self.http_client.clone();
task = tasks::navigate_to_next_url(client, class);
}
Message::NewDocument(class, Some(url)) => {
let mut document: RdfSource<KeyedDataset<RowState>> = RdfSource::new(url.clone());
2026-08-26 20:43:01 -04:00
let mut nodes_to_look_up = HashSet::new();
let document_node = NamedNode::new_unchecked(url.as_str());
for triple in &class.template(document_node) {
let quad = document.quad_from_triple(triple.into_owned());
document.dataset_mut().quads.insert(quad);
2026-06-08 19:33:49 -04:00
2026-09-02 23:05:29 -04:00
if triple.predicate == vocab::rdf::TYPE
&& let TermRef::NamedNode(node) = triple.object
{
2026-08-26 20:43:01 -04:00
nodes_to_look_up.insert(node.into_owned());
} else {
nodes_to_look_up.insert(triple.predicate.into_owned());
}
}
task = Task::done(Message::LookupResourceDescriptions(nodes_to_look_up))
.chain(Task::done(Message::HideNewDocumentButtons))
2026-08-26 19:44:18 -04:00
.chain(Task::done(Message::URLInputChanged(url.to_string())))
2026-08-24 21:44:23 -04:00
.chain(Task::done(Message::LoadDocument(document)));
2026-06-08 19:33:49 -04:00
}
Message::ResetState => {
self.modified = false;
self.show_new_document_buttons = false;
self.show_overwrite_confirmation = false;
2026-08-26 19:44:18 -04:00
self.resource_not_found = false;
2026-06-08 19:33:49 -04:00
}
2026-07-12 13:52:13 -04:00
Message::NavigateToPredicate(key) => {
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
2026-07-14 21:31:15 -04:00
let url = Url::parse(quad.predicate.as_str()).expect("Invalid URL");
task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::URLInputSubmitted));
2026-07-12 13:52:13 -04:00
}
}
Message::NavigateToObject(key) => {
2026-08-30 18:50:48 -04:00
if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) {
let url = if let Term::NamedNode(node) = &quad.object {
Url::parse(node.as_str()).ok()
} else if let Term::Literal(literal) = &quad.object {
Url::parse(literal.value()).ok()
2026-09-02 23:05:29 -04:00
} else {
None
};
2026-08-30 18:50:48 -04:00
if let Some(url) = url {
2026-07-14 21:31:15 -04:00
task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::URLInputSubmitted));
2026-08-30 18:50:48 -04:00
}
2026-07-12 13:52:13 -04:00
}
}
2026-07-14 21:31:15 -04:00
Message::NavigateBack => {
let url = self.navigator.back().clone();
task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::NavigateTo(url)));
}
Message::NavigateForward => {
let url = self.navigator.forward().clone();
task = Task::done(Message::URLInputChanged(url.to_string()))
.chain(Task::done(Message::NavigateTo(url)));
}
2026-08-07 19:28:08 -04:00
Message::RunInference => {
2026-08-20 20:43:18 -04:00
if let Some(ontology) = &mut self.ontology {
2026-09-02 23:05:29 -04:00
let graph = self
.document
2026-08-20 20:43:18 -04:00
.dataset()
.quads
.iter()
.map(|(_, quad)| Triple::from(quad.clone()))
.collect();
task = tasks::run_inference(ontology.clone(), graph);
}
2026-08-07 19:28:08 -04:00
}
2026-08-20 20:43:18 -04:00
Message::SetInferredTriples(graph) => {
2026-08-23 17:01:13 -04:00
if let Some(ontology) = &mut self.ontology {
2026-09-02 23:05:29 -04:00
let predicates = graph.iter().map(|triple| triple.predicate.into_owned());
2026-08-23 17:01:13 -04:00
2026-09-02 23:05:29 -04:00
let objects = graph.iter().filter_map(|triple| match triple.object {
TermRef::NamedNode(node) => Some(node.into_owned()),
_ => None,
});
2026-08-23 17:01:13 -04:00
let mut nodes_to_look_up: HashSet<NamedNode> = HashSet::from_iter(predicates);
nodes_to_look_up.extend(objects);
task = tasks::lookup_resource_descriptions(ontology.clone(), nodes_to_look_up);
}
2026-08-20 20:43:18 -04:00
self.inferred_triples = graph;
2026-08-07 19:28:08 -04:00
}
2026-08-30 18:50:48 -04:00
Message::ShowInferencesWindow => {
if let Some(id) = self.inference_window_id {
task = window::gain_focus(id);
} else {
let (id, window_task) = window::open(Settings::default());
self.inference_window_id = Some(id);
task = window_task.map(|_| Message::None);
}
}
2026-08-31 13:45:15 -04:00
Message::ShowReadOnlyWindow => {
if let Some(id) = self.read_only_window_id {
task = window::gain_focus(id);
} else {
let (id, window_task) = window::open(Settings::default());
self.read_only_window_id = Some(id);
task = window_task.map(|_| Message::None);
}
}
2026-08-05 11:21:25 -04:00
Message::Event(Event::KeyPressed {
2026-08-08 23:50:04 -04:00
key: keyboard::Key::Named(key::Named::Tab),
modifiers,
..
}) => {
2026-08-05 11:21:25 -04:00
if modifiers.shift() {
task = operation::focus_previous();
} else {
task = operation::focus_next();
}
}
2026-06-08 19:33:49 -04:00
_ => {}
}
task
}
2026-08-30 18:50:48 -04:00
pub(crate) fn title(&self, window: window::Id) -> String {
2026-09-02 23:05:29 -04:00
if let Some(inference_window_id) = self.inference_window_id
&& window == inference_window_id
{
2026-08-30 18:50:48 -04:00
"Graph of Liberty Publisher - Inferences".to_string()
2026-09-02 23:05:29 -04:00
} else if let Some(search_state) = &self.search_state
&& window == search_state.window_id
{
2026-08-30 18:50:48 -04:00
"Graph of Liberty Publisher - Search".to_string()
2026-09-02 23:05:29 -04:00
} else if let Some(read_only_window_id) = self.read_only_window_id
&& window == read_only_window_id
{
2026-08-31 13:45:15 -04:00
"Graph of Liberty Publisher - Read Only Triples".to_string()
2026-08-30 18:50:48 -04:00
} else {
"Graph of Liberty Publisher".to_string()
}
2026-06-08 19:33:49 -04:00
}
pub(crate) fn subscription(&self) -> Subscription<Message> {
2026-06-23 21:55:52 -04:00
Subscription::batch([
window::close_events().map(Message::WindowClosed),
2026-08-05 11:21:25 -04:00
keyboard::listen().map(Message::Event),
2026-06-23 21:55:52 -04:00
])
2026-06-08 19:33:49 -04:00
}
2026-09-02 23:05:29 -04:00
fn view_inferred_class_table<'a>(
&'a self,
triples: impl IntoIterator<Item = TripleRef<'a>>,
) -> Element<'a, Message> {
let bold = |t| {
text(t).font(Font {
weight: font::Weight::Bold,
..Font::default()
})
};
2026-08-31 13:45:15 -04:00
let subject_column = table::column(bold("Subject"), |(subject, _): (NamedNodeRef, _)| {
2026-09-02 23:05:29 -04:00
text(
self.curie_helper
.abbreviate(Some(self.document.origin()), subject.as_str())
.unwrap_or_else(|| subject.as_str().to_string()),
)
2026-08-31 13:45:15 -04:00
});
2026-09-02 23:05:29 -04:00
let class_column = table::column(
bold("Classes"),
|(_, classes): (_, BTreeSet<NamedNodeRef>)| {
let textual_classes = classes
.iter()
.map(|class| {
let curie = self
.curie_helper
.abbreviate(Some(self.document.origin()), class.as_str())
.unwrap_or_else(|| class.as_str().to_string());
2026-08-31 13:45:15 -04:00
2026-09-02 23:05:29 -04:00
let label = self
.resource_descriptions
.get(&class.into_owned())
.and_then(|description| description.label.clone());
2026-08-31 13:45:15 -04:00
2026-09-02 23:05:29 -04:00
if let Some(label) = label {
format!("{curie} ({label})")
} else {
curie
}
})
.collect::<Vec<_>>()
.join("\n");
2026-08-31 13:45:15 -04:00
2026-09-02 23:05:29 -04:00
text(textual_classes)
},
);
2026-08-31 13:45:15 -04:00
let mut inferred_classes: BTreeMap<NamedNodeRef, BTreeSet<NamedNodeRef>> = BTreeMap::new();
for triple in triples {
2026-09-02 23:05:29 -04:00
if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject
&& let TermRef::NamedNode(class) = triple.object
{
inferred_classes
.entry(subject)
.and_modify(|classes| {
classes.insert(class);
})
2026-08-31 13:45:15 -04:00
.or_insert(BTreeSet::from_iter([class]));
}
}
table([subject_column, class_column], inferred_classes).into()
}
2026-09-02 23:05:29 -04:00
fn view_triple_table<'a>(
&'a self,
triples: impl IntoIterator<Item = TripleRef<'a>>,
) -> Element<'a, Message> {
let bold = |t| {
text(t).font(Font {
weight: font::Weight::Bold,
..Font::default()
})
};
2026-08-31 13:45:15 -04:00
let subject_column = table::column(bold("Subject"), |triple: TripleRef| {
let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject {
2026-09-02 23:05:29 -04:00
Some(
self.curie_helper
.abbreviate(Some(self.document.origin()), subject.as_str())
.unwrap_or_else(|| subject.as_str().to_string()),
)
2026-08-31 13:45:15 -04:00
} else {
None
};
curie.map(text)
});
let predicate_column = table::column(bold("Predicate"), |triple: TripleRef| {
2026-09-02 23:05:29 -04:00
let curie = self
.curie_helper
2026-08-31 13:45:15 -04:00
.abbreviate(None, triple.predicate.as_str())
.unwrap_or_else(|| triple.predicate.as_str().to_string());
2026-09-02 23:05:29 -04:00
let label = self
.resource_descriptions
2026-08-31 13:45:15 -04:00
.get(&triple.predicate.into_owned())
.and_then(|description| description.label.clone());
if let Some(label) = label {
text(format!("{curie} ({label})"))
} else {
text(curie)
}
});
2026-09-02 23:05:29 -04:00
let object_column =
table::column(bold("Object"), |triple: TripleRef| match triple.object {
2026-08-31 13:45:15 -04:00
TermRef::NamedNode(node) => {
2026-09-02 23:05:29 -04:00
let curie = self
.curie_helper
2026-08-31 13:45:15 -04:00
.abbreviate(Some(self.document.origin()), node.as_str())
.unwrap_or_else(|| node.as_str().to_string());
2026-09-02 23:05:29 -04:00
let label = self
.resource_descriptions
2026-08-31 13:45:15 -04:00
.get(&node.into_owned())
.and_then(|description| description.label.clone());
if let Some(label) = label {
text(format!("{curie} ({label})"))
} else {
text(curie)
}
}
_ => text(triple.object.to_string()),
2026-09-02 23:05:29 -04:00
});
2026-08-31 13:45:15 -04:00
table([subject_column, predicate_column, object_column], triples).into()
}
fn view_search_window<'a>(&'a self, search_state: &'a SearchState) -> Element<'a, Message> {
let search_input = text_input("Query", &search_state.query)
.on_input(Message::QueryUpdated)
.id("query");
let type_selector = pick_list(
search_state.entity_class_selection.as_ref(),
Class::ALL,
ToString::to_string,
2026-09-02 23:05:29 -04:00
)
.on_select(Message::UpdateQueryClass);
2026-08-31 13:45:15 -04:00
2026-09-02 23:05:29 -04:00
let mut columns = vec![table::column(
text("CURIE"),
|document: &HashMap<gl_search::Field, gl_search::OwnedValue>| {
let iri = document
.get(&Schema::iri_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
2026-08-31 13:45:15 -04:00
2026-09-02 23:05:29 -04:00
let abbreviated_iri = document
.get(&Schema::curie_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
2026-08-31 13:45:15 -04:00
2026-09-02 23:05:29 -04:00
button(text(abbreviated_iri).wrapping(Wrapping::Word))
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
.style(button::text)
},
)];
2026-08-31 13:45:15 -04:00
2026-09-02 23:05:29 -04:00
let fields = search_state
.entity_class_selection
2026-08-31 13:45:15 -04:00
.as_ref()
2026-09-02 23:05:29 -04:00
.map(|class| class.fields(language::ENGLISH_TAG.clone()))
.unwrap_or_default();
2026-08-31 13:45:15 -04:00
for (field, label) in fields {
columns.push(
2026-09-02 23:05:29 -04:00
table::column(
text(label),
move |document: &HashMap<gl_search::Field, gl_search::OwnedValue>| {
let iri = document
.get(&Schema::iri_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
2026-08-31 13:45:15 -04:00
2026-09-02 23:05:29 -04:00
let value = document
.get(&field)
.and_then(|value| value.as_str())
.unwrap_or_default();
2026-08-31 13:45:15 -04:00
2026-09-02 23:05:29 -04:00
button(text(value).wrapping(Wrapping::Word))
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
.style(button::text)
},
)
.width(Length::Fixed(256.0)),
2026-08-31 13:45:15 -04:00
);
}
let results_table = if columns.is_empty() {
None
} else {
Some(scrollable(table(columns, &search_state.results)))
};
column![row![search_input, type_selector], results_table].into()
}
fn view_inference_window(&self) -> Element<'_, Message> {
2026-09-02 23:05:29 -04:00
let classes = self
.inferred_triples
.triples_for_predicate(vocab::rdf::TYPE);
2026-08-31 13:45:15 -04:00
column![
self.view_inferred_class_table(classes),
scrollable(self.view_triple_table(&self.inferred_triples)),
2026-09-02 23:05:29 -04:00
]
.into()
2026-08-31 13:45:15 -04:00
}
fn view_read_only_window(&self) -> Element<'_, Message> {
2026-09-02 23:05:29 -04:00
let read_only_triples = self
.document
2026-08-31 13:45:15 -04:00
.dataset()
.iter_both()
.filter_map(|(_, quad, state)| if state.read_only { Some(quad) } else { None })
.map(|quad| TripleRef::from(quad.as_ref()));
2026-09-02 23:05:29 -04:00
column![scrollable(self.view_triple_table(read_only_triples)),].into()
2026-08-31 13:45:15 -04:00
}
2026-06-08 19:33:49 -04:00
fn view_row<'a>(
&'a self,
key: QuadKey,
2026-08-20 20:43:18 -04:00
quad: &'a Quad,
2026-06-08 19:33:49 -04:00
state: &'a RowState,
) -> Element<'a, Message> {
const BUTTON_WIDTH: Length = Length::Fixed(35.0);
let delete_button = button("\u{274c}")
.style(|_, _| Style::default().with_background(Background::Color(color!(255, 0, 0))))
.on_press(Message::DeleteRow(key));
let button_area = if self.hovered_row == Some(key) && !state.read_only {
container(delete_button).width(BUTTON_WIDTH)
} else {
container(space()).width(BUTTON_WIDTH)
};
2026-08-27 13:00:36 -04:00
let base = self.document.origin();
2026-07-24 19:25:07 -04:00
2026-08-20 20:43:18 -04:00
let subject = match &quad.subject {
2026-07-24 19:25:07 -04:00
NamedOrBlankNode::NamedNode(subject) => subject.as_str(),
_ => "",
};
2026-08-27 13:00:36 -04:00
let subject_input_base = iri_input(&self.curie_helper, "Subject", Some(base), subject);
2026-08-20 20:43:18 -04:00
let subject_input = if state.read_only {
2026-07-24 19:25:07 -04:00
subject_input_base
} else {
subject_input_base.on_input(move |value| Message::SubjectUpdated(key, value))
};
2026-08-08 23:50:04 -04:00
let predicate_input_base = iri_input(
&self.curie_helper,
"Predicate",
2026-08-27 13:00:36 -04:00
Some(base),
2026-08-20 20:43:18 -04:00
quad.predicate.as_str(),
2026-08-08 23:50:04 -04:00
);
2026-08-20 20:43:18 -04:00
let predicate_input = if state.read_only {
2026-07-10 23:14:50 -04:00
predicate_input_base
} else {
2026-08-08 23:50:04 -04:00
predicate_input_base
.on_input(move |value| Message::PredicateUpdated(key, value))
.on_control_click(Message::OpenQueryWindow(
SearchResultClickAction::Predicate(key),
))
2026-07-12 13:52:13 -04:00
.on_shift_click(Message::NavigateToPredicate(key))
2026-07-10 23:14:50 -04:00
};
2026-06-08 19:33:49 -04:00
2026-09-02 23:05:29 -04:00
let predicate_label = self
.resource_descriptions
2026-08-20 20:43:18 -04:00
.get(&quad.predicate)
.and_then(|description| description.label.clone())
.map(|label| container(text(label)));
2026-07-13 14:05:40 -04:00
2026-08-20 20:43:18 -04:00
let term = TermHelper::new(&quad.object);
2026-06-08 19:33:49 -04:00
2026-09-02 23:05:29 -04:00
let value_label = term
.value_as_named_node()
2026-08-20 20:43:18 -04:00
.and_then(|node| self.resource_descriptions.get(&node.into_owned()))
.and_then(|description| description.label.clone())
.map(|label| container(text(label)));
2026-06-08 19:33:49 -04:00
let value = term
.value_as_named_node()
2026-06-26 11:28:44 -04:00
.map(|node| node.as_str().to_string())
2026-06-08 19:33:49 -04:00
.unwrap_or(term.value().to_string());
let value_alignment = term
.direction()
.map(|direction| match direction {
BaseDirection::Ltr => Horizontal::Left,
BaseDirection::Rtl => Horizontal::Right,
})
.unwrap_or(Horizontal::Left);
2026-08-30 18:50:48 -04:00
/* If the object is not a valid URL (either as a literal string or as a named node), then
* use a normal text input instead of an iri_input.
*/
2026-09-02 23:05:29 -04:00
let object_input: Element<Message> =
if term.is_named_node() || Url::parse(term.value()).is_ok() {
let base = iri_input(&self.curie_helper, "Object", Some(base), value.as_str())
.on_shift_click(Message::NavigateToObject(key));
if state.read_only {
base.into()
} else {
base.align_x(value_alignment)
.on_input(move |value| Message::ObjectUpdated(key, value))
.on_control_click(Message::OpenQueryWindow(
SearchResultClickAction::Object(key),
))
.into()
}
2026-07-25 23:08:19 -04:00
} else {
2026-09-02 23:05:29 -04:00
let base = text_input("Object", value.clone());
if state.read_only {
base.into()
} else {
base.on_input(move |value| Message::ObjectUpdated(key, value))
.into()
}
};
2026-06-23 21:55:52 -04:00
2026-08-08 23:50:04 -04:00
let selected_datatype = term
.datatype()
2026-07-24 19:25:07 -04:00
.and_then(|node| self.curie_helper.abbreviate(None, node.as_str()));
2026-07-13 14:05:40 -04:00
2026-06-08 19:33:49 -04:00
let datatype_selector: Element<Message> = if state.read_only {
selected_datatype.map(text).into()
} else {
combo_box(
&state.datatype_state,
"Datatype",
selected_datatype.as_ref(),
move |selection| Message::DatatypeUpdated(key, Some(selection)),
)
.on_input(move |input| {
let value = if input.is_empty() { None } else { Some(input) };
Message::DatatypeUpdated(key, value)
2026-08-08 23:50:04 -04:00
})
.into()
2026-06-08 19:33:49 -04:00
};
2026-08-05 11:21:25 -04:00
let language = term.language().unwrap_or("en").to_owned();
2026-06-08 19:33:49 -04:00
let language_input = match term.datatype() {
2026-08-24 21:44:23 -04:00
Some(vocab::rdf::LANG_STRING) | Some(vocab::rdf::DIR_LANG_STRING) => Some(container(
2026-08-05 11:21:25 -04:00
text_input("Language", language).on_input(move |input| {
2026-06-08 19:33:49 -04:00
let value = if input.is_empty() { None } else { Some(input) };
Message::LanguageUpdated(key, value)
}),
)),
_ => None,
};
let direction_slider = term
.direction()
.map(|direction| match direction {
BaseDirection::Ltr => toggler(false),
BaseDirection::Rtl => toggler(true),
})
.map(|toggler| {
container(row![
text("LTR"),
toggler.on_toggle(move |new_state| {
let direction = if new_state {
BaseDirection::Rtl
} else {
BaseDirection::Ltr
};
Message::DirectionToggled(key, direction)
}),
text("RTL"),
])
});
let row = row![
button_area,
2026-07-24 19:25:07 -04:00
subject_input,
2026-07-07 22:06:39 -04:00
predicate_input,
2026-07-13 14:05:40 -04:00
predicate_label,
2026-06-30 19:25:15 -04:00
object_input,
2026-07-13 14:05:40 -04:00
value_label,
2026-06-08 19:33:49 -04:00
datatype_selector,
language_input,
direction_slider,
];
mouse_area(row)
.on_enter(Message::HoverRow(key))
.on_exit(Message::UnhoverRow(key))
.into()
}
2026-08-26 19:44:18 -04:00
pub(crate) fn view(&self, window: window::Id) -> Element<'_, Message> {
2026-09-02 23:05:29 -04:00
if let Some(search_state) = &self.search_state
&& search_state.window_id == window
{
2026-08-26 19:44:18 -04:00
return self.view_search_window(search_state);
2026-06-08 19:33:49 -04:00
}
2026-09-02 23:05:29 -04:00
if let Some(inference_window_id) = self.inference_window_id
&& window == inference_window_id
{
2026-08-31 13:45:15 -04:00
return self.view_inference_window();
}
2026-09-02 23:05:29 -04:00
if let Some(read_only_window_id) = self.read_only_window_id
&& window == read_only_window_id
{
2026-08-31 13:45:15 -04:00
return self.view_read_only_window();
2026-08-30 18:50:48 -04:00
}
let show_inferences_button = button("\u{1f9e0}").on_press(Message::ShowInferencesWindow);
2026-08-31 13:45:15 -04:00
let show_read_only_button = button("\u{1f6ab}").on_press(Message::ShowReadOnlyWindow);
2026-07-14 21:49:04 -04:00
let back_button = button("\u{1f870}").on_press(Message::NavigateBack);
let forward_button = button("\u{1f872}").on_press(Message::NavigateForward);
2026-08-26 19:44:18 -04:00
let new_document_button = button("New Document").on_press(Message::ShowNewDocumentButtons);
let add_row_button = button("Add Row").on_press(Message::AddRow(None));
2026-06-08 19:33:49 -04:00
2026-07-24 19:25:07 -04:00
let address_input = iri_input(&self.curie_helper, "URL", None, &self.url_input)
2026-06-30 19:25:15 -04:00
.on_input(Message::URLInputChanged)
.on_submit(Message::URLInputSubmitted)
2026-07-28 15:09:33 -04:00
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::URLInput))
2026-08-08 23:50:04 -04:00
.style(crate::theme::styles::text_input::required(
!self.url_input_valid,
));
2026-06-08 19:33:49 -04:00
2026-09-02 23:05:29 -04:00
let rows: Vec<Element<Message>> = self
.document
2026-06-08 19:33:49 -04:00
.dataset()
.iter_both()
2026-08-31 13:45:15 -04:00
.filter(|(_, _, state)| !state.read_only)
2026-06-08 19:33:49 -04:00
.map(|(key, quad, state)| self.view_row(key, quad, state))
.collect();
2026-08-24 21:44:23 -04:00
let body: Element<Message> = if self.show_new_document_buttons {
2026-09-02 23:05:29 -04:00
let buttons = Class::CREATABLE.iter().map(|class| {
let url = if self.resource_not_found {
Url::parse(&self.url_input).ok()
} else {
None
};
2026-08-24 21:44:23 -04:00
2026-09-02 23:05:29 -04:00
button(text(class.to_string()))
.on_press(Message::NewDocument(class.clone(), url))
.into()
});
2026-08-24 21:44:23 -04:00
2026-09-02 23:05:29 -04:00
column![grid(buttons).height(Sizing::EvenlyDistribute(Length::Shrink))].into()
2026-08-24 21:44:23 -04:00
} else {
column(rows).into()
};
2026-06-08 19:33:49 -04:00
2026-09-02 23:05:29 -04:00
let download_button_base = button("\u{2b73}");
let download_button =
if self.document.origin_type() == Some(&ldp::ResourceType::NonRdfSource) {
download_button_base.on_press(Message::DownloadResource)
} else {
download_button_base
};
2026-06-08 19:33:49 -04:00
let save_button_base = button(text("\u{1f4be}"));
let save_button = if self.modified {
save_button_base.on_press(Message::SaveGraph(false))
} else {
save_button_base
};
2026-07-14 21:31:15 -04:00
let content = navigation_area(column![
2026-06-08 19:33:49 -04:00
row![
2026-08-30 18:50:48 -04:00
show_inferences_button,
2026-08-31 13:45:15 -04:00
show_read_only_button,
2026-08-26 19:44:18 -04:00
new_document_button,
2026-07-22 21:41:57 -04:00
add_row_button,
2026-07-14 21:31:15 -04:00
back_button,
forward_button,
2026-06-30 19:25:15 -04:00
address_input,
2026-09-02 23:05:29 -04:00
download_button,
2026-06-08 19:33:49 -04:00
save_button,
],
2026-08-07 19:28:08 -04:00
scrollable(body),
2026-07-14 21:31:15 -04:00
])
2026-08-08 23:50:04 -04:00
.on_back_click(Message::NavigateBack)
.on_forward_click(Message::NavigateForward);
2026-06-08 19:33:49 -04:00
if self.show_overwrite_confirmation {
let confirmation_modal = container(column![
text("The resource has changed since it was fetched. Overwrite?"),
row![
button(text("Yes"))
.style(button::danger)
.on_press(Message::ConfirmOverwrite),
space::horizontal(),
button(text("No")).on_press(Message::HideOverwriteConfirmationModal),
],
])
.width(Length::Shrink);
modal(content, confirmation_modal, Message::None)
} else {
content.into()
}
}
}
fn modal<'a, Message>(
base: impl Into<Element<'a, Message>>,
content: impl Into<Element<'a, Message>>,
on_blur: Message,
) -> Element<'a, Message>
where
Message: Clone + 'a,
{
stack![
base.into(),
opaque(
mouse_area(center(opaque(content)).style(|_theme| {
container::Style {
background: Some(
Color {
a: 0.8,
..Color::BLACK
}
.into(),
),
..container::Style::default()
}
}))
.on_press(on_blur)
)
]
.into()
2026-09-02 23:05:29 -04:00
}