Skip to content

Commit 2f1381a

Browse files
committed
Viewer: More tools from game picker (rename, move)
1 parent e076b6b commit 2f1381a

4 files changed

Lines changed: 358 additions & 5 deletions

File tree

.cursor/rules/viewer.mdc

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,10 @@ The application has two main pages:
271271
* **Batch Operations**:
272272
- "Select All" checkbox to toggle all games
273273
- Styled action dropdown with custom arrow and hover effects for batch operations on selected games
274-
- Copy pathnames copies selected game paths as space-separated string to clipboard
274+
- **Copy paths**: Copies selected game paths as space-separated string to clipboard
275+
- **Add suffix**: Prompts user for suffix text and adds it to folder names with dot separator (e.g., `experiment1` → `experiment1.backup`)
276+
- **Copy foldernames**: Copies only folder names (not full paths) as space-separated string to clipboard
277+
- **Move to subfolder**: Prompts user for subfolder name and moves all selected folders into it (creates subfolder if needed)
275278

276279
### Layout
277280

@@ -347,6 +350,36 @@ The trajectory template displays:
347350

348351
**Content Preview**: Long message content (>5 lines) shows a preview with expand/collapse functionality to keep the interface manageable.
349352

353+
## Multi-Select Actions Specification
354+
355+
The game picker includes four batch operations for selected game folders, accessible via the action dropdown:
356+
357+
### 1. Copy Paths (`copy-paths`)
358+
- Copies full relative paths of selected games as space-separated string
359+
- Example: `fuchur/experiment1 fuchur/experiment2`
360+
- Uses modern clipboard API with execCommand fallback for non-HTTPS environments
361+
362+
### 2. Add Suffix (`add-suffix`)
363+
- Prompts user for suffix text using browser's native `prompt()`
364+
- Adds suffix with dot separator: `experiment1` → `experiment1.backup`
365+
- Server endpoint: `POST /rename-folders` with `action: "add-suffix"`
366+
- Validates folder existence and prevents name conflicts
367+
- Shows success message and refreshes page after operation
368+
369+
### 3. Copy Foldernames (`copy-foldernames`)
370+
- Copies only folder names (not full paths) as space-separated string
371+
- Example: From `fuchur/experiment1` copies just `experiment1`
372+
- Uses modern clipboard API with execCommand fallback for non-HTTPS environments
373+
374+
### 4. Move to Subfolder (`move-to-subfolder`)
375+
- Prompts user for subfolder name using browser's native `prompt()`
376+
- Moves selected folders to new/existing subfolder in their parent directory
377+
- Server endpoint: `POST /move-to-subfolder`
378+
- Creates subfolder if it doesn't exist
379+
- Validates subfolder name (no path separators)
380+
- Shows success message and refreshes page after operation
381+
382+
350383
### Important
351384

352385
* Always use the player name from the metadata.json file, do not assume players are called p1, p2, etc.

codeclash/viewer/app.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,4 +647,144 @@ def load_readme():
647647
return jsonify({"success": False, "error": str(e)})
648648

649649

