From 4385244d39b37d003dbb7a66a2ff3b05079b7047 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sat, 4 Jul 2026 23:04:00 -0400 Subject: feat: drag/drop files in gui to change directories --- data/.admin_history.json | 1 + internal/admin/server.go | 45 +++++++++++++++++++ internal/admin/static/admin.css | 9 ++-- internal/admin/static/dropeditor.js | 3 +- internal/admin/static/editor.js | 82 +++++++++++++++++++++++++++++++---- internal/admin/static/hazardeditor.js | 3 +- internal/admin/static/itemeditor.js | 3 +- internal/admin/static/mobeditor.js | 3 +- internal/admin/static/moduleeditor.js | 3 +- internal/admin/static/objecteditor.js | 3 +- internal/admin/static/techeditor.js | 3 +- internal/admin/undo.go | 21 ++++++--- internal/admin/yaml_util.go | 64 +++++++++++++++++++++++++++ 13 files changed, 219 insertions(+), 24 deletions(-) create mode 100644 data/.admin_history.json diff --git a/data/.admin_history.json b/data/.admin_history.json new file mode 100644 index 0000000..4639022 --- /dev/null +++ b/data/.admin_history.json @@ -0,0 +1 @@ +{"history":[{"time":"2026-07-04T23:03:14-04:00","description":"move objects teak_tree to test/","file_path":"data/objects/teak_tree.yaml","new_file_path":"data/objects/test/teak_tree.yaml","old_content":"color: \"B3\"\ngather:\n deplete_timer: 50\n drops:\n - depletes: false\n item_id: teak_logs\n level: 35\n success_message: You get some {B3}teak logs{/}.\n weight: 100\n xp: 85\n fail_message: You swing but get no logs.\n gather_message: You swing your axe at the teak tree...\n nest_chance: 256\n respawn_broadcast: A {name} grows back.\n respawn_timer: 15\n skill: woodcutting\n success:\n base: 0.32\n cap: 0.82\n per_level: 0.01\n tools:\n - axe\nname: teak tree\n","new_content":"color: \"B3\"\ngather:\n deplete_timer: 50\n drops:\n - depletes: false\n item_id: teak_logs\n level: 35\n success_message: You get some {B3}teak logs{/}.\n weight: 100\n xp: 85\n fail_message: You swing but get no logs.\n gather_message: You swing your axe at the teak tree...\n nest_chance: 256\n respawn_broadcast: A {name} grows back.\n respawn_timer: 15\n skill: woodcutting\n success:\n base: 0.32\n cap: 0.82\n per_level: 0.01\n tools:\n - axe\nname: teak tree\n","is_delete":false,"is_create":false},{"time":"2026-07-04T23:03:19-04:00","description":"move objects teak_tree to root","file_path":"data/objects/test/teak_tree.yaml","new_file_path":"data/objects/teak_tree.yaml","old_content":"color: \"B3\"\ngather:\n deplete_timer: 50\n drops:\n - depletes: false\n item_id: teak_logs\n level: 35\n success_message: You get some {B3}teak logs{/}.\n weight: 100\n xp: 85\n fail_message: You swing but get no logs.\n gather_message: You swing your axe at the teak tree...\n nest_chance: 256\n respawn_broadcast: A {name} grows back.\n respawn_timer: 15\n skill: woodcutting\n success:\n base: 0.32\n cap: 0.82\n per_level: 0.01\n tools:\n - axe\nname: teak tree\n","new_content":"color: \"B3\"\ngather:\n deplete_timer: 50\n drops:\n - depletes: false\n item_id: teak_logs\n level: 35\n success_message: You get some {B3}teak logs{/}.\n weight: 100\n xp: 85\n fail_message: You swing but get no logs.\n gather_message: You swing your axe at the teak tree...\n nest_chance: 256\n respawn_broadcast: A {name} grows back.\n respawn_timer: 15\n skill: woodcutting\n success:\n base: 0.32\n cap: 0.82\n per_level: 0.01\n tools:\n - axe\nname: teak tree\n","is_delete":false,"is_create":false},{"time":"2026-07-04T23:03:22-04:00","description":"delete directory objects/test","file_path":"data/objects/test","old_content":"","new_content":"","is_delete":true,"is_create":false}],"redo":null} \ No newline at end of file diff --git a/internal/admin/server.go b/internal/admin/server.go index b1f641a..04e68a6 100644 --- a/internal/admin/server.go +++ b/internal/admin/server.go @@ -118,24 +118,31 @@ func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStor apiMux.HandleFunc("/api/rooms/new-empty", s.handleCreateEmptyRoom) apiMux.HandleFunc("/api/rooms/", s.handleRoomByID) apiMux.HandleFunc("/api/objects/move", s.handleEntityMove("objects")) + apiMux.HandleFunc("/api/objects/dirs", s.handleEntityDirs("objects")) apiMux.HandleFunc("/api/objects", s.handleObjects) apiMux.HandleFunc("/api/objects/", s.handleObjectByID) apiMux.HandleFunc("/api/items/move", s.handleEntityMove("items")) + apiMux.HandleFunc("/api/items/dirs", s.handleEntityDirs("items")) apiMux.HandleFunc("/api/items", s.handleItems) apiMux.HandleFunc("/api/items/", s.handleItemByID) apiMux.HandleFunc("/api/mobs/move", s.handleEntityMove("mobs")) + apiMux.HandleFunc("/api/mobs/dirs", s.handleEntityDirs("mobs")) apiMux.HandleFunc("/api/mobs", s.handleMobs) apiMux.HandleFunc("/api/mobs/", s.handleMobByID) apiMux.HandleFunc("/api/drops/move", s.handleEntityMove("drops")) + apiMux.HandleFunc("/api/drops/dirs", s.handleEntityDirs("drops")) apiMux.HandleFunc("/api/drops", s.handleDrops) apiMux.HandleFunc("/api/drops/", s.handleDropByID) apiMux.HandleFunc("/api/hazards/move", s.handleEntityMove("hazards")) + apiMux.HandleFunc("/api/hazards/dirs", s.handleEntityDirs("hazards")) apiMux.HandleFunc("/api/hazards", s.handleHazards) apiMux.HandleFunc("/api/hazards/", s.handleHazardByID) apiMux.HandleFunc("/api/techs/move", s.handleEntityMove("techs")) + apiMux.HandleFunc("/api/techs/dirs", s.handleEntityDirs("techs")) apiMux.HandleFunc("/api/techs", s.handleTechs) apiMux.HandleFunc("/api/techs/", s.handleTechByID) apiMux.HandleFunc("/api/modules/move", s.handleEntityMove("modules")) + apiMux.HandleFunc("/api/modules/dirs", s.handleEntityDirs("modules")) apiMux.HandleFunc("/api/modules", s.handleModules) apiMux.HandleFunc("/api/modules/", s.handleModuleByID) apiMux.HandleFunc("/api/players", s.handlePlayers) @@ -466,6 +473,44 @@ func (s *AdminServer) doRedo(w http.ResponseWriter, r *http.Request) { writeJSON(w, resp) } +func (s *AdminServer) handleEntityDirs(entityType string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var body struct { + Action string `json:"action"` + Name string `json:"name"` + Dir string `json:"dir"` + } + if err := readJSON(r, &body); err != nil { + writeJSON(w, map[string]any{"error": "invalid"}) + return + } + switch body.Action { + case "create": + if err := createEntityDir(s.dataDir, entityType, body.Name); err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } + writeJSON(w, map[string]any{"ok": true, "name": body.Name}) + case "delete": + changes, err := deleteEntityDir(s.dataDir, entityType, body.Dir) + if err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } + for _, ch := range changes { + s.undoStack.Push(ch) + } + writeJSON(w, map[string]any{"ok": true, "moved": len(changes) - 1}) + default: + writeJSON(w, map[string]any{"error": "unknown action"}) + } + } +} + func (s *AdminServer) handleEntityMove(entityType string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index 42e64fa..33dbdc4 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -2,7 +2,7 @@ *{box-sizing:border-box;margin:0;padding:0} input[type=number]{-moz-appearance:textfield} input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0} -body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100vh} +body{font-family:monospace;background:var(--bg);color:var(--text);display:flex;flex-direction:column;height:100vh;overflow:hidden} .nav{background:var(--panel);border-bottom:1px solid var(--border);padding:8px 16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap} .nav a{color:var(--text);text-decoration:none;padding:4px 10px;border-radius:3px;font-size:13px} .nav a:hover,.nav a.active{background:var(--accent)} @@ -11,7 +11,7 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100 .nav .undo-bar button:disabled{opacity:.4;cursor:default} .nav .undo-bar button:not(:disabled):hover{background:var(--accent)} .nav .undo-bar span{color:#888;flex:1;text-align:right;margin-right:8px} -.layout{display:flex;height:calc(100vh - 38px)} +.layout{display:flex;flex:1;min-height:0} .main{flex:1;overflow:auto;position:relative;user-select:none;-webkit-user-select:none} #mapSvg{display:block} .panel{width:300px;background:var(--panel);border-left:1px solid var(--border);overflow-y:auto;padding:16px;flex-shrink:0;position:relative} @@ -187,7 +187,7 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100 .color-picker{display:flex;flex-wrap:wrap;gap:2px;margin-top:4px;max-height:200px;overflow-y:auto} .color-picker .swatch{width:18px;height:18px;border:1px solid transparent;cursor:pointer;border-radius:1px} .color-picker .swatch:hover,.color-picker .swatch.selected{border-color:#fff;transform:scale(1.2);z-index:1} -.editor-container{display:flex;height:calc(100vh - 38px)} +.editor-container{display:flex;flex:1;min-height:0} .editor-list{width:260px;background:var(--panel);border-right:1px solid var(--border);overflow-y:auto;padding:10px;flex-shrink:0} .editor-main{flex:1;overflow-y:auto;padding:16px} .editor-list .item{padding:5px 8px;font-size:12px;cursor:pointer;border-radius:3px;margin-bottom:2px} @@ -202,6 +202,7 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100 .dir-count{font-size:9px;color:#555} .dir-items{padding-left:10px} .dir-items .item{font-size:11px} +.dir-drop-target{background:rgba(15,52,96,.3)!important;outline:1px dashed var(--accent);outline-offset:-2px} .editor-main pre{background:var(--input-bg);padding:12px;border-radius:4px;font-size:12px;overflow-x:auto;border:1px solid var(--border)} .form-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:4px 10px} .form-grid .form-group{margin-bottom:4px} @@ -306,7 +307,7 @@ g.ud-hover:hover text{font-weight:bold} .obj-sub-remove{align-self:flex-end;padding:2px 6px;font-size:10px;min-width:auto;background:var(--danger);color:#fff;border:none;border-radius:3px;cursor:pointer;margin-bottom:3px} .obj-sub-add{display:block;width:100%;margin-top:4px;padding:4px 8px;font-size:11px;background:var(--accent);color:var(--text);border:1px solid #1a5a8e;border-radius:3px;cursor:pointer;font-family:monospace} .obj-sub-add:hover{background:var(--hover)} -.add-section-bar{display:flex;gap:6px;align-items:center;margin-top:12px;padding:8px 0;border-top:1px solid var(--border);max-width:calc(50% - 5px)} +.add-section-bar{display:flex;gap:6px;align-items:center;margin-top:12px;padding:8px 0;border-top:1px solid var(--border);max-width:100%;flex-wrap:wrap} .add-section-bar select{padding:4px 8px;font-size:11px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;font-family:monospace;flex:1} .add-section-bar button{padding:4px 12px;font-size:11px} .obj-kv-list{margin-top:4px} diff --git a/internal/admin/static/dropeditor.js b/internal/admin/static/dropeditor.js index dddcddb..cbd51c7 100644 --- a/internal/admin/static/dropeditor.js +++ b/internal/admin/static/dropeditor.js @@ -66,7 +66,7 @@ function renderDropEditor(id, data) { html += renderCard(sec, data); }); html += ''; - html += renderMoveBar(); + html += '
'; html += ''; html += ''; html += ''; html += renderMoveBar(); html += ''; + html += ''; html += ''; $('#editorMain').innerHTML = html; diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index 802b48a..1758673 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -173,9 +173,9 @@ function renderMobEditor(id, data) { }); html += ''; html += ''; + html += renderMoveBar(); html += ''; - html += renderMoveBar(); html += ''; html += ''; $('#editorMain').innerHTML = html; diff --git a/internal/admin/static/moduleeditor.js b/internal/admin/static/moduleeditor.js index 1e7de30..6d6a3e0 100644 --- a/internal/admin/static/moduleeditor.js +++ b/internal/admin/static/moduleeditor.js @@ -90,9 +90,9 @@ function renderModuleEditor(id, data) { }); html += ''; html += ''; - html += ''; html += renderMoveBar(); html += ''; + html += ''; html += ''; - html += renderMoveBar(); html += ''; html += ''; $('#editorMain').innerHTML = html; diff --git a/internal/admin/static/techeditor.js b/internal/admin/static/techeditor.js index 4849fb1..cd4f50f 100644 --- a/internal/admin/static/techeditor.js +++ b/internal/admin/static/techeditor.js @@ -80,7 +80,7 @@ function renderTechEditor(id, data) { html += renderCard(sec, data); }); html += ''; - html += renderMoveBar(); + html += ''; html += ''; html += '