updates incoming
This commit is contained in:
parent
0e9f020e26
commit
f484c9b54c
File diff suppressed because it is too large
Load Diff
@ -249,7 +249,7 @@ impl<'a> IntoIterator for &'a Dictionary {
|
||||
#[derive(Clone, Debug, PartialEq, DataSize)]
|
||||
pub struct PdfStream {
|
||||
pub info: Dictionary,
|
||||
pub (crate) inner: StreamInner,
|
||||
pub inner: StreamInner,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DataSize)]
|
||||
|
||||
74
src-tauri/Cargo.lock
generated
74
src-tauri/Cargo.lock
generated
@ -1011,6 +1011,12 @@ version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728"
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
|
||||
|
||||
[[package]]
|
||||
name = "datasize"
|
||||
version = "0.2.15"
|
||||
@ -3681,6 +3687,7 @@ name = "pdf-forge"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"image 0.25.5",
|
||||
"indexmap 1.9.3",
|
||||
"lazy_static",
|
||||
"pathfinder_geometry",
|
||||
"pathfinder_rasterize",
|
||||
@ -3694,6 +3701,7 @@ dependencies = [
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-websocket",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@ -4811,6 +4819,17 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
@ -5350,6 +5369,25 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-websocket"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af3ac71aec5fb0ae5441e830cd075b1cbed49ac3d39cb975a4894ea8fa2e62b9"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.11",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.3.0"
|
||||
@ -5607,6 +5645,22 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4bf6fecd69fcdede0ec680aaf474cdab988f9de6bc73d3758f0160e3b7025a"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tungstenite",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.13"
|
||||
@ -5775,6 +5829,26 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror 2.0.11",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tuple"
|
||||
version = "0.5.2"
|
||||
|
||||
@ -33,3 +33,5 @@ image = { version = "0.25.5", features = ["jpeg"] }
|
||||
pdf_render = { path = "../../pdf-render/render" }
|
||||
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"
|
||||
|
||||
@ -11,14 +11,13 @@ 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};
|
||||
use pdf::object::{Object, 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::DerefMut;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tauri::ipc::{InvokeResponseBody, Response};
|
||||
@ -26,7 +25,6 @@ use tauri::{Manager, State};
|
||||
use uuid::Uuid;
|
||||
|
||||
type CosFile = File<Vec<u8>, ObjectCache, StreamCache, NoLog>;
|
||||
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct XRefTableModel {
|
||||
pub size: usize,
|
||||
@ -113,7 +111,7 @@ fn get_all_files(session: State<Session>) -> Vec<PdfFile> {
|
||||
session
|
||||
.get_all_files()
|
||||
.iter()
|
||||
.map(|s| s.pdf_file.clone())
|
||||
.map(|s| s.read().unwrap().pdf_file.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
@ -129,7 +127,9 @@ fn close_file(id: &str, session: State<Session>) {
|
||||
|
||||
#[tauri::command]
|
||||
fn get_file_by_id(id: &str, session: State<Session>) -> Result<PdfFile, String> {
|
||||
session.get_file(id).map(|file| file.pdf_file.clone())
|
||||
let lock = session.get_file(id)?;
|
||||
let file = lock.read().unwrap();
|
||||
Ok(file.pdf_file.clone())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -183,7 +183,8 @@ fn to_pdf_file(path: &str, file: &CosFile) -> Result<PdfFile, String> {
|
||||
|
||||
#[tauri::command]
|
||||
fn get_contents(id: &str, path: &str, session: State<Session>) -> Result<ContentsModel, String> {
|
||||
let file = session.get_file(id)?;
|
||||
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();
|
||||
@ -208,11 +209,30 @@ fn get_stream_data_as_string(
|
||||
path: &str,
|
||||
session: State<Session>,
|
||||
) -> Result<String, String> {
|
||||
let file = session.get_file(id)?;
|
||||
let lock = session.get_file(id)?;
|
||||
let file = lock.read().unwrap();
|
||||
let data = get_stream_data_by_path_with_file(path, &file.cos_file)?;
|
||||
Ok(String::from_utf8_lossy(&data).into_owned())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_stream_data_as_string(
|
||||
id: &str,
|
||||
path: &str,
|
||||
new_data: &str,
|
||||
session: State<'_, Session>,
|
||||
) -> Result<(), String> {
|
||||
let lock = session.get_file(id)?;
|
||||
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 stream = Stream::new((), new_data.as_bytes());
|
||||
let _ = t!(file.cos_file.update(plain_ref, stream));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_img_as_response(img: DynamicImage) -> Result<Response, String> {
|
||||
use std::mem;
|
||||
|
||||
@ -248,7 +268,8 @@ async fn get_stream_data_as_image(
|
||||
path: &str,
|
||||
session: State<'_, Session>,
|
||||
) -> Result<Response, String> {
|
||||
let file = session.get_file(id)?;
|
||||
let lock = session.get_file(id)?;
|
||||
let file = lock.read().unwrap();
|
||||
let img = retrieval::get_image_by_path(path, &file.cos_file)?;
|
||||
format_img_as_response(img)
|
||||
}
|
||||
@ -259,21 +280,39 @@ async fn get_page_by_num(
|
||||
num: u32,
|
||||
session: State<'_, Session>,
|
||||
) -> Result<Response, String> {
|
||||
let file = session.get_file(id)?;
|
||||
render_page_by_path(id, &format!("Page{}", num), session).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn render_page_by_path(
|
||||
id: &str,
|
||||
path: &str,
|
||||
session: State<'_, Session>,
|
||||
) -> Result<Response, String> {
|
||||
let lock = session.get_file(id)?;
|
||||
let file = lock.read().unwrap();
|
||||
let page = get_page_by_path(path, &file.cos_file)?;
|
||||
let mut renderer = Renderer::new(&file.cos_file, 150);
|
||||
let img = renderer.render(num)?;
|
||||
let img = renderer.render(&page)?;
|
||||
|
||||
format_img_as_response(img)
|
||||
}
|
||||
|
||||
fn get_page_by_path(path: &str, file: &CosFile) -> Result<pdf::object::Page, String> {
|
||||
let resolver = file.resolver();
|
||||
let (_, page_prim, _) = get_prim_by_path_with_file(path, file)?;
|
||||
let page = t!(pdf::object::Page::from_primitive(page_prim, &resolver));
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_prim_by_path(
|
||||
id: &str,
|
||||
path: &str,
|
||||
session: State<Session>,
|
||||
) -> Result<PrimitiveModel, String> {
|
||||
let file = session.get_file(id)?;
|
||||
|
||||
let lock = session.get_file(id)?;
|
||||
let file = lock.read().unwrap();
|
||||
get_prim_model_by_path_with_file(path, &file.cos_file)
|
||||
}
|
||||
|
||||
@ -313,7 +352,8 @@ fn get_prim_tree_by_path(
|
||||
paths: Vec<TreeViewRequest>,
|
||||
session: State<Session>,
|
||||
) -> Result<Vec<PrimitiveTreeView>, String> {
|
||||
let file = session.get_file(id)?;
|
||||
let lock = session.get_file(id)?;
|
||||
let file = lock.read().unwrap();
|
||||
|
||||
let results = paths
|
||||
.into_iter()
|
||||
@ -659,9 +699,11 @@ impl PrimitiveTreeView {
|
||||
}
|
||||
#[tauri::command]
|
||||
fn get_xref_table(id: &str, session: State<Session>) -> Result<XRefTableModel, String> {
|
||||
let file = session.get_file(id)?;
|
||||
let lock = session.get_file(id)?;
|
||||
let file = lock.read().unwrap();
|
||||
get_xref_table_model_with_file(&file.cos_file)
|
||||
}
|
||||
|
||||
fn get_xref_table_model_with_file(file: &CosFile) -> Result<XRefTableModel, String> {
|
||||
let resolver = file.resolver();
|
||||
let x_ref_table = file.get_xref();
|
||||
@ -719,7 +761,7 @@ fn get_xref_table_model_with_file(file: &CosFile) -> Result<XRefTableModel, Stri
|
||||
}
|
||||
|
||||
struct Session {
|
||||
files: RwLock<HashMap<String, Arc<SessionFile>>>,
|
||||
files: RwLock<HashMap<String, Arc<RwLock<SessionFile>>>>,
|
||||
}
|
||||
|
||||
struct SessionFile {
|
||||
@ -734,20 +776,15 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_file(&self, id: &str) -> Result<Arc<SessionFile>, String> {
|
||||
fn get_file(&self, id: &str) -> Result<Arc<RwLock<SessionFile>>, String> {
|
||||
let lock = self.files.read().unwrap();
|
||||
lock.get(id)
|
||||
.cloned()
|
||||
.ok_or(format!(" File {} not found!", id))
|
||||
}
|
||||
|
||||
fn get_all_files(&self) -> Vec<Arc<SessionFile>> {
|
||||
self.files
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.map(|f| f.clone())
|
||||
.collect()
|
||||
fn get_all_files(&self) -> Vec<Arc<RwLock<SessionFile>>> {
|
||||
self.files.read().unwrap().values().cloned().collect()
|
||||
}
|
||||
|
||||
fn get_all_file_ids(&self) -> Vec<String> {
|
||||
@ -763,10 +800,10 @@ impl Session {
|
||||
if let Ok(mut files) = self.files.write() {
|
||||
return match files.insert(
|
||||
pdf_file.id.clone(),
|
||||
Arc::new(SessionFile {
|
||||
Arc::new(RwLock::new(SessionFile {
|
||||
pdf_file: pdf_file,
|
||||
cos_file: cos_file,
|
||||
}),
|
||||
})),
|
||||
) {
|
||||
Some(_) => Err("File could not be uploaded!".to_string()),
|
||||
None => Ok(()),
|
||||
@ -802,7 +839,8 @@ pub fn run() {
|
||||
get_contents,
|
||||
get_stream_data_as_string,
|
||||
get_stream_data_as_image,
|
||||
get_page_by_num
|
||||
get_page_by_num,
|
||||
update_stream_data_as_string
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@ -5,6 +5,7 @@ use crate::{t, CosFile};
|
||||
use image::{DynamicImage, RgbaImage};
|
||||
use pathfinder_geometry::transform2d::Transform2F;
|
||||
use pathfinder_rasterize::Rasterizer;
|
||||
use pdf::object::Page;
|
||||
use pdf_render::{render_page, Cache, SceneBackend};
|
||||
|
||||
pub struct Renderer<'a> {
|
||||
@ -22,9 +23,8 @@ impl<'a> Renderer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self, page_num: u32) -> Result<DynamicImage, String> {
|
||||
pub fn render(&mut self, page: &Page) -> Result<DynamicImage, String> {
|
||||
let resolver = self.file.resolver();
|
||||
let page = t!(self.file.get_page(page_num - 1));
|
||||
let mut backend = SceneBackend::new(&mut self.cache);
|
||||
t!(render_page(&mut backend, &resolver, &page, self.transform));
|
||||
let scene = backend.finish();
|
||||
|
||||
@ -97,6 +97,7 @@
|
||||
closeTab={closeFile}
|
||||
openTab={upload}
|
||||
selectTab={(state) => (fState = state)}
|
||||
reload={() => fState?.reload()}
|
||||
></TitleBar>
|
||||
</div>
|
||||
<div class="fileview_container" style="height: {fileViewHeight}px">
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
|
||||
import ContentsView from "./ContentsView.svelte";
|
||||
import RenderedPageView from "./RenderedPageView.svelte";
|
||||
import {Pane, Splitpanes} from "svelte-splitpanes";
|
||||
import {PageViewState} from "../models/PageViewState.svelte";
|
||||
@ -8,30 +6,14 @@
|
||||
import StreamEditor from "./StreamEditor.svelte";
|
||||
|
||||
let {fState, height}: {fState: FileViewState, height: number} = $props();
|
||||
|
||||
let states: PageViewState[] = [];
|
||||
let state: PageViewState | undefined = $state(undefined);
|
||||
|
||||
$effect(() => {
|
||||
if (!(fState.container_prim && fState.container_prim.isPage())) return;
|
||||
|
||||
let page_num = fState.container_prim.getPageNumber();
|
||||
|
||||
if (state && state.file_id == fState.file.id && state.page_num === page_num) return;
|
||||
|
||||
state = states.find(s => s.file_id === fState.file.id && s.page_num == page_num)
|
||||
if (!state) {
|
||||
state = new PageViewState(fState.file.id, page_num, (m) => fState.logError(m));
|
||||
states.push(state);
|
||||
}
|
||||
})
|
||||
let state: PageViewState | undefined = $derived(fState.pageViewState);
|
||||
|
||||
</script>
|
||||
{#if fState.container_prim && fState.container_prim.isPage() && state}
|
||||
{#if state}
|
||||
<Splitpanes theme="forge-movable">
|
||||
<Pane minSize={1}>
|
||||
<div class="overflow-hidden">
|
||||
<StreamEditor stream_data={state.contents} height={height - 1}></StreamEditor>
|
||||
<StreamEditor save={state.handleSave} stream_data={state.contents.toDisplay()} height={height - 1}></StreamEditor>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane minSize={1}>
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
import type Primitive from "../models/Primitive.svelte";
|
||||
import PrimitiveIcon from "./PrimitiveIcon.svelte";
|
||||
import {Pane, Splitpanes} from "svelte-splitpanes";
|
||||
import { Pane, Splitpanes } from "svelte-splitpanes";
|
||||
import StreamDataView from "./StreamDataView.svelte";
|
||||
|
||||
const cellH = 29;
|
||||
const headerOffset = 24;
|
||||
let {fState, height}: { fState: FileViewState; height: number } =
|
||||
let { fState, height }: { fState: FileViewState; height: number } =
|
||||
$props();
|
||||
let fillerHeight: number = $state(0);
|
||||
let firstEntry = $state(0);
|
||||
@ -20,9 +20,6 @@
|
||||
let tableHeight = $derived(prim ? prim.children.length * cellH : 0);
|
||||
|
||||
let bodyHeight = $derived(height);
|
||||
let editorHeight = $derived(
|
||||
Math.max(800, bodyHeight - tableHeight - headerOffset),
|
||||
);
|
||||
|
||||
let locallySelected: Primitive | undefined = $state(undefined);
|
||||
$effect(() => {
|
||||
@ -50,53 +47,65 @@
|
||||
fState.selectPath(_path);
|
||||
}
|
||||
</script>
|
||||
<Splitpanes theme="forge-movable">
|
||||
|
||||
<Splitpanes theme="forge-movable">
|
||||
<Pane minSize={1}>
|
||||
{#if prim && prim.children && prim.children.length > 0}
|
||||
<div
|
||||
class="overflow-auto"
|
||||
onscroll={handleScroll}
|
||||
style="height: {bodyHeight}px"
|
||||
class="overflow-auto"
|
||||
onscroll={handleScroll}
|
||||
style="height: {bodyHeight}px"
|
||||
>
|
||||
<div class="w-[851px]">
|
||||
<table style="position: relative; top: {scrollY}px">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="page-cell t-header border-forge-prim">Key
|
||||
</td
|
||||
>
|
||||
<td class="ref-cell t-header border-forge-prim">Type
|
||||
</td
|
||||
>
|
||||
<td class="cell t-header border-forge-sec">Value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="page-cell t-header border-forge-prim"
|
||||
>Key
|
||||
</td>
|
||||
<td class="ref-cell t-header border-forge-prim"
|
||||
>Type
|
||||
</td>
|
||||
<td class="cell t-header border-forge-sec"
|
||||
>Value</td
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div class="container" style="height: {tableHeight}px">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr class="filler" style="height: {fillerHeight}px"
|
||||
></tr>
|
||||
{#each entriesToDisplay as entry}
|
||||
<tr
|
||||
class="filler"
|
||||
style="height: {fillerHeight}px"
|
||||
></tr>
|
||||
{#each entriesToDisplay as entry}
|
||||
<tr
|
||||
class:selected={entry.key ===
|
||||
locallySelected?.key}
|
||||
locallySelected?.key}
|
||||
class="row"
|
||||
onclick={() => handlePrimClick(entry)}
|
||||
ondblclick={() => handlePrimDbLClick(entry)}
|
||||
>
|
||||
<td class="page-cell t-data">
|
||||
<div class="key-field">
|
||||
<PrimitiveIcon ptype={entry.ptype}/>
|
||||
<p class="text-left">
|
||||
{entry.key}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="ref-cell t-data">{entry.ptype}</td>
|
||||
<td class="cell t-data">{entry.value}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
ondblclick={() =>
|
||||
handlePrimDbLClick(entry)}
|
||||
>
|
||||
<td class="page-cell t-data">
|
||||
<div class="key-field">
|
||||
<PrimitiveIcon
|
||||
ptype={entry.ptype}
|
||||
/>
|
||||
<p class="text-left">
|
||||
{entry.key}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td class="ref-cell t-data"
|
||||
>{entry.ptype}</td
|
||||
>
|
||||
<td class="cell t-data"
|
||||
>{entry.value}</td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -105,17 +114,18 @@
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane
|
||||
size={fState.container_prim?.stream_data ? 50 : 0}
|
||||
minSize={fState.container_prim?.stream_data ? 1 : 0}
|
||||
maxSize={fState.container_prim?.stream_data ? 100 : 0}
|
||||
size={fState.container_prim?.stream_data ? 50 : 0}
|
||||
minSize={fState.container_prim?.stream_data ? 1 : 0}
|
||||
maxSize={fState.container_prim?.stream_data ? 100 : 0}
|
||||
>
|
||||
<div style:height={height} class="bg-forge-dark">
|
||||
<div style:height class="bg-forge-dark">
|
||||
{#if fState.container_prim?.stream_data}
|
||||
<StreamDataView {fState} {height}></StreamDataView>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
<style lang="postcss">
|
||||
.key-field {
|
||||
display: flex;
|
||||
|
||||
@ -1,29 +1,33 @@
|
||||
<script lang="ts">
|
||||
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
import StreamEditor from "./StreamEditor.svelte";
|
||||
import type {StreamData} from "../models/StreamData.svelte";
|
||||
import type { StreamData } from "../models/StreamData.svelte";
|
||||
import RenderedPageView from "./RenderedPageView.svelte";
|
||||
|
||||
let {fState, height}: { fState: FileViewState, height: number } = $props()
|
||||
let streamData: StreamData | undefined = $derived(fState.container_prim?.stream_data);
|
||||
|
||||
|
||||
let {
|
||||
fState,
|
||||
height,
|
||||
}: {
|
||||
fState: FileViewState;
|
||||
height: number;
|
||||
} = $props();
|
||||
let streamData: StreamData | undefined = $derived(
|
||||
fState.container_prim?.stream_data,
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if streamData}
|
||||
{#if streamData.type === "Image"}
|
||||
<RenderedPageView img={streamData.imageData} {height}></RenderedPageView>
|
||||
{:else}
|
||||
{#if streamData.textData}
|
||||
<StreamEditor
|
||||
stream_data={streamData.textData}
|
||||
{height}
|
||||
></StreamEditor>
|
||||
{/if}
|
||||
<RenderedPageView img={streamData.imageData} {height}
|
||||
></RenderedPageView>
|
||||
{:else if streamData.textData}
|
||||
<StreamEditor
|
||||
stream_data={streamData.textData}
|
||||
{height}
|
||||
save={(updated_data) => fState.saveStreamData(updated_data)}
|
||||
></StreamEditor>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -1,20 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { UploadOutline } from "flowbite-svelte-icons";
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
monaco.editor.defineTheme('forge-dark', {
|
||||
monaco.editor.defineTheme("forge-dark", {
|
||||
base: "vs-dark",
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
"editor.background": '#1E1F22FF'
|
||||
}
|
||||
"editor.background": "#1E1F22FF",
|
||||
},
|
||||
});
|
||||
let { stream_data, height }: { stream_data: string; height: number } =
|
||||
$props();
|
||||
let {
|
||||
stream_data,
|
||||
height,
|
||||
save,
|
||||
}: {
|
||||
stream_data: string;
|
||||
height: number;
|
||||
save: (updated_data: string) => void;
|
||||
} = $props();
|
||||
|
||||
let editorContainer: HTMLElement;
|
||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||
|
||||
let isEdited = $state(false);
|
||||
let last_data: string | undefined = $state(undefined);
|
||||
|
||||
onMount(() => {
|
||||
editor = monaco.editor.create(editorContainer, {
|
||||
value: "",
|
||||
@ -26,6 +38,27 @@
|
||||
automaticLayout: true,
|
||||
});
|
||||
|
||||
loadContents(stream_data);
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
isEdited = editor.getValue() !== stream_data;
|
||||
});
|
||||
|
||||
editor.addCommand(
|
||||
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS,
|
||||
(context, args) => {
|
||||
if (isEdited) {
|
||||
save(editor.getValue());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
editor.dispose();
|
||||
};
|
||||
@ -38,11 +71,36 @@
|
||||
function loadContents(stream_data: string | undefined) {
|
||||
if (!stream_data || !editor) return;
|
||||
|
||||
if (last_data === stream_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
isEdited = false;
|
||||
last_data = stream_data;
|
||||
editor.setValue(stream_data);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={editorContainer} style:height = {height - 1 + "px"}></div>
|
||||
<div class="relative">
|
||||
<div bind:this={editorContainer} style:height={height + "px"}></div>
|
||||
{#if isEdited}
|
||||
<button onclick={() => save(editor.getValue())} class="save-button">
|
||||
<UploadOutline size="xl" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.save-button {
|
||||
@apply p-2 bg-forge-sec text-forge-text border-t border-forge-bound cursor-pointer;
|
||||
position: absolute;
|
||||
right: 2em;
|
||||
bottom: 0px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.save-button:hover {
|
||||
background-color: #3c3d41;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
CaretDownOutline,
|
||||
PlusOutline,
|
||||
FilePdfSolid,
|
||||
ArrowsRepeatOutline,
|
||||
} from "flowbite-svelte-icons";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { homeDir } from "@tauri-apps/api/path";
|
||||
@ -21,12 +22,14 @@
|
||||
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);
|
||||
@ -167,7 +170,15 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="titlebar-button-group">
|
||||
<button
|
||||
onclick={reload}
|
||||
class="titlebar-button"
|
||||
id="titlebar-maximize"
|
||||
>
|
||||
<ArrowsRepeatOutline />
|
||||
</button>
|
||||
<button
|
||||
onclick={minimize}
|
||||
class="titlebar-button"
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
import type {TreeViewModel} from "../models/TreeViewModel";
|
||||
import TreeViewEntry from "./TreeViewEntry.svelte";
|
||||
|
||||
const rowHeight = 24;
|
||||
let {
|
||||
fState,
|
||||
height,
|
||||
@ -17,54 +16,36 @@
|
||||
const file_id = $derived(fState.file.id);
|
||||
|
||||
let scrollY = $state(0);
|
||||
let scrollContainer: HTMLElement;
|
||||
let fillerHeight: number = $state(0);
|
||||
let states: TreeViewState[] = [];
|
||||
|
||||
let treeState: TreeViewState | undefined = $state(undefined);
|
||||
let treeState: TreeViewState | undefined = $state(fState.treeState);
|
||||
|
||||
let totalHeight: number = $state(100);
|
||||
let entries: TreeViewModel[] = $state([]);
|
||||
let stickies: TreeViewModel[] = $state([]);
|
||||
let fillerHeight: number = $derived(
|
||||
treeState ? treeState.filler_height : 0,
|
||||
);
|
||||
let totalHeight: number = $derived(treeState ? treeState.total_height : 0);
|
||||
let entries: TreeViewModel[] = $derived(treeState ? treeState.entries : []);
|
||||
let stickies: TreeViewModel[] = $derived(
|
||||
treeState ? treeState.stickies : [],
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
treeState = states.find((state) => state.file_id === file_id);
|
||||
updateTreeView();
|
||||
});
|
||||
|
||||
|
||||
async function updateTreeView() {
|
||||
if (!treeState || treeState.file_id !== file_id) {
|
||||
let newState = new TreeViewState(file_id, fState);
|
||||
await newState.loadTreeView();
|
||||
states.push(newState);
|
||||
treeState = newState;
|
||||
totalHeight = treeState.getEntryCount() * rowHeight;
|
||||
treeState?.loadTreeView(scrollY, height);
|
||||
}
|
||||
scrollY = scrollContainer.scrollTop;
|
||||
let firstEntry = Math.floor(scrollY / rowHeight);
|
||||
let lastEntry = Math.ceil((scrollY + height) / rowHeight);
|
||||
let entryCount = treeState?.getEntryCount();
|
||||
totalHeight = Math.max(entryCount * rowHeight, 0);
|
||||
fillerHeight = firstEntry * rowHeight;
|
||||
let entriesAndStickies = treeState.getEntries(firstEntry, lastEntry);
|
||||
entries = entriesAndStickies[0];
|
||||
stickies = entriesAndStickies[1];
|
||||
}
|
||||
)
|
||||
|
||||
function handleSelect(prim: TreeViewModel) {
|
||||
if (prim.expanded && prim.container) {
|
||||
treeState
|
||||
?.collapseTree(prim.path.map((path) => path.key))
|
||||
.then(() => {
|
||||
updateTreeView();
|
||||
treeState?.loadTreeView(scrollY, height);
|
||||
});
|
||||
return;
|
||||
} else if (prim.container) {
|
||||
treeState
|
||||
?.expandTree(prim.path.map((path) => path.key))
|
||||
.then(() => {
|
||||
updateTreeView();
|
||||
treeState?.loadTreeView(scrollY, height);
|
||||
});
|
||||
}
|
||||
fState.selectPathHandler(
|
||||
@ -73,19 +54,22 @@
|
||||
prim.path.map((path) => path.key),
|
||||
),
|
||||
);
|
||||
updateTreeView();
|
||||
treeState?.loadTreeView(scrollY, height);
|
||||
}
|
||||
|
||||
function handleScroll(event: Event & { currentTarget: HTMLElement }) {
|
||||
updateTreeView();
|
||||
scrollY = event.currentTarget.scrollTop;
|
||||
treeState?.loadTreeView(scrollY, height);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div bind:this={scrollContainer} onscroll={handleScroll} class="overflow-auto" style="height: {height}px">
|
||||
<div onscroll={handleScroll} class="overflow-auto" style="height: {height}px">
|
||||
{#if entries}
|
||||
<div style="height: {totalHeight}px; width: 100%">
|
||||
<table class="border-b-2 border-forge-bound bg-forge-prim" style="position: relative; top: {scrollY}px">
|
||||
<table
|
||||
class="border-b-2 border-forge-bound bg-forge-prim"
|
||||
style="position: relative; top: {scrollY}px"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
{#each stickies as entry}
|
||||
@ -99,8 +83,9 @@
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
<div class="filler"
|
||||
style="height: {fillerHeight}px; width: 100%"
|
||||
<div
|
||||
class="filler"
|
||||
style="height: {fillerHeight}px; width: 100%"
|
||||
></div>
|
||||
<div>
|
||||
{#each entries as entry}
|
||||
|
||||
@ -1,3 +1,42 @@
|
||||
export default interface ContentModel {
|
||||
readonly parts: string[][];
|
||||
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;
|
||||
export default class ContentModel {
|
||||
parts: string[][];
|
||||
constructor(parts: string[][]) {
|
||||
this.parts = parts;
|
||||
}
|
||||
|
||||
public toDisplay(): string {
|
||||
|
||||
let text = "";
|
||||
if (this.parts.length > 1) {
|
||||
let i = 0;
|
||||
for (let part of this.parts) {
|
||||
text += CONTENTS.replace("%d", i.toString());
|
||||
for (let line of part) {
|
||||
text += " " + line + "\n";
|
||||
}
|
||||
text += EOF;
|
||||
i++;
|
||||
}
|
||||
} else if (this.parts.length == 1) {
|
||||
text = this.parts[0].join("\n");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
public 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 !== "");
|
||||
parts.push(lines);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
parts.push(display.split("\n").map(line => line.trim()));
|
||||
}
|
||||
return new ContentModel(parts);
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,8 @@ import {StreamData} from "./StreamData.svelte";
|
||||
import {ForgeNotification} from "./ForgeNotification.svelte";
|
||||
import {Mutex} from 'async-mutex';
|
||||
import {RawImageData} from "./RawImageData";
|
||||
import TreeViewState from "./TreeViewState.svelte";
|
||||
import {PageViewState} from "./PageViewState.svelte";
|
||||
|
||||
export default class FileViewState {
|
||||
public file: PdfFile;
|
||||
@ -24,6 +26,10 @@ export default class FileViewState {
|
||||
public selected_leaf_prim: Primitive | undefined = $state();
|
||||
public xref_entries: XRefEntry[] = $state([]);
|
||||
|
||||
public treeState: TreeViewState | undefined = $state();
|
||||
pageViewStates: Map<number, PageViewState> = new Map();
|
||||
public pageViewState: PageViewState | undefined = $state();
|
||||
|
||||
public notifications: ForgeNotification[] = $state([]);
|
||||
notificationMutex = new Mutex();
|
||||
|
||||
@ -33,6 +39,7 @@ export default class FileViewState {
|
||||
this.selectPath(this.path);
|
||||
this.loadXrefEntries()
|
||||
this.notificationMutex = new Mutex();
|
||||
this.treeState = new TreeViewState(this);
|
||||
}
|
||||
|
||||
getLastJump(): string | number | undefined {
|
||||
@ -43,6 +50,31 @@ export default class FileViewState {
|
||||
return this.container_prim?.getFirstJump()
|
||||
}
|
||||
|
||||
public saveStreamData(updated_data: string) {
|
||||
if (!this.selected_leaf_prim) return;
|
||||
if (!this.container_prim?.stream_data) return;
|
||||
invoke("update_stream_data_as_string", {
|
||||
id: this.file.id,
|
||||
path: this.formatPaths(this.path),
|
||||
newData: updated_data
|
||||
})
|
||||
.then(() => {
|
||||
console.log("saved");
|
||||
this.reload();
|
||||
this.container_prim?.stream_data?.setData(updated_data);
|
||||
})
|
||||
.catch(err => this.logError(err));
|
||||
}
|
||||
|
||||
public reload() {
|
||||
this.container_prim = undefined;
|
||||
this.selected_leaf_prim = undefined;
|
||||
this.loadXrefEntries();
|
||||
this.selectPath(this.path);
|
||||
this.treeState?.reload();
|
||||
this.reloadPageState();
|
||||
}
|
||||
|
||||
public logError(message: string) {
|
||||
this.notificationMutex.acquire().then(release => {
|
||||
try {
|
||||
@ -54,6 +86,10 @@ export default class FileViewState {
|
||||
});
|
||||
}
|
||||
|
||||
public setTreeViewState(state: TreeViewState) {
|
||||
this.treeState = state;
|
||||
}
|
||||
|
||||
public async deleteNotification(timestamp: number) {
|
||||
const release = await this.notificationMutex.acquire();
|
||||
try {
|
||||
@ -95,6 +131,7 @@ export default class FileViewState {
|
||||
if (newPrim.isContainer()) {
|
||||
this.container_prim = newPrim;
|
||||
this.path = newPath
|
||||
this.loadPageState().catch(err => this.logError(err));
|
||||
return;
|
||||
}
|
||||
this.selected_leaf_prim = newPrim;
|
||||
@ -174,4 +211,21 @@ export default class FileViewState {
|
||||
}
|
||||
|
||||
|
||||
private async reloadPageState() {
|
||||
this.pageViewState = undefined;
|
||||
this.pageViewStates = new Map();
|
||||
}
|
||||
|
||||
private async loadPageState() {
|
||||
if (!(this.container_prim && this.container_prim.isPage())) return;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,62 +1,55 @@
|
||||
import type ContentModel from "./ContentModel.svelte";
|
||||
import ContentModel from "./ContentModel.svelte";
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
import {RawImageData} from "./RawImageData";
|
||||
import type FileViewState from "./FileViewState.svelte";
|
||||
|
||||
export class PageViewState {
|
||||
file_id: string;
|
||||
page_num: number;
|
||||
errorHandler: (arg0: string) => void;
|
||||
fState: FileViewState;
|
||||
|
||||
img_data: RawImageData | undefined = $state();
|
||||
contents: string = $state("");
|
||||
contents: ContentModel = $state(new ContentModel([]));
|
||||
|
||||
constructor(file_id: string, page_num: number, errorHandler: (arg0: string) => void) {
|
||||
this.file_id = file_id;
|
||||
constructor(page_num: number, fState: FileViewState) {
|
||||
this.file_id = fState.file.id;
|
||||
this.page_num = page_num;
|
||||
this.errorHandler = errorHandler;
|
||||
this.load();
|
||||
this.fState = fState;
|
||||
|
||||
this.load(page_num);
|
||||
}
|
||||
|
||||
public async load() {
|
||||
invoke<ContentModel>("get_contents", {id: this.file_id, path: "Page" + this.page_num})
|
||||
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})
|
||||
.then((result) => {
|
||||
this.contents = this.mapToString(result);
|
||||
this.loadImage();
|
||||
this.page_num = page_num;
|
||||
this.contents = new ContentModel(result.parts);
|
||||
this.loadImage(page_num);
|
||||
})
|
||||
.catch((err) => this.errorHandler(err));
|
||||
.catch(this.logError);
|
||||
}
|
||||
|
||||
public async loadImage() {
|
||||
public async loadImage(page_num: number) {
|
||||
let result = await invoke<ArrayBuffer>("get_page_by_num", {
|
||||
id: this.file_id,
|
||||
num: this.page_num,
|
||||
}).catch(err => this.errorHandler(err));
|
||||
num: page_num,
|
||||
})
|
||||
.catch(this.logError);
|
||||
if (result) {
|
||||
this.img_data = new RawImageData(result);
|
||||
}
|
||||
}
|
||||
|
||||
private mapToString(model: ContentModel | undefined): string {
|
||||
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;
|
||||
public handleSave(newData: string) {
|
||||
invoke("update_contents", {id: this.file_id, path: "Page" + this.page_num, newData: newData})
|
||||
.then((_) => {
|
||||
this.loadImage(this.page_num);
|
||||
})
|
||||
.catch(this.logError);
|
||||
}
|
||||
|
||||
}
|
||||
@ -4,21 +4,29 @@ import {TreeViewModel} from "./TreeViewModel";
|
||||
import type {Trace} from "./Primitive.svelte";
|
||||
import type FileViewState from "./FileViewState.svelte";
|
||||
|
||||
const ROW_HEIGHT = 24;
|
||||
const MAX_STICKIES: number = 5;
|
||||
export default class TreeViewState {
|
||||
|
||||
private initialRequest: TreeViewRequest[];
|
||||
private activeRequest: Map<string, TreeViewRequest> = $state(new Map());
|
||||
private activeEntries: Map<string, TreeViewModel[]> = $state(new Map());
|
||||
scrollY: number = 0;
|
||||
private activeRequest: Map<string, TreeViewRequest> = new Map();
|
||||
private activeEntries: Map<string, TreeViewModel[]> = new Map();
|
||||
|
||||
public total_height: number = $state(100);
|
||||
public filler_height: number = $state(0);
|
||||
public entries: TreeViewModel[] = $state([]);
|
||||
public stickies: TreeViewModel[] = $state([]);
|
||||
|
||||
private scrollY = 0;
|
||||
private height = 100;
|
||||
file_id: string;
|
||||
|
||||
constructor(file_id: string, fState: FileViewState) {
|
||||
constructor(fState: FileViewState) {
|
||||
|
||||
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 = file_id;
|
||||
this.file_id = fState.file.id;
|
||||
}
|
||||
|
||||
public getEntryCount() {
|
||||
@ -106,9 +114,22 @@ export default class TreeViewState {
|
||||
);
|
||||
}
|
||||
|
||||
public async loadTreeView() {
|
||||
public async reload() {
|
||||
await this.loadTreeView(this.scrollY, this.height);
|
||||
}
|
||||
|
||||
public async loadTreeView(scrollY: number, height: number) {
|
||||
const activeRequests = Array.from(this.activeRequest.values());
|
||||
await this.updateTreeViewRequest(activeRequests);
|
||||
let firstEntry = Math.floor(scrollY / ROW_HEIGHT);
|
||||
let lastEntry = Math.ceil((scrollY + height) / ROW_HEIGHT);
|
||||
this.total_height = this.getEntryCount() * ROW_HEIGHT;
|
||||
this.filler_height = firstEntry * ROW_HEIGHT;
|
||||
let entriesAndStickies = this.getEntries(firstEntry, lastEntry);
|
||||
this.entries = entriesAndStickies[0];
|
||||
this.stickies = entriesAndStickies[1];
|
||||
this.scrollY = scrollY;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public getRoot(): TreeViewRequest[] {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user