updates working
This commit is contained in:
parent
f484c9b54c
commit
4e9e01bf94
@ -1269,7 +1269,13 @@ impl ObjectWrite for Content {
|
||||
let obj = self.parts[0].to_primitive(update)?;
|
||||
update.create(obj)?.to_primitive(update)
|
||||
} else {
|
||||
self.parts.to_primitive(update)
|
||||
let mut arr = vec![];
|
||||
for part in &self.parts {
|
||||
let pdf_stream = part.to_primitive(update)?;
|
||||
let rc_ref = update.create(pdf_stream)?;
|
||||
arr.push(rc_ref.to_primitive(update)?);
|
||||
}
|
||||
Ok(Primitive::Array(arr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,11 +377,12 @@ where
|
||||
let r = match self.refs.get(old.id)? {
|
||||
XRef::Free { .. } => panic!(),
|
||||
XRef::Raw { gen_nr, .. } => PlainRef { id: old.id, gen: gen_nr },
|
||||
XRef::Stream { .. } => return self.create(obj),
|
||||
XRef::Stream { .. } => PlainRef { id: old.id, gen: 0 },
|
||||
XRef::Promised => PlainRef { id: old.id, gen: 0 },
|
||||
XRef::Invalid => panic!()
|
||||
};
|
||||
let primitive = obj.to_primitive(self)?;
|
||||
|
||||
match self.changes.entry(old.id) {
|
||||
Entry::Vacant(e) => {
|
||||
e.insert((primitive, r.gen));
|
||||
@ -499,6 +500,7 @@ where
|
||||
fn fulfill<T: ObjectWrite>(&mut self, promise: PromisedRef<T>, obj: T) -> Result<RcRef<T>> {
|
||||
self.storage.fulfill(promise, obj)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<OC, SC, L> File<Vec<u8>, OC, SC, L>
|
||||
@ -660,6 +662,14 @@ where
|
||||
pub fn log(&self) -> &L {
|
||||
&self.storage.log
|
||||
}
|
||||
|
||||
pub fn changes(&self) -> &HashMap<ObjNr, (Primitive, GenNr)> {
|
||||
&self.storage.changes
|
||||
}
|
||||
pub fn clear_changes(&mut self) {
|
||||
self.storage.changes.clear();
|
||||
self.storage.refs.remove_promised();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Object, ObjectWrite, DataSize)]
|
||||
|
||||
@ -93,6 +93,18 @@ impl XRefTable {
|
||||
pub fn num_entries(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
pub fn remove_promised(&mut self) {
|
||||
let mut new_entries = vec![];
|
||||
for entry in self.entries.iter() {
|
||||
if let XRef::Promised = entry {
|
||||
continue;
|
||||
}
|
||||
new_entries.push(entry.clone());
|
||||
}
|
||||
self.entries = new_entries;
|
||||
}
|
||||
|
||||
pub fn max_field_widths(&self) -> (u64, u64) {
|
||||
let mut max_a = 0;
|
||||
let mut max_b = 0;
|
||||
|
||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@ -3689,6 +3689,7 @@ dependencies = [
|
||||
"image 0.25.5",
|
||||
"indexmap 1.9.3",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"pathfinder_geometry",
|
||||
"pathfinder_rasterize",
|
||||
"pdf",
|
||||
|
||||
@ -35,3 +35,4 @@ pathfinder_rasterize = { git = "https://github.com/s3bk/pathfinder_rasterizer" }
|
||||
pathfinder_geometry = { git = "https://github.com/servo/pathfinder" }
|
||||
indexmap = "1.9.3"
|
||||
tauri-plugin-websocket = "2"
|
||||
log = "0.4.25"
|
||||
|
||||
@ -11,18 +11,20 @@ use crate::render::Renderer;
|
||||
use image::DynamicImage;
|
||||
use lazy_static::lazy_static;
|
||||
use pdf::file::{File, FileOptions, NoLog, ObjectCache, StreamCache};
|
||||
use pdf::object::{Object, PlainRef, Stream, Updater};
|
||||
use pdf::object::{Object, ObjectWrite, Page, PageRc, PlainRef, Stream, Updater};
|
||||
use pdf::primitive::{Dictionary, Primitive};
|
||||
use pdf::xref::XRef;
|
||||
use regex::Regex;
|
||||
use retrieval::{get_prim_by_path_with_file, get_stream_data_by_path_with_file};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tauri::ipc::{InvokeResponseBody, Response};
|
||||
use tauri::{Manager, State};
|
||||
use uuid::Uuid;
|
||||
use pdf::backend::Backend;
|
||||
|
||||
type CosFile = File<Vec<u8>, ObjectCache, StreamCache, NoLog>;
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
@ -74,11 +76,27 @@ pub struct PrimitiveTreeView {
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct PathTrace {
|
||||
pub key: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub last_ref: Option<PlainRef>,
|
||||
pub last_jump: String,
|
||||
}
|
||||
impl PathTrace {
|
||||
fn new(key: String, last_jump: String) -> PathTrace {
|
||||
PathTrace { key, last_jump }
|
||||
fn new(key: String, last_ref: Option<PlainRef>) -> PathTrace {
|
||||
PathTrace {
|
||||
key,
|
||||
last_jump: last_ref
|
||||
.map(|r| r.id.to_string())
|
||||
.unwrap_or("Trailer".to_string()),
|
||||
last_ref: last_ref,
|
||||
}
|
||||
}
|
||||
|
||||
fn trailer() -> PathTrace {
|
||||
PathTrace {
|
||||
key: "Trailer".to_string(),
|
||||
last_ref: None,
|
||||
last_jump: "Trailer".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,6 +133,22 @@ fn get_all_files(session: State<Session>) -> Vec<PdfFile> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_changes(id: &str, session: State<Session>) -> Result<Vec<PrimitiveModel>, String> {
|
||||
let lock = session.get_file(id)?;
|
||||
let file = lock.read().unwrap();
|
||||
Ok(file.cos_file.changes().iter()
|
||||
.map(|(obj, (prim, gen))| PrimitiveModel::from_primitive_until_refs(prim, PathTrace::new(obj.to_string(), Some(PlainRef{id: *obj, gen: *gen}))))
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn clear_changes(id: &str, session: State<Session>) -> Result<(), String> {
|
||||
let lock = session.get_file(id)?;
|
||||
let mut file = lock.write().unwrap();
|
||||
file.cos_file.clear_changes();
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
fn get_all_file_ids(session: State<Session>) -> Vec<String> {
|
||||
session.get_all_file_ids()
|
||||
@ -181,21 +215,16 @@ fn to_pdf_file(path: &str, file: &CosFile) -> Result<PdfFile, String> {
|
||||
Ok(pdf_file)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_contents(id: &str, path: &str, session: State<Session>) -> Result<ContentsModel, String> {
|
||||
let lock = session.get_file(id)?;
|
||||
let file = lock.read().unwrap();
|
||||
|
||||
let (_, page_prim, _) = get_prim_by_path_with_file(path, &file.cos_file)?;
|
||||
let resolver = file.cos_file.resolver();
|
||||
|
||||
let page = t!(pdf::object::Page::from_primitive(page_prim, &resolver));
|
||||
if let Some(contents) = page.contents {
|
||||
fn read_contents(page: &Page, resolver: &impl Resolve) -> Result<ContentsModel, String> {
|
||||
if let Some(ref contents) = page.contents {
|
||||
let mut parts = vec![];
|
||||
for part in contents.parts {
|
||||
let data = &t!(part.data(&resolver));
|
||||
let ops = t!(pdf::content::parse_ops(&data, &resolver));
|
||||
let part = t!(pdf::content::display_ops(&ops));
|
||||
for part in &contents.parts {
|
||||
let data = &t!(part.data(resolver));
|
||||
let string = String::from_utf8_lossy(data);
|
||||
let part = string
|
||||
.split("\n")
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
parts.push(part);
|
||||
}
|
||||
return Ok(ContentsModel { parts });
|
||||
@ -204,10 +233,53 @@ fn get_contents(id: &str, path: &str, session: State<Session>) -> Result<Content
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_stream_data_as_string(
|
||||
async fn get_contents(id: &str, path: &str, session: State<'_, Session>) -> Result<ContentsModel, String> {
|
||||
let lock = session.get_file(id)?;
|
||||
let file = lock.read().unwrap();
|
||||
|
||||
let (_, page_prim, _) = get_prim_by_path_with_file(path, &file.cos_file)?;
|
||||
let resolver = file.cos_file.resolver();
|
||||
|
||||
let page = t!(pdf::object::Page::from_primitive(page_prim, &resolver));
|
||||
read_contents(&page, &resolver)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_contents(
|
||||
id: &str,
|
||||
path: &str,
|
||||
session: State<Session>,
|
||||
session: State<'_, Session>,
|
||||
contents: ContentsModel,
|
||||
) -> Result<(), String> {
|
||||
let lock = session.get_file(id)?;
|
||||
let mut file = lock.write().unwrap();
|
||||
update_contents_with_file(path, contents, &mut file.cos_file)
|
||||
}
|
||||
|
||||
fn update_contents_with_file(path: &str, contents: ContentsModel, file: &mut CosFile) -> Result<(), String> {
|
||||
let mut parts = vec![];
|
||||
for part in contents.parts {
|
||||
let stream_ref = Stream::new((), part.join("\n").as_bytes());
|
||||
parts.push(stream_ref);
|
||||
}
|
||||
let new_contents = pdf::content::Content{parts};
|
||||
|
||||
if let Some(Step::Page(num)) = Step::parse(path).pop_front() {
|
||||
let page_rc: PageRc = t!(file.get_page(num - 1));
|
||||
let page_ref = page_rc.get_ref().get_inner().clone();
|
||||
let mut page = page_rc.deref().clone();
|
||||
page.contents = Some(new_contents);
|
||||
let _ = t!(file.update(page_ref, page));
|
||||
return Ok(());
|
||||
}
|
||||
Err(String::from("Path does not lead to a page!"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_stream_data_as_string(
|
||||
id: &str,
|
||||
path: &str,
|
||||
session: State<'_, Session>,
|
||||
) -> Result<String, String> {
|
||||
let lock = session.get_file(id)?;
|
||||
let file = lock.read().unwrap();
|
||||
@ -226,8 +298,7 @@ async fn update_stream_data_as_string(
|
||||
let mut file = lock.write().unwrap();
|
||||
let (_, _, trace) = get_prim_by_path_with_file(path, &file.cos_file)?;
|
||||
let i = trace.len() - 1;
|
||||
let id = t!(trace[i].last_jump.parse::<u64>());
|
||||
let plain_ref = PlainRef { id, gen: 0 };
|
||||
let plain_ref = trace[i].last_ref.expect("No trace to update");
|
||||
let stream = Stream::new((), new_data.as_bytes());
|
||||
let _ = t!(file.cos_file.update(plain_ref, stream));
|
||||
Ok(())
|
||||
@ -398,18 +469,19 @@ fn expand(
|
||||
return Ok(());
|
||||
}
|
||||
let step = node.step()?;
|
||||
let prim = retrieval::resolve_step(parent, &step)?;
|
||||
if let Primitive::Reference(x_ref) = prim {
|
||||
let jump = retrieval::resolve_xref(x_ref.id, file)?;
|
||||
let mut jump_trace = parent_model.trace.clone();
|
||||
jump_trace.push(PathTrace::new(step.get_key(), x_ref.id.to_string()));
|
||||
let mut to_expand = parent_model.get_child(step.get_key()).unwrap();
|
||||
to_expand.add_children(&jump, &jump_trace);
|
||||
expand_children(node, file, &jump, &mut to_expand)?;
|
||||
} else {
|
||||
let mut to_expand = parent_model.get_child(step.get_key()).unwrap();
|
||||
to_expand.add_children(prim, &to_expand.trace.clone());
|
||||
expand_children(node, file, prim, &mut to_expand)?;
|
||||
if let Ok(prim) = retrieval::resolve_step(parent, &step) {
|
||||
if let Primitive::Reference(x_ref) = prim {
|
||||
let jump = retrieval::resolve_xref(x_ref.id, file)?;
|
||||
let mut jump_trace = parent_model.trace.clone();
|
||||
jump_trace.push(PathTrace::new(step.get_key(), Some(x_ref.clone())));
|
||||
let mut to_expand = parent_model.get_child(step.get_key()).unwrap();
|
||||
to_expand.add_children(&jump, &jump_trace);
|
||||
expand_children(node, file, &jump, &mut to_expand)?;
|
||||
} else {
|
||||
let mut to_expand = parent_model.get_child(step.get_key()).unwrap();
|
||||
to_expand.add_children(prim, &to_expand.trace.clone());
|
||||
expand_children(node, file, prim, &mut to_expand)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -491,7 +563,7 @@ impl Step {
|
||||
|
||||
fn append_path(key: String, path: &Vec<PathTrace>) -> Vec<PathTrace> {
|
||||
let mut new_path = path.clone();
|
||||
let last_jump = new_path.last().unwrap().last_jump.clone();
|
||||
let last_jump = new_path.last().unwrap().last_ref.clone();
|
||||
new_path.push(PathTrace::new(key, last_jump));
|
||||
new_path
|
||||
}
|
||||
@ -611,6 +683,17 @@ impl PrimitiveModel {
|
||||
model
|
||||
}
|
||||
|
||||
fn from_primitive_until_refs(primitive: &Primitive, trace: PathTrace) -> PrimitiveModel {
|
||||
let mut parent_model = PrimitiveModel::from_primitive(
|
||||
trace.key.clone(),
|
||||
primitive,
|
||||
vec![trace.clone()],
|
||||
);
|
||||
let mut model = parent_model;
|
||||
model.add_children(primitive, &vec!(trace));
|
||||
model
|
||||
}
|
||||
|
||||
fn add_children(&mut self, primitive: &Primitive, path: &Vec<PathTrace>) {
|
||||
self.expanded = true;
|
||||
match primitive {
|
||||
@ -837,10 +920,13 @@ pub fn run() {
|
||||
get_prim_tree_by_path,
|
||||
get_xref_table,
|
||||
get_contents,
|
||||
update_contents,
|
||||
get_stream_data_as_string,
|
||||
update_stream_data_as_string,
|
||||
get_stream_data_as_image,
|
||||
get_page_by_num,
|
||||
update_stream_data_as_string
|
||||
get_changes,
|
||||
clear_changes
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@ -34,7 +34,7 @@ pub fn get_prim_by_steps_with_file(
|
||||
let step = steps.pop_front().unwrap();
|
||||
let (mut parent, trace) = resolve_parent(step.clone(), file)?;
|
||||
|
||||
let mut last_jump = trace.last_jump.clone();
|
||||
let mut last_jump: Option<PlainRef> = trace.last_ref.clone();
|
||||
let mut trace = vec![trace];
|
||||
|
||||
let mut current_prim = &parent;
|
||||
@ -43,7 +43,7 @@ pub fn get_prim_by_steps_with_file(
|
||||
|
||||
current_prim = resolve_step(¤t_prim, &step)?;
|
||||
if let Primitive::Reference(xref) = current_prim {
|
||||
last_jump = xref.id.to_string();
|
||||
last_jump = Some(xref.clone());
|
||||
parent = resolve_p_ref(xref.clone(), file)?;
|
||||
current_prim = &parent;
|
||||
}
|
||||
@ -53,13 +53,18 @@ pub fn get_prim_by_steps_with_file(
|
||||
}
|
||||
|
||||
pub fn resolve_parent(step: Step, file: &CosFile) -> Result<(Primitive, PathTrace), String> {
|
||||
let parent = match step {
|
||||
Step::Page(page_num) => return retrieve_page(page_num, file),
|
||||
Step::Number(obj_num) => resolve_xref(obj_num, file)?,
|
||||
Step::Trailer => retrieve_trailer(file),
|
||||
_ => return Err(String::from(format!("{:?} is not a valid path!", step))),
|
||||
};
|
||||
Ok((parent, PathTrace::new(step.get_key(), step.get_key())))
|
||||
match step {
|
||||
Step::Page(page_num) => retrieve_page(page_num, file),
|
||||
Step::Number(obj_num) => {
|
||||
let parent = resolve_xref(obj_num, file)?;
|
||||
Ok((parent, PathTrace::new(step.get_key(), Some(PlainRef {id: obj_num, gen: 0}))))
|
||||
},
|
||||
Step::Trailer => {
|
||||
let parent = retrieve_trailer(file);
|
||||
Ok((parent, PathTrace::trailer()))
|
||||
},
|
||||
_ => Err(String::from(format!("{:?} is not a valid path!", step))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve_step<'a>(current_prim: &'a Primitive, step: &Step) -> Result<&'a Primitive, String> {
|
||||
@ -125,7 +130,7 @@ pub fn retrieve_page(page_num: u32, file: &CosFile) -> Result<(Primitive, PathTr
|
||||
let p_ref = page_rc.get_ref().get_inner();
|
||||
Ok((
|
||||
resolve_p_ref(p_ref, file)?,
|
||||
PathTrace::new(format!("Page{}", page_num), p_ref.id.to_string()),
|
||||
PathTrace::new(format!("Page{}", page_num), Some(p_ref)),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,8 @@ mod tests {
|
||||
use crate::{
|
||||
get_prim_by_path_with_file, get_prim_model_by_path_with_file,
|
||||
get_prim_tree_by_path_with_file, get_stream_data_by_path_with_file,
|
||||
get_xref_table_model_with_file, to_pdf_file, PathTrace, PrimitiveModel, TreeViewRequest,
|
||||
get_xref_table_model_with_file, to_pdf_file, update_contents, update_contents_with_file,
|
||||
ContentsModel, PathTrace, PrimitiveModel, TreeViewRequest,
|
||||
};
|
||||
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
@ -155,7 +156,7 @@ mod tests {
|
||||
);
|
||||
let trail_model = PrimitiveModel::from_primitive_with_children(
|
||||
&trail,
|
||||
vec![PathTrace::new("Trailer".to_string(), "Trailer".to_string())],
|
||||
vec![PathTrace::new("Trailer".to_string(), None)],
|
||||
);
|
||||
print_node(trail_model, 5);
|
||||
println!("{:?}", file.trailer.info_dict);
|
||||
@ -264,7 +265,10 @@ mod tests {
|
||||
let mut rendered_pages: Vec<DynamicImage> = vec![];
|
||||
timed!(
|
||||
for i in 0..1 {
|
||||
let img = timed!(renderer.render(i), format!("render page {}", i));
|
||||
let img = timed!(
|
||||
renderer.render(&file.get_page(i).unwrap()),
|
||||
format!("render page {}", i)
|
||||
);
|
||||
rendered_pages.push(img.unwrap())
|
||||
},
|
||||
"rendering some pages"
|
||||
@ -276,4 +280,21 @@ mod tests {
|
||||
"saving images"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_update_contents() {
|
||||
let mut file = timed!(
|
||||
FileOptions::cached().open(PDF_SPEC_PATH).unwrap(),
|
||||
"Loading file"
|
||||
);
|
||||
let string = timed!(update_contents_with_file(
|
||||
"Page1",
|
||||
ContentsModel {
|
||||
parts: vec![vec!["q".to_string(), "Q".to_string()]],
|
||||
},
|
||||
&mut file,
|
||||
)
|
||||
.unwrap(), "Updating contents");
|
||||
|
||||
println!("{}", string);
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,7 +97,6 @@
|
||||
closeTab={closeFile}
|
||||
openTab={upload}
|
||||
selectTab={(state) => (fState = state)}
|
||||
reload={() => fState?.reload()}
|
||||
></TitleBar>
|
||||
</div>
|
||||
<div class="fileview_container" style="height: {fileViewHeight}px">
|
||||
|
||||
49
src/components/ChangesModal.svelte
Normal file
49
src/components/ChangesModal.svelte
Normal file
@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
import {
|
||||
TrashBinSolid
|
||||
} from "flowbite-svelte-icons";
|
||||
|
||||
let {fState}: { fState: FileViewState } = $props();
|
||||
let changes = $derived(fState.changes);
|
||||
</script>
|
||||
|
||||
<div class="border-l border-r border-forge-sec bg-forge-sec flex flex-row" style="width: 100%; height: 24px">
|
||||
<div class="text-sm p-1">
|
||||
Changes:
|
||||
</div>
|
||||
<button class="clear" onclick={ () => fState.clearChanges()}>
|
||||
<div class="img">
|
||||
<TrashBinSolid size="sm"/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
{#each changes as change}
|
||||
<div class="text-xs flex flex-row">
|
||||
<p class="whitespace-nowrap">{change.key + ":"}</p>
|
||||
 
|
||||
<p>{change.ptype}</p> | 
|
||||
<p>{change.sub_type}</p> | 
|
||||
<p>{change.value}</p>
|
||||
</div>
|
||||
<div class="bg-forge-bound mt-1 mb-1" style="height: 1px; width: 100%"></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="postcss">
|
||||
|
||||
.clear {
|
||||
@apply hover:bg-forge-acc h-[24px] w-[24px] rounded-2xl;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.img {
|
||||
position: fixed;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
}
|
||||
</style>
|
||||
@ -1,59 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
import type ContentModel from "../models/ContentModel.svelte";
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
import StreamEditor from "./StreamEditor.svelte";
|
||||
|
||||
let {fState, height}: { fState: FileViewState; height: number } =
|
||||
$props();
|
||||
let h = $derived(height);
|
||||
let path = $derived(fState.container_prim?.getFirstJump()?.toString());
|
||||
let id = $derived(fState.file.id);
|
||||
let contentsModel: ContentModel | undefined = $state(undefined);
|
||||
let contents: string | undefined = $derived(mapToString(contentsModel));
|
||||
|
||||
$effect(() => {
|
||||
loadContents(path, id);
|
||||
});
|
||||
|
||||
function loadContents(path: string | undefined, id: string) {
|
||||
if (!path || !id) return;
|
||||
|
||||
invoke<ContentModel>("get_contents", {id, path})
|
||||
.then((result) => {
|
||||
contentsModel = result;
|
||||
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}
|
||||
|
||||
function mapToString(model: ContentModel | undefined) {
|
||||
if (!model) return "";
|
||||
|
||||
let text = "";
|
||||
if (model.parts.length > 1) {
|
||||
let i = 0;
|
||||
for (let part of model.parts) {
|
||||
text +=
|
||||
"%----------------% Contents[" +
|
||||
i +
|
||||
"] %--------------%\n\n";
|
||||
for (let line of part) {
|
||||
text += " " + line + "\n";
|
||||
}
|
||||
text +=
|
||||
"\n%-------------------% EOF %-------------------%\n\n";
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
text = model.parts[0].join("\n");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
||||
@ -8,6 +8,7 @@
|
||||
import type {PathSelectedEvent} from "../events/PathSelectedEvent";
|
||||
import {onMount} from "svelte";
|
||||
import NotificationModal from "./NotificationModal.svelte";
|
||||
import ChangesModal from "./ChangesModal.svelte";
|
||||
|
||||
let {
|
||||
fState,
|
||||
@ -18,20 +19,26 @@
|
||||
} = $props();
|
||||
|
||||
let width: number = $state(0);
|
||||
let modalWidth = $derived(fState.xRefShowing || fState.notificationsShowing ? 281 : 0);
|
||||
let modalWidth = $derived(fState.xRefShowing || fState.notificationsShowing || fState.changesShowing ? 281 : 0);
|
||||
let splitPanesWidth: number = $derived(width - modalWidth);
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// Check for "Alt + Left Arrow"
|
||||
if (event.altKey && event.key === "ArrowLeft") {
|
||||
fState.popPath();
|
||||
fState.back();
|
||||
}
|
||||
if (event.altKey && event.key === "ArrowRight") {
|
||||
fState.forward();
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseButton(event: MouseEvent) {
|
||||
// Check for mouse back button (button 4)
|
||||
if (event.button === 3) {
|
||||
fState.popPath();
|
||||
fState.back();
|
||||
}
|
||||
if (event.button === 4) {
|
||||
fState.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,6 +93,11 @@
|
||||
<NotificationModal {fState}></NotificationModal>
|
||||
</div>
|
||||
{/if}
|
||||
{#if fState.changesShowing}
|
||||
<div class="xref-modal" class:visible={fState.changesShowing}>
|
||||
<ChangesModal {fState}></ChangesModal>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
|
||||
@ -4,16 +4,18 @@
|
||||
import {PageViewState} from "../models/PageViewState.svelte";
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
import StreamEditor from "./StreamEditor.svelte";
|
||||
import ContentModel from "../models/ContentModel.svelte";
|
||||
|
||||
let {fState, height}: {fState: FileViewState, height: number} = $props();
|
||||
let state: PageViewState | undefined = $derived(fState.pageViewState);
|
||||
let contents: ContentModel | undefined = $derived(state?.contents)
|
||||
|
||||
</script>
|
||||
{#if state}
|
||||
<Splitpanes theme="forge-movable">
|
||||
<Pane minSize={1}>
|
||||
<div class="overflow-hidden">
|
||||
<StreamEditor save={state.handleSave} stream_data={state.contents.toDisplay()} height={height - 1}></StreamEditor>
|
||||
<StreamEditor save={(newData) => state.handleSave(newData)} stream_data={contents ? contents.toDisplay() : ""} height={height - 1}></StreamEditor>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane minSize={1}>
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
PlusOutline,
|
||||
FilePdfSolid,
|
||||
ArrowsRepeatOutline,
|
||||
DownloadOutline,
|
||||
ArrowLeftOutline,
|
||||
ArrowRightOutline,
|
||||
AngleDownOutline
|
||||
} from "flowbite-svelte-icons";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { homeDir } from "@tauri-apps/api/path";
|
||||
@ -22,25 +26,25 @@
|
||||
selectTab,
|
||||
closeTab,
|
||||
openTab: newFile,
|
||||
reload
|
||||
}: {
|
||||
fStates: FileViewState[];
|
||||
fState: FileViewState | undefined;
|
||||
selectTab: (arg0: FileViewState) => void;
|
||||
closeTab: (arg0: FileViewState) => void;
|
||||
openTab: () => void;
|
||||
reload: () => void;
|
||||
} = $props();
|
||||
|
||||
let dropdownVisible = $state(false);
|
||||
let dropdownEl: HTMLElement;
|
||||
let historyVisible = $state(false);
|
||||
let historyEl: HTMLElement;
|
||||
|
||||
async function getHome() {
|
||||
home = await homeDir();
|
||||
}
|
||||
|
||||
function formatFileName(name: string) {
|
||||
if (name.length < 20) {
|
||||
if (name.length < 100) {
|
||||
return name;
|
||||
}
|
||||
return name.substring(0, 17) + "...";
|
||||
@ -84,6 +88,20 @@
|
||||
dropdownVisible = !dropdownVisible;
|
||||
}
|
||||
|
||||
function toggleHistoryDropdown() {
|
||||
historyVisible = !historyVisible;
|
||||
}
|
||||
|
||||
function handleFocusOutHistory(e: FocusEvent) {
|
||||
const dropdown = historyEl;
|
||||
if (!dropdown) return;
|
||||
|
||||
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||
if (!dropdown.contains(relatedTarget)) {
|
||||
historyVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocusOut(e: FocusEvent) {
|
||||
const dropdown = dropdownEl;
|
||||
if (!dropdown) return;
|
||||
@ -94,6 +112,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function handleSelectTab(state: FileViewState) {
|
||||
selectTab(state);
|
||||
dropdownVisible = false;
|
||||
@ -117,10 +137,46 @@
|
||||
alt="PDF Forge Logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mx-4"></div>
|
||||
<div class="flex flex-row">
|
||||
<button
|
||||
onclick={() => fState?.back()}
|
||||
class="nav-button"
|
||||
id="titlebar-back"
|
||||
class:possible={fState?.canBack}
|
||||
>
|
||||
<ArrowLeftOutline/>
|
||||
</button>
|
||||
<button
|
||||
onfocusout={handleFocusOutHistory}
|
||||
onclick={() => toggleHistoryDropdown()}
|
||||
class="nav-button possible"
|
||||
id="titlebar-forward"
|
||||
>
|
||||
<AngleDownOutline/>
|
||||
</button>
|
||||
{#if historyVisible && fState}
|
||||
<div class="dropdown-menu" bind:this={historyEl}>
|
||||
{#each fState.pathHistory.history as path, index}
|
||||
<button class="dropdown-item new-tab active:bg-forge-sec" class:active={fState.pathHistory.currentIndex === index} onclick={() => fState.jump(index)}>
|
||||
{path.join("/")}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => fState?.forward()}
|
||||
class="nav-button"
|
||||
id="titlebar-forward"
|
||||
class:possible={fState?.canForward}
|
||||
>
|
||||
<ArrowRightOutline/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="file-selector" onfocusout={handleFocusOut}>
|
||||
<button class="file-dropdown-button" onclick={toggleDropdown}>
|
||||
<span
|
||||
<FilePdfSolid size="sm" />
|
||||
<span class="px-2"
|
||||
>{fState
|
||||
? formatFileName(fState.file.name)
|
||||
: "Select File"}</span
|
||||
@ -170,15 +226,25 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row">
|
||||
<button
|
||||
onclick={() => fState?.reload()}
|
||||
class="titlebar-button"
|
||||
id="titlebar-reload"
|
||||
>
|
||||
<ArrowsRepeatOutline/>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => fState?.save()}
|
||||
class="titlebar-button"
|
||||
id="titlebar-save"
|
||||
>
|
||||
<DownloadOutline/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="titlebar-button-group">
|
||||
<button
|
||||
onclick={reload}
|
||||
class="titlebar-button"
|
||||
id="titlebar-maximize"
|
||||
>
|
||||
<ArrowsRepeatOutline />
|
||||
</button>
|
||||
<button
|
||||
onclick={minimize}
|
||||
class="titlebar-button"
|
||||
@ -200,6 +266,22 @@
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
|
||||
.nav-button.possible {
|
||||
@apply hover:bg-forge-acc text-forge-text
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
@apply text-forge-text_sec rounded ;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
@apply border border-forge-bound;
|
||||
height: 30px;
|
||||
@ -220,7 +302,7 @@
|
||||
}
|
||||
|
||||
.titlebar-button {
|
||||
@apply text-forge-text hover:bg-forge-acc;
|
||||
@apply text-forge-text hover:bg-forge-acc rounded;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -231,8 +313,7 @@
|
||||
}
|
||||
|
||||
.file-selector {
|
||||
@apply relative ml-10;
|
||||
z-index: 100;
|
||||
@apply relative ml-4;
|
||||
}
|
||||
|
||||
.file-dropdown-button {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {ToolbarButton} from "flowbite-svelte";
|
||||
import {ListOutline} from "flowbite-svelte-icons";
|
||||
import {ListOutline, InfoCircleOutline, UndoOutline} from "flowbite-svelte-icons";
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
|
||||
let {fState}: { fState: FileViewState | undefined } = $props()
|
||||
@ -10,6 +10,9 @@
|
||||
if (fState.notificationsShowing) {
|
||||
fState.notificationsShowing = false;
|
||||
}
|
||||
if (fState.changesShowing) {
|
||||
fState.changesShowing = false
|
||||
}
|
||||
fState.xRefShowing = !fState.xRefShowing;
|
||||
}
|
||||
|
||||
@ -18,9 +21,23 @@
|
||||
if (fState.xRefShowing) {
|
||||
fState.xRefShowing = false;
|
||||
}
|
||||
if (fState.changesShowing) {
|
||||
fState.changesShowing = false
|
||||
}
|
||||
fState.notificationsShowing = !fState.notificationsShowing;
|
||||
}
|
||||
|
||||
function toggleChanges() {
|
||||
if (!fState) return;
|
||||
if (fState.xRefShowing) {
|
||||
fState.xRefShowing = false;
|
||||
}
|
||||
if (fState.notificationsShowing) {
|
||||
fState.notificationsShowing = false;
|
||||
}
|
||||
fState.changesShowing = !fState.changesShowing;
|
||||
}
|
||||
|
||||
</script>
|
||||
<div class="grid grid-cols-1">
|
||||
|
||||
@ -33,10 +50,17 @@
|
||||
|
||||
<button class={ fState?.notificationsShowing ? "tool-button active" : "tool-button" } onclick={toggleNots}>
|
||||
<div class="justify-center flex text-forge-text">
|
||||
<ListOutline/>
|
||||
<InfoCircleOutline/>
|
||||
</div>
|
||||
<b class="button-title">Notifications</b>
|
||||
</button>
|
||||
|
||||
<button class={ fState?.changesShowing ? "tool-button active" : "tool-button" } onclick={toggleChanges}>
|
||||
<div class="justify-center flex text-forge-text">
|
||||
<UndoOutline/>
|
||||
</div>
|
||||
<b class="button-title">Changes</b>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
import type {TreeViewModel} from "../models/TreeViewModel";
|
||||
import TreeViewEntry from "./TreeViewEntry.svelte";
|
||||
import {onMount} from "svelte";
|
||||
|
||||
let {
|
||||
fState,
|
||||
@ -28,24 +29,34 @@
|
||||
treeState ? treeState.stickies : [],
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
onMount(() => {
|
||||
scrollY = Math.min(scrollY, totalHeight);
|
||||
treeState?.loadTreeView(scrollY, height);
|
||||
}
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
treeState = fState.treeState;
|
||||
if (treeState) {
|
||||
scrollY = Math.min(scrollY, totalHeight);
|
||||
treeState.updateTreeView(scrollY, height);
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function handleSelect(prim: TreeViewModel) {
|
||||
if (prim.expanded && prim.container) {
|
||||
treeState
|
||||
?.collapseTree(prim.path.map((path) => path.key))
|
||||
.then(() => {
|
||||
treeState?.loadTreeView(scrollY, height);
|
||||
treeState?.updateTreeView(scrollY, height);
|
||||
});
|
||||
return;
|
||||
} else if (prim.container) {
|
||||
treeState
|
||||
?.expandTree(prim.path.map((path) => path.key))
|
||||
.then(() => {
|
||||
treeState?.loadTreeView(scrollY, height);
|
||||
treeState?.updateTreeView(scrollY, height);
|
||||
});
|
||||
}
|
||||
fState.selectPathHandler(
|
||||
@ -54,12 +65,12 @@
|
||||
prim.path.map((path) => path.key),
|
||||
),
|
||||
);
|
||||
treeState?.loadTreeView(scrollY, height);
|
||||
treeState?.updateTreeView(scrollY, height);
|
||||
}
|
||||
|
||||
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
|
||||
scrollY = event.currentTarget.scrollTop;
|
||||
treeState?.loadTreeView(scrollY, height);
|
||||
treeState?.updateTreeView(scrollY, height);
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -102,34 +113,5 @@
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.prim_name {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caret {
|
||||
@apply text-forge-sec;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.details {
|
||||
@apply ml-1 text-forge-prim whitespace-nowrap font-extralight;
|
||||
}
|
||||
|
||||
.row {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.no-caret {
|
||||
@apply pl-5;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -15,50 +15,50 @@
|
||||
return key;
|
||||
}
|
||||
</script>
|
||||
{#if entry.depth == 0}
|
||||
<div
|
||||
style="height: 1px; width: 100%;"
|
||||
class="bg-forge-bound"
|
||||
></div>
|
||||
{/if}
|
||||
<button
|
||||
class="row text-sm hover:bg-forge-sec w-full group whitespace-nowrap"
|
||||
style="height: {rowHeight}px;"
|
||||
onclick={onclick}
|
||||
>
|
||||
<div style="margin-left: {entry.depth * 1.25}em">
|
||||
{#if entry.container}
|
||||
<div>
|
||||
<span
|
||||
class="caret group-hover:text-forge-text_hint"
|
||||
>{#if entry.expanded}<CaretDownOutline
|
||||
/>{:else}<CaretRightOutline
|
||||
/>{/if}</span
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="no-caret"></span>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<PrimitiveIcon ptype={entry.ptype}/>
|
||||
</div>
|
||||
<div class="pl-1 prim_name whitespace-nowrap">
|
||||
<p>
|
||||
{formatDisplayKey(entry.key)}
|
||||
</p>
|
||||
<div
|
||||
class="details group-hover:text-forge-text_hint"
|
||||
>
|
||||
{" | " +
|
||||
entry.value +
|
||||
" | " +
|
||||
entry.sub_type +
|
||||
" | " +
|
||||
entry.ptype}
|
||||
{#if entry.depth == 0}
|
||||
<div
|
||||
style="height: 1px; width: 100%;"
|
||||
class="bg-forge-bound"
|
||||
></div>
|
||||
{/if}
|
||||
<button
|
||||
class="row text-sm hover:bg-forge-sec w-full group whitespace-nowrap"
|
||||
style="height: {rowHeight}px;"
|
||||
onclick={onclick}
|
||||
>
|
||||
<div style="margin-left: {entry.depth * 1.25}em">
|
||||
{#if entry.container}
|
||||
<div class="caret group-hover:text-forge-text_hint">
|
||||
{#if entry.expanded}
|
||||
<CaretDownOutline />
|
||||
{:else}
|
||||
<CaretRightOutline />
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
{:else}
|
||||
<span class="no-caret"></span>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<PrimitiveIcon ptype={entry.ptype}/>
|
||||
</div>
|
||||
<div class="pl-1 prim_name whitespace-nowrap">
|
||||
<p>
|
||||
{formatDisplayKey(entry.key)}
|
||||
</p>
|
||||
<div
|
||||
class="details group-hover:text-forge-text_hint"
|
||||
>
|
||||
{" | " +
|
||||
entry.value +
|
||||
" | " +
|
||||
entry.sub_type +
|
||||
" | " +
|
||||
entry.ptype}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style lang="postcss">
|
||||
.prim_name {
|
||||
@ -70,9 +70,10 @@
|
||||
}
|
||||
|
||||
.caret {
|
||||
@apply text-forge-sec;
|
||||
@apply text-forge-sec align-middle;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.details {
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
fState.getLastJump(),
|
||||
);
|
||||
let totalBodyHeight: number = $derived(
|
||||
Math.max(0, fState.file.xref_entries * cellH - headerOffset),
|
||||
Math.max(0, fState.xref_entries.length * cellH - headerOffset),
|
||||
);
|
||||
let scrollContainer: HTMLElement;
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const EOF: string = "\n%-------------------% EOF %-------------------%\n\n";
|
||||
const CONTENTS: string = "%----------------% Contents[%d] %--------------%\n\n";
|
||||
const CONTENTS_PATTERN = /%----------------% Contents\[\d+] %--------------%\n\n([\s\S]*?)(?=%-------------------% EOF %-------------------%|$)/gm;
|
||||
const CONTENTS_PATTERN = /%----------------% Contents\[\d+\] %--------------%((?:.|\s)*?)%-------------------% EOF %-------------------%/gmu;
|
||||
export default class ContentModel {
|
||||
parts: string[][];
|
||||
constructor(parts: string[][]) {
|
||||
@ -26,17 +26,17 @@ export default class ContentModel {
|
||||
return text;
|
||||
}
|
||||
|
||||
public fromDisplay(display: string): ContentModel {
|
||||
public static fromDisplay(display: string): ContentModel {
|
||||
const parts: string[][] = [];
|
||||
let match;
|
||||
while ((match = CONTENTS_PATTERN.exec(display)) !== null) {
|
||||
const lines = match[1].split("\n").map(line => line.trim()).filter(line => line !== "");
|
||||
let lines = match[1].split("\n").map(line => line.trim()).filter(line => line.length > 0);
|
||||
parts.push(lines);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
if (parts.length == 0) {
|
||||
parts.push(display.split("\n").map(line => line.trim()));
|
||||
}
|
||||
|
||||
return new ContentModel(parts);
|
||||
}
|
||||
}
|
||||
@ -11,15 +11,20 @@ import {Mutex} from 'async-mutex';
|
||||
import {RawImageData} from "./RawImageData";
|
||||
import TreeViewState from "./TreeViewState.svelte";
|
||||
import {PageViewState} from "./PageViewState.svelte";
|
||||
import PathHistory from "./PathHistory.svelte";
|
||||
|
||||
export default class FileViewState {
|
||||
public pathHistory: PathHistory = new PathHistory();
|
||||
|
||||
public file: PdfFile;
|
||||
|
||||
public treeMode: boolean = $state(true);
|
||||
public pageMode: boolean = $state(false);
|
||||
public xRefShowing: boolean = $state(false);
|
||||
public notificationsShowing: boolean = $state(false);
|
||||
|
||||
public changesShowing: boolean = $state(false);
|
||||
public canForward: boolean = $derived(this.pathHistory.canForward)
|
||||
public canBack: boolean = $derived(this.pathHistory.canBack)
|
||||
|
||||
public path: string[] = $state(["Trailer"]);
|
||||
public container_prim: Primitive | undefined = $state();
|
||||
@ -33,6 +38,9 @@ export default class FileViewState {
|
||||
public notifications: ForgeNotification[] = $state([]);
|
||||
notificationMutex = new Mutex();
|
||||
|
||||
public changes: PrimitiveModel[] = $state([]);
|
||||
|
||||
|
||||
constructor(file: PdfFile) {
|
||||
|
||||
this.file = file;
|
||||
@ -59,20 +67,24 @@ export default class FileViewState {
|
||||
newData: updated_data
|
||||
})
|
||||
.then(() => {
|
||||
console.log("saved");
|
||||
this.reload();
|
||||
this.container_prim?.stream_data?.setData(updated_data);
|
||||
})
|
||||
.catch(err => this.logError(err));
|
||||
}
|
||||
|
||||
public reload() {
|
||||
public async reload() {
|
||||
this.container_prim = undefined;
|
||||
this.selected_leaf_prim = undefined;
|
||||
|
||||
this.loadXrefEntries();
|
||||
this.selectPath(this.path);
|
||||
this.treeState?.reload();
|
||||
this.reloadPageState();
|
||||
await this.selectPath(this.path);
|
||||
this.pageViewStates.clear();
|
||||
if (this.pageViewState) {
|
||||
this.pageViewState.reload()
|
||||
this.pageViewStates.set(this.pageViewState.page_num, this.pageViewState);
|
||||
}
|
||||
}
|
||||
|
||||
public logError(message: string) {
|
||||
@ -86,10 +98,6 @@ export default class FileViewState {
|
||||
});
|
||||
}
|
||||
|
||||
public setTreeViewState(state: TreeViewState) {
|
||||
this.treeState = state;
|
||||
}
|
||||
|
||||
public async deleteNotification(timestamp: number) {
|
||||
const release = await this.notificationMutex.acquire();
|
||||
try {
|
||||
@ -108,12 +116,26 @@ export default class FileViewState {
|
||||
}
|
||||
}
|
||||
|
||||
public async clearChanges() {
|
||||
invoke<PrimitiveModel[]>("clear_changes", {id: this.file.id})
|
||||
.then(_ => {
|
||||
this.reload()
|
||||
})
|
||||
.catch(err => this.logError(err));
|
||||
}
|
||||
|
||||
public loadXrefEntries() {
|
||||
invoke<XRefTable>("get_xref_table", {id: this.file.id})
|
||||
.then(result => {
|
||||
this.xref_entries = result.entries;
|
||||
})
|
||||
.catch(err => this.logError(err));
|
||||
|
||||
invoke<PrimitiveModel[]>("get_changes", {id: this.file.id})
|
||||
.then(result => {
|
||||
this.changes = result;
|
||||
})
|
||||
.catch(err => this.logError(err));
|
||||
}
|
||||
|
||||
public async selectPath(newPath: string[]) {
|
||||
@ -131,6 +153,7 @@ export default class FileViewState {
|
||||
if (newPrim.isContainer()) {
|
||||
this.container_prim = newPrim;
|
||||
this.path = newPath
|
||||
this.pathHistory.newPath(newPath);
|
||||
this.loadPageState().catch(err => this.logError(err));
|
||||
return;
|
||||
}
|
||||
@ -173,13 +196,31 @@ export default class FileViewState {
|
||||
return this.formatPaths(this.path);
|
||||
}
|
||||
|
||||
public popPath() {
|
||||
let path = this.copyPath();
|
||||
if (path.length == 1) {
|
||||
return
|
||||
public back() {
|
||||
if (!this.canBack) return;
|
||||
let prevPath = this.pathHistory.goBack();
|
||||
if (prevPath) {
|
||||
this.selectPath(prevPath)
|
||||
}
|
||||
path.pop()
|
||||
this.selectPath(path);
|
||||
}
|
||||
|
||||
public forward() {
|
||||
if (!this.canForward) return;
|
||||
let prevPath = this.pathHistory.goForward();
|
||||
if (prevPath) {
|
||||
this.selectPath(prevPath)
|
||||
}
|
||||
}
|
||||
|
||||
public jump(i: number) {
|
||||
let prevPath = this.pathHistory.jump(i);
|
||||
if (prevPath) {
|
||||
this.selectPath(prevPath)
|
||||
}
|
||||
}
|
||||
|
||||
public save() {
|
||||
|
||||
}
|
||||
|
||||
public copyPath() {
|
||||
@ -211,9 +252,10 @@ export default class FileViewState {
|
||||
}
|
||||
|
||||
|
||||
private async reloadPageState() {
|
||||
this.pageViewState = undefined;
|
||||
this.pageViewStates = new Map();
|
||||
private async reloadPageState(page_num: number) {
|
||||
|
||||
this.pageViewState = new PageViewState(page_num, this);
|
||||
this.pageViewStates.set(page_num, this.pageViewState);
|
||||
}
|
||||
|
||||
private async loadPageState() {
|
||||
@ -221,11 +263,10 @@ export default class FileViewState {
|
||||
let page_num = this.container_prim.getPageNumber();
|
||||
|
||||
if (this.pageViewState && this.pageViewState.page_num === page_num) return;
|
||||
|
||||
this.pageViewState = this.pageViewStates.get(page_num);
|
||||
|
||||
if (!this.pageViewState) {
|
||||
this.pageViewState = new PageViewState(page_num, this);
|
||||
this.pageViewStates.set(page_num, this.pageViewState);
|
||||
await this.reloadPageState(page_num);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,34 +9,40 @@ export class PageViewState {
|
||||
fState: FileViewState;
|
||||
|
||||
img_data: RawImageData | undefined = $state();
|
||||
contents: ContentModel = $state(new ContentModel([]));
|
||||
contents: ContentModel | undefined = $state(undefined);
|
||||
|
||||
constructor(page_num: number, fState: FileViewState) {
|
||||
this.file_id = fState.file.id;
|
||||
this.page_num = page_num;
|
||||
this.fState = fState;
|
||||
|
||||
this.load(page_num);
|
||||
this.load();
|
||||
}
|
||||
|
||||
private logError(err: string) {
|
||||
this.fState.logError(err);
|
||||
}
|
||||
|
||||
public async load(page_num: number) {
|
||||
invoke<ContentModel>("get_contents", {id: this.file_id, path: "Page" + page_num})
|
||||
public async reload() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
public async load() {
|
||||
invoke<ContentModel>("get_contents", {id: this.file_id, path: "Page" + this.page_num})
|
||||
.then((result) => {
|
||||
this.page_num = page_num;
|
||||
this.contents = undefined;
|
||||
this.contents = new ContentModel(result.parts);
|
||||
this.loadImage(page_num);
|
||||
this.loadImage();
|
||||
})
|
||||
.catch(this.logError);
|
||||
}
|
||||
|
||||
public async loadImage(page_num: number) {
|
||||
public async loadImage() {
|
||||
this.img_data?.dispose();
|
||||
this.img_data = undefined;
|
||||
let result = await invoke<ArrayBuffer>("get_page_by_num", {
|
||||
id: this.file_id,
|
||||
num: page_num,
|
||||
num: this.page_num,
|
||||
})
|
||||
.catch(this.logError);
|
||||
if (result) {
|
||||
@ -45,9 +51,12 @@ export class PageViewState {
|
||||
}
|
||||
|
||||
public handleSave(newData: string) {
|
||||
invoke("update_contents", {id: this.file_id, path: "Page" + this.page_num, newData: newData})
|
||||
.then((_) => {
|
||||
this.loadImage(this.page_num);
|
||||
let contents = ContentModel.fromDisplay(newData);
|
||||
console.log(contents);
|
||||
invoke<ContentModel>("update_contents", {id: this.file_id, path: "Page" + this.page_num, contents: contents})
|
||||
.then((result) => {
|
||||
this.contents = undefined;
|
||||
this.fState.reload();
|
||||
})
|
||||
.catch(this.logError);
|
||||
}
|
||||
|
||||
54
src/models/PathHistory.svelte.ts
Normal file
54
src/models/PathHistory.svelte.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import {arraysAreEqual} from "../utils";
|
||||
|
||||
export default class PathHistory {
|
||||
public currentIndex: number = $state(-1);
|
||||
public history: string[][] = $state([]);
|
||||
|
||||
public canForward: boolean = $derived(this.currentIndex < this.history.length - 1);
|
||||
public canBack: boolean = $derived(this.currentIndex > 0);
|
||||
|
||||
public newPath(path: string[]) {
|
||||
let current = this.getCurrent();
|
||||
if (current && arraysAreEqual(path, current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentIndex ++;
|
||||
this.history = this.history.slice(0, this.currentIndex);
|
||||
this.history.push(this.copy(path));
|
||||
}
|
||||
|
||||
public goBack(): string[] | undefined {
|
||||
if (!this.canBack) return undefined;
|
||||
this.currentIndex--;
|
||||
return this.getCurrent();
|
||||
}
|
||||
|
||||
public goForward(): string[] | undefined {
|
||||
if (!this.canForward) return undefined;
|
||||
this.currentIndex++;
|
||||
return this.getCurrent();
|
||||
}
|
||||
|
||||
public jump(i: number): string[] | undefined {
|
||||
if (i >= 0 && i < this.history.length) {
|
||||
this.currentIndex = i;
|
||||
return this.getCurrent();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getCurrent(): string[] | undefined {
|
||||
if (!this.history || this.currentIndex < 0 || this.currentIndex >= this.history.length) return undefined;
|
||||
return this.copy(this.history[this.currentIndex])
|
||||
}
|
||||
|
||||
private copy(path: string[]) {
|
||||
const _path: string[] = [];
|
||||
|
||||
for (let item of path) {
|
||||
_path.push(item);
|
||||
}
|
||||
return _path;
|
||||
}
|
||||
}
|
||||
@ -25,8 +25,9 @@ export default class TreeViewState {
|
||||
|
||||
this.initialRequest = [TreeViewRequest.TRAILER];
|
||||
this.initialRequest = this.initialRequest.concat(TreeViewRequest.fromPageCount(+fState.file.page_count));
|
||||
this.activeRequest.set(this.initialRequest[0].key, this.initialRequest[0]);
|
||||
this.file_id = fState.file.id;
|
||||
this.updateTreeViewRequest([this.initialRequest[0]]);
|
||||
|
||||
}
|
||||
|
||||
public getEntryCount() {
|
||||
@ -121,6 +122,10 @@ export default class TreeViewState {
|
||||
public async loadTreeView(scrollY: number, height: number) {
|
||||
const activeRequests = Array.from(this.activeRequest.values());
|
||||
await this.updateTreeViewRequest(activeRequests);
|
||||
await this.updateTreeView(scrollY, height);
|
||||
}
|
||||
|
||||
public async updateTreeView(scrollY: number, height: number) {
|
||||
let firstEntry = Math.floor(scrollY / ROW_HEIGHT);
|
||||
let lastEntry = Math.ceil((scrollY + height) / ROW_HEIGHT);
|
||||
this.total_height = this.getEntryCount() * ROW_HEIGHT;
|
||||
@ -141,7 +146,6 @@ export default class TreeViewState {
|
||||
}
|
||||
|
||||
public async updateTreeViewRequest(treeViewRequests: TreeViewRequest[]) {
|
||||
|
||||
let result = await invoke<TreeViewModel[]>("get_prim_tree_by_path", {
|
||||
id: this.file_id,
|
||||
paths: treeViewRequests,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user