updates working

This commit is contained in:
Kilian Schuettler 2025-02-18 01:37:16 +01:00
parent f484c9b54c
commit 4e9e01bf94
23 changed files with 589 additions and 248 deletions

View File

@ -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))
}
}
}

View File

@ -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)]

View File

@ -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
View File

@ -3689,6 +3689,7 @@ dependencies = [
"image 0.25.5",
"indexmap 1.9.3",
"lazy_static",
"log",
"pathfinder_geometry",
"pathfinder_rasterize",
"pdf",

View File

@ -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"

View File

@ -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");

View File

@ -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(&current_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)),
))
}

View File

@ -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);
}
}

View File

@ -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">

View 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>
&nbsp
<p>{change.ptype}</p>&nbsp|&nbsp
<p>{change.sub_type}</p>&nbsp|&nbsp
<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>

View File

@ -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>

View File

@ -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">

View File

@ -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}>

View File

@ -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 {

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}

View 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;
}
}

View File

@ -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,