mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 19:19:10 -06:00
This commit is contained in:
+9
-9
@@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "50ec1133889f3a3948a3901a9fa40b3499f3c549b605e55848138483946d74da"
|
||||
sha256 = "8444a3bd135d61feb6c5b559a9eb0babb50de2ebbd1d86b1e87ccb064e2d3f0b"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
|
||||
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "3746a7d87fb98c9f3aeb41c63ce50371c42dfebca64b67ccbb599abb36a93ba8"
|
||||
sha256 = "8771d80405b8fba9da4a909fdd97b760f6b25a822807ca83ff20dc5cec83a73c"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/cs.tsv"
|
||||
@@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "2c09a121e493fb123d9f886529f62268119ca580ad5d5300eca61d4a39216827"
|
||||
sha256 = "bf1ab8079af725264f9c4298b9737359ba831170dcba76a6bd39228239c87e83"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/pt-br.tsv"
|
||||
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "0ccf62e34424981d5980eeaee8799db2653a4c186807df143326ef9881959006"
|
||||
sha256 = "b23b03cf4ac163e7a63e49a0900a125507c05ad4fc7930468b859acf32b09ecd"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/es.tsv"
|
||||
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "0b579afad6e52b98cef5e61e07e238d9cf76cc7f295395f7e1c855400e5fbaf9"
|
||||
sha256 = "1b3371f239ca89a658e2c9b80cc070be5cd941dd1ecd3e7a247696716f25721d"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/fr.tsv"
|
||||
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "956753e05b5a9e1d4c4a63ee57667cc9fa5bb95288c3d46c6b15435b83d6ed8d"
|
||||
sha256 = "dbf725ffc9177a435e894f4405834e87c70b6fe31a34f1be533c60c356510b97"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/te.tsv"
|
||||
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "e8b96edd718a1cd212b7440c4ab100f2555c97dccb0cda289bdb1775d5f3aa61"
|
||||
sha256 = "f7a5ab0bcbdb8641b1bd4ae25db54ef187757dde3e4fe372e433a9e5c0fc4f3a"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ru.tsv"
|
||||
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "136a9ae206c4ee5aad29d248613c568799080b4b3ee9cd197233072ee9a2983a"
|
||||
sha256 = "259f795a3cdc032aa670693d2ebff010c5dd3bcf57dda11548d5bd46e78969b8"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ja.tsv"
|
||||
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "d2e7114c70da3c52200cf68e9023dc32ff462439795784e49c2ae771531240e2"
|
||||
sha256 = "cdcdba828efea9d402cb9ce8be2b52ab1d58c8f7837d75d3b9c86f2a05e7fb2e"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/accessibility.svg"
|
||||
|
||||
@@ -224,6 +224,9 @@ fn main() -> eframe::Result {
|
||||
app.notify(e);
|
||||
}
|
||||
} else {
|
||||
// With the preference on, last session's files come back first; files named on
|
||||
// the command line open after them, in front (#442).
|
||||
app.reopen_last_files(&files);
|
||||
for f in files {
|
||||
app.open_path(&f);
|
||||
}
|
||||
|
||||
@@ -464,6 +464,14 @@ impl PdfCraftApp {
|
||||
}
|
||||
},
|
||||
};
|
||||
// Quitting closes the unsaved tabs one by one (and brings each forward to save it):
|
||||
// remember what was open, and which tab was active, before the first one goes (#442).
|
||||
if req == CloseRequest::Quit {
|
||||
match choice {
|
||||
Some(_) => self.note_quit_session(),
|
||||
None => self.forget_quit_session(),
|
||||
}
|
||||
}
|
||||
match choice {
|
||||
None => {} // cancelled: nothing closes
|
||||
Some(false) => self.close_and_continue(ctx, index, req),
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
Fit to width scrolling Plynulé posouvání s přizpůsobením šířce
|
||||
Fit one full page Celá jedna stránka
|
||||
Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Používá se, když PDF nepožaduje rozložení ani zvětšení. Plynulé posouvání mezi stránkami neskáče.
|
||||
Reopen the files that were open when PdfCraft last closed Znovu otevřít soubory, které byly otevřené při posledním zavření aplikace PdfCraft
|
||||
Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Vyberte obrázek PNG nebo JPEG (nejvýše 4 MiB). Průhlednost bude zachována.
|
||||
Couldn't import the signature image: {e} Obrázek podpisu nelze importovat: {e}
|
||||
Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. Přenosný režim je vypnutý: do složky {folder} nelze zapisovat ({error}). Nastavení se místo toho ukládá do vaší uživatelské složky.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 81.
|
@@ -1986,6 +1986,7 @@
|
||||
Default page display Presentación de página predeterminada
|
||||
Default zoom Zoom predeterminado
|
||||
Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Se usa cuando un PDF no pide una presentación ni un zoom. El desplazamiento continuo nunca salta de página en página.
|
||||
Reopen the files that were open when PdfCraft last closed Volver a abrir los archivos que estaban abiertos al cerrar PdfCraft por última vez
|
||||
Created a PDF from {n} files Se creó un PDF a partir de {n} archivos
|
||||
PDF, images and text PDF, imágenes y texto
|
||||
Insert a file before page {label} Insertar un archivo antes de la página {label}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 60.
|
@@ -1909,6 +1909,7 @@
|
||||
Default page display Affichage des pages par défaut
|
||||
Default zoom Zoom par défaut
|
||||
Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Utilisé lorsqu'un PDF ne demande ni présentation ni zoom. Le défilement continu ne saute jamais d'une page à l'autre.
|
||||
Reopen the files that were open when PdfCraft last closed Rouvrir les fichiers qui étaient ouverts lors de la dernière fermeture de PdfCraft
|
||||
Show Afficher
|
||||
Sort Trier
|
||||
Reverse the order Inverser l'ordre
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 39.
|
@@ -1975,3 +1975,4 @@
|
||||
Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. PNG または JPEG 画像を選択してください(最大 4 MiB)。透明度は保持されます。
|
||||
Couldn't import the signature image: {e} 署名画像を読み込めませんでした: {e}
|
||||
Go to page… ページへ移動…
|
||||
Reopen the files that were open when PdfCraft last closed 前回 PdfCraft を閉じたときに開いていたファイルを再度開く
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -66,3 +66,4 @@
|
||||
Couldn't import the signature image: {e} Não foi possível importar a imagem da assinatura: {e}
|
||||
Portable mode is off: {folder} can't be written ({error}). Settings are kept in your user folder instead. O modo portátil está desativado: não é possível gravar em {folder} ({error}). As configurações ficam na sua pasta de usuário.
|
||||
Go to page… Ir para a página…
|
||||
Reopen the files that were open when PdfCraft last closed Reabrir os arquivos que estavam abertos quando o PdfCraft foi fechado pela última vez
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -1972,6 +1972,7 @@
|
||||
Default page display Отображение страниц по умолчанию
|
||||
Default zoom Масштаб по умолчанию
|
||||
Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. Используется, когда PDF не запрашивает макет или масштаб. Непрерывная прокрутка никогда не привязывается между страницами.
|
||||
Reopen the files that were open when PdfCraft last closed Повторно открывать файлы, которые были открыты при последнем закрытии PdfCraft
|
||||
Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. Выберите изображение PNG или JPEG (до 4 МиБ). Прозрачность сохраняется.
|
||||
Couldn't import the signature image: {e} Не удалось импортировать изображение подписи: {e}
|
||||
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -1911,6 +1911,7 @@ action wizard Start ప్రారంభించు
|
||||
Default page display డిఫాల్ట్ పేజీ ప్రదర్శన
|
||||
Default zoom డిఫాల్ట్ జూమ్
|
||||
Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. PDF ఏ లేఅవుట్ను గానీ జూమ్ను గానీ కోరనప్పుడు ఉపయోగించబడుతుంది. నిరంతర స్క్రోలింగ్ పేజీల మధ్య ఎప్పుడూ స్నాప్ అవ్వదు.
|
||||
Reopen the files that were open when PdfCraft last closed PdfCraftను చివరిసారి మూసివేసినప్పుడు తెరిచి ఉన్న ఫైల్లను మళ్లీ తెరువు
|
||||
About పరిచయం
|
||||
Contributors సహకారులు
|
||||
Models మోడల్లు
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 13 and column 16.
|
@@ -2005,6 +2005,7 @@
|
||||
Text recognition isn't installed: its models are missing (run `cargo xtask models`, or set PDFCRAFT_MODELS). 尚未安装文本识别功能:缺少模型(请运行 `cargo xtask models` 或设置 PDFCRAFT_MODELS)。
|
||||
Used when opening PDFs. An explicit launch or control mode takes precedence for the session. 用于打开 PDF 时。如果启动参数或控制通道明确指定了模式,则本次会话优先使用该模式。
|
||||
Used when a PDF doesn't ask for a layout or zoom. Continuous scrolling never snaps between pages. PDF 未指定布局或缩放时使用。连续滚动时页面不会逐页吸附。
|
||||
Reopen the files that were open when PdfCraft last closed 重新打开上次关闭 PdfCraft 时打开的文件
|
||||
Write this email? 是否撰写此邮件?
|
||||
{who} in this document wants to open this address in your browser: 此文档中的{who}请求在浏览器中打开以下地址:
|
||||
{who} in this document wants to open your email app with this address: 此文档中的{who}请求打开邮件应用并使用以下地址:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -1968,3 +1968,4 @@
|
||||
Choose a PNG or JPEG image (up to 4 MiB). Transparency is preserved. 選擇 PNG 或 JPEG 圖像(最大 4 MiB)。透明度會保留。
|
||||
Couldn't import the signature image: {e} 無法匯入簽名圖像:{e}
|
||||
Go to page… 轉到頁面…
|
||||
Reopen the files that were open when PdfCraft last closed 重新開啟上次關閉 PdfCraft 時開啟的檔案
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -295,6 +295,9 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
// The web build has no file paths to reopen.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
ui.checkbox(&mut app.reopen_last_session, tl!("Reopen the files that were open when PdfCraft last closed"));
|
||||
ui.add_space(8.0);
|
||||
// Identity: the author of new comments (Acrobat: Preferences ▸ Identity).
|
||||
ui.label(egui::RichText::new(tl!("Identity")).font(theme::semibold(13.0)));
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
//! Reopen the files from the last session (Preferences ▸ Documents and view, #442): which files
|
||||
//! were open when PdfCraft last closed, the page each showed and the active tab. Local only, like
|
||||
//! the recent-files list, and kept only while the preference is on.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::PdfCraftApp;
|
||||
|
||||
/// More files than anyone keeps open. Settings are untrusted, so a longer list is cut here.
|
||||
pub const MAX_FILES: usize = 64;
|
||||
|
||||
/// The files open when PdfCraft last closed.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct LastSession {
|
||||
pub files: Vec<SessionFile>,
|
||||
/// The path of the tab that was active.
|
||||
pub active: Option<String>,
|
||||
}
|
||||
|
||||
/// One open file and where the reader was in it.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct SessionFile {
|
||||
/// Absolute, so a file opened from the command line by a relative path is found again.
|
||||
pub path: String,
|
||||
/// The page the tab showed (0-based).
|
||||
pub page: usize,
|
||||
}
|
||||
|
||||
impl LastSession {
|
||||
/// Read what [`PdfCraftApp::persist`] wrote. Malformed entries are skipped and the list is
|
||||
/// capped at [`MAX_FILES`].
|
||||
pub(crate) fn from_json(v: &serde_json::Value) -> Self {
|
||||
let files = v["files"]
|
||||
.as_array()
|
||||
.map(|files| {
|
||||
files
|
||||
.iter()
|
||||
.filter_map(|f| {
|
||||
let path = f["path"].as_str().filter(|p| !p.is_empty())?.to_string();
|
||||
// `go_to_page` clamps a page past the end.
|
||||
let page = f["page"].as_u64().and_then(|p| usize::try_from(p).ok()).unwrap_or(0);
|
||||
Some(SessionFile { path, page })
|
||||
})
|
||||
.take(MAX_FILES)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self { files, active: v["active"].as_str().map(str::to_string) }
|
||||
}
|
||||
}
|
||||
|
||||
/// `path` made absolute against the current directory, without resolving links (and without
|
||||
/// Windows' `\\?\` prefix, which `canonicalize` adds).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn absolute(path: &str) -> String {
|
||||
std::path::absolute(path).map_or_else(|_| path.to_string(), |p| p.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
impl PdfCraftApp {
|
||||
/// The files open now. Documents that were never saved to a file are left out (crash
|
||||
/// recovery keeps their unsaved work), and so is everything on the web, which has no paths.
|
||||
pub(crate) fn open_session(&self) -> LastSession {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let path = |v: &crate::DocView| self.session.get(v.id).and_then(|d| d.path.as_deref()).map(absolute);
|
||||
let files = self.views.iter().filter_map(|v| Some(SessionFile { path: path(v)?, page: v.current })).take(MAX_FILES).collect();
|
||||
let active = self.active.and_then(|i| self.views.get(i)).and_then(path);
|
||||
LastSession { files, active }
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
LastSession::default()
|
||||
}
|
||||
|
||||
/// What to save as the last session: the files open when the quit began, once the app is
|
||||
/// really quitting (unsaved tabs close one by one before that), else the files open now.
|
||||
pub(crate) fn session_to_save(&self) -> LastSession {
|
||||
match &self.quit_session {
|
||||
Some(s) if self.allow_quit => s.clone(),
|
||||
_ => self.open_session(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A quit is about to close its first tab: remember everything that is still open.
|
||||
pub(crate) fn note_quit_session(&mut self) {
|
||||
if self.quit_session.is_none() {
|
||||
self.quit_session = Some(self.open_session());
|
||||
}
|
||||
}
|
||||
|
||||
/// The quit was cancelled: what is open from now on is the session again.
|
||||
pub(crate) fn forget_quit_session(&mut self) {
|
||||
self.quit_session = None;
|
||||
}
|
||||
|
||||
/// At startup, with the preference on: reopen the last session's files at the pages they
|
||||
/// showed and bring its active tab forward. Files that are gone are skipped, and so are
|
||||
/// `skip` (files the app is about to open anyway, e.g. from the command line) and documents
|
||||
/// the Recovery dialog offers, so neither opens twice.
|
||||
///
|
||||
/// Only one password prompt can wait at a time: the first encrypted file asks for its
|
||||
/// password and later ones stay closed (they are in the recent-files list).
|
||||
pub fn reopen_last_files(&mut self, skip: &[String]) {
|
||||
let last = std::mem::take(&mut self.last_session);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.reopen_last_session {
|
||||
let skip: Vec<String> =
|
||||
skip.iter().map(|p| absolute(p)).chain(self.recoverable.iter().filter_map(|m| m.path.as_deref().map(absolute))).collect();
|
||||
let mut active = None;
|
||||
let mut prompt = self.password_prompt.take();
|
||||
for f in &last.files {
|
||||
let open = |app: &Self| {
|
||||
app.views.iter().any(|v| app.session.get(v.id).and_then(|d| d.path.as_deref()).map(absolute).as_deref() == Some(f.path.as_str()))
|
||||
};
|
||||
if skip.contains(&f.path) || !std::path::Path::new(&f.path).is_file() || open(self) {
|
||||
continue;
|
||||
}
|
||||
let before = self.views.len();
|
||||
self.open_path(&f.path);
|
||||
if let Some(p) = self.password_prompt.take() {
|
||||
prompt.get_or_insert(p);
|
||||
}
|
||||
if self.views.len() > before
|
||||
&& let Some(view) = self.views.last_mut()
|
||||
{
|
||||
view.go_to_page(f.page);
|
||||
if last.active.as_ref() == Some(&f.path) {
|
||||
active = Some(self.views.len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.password_prompt = prompt;
|
||||
if active.is_some() {
|
||||
self.active = active;
|
||||
}
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = (last, skip);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn malformed_sessions_are_skipped_and_capped() {
|
||||
let v = serde_json::json!({
|
||||
"files": [{"path": 5}, {"path": ""}, "x", {"path": "/a.pdf", "page": -3}, {"path": "/b.pdf", "page": 1e300}, {"path": "/c.pdf", "page": 7}],
|
||||
"active": 3,
|
||||
});
|
||||
let s = LastSession::from_json(&v);
|
||||
let pages: Vec<(&str, usize)> = s.files.iter().map(|f| (f.path.as_str(), f.page)).collect();
|
||||
assert_eq!(pages, [("/a.pdf", 0), ("/b.pdf", 0), ("/c.pdf", 7)]);
|
||||
assert_eq!(s.active, None);
|
||||
let many: Vec<serde_json::Value> = (0..1000).map(|i| serde_json::json!({"path": format!("/{i}.pdf")})).collect();
|
||||
assert_eq!(LastSession::from_json(&serde_json::json!({"files": many})).files.len(), MAX_FILES);
|
||||
assert_eq!(LastSession::from_json(&serde_json::json!("garbage")), LastSession::default());
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ pub mod forms_ui;
|
||||
mod home;
|
||||
mod icon_data;
|
||||
pub mod icons;
|
||||
pub mod last_session;
|
||||
mod pageboxes;
|
||||
mod palette;
|
||||
mod panels;
|
||||
@@ -377,6 +378,13 @@ pub struct PdfCraftApp {
|
||||
pub palette_query: String,
|
||||
pub all_tools_expanded: bool,
|
||||
pub recent: Vec<RecentFile>,
|
||||
/// Preferences: reopen the files that were open when PdfCraft last closed (#442).
|
||||
pub reopen_last_session: bool,
|
||||
/// The files open when PdfCraft last closed, read from the settings for
|
||||
/// [`PdfCraftApp::reopen_last_files`].
|
||||
pub last_session: last_session::LastSession,
|
||||
/// Quitting closes unsaved tabs one by one: what was open when the quit began.
|
||||
quit_session: Option<last_session::LastSession>,
|
||||
/// Folders pinned to Home, and what they held when last listed.
|
||||
pub pinned: folders_ui::PinnedFolders,
|
||||
pub toast: Option<(String, f64)>,
|
||||
@@ -619,6 +627,9 @@ impl PdfCraftApp {
|
||||
palette_query: String::new(),
|
||||
all_tools_expanded: false,
|
||||
recent: Vec::new(),
|
||||
reopen_last_session: false,
|
||||
last_session: Default::default(),
|
||||
quit_session: None,
|
||||
pinned: Default::default(),
|
||||
toast: None,
|
||||
integrated_titlebar: false,
|
||||
@@ -1186,6 +1197,9 @@ impl PdfCraftApp {
|
||||
let trusted: Vec<String> = self.session.trusted_certificates().iter().map(pdfcraft_engine::sign::x509::to_pem).collect();
|
||||
serde_json::json!({
|
||||
"recent": self.recent,
|
||||
"reopen_last_session": self.reopen_last_session,
|
||||
// Kept only while the preference is on.
|
||||
"last_session": self.reopen_last_session.then(|| self.session_to_save()),
|
||||
"pinned_folders": self.pinned.folders,
|
||||
"theme": self.theme_preference,
|
||||
"default_mode": self.default_mode,
|
||||
@@ -1221,6 +1235,10 @@ impl PdfCraftApp {
|
||||
let r: Vec<RecentFile> = r.into_iter().filter(|f| std::path::Path::new(&f.path).exists()).collect();
|
||||
self.recent = r;
|
||||
}
|
||||
if let Some(on) = v["reopen_last_session"].as_bool() {
|
||||
self.reopen_last_session = on;
|
||||
}
|
||||
self.last_session = last_session::LastSession::from_json(&v["last_session"]);
|
||||
self.pinned.restore(&v["pinned_folders"]);
|
||||
if let Ok(preference) = serde_json::from_value::<ThemePreference>(v["theme"].clone()) {
|
||||
self.set_theme_preference(preference);
|
||||
|
||||
@@ -388,6 +388,119 @@ fn quitting_with_unsaved_changes_asks_for_each_document() {
|
||||
assert!(h.state().close_request.is_none());
|
||||
}
|
||||
|
||||
/// PDFs written to a fresh temp folder, removed when the test ends.
|
||||
struct TempPdfs(std::path::PathBuf);
|
||||
|
||||
impl TempPdfs {
|
||||
fn new(test: &str) -> Self {
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-session-{test}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
Self(dir)
|
||||
}
|
||||
|
||||
fn file(&self, name: &str, pages: usize) -> String {
|
||||
let path = self.0.join(name);
|
||||
std::fs::write(&path, fixture(pages)).unwrap();
|
||||
path.to_string_lossy().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempPdfs {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// The app with Preferences ▸ Reopen the files that were open when PdfCraft last closed on.
|
||||
fn session_harness(setup: impl FnOnce(&mut PdfCraftApp) + 'static) -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.reopen_last_session = true;
|
||||
setup(&mut app);
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
fn open_paths(app: &PdfCraftApp) -> Vec<String> {
|
||||
app.views.iter().map(|v| app.session.get(v.id).and_then(|d| d.path.clone()).unwrap_or_default()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_last_session_reopens_its_files_pages_and_active_tab() {
|
||||
let dir = TempPdfs::new("reopen");
|
||||
let files = [dir.file("a.pdf", 1), dir.file("b.pdf", 5), dir.file("c.pdf", 2)];
|
||||
let opened = files.clone();
|
||||
let mut h = session_harness(move |app| opened.iter().for_each(|f| app.open_path(f)));
|
||||
h.state_mut().views[1].go_to_page(3);
|
||||
h.state_mut().active = Some(1);
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().views[1].current, 3, "b.pdf shows page 4");
|
||||
let settings = h.state().persist();
|
||||
let mut h = session_harness(move |app| {
|
||||
app.restore(&settings);
|
||||
app.reopen_last_files(&[]);
|
||||
});
|
||||
h.run_steps(4);
|
||||
let app = h.state();
|
||||
assert_eq!(open_paths(app), files, "the same files, in the same order");
|
||||
assert_eq!(app.active, Some(1), "b.pdf is in front again");
|
||||
assert_eq!(app.views[1].current, 3, "at the page it showed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quitting_with_unsaved_changes_still_remembers_every_file() {
|
||||
let dir = TempPdfs::new("quit");
|
||||
let files = [dir.file("a.pdf", 1), dir.file("b.pdf", 1)];
|
||||
let opened = files.clone();
|
||||
let mut h = session_harness(move |app| opened.iter().for_each(|f| app.open_path(f)));
|
||||
for tab in 0..2 {
|
||||
h.state_mut().active = Some(tab);
|
||||
h.state_mut().views[tab].select_pages(&[0]);
|
||||
h.state_mut().apply_edit(pdfcraft_engine::Edit::RotatePages { pages: vec![0], degrees: 90 });
|
||||
}
|
||||
h.state_mut().active = Some(1);
|
||||
h.state_mut().close_request = Some(CloseRequest::Quit);
|
||||
h.run_steps(3);
|
||||
// Each unsaved tab closes as it is answered, before PdfCraft quits.
|
||||
for _ in 0..2 {
|
||||
h.get_by_label("Don't save").click();
|
||||
h.run_steps(3);
|
||||
}
|
||||
assert!(h.state().views.is_empty());
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.restore(&h.state().persist());
|
||||
app.reopen_last_files(&[]);
|
||||
assert_eq!(open_paths(&app), files, "both files come back");
|
||||
assert_eq!(app.active, Some(1), "with b.pdf in front, as when the quit began");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_last_session_is_off_by_default_and_skips_missing_and_duplicate_files() {
|
||||
let dir = TempPdfs::new("skip");
|
||||
let (a, b) = (dir.file("a.pdf", 1), dir.file("b.pdf", 1));
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.open_path(&a);
|
||||
app.open_path(&b);
|
||||
let off: serde_json::Value = serde_json::from_str(&app.persist()).unwrap();
|
||||
assert!(off["last_session"].is_null(), "nothing is kept while the preference is off");
|
||||
app.reopen_last_session = true;
|
||||
let settings = app.persist();
|
||||
let mut later = PdfCraftApp::new();
|
||||
later.restore(&settings);
|
||||
later.reopen_last_session = false;
|
||||
later.reopen_last_files(&[]);
|
||||
assert!(later.views.is_empty(), "turned off again: nothing reopens");
|
||||
// b.pdf is gone, and a.pdf is about to open from the command line.
|
||||
std::fs::remove_file(&b).unwrap();
|
||||
let mut later = PdfCraftApp::new();
|
||||
later.restore(&settings);
|
||||
later.reopen_last_files(&[a]);
|
||||
assert!(later.views.is_empty(), "neither opens from the session");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_prompt_stays_inside_the_screen_for_a_long_filename() {
|
||||
// Issue #161: an unwrapped title carrying a long filename widened the centered modal past
|
||||
|
||||
@@ -661,6 +661,18 @@ tools = []
|
||||
evidence = ["crates/ui-egui/tests/ui.rs::persistence_round_trips_and_tolerates_garbage", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_lists_files_and_opens_one", "crates/ui-egui/tests/commands.rs::the_open_recent_menu_is_disabled_while_the_list_is_empty"]
|
||||
notes = "never reads other apps' lists; File ▸ Open Recent lists them (#290), an already-open file focuses its tab instead of reopening it"
|
||||
|
||||
[[feature]]
|
||||
id = "core.reopen-last-session"
|
||||
title = "Reopen the files from the last session at startup"
|
||||
area = "A"
|
||||
tier = "P2"
|
||||
milestone = "M3"
|
||||
status = "shipped"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = ["crates/ui-egui/tests/editing.rs::the_last_session_reopens_its_files_pages_and_active_tab", "crates/ui-egui/tests/editing.rs::quitting_with_unsaved_changes_still_remembers_every_file", "crates/ui-egui/tests/editing.rs::the_last_session_is_off_by_default_and_skips_missing_and_duplicate_files", "crates/ui-egui/src/last_session.rs::malformed_sessions_are_skipped_and_capped"]
|
||||
notes = "Desktop, off by default (Preferences ▸ Documents and view, #442): the files open when PdfCraft last closed reopen at the page each showed, with the same tab in front; files named on the command line open after them, in front; files that are gone, and documents the Recovery dialog offers, are skipped. Quitting with unsaved changes still remembers every tab. Not yet: zoom, layout and scroll position within the page (#418); only the first encrypted file asks for its password (later ones stay closed), and a file opened after it dismisses that prompt; a save that fails partway through a quit can leave the remembered list from that quit; never-saved documents and the web build have no paths to reopen."
|
||||
|
||||
[[feature]]
|
||||
id = "core.usage-rights-preserve"
|
||||
title = "Preserve usage-rights (UR3) signatures from Reader-extended files"
|
||||
|
||||
Reference in New Issue
Block a user