feat: Ordner in andere Ordner verschieben (Issue #18)
- 📂 auch bei Ordnern: verschiebt den Ordner samt Unterordnern und Notizbüchern in einen anderen Ordner oder auf die oberste Ebene - Nicht in sich selbst oder einen eigenen Unterordner; gibt es am Ziel schon einen gleichnamigen Ordner, kommt eine Meldung statt einer Zusammenlegung Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,7 @@ Getestet wird vor allem unter Zorin OS 18 / Ubuntu 24.04 (Wayland und X11).
|
|||||||
|
|
||||||
### Notizbücher und Speichern
|
### Notizbücher und Speichern
|
||||||
- Mehrere **Notizbücher** anlegen, umbenennen, wechseln und löschen (gelöschte landen zunächst im Papierkorb und lassen sich wiederherstellen).
|
- Mehrere **Notizbücher** anlegen, umbenennen, wechseln und löschen (gelöschte landen zunächst im Papierkorb und lassen sich wiederherstellen).
|
||||||
- **Ordner** für Notizbücher, beliebig verschachtelt (z. B. 3. Semester › Mathe): anlegen, umbenennen, löschen (der Inhalt rückt eine Ebene nach oben) und Notizbücher mit 📂 verschieben. Neue Notizbücher landen im gerade geöffneten Ordner.
|
- **Ordner** für Notizbücher, beliebig verschachtelt (z. B. 3. Semester › Mathe): anlegen, umbenennen, löschen (der Inhalt rückt eine Ebene nach oben) und Notizbücher wie Ordner (samt Inhalt) mit 📂 verschieben. Neue Notizbücher landen im gerade geöffneten Ordner.
|
||||||
- **Automatisches Speichern** im Hintergrund: 1,5 Sekunden nach der letzten Änderung, bei ununterbrochenem Schreiben spätestens alle 10 Sekunden. Zeichnen wird dabei nicht unterbrochen.
|
- **Automatisches Speichern** im Hintergrund: 1,5 Sekunden nach der letzten Änderung, bei ununterbrochenem Schreiben spätestens alle 10 Sekunden. Zeichnen wird dabei nicht unterbrochen.
|
||||||
- Jede Datei wird erst vollständig in eine temporäre Datei geschrieben und dann ersetzt – ein Absturz beim Speichern hinterlässt keine halbe Datei.
|
- Jede Datei wird erst vollständig in eine temporäre Datei geschrieben und dann ersetzt – ein Absturz beim Speichern hinterlässt keine halbe Datei.
|
||||||
- **Beim Schließen** (Fenster oder `Strg + Q`) und beim Wechsel des Notizbuchs wird alles Ausstehende geschrieben, bevor es weitergeht. Schlägt das fehl, bleibt die App offen und bietet Wiederholen oder eine Notfall-Sicherung an.
|
- **Beim Schließen** (Fenster oder `Strg + Q`) und beim Wechsel des Notizbuchs wird alles Ausstehende geschrieben, bevor es weitergeht. Schlägt das fehl, bleibt die App offen und bietet Wiederholen oder eine Notfall-Sicherung an.
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ Dieses Dokument beschreibt, wo und wie ink-notes Notizbücher speichert, und die
|
|||||||
- Alle Dateien werden mit Rechten `0600`, Ordner mit `0700` angelegt.
|
- Alle Dateien werden mit Rechten `0600`, Ordner mit `0700` angelegt.
|
||||||
|
|
||||||
### Ordner (`library.json`)
|
### Ordner (`library.json`)
|
||||||
Ordner wie „3. Semester › Mathe“ sind rein logisch: Die Notizbücher bleiben in ihren Verzeichnissen, `library.json` hält nur fest, welcher Ordner welches Notizbuch enthält. Umbenennen, Verschieben und Löschen von Ordnern ändern deshalb nie eine Notizbuch-Datei.
|
Ordner wie „3. Semester › Mathe“ sind rein logisch: Die Notizbücher bleiben in ihren Verzeichnissen, `library.json` hält nur fest, welcher Ordner welches Notizbuch enthält. Umbenennen, Verschieben (samt Inhalt) und Löschen von Ordnern ändern deshalb nie eine Notizbuch-Datei.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -236,6 +236,39 @@ impl Library {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Verschiebt einen Ordner samt Inhalt nach `new_parent` (oberste Ebene: [`ROOT`]) und
|
||||||
|
/// gibt seinen neuen Pfad zurück. Nicht in sich selbst oder einen eigenen Unterordner.
|
||||||
|
pub fn move_folder(&mut self, path: &str, new_parent: &str) -> Result<String, String> {
|
||||||
|
if !self.folders.contains(path) {
|
||||||
|
return Err("Den Ordner gibt es nicht mehr".to_string());
|
||||||
|
}
|
||||||
|
if !new_parent.is_empty() && !self.folders.contains(new_parent) {
|
||||||
|
return Err("Den Zielordner gibt es nicht mehr".to_string());
|
||||||
|
}
|
||||||
|
if is_within(new_parent, path) {
|
||||||
|
return Err("Ein Ordner kann nicht in sich selbst verschoben werden".to_string());
|
||||||
|
}
|
||||||
|
let name = folder_name(path);
|
||||||
|
let new_path = join(new_parent, name);
|
||||||
|
if new_path == path {
|
||||||
|
return Ok(new_path);
|
||||||
|
}
|
||||||
|
if self.folders.contains(&new_path) {
|
||||||
|
return Err(format!("Im Zielordner gibt es schon einen Ordner „{name}“"));
|
||||||
|
}
|
||||||
|
self.move_contents(path, &new_path);
|
||||||
|
Ok(new_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mögliche Ziele für das Verschieben des Ordners `path`: alle Ordner außer ihm selbst und
|
||||||
|
/// seinen Unterordnern (die oberste Ebene ist immer ein Ziel).
|
||||||
|
pub fn folder_move_targets(&self, path: &str) -> Vec<&str> {
|
||||||
|
self.all_folders()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|f| !is_within(f, path))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Legt das Notizbuch in `folder` (oberste Ebene: [`ROOT`]).
|
/// Legt das Notizbuch in `folder` (oberste Ebene: [`ROOT`]).
|
||||||
pub fn move_notebook(&mut self, notebook_id: &str, folder: &str) -> Result<(), String> {
|
pub fn move_notebook(&mut self, notebook_id: &str, folder: &str) -> Result<(), String> {
|
||||||
if folder.is_empty() {
|
if folder.is_empty() {
|
||||||
@@ -338,6 +371,38 @@ mod tests {
|
|||||||
assert_eq!(lib.subfolders(ROOT), ["Mathe", "Physik"]);
|
assert_eq!(lib.subfolders(ROOT), ["Mathe", "Physik"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_move_folder() {
|
||||||
|
let mut lib = library();
|
||||||
|
lib.create_folder(ROOT, "Mathe-Sammlung").unwrap();
|
||||||
|
// Ordner samt Inhalt in einen anderen Ordner schieben
|
||||||
|
let new = lib
|
||||||
|
.move_folder("3. Semester/Mathe", "Mathe-Sammlung")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(new, "Mathe-Sammlung/Mathe");
|
||||||
|
assert_eq!(lib.folder_of("analysis"), "Mathe-Sammlung/Mathe");
|
||||||
|
assert!(!lib.folders.contains("3. Semester/Mathe"));
|
||||||
|
// Auf die oberste Ebene und zurück
|
||||||
|
assert_eq!(
|
||||||
|
lib.move_folder("Mathe-Sammlung/Mathe", ROOT).unwrap(),
|
||||||
|
"Mathe"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
lib.move_folder("Mathe", "3. Semester").unwrap(),
|
||||||
|
"3. Semester/Mathe"
|
||||||
|
);
|
||||||
|
// Nicht in sich selbst oder in einen eigenen Unterordner
|
||||||
|
assert!(lib.move_folder("3. Semester", "3. Semester").is_err());
|
||||||
|
assert!(lib.move_folder("3. Semester", "3. Semester/Mathe").is_err());
|
||||||
|
assert_eq!(lib.folder_move_targets("3. Semester"), ["Mathe-Sammlung"]);
|
||||||
|
// Namenskollision am Ziel
|
||||||
|
lib.create_folder("Mathe-Sammlung", "Physik").unwrap();
|
||||||
|
assert!(lib
|
||||||
|
.move_folder("3. Semester/Physik", "Mathe-Sammlung")
|
||||||
|
.is_err());
|
||||||
|
assert_eq!(lib.folder_of("uebung"), "3. Semester");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_move_notebook() {
|
fn test_move_notebook() {
|
||||||
let mut lib = library();
|
let mut lib = library();
|
||||||
|
|||||||
+21
@@ -3045,6 +3045,27 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
UiAction::MoveFolder { path, new_parent } => {
|
||||||
|
if let Some(new_path) = change_library(&storage_base_dir, &mut ui_state, |library| {
|
||||||
|
library.move_folder(&path, &new_parent)
|
||||||
|
}) {
|
||||||
|
// Der gezeigte Ordner kann darin liegen
|
||||||
|
let shown = &ui_state.notebook_folder;
|
||||||
|
if shown == &path || shown.starts_with(&format!("{path}/")) {
|
||||||
|
ui_state.notebook_folder = format!("{new_path}{}", &shown[path.len()..]);
|
||||||
|
}
|
||||||
|
let target = if new_parent.is_empty() {
|
||||||
|
"die oberste Ebene".to_string()
|
||||||
|
} else {
|
||||||
|
format!("„{}“", ink_notes::library::folder_name(&new_parent))
|
||||||
|
};
|
||||||
|
ui_state.set_export_message(format!(
|
||||||
|
"Ordner „{}“ verschoben in {target}",
|
||||||
|
ink_notes::library::folder_name(&path)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
UiAction::MoveNotebook { id, folder } => {
|
UiAction::MoveNotebook { id, folder } => {
|
||||||
if change_library(&storage_base_dir, &mut ui_state, |library| {
|
if change_library(&storage_base_dir, &mut ui_state, |library| {
|
||||||
library.move_notebook(&id, &folder)
|
library.move_notebook(&id, &folder)
|
||||||
|
|||||||
@@ -171,6 +171,14 @@ impl LongPress {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Was im Dokumenten-Manager gerade verschoben wird.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum MoveSubject {
|
||||||
|
Notebook(String),
|
||||||
|
/// Pfad des Ordners
|
||||||
|
Folder(String),
|
||||||
|
}
|
||||||
|
|
||||||
/// Kontextmenü nach langem Drücken auf das Blatt.
|
/// Kontextmenü nach langem Drücken auf das Blatt.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct ContextMenu {
|
pub struct ContextMenu {
|
||||||
@@ -248,6 +256,11 @@ pub enum UiAction {
|
|||||||
id: String,
|
id: String,
|
||||||
folder: String,
|
folder: String,
|
||||||
},
|
},
|
||||||
|
/// Ordner samt Inhalt nach `new_parent` verschieben
|
||||||
|
MoveFolder {
|
||||||
|
path: String,
|
||||||
|
new_parent: String,
|
||||||
|
},
|
||||||
RenameNotebook {
|
RenameNotebook {
|
||||||
id: String,
|
id: String,
|
||||||
new_title: String,
|
new_title: String,
|
||||||
@@ -331,8 +344,8 @@ pub struct UiState {
|
|||||||
/// Ordner, der gerade umbenannt wird, bzw. dessen Löschen bestätigt werden soll
|
/// Ordner, der gerade umbenannt wird, bzw. dessen Löschen bestätigt werden soll
|
||||||
pub renaming_folder: Option<String>,
|
pub renaming_folder: Option<String>,
|
||||||
pub confirm_delete_folder: Option<String>,
|
pub confirm_delete_folder: Option<String>,
|
||||||
/// Notizbuch, für das gerade ein Zielordner gewählt wird
|
/// Notizbuch oder Ordner, für das gerade ein Zielordner gewählt wird
|
||||||
pub moving_notebook_id: Option<String>,
|
pub moving: Option<MoveSubject>,
|
||||||
pub renaming_title_input: String,
|
pub renaming_title_input: String,
|
||||||
pub document_modal_rect: Option<egui::Rect>,
|
pub document_modal_rect: Option<egui::Rect>,
|
||||||
/// Gitea Release- & Update-Manager
|
/// Gitea Release- & Update-Manager
|
||||||
@@ -439,7 +452,7 @@ impl Default for UiState {
|
|||||||
notebook_folder: String::new(),
|
notebook_folder: String::new(),
|
||||||
renaming_folder: None,
|
renaming_folder: None,
|
||||||
confirm_delete_folder: None,
|
confirm_delete_folder: None,
|
||||||
moving_notebook_id: None,
|
moving: None,
|
||||||
renaming_title_input: String::new(),
|
renaming_title_input: String::new(),
|
||||||
document_modal_rect: None,
|
document_modal_rect: None,
|
||||||
is_update_dialog_open: false,
|
is_update_dialog_open: false,
|
||||||
@@ -1819,6 +1832,13 @@ fn folder_row(ui: &mut egui::Ui, state: &mut UiState, path: &str, actions: &mut
|
|||||||
{
|
{
|
||||||
state.confirm_delete_folder = Some(path.to_string());
|
state.confirm_delete_folder = Some(path.to_string());
|
||||||
}
|
}
|
||||||
|
if ui
|
||||||
|
.button(egui::RichText::new("📂").size(16.0))
|
||||||
|
.on_hover_text("Ordner samt Inhalt in einen anderen Ordner verschieben")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
state.moving = Some(MoveSubject::Folder(path.to_string()));
|
||||||
|
}
|
||||||
if ui
|
if ui
|
||||||
.button(egui::RichText::new("✏").size(16.0))
|
.button(egui::RichText::new("✏").size(16.0))
|
||||||
.on_hover_text("Ordner umbenennen")
|
.on_hover_text("Ordner umbenennen")
|
||||||
@@ -1860,24 +1880,35 @@ fn folder_row(ui: &mut egui::Ui, state: &mut UiState, path: &str, actions: &mut
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zielordner für das Verschieben eines Notizbuchs: oberste Ebene und alle Ordner als Baum.
|
/// Zielordner für das Verschieben eines Notizbuchs oder Ordners: oberste Ebene und alle
|
||||||
|
/// Ordner als Baum (ein Ordner nicht in sich selbst oder seine Unterordner).
|
||||||
fn move_target_list(
|
fn move_target_list(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
state: &mut UiState,
|
state: &mut UiState,
|
||||||
notebook_id: &str,
|
subject: &MoveSubject,
|
||||||
actions: &mut Vec<UiAction>,
|
actions: &mut Vec<UiAction>,
|
||||||
) {
|
) {
|
||||||
let title = state
|
let (title, current, folders) = match subject {
|
||||||
.available_notebooks
|
MoveSubject::Notebook(id) => (
|
||||||
.iter()
|
state
|
||||||
.find(|s| s.id == notebook_id)
|
.available_notebooks
|
||||||
.map_or(notebook_id, |s| s.title.as_str())
|
.iter()
|
||||||
.to_string();
|
.find(|s| &s.id == id)
|
||||||
let current = state.library.folder_of(notebook_id).to_string();
|
.map_or(id.as_str(), |s| s.title.as_str())
|
||||||
|
.to_string(),
|
||||||
|
state.library.folder_of(id).to_string(),
|
||||||
|
state.library.all_folders(),
|
||||||
|
),
|
||||||
|
MoveSubject::Folder(path) => (
|
||||||
|
format!("📁 {}", crate::library::folder_name(path)),
|
||||||
|
crate::library::parent_folder(path).to_string(),
|
||||||
|
state.library.folder_move_targets(path),
|
||||||
|
),
|
||||||
|
};
|
||||||
ui.label(egui::RichText::new(format!("„{title}“ verschieben nach:")).strong());
|
ui.label(egui::RichText::new(format!("„{title}“ verschieben nach:")).strong());
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
let mut targets = vec![(String::new(), "📚 Oberste Ebene".to_string(), 0)];
|
let mut targets = vec![(String::new(), "📚 Oberste Ebene".to_string(), 0)];
|
||||||
for path in state.library.all_folders() {
|
for path in folders {
|
||||||
let depth = path.matches('/').count() + 1;
|
let depth = path.matches('/').count() + 1;
|
||||||
targets.push((
|
targets.push((
|
||||||
path.to_string(),
|
path.to_string(),
|
||||||
@@ -1897,18 +1928,24 @@ fn move_target_list(
|
|||||||
.on_disabled_hover_text("Liegt bereits hier")
|
.on_disabled_hover_text("Liegt bereits hier")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
actions.push(UiAction::MoveNotebook {
|
actions.push(match subject {
|
||||||
id: notebook_id.to_string(),
|
MoveSubject::Notebook(id) => UiAction::MoveNotebook {
|
||||||
folder: path.clone(),
|
id: id.clone(),
|
||||||
|
folder: path.clone(),
|
||||||
|
},
|
||||||
|
MoveSubject::Folder(folder) => UiAction::MoveFolder {
|
||||||
|
path: folder.clone(),
|
||||||
|
new_parent: path.clone(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
state.moving_notebook_id = None;
|
state.moving = None;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
if ui.button("Abbrechen").clicked() {
|
if ui.button("Abbrechen").clicked() {
|
||||||
state.moving_notebook_id = None;
|
state.moving = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2537,7 +2574,7 @@ pub fn render_ui(
|
|||||||
.to_string();
|
.to_string();
|
||||||
state.renaming_folder = None;
|
state.renaming_folder = None;
|
||||||
state.confirm_delete_folder = None;
|
state.confirm_delete_folder = None;
|
||||||
state.moving_notebook_id = None;
|
state.moving = None;
|
||||||
actions.push(UiAction::RefreshNotebooks);
|
actions.push(UiAction::RefreshNotebooks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3804,7 +3841,7 @@ pub fn render_ui(
|
|||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
|
|
||||||
// Zielordner für „Verschieben“ wählen
|
// Zielordner für „Verschieben“ wählen
|
||||||
if let Some(moving) = state.moving_notebook_id.clone() {
|
if let Some(moving) = state.moving.clone() {
|
||||||
move_target_list(ui, state, &moving, &mut actions);
|
move_target_list(ui, state, &moving, &mut actions);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3917,7 +3954,7 @@ pub fn render_ui(
|
|||||||
.on_hover_text("In einen anderen Ordner verschieben")
|
.on_hover_text("In einen anderen Ordner verschieben")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
state.moving_notebook_id = Some(summary.id.clone());
|
state.moving = Some(MoveSubject::Notebook(summary.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.button(egui::RichText::new("✏").size(16.0))
|
if ui.button(egui::RichText::new("✏").size(16.0))
|
||||||
|
|||||||
Reference in New Issue
Block a user