diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/admin/server.go | 43 | ||||
| -rw-r--r-- | internal/admin/static/admin.css | 2 | ||||
| -rw-r--r-- | internal/admin/static/dropeditor.js | 3 | ||||
| -rw-r--r-- | internal/admin/static/editor.js | 29 | ||||
| -rw-r--r-- | internal/admin/static/hazardeditor.js | 3 | ||||
| -rw-r--r-- | internal/admin/static/itemeditor.js | 3 | ||||
| -rw-r--r-- | internal/admin/static/mobeditor.js | 3 | ||||
| -rw-r--r-- | internal/admin/static/moduleeditor.js | 3 | ||||
| -rw-r--r-- | internal/admin/static/objecteditor.js | 3 | ||||
| -rw-r--r-- | internal/admin/static/techeditor.js | 3 | ||||
| -rw-r--r-- | internal/admin/yaml_util.go | 44 |
11 files changed, 129 insertions, 10 deletions
diff --git a/internal/admin/server.go b/internal/admin/server.go index ef95e73..b1f641a 100644 --- a/internal/admin/server.go +++ b/internal/admin/server.go @@ -117,20 +117,25 @@ func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStor apiMux.HandleFunc("/api/rooms", s.handleRooms) 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", s.handleObjects) apiMux.HandleFunc("/api/objects/", s.handleObjectByID) + apiMux.HandleFunc("/api/items/move", s.handleEntityMove("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", s.handleMobs) apiMux.HandleFunc("/api/mobs/", s.handleMobByID) + apiMux.HandleFunc("/api/drops/move", s.handleEntityMove("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", s.handleHazards) apiMux.HandleFunc("/api/hazards/", s.handleHazardByID) + apiMux.HandleFunc("/api/techs/move", s.handleEntityMove("techs")) apiMux.HandleFunc("/api/techs", s.handleTechs) apiMux.HandleFunc("/api/techs/", s.handleTechByID) - apiMux.HandleFunc("/api/courses", s.handleCourses) - apiMux.HandleFunc("/api/courses/", s.handleCourseByID) + apiMux.HandleFunc("/api/modules/move", s.handleEntityMove("modules")) apiMux.HandleFunc("/api/modules", s.handleModules) apiMux.HandleFunc("/api/modules/", s.handleModuleByID) apiMux.HandleFunc("/api/players", s.handlePlayers) @@ -461,6 +466,40 @@ func (s *AdminServer) doRedo(w http.ResponseWriter, r *http.Request) { writeJSON(w, resp) } +func (s *AdminServer) handleEntityMove(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 { + ID string `json:"id"` + Dir string `json:"dir"` + } + if err := readJSON(r, &body); err != nil || body.ID == "" { + writeJSON(w, map[string]any{"error": "invalid"}) + return + } + oldPath, newPath, data, err := moveEntityFile(s.dataDir, entityType, body.ID, body.Dir) + if err != nil { + writeJSON(w, map[string]any{"error": err.Error()}) + return + } + desc := fmt.Sprintf("move %s %s to %s/", entityType, body.ID, body.Dir) + if body.Dir == "" { + desc = fmt.Sprintf("move %s %s to root", entityType, body.ID) + } + s.undoStack.Push(ChangeDesc{ + Description: desc, + FilePath: oldPath, + NewFilePath: newPath, + OldContent: data, + NewContent: data, + }) + writeJSON(w, map[string]any{"ok": true}) + } +} + func (s *AdminServer) rebuildAfterUndo(change *ChangeDesc) { s.world.RebuildRoomIndex(s.dataDir) s.world.ClearHazardCache() diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index f068207..42e64fa 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -42,7 +42,7 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100 .login{max-width:360px;margin:80px auto;background:var(--panel);padding:30px;border:1px solid var(--border);border-radius:6px} .login h1{font-size:18px;margin-bottom:20px;text-align:center} .z-controls-wrapper{position:absolute;top:12px;left:12px;z-index:10} -.z-controls{position:relative;display:flex;gap:6px;align-items:center;background:var(--panel);padding:6px 10px;border-radius:4px;border:1px solid var(--border)} +.z-controls{position:relative;display:flex;gap:6px;align-items:center;flex-wrap:wrap;background:var(--panel);padding:6px 10px;border-radius:4px;border:1px solid var(--border)} .z-controls button{width:28px;height:28px;font-size:16px;line-height:1;background:var(--input-bg);color:var(--text);border:1px solid var(--border);border-radius:3px;cursor:pointer} .z-controls button:hover{background:var(--accent)} .z-controls .z-label{font-size:13px;min-width:40px;text-align:center;font-weight:bold} diff --git a/internal/admin/static/dropeditor.js b/internal/admin/static/dropeditor.js index 1569a04..dddcddb 100644 --- a/internal/admin/static/dropeditor.js +++ b/internal/admin/static/dropeditor.js @@ -66,6 +66,7 @@ function renderDropEditor(id, data) { html += renderCard(sec, data); }); html += '</div>'; + html += renderMoveBar(); html += '</div>'; html += '<div class="tab-content" id="tabYaml" style="display:none">'; @@ -93,8 +94,8 @@ function renderCard(sec, data) { var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; - h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; h += '<span class="obj-card-label">' + sec.label + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>'; h += '</div>'; if (!collapsed) { h += '<div class="obj-card-body">'; diff --git a/internal/admin/static/editor.js b/internal/admin/static/editor.js index 37bc797..35bdbe6 100644 --- a/internal/admin/static/editor.js +++ b/internal/admin/static/editor.js @@ -233,3 +233,32 @@ function esc(s) { return String(s || '').replace(/&/g,'&').replace(/</g,'< function escAttr(s) { return String(s || '').replace(/&/g,'&').replace(/"/g,'"'); } window.refreshPage = function() { loadList(); if (currentID) loadItem(currentID); }; + +function renderMoveBar() { + var dirs = Object.keys(treeData.dirs).sort(); + var html = '<div class="add-section-bar">'; + html += '<select id="moveDirSelect">'; + html += '<option value="">Move to directory...</option>'; + html += '<option value="">Root</option>'; + dirs.forEach(function(dir) { + html += '<option value="' + escAttr(dir) + '">' + esc(dir) + '</option>'; + }); + html += '</select>'; + html += '<button class="btn btn-primary btn-sm" onclick="moveToDirectory()">Move</button>'; + html += '</div>'; + return html; +} + +function moveToDirectory() { + var sel = $('#moveDirSelect'); + var idx = sel.selectedIndex; + if (idx === 0) return; + var dir = sel.value; + if (!confirm('Move ' + currentID + ' to ' + (dir || 'Root') + '?')) return; + API.post('/api/' + editorType + '/move', {id: currentID, dir: dir}).then(function() { + notify('Moved ' + currentID + ' to ' + (dir || 'Root'), 'success'); + updateUndoBar(); + loadList(); + loadItem(currentID); + }).catch(function(e) { notify('Move failed: ' + e.message, 'error'); }); +} diff --git a/internal/admin/static/hazardeditor.js b/internal/admin/static/hazardeditor.js index b07456e..ad317cc 100644 --- a/internal/admin/static/hazardeditor.js +++ b/internal/admin/static/hazardeditor.js @@ -90,6 +90,7 @@ function renderHazardEditor(id, data) { html += '</select>'; html += '<button class="btn btn-primary btn-sm" onclick="addSection()">Add</button>'; html += '</div>'; + html += renderMoveBar(); html += '</div>'; html += '<div class="tab-content" id="tabYaml" style="display:none">'; @@ -117,8 +118,8 @@ function renderCard(sec, data) { var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; - h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; h += '<span class="obj-card-label">' + sec.label + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>'; if (!sec.always) { h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>'; } diff --git a/internal/admin/static/itemeditor.js b/internal/admin/static/itemeditor.js index 641f064..ce28581 100644 --- a/internal/admin/static/itemeditor.js +++ b/internal/admin/static/itemeditor.js @@ -200,6 +200,7 @@ function renderItemEditor(id, data) { html += '</select>'; html += '<button class="btn btn-primary btn-sm" onclick="addSection()">Add</button>'; html += '</div>'; + html += renderMoveBar(); html += '</div>'; html += '<div class="tab-content" id="tabYaml" style="display:none">'; @@ -237,8 +238,8 @@ function renderCard(sec, data) { var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; - h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; h += '<span class="obj-card-label">' + sec.label + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>'; if (!sec.always) { h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>'; } diff --git a/internal/admin/static/mobeditor.js b/internal/admin/static/mobeditor.js index 4260e49..802b48a 100644 --- a/internal/admin/static/mobeditor.js +++ b/internal/admin/static/mobeditor.js @@ -175,6 +175,7 @@ function renderMobEditor(id, data) { html += '<button class="btn btn-primary btn-sm" onclick="mobAddSection()">Add</button>'; html += '</div>'; + html += renderMoveBar(); html += '</div>'; html += '<div class="tab-content" id="tabYaml" style="display:none">'; @@ -212,8 +213,8 @@ function renderMobCard(sec, data) { var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; h += '<div class="obj-card-header" onclick="toggleMobCard(\'' + sec.id + '\')">'; h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; - h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleMobCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; h += '<span class="obj-card-label">' + sec.label + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleMobCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>'; if (!sec.always) { h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeMobSection(\'' + sec.id + '\')">X</button>'; } diff --git a/internal/admin/static/moduleeditor.js b/internal/admin/static/moduleeditor.js index 128148d..1e7de30 100644 --- a/internal/admin/static/moduleeditor.js +++ b/internal/admin/static/moduleeditor.js @@ -91,6 +91,7 @@ function renderModuleEditor(id, data) { html += '</select>'; html += '<button class="btn btn-primary btn-sm" onclick="addSection()">Add</button>'; html += '</div>'; + html += renderMoveBar(); html += '</div>'; html += '<div class="tab-content" id="tabYaml" style="display:none">'; @@ -118,8 +119,8 @@ function renderCard(sec, data) { var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; - h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; h += '<span class="obj-card-label">' + sec.label + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>'; if (!sec.always) { h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>'; } diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js index 3cbb37a..c51628f 100644 --- a/internal/admin/static/objecteditor.js +++ b/internal/admin/static/objecteditor.js @@ -177,6 +177,7 @@ function renderObjectEditor(id, data) { html += '<button class="btn btn-primary btn-sm" onclick="addSection()">Add</button>'; html += '</div>'; + html += renderMoveBar(); html += '</div>'; html += '<div class="tab-content" id="tabYaml" style="display:none">'; @@ -226,8 +227,8 @@ function renderCard(sec, data) { var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; - h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; h += '<span class="obj-card-label">' + sec.label + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>'; if (!sec.always) { h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>'; } diff --git a/internal/admin/static/techeditor.js b/internal/admin/static/techeditor.js index 3d74254..4849fb1 100644 --- a/internal/admin/static/techeditor.js +++ b/internal/admin/static/techeditor.js @@ -80,6 +80,7 @@ function renderTechEditor(id, data) { html += renderCard(sec, data); }); html += '</div>'; + html += renderMoveBar(); html += '</div>'; html += '<div class="tab-content" id="tabYaml" style="display:none">'; @@ -107,8 +108,8 @@ function renderCard(sec, data) { var h = '<div class="obj-card' + fullClass + '" data-card="' + sec.id + '">'; h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">'; h += '<span class="obj-card-arrow">' + (collapsed ? '▶' : '▼') + '</span>'; - h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u229F' : '\u229E') + '</button>'; h += '<span class="obj-card-label">' + sec.label + '</span>'; + h += '<button class="btn btn-sm obj-card-expand" onclick="event.stopPropagation();toggleCardWidth(\'' + sec.id + '\')" title="' + (isFull ? 'Half width' : 'Full width') + '">' + (isFull ? '\u2190' : '\u2192') + '</button>'; h += '</div>'; if (!collapsed) { h += '<div class="obj-card-body">'; diff --git a/internal/admin/yaml_util.go b/internal/admin/yaml_util.go index 4d0a814..465be38 100644 --- a/internal/admin/yaml_util.go +++ b/internal/admin/yaml_util.go @@ -2,6 +2,7 @@ package admin import ( "bytes" + "fmt" "log" "os" "path/filepath" @@ -154,3 +155,46 @@ func backupFile(path string) []byte { func snapshotFile(path string) ([]byte, error) { return os.ReadFile(path) } + +func findEntityPath(dataDir, subdir, id string) (string, error) { + _, path, err := readYAMLFile(dataDir, subdir, id) + if err == nil { + return path, nil + } + _, path, err = findYAMLFileInSubdirs(dataDir, subdir, id) + return path, err +} + +func moveEntityFile(dataDir, subdir, id, targetDir string) (string, string, []byte, error) { + oldPath, err := findEntityPath(dataDir, subdir, id) + if err != nil { + return "", "", nil, fmt.Errorf("entity not found: %s/%s", subdir, id) + } + data, err := os.ReadFile(oldPath) + if err != nil { + return "", "", nil, fmt.Errorf("read: %w", err) + } + var newPath string + if targetDir == "" { + newPath = filepath.Join(dataDir, subdir, id+".yaml") + } else { + destDir := filepath.Join(dataDir, subdir, targetDir) + if _, err := os.Stat(destDir); os.IsNotExist(err) { + return "", "", nil, fmt.Errorf("directory does not exist: %s", targetDir) + } + newPath = filepath.Join(destDir, id+".yaml") + } + if oldPath == newPath { + return "", "", nil, fmt.Errorf("already in target directory") + } + if _, err := os.Stat(newPath); err == nil { + return "", "", nil, fmt.Errorf("entity already exists in target directory") + } + if err := os.WriteFile(newPath, data, 0644); err != nil { + return "", "", nil, fmt.Errorf("write: %w", err) + } + if err := os.Remove(oldPath); err != nil { + return "", "", nil, fmt.Errorf("remove old: %w", err) + } + return oldPath, newPath, data, nil +} |
