New mobile layout, new regular screen, added the ability to edit directory names from settings

This commit is contained in:
2026-08-22 08:37:25 -04:00
parent fa00a0b0c4
commit c92268e04c
4 changed files with 370 additions and 47 deletions
+90
View File
@@ -557,6 +557,48 @@ def set_path_hidden(path: str, hidden: bool) -> List[str]:
return result
def _rebase_prefix(value: str, old_abs: str, new_abs: str) -> str:
"""If `value` equals or is nested under `old_abs`, rewrite that prefix to `new_abs`."""
if value == old_abs:
return new_abs
if value.startswith(old_abs + os.sep):
return new_abs + value[len(old_abs):]
return value
def rebase_library_path(old_abs: str, new_abs: str) -> None:
"""
After a directory in the library is renamed/moved on disk, rewrite any
stored absolute paths that pointed inside it (hidden_paths.json,
recent_views.json's course_path) so curation and Recently Viewed don't
silently go stale. Lesson-level progress needs no rebasing - it lives
inside the directory itself, keyed by paths relative to it, so it moves
with the rename automatically.
"""
hidden = get_hidden_paths()
rebased_hidden = [_rebase_prefix(p, old_abs, new_abs) for p in hidden]
if rebased_hidden != hidden:
os.makedirs(DATA_DIR, exist_ok=True)
with open(HIDDEN_PATHS_FILE, 'w') as f:
json.dump(sorted(set(rebased_hidden)), f, indent=2)
views = get_recent_views()
changed = False
for entry in views:
course_path = entry.get('course_path', '')
rebased = _rebase_prefix(course_path, old_abs, new_abs)
if rebased != course_path:
entry['course_path'] = rebased
changed = True
if changed:
try:
os.makedirs(DATA_DIR, exist_ok=True)
with open(RECENT_VIEWS_FILE, 'w') as f:
json.dump(views, f, indent=2)
except OSError as e:
print(f"Could not rebase recent views after rename: {e}")
def _contains_visible_course(directory: Path, hidden_set: set) -> bool:
"""
Whether `directory` leads to at least one course that isn't curated
@@ -1037,6 +1079,54 @@ def set_hidden_path_api():
return jsonify({'success': True, 'hidden_paths': updated})
@app.route('/api/rename-path', methods=['POST'])
def rename_path_api():
"""Rename a course/folder directory in the library, in place on disk."""
global current_course
data = request.json or {}
path = data.get('path', '')
new_name = (data.get('new_name') or '').strip()
if not path:
return jsonify({'error': 'path is required'}), 400
if not new_name:
return jsonify({'error': 'New name cannot be empty'}), 400
if '/' in new_name or '\\' in new_name or '\x00' in new_name or new_name in ('.', '..'):
return jsonify({'error': 'New name cannot contain path separators'}), 400
if new_name.startswith('.'):
return jsonify({'error': 'New name cannot start with a dot'}), 400
library_root = os.path.abspath(get_library_root())
old_abs = os.path.abspath(path)
if not (old_abs == library_root or old_abs.startswith(library_root + os.sep)):
return jsonify({'error': 'Path outside library root'}), 403
if old_abs == library_root:
return jsonify({'error': 'Cannot rename the library root itself'}), 400
if not os.path.isdir(old_abs):
return jsonify({'error': 'Directory not found'}), 404
new_abs = os.path.join(os.path.dirname(old_abs), new_name)
if os.path.exists(new_abs):
return jsonify({'error': f'"{new_name}" already exists here'}), 409
try:
os.rename(old_abs, new_abs)
except OSError as e:
return jsonify({'error': f'Rename failed: {e}'}), 500
rebase_library_path(old_abs, new_abs)
active_course_reset = False
if current_course is not None:
course_abs = os.path.abspath(current_course.path)
if course_abs == old_abs or course_abs.startswith(old_abs + os.sep):
current_course = None
active_course_reset = True
return jsonify({'success': True, 'new_path': new_abs, 'active_course_reset': active_course_reset})
@app.route('/settings')
def settings_page():
"""Render the display-settings page."""