Moved tab bar to dropdown in TitleBar.svelte
Made image rendering dynamic
This commit is contained in:
parent
d15aa2a4ed
commit
4b20d20783
1301
src-tauri/Cargo.lock
generated
1301
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@
|
||||
name = "pdf-forge"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
authors = ["Kilian Schuettler"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
@ -22,7 +22,7 @@ tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
pdf = { path = "../src-pdfrs/pdf", features = ["cache"] }
|
||||
pdf = { path = "/home/kschuettler/rust/pdf-forge/src-pdfrs/pdf", features = ["cache", "dump"], default-features=false}
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
uuid = { version = "1.12.0", features = ["v4"] }
|
||||
@ -31,3 +31,6 @@ lazy_static = "1.4.0"
|
||||
fax = "0.2"
|
||||
base64 = "0.21"
|
||||
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" }
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
mod render;
|
||||
mod retrieval;
|
||||
|
||||
#[cfg(test)]
|
||||
@ -6,24 +7,24 @@ mod tests;
|
||||
extern crate pdf;
|
||||
|
||||
use crate::pdf::object::Resolve;
|
||||
use crate::render::Renderer;
|
||||
use base64;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::ImageFormat;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use image::{ImageFormat, RgbImage};
|
||||
use lazy_static::lazy_static;
|
||||
use pdf::file::{File, FileOptions, NoLog, ObjectCache, StreamCache};
|
||||
use pdf::object::{ImageXObject, Object, PlainRef};
|
||||
use pdf::object::{Object, PlainRef};
|
||||
use pdf::primitive::{Dictionary, Primitive};
|
||||
use pdf::xref::XRef;
|
||||
use regex::Regex;
|
||||
use retrieval::{
|
||||
get_pdf_stream_by_path_with_file, get_prim_by_path_with_file, get_stream_data_by_path_with_file,
|
||||
};
|
||||
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::io::Cursor;
|
||||
use std::ops::DerefMut;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex, MutexGuard, RwLock};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tauri::{Manager, State};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -112,7 +113,11 @@ pub struct ContentsModel {
|
||||
|
||||
#[tauri::command]
|
||||
fn get_all_files(session: State<Session>) -> Vec<PdfFile> {
|
||||
session.get_all_files().iter().map(|s| s.pdf_file.clone()).collect()
|
||||
session
|
||||
.get_all_files()
|
||||
.iter()
|
||||
.map(|s| s.pdf_file.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -180,12 +185,7 @@ 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> {
|
||||
|
||||
fn get_contents(id: &str, path: &str, session: State<Session>) -> Result<ContentsModel, String> {
|
||||
let file = session.get_file(id)?;
|
||||
|
||||
let (_, page_prim, _) = get_prim_by_path_with_file(path, &file.cos_file)?;
|
||||
@ -211,22 +211,12 @@ fn get_stream_data_as_string(
|
||||
path: &str,
|
||||
session: State<Session>,
|
||||
) -> Result<String, String> {
|
||||
|
||||
let file = session.get_file(id)?;
|
||||
let data = get_stream_data_by_path_with_file(path, &file.cos_file)?;
|
||||
Ok(String::from_utf8_lossy(&data).into_owned())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_stream_data_as_image(
|
||||
id: &str,
|
||||
path: &str,
|
||||
session: State<Session>,
|
||||
) -> Result<String, String> {
|
||||
use base64::prelude::*;
|
||||
|
||||
let file = session.get_file(id)?;
|
||||
let img = retrieval::get_image_by_path(path, &file.cos_file)?;
|
||||
fn encode_b64(img: RgbImage) -> Result<String, String> {
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
match img.write_to(&mut writer, ImageFormat::Jpeg) {
|
||||
Ok(_) => Ok(format!(
|
||||
@ -238,6 +228,23 @@ fn get_stream_data_as_image(
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_stream_data_as_image(id: &str, path: &str, session: State<'_, Session>,) -> Result<String, String> {
|
||||
use base64::prelude::*;
|
||||
|
||||
let file = session.get_file(id)?;
|
||||
let img = retrieval::get_image_by_path(path, &file.cos_file)?;
|
||||
encode_b64(img.into_rgb8())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_page_by_num(id: &str, num: u32, session: State<'_, Session>) -> Result<String, String> {
|
||||
let file = session.get_file(id)?;
|
||||
let mut renderer = Renderer::new(&file.cos_file, 150);
|
||||
let img = renderer.render(num)?;
|
||||
encode_b64(img.into_rgb8())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_prim_by_path(
|
||||
id: &str,
|
||||
@ -285,7 +292,6 @@ fn get_prim_tree_by_path(
|
||||
paths: Vec<TreeViewRequest>,
|
||||
session: State<Session>,
|
||||
) -> Result<Vec<PrimitiveTreeView>, String> {
|
||||
|
||||
let file = session.get_file(id)?;
|
||||
|
||||
let results = paths
|
||||
@ -632,7 +638,6 @@ impl PrimitiveTreeView {
|
||||
}
|
||||
#[tauri::command]
|
||||
fn get_xref_table(id: &str, session: State<Session>) -> Result<XRefTableModel, String> {
|
||||
|
||||
let file = session.get_file(id)?;
|
||||
get_xref_table_model_with_file(&file.cos_file)
|
||||
}
|
||||
@ -701,7 +706,6 @@ struct SessionFile {
|
||||
cos_file: CosFile,
|
||||
}
|
||||
|
||||
|
||||
impl Session {
|
||||
fn load() -> Session {
|
||||
Session {
|
||||
@ -711,15 +715,27 @@ impl Session {
|
||||
|
||||
fn get_file(&self, id: &str) -> Result<Arc<SessionFile>, String> {
|
||||
let lock = self.files.read().unwrap();
|
||||
lock.get(id).cloned().ok_or(format!(" File {} not found!", id))
|
||||
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()
|
||||
self.files
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.map(|f| f.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_all_file_ids(&self) -> Vec<String> {
|
||||
self.files.read().unwrap().keys().map(|f| f.clone()).collect()
|
||||
self.files
|
||||
.read()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.map(|f| f.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn handle_upload(&self, pdf_file: PdfFile, cos_file: CosFile) -> Result<(), String> {
|
||||
@ -729,11 +745,11 @@ impl Session {
|
||||
Arc::new(SessionFile {
|
||||
pdf_file: pdf_file,
|
||||
cos_file: cos_file,
|
||||
},
|
||||
)) {
|
||||
}),
|
||||
) {
|
||||
Some(_) => Err("File could not be uploaded!".to_string()),
|
||||
None => Ok(()),
|
||||
}
|
||||
};
|
||||
};
|
||||
Err("Lock could not be acquired!".to_string())
|
||||
}
|
||||
@ -764,7 +780,8 @@ pub fn run() {
|
||||
get_xref_table,
|
||||
get_contents,
|
||||
get_stream_data_as_string,
|
||||
get_stream_data_as_image
|
||||
get_stream_data_as_image,
|
||||
get_page_by_num
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
53
src-tauri/src/render.rs
Normal file
53
src-tauri/src/render.rs
Normal file
@ -0,0 +1,53 @@
|
||||
extern crate pdf_render;
|
||||
|
||||
use crate::{t, CosFile};
|
||||
|
||||
use image::{DynamicImage, RgbaImage};
|
||||
use pathfinder_geometry::transform2d::Transform2F;
|
||||
use pathfinder_rasterize::Rasterizer;
|
||||
use pdf_render::{render_page, Cache, SceneBackend};
|
||||
|
||||
pub struct Renderer<'a> {
|
||||
file: &'a CosFile,
|
||||
cache: Cache,
|
||||
transform: Transform2F,
|
||||
}
|
||||
|
||||
impl<'a> Renderer<'a> {
|
||||
pub fn new(file: &'a CosFile, dpi: u32) -> Renderer<'a> {
|
||||
Renderer {
|
||||
file,
|
||||
cache: Cache::new(),
|
||||
transform: Transform2F::from_scale(dpi as f32 / 25.4),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self, page_num: u32) -> 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();
|
||||
let mut rasterizer = Rasterizer::new();
|
||||
Ok(DynamicImage::ImageRgba8(rasterizer.rasterize(scene, None)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(file: &CosFile, page_num: u32) -> Result<RgbaImage, String> {
|
||||
render_with_dpi(file, page_num, 300)
|
||||
}
|
||||
pub fn render_with_dpi(file: &CosFile, page_num: u32, dpi: u32) -> Result<RgbaImage, String> {
|
||||
let resolver = file.resolver();
|
||||
let page = t!(file.get_page(page_num));
|
||||
|
||||
let mut cache = Cache::new();
|
||||
let mut backend = SceneBackend::new(&mut cache);
|
||||
|
||||
t!(render_page(
|
||||
&mut backend,
|
||||
&resolver,
|
||||
&page,
|
||||
Transform2F::from_scale(dpi as f32 / 25.4)
|
||||
));
|
||||
Ok(Rasterizer::new().rasterize(backend.finish(), None))
|
||||
}
|
||||
@ -3,7 +3,7 @@ extern crate pdf;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::retrieval::{get_image_by_path, get_pdf_stream_by_path_with_file};
|
||||
use crate::retrieval::get_image_by_path;
|
||||
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,
|
||||
@ -11,14 +11,14 @@ mod tests {
|
||||
};
|
||||
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::imageops::FilterType;
|
||||
use image::ImageFormat;
|
||||
use image::{DynamicImage, ImageFormat, RgbaImage};
|
||||
use pdf::content::{display_ops, serialize_ops, Op};
|
||||
use pdf::file::FileOptions;
|
||||
use pdf::object::{Object, ObjectWrite, Page, PlainRef, Resolve};
|
||||
use pdf::primitive::Primitive;
|
||||
use std::io::Cursor;
|
||||
use std::time::Instant;
|
||||
|
||||
macro_rules! timed {
|
||||
($func_call:expr, $label:expr) => {{
|
||||
let start = std::time::Instant::now();
|
||||
@ -29,14 +29,15 @@ mod tests {
|
||||
}};
|
||||
}
|
||||
// Import items to be tested from the parent module
|
||||
const FILE_PATH: &str =
|
||||
const PDF_SPEC_PATH: &str =
|
||||
"/home/kschuettler/Dokumente/Scientific Papers/PDF Specification/ISO_32000-2_2020(en).pdf";
|
||||
const STUDY_402: &str = "/home/kschuettler/Dokumente/TestFiles/402Study.pdf";
|
||||
|
||||
#[test]
|
||||
fn test_read_x_ref() {
|
||||
let start = Instant::now();
|
||||
let file = timed!(
|
||||
FileOptions::cached().open(FILE_PATH).unwrap(),
|
||||
FileOptions::cached().open(PDF_SPEC_PATH).unwrap(),
|
||||
"Loading file"
|
||||
);
|
||||
let resolver = file.resolver();
|
||||
@ -62,7 +63,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_read_tree() {
|
||||
let file = timed!(
|
||||
FileOptions::cached().open(FILE_PATH).unwrap(),
|
||||
FileOptions::cached().open(PDF_SPEC_PATH).unwrap(),
|
||||
"Loading file"
|
||||
);
|
||||
let mut path = Vec::new();
|
||||
@ -116,9 +117,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_read_by_path() {
|
||||
let file = timed!(
|
||||
FileOptions::cached()
|
||||
.open("/home/kschuettler/Dokumente/TestFiles/402Study.pdf")
|
||||
.unwrap(),
|
||||
FileOptions::cached().open(STUDY_402).unwrap(),
|
||||
"Loading file"
|
||||
);
|
||||
let path = "/Page4/Resources/XObject/X13/Data";
|
||||
@ -145,7 +144,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_read_trailer() {
|
||||
let file = timed!(
|
||||
FileOptions::cached().open(FILE_PATH).unwrap(),
|
||||
FileOptions::cached().open(PDF_SPEC_PATH).unwrap(),
|
||||
"Loading file"
|
||||
);
|
||||
let mut file2 = timed!(FileOptions::uncached().storage(), "Loading storage");
|
||||
@ -166,18 +165,18 @@ mod tests {
|
||||
fn test_read_pdf_file() {
|
||||
use crate::to_pdf_file;
|
||||
let file = timed!(
|
||||
FileOptions::cached().open(FILE_PATH).unwrap(),
|
||||
FileOptions::cached().open(PDF_SPEC_PATH).unwrap(),
|
||||
"Loading file"
|
||||
);
|
||||
|
||||
let _pdf_file = timed!(to_pdf_file(FILE_PATH, &file), "pages 1");
|
||||
let pdf_file = timed!(to_pdf_file(FILE_PATH, &file), "pages 2");
|
||||
let _pdf_file = timed!(to_pdf_file(PDF_SPEC_PATH, &file), "pages 1");
|
||||
let pdf_file = timed!(to_pdf_file(PDF_SPEC_PATH, &file), "pages 2");
|
||||
println!("{:?}", pdf_file);
|
||||
}
|
||||
#[test]
|
||||
fn test_read_contents() {
|
||||
let file = timed!(
|
||||
FileOptions::cached().open(FILE_PATH).unwrap(),
|
||||
FileOptions::cached().open(PDF_SPEC_PATH).unwrap(),
|
||||
"Loading file"
|
||||
);
|
||||
|
||||
@ -200,7 +199,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_read_stream() {
|
||||
let file = timed!(
|
||||
FileOptions::cached().open(FILE_PATH).unwrap(),
|
||||
FileOptions::cached().open(PDF_SPEC_PATH).unwrap(),
|
||||
"Loading file"
|
||||
);
|
||||
let prim = timed!(
|
||||
@ -252,4 +251,29 @@ mod tests {
|
||||
100.0 - ((size as f32 / bytes as f32) * 100.0)
|
||||
);
|
||||
}
|
||||
|
||||
use crate::render::{render, render_with_dpi, Renderer};
|
||||
|
||||
#[test]
|
||||
fn test_render() {
|
||||
let file = timed!(
|
||||
FileOptions::cached().open(PDF_SPEC_PATH).unwrap(),
|
||||
"Loading file"
|
||||
);
|
||||
let mut renderer = Renderer::new(&file, 150);
|
||||
let mut rendered_pages: Vec<DynamicImage> = vec![];
|
||||
timed!(
|
||||
for i in 0..1 {
|
||||
let img = timed!(renderer.render(i), format!("render page {}", i));
|
||||
rendered_pages.push(img.unwrap())
|
||||
},
|
||||
"rendering some pages"
|
||||
);
|
||||
timed!(
|
||||
for (num, image) in rendered_pages.iter().enumerate() {
|
||||
image.to_rgb8().save(format!("page_{}.jpg", num)).unwrap()
|
||||
},
|
||||
"saving images"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,44 +1,34 @@
|
||||
<script lang="ts">
|
||||
import WelcomeScreen from "./WelcomeScreen.svelte";
|
||||
import {Pane, Splitpanes} from "svelte-splitpanes";
|
||||
import {open} from "@tauri-apps/plugin-dialog";
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
import { Pane, Splitpanes } from "svelte-splitpanes";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type PdfFile from "../models/PdfFile";
|
||||
import ToolbarLeft from "./ToolbarLeft.svelte";
|
||||
import FileView from "./FileView.svelte";
|
||||
import TabBar from "./TabBar.svelte";
|
||||
import FileViewState from "../models/FileViewState.svelte";
|
||||
import TitleBar from "./TitleBar.svelte";
|
||||
import ToolbarRight from "./ToolbarRight.svelte";
|
||||
import Footer from "./Footer.svelte";
|
||||
import NotificationModal from "./NotificationModal.svelte";
|
||||
|
||||
const footerHeight: number = 30;
|
||||
const titleBarHeight: number = 30;
|
||||
const tabBarHeight: number = 30;
|
||||
let files: PdfFile[] = $state([]);
|
||||
let innerHeight: number = $state(1060);
|
||||
let xrefTableShowing: boolean = $state(false);
|
||||
let treeShowing: boolean = $state(true);
|
||||
let notificationsShowing: boolean = $state(false);
|
||||
let pagesShowing: boolean = $state(false);
|
||||
|
||||
let fileViewHeight: number = $derived(
|
||||
Math.max(innerHeight - footerHeight - tabBarHeight - titleBarHeight, 0),
|
||||
Math.max(innerHeight - footerHeight - titleBarHeight, 0),
|
||||
);
|
||||
|
||||
let fStates: Map<string, FileViewState> = new Map<string, FileViewState>();
|
||||
let fStates: FileViewState[] = $state([]);
|
||||
let fState: FileViewState | undefined = $state();
|
||||
let selected_file = $derived(fState ? fState.file : undefined);
|
||||
|
||||
initialLoadAllFiles();
|
||||
|
||||
function initialLoadAllFiles() {
|
||||
invoke<PdfFile[]>("get_all_files")
|
||||
.then((result_list) => {
|
||||
files = result_list;
|
||||
createFileStates(files);
|
||||
if (files.length > 0) {
|
||||
selectFile(files[files.length - 1]);
|
||||
}
|
||||
createFileStates(result_list);
|
||||
fState = fStates[0];
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("File retrieval failed: " + error);
|
||||
@ -47,11 +37,12 @@
|
||||
|
||||
function createFileStates(files: PdfFile[]) {
|
||||
for (let file of files) {
|
||||
if (fStates.has(file.id)) {
|
||||
let fState = fStates.find((state) => state.file.id === file.id);
|
||||
if (fState) {
|
||||
continue;
|
||||
}
|
||||
let fState = new FileViewState(file);
|
||||
fStates.set(file.id, fState);
|
||||
fState = new FileViewState(file);
|
||||
fStates.push(fState);
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,72 +50,63 @@
|
||||
let file_path = await open({
|
||||
multiple: false,
|
||||
directory: false,
|
||||
filters: [{name: "pdf", extensions: ["pdf"]}],
|
||||
filters: [{ name: "pdf", extensions: ["pdf"] }],
|
||||
});
|
||||
if (file_path === null || Array.isArray(file_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
invoke<String>("upload", {path: file_path})
|
||||
.then((result) => {
|
||||
invoke<PdfFile[]>("get_all_files")
|
||||
.then((result_list) => {
|
||||
files = result_list;
|
||||
createFileStates(files);
|
||||
let file = files.find((file) => file.id === result);
|
||||
if (file) {
|
||||
selectFile(file);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Fetch all files failed with: " + error);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
let uploadedId = await invoke<string>("upload", {
|
||||
path: file_path,
|
||||
}).catch((error) => {
|
||||
console.error("File upload failed with: " + error);
|
||||
});
|
||||
if (!uploadedId) return;
|
||||
|
||||
let allFiles = await invoke<PdfFile[]>("get_all_files").catch(
|
||||
(error) => {
|
||||
console.error("Fetch all files failed with: " + error);
|
||||
},
|
||||
);
|
||||
if (!allFiles) return;
|
||||
|
||||
createFileStates(allFiles);
|
||||
fState = fStates.find((state) => state.file.id === uploadedId);
|
||||
}
|
||||
|
||||
function selectFile(file: PdfFile) {
|
||||
fState = fStates.get(file.id);
|
||||
}
|
||||
|
||||
function closeFile(file: PdfFile) {
|
||||
invoke("close_file", {id: file.id})
|
||||
function closeFile(stateToClose: FileViewState) {
|
||||
invoke("close_file", { id: stateToClose.file.id })
|
||||
.then((_) => {
|
||||
files = files.filter((f) => f.id != file.id);
|
||||
if (file === selected_file) {
|
||||
fState = undefined;
|
||||
fStates = fStates.filter(
|
||||
(state) => state.file.id !== stateToClose.file.id,
|
||||
);
|
||||
if (stateToClose.file.id === fState?.file.id) {
|
||||
fState = fStates[0];
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(err));
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight/>
|
||||
<svelte:window bind:innerHeight />
|
||||
<main style="height: {innerHeight}px; overflow: hidden;">
|
||||
<div style="height: {titleBarHeight}px">
|
||||
<TitleBar></TitleBar>
|
||||
</div>
|
||||
<div class="fileview_container" style="height: {fileViewHeight + 30}px">
|
||||
<Splitpanes theme="forge-movable" dblClickSplitter={false}>
|
||||
<Pane size={2.5} minSize={1.5} maxSize={4}>
|
||||
<ToolbarLeft {fState}
|
||||
></ToolbarLeft>
|
||||
</Pane>
|
||||
<Pane>
|
||||
<TabBar
|
||||
{files}
|
||||
{selected_file}
|
||||
<div style="height: {titleBarHeight}px; z-index: 100; position: relative;">
|
||||
<TitleBar
|
||||
{fStates}
|
||||
{fState}
|
||||
closeTab={closeFile}
|
||||
openTab={upload}
|
||||
selectTab={selectFile}
|
||||
></TabBar>
|
||||
selectTab={(state) => (fState = state)}
|
||||
></TitleBar>
|
||||
</div>
|
||||
<div class="fileview_container" style="height: {fileViewHeight}px">
|
||||
<Splitpanes theme="forge-movable" dblClickSplitter={false}>
|
||||
<Pane size={2.5} minSize={1.5} maxSize={4}>
|
||||
<ToolbarLeft {fState}></ToolbarLeft>
|
||||
</Pane>
|
||||
<Pane>
|
||||
{#if fState}
|
||||
<FileView
|
||||
{fState}
|
||||
height={fileViewHeight}
|
||||
></FileView>
|
||||
<FileView {fState} height={fileViewHeight}></FileView>
|
||||
{:else}
|
||||
<WelcomeScreen {upload}></WelcomeScreen>
|
||||
{/if}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
import type ContentModel from "../models/ContentModel.svelte";
|
||||
import {onMount} from "svelte";
|
||||
import * as monaco from "monaco-editor";
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
import StreamEditor from "./StreamEditor.svelte";
|
||||
|
||||
@ -55,7 +53,7 @@
|
||||
|
||||
</script>
|
||||
|
||||
<StreamEditor stream_data={contents} height={height}></StreamEditor>
|
||||
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
import PrimitiveView from "./PrimitiveView.svelte";
|
||||
import TreeView from "./TreeView.svelte";
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
import ContentsView from "./ContentsView.svelte";
|
||||
import PageView from "./PageView.svelte";
|
||||
import type {PathSelectedEvent} from "../events/PathSelectedEvent";
|
||||
import {onMount} from "svelte";
|
||||
import NotificationModal from "./NotificationModal.svelte";
|
||||
@ -70,9 +70,7 @@
|
||||
<PrimitiveView {fState} {height}></PrimitiveView>
|
||||
{/if}
|
||||
{#if fState.pageMode}
|
||||
<div class="overflow-hidden">
|
||||
<ContentsView {fState} {height}></ContentsView>
|
||||
</div>
|
||||
<PageView {fState} {height}></PageView>
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
46
src/components/PageView.svelte
Normal file
46
src/components/PageView.svelte
Normal file
@ -0,0 +1,46 @@
|
||||
<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";
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
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);
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
{#if fState.container_prim && fState.container_prim.isPage() && state}
|
||||
<Splitpanes theme="forge-movable">
|
||||
<Pane minSize={1}>
|
||||
<div class="overflow-hidden">
|
||||
<StreamEditor stream_data={state.contents} height={height - 1}></StreamEditor>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane minSize={1}>
|
||||
<RenderedPageView imgB64={state.img_data} {height}></RenderedPageView>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{:else }
|
||||
<h1>Please select a Page!</h1>
|
||||
{/if}
|
||||
<style lang="postcss">
|
||||
|
||||
</style>
|
||||
30
src/components/RenderedPageView.svelte
Normal file
30
src/components/RenderedPageView.svelte
Normal file
@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import ZoomableContainer from "./ZoomableContainer.svelte";
|
||||
import { RefreshOutline } from "flowbite-svelte-icons";
|
||||
|
||||
let { imgB64, height }: { imgB64: string | undefined; height: number } =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<div class="page-container" style:height={height + "px"}>
|
||||
{#if !imgB64}
|
||||
<div class="loading-container">
|
||||
<RefreshOutline class="animate-spin" size="xl" />
|
||||
</div>
|
||||
{:else}
|
||||
<ZoomableContainer {imgB64} {height} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.page-container {
|
||||
@apply w-full bg-forge-dark relative m-auto;
|
||||
}
|
||||
.image-container {
|
||||
@apply flex items-center justify-center h-full;
|
||||
width: fit-content;
|
||||
}
|
||||
.loading-container {
|
||||
@apply flex items-center justify-center h-full;
|
||||
}
|
||||
</style>
|
||||
@ -2,17 +2,16 @@
|
||||
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
import StreamEditor from "./StreamEditor.svelte";
|
||||
import type {StreamData} from "../models/StreamData";
|
||||
import type {StreamData} from "../models/StreamData.svelte";
|
||||
import RenderedPageView from "./RenderedPageView.svelte";
|
||||
|
||||
let {fState, editorHeight}: { fState: FileViewState, editorHeight: number } = $props()
|
||||
let streamData: StreamData = $derived(fState.container_prim?.stream_data ?);
|
||||
let streamData: StreamData = $derived(fState.container_prim?.stream_data?);
|
||||
|
||||
|
||||
</script>
|
||||
{#if streamData.type === "Image"}
|
||||
<div class="w-full m-auto">
|
||||
<img alt="x-object" src={streamData.data}/>
|
||||
</div>
|
||||
<RenderedPageView imgB64={streamData.data} height={editorHeight}></RenderedPageView>
|
||||
{:else}
|
||||
<StreamEditor
|
||||
stream_data={streamData.data}
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
let { stream_data, height }: { stream_data: string; height: number } =
|
||||
$props();
|
||||
|
||||
let contents: string | undefined = $state(undefined);
|
||||
let editorContainer: HTMLElement;
|
||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||
onMount(() => {
|
||||
@ -43,7 +42,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={editorContainer} style="height: {height}px; width: 100%;"></div>
|
||||
<div bind:this={editorContainer} style:height = {height - 1 + "px"}></div>
|
||||
|
||||
<style lang="postcss">
|
||||
</style>
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
<script lang="ts">
|
||||
import {PlusOutline, CloseOutline} from "flowbite-svelte-icons";
|
||||
import type PdfFile from "../models/PdfFile";
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
|
||||
let {files, selected_file, selectTab, closeTab, openTab}: { files: PdfFile[], selected_file: PdfFile | undefined, selectTab: any, closeTab: any, openTab: any } = $props();
|
||||
let {fStates, fState, selectTab, closeTab, openTab}:
|
||||
{ fStates: FileViewState[],
|
||||
fState: FileViewState | undefined,
|
||||
selectTab: (arg0: FileViewState) => void,
|
||||
closeTab: (arg0: FileViewState) => void,
|
||||
openTab: () => void } = $props();
|
||||
|
||||
function formatFileName(name: string) {
|
||||
if (name.length < 20) {
|
||||
@ -12,15 +17,15 @@
|
||||
}
|
||||
</script>
|
||||
<div class="tab-bar">
|
||||
{#each files as file}
|
||||
<div class={file.id === selected_file.id ? "tab-outer bg-forge-prim border-b-2 border-forge-acc" : "tab-outer bg-forge-dark"}>
|
||||
{#each fStates as state}
|
||||
<div class={state.file.id === fState?.file.id ? "tab-outer bg-forge-prim border-b-2 border-forge-acc" : "tab-outer bg-forge-dark"}>
|
||||
<div class="tab-text">
|
||||
<button onclick={() => selectTab(file)} class="tab-click">
|
||||
{formatFileName(file.name)}
|
||||
<button onclick={() => selectTab(state)} class="tab-click">
|
||||
{formatFileName(state.file.name)}
|
||||
</button>
|
||||
</div>
|
||||
<div class="justify-center flex">
|
||||
<button onclick={() => closeTab(file)} class="rounded-md hover:bg-forge-active">
|
||||
<button onclick={() => closeTab(state)} class="rounded-md hover:bg-forge-active">
|
||||
<CloseOutline size="xs" color="custom"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -1,31 +1,186 @@
|
||||
<script lang="ts">
|
||||
import {CloseOutline, MinusOutline, WindowRestoreSolid} from 'flowbite-svelte-icons'
|
||||
import {getCurrentWindow} from '@tauri-apps/api/window';
|
||||
import {
|
||||
CloseOutline,
|
||||
MinusOutline,
|
||||
WindowRestoreSolid,
|
||||
CaretDownOutline,
|
||||
PlusOutline,
|
||||
FilePdfSolid,
|
||||
} from "flowbite-svelte-icons";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { homeDir } from "@tauri-apps/api/path";
|
||||
import type FileViewState from "../models/FileViewState.svelte";
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
let home = "";
|
||||
getHome();
|
||||
|
||||
let {
|
||||
fStates,
|
||||
fState,
|
||||
selectTab,
|
||||
closeTab,
|
||||
openTab: newFile,
|
||||
}: {
|
||||
fStates: FileViewState[];
|
||||
fState: FileViewState | undefined;
|
||||
selectTab: (arg0: FileViewState) => void;
|
||||
closeTab: (arg0: FileViewState) => void;
|
||||
openTab: () => void;
|
||||
} = $props();
|
||||
|
||||
let dropdownVisible = $state(false);
|
||||
let dropdownEl: HTMLElement;
|
||||
|
||||
async function getHome() {
|
||||
home = await homeDir();
|
||||
}
|
||||
|
||||
function formatFileName(name: string) {
|
||||
if (name.length < 20) {
|
||||
return name;
|
||||
}
|
||||
return name.substring(0, 17) + "...";
|
||||
}
|
||||
|
||||
function formatFilePath(path: string) {
|
||||
// Replace home directory with ~
|
||||
|
||||
if (path.startsWith(home)) {
|
||||
path = path.replace(home, "~");
|
||||
}
|
||||
|
||||
if (path.length < 40) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const parts = path.split(/[/\\]/);
|
||||
const first = parts[0];
|
||||
const last = parts[parts.length - 1];
|
||||
const middle = parts
|
||||
.slice(1, -1)
|
||||
.map((part) => part.charAt(0) + "...")
|
||||
.join("/");
|
||||
|
||||
return `${first}/${middle}/${last}`;
|
||||
}
|
||||
|
||||
function minimize() {
|
||||
appWindow.minimize();
|
||||
}
|
||||
|
||||
function maximize() {
|
||||
appWindow.toggleMaximize()
|
||||
appWindow.toggleMaximize();
|
||||
}
|
||||
|
||||
function close() {
|
||||
appWindow.close()
|
||||
appWindow.close();
|
||||
}
|
||||
|
||||
function toggleDropdown() {
|
||||
dropdownVisible = !dropdownVisible;
|
||||
}
|
||||
|
||||
function handleFocusOut(e: FocusEvent) {
|
||||
const dropdown = dropdownEl;
|
||||
if (!dropdown) return;
|
||||
|
||||
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||
if (!dropdown.contains(relatedTarget)) {
|
||||
dropdownVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectTab(state: FileViewState) {
|
||||
selectTab(state);
|
||||
dropdownVisible = false;
|
||||
}
|
||||
|
||||
function handleNewFile() {
|
||||
newFile();
|
||||
dropdownVisible = false;
|
||||
}
|
||||
|
||||
function handleCloseTab(state: FileViewState) {
|
||||
closeTab(state);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div data-tauri-drag-region class="titlebar">
|
||||
<div class="m-[2px]">
|
||||
<img src="/pdf-forge-logo-30x30.png" class="logo forge" alt="PDF Forge Logo"/>
|
||||
<img
|
||||
src="/pdf-forge-logo-30x30.png"
|
||||
class="logo forge"
|
||||
alt="PDF Forge Logo"
|
||||
/>
|
||||
</div>
|
||||
<div class="titlebar-button-group">
|
||||
<button onclick={minimize} class="titlebar-button" id="titlebar-minimize">
|
||||
<MinusOutline/>
|
||||
|
||||
<div class="file-selector" onfocusout={handleFocusOut}>
|
||||
<button class="file-dropdown-button" onclick={toggleDropdown}>
|
||||
<span
|
||||
>{fState
|
||||
? formatFileName(fState.file.name)
|
||||
: "Select File"}</span
|
||||
>
|
||||
<CaretDownOutline class="ml-1" size="xs" />
|
||||
</button>
|
||||
<button onclick={maximize} class="titlebar-button" id="titlebar-maximize">
|
||||
<WindowRestoreSolid/>
|
||||
|
||||
{#if dropdownVisible}
|
||||
<div class="dropdown-menu" bind:this={dropdownEl}>
|
||||
<button class="dropdown-item new-tab" onclick={handleNewFile}>
|
||||
<PlusOutline size="sm" />
|
||||
<p class="ml-1">New File...</p>
|
||||
</button>
|
||||
<div class="divider"></div>
|
||||
<div class="w-full text-forge-text_sec text-xs ml-3 mt-1 mb-1">
|
||||
<p>Open Files</p>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
{#each fStates as state}
|
||||
<div
|
||||
class="dropdown-item justify-between"
|
||||
class:active={state.file.id === fState?.file.id}
|
||||
>
|
||||
<button
|
||||
class="select-button text-left"
|
||||
onclick={() => handleSelectTab(state)}
|
||||
>
|
||||
<FilePdfSolid size="md" />
|
||||
<div class="flex flex-col">
|
||||
<p class="ml-1">
|
||||
{formatFileName(state.file.name)}
|
||||
</p>
|
||||
<p class="path-text">
|
||||
{formatFilePath(state.file.path)}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="close-button"
|
||||
onclick={() => handleCloseTab(state)}
|
||||
>
|
||||
<CloseOutline size="xs" />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="titlebar-button-group">
|
||||
<button
|
||||
onclick={minimize}
|
||||
class="titlebar-button"
|
||||
id="titlebar-minimize"
|
||||
>
|
||||
<MinusOutline />
|
||||
</button>
|
||||
<button
|
||||
onclick={maximize}
|
||||
class="titlebar-button"
|
||||
id="titlebar-maximize"
|
||||
>
|
||||
<WindowRestoreSolid />
|
||||
</button>
|
||||
<button onclick={close} class="titlebar-button" id="titlebar-close">
|
||||
<CloseOutline></CloseOutline>
|
||||
@ -64,4 +219,56 @@
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.file-selector {
|
||||
@apply relative ml-10;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.file-dropdown-button {
|
||||
@apply flex flex-row items-center px-2 py-1 text-xs text-forge-text rounded hover:bg-forge-acc;
|
||||
height: 26px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@apply absolute top-full left-0 mt-1 bg-forge-prim border border-forge-sec rounded-md border;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
@apply flex flex-row items-center w-full text-xs text-forge-text hover:bg-forge-acc;
|
||||
}
|
||||
|
||||
.dropdown-item.active {
|
||||
@apply bg-forge-active hover:bg-forge-acc;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
@apply ml-2 p-1 rounded hover:bg-forge-active;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dropdown-item:hover .close-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.new-tab {
|
||||
@apply border-t border-forge-bound px-3 py-3;
|
||||
}
|
||||
|
||||
.select-button {
|
||||
@apply m-0 px-2 py-1 flex flex-row;
|
||||
}
|
||||
|
||||
.path-text {
|
||||
@apply text-forge-text_sec ml-2 mt-1;
|
||||
max-width: 100%;
|
||||
font-size: 10px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.divider {
|
||||
@apply h-[1px] bg-forge-sec w-full;
|
||||
}
|
||||
</style>
|
||||
@ -28,7 +28,6 @@
|
||||
let stickies: TreeViewModel[] = $state([]);
|
||||
|
||||
$effect(() => {
|
||||
console.log("Effect triggered")
|
||||
treeState = states.find((state) => state.file_id === file_id);
|
||||
updateTreeView();
|
||||
});
|
||||
@ -36,7 +35,6 @@
|
||||
|
||||
async function updateTreeView() {
|
||||
if (!treeState || treeState.file_id !== file_id) {
|
||||
console.log("New State")
|
||||
let newState = new TreeViewState(file_id, fState);
|
||||
await newState.loadTreeView();
|
||||
states.push(newState);
|
||||
|
||||
160
src/components/ZoomableContainer.svelte
Normal file
160
src/components/ZoomableContainer.svelte
Normal file
@ -0,0 +1,160 @@
|
||||
<script lang="ts">
|
||||
import ZoomControls from "../components/ZoomControls.svelte";
|
||||
|
||||
type ZoomableProps = {
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
zoomStep?: number;
|
||||
imgB64: string;
|
||||
height?: string | number;
|
||||
};
|
||||
|
||||
let {
|
||||
minZoom = 0.5,
|
||||
maxZoom = 4,
|
||||
zoomStep = 0.1,
|
||||
imgB64,
|
||||
height,
|
||||
}: ZoomableProps = $props();
|
||||
|
||||
let scale = $state(1);
|
||||
|
||||
// DOM refs
|
||||
let container = $state<HTMLElement>();
|
||||
let image = $state<HTMLElement>();
|
||||
|
||||
// Drag state
|
||||
type DragState = {
|
||||
startX: number;
|
||||
startY: number;
|
||||
scrollLeft: number;
|
||||
scrollTop: number;
|
||||
};
|
||||
|
||||
let dragState: DragState | undefined = $state({
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
scrollLeft: 0,
|
||||
scrollTop: 0,
|
||||
});
|
||||
|
||||
// Zoom handlers
|
||||
function handleZoom(event: WheelEvent) {
|
||||
if (!container || !image || !event.ctrlKey) return;
|
||||
|
||||
event.preventDefault();
|
||||
const delta = -Math.sign(event.deltaY);
|
||||
const newScale = Math.min(
|
||||
Math.max(scale + delta * zoomStep, minZoom),
|
||||
maxZoom,
|
||||
);
|
||||
|
||||
if (newScale !== scale) {
|
||||
const rect = image.getBoundingClientRect();
|
||||
const x = event.clientX;
|
||||
const y = event.clientY;
|
||||
|
||||
const scaleChange = newScale / scale;
|
||||
container.scrollLeft = x * scaleChange - x + container.scrollLeft;
|
||||
container.scrollTop = y * scaleChange - y + container.scrollTop;
|
||||
|
||||
scale = newScale;
|
||||
}
|
||||
}
|
||||
|
||||
// Drag handlers
|
||||
function handleDragStart(event: MouseEvent) {
|
||||
if (!container) return;
|
||||
|
||||
event.preventDefault();
|
||||
dragState = {
|
||||
startX: event.pageX - container.offsetLeft,
|
||||
startY: event.pageY - container.offsetTop,
|
||||
scrollLeft: container.scrollLeft,
|
||||
scrollTop: container.scrollTop,
|
||||
};
|
||||
}
|
||||
|
||||
function handleDragMove(event: MouseEvent) {
|
||||
if (!container || !dragState) return;
|
||||
|
||||
event.preventDefault();
|
||||
const x = event.pageX - container.offsetLeft;
|
||||
const y = event.pageY - container.offsetTop;
|
||||
|
||||
container.scrollLeft = dragState.scrollLeft - (x - dragState.startX);
|
||||
container.scrollTop = dragState.scrollTop - (y - dragState.startY);
|
||||
}
|
||||
|
||||
function handleDragEnd(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
dragState = undefined;
|
||||
}
|
||||
|
||||
function resetZoom() {
|
||||
scale = 1;
|
||||
if (container) {
|
||||
container.scrollLeft = 0;
|
||||
container.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Event listener management
|
||||
$effect(() => {
|
||||
if (container) {
|
||||
container.addEventListener("wheel", handleZoom, { passive: false });
|
||||
return () => container.removeEventListener("wheel", handleZoom);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative w-full h-full">
|
||||
<ZoomControls
|
||||
{scale}
|
||||
onZoomIn={() => (scale = Math.min(scale + zoomStep, maxZoom))}
|
||||
onZoomOut={() => (scale = Math.max(scale - zoomStep, minZoom))}
|
||||
{resetZoom}
|
||||
/>
|
||||
<div
|
||||
class="container"
|
||||
bind:this={container}
|
||||
onmousedown={handleDragStart}
|
||||
onmousemove={handleDragMove}
|
||||
onmouseup={handleDragEnd}
|
||||
onmouseleave={handleDragEnd}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="image-container"
|
||||
bind:this={image}
|
||||
style:transform="scale({scale})"
|
||||
style:height={height + "px"}
|
||||
>
|
||||
<img
|
||||
alt="rendered-page"
|
||||
src={imgB64}
|
||||
style:max-height={height + "px"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.container {
|
||||
@apply w-full h-full bg-forge-dark;
|
||||
overflow: auto;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.container:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
transform-origin: 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@ -1,20 +1,20 @@
|
||||
import type PdfFile from "./PdfFile";
|
||||
import type XRefEntry from "./XRefEntry";
|
||||
import Primitive from "./Primitive.svelte";
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type XRefTable from "./XRefTable";
|
||||
import type {PathSelectedEvent} from "../events/PathSelectedEvent";
|
||||
import type {PrimitiveModel} from "./PrimitiveModel";
|
||||
import {StreamData} from "./StreamData";
|
||||
import {ForgeNotification} from "./ForgeNotification.svelte";
|
||||
import {Mutex} from 'async-mutex';
|
||||
import type { PathSelectedEvent } from "../events/PathSelectedEvent";
|
||||
import type { PrimitiveModel } from "./PrimitiveModel";
|
||||
import { StreamData } from "./StreamData.svelte";
|
||||
import { ForgeNotification } from "./ForgeNotification.svelte";
|
||||
import { Mutex } from 'async-mutex';
|
||||
|
||||
export default class FileViewState {
|
||||
public file: PdfFile;
|
||||
|
||||
public treeMode: boolean = $state(true);
|
||||
public pageMode: boolean = $state(false);
|
||||
public xRefShowing: boolean = $state(true);
|
||||
public xRefShowing: boolean = $state(false);
|
||||
public notificationsShowing: boolean = $state(false);
|
||||
|
||||
|
||||
@ -31,6 +31,7 @@ export default class FileViewState {
|
||||
this.file = file;
|
||||
this.selectPath(this.path);
|
||||
this.loadXrefEntries()
|
||||
this.notificationMutex = new Mutex();
|
||||
}
|
||||
|
||||
getLastJump(): string | number | undefined {
|
||||
@ -41,14 +42,15 @@ export default class FileViewState {
|
||||
return this.container_prim?.getFirstJump()
|
||||
}
|
||||
|
||||
public async logError(message: string) {
|
||||
const release = await this.notificationMutex.acquire();
|
||||
public logError(message: string) {
|
||||
this.notificationMutex.acquire().then(release => {
|
||||
try {
|
||||
console.error(message)
|
||||
this.notifications.push(new ForgeNotification(Date.now(), "ERROR", message));
|
||||
} finally {
|
||||
release(); // Always release the lock
|
||||
release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteNotification(timestamp: number) {
|
||||
@ -70,7 +72,7 @@ export default class FileViewState {
|
||||
}
|
||||
|
||||
public loadXrefEntries() {
|
||||
invoke<XRefTable>("get_xref_table", {id: this.file.id})
|
||||
invoke<XRefTable>("get_xref_table", { id: this.file.id })
|
||||
.then(result => {
|
||||
this.xref_entries = result.entries;
|
||||
})
|
||||
@ -105,16 +107,17 @@ export default class FileViewState {
|
||||
leaf.isChildOf(container)
|
||||
) {
|
||||
const path = this.formatPaths(this.path) + "/Data";
|
||||
container.stream_data = new StreamData(leaf.sub_type)
|
||||
if (leaf.sub_type === "Image") {
|
||||
const data = await invoke<string>("get_stream_data_as_image", {id: this.file.id, path: path})
|
||||
const data = await invoke<string>("get_stream_data_as_image", { id: this.file.id, path: path })
|
||||
.catch(err => this.logError(err))
|
||||
if (!data) return;
|
||||
container.stream_data = new StreamData(leaf.sub_type, data);
|
||||
container.stream_data.setData(data);
|
||||
} else {
|
||||
const data = await invoke<string>("get_stream_data_as_string", {id: this.file.id, path: path})
|
||||
const data = await invoke<string>("get_stream_data_as_string", { id: this.file.id, path: path })
|
||||
.catch(err => this.logError(err))
|
||||
if (!data) return;
|
||||
container.stream_data = new StreamData(leaf.sub_type, data);
|
||||
container.stream_data.setData(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
src/models/PageViewState.svelte.ts
Normal file
61
src/models/PageViewState.svelte.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import type ContentModel from "./ContentModel.svelte";
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
|
||||
export class PageViewState {
|
||||
file_id: string;
|
||||
page_num: number;
|
||||
errorHandler: (arg0: string) => void;
|
||||
|
||||
img_data: string | undefined = $state();
|
||||
contents: string = $state("");
|
||||
|
||||
constructor(file_id: string, page_num: number, errorHandler: (arg0: string) => void) {
|
||||
this.file_id = file_id;
|
||||
this.page_num = page_num;
|
||||
this.errorHandler = errorHandler;
|
||||
this.load();
|
||||
}
|
||||
|
||||
public async load() {
|
||||
invoke<ContentModel>("get_contents", {id: this.file_id, path: "Page" + this.page_num})
|
||||
.then((result) => {
|
||||
this.contents = this.mapToString(result);
|
||||
this.loadImage();
|
||||
})
|
||||
.catch((err) => this.errorHandler(err));
|
||||
}
|
||||
|
||||
public async loadImage() {
|
||||
let result = await invoke<string>("get_page_by_num", {
|
||||
id: this.file_id,
|
||||
num: this.page_num,
|
||||
}).catch(err => this.errorHandler(err));
|
||||
if (result) {
|
||||
this.img_data = 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;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import {tracesAreEqual, tracesAreEqual2} from "../utils";
|
||||
import type {PrimitiveModel} from "./PrimitiveModel";
|
||||
import type {StreamData} from "./StreamData";
|
||||
import type {StreamData} from "./StreamData.svelte";
|
||||
|
||||
export default class Primitive {
|
||||
// input from api
|
||||
|
||||
12
src/models/StreamData.svelte.ts
Normal file
12
src/models/StreamData.svelte.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export class StreamData {
|
||||
// Image, Text
|
||||
public type: string;
|
||||
public data: string = $state("");
|
||||
constructor(type: string) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public setData(data: string) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
export class StreamData {
|
||||
// Image, Text
|
||||
public type: string;
|
||||
public data: string;
|
||||
constructor(type: string, value: string) {
|
||||
this.type = type;
|
||||
this.data = value;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user