Add project-mode tooling: characters, outline, diff and revision status
Rounds out project mode, where the workspace is the project root and one subfolder holds the manuscript proper: - Characters and Outline windows, backed by new `characters` and `outline` modules that read the cast from character sheets and measure how much of the snowflake outline is actually written. - Edit ▸ Changes… diffs the open file against its last committed version. - Revision status, per-file and project word counts, an archive action and hidden folders in the file panel. - Chapter-file export alongside the ODT master, richer header parsing, and a project word list for names and invented terms. - The export path now follows the workspace: opening a project points it at that project root, keeping a file name you chose yourself and re-deriving one that merely echoed the folder it sat in. - Clicking an issue in the grammar/spelling panel takes the editor to it, selecting the words and centring them; applying a suggestion jumps to the rewritten text as well. README covers the new windows and workflows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017bSn3Xijp8GofZVUnRX4oq
This commit is contained in:
+86
-16
@@ -114,21 +114,20 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Point the workspace at a generated project: its configured drafting
|
||||
/// subfolder when the template produced one, otherwise the project root.
|
||||
/// Point the workspace at a generated project. The project *root* is opened
|
||||
/// rather than the drafting folder: the app recognises the layout and treats
|
||||
/// the drafting folder as the manuscript, keeping the characters, outline and
|
||||
/// the rest reachable in the same tree.
|
||||
fn open_project(&mut self, project: &Path) {
|
||||
let subdir = self.config.project_open_subdir.trim();
|
||||
let (workspace, note) = match subdir {
|
||||
"" => (project.to_path_buf(), String::new()),
|
||||
sub if project.join(sub).is_dir() => (project.join(sub), String::new()),
|
||||
sub => (
|
||||
project.to_path_buf(),
|
||||
format!(" (no {sub} folder in it, opened the project root)"),
|
||||
),
|
||||
let note = if subdir.is_empty() || project.join(subdir).is_dir() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" (no {subdir} folder in it, so nothing is marked as the manuscript)")
|
||||
};
|
||||
self.save_current();
|
||||
self.workspace_input = workspace.display().to_string();
|
||||
self.config.workspace = workspace;
|
||||
self.workspace_input = project.display().to_string();
|
||||
self.config.workspace = project.to_path_buf();
|
||||
// Export alongside the new project rather than into the previous one.
|
||||
self.config.export_path = project.join(format!(
|
||||
"{}.odt",
|
||||
@@ -217,7 +216,7 @@ impl App {
|
||||
if !subdir.is_empty() {
|
||||
ui.label(
|
||||
egui::RichText::new(format!(
|
||||
"Then opens its {subdir} folder as the workspace."
|
||||
"Then opens the project, with {subdir} as the manuscript."
|
||||
))
|
||||
.small()
|
||||
.weak(),
|
||||
@@ -281,6 +280,71 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Title and author written into exported documents.
|
||||
pub(super) fn manuscript_settings_window(&mut self, ctx: &egui::Context) {
|
||||
let mut open = self.show_manuscript_settings;
|
||||
let mut close = false;
|
||||
egui::Window::new("Manuscript details")
|
||||
.open(&mut open)
|
||||
.resizable(false)
|
||||
.collapsible(false)
|
||||
.default_width(380.0)
|
||||
.show(ctx, |ui| {
|
||||
let mut save_now = false;
|
||||
egui::Grid::new("manuscript_details_grid")
|
||||
.num_columns(2)
|
||||
.spacing([10.0, 8.0])
|
||||
.show(ui, |ui| {
|
||||
ui.label("Title:");
|
||||
// Resolved first: the field borrows `self.config` mutably.
|
||||
let fallback = self
|
||||
.workspace()
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("Manuscript")
|
||||
.to_string();
|
||||
let r = ui
|
||||
.add(
|
||||
egui::TextEdit::singleline(&mut self.config.manuscript_title)
|
||||
.hint_text(fallback)
|
||||
.desired_width(240.0),
|
||||
)
|
||||
.on_hover_text("Blank uses the project folder's name");
|
||||
save_now |= r.lost_focus();
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Author:");
|
||||
let r = ui.add(
|
||||
egui::TextEdit::singleline(&mut self.config.manuscript_author)
|
||||
.desired_width(240.0),
|
||||
);
|
||||
save_now |= r.lost_focus();
|
||||
ui.end_row();
|
||||
});
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Written into exported .odt files as their document \
|
||||
properties, which is what a word processor shows under \
|
||||
File ▸ Properties.",
|
||||
)
|
||||
.small()
|
||||
.weak(),
|
||||
);
|
||||
ui.separator();
|
||||
if ui.button("Close").clicked() {
|
||||
close = true;
|
||||
}
|
||||
if save_now {
|
||||
self.config.save();
|
||||
}
|
||||
});
|
||||
let now_open = open && !close;
|
||||
if self.show_manuscript_settings && !now_open {
|
||||
self.config.save();
|
||||
}
|
||||
self.show_manuscript_settings = now_open;
|
||||
}
|
||||
|
||||
/// Settings for **File ▸ New project…**: which template to render, how to
|
||||
/// run it, and the credentials its hooks read.
|
||||
pub(super) fn project_settings_window(&mut self, ctx: &egui::Context) {
|
||||
@@ -331,12 +395,18 @@ impl App {
|
||||
.num_columns(2)
|
||||
.spacing([10.0, 8.0])
|
||||
.show(ui, |ui| {
|
||||
ui.label("Open subfolder:");
|
||||
let r = ui.add(
|
||||
egui::TextEdit::singleline(&mut self.config.project_open_subdir)
|
||||
ui.label("Manuscript folder:");
|
||||
let r = ui
|
||||
.add(
|
||||
egui::TextEdit::singleline(
|
||||
&mut self.config.project_open_subdir,
|
||||
)
|
||||
.hint_text("06-First Draft")
|
||||
.desired_width(280.0),
|
||||
);
|
||||
)
|
||||
.on_hover_text(
|
||||
"Which folder of a project holds the book. Its files are ordered, numbered and exported; the rest of the project is reference. A leading number is optional, so “First Draft” also matches “06-First Draft”.",
|
||||
);
|
||||
save_now |= r.lost_focus();
|
||||
ui.end_row();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user