650+
@app.route("/rename-folders", methods=["POST"])
651+
def rename_folders():
652+
"""Add suffix to selected folders"""
653+
try:
654+
data = request.get_json()
655+
action = data.get("action")
656+
paths = data.get("paths", [])
657+
suffix = data.get("suffix", "")
658+
659+
if not paths:
660+
return jsonify({"success": False, "error": "No paths provided"})
661+
662+
if action != "add-suffix":
663+
return jsonify({"success": False, "error": "Invalid action"})
664+
665+
if not suffix:
666+
return jsonify({"success": False, "error": "No suffix provided"})
667+
668+
successful_renames = []
669+
failed_renames = []
670+
671+
for relative_path in paths:
672+
try:
673+
# Get the full path
674+
old_path = LOG_BASE_DIR / relative_path
675+
676+
if not old_path.exists():
677+
failed_renames.append(f"{relative_path}: does not exist")
678+
continue
679+
680+
# Create new path with suffix
681+
parent_dir = old_path.parent
682+
old_name = old_path.name
683+
new_name = f"{old_name}.{suffix}"
684+
new_path = parent_dir / new_name
685+
686+
# Check if target already exists
687+
if new_path.exists():
688+
failed_renames.append(f"{relative_path}: target already exists")
689+
continue
690+
691+
# Perform the rename
692+
old_path.rename(new_path)
693+
successful_renames.append(f"{relative_path}{old_name}.{suffix}")
694+
695+
except Exception as e:
696+
failed_renames.append(f"{relative_path}: {str(e)}")
697+
698+
if failed_renames:
699+
error_msg = "Some renames failed:\n" + "\n".join(failed_renames)
700+
if successful_renames:
701+
error_msg += "\n\nSuccessful renames:\n" + "\n".join(successful_renames)
702+
return jsonify({"success": False, "error": error_msg})
703+
704+
return jsonify(
705+
{
706+
"success": True,
707+
"message": f"Successfully renamed {len(successful_renames)} folder(s)",
708+
"renamed": successful_renames,
709+
}
710+
)
711+
712+
except Exception as e:
713+
return jsonify({"success": False, "error": str(e)})
714+
715+
716+
@app.route("/move-to-subfolder", methods=["POST"])
717+
def move_to_subfolder():
718+
"""Move selected folders to a new subfolder"""
719+
try:
720+
data = request.get_json()
721+
paths = data.get("paths", [])
722+
subfolder_name = data.get("subfolder", "")
723+
724+
if not paths:
725+
return jsonify({"success": False, "error": "No paths provided"})
726+
727+
if not subfolder_name:
728+
return jsonify({"success": False, "error": "No subfolder name provided"})
729+
730+
# Validate subfolder name (no path separators, etc.)
731+
if "/" in subfolder_name or "\\" in subfolder_name or ".." in subfolder_name:
732+
return jsonify({"success": False, "error": "Invalid subfolder name"})
733+
734+
successful_moves = []
735+
failed_moves = []
736+
737+
for relative_path in paths:
738+
try:
739+
# Get the full path
740+
old_path = LOG_BASE_DIR / relative_path
741+
742+
if not old_path.exists():
743+
failed_moves.append(f"{relative_path}: does not exist")
744+
continue
745+
746+
# Determine where to create the subfolder
747+
parent_dir = old_path.parent
748+
subfolder_path = parent_dir / subfolder_name
749+
750+
# Create subfolder if it doesn't exist
751+
subfolder_path.mkdir(exist_ok=True)
752+
753+
# Create new path inside subfolder
754+
folder_name = old_path.name
755+
new_path = subfolder_path / folder_name
756+
757+
# Check if target already exists
758+
if new_path.exists():
759+
failed_moves.append(f"{relative_path}: target already exists in subfolder")
760+
continue
761+
762+
# Perform the move
763+
old_path.rename(new_path)
764+
765+
# Calculate the new relative path for display
766+
new_relative_path = str(new_path.relative_to(LOG_BASE_DIR))
767+
successful_moves.append(f"{relative_path}{new_relative_path}")
768+
769+
except Exception as e:
770+
failed_moves.append(f"{relative_path}: {str(e)}")
771+
772+
if failed_moves:
773+
error_msg = "Some moves failed:\n" + "\n".join(failed_moves)
774+
if successful_moves:
775+
error_msg += "\n\nSuccessful moves:\n" + "\n".join(successful_moves)
776+
return jsonify({"success": False, "error": error_msg})
777+
778+
return jsonify(
779+
{
780+
"success": True,
781+
"message": f"Successfully moved {len(successful_moves)} folder(s) to subfolder '{subfolder_name}'",
782+
"moved": successful_moves,
783+
}
784+
)
785+
786+
except Exception as e:
787+
return jsonify({"success": False, "error": str(e)})
788+
789+
650790
# Use run_viewer.py to launch the application

0 commit comments

Comments
 (0)