<?php
// =======================================================================
//  R-Scope 2.0 — File Manager (standalone)
//  Browse, upload, field photo annotations (.rnote), pixel tools
// =======================================================================
define('APP_NAME', 'R-Scope');
define('APP_SCOPE', 'R-Scope');
define('APP_VERSION', '2.2.2');
define('APP_UPDATE_DATE', '2026/06/02');
define(
    'APP_DESCRIPTION',
    '管理者向け — 資料の整理・アップロード・保護（.protected）・画像注釈（.rnote）'
);

// Buffer output to prevent JSON corruption
if (ob_get_level() == 0) ob_start();

header('X-Frame-Options: SAMEORIGIN');

require_once __DIR__ . '/lib/boardroom-auth.php';

boardroom_session_start();

if (isset($_GET['logout'])) {
    boardroom_logout();
    header('Location: R-Scope.php');
    exit;
}

// 1. Root Configuration
if (!isset($docRoot)) {
    $docRoot = rtrim(realpath(__DIR__), DIRECTORY_SEPARATOR);
}
if ($docRoot === false || !is_dir($docRoot)) {
    ob_end_clean(); http_response_code(500); exit(json_encode(['status'=>'error', 'message'=>'Invalid root']));
}

// 2. API Handler
if (isset($_REQUEST['action'])) {
    error_reporting(0);
    ini_set('display_errors', 0);
    while (ob_get_level()) ob_end_clean();

    header('Content-Type: application/json; charset=utf-8');
    $action = (string)$_REQUEST['action'];

    $rscopePublicActions = ['boardroom_login', 'boardroom_status'];
    if (!in_array($action, $rscopePublicActions, true) && !boardroom_is_logged_in()) {
        http_response_code(403);
        echo json_encode(['status' => 'error', 'message' => 'Login required', 'login' => true], JSON_UNESCAPED_UNICODE);
        exit;
    }
    
    function jsonOut($v, $code=200) { 
        http_response_code($code); 
        echo json_encode($v, JSON_UNESCAPED_UNICODE); 
        exit; 
    }
    function safe_join($root, $rel) {
        $rel = trim((string)$rel, " \t\n\r\0\x0B/");
        if ($rel === '') return $root;
        if (strpos($rel, '..') !== false) return false;
        $path = $root . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $rel);
        $real = realpath($path);
        if ($real && strpos($real, $root) === 0) return $real;
        $parent = dirname($path);
        if (realpath($parent) && strpos(realpath($parent), $root) === 0) return $path;
        return false;
    }

    try {
        // LIST
        if ($action === 'list') {
            $dirRel = boardroom_norm_rel($_GET['dir'] ?? '');
            if (!boardroom_can_access($dirRel)) {
                jsonOut(['status' => 'error', 'message' => 'Password Required', 'login' => true]);
            }
            $real = safe_join($docRoot, $_GET['dir']??'');
            if (!$real || !is_dir($real)) jsonOut([]);
            $items = [];
            foreach (scandir($real) as $e) {
                if ($e === '.' || $e === '..') continue;
                if (boardroom_is_browse_hidden($e, $dirRel, $real)) continue;
                $full = $real . DIRECTORY_SEPARATOR . $e;
                $is_dir = is_dir($full);
                $rel = str_replace('\\', '/', ltrim(substr($full, strlen($docRoot)), '/\\'));
                $size = $is_dir ? '-' : (filesize($full) < 1024 ? filesize($full).' B' : round(filesize($full)/1024,1).' KB');
                $perms = substr(sprintf('%o', fileperms($full)), -4);
$items[] = [
    'name'      => $e,
    'path'      => $rel,
    'is_dir'    => $is_dir,
    'type'      => $is_dir ? 'dir' : 'file',
    'size'      => $size,
    'mtime'     => date('Y-m-d H:i:s', filemtime($full)),
    'perms'     => $perms,
    'protected' => $is_dir && boardroom_is_protected_dir($full),
];
            }
            jsonOut($items);
        }
        // TREE
        if ($action === 'tree') {
            function get_tree($base, $root, $rel='') {
                $full = $base . ($rel ? DIRECTORY_SEPARATOR . $rel : "");
                if (!is_dir($full)) return [];
                $nodes = [];
                foreach (scandir($full) as $e) {
                    if ($e === '.' || $e === '..') continue;
                    if (boardroom_is_browse_hidden($e, $rel, $full)) continue;
                    $childRel = $rel ? $rel.'/'.$e : $e;
                    $fullChild = $full.DIRECTORY_SEPARATOR.$e;
                    $is_dir = is_dir($fullChild);
                    $webPath = str_replace('\\', '/', $childRel);
                    $node = ['name'=>$e, 'path'=>$webPath, 'is_dir'=>$is_dir, 'mtime'=>date('Y-m-d H:i:s', filemtime($fullChild))];
                    if ($is_dir) { $node['children'] = get_tree($base, $root, $childRel); }
                    $nodes[] = $node;
                }
                usort($nodes, function($a,$b){ return ($a['is_dir']!==$b['is_dir']) ? ($b['is_dir']?1:-1) : strnatcasecmp($a['name'], $b['name']); });
                return $nodes;
            }
            jsonOut(get_tree($docRoot, $docRoot));
        }
        // GET_FILE
        if ($action === 'get_file') {
            $pathRel = boardroom_norm_rel($_GET['path'] ?? '');
            if (!boardroom_can_access($pathRel)) {
                http_response_code(403);
                exit('Password Required');
            }
            $real = safe_join($docRoot, $_GET['path']??'');
            if (!$real || !is_file($real)) { http_response_code(404); exit; }
            $mime = mime_content_type($real) ?: 'application/octet-stream';
            header("Content-Type: $mime");
            readfile($real); exit;
        }
        // GET_META (Lister 互換: Ver / 区分 / 概要 / メモ)
        if ($action === 'get_meta') {
            require_once __DIR__ . '/data/fl-meta.php';
            $pathRel = boardroom_norm_rel($_GET['path'] ?? '');
            if (!boardroom_can_access($pathRel)) {
                jsonOut(['status' => 'error', 'message' => 'Password Required', 'login' => true]);
            }
            $real = safe_join($docRoot, $_GET['path'] ?? '');
            if (!$real || !is_file($real)) {
                jsonOut(['status' => 'error', 'message' => 'Not found']);
            }
            $meta = fl_read_meta($real);
            $mtime = filemtime($real) ?: 0;
            $name = basename($real);
            $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
            $ver = $meta['APP_VERSION'] !== '' ? $meta['APP_VERSION'] : fl_display_version($meta, $name, $mtime);
            $norm = str_replace('\\', '/', $pathRel);
            $listerPath = '';
            if (preg_match('#^data(?:/|$)#i', $norm)) {
                $listerPath = preg_replace('#^data/?#i', '', $norm);
            }
            $bytes = (int) filesize($real);
            jsonOut([
                'status' => 'ok',
                'name' => $name,
                'ext' => $ext,
                'ver' => $ver,
                'category' => $meta['APP_CATEGORY'],
                'description' => $meta['APP_DESCRIPTION'],
                'memo' => $meta['APP_MEMO'],
                'size' => $bytes,
                'size_human' => $bytes < 1024 ? $bytes . ' B' : round($bytes / 1024, 1) . ' KB',
                'mtime' => date('Y-m-d H:i', $mtime),
                'meta_editable' => fl_is_meta_editable($ext),
                'lister_path' => $listerPath,
            ]);
        }
        // GET_RNOTE (sidecar annotations)
        if ($action === 'get_rnote') {
            $pathRel = $_GET['path'] ?? '';
            $imgPath = safe_join($docRoot, $pathRel);
            if (!$imgPath || !is_file($imgPath)) {
                jsonOut(['status'=>'error', 'message'=>'Image not found']);
            }
            $rnotePath = $imgPath . '.rnote';
            if (!is_file($rnotePath)) {
                jsonOut(['status'=>'ok', 'data'=>null]);
            }
            $raw = file_get_contents($rnotePath);
            $data = json_decode($raw, true);
            if (!is_array($data)) {
                jsonOut(['status'=>'error', 'message'=>'Invalid .rnote file']);
            }
            jsonOut(['status'=>'ok', 'data'=>$data]);
        }
        // DOWNLOAD
        if ($action === 'download') {
            $pathRel = boardroom_norm_rel($_GET['path'] ?? '');
            if (!boardroom_can_access($pathRel)) {
                jsonOut(['status' => 'error', 'message' => 'Password Required', 'login' => true]);
            }
            $real = safe_join($docRoot, $_GET['path']??'');
            if ($real && is_file($real)) {
                if (function_exists('apache_setenv')) @apache_setenv('no-gzip', 1);
                @ini_set('zlib.output_compression', 'Off');
                while (ob_get_level()) ob_end_clean();
                header('Content-Type: application/octet-stream');
                header("Content-Disposition: attachment; filename*=UTF-8''".rawurlencode(basename($real)));
                header('Content-Length: '.filesize($real));
                readfile($real); exit;
            }
        }
// ZIP COMPRESS
if ($action === 'zip_compress') {

    $target = $_POST['path'] ?? '';
    $real = safe_join($docRoot, $target);

    if (!$real || !file_exists($real)) {
        jsonOut(['status'=>'error','message'=>'Invalid target']);
    }

    // ZIP 出力ファイル名を決定
    $zipBase = basename($real);
    $zipName = $zipBase . '.zip';
    $zipPath = dirname($real) . DIRECTORY_SEPARATOR . $zipName;

    // 衝突回避
    $i = 1;
    while (file_exists($zipPath)) {
        $zipName = $zipBase . "($i).zip";
        $zipPath = dirname($real) . DIRECTORY_SEPARATOR . $zipName;
        $i++;
    }

    // ZipArchive available?
    if (class_exists('ZipArchive')) {
        $zip = new ZipArchive();
        if ($zip->open($zipPath, ZipArchive::CREATE) !== TRUE) {
            jsonOut(['status'=>'error','message'=>'Cannot create ZIP']);
        }

        // ファイル圧縮
        if (is_file($real)) {
            $zip->addFile($real, basename($real));
        }
        // フォルダ圧縮
        elseif (is_dir($real)) {

            $baseLen = strlen(dirname($real)) + 1;

            $iterator = new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($real, FilesystemIterator::SKIP_DOTS),
                RecursiveIteratorIterator::SELF_FIRST
            );

            foreach ($iterator as $file) {
                $filePath = $file->getRealPath();
                $localName = substr($filePath, $baseLen);

                if ($file->isDir()) {
                    $zip->addEmptyDir($localName);
                } else {
                    $zip->addFile($filePath, $localName);
                }
            }
        }

        $zip->close();
        jsonOut(['status'=>'ok', 'zip'=>basename($zipPath)]);
    }
    else {
        jsonOut(['status'=>'error','message'=>'ZipArchive not supported']);
    }
}


// ZIP EXTRACT
if ($action === 'zip_extract') {

    $target = $_POST['path'] ?? '';
    $real = safe_join($docRoot, $target);

    if (!$real || !is_file($real)) {
        jsonOut(['status'=>'error','message'=>'Invalid ZIP']);
    }

    // ZIP名から展開フォルダを決定
    $base = basename($real, '.zip');
    $extractRoot = dirname($real) . DIRECTORY_SEPARATOR . $base;

    // 衝突回避
    $i = 1;
    $tmp = $extractRoot;
    while (file_exists($tmp)) {
        $tmp = $extractRoot . "($i)";
        $i++;
    }
    $extractRoot = $tmp;

    mkdir($extractRoot, 0755, true);

    if (!class_exists('ZipArchive')) {
        jsonOut(['status'=>'error','message'=>'ZipArchive not supported']);
    }

    $zip = new ZipArchive();
    if ($zip->open($real) !== TRUE) {
        jsonOut(['status'=>'error','message'=>'Cannot open ZIP']);
    }

    $zip->extractTo($extractRoot);
    $zip->close();

    jsonOut(['status'=>'ok', 'folder'=>basename($extractRoot)]);
}
		
// POST Actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    if ($action === 'mkdir') {
        $path = safe_join($docRoot, ($_POST['dir']??'').'/'.($_POST['name']??''));
        if ($path && !file_exists($path) && mkdir($path, 0755, true)) jsonOut(['status'=>'ok']);
    }

    if ($action === 'create_file') {
        $p = $_POST['path'] ?? (($_POST['dir']??'').'/'.($_POST['name']??''));
        $path = safe_join($docRoot, $p);
        $content = $_POST['content'] ?? '';
        if ($path && file_put_contents($path, $content) !== false) jsonOut(['status'=>'ok']);
        jsonOut(['status'=>'error', 'message'=>'Save failed']);
    }

    if ($action === 'delete') {
        $path = safe_join($docRoot, $_POST['path']??'');
        if ($path && file_exists($path)) {
            (is_dir($path) ? rmdir($path) : unlink($path))
                ? jsonOut(['status'=>'ok'])
                : jsonOut(['status'=>'error']);
        }
    }

    if ($action === 'rename') {
        $src = safe_join($docRoot, $_POST['src']??'');
        $dst = safe_join($docRoot, $_POST['dst']??'');
        if ($src && $dst && rename($src, $dst)) jsonOut(['status'=>'ok']);
    }

    if ($action === 'upload') {
        $dir = safe_join($docRoot, $_POST['dir'] ?? '');
        if (!$dir || !is_dir($dir)) {
            jsonOut(['status'=>'error', 'message'=>'Invalid folder']);
        }
        if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
            jsonOut(['status'=>'error', 'message'=>'Upload failed']);
        }

        $rel = trim(str_replace('\\', '/', (string)($_POST['relative_path'] ?? '')), '/');
        if ($rel === '') {
            $rel = basename($_FILES['file']['name']);
        }
        if (strpos($rel, '..') !== false) {
            jsonOut(['status'=>'error', 'message'=>'Invalid path']);
        }

        $rel = preg_replace('#/+#', '/', $rel);
        $parts = array_filter(explode('/', $rel), function ($p) {
            return $p !== '' && $p !== '.';
        });
        $rel = implode('/', $parts);
        if ($rel === '') {
            jsonOut(['status'=>'error', 'message'=>'Invalid filename']);
        }

        $target = $dir;
        foreach ($parts as $part) {
            $target .= DIRECTORY_SEPARATOR . $part;
        }
        $targetDir = dirname($target);
        $realDir = realpath($targetDir);
        $realRoot = realpath($dir);
        if ($realDir === false) {
            if (!@mkdir($targetDir, 0755, true)) {
                jsonOut(['status'=>'error', 'message'=>'Cannot create folder']);
            }
            $realDir = realpath($targetDir);
        }
        if (!$realDir || !$realRoot || strpos($realDir, $realRoot) !== 0) {
            jsonOut(['status'=>'error', 'message'=>'Path not allowed']);
        }

        if (move_uploaded_file($_FILES['file']['tmp_name'], $target)) {
            jsonOut(['status'=>'ok', 'path'=>$rel]);
        }
        jsonOut(['status'=>'error', 'message'=>'Save failed']);
    }

    if ($action === 'chmod') {
        $path = safe_join($docRoot, $_POST['path']??'');
        if ($path && chmod($path, octdec($_POST['mode']??''))) jsonOut(['status'=>'ok']);
    }
    if ($action === 'protect') {
        $rel = boardroom_norm_rel($_POST['path'] ?? '');
        $r = boardroom_protect_folder($rel);
        jsonOut($r['ok'] ? ['status' => 'ok'] : ['status' => 'error', 'message' => $r['message'] ?? '']);
    }

    if ($action === 'unprotect') {
        $rel = boardroom_norm_rel($_POST['path'] ?? '');
        $r = boardroom_unprotect_folder($rel);
        jsonOut($r['ok'] ? ['status' => 'ok'] : ['status' => 'error', 'message' => $r['message'] ?? '']);
    }

    if ($action === 'set_boardroom_password') {
        $new = (string)($_POST['password'] ?? '');
        $r = boardroom_set_password($new);
        jsonOut($r['ok'] ? ['status' => 'ok'] : ['status' => 'error', 'message' => $r['message'] ?? '']);
    }

    if ($action === 'boardroom_login') {
        $r = boardroom_login((string)($_POST['password'] ?? ''));
        jsonOut($r['ok'] ? ['status' => 'ok'] : ['status' => 'error', 'message' => $r['message'] ?? '']);
    }

    if ($action === 'boardroom_status') {
        jsonOut([
            'status' => 'ok',
            'has_password' => boardroom_has_password(),
            'logged_in' => boardroom_is_logged_in(),
        ]);
    }
    // -------------------------
    // Save with backup (.bak) - new action
    // -------------------------
    if ($action === 'save_with_backup') {
        $pathRel = $_POST['path'] ?? '';
        $path = safe_join($docRoot, $pathRel);
        if (!$path) jsonOut(['status'=>'error','message'=>'Invalid path']);

        $content = $_POST['content'] ?? '';

        // make backup if exists
        if (file_exists($path)) {
            @copy($path, $path . '.bak');
        } else {
            // ensure parent exists for new files
            $dir = dirname($path);
            if (!is_dir($dir)) @mkdir($dir, 0755, true);
        }

        if (file_put_contents($path, $content) !== false) {
            jsonOut(['status'=>'ok']);
        } else {
            jsonOut(['status'=>'error','message'=>'Write failed']);
        }
    }

    // -------------------------
    // Undo restore from .bak
    // -------------------------
    if ($action === 'undo_restore') {
        $pathRel = $_POST['path'] ?? '';
        $path = safe_join($docRoot, $pathRel);
        if (!$path) jsonOut(['status'=>'error','message'=>'Invalid path']);

        $bak = $path . '.bak';
        if (!file_exists($bak)) jsonOut(['status'=>'error','message'=>'No backup']);

        if (@copy($bak, $path)) {
            jsonOut(['status'=>'ok']);
        } else {
            jsonOut(['status'=>'error','message'=>'Restore failed']);
        }
    }

    // Save edited image (base64 data URL from canvas)
    if ($action === 'save_image') {
        $pathRel = $_POST['path'] ?? '';
        $path = safe_join($docRoot, $pathRel);
        if (!$path) jsonOut(['status'=>'error', 'message'=>'Invalid path']);

        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
        if (!in_array($ext, $allowed, true)) {
            jsonOut(['status'=>'error', 'message'=>'Not a raster image']);
        }

        $imageData = (string)($_POST['image'] ?? '');
        if (!preg_match('/^data:image\/(\w+);base64,(.+)$/s', $imageData, $m)) {
            jsonOut(['status'=>'error', 'message'=>'Invalid image data']);
        }

        $bin = base64_decode($m[2], true);
        if ($bin === false) jsonOut(['status'=>'error', 'message'=>'Decode failed']);

        if (file_exists($path)) @copy($path, $path . '.bak');

        if (file_put_contents($path, $bin) !== false) {
            jsonOut(['status'=>'ok']);
        }
        jsonOut(['status'=>'error', 'message'=>'Write failed']);
    }

    // Save image as new file (no overwrite)
    if ($action === 'save_image_as') {
        $pathRel = trim(str_replace('\\', '/', (string)($_POST['path'] ?? '')), '/');
        if ($pathRel === '' || strpos($pathRel, '..') !== false) {
            jsonOut(['status'=>'error', 'message'=>'Invalid path']);
        }
        $path = safe_join($docRoot, $pathRel);
        if (!$path) jsonOut(['status'=>'error', 'message'=>'Invalid path']);

        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
        if (!in_array($ext, $allowed, true)) {
            jsonOut(['status'=>'error', 'message'=>'Not a raster image']);
        }

        if (file_exists($path)) {
            jsonOut(['status'=>'error', 'message'=>'File already exists']);
        }

        $parent = dirname($path);
        if (!is_dir($parent)) {
            if (!@mkdir($parent, 0755, true)) {
                jsonOut(['status'=>'error', 'message'=>'Cannot create folder']);
            }
        }

        $imageData = (string)($_POST['image'] ?? '');
        if (!preg_match('/^data:image\/(\w+);base64,(.+)$/s', $imageData, $m)) {
            jsonOut(['status'=>'error', 'message'=>'Invalid image data']);
        }

        $bin = base64_decode($m[2], true);
        if ($bin === false) jsonOut(['status'=>'error', 'message'=>'Decode failed']);

        if (file_put_contents($path, $bin) !== false) {
            jsonOut(['status'=>'ok', 'path'=>$pathRel]);
        }
        jsonOut(['status'=>'error', 'message'=>'Write failed']);
    }

    // Save sidecar annotations (image.jpg.rnote) — does not modify the image
    if ($action === 'save_rnote') {
        $pathRel = $_POST['path'] ?? '';
        $imgPath = safe_join($docRoot, $pathRel);
        if (!$imgPath || !is_file($imgPath)) {
            jsonOut(['status'=>'error', 'message'=>'Image not found']);
        }

        $content = (string)($_POST['content'] ?? '');
        if ($content === '') {
            jsonOut(['status'=>'error', 'message'=>'Empty content']);
        }

        $decoded = json_decode($content, true);
        if (!is_array($decoded)) {
            jsonOut(['status'=>'error', 'message'=>'Invalid JSON']);
        }

        $rnotePath = $imgPath . '.rnote';
        if (is_file($rnotePath)) {
            @copy($rnotePath, $rnotePath . '.bak');
        }

        $written = file_put_contents(
            $rnotePath,
            json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
        );
        if ($written !== false) {
            jsonOut(['status'=>'ok', 'rnote'=>basename($rnotePath)]);
        }
        jsonOut(['status'=>'error', 'message'=>'Write failed']);
    }

    // default fallback for POST
    jsonOut(['status'=>'error']);
}
  
// ← POST ブロック終了

} catch (Exception $e) {
    jsonOut(['status'=>'error', 'message'=>$e->getMessage()], 500);
}

jsonOut(['status'=>'error', 'message'=>'Unknown action'], 400);
}

$rscopeLoginError = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['boardroom_password']) && !isset($_REQUEST['action'])) {
    $r = boardroom_login((string)$_POST['boardroom_password']);
    if ($r['ok']) {
        while (ob_get_level()) {
            ob_end_clean();
        }
        header('Location: R-Scope.php');
        exit;
    }
    $rscopeLoginError = $r['message'];
}

if (!boardroom_is_logged_in()) {
    while (ob_get_level()) {
        ob_end_clean();
    }
    header('Content-Type: text/html; charset=utf-8');
    $hasPw = boardroom_has_password();
    ?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars(APP_NAME) ?> — Login</title>
<style>
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Hiragino Sans","Noto Sans JP",sans-serif;margin:0;background:#f2f4f8;height:100vh;display:flex;align-items:center;justify-content:center;}
.box{border:none;padding:0;border-radius:12px;background:#fff;width:380px;max-width:90%;box-shadow:0 8px 32px rgba(26,39,68,0.15);overflow:hidden;}
.box-header{background:#1a2744;padding:20px 24px;display:flex;align-items:center;gap:12px;}
.box-header-icon{width:40px;height:40px;border-radius:8px;background:rgba(74,144,226,0.2);display:flex;align-items:center;justify-content:center;font-size:20px;}
.box-header h2{margin:0;font-size:15px;font-weight:700;color:#fff;}
.box-header p{margin:4px 0 0;font-size:11px;color:#7a90b8;}
.box-body{padding:24px;}
label{display:block;font-size:12px;font-weight:600;color:#4a5068;margin-bottom:6px;}
input{width:100%;padding:10px 12px;box-sizing:border-box;border:1px solid #d0d8ea;border-radius:6px;font-size:13px;color:#1a2744;outline:none;transition:border-color 0.15s;}
input:focus{border-color:#4a90e2;}
button{width:100%;padding:11px;background:#4a90e2;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600;margin-top:16px;transition:background 0.15s;}
button:hover{background:#3a7fd2;}
.err{color:#d4183d;font-size:12px;margin-bottom:12px;padding:8px 12px;background:#fff0f0;border-radius:6px;border-left:3px solid #d4183d;}
p.meta{font-size:11px;color:#888;margin:0;}
</style>
</head>
<body>
<div class="box">
<div class="box-header">
  <div class="box-header-icon">🔒</div>
  <div>
    <h2><?= htmlspecialchars(APP_NAME) ?> — Admin Login</h2>
    <p>Enter the site password to manage files.</p>
  </div>
</div>
<div class="box-body">
<?php if (!$hasPw): ?>
<p class="err">Password not configured. Set one from R-Scope (🔑 Password) after first access, or edit config/boardroom.json.</p>
<?php else: ?>
<?php if ($rscopeLoginError !== ''): ?><p class="err"><?= htmlspecialchars($rscopeLoginError) ?></p><?php endif; ?>
<form method="post">
<label>Password</label>
<input type="password" name="boardroom_password" autofocus required>
<button type="submit">Log in →</button>
</form>
<?php endif; ?>
</div>
</div>
</body>
</html>
    <?php
    exit;
}

if (ob_get_level()) ob_end_flush();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?php echo APP_NAME; ?></title>
<style>
  * { box-sizing: border-box; font-size: 12px !important; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Hiragino Sans", "Noto Sans JP", sans-serif; }
  body { margin: 0; background: #f2f4f8; height: 100vh; overflow: hidden; }
  header {
    background: #1a2744;
    color: #fff;
    padding: 0 20px;
    height: 44px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    border-bottom: 2px solid #142038;
    box-shadow: 0 2px 8px rgba(0,0,0,0.3);
  }
  .app-title {
    font-size: 15px !important;
    font-weight: 700;
    white-space: nowrap;
    color: #fff;
    letter-spacing: 0.03em;
    display: flex;
    align-items: center;
    gap: 6px;
  }
  .app-title .app-ver {
    font-size: 11px !important;
    font-weight: 400;
    color: #7a90b8;
    margin-left: 4px;
  }
  .app-info {
    font-size: 11px !important;
    color: #7a90b8;
    text-align: right;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    max-width: min(58vw, 520px);
  }
  #toolbar {
    padding: 0 12px;
    background: #1a2744;
    border-bottom: 1px solid #0e1829;
    display: flex;
    gap: 4px;
    height: 38px;
    align-items: center;
  }
  button {
    font-size: 11px !important;
    padding: 4px 10px;
    cursor: pointer;
    border: 1px solid rgba(255,255,255,0.15);
    background: rgba(255,255,255,0.08);
    color: #c8d8f0;
    border-radius: 5px;
    transition: background 0.15s, color 0.15s, border-color 0.15s;
    white-space: nowrap;
  }
  button:hover {
    background: #4a90e2;
    color: #fff;
    border-color: #4a90e2;
  }
  button.active {
    background: #4a90e2 !important;
    color: #fff !important;
    border-color: #3a7fd2 !important;
  }
  #toolbar span[style*="border-left"] {
    border-color: rgba(255,255,255,0.15) !important;
  }
  #toolbar a {
    font-size: 11px !important;
    color: #9ab4d4;
    text-decoration: none;
    padding: 4px 10px;
    border: 1px solid rgba(255,255,255,0.15);
    border-radius: 5px;
    background: rgba(255,255,255,0.06);
    transition: background 0.15s, color 0.15s;
  }
  #toolbar a:hover {
    background: rgba(255,255,255,0.15);
    color: #fff;
  }
  #wrap { display: flex; height: calc(100vh - 82px); }
  #folders, #files {
    flex: 1;
    display: flex;
    flex-direction: column;
    background: #fff;
    border-right: 1px solid #e4e7ef;
    min-width: 200px;
  }
  #files { border-right: none; }
  #preview {
    flex: 1.5;
    display: flex;
    flex-direction: column;
    background: #111827;
    border-left: 1px solid #0e1829;
    min-width: 300px;
  }
  #resizer {
    width: 5px;
    background: #e4e7ef;
    cursor: col-resize;
    z-index: 10;
    transition: background 0.15s;
  }
  #resizer:hover, #resizer.active { background: #4a90e2; }
  body.resizing { user-select: none; cursor: col-resize; }

  #folderList, #fileList, #previewBox { flex: 1; overflow-y: auto; padding: 0; }
  .tree-item, .file-list-item, .file-list-header {
    display: flex;
    align-items: center;
    padding: 6px 10px;
    border-bottom: 1px solid #f0f2f6;
  }
  .tree-item:hover, .file-list-item:hover {
    background: #f0f5ff;
    cursor: pointer;
  }
  .active {
    background: #dce8fb !important;
    font-weight: bold;
    color: #1a3a6e !important;
  }
  .file-list-header {
    background: #f5f7fb;
    border-bottom: 2px solid #e4e7ef;
    font-weight: 600;
    color: #4a5068;
    cursor: pointer;
    user-select: none;
    position: sticky;
    top: 0;
    z-index: 1;
  }
  .tree-children { padding-left: 18px; }
  .tree-toggle { width: 20px; text-align: center; margin-right: 4px; color: #7a90b8; }
  .file-name-col { flex: 2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #1a2744; }
  .file-size-col, .file-perm-col { flex: 0.6; text-align: right; padding-left: 10px; color: #9aa0b8; }
  .file-date-col { flex: 1.2; text-align: right; padding-left: 10px; color: #9aa0b8; }
  #fileSearch {
    width: 100%; padding: 6px 10px; font-size: 12px !important;
    border: none; border-bottom: 1px solid #e4e7ef;
    outline: none; background: #f8f9fb; color: #2a3050;
  }
  #fileSearch:focus { background: #fff; border-bottom-color: #4a90e2; }
  #fileInfoBar { background: #0d1a2e; border-bottom: 1px solid #1e2d44; padding: 0; flex-shrink: 0; }
  .meta-lister-wrap { background: #0d1a2e; color: #c8d8f0; padding: 8px 10px 0; overflow-x: auto; }
  .meta-lister-table { width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 720px; }
  .meta-lister-table th, .meta-lister-table td {
    border: 1px solid #1e3050;
    padding: 6px 8px;
    text-align: left;
    vertical-align: middle;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .meta-lister-table th { background: #142038; color: #7a90b8; font-weight: 600; font-size: 11px !important; }
  .meta-lister-table td { background: #0d1a2e; color: #c8d8f0; font-size: 12px !important; }
  .meta-lister-table td.meta-type { text-align: center; width: 42px; font-size: 18px !important; }
  .meta-lister-table td.meta-name { color: #74b3f8; }
  .meta-lister-table td.meta-ph { color: #3a506e; }
  .meta-lister-table a { color: #74b3f8; text-decoration: none; }
  .meta-lister-table a:hover { text-decoration: underline; color: #a0caff; }
  #fileInfoBar #actionButtons {
    background: #0a1525;
    padding: 6px 10px;
    border-top: 1px solid #1e3050;
    margin: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
  }
  #fileInfoBar #actionButtons button {
    background: rgba(74,144,226,0.15);
    border-color: rgba(74,144,226,0.35);
    color: #74b3f8;
  }
  #fileInfoBar #actionButtons button:hover {
    background: #4a90e2;
    color: #fff;
  }
  #photoAttrStrip {
    display: none;
    font-size: 10px !important;
    color: #5a6e90;
    margin: 0;
    padding: 4px 10px 6px;
    border-top: 1px solid #1e3050;
    background: #0d1a2e;
  }
  #preview { display: flex; flex-direction: column; min-height: 0; background: #111827; }
  #previewBox { flex: 1; min-height: 0; overflow: auto; background: #f5f7fb; }

  #contextMenu {
    display: none;
    position: absolute;
    z-index: 12000;
    background: #fff;
    border: 1px solid #d0d8ea;
    box-shadow: 0 8px 24px rgba(26,39,68,0.15);
    min-width: 190px;
    padding: 5px 0;
    border-radius: 8px;
  }
  .ctx-item {
    padding: 8px 16px;
    cursor: pointer;
    display: block;
    color: #2a3050;
    font-size: 12px !important;
    transition: background 0.1s;
  }
  .ctx-item:hover { background: #4a90e2; color: #fff; border-radius: 4px; margin: 0 4px; }
  .ctx-sep { border-top: 1px solid #e4e7ef; margin: 4px 0; }
  #systemModal { z-index: 11000; }
  #systemModal > div {
    background: #fff !important;
    width: 420px;
    max-width: 90%;
    margin: 15vh auto;
    border-radius: 10px !important;
    box-shadow: 0 10px 40px rgba(26,39,68,0.25) !important;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    border: 1px solid #e4e7ef;
  }
  #sysModalTitle {
    background: #1a2744 !important;
    color: #fff !important;
    font-weight: 600 !important;
    padding: 14px 16px !important;
    font-size: 13px !important;
  }
  #systemModal > div > div:nth-child(2) { padding: 20px; }
  #sysModalMessage { margin-bottom: 10px; color: #2a3050; }
  #sysModalLabel { display: block; font-weight: 600; margin-bottom: 5px; color: #4a5070; }
  #sysModalInput {
    width: 100%; padding: 8px 10px; font-size: 13px !important;
    border: 1px solid #d0d8ea; border-radius: 6px; outline: none; background: #f8f9fb;
  }
  #sysModalInput:focus { border-color: #4a90e2; box-shadow: 0 0 0 3px rgba(74,144,226,0.12); }
  #sysModalTree {
    border: 1px solid #e4e7ef; height: 150px; overflow: auto;
    padding: 5px; border-radius: 6px; background: #f8f9fb;
  }
  #sysModalPath { font-size: 11px !important; color: #8a93a8; margin-top: 4px; }
  #systemModal > div > div:last-child {
    padding: 10px 16px; background: #f8f9fb;
    text-align: right; border-top: 1px solid #f0f2f6; display: flex; gap: 8px; justify-content: flex-end;
  }
  #systemModal button { color: #2a3050; }
  #sysModalSubmit { background: #4a90e2 !important; border-color: #4a90e2 !important; color: #fff !important; }
  #sysModalSubmit:hover { background: #3a7fd2 !important; }

  .image-editor-wrap { display: flex; flex-direction: column; height: 100%; min-height: 0; }
  .image-editor-toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    padding: 8px;
    background: #f5f7fb;
    border-bottom: 1px solid #e4e7ef;
  }
  .image-editor-toolbar button { color: #2a3050; border-color: #d0d8ea; background: #fff; }
  .image-editor-toolbar button.active { background: #4a90e2; color: #fff; border-color: #3a7fd2; }
  .image-editor-toolbar button:hover { background: #4a90e2; color: #fff; border-color: #4a90e2; }
  .image-editor-viewport {
    flex: 1; overflow: auto; position: relative; background: #1a2744;
    display: flex; align-items: center; justify-content: center; min-height: 200px; padding: 12px;
  }
  .image-editor-stage { position: relative; display: inline-block; line-height: 0; }
  .image-editor-canvas { display: block; max-width: none; box-shadow: 0 2px 16px rgba(0,0,0,0.5); cursor: default; }
  .image-editor-canvas.crop-mode { cursor: crosshair; }
  .image-crop-overlay {
    position: absolute; border: 2px dashed #4a90e2; background: rgba(74,144,226,0.15);
    box-shadow: 0 0 0 9999px rgba(0,0,0,0.5); pointer-events: none; display: none;
  }
  .image-editor-status { padding: 6px 10px; font-size: 11px !important; color: #7a90b8; background: #f5f7fb; border-top: 1px solid #e4e7ef; }
  .image-editor-panel {
    display: none; flex-wrap: wrap; gap: 8px; align-items: center;
    width: 100%; padding: 8px 10px; background: #eef4ff; border-bottom: 1px solid #d0d8ea;
  }
  .image-editor-panel.open { display: flex; }
  .image-editor-panel label { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; color: #2a3050; }
  .image-editor-panel input[type=range] { width: 90px; vertical-align: middle; accent-color: #4a90e2; }
  .image-editor-panel input[type=number] { width: 72px; padding: 4px; border: 1px solid #d0d8ea; border-radius: 4px; }
  .image-quality-cmds { display: flex; flex-wrap: wrap; gap: 5px; width: 100%; margin-top: 4px; }
  .image-quality-cmds button { font-size: 11px !important; padding: 4px 8px !important; }
  .image-editor-sep { width: 100%; height: 0; border: none; border-top: 1px dashed #d0d8ea; margin: 2px 0; }
  .image-editor-mode-bar {
    display: flex; gap: 6px; padding: 6px 8px;
    background: #eef4ff; border-bottom: 1px solid #d0d8ea;
  }
  .image-editor-mode-bar button { color: #2a3050; border-color: #d0d8ea; background: #fff; }
  .image-editor-mode-bar button.active { background: #1a2744; color: #fff; border-color: #1a2744; }
  .image-annot-panel { display: none; flex-wrap: wrap; gap: 5px; width: 100%; padding: 8px 10px; background: #fff8e6; border-bottom: 1px solid #e6d9a8; }
  .image-annot-panel.open { display: flex; }
  .image-annot-panel button.active { background: #f57c00; color: #fff; border-color: #e65100; }
  .image-editor-canvas.annot-layer { position: absolute; left: 0; top: 0; pointer-events: auto; }
  .image-editor-stage.annotating .image-editor-canvas.photo-layer { pointer-events: none; }
  #files.drop-target-active, #previewBox.drop-target-active, #fileList.drop-target-active {
    outline: 3px dashed #4a90e2;
    outline-offset: -3px;
    background: #eef4ff !important;
  }
  #dropOverlay {
    display: none; position: fixed; left: 0; top: 82px; right: 0; bottom: 0;
    z-index: 10000; background: rgba(74,144,226,0.1); pointer-events: none;
    align-items: center; justify-content: center; font-size: 18px !important;
    color: #1a3a80; font-weight: bold; border: 3px dashed #4a90e2; border-radius: 8px; margin: 8px;
  }
  #dropOverlay.visible { display: flex; }
  #scopesColumn { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
  .scope-pane { flex: 1; display: flex; flex-direction: column; min-height: 0; border-bottom: 2px solid #d0d8ea; background: #fff; }
  .scope-pane:last-child { border-bottom: none; }
  .scope-pane-head {
    flex-shrink: 0; display: flex; align-items: center; gap: 8px; padding: 5px 10px;
    background: #f0f4ff; border-bottom: 1px solid #d8e2f4;
    font-weight: 600; color: #1a2744; font-size: 12px !important;
  }
  .scope-pane-head .scope-range { font-weight: 400; font-size: 11px !important; color: #7a90b8; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .scope-pane-body { flex: 1; display: flex; min-height: 0; }
  .scope-pane .scope-folders, .scope-pane .scope-files { flex: 1; display: flex; flex-direction: column; min-width: 140px; border-right: 1px solid #e4e7ef; }
  .scope-pane .scope-files { min-width: 200px; border-right: none; }
  .scope-folder-list, .scope-file-list { flex: 1; overflow-y: auto; }
  .scope-pane-head .btn-remove-scope { font-size: 11px !important; padding: 2px 8px; color: #c0504a; border-color: #daa; background: rgba(255,100,100,0.08); }
  .scope-pane-head .btn-scope-range { font-size: 11px !important; padding: 2px 8px; margin-left: auto; }
  #scopeRangeBar { display: block !important; visibility: visible !important; }
  #scopesEmptyState {
    flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
    padding: 24px; color: #7a90b8; text-align: center; gap: 14px;
    background: repeating-linear-gradient(45deg, #f7f9ff, #f7f9ff 10px, #f2f4fb 10px, #f2f4fb 20px);
  }
  #scopesEmptyState.hidden { display: none; }
  #scopesEmptyState p { color: #6a7898; margin: 0; }
  #scopeSetupOverlay { display: none; position: fixed; inset: 82px 0 0 0; z-index: 15000; background: #f2f4f8; flex-direction: column; }
  #scopeSetupOverlay.open { display: flex; }
  .scope-setup-bar {
    flex-shrink: 0; padding: 10px 16px;
    background: #1a2744; color: #fff;
    display: flex; align-items: center; gap: 12px;
    border-bottom: 1px solid #142038;
  }
  .scope-setup-bar button { background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.2); color: #fff; }
  .scope-setup-bar button:hover { background: #4a90e2; border-color: #4a90e2; }
  .scope-setup-main { flex: 1; display: flex; min-height: 0; padding: 12px; gap: 12px; }
  .scope-setup-tree-wrap {
    flex: 1; display: flex; min-height: 0; background: #fff;
    border: 1px solid #d0d8ea; border-radius: 8px; overflow: hidden; position: relative;
  }
  .scope-setup-tree { flex: 1; overflow-y: auto; padding: 8px 8px 8px 48px; position: relative; }
  .scope-setup-tree .tree-item.scope-out { background: #f0f2f6 !important; color: #b0b8cc !important; }
  .scope-setup-tree .tree-item.scope-in { background: #fff; }
  .scope-setup-tree .tree-item.scope-edge-top { box-shadow: inset 0 3px 0 #4a90e2; }
  .scope-setup-tree .tree-item.scope-edge-bottom { box-shadow: inset 0 -3px 0 #4a90e2; }
  #scopeRangeBar {
    position: absolute; left: 8px; width: 28px;
    background: rgba(74,144,226,0.2); border: 2px solid #4a90e2;
    border-radius: 4px; cursor: ns-resize; z-index: 5; min-height: 24px; box-sizing: border-box;
  }
  #scopeRangeBar .scope-bar-handle { position: absolute; left: 0; right: 0; height: 8px; cursor: ns-resize; background: #4a90e2; opacity: 0.85; }
  #scopeRangeBar .scope-bar-handle.top { top: -4px; border-radius: 4px 4px 0 0; }
  #scopeRangeBar .scope-bar-handle.bottom { bottom: -4px; border-radius: 0 0 4px 4px; }
  .scope-setup-side {
    width: 280px; flex-shrink: 0; background: #fff;
    border: 1px solid #d0d8ea; border-radius: 8px; padding: 16px; font-size: 13px !important;
  }
  .scope-setup-side label { display: block; margin-top: 10px; font-weight: 600; color: #1a2744; font-size: 12px !important; }
  .scope-setup-side select, .scope-setup-side input {
    width: 100%; margin-top: 4px; padding: 7px 8px;
    border: 1px solid #d0d8ea; border-radius: 5px; outline: none;
    font-size: 12px !important; color: #2a3050;
  }
  .scope-setup-side select:focus, .scope-setup-side input:focus { border-color: #4a90e2; }
  .scope-setup-size { min-height: 280px; }
</style>
</head>
<body>
<header>
    <div class="app-title" title="更新: <?php echo htmlspecialchars(APP_UPDATE_DATE, ENT_QUOTES, 'UTF-8'); ?>">
        📁 <?php echo htmlspecialchars(APP_NAME, ENT_QUOTES, 'UTF-8'); ?>
        <span class="app-ver">v<?php echo htmlspecialchars(APP_VERSION, ENT_QUOTES, 'UTF-8'); ?></span>
    </div>
    <div class="app-info" title="<?php echo htmlspecialchars(APP_DESCRIPTION, ENT_QUOTES, 'UTF-8'); ?>">
        <?php echo htmlspecialchars(APP_DESCRIPTION, ENT_QUOTES, 'UTF-8'); ?>
    </div>
</header>
<div id="toolbar">

  <!-- 左側 -->
  <div style="display:flex; align-items:center; gap:5px;">

    <button id="btnBack">⬅ Back</button>
    <button id="btnForward">➡ Forward</button>

    <button id="btnReload">🔄 Reload</button>
    <button id="btnUpload">⬆ Upload</button>
    <button id="btnUploadFolder" title="フォルダごとアップロード">📂 Folder</button>
    <button id="btnListView">🗂️ List</button>
    <button id="btnThumbView">🖼️ Thumb</button>

    <span style="border-left:1px solid #ccc; height:20px; margin:0 5px;"></span>

    <button id="btnNewFolder">📁 New Folder</button>
    <button id="btnNewFile">📄 New File</button>

    <span style="border-left:1px solid #ccc; height:20px; margin:0 5px;"></span>

    <button id="btnRename">🏷️ Rename</button>
    <button id="btnDelete">🗑️ Delete</button>
	<button id="btnProtect" title=".protected を付ける">🔒 保護</button>
<button id="btnUnprotect" title=".protected を外す">🔓 保護解除</button>
<button id="btnPassword" title="理事会共通パスワード">🔑 理事会パスワード</button>
  </div>

  <!-- 右側 -->
  <div style="margin-left:auto; display:flex; gap:5px; align-items:center;">
<a href="R-Scope-manual.php" target="_blank" title="取扱説明書（Board Room版）" style="text-decoration:none;padding:5px 10px;border:1px solid #ccc;border-radius:3px;background:#f8f8f8;color:#333;">📖 説明書</a>
<button onclick="location.href='scope-proxy.php'" title="index スコープ">SCOPE</button>
<a href="?logout=1">ログアウト</a>
  </div>

</div>
<div id="wrap">
  <div id="folders"><div id="folderList">Loading...</div></div>
  <div id="files">
    <div style="padding:0; border-bottom:1px solid #e4e7ef;">
      <input id="fileSearch" type="text" placeholder="🔍  Search...">
    </div>
    <div id="fileList">Select a folder</div>
  </div>

  <div id="resizer"></div>
  <div id="preview">
    <div id="fileInfoBar" style="display:none;">
      <div class="meta-lister-wrap">
        <table class="meta-lister-table">
          <thead>
            <tr>
              <th>Type</th>
              <th>Name</th>
              <th>Ver</th>
              <th>Cat</th>
              <th>Desc</th>
              <th>Memo</th>
              <th>Size</th>
              <th>Date</th>
              <th>List</th>
            </tr>
          </thead>
          <tbody>
            <tr id="fileMetaRow">
              <td class="meta-type" id="metaCellType">📄</td>
              <td class="meta-name" id="metaCellName">—</td>
              <td id="metaCellVer" class="meta-ph">(Ver)</td>
              <td id="metaCellCat" class="meta-ph">(Cat)</td>
              <td id="metaCellDesc" class="meta-ph">(Desc)</td>
              <td id="metaCellMemo" class="meta-ph">(Memo)</td>
              <td id="metaCellSize">—</td>
              <td id="metaCellDate">—</td>
              <td id="metaCellList">—</td>
            </tr>
          </tbody>
        </table>
        <div id="photoAttrStrip">Photo: (EXIF etc. — coming soon)</div>
      </div>
      <div id="actionButtons" style="display:flex; flex-wrap:wrap; gap:5px;"></div>
    </div>
    <div id="previewBox">Select a file</div>
  </div>
</div>
<div id="dropOverlay">Explorer / Outlook からドロップ → 表示中フォルダへコピー</div>
<div id="contextMenu"></div>

<div id="systemModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.4); z-index:11000;">
    <div style="background:#fff; width:400px; max-width:90%; margin:15vh auto; border-radius:6px; box-shadow:0 5px 25px rgba(0,0,0,0.2); display:flex; flex-direction:column;">
        <div id="sysModalTitle" style="padding:12px; background:#2d3e50; color:#fff; font-weight:bold;">Action</div>
        <div style="padding:20px;">
            <div id="sysModalMessage" style="margin-bottom:10px;"></div>
            <div id="sysModalInputGroup"><label id="sysModalLabel" style="display:block; font-weight:bold; margin-bottom:5px;">Name:</label><input type="text" id="sysModalInput" style="width:100%; padding:8px;"></div>
            <div id="sysModalTreeGroup" style="margin-top:10px;"><div id="sysModalTree" style="border:1px solid #ccc; height:150px; overflow:auto; padding:5px;"></div><div id="sysModalPath" style="font-size:11px; color:#888;">/</div></div>
        </div>
        <div style="padding:10px; background:#f5f5f5; text-align:right;">
            <button onclick="closeSystemModal()">Cancel</button> <button id="sysModalSubmit" style="background:#007bff; color:white; border:none;">OK</button>
        </div>
    </div>
</div>

<script>
//const API_PROXY = '/proxy.php';
const API_PROXY = location.pathname;
const API_GET_FILE = API_PROXY + '?action=get_file';
const API_GET_META = API_PROXY + '?action=get_meta';
const LISTER_URL = 'data/file-lister.php';

const API_MKDIR = API_PROXY;
const API_CREATE_FILE = API_PROXY;
const API_UPLOAD = API_PROXY;
const API_DELETE = API_PROXY;
const API_RENAME = API_PROXY;
const API_CHMOD = API_PROXY;

let currentFolder='', currentFile='', baseFolder='', loadedTreeData=[], loadedFileData=[];
let folderHistory = [];
let historyIndex = -1;
let historyLock = false;   // ループ防止
let ctxTarget='', ctxIsDir=false; // declared globals for context menu
let folderSortState={key:'name',order:'asc'}, fileSortState={key:'name',order:'asc'};
let sysDialogMode='', sysTargetDir='', sysOriginalPath='', sysCallback=null;
let viewMode = 'list';   // ← 表示モード: list / thumb
let undoStack = [];
let redoStack = [];
const UNDO_LIMIT = 50; // B: 50段階（Notepad++ 相当）

function escMeta(s) {
    return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
}

function rscopeFileIcon(filename) {
    const ext = (filename.split('.').pop() || '').toLowerCase();
    return ({ zip:'🗜', pdf:'📚', png:'🖼', jpg:'🖼', jpeg:'🖼', gif:'🖼', webp:'🖼', txt:'📝', php:'💻', html:'🌐' })[ext] || '📄';
}

function metaCellValue(value, placeholder) {
    const s = (value !== undefined && value !== null) ? String(value).trim() : '';
    if (s === '') {
        return { html: escMeta(placeholder), ph: true };
    }
    return { html: escMeta(s), ph: false, title: escMeta(s) };
}

function setMetaCell(id, value, placeholder) {
    const el = document.getElementById(id);
    if (!el) return;
    const v = metaCellValue(value, placeholder);
    el.innerHTML = v.html;
    el.className = v.ph ? 'meta-ph' : '';
    el.title = v.title || '';
}

function loadFileMetaStrip(path, filename) {
    const bar = document.getElementById('fileInfoBar');
    const photoStrip = document.getElementById('photoAttrStrip');
    bar.style.display = 'block';
    photoStrip.style.display = 'none';

    document.getElementById('metaCellType').textContent = rscopeFileIcon(filename);
    const nameEl = document.getElementById('metaCellName');
    nameEl.textContent = filename;
    nameEl.className = 'meta-name';
    nameEl.title = filename;
    setMetaCell('metaCellVer', '', '(Ver)');
    setMetaCell('metaCellCat', '', '(Cat)');
    setMetaCell('metaCellDesc', '', '(Desc)');
    setMetaCell('metaCellMemo', '', '(Memo)');
    document.getElementById('metaCellSize').textContent = '…';
    document.getElementById('metaCellSize').className = '';
    document.getElementById('metaCellDate').textContent = '…';
    document.getElementById('metaCellList').innerHTML = '…';

    fetch(API_GET_META + '&path=' + encodeURIComponent(path) + '&t=' + Date.now())
        .then(r => r.json())
        .then(j => {
            if (j.status !== 'ok') {
                return;
            }
            document.getElementById('metaCellType').textContent = rscopeFileIcon(j.name || filename);
            const nameEl2 = document.getElementById('metaCellName');
            const nm = j.name || filename;
            nameEl2.textContent = nm;
            nameEl2.title = nm;
            setMetaCell('metaCellVer', j.ver, '(Ver)');
            setMetaCell('metaCellCat', j.category, '(Cat)');
            setMetaCell('metaCellDesc', j.description, '(Desc)');
            setMetaCell('metaCellMemo', j.memo, '(Memo)');
            const sizeEl = document.getElementById('metaCellSize');
            const sz = j.size_human || (j.size != null ? j.size + ' B' : '');
            sizeEl.textContent = sz || '—';
            sizeEl.className = '';
            sizeEl.title = sz;
            const dateEl = document.getElementById('metaCellDate');
            dateEl.textContent = j.mtime || '—';
            dateEl.title = j.mtime || '';

            const listEl = document.getElementById('metaCellList');
            if (j.lister_path !== undefined && j.lister_path !== null) {
                const href = LISTER_URL + '?path=' + encodeURIComponent(j.lister_path);
                listEl.innerHTML = '<a href="' + escMeta(href) + '" target="_blank" rel="noopener noreferrer">Open</a>';
                listEl.className = '';
            } else {
                listEl.textContent = '—';
                listEl.className = 'meta-ph';
            }

            const imgExt = ['jpg','jpeg','png','gif','webp','bmp'];
            if (imgExt.includes((j.ext || '').toLowerCase())) {
                photoStrip.style.display = 'block';
            }
        })
        .catch(() => {});
}

// 1. Preview Logic (Simple Editor Enabled - SAFE VERSION)
function selectFile(folder, filename) {
    const path = folder ? folder + '/' + filename : filename;
    const box = document.getElementById('previewBox');
    loadFileMetaStrip(path, filename);
    const actions = document.getElementById('actionButtons');
    actions.innerHTML = ''; box.innerHTML = '';

    const ext = filename.split('.').pop().toLowerCase();
    const fileUrl = API_GET_FILE + '&path=' + encodeURIComponent(path);

    const editable = ['txt','html','css','js','php','json','md'];

if (editable.includes(ext)) {
    fetch(fileUrl)
        .then(r => r.text())
        .then(content => {
            // サニタイズ（script タグの無効化）
            const safe = content
                .replace(/<script/gi, '&lt;script')
                .replace(/<\/script>/gi, '&lt;/script&gt;');

            // textarea（簡易エディタ）
            const ta = document.createElement('textarea');
            ta.id = 'simpleEditor';
            ta.style.width = '100%';
            ta.style.height = '100%';
            ta.style.padding = '10px';
            ta.style.fontFamily = 'monospace';
            ta.style.fontSize = '12px';
            ta.value = safe;
            box.innerHTML = '';
            box.appendChild(ta);

            // --- Undo/Redo 初期化 ---
            undoStack = [ta.value]; // 最初の状態を入れる
            redoStack = [];

            // 変更イベントで Undo スタックに積む（無駄登録抑止）
            let lastPush = ta.value;
            ta.addEventListener('input', () => {
                const val = ta.value;
                if (undoStack[undoStack.length - 1] !== val) {
                    undoStack.push(val);
                    if (undoStack.length > UNDO_LIMIT) undoStack.shift();
                    // 編集が発生したら redo をクリア
                    redoStack = [];
                }
                lastPush = val;
            });

            // キーボードショートカット（Ctrl+Z / Ctrl+Y）
            ta.addEventListener('keydown', (e) => {
                if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === 'z') {
                    e.preventDefault(); // ブラウザ既定の undo を抑止
                    // Undo 操作
                    if (undoStack.length > 1) {
                        const cur = undoStack.pop();
                        redoStack.push(cur);
                        ta.value = undoStack[undoStack.length - 1];
                    }
                } else if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.shiftKey && e.key === 'Z'))) {
                    e.preventDefault();
                    // Redo 操作
                    if (redoStack.length > 0) {
                        const v = redoStack.pop();
                        undoStack.push(v);
                        ta.value = v;
                    }
                }
            });
        });

    // --- ボタン群（Save の右に Undo / Redo） ---
    const saveBtn = document.createElement('button');
    saveBtn.innerHTML = '💾 Save';
    saveBtn.onclick = () => {
        const newContent = document.getElementById('simpleEditor').value;
        const fd = new FormData();
        fd.append('action','save_with_backup');
        fd.append('path', path);
        fd.append('content', newContent);

        fetch(API_PROXY, {method:'POST', body: fd})
            .then(r=>r.json())
            .then(j=>{
                if(j.status==='ok') {
                    alert('Saved');
                    // 保存したら redo は無効化（履歴を維持）
                    redoStack = [];
                } else alert('Save Error: ' + (j.message||''));
            })
            .catch(()=>alert('Save Error'));
    };
    actions.appendChild(saveBtn);

    // Undo ボタン（Save の右）
    const undoBtn = document.createElement('button');
    undoBtn.innerHTML = '↩ Undo';
    undoBtn.onclick = () => {
        const ta = document.getElementById('simpleEditor');
        if (!ta) return;
        if (undoStack.length > 1) {
            const cur = undoStack.pop();
            redoStack.push(cur);
            ta.value = undoStack[undoStack.length - 1];
        } else {
            alert('No more undo steps');
        }
    };
    actions.appendChild(undoBtn);

    // Redo ボタン（Undo の右）
    const redoBtn = document.createElement('button');
    redoBtn.innerHTML = '↪ Redo';
    redoBtn.onclick = () => {
        const ta = document.getElementById('simpleEditor');
        if (!ta) return;
        if (redoStack.length > 0) {
            const v = redoStack.pop();
            undoStack.push(v);
            ta.value = v;
        } else {
            alert('No more redo steps');
        }
    };
    actions.appendChild(redoBtn);

    // Download button (既存)
    const dlBtn = document.createElement('button'); dlBtn.innerHTML = '⬇ Download';
    dlBtn.onclick = () => {
        const a = document.createElement('a');
        a.href = API_PROXY + '?action=download&path=' + encodeURIComponent(path);
        document.body.appendChild(a);
        a.click();
        a.remove();
    };
    actions.appendChild(dlBtn);

    // Print button (既存)
    const printBtn = document.createElement('button');
    printBtn.innerHTML = '🖨️ Print';
    printBtn.onclick = () => {
        const w = window.open(fileUrl, 'printWindow', 'width=900,height=700,scrollbars=yes,resizable=yes');
        const timer = setInterval(() => {
            if (w && w.document && w.document.readyState === 'complete') {
                clearInterval(timer);
                w.focus();
                w.print();
            }
        }, 200);
    };
    actions.appendChild(printBtn);

    return;
}


    // Image editor (raster images)
    if (['jpg','jpeg','png','gif','webp','bmp'].includes(ext)) {
        initImageEditor(box, actions, path, fileUrl);
        return;
    }
    if (ext === 'svg') {
        box.innerHTML = '<div style="text-align:center; padding:10px;"><img src="' + fileUrl + '" style="max-width:100%;"></div>';
    }
    else if (ext === 'pdf') {
        box.innerHTML = '<iframe src="' + fileUrl + '" style="width:100%; height:100%; border:none;"></iframe>';
    }
    else if (['txt','html','css','js','php','json','md'].includes(ext)) {
        fetch(fileUrl)
            .then(r => r.text())
            .then(t => {
               const preview = t.length > 3000 ? t.substring(0,3000) + '... (Truncated)' : t;
                const pre = document.createElement('pre');
                pre.style.padding = '10px';
                pre.style.overflow = 'auto';
                pre.style.height = '100%';
                pre.textContent = preview;
                box.innerHTML = '';
                box.appendChild(pre);
            });
    }
    else {
        box.innerHTML = '<div style="padding:40px; text-align:center; color:#888;">No Preview Available</div>';
    }

    const dlBtn = document.createElement('button'); dlBtn.innerHTML = '⬇ Download';
    dlBtn.onclick = () => {
        const a = document.createElement('a');
        a.href = API_PROXY + '?action=download&path=' + encodeURIComponent(path);
        document.body.appendChild(a);
        a.click();
        a.remove();
    };
    actions.appendChild(dlBtn);

    const printBtn = document.createElement('button'); printBtn.innerHTML = '🖨️ Print';
    printBtn.onclick = () => {
        const w = window.open(fileUrl, 'printWindow', 'width=900,height=700,scrollbars=yes,resizable=yes');
        const timer = setInterval(() => {
            if (w && w.document && w.document.readyState === 'complete') {
                clearInterval(timer);
                w.focus();
                w.print();
            }
        }, 200);
    };
    actions.appendChild(printBtn);
}
// =======================
// Explorer drag & drop → upload (files + folders)
// =======================
(function setupExplorerDrop() {
    const dropTargets = [
        document.getElementById('files'),
        document.getElementById('previewBox')
    ];
    const overlay = document.getElementById('dropOverlay');
    let dragDepth = 0;

    function hasFiles(dt) {
        if (!dt) return false;
        if (dt.types) {
            const types = Array.from(dt.types);
            if (types.includes('Files')) return true;
            if (types.some(t => String(t).indexOf('image/') === 0)) return true;
        }
        return (dt.files && dt.files.length > 0);
    }

    function defaultImageName(file, mimeHint) {
        if (file.name && file.name !== 'image' && file.name.indexOf('.') > 0) return file.name;
        const ext = (mimeHint || file.type || 'image/png').split('/')[1] || 'png';
        const safe = ext === 'jpeg' ? 'jpg' : ext.replace(/[^a-z0-9]/gi, '') || 'png';
        return 'drop-' + Date.now() + '.' + safe;
    }

    function setDropHighlight(on) {
        dropTargets.forEach(el => {
            if (el) el.classList.toggle('drop-target-active', on);
        });
        if (overlay) overlay.classList.toggle('visible', on);
    }

    function readDirectoryEntries(reader) {
        return new Promise(resolve => {
            reader.readEntries(resolve, () => resolve([]));
        });
    }

    async function readAllDirectoryEntries(reader) {
        const all = [];
        let batch;
        do {
            batch = await readDirectoryEntries(reader);
            all.push.apply(all, batch);
        } while (batch.length > 0);
        return all;
    }

    async function walkEntry(entry, prefix, out) {
        if (!entry) return;
        if (entry.isFile) {
            await new Promise(resolve => {
                entry.file(file => {
                    const rel = prefix ? (prefix + file.name) : file.name;
                    out.push({ file: file, relativePath: rel.replace(/\\/g, '/') });
                    resolve();
                }, () => resolve());
            });
        } else if (entry.isDirectory) {
            const reader = entry.createReader();
            const children = await readAllDirectoryEntries(reader);
            const sub = prefix ? (prefix + entry.name + '/') : (entry.name + '/');
            for (let i = 0; i < children.length; i++) {
                await walkEntry(children[i], sub, out);
            }
        }
    }

    async function collectDroppedFiles(dataTransfer) {
        const out = [];
        const items = dataTransfer.items;
        if (items && items.length) {
            const tasks = [];
            for (let i = 0; i < items.length; i++) {
                const item = items[i];
                if (item.kind !== 'file') continue;
                const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null;
                if (entry) {
                    tasks.push(walkEntry(entry, '', out));
                } else {
                    const f = item.getAsFile();
                    if (f) {
                        const rel = (f.webkitRelativePath && f.webkitRelativePath.length)
                            ? f.webkitRelativePath
                            : defaultImageName(f, item.type);
                        out.push({ file: f, relativePath: rel.replace(/\\/g, '/') });
                    }
                }
            }
            await Promise.all(tasks);
        }
        if (dataTransfer.files && dataTransfer.files.length) {
            for (let i = 0; i < dataTransfer.files.length; i++) {
                const f = dataTransfer.files[i];
                const rel = (f.webkitRelativePath && f.webkitRelativePath.length)
                    ? f.webkitRelativePath
                    : defaultImageName(f, f.type);
                const exists = out.some(x => x.relativePath === rel.replace(/\\/g, '/'));
                if (!exists) out.push({ file: f, relativePath: rel.replace(/\\/g, '/') });
            }
        }
        if (!out.length && items && items.length) {
            for (let i = 0; i < items.length; i++) {
                const item = items[i];
                if (item.kind === 'file' && item.type && item.type.indexOf('image/') === 0) {
                    const f = item.getAsFile();
                    if (f) out.push({ file: f, relativePath: defaultImageName(f, item.type) });
                }
            }
        }
        return out;
    }

    function uploadOne(file, relativePath) {
        const fd = new FormData();
        fd.append('action', 'upload');
        fd.append('dir', currentFolder);
        fd.append('file', file, file.name);
        fd.append('relative_path', relativePath);
        return fetch(API_UPLOAD, { method: 'POST', body: fd }).then(r => r.json());
    }

    async function uploadFileList(list, label) {
        if (!list.length) {
            alert('コピーするファイルがありません');
            return;
        }

        const box = document.getElementById('fileList');
        let ok = 0;
        let fail = 0;
        const cap = label || 'コピー';

        for (let i = 0; i < list.length; i++) {
            const item = list[i];
            if (box) {
                box.innerHTML = '<div style="padding:12px;">' + cap + '中… ' + (i + 1) + ' / ' + list.length +
                    '<br><small>' + item.relativePath + '</small></div>';
            }
            try {
                const j = await uploadOne(item.file, item.relativePath);
                if (j.status === 'ok') ok++;
                else fail++;
            } catch (e) {
                fail++;
            }
        }

        selectFolder(currentFolder);
        if (fail > 0) {
            alert(cap + '完了: 成功 ' + ok + ' 件 / 失敗 ' + fail + ' 件');
        } else if (ok > 1) {
            alert(cap + 'しました: ' + ok + ' 件');
        }
    }

    async function uploadDroppedList(list) {
        await uploadFileList(list, 'コピー');
    }

    window.rscopeUploadFileList = uploadFileList;

    function onDragEnter(e) {
        if (!hasFiles(e.dataTransfer)) return;
        e.preventDefault();
        dragDepth++;
        setDropHighlight(true);
    }

    function onDragLeave(e) {
        if (!hasFiles(e.dataTransfer)) return;
        dragDepth = Math.max(0, dragDepth - 1);
        if (dragDepth === 0) setDropHighlight(false);
    }

    function onDragOver(e) {
        if (!hasFiles(e.dataTransfer)) return;
        e.preventDefault();
        e.dataTransfer.dropEffect = 'copy';
    }

    async function onDrop(e) {
        if (!hasFiles(e.dataTransfer)) return;
        e.preventDefault();
        dragDepth = 0;
        setDropHighlight(false);

        const list = await collectDroppedFiles(e.dataTransfer);
        await uploadDroppedList(list);
    }

    dropTargets.forEach(t => {
        if (!t) return;
        t.addEventListener('dragenter', onDragEnter);
        t.addEventListener('dragleave', onDragLeave);
        t.addEventListener('dragover', onDragOver);
        t.addEventListener('drop', onDrop);
    });
})();

// 2. Core Logic
function loadFolders() {
    const box = document.getElementById('folderList'); box.innerHTML = 'Loading...';
    fetch(API_PROXY+'?action=tree&t='+Date.now()).then(r=>r.json()).then(t=>{
        box.innerHTML=''; if(!t || t.status==='error'){ box.innerHTML='<div style="padding:10px;">No folders</div>'; return; }
        const h=document.createElement('div'); h.className='file-list-header';
        h.innerHTML=`<span class="tree-toggle"></span><span class="file-name-col" onclick="sortFolders('name')">Name ${getSortIcon('name', folderSortState)}</span><span class="file-date-col" onclick="sortFolders('mtime')">Date ${getSortIcon('mtime', folderSortState)}</span>`;
        box.appendChild(h); loadedTreeData=t; sortTreeRecursive(loadedTreeData, folderSortState.key, folderSortState.order); renderTree(loadedTreeData, box);
    }).catch(e=>box.innerHTML='Error');
}
function renderTree(n,p) { if(!Array.isArray(n))return; n.forEach(x=>{ const d=document.createElement('div'); d.className='tree-item'; const hasC=x.children&&x.children.length>0; d.innerHTML=`<span class="tree-toggle">${hasC?'▶':'　'}</span><span class="tree-name">${(x.is_dir?'📁 ':'📄 ')+x.name}</span><span class="file-date-col">${x.mtime?new Date(x.mtime).toLocaleString():''}</span>`; p.appendChild(d); d.oncontextmenu=(e)=>{e.preventDefault(); selectItemAndMenu(x.path,x.is_dir,e.pageX,e.pageY);}; if(hasC){const c=document.createElement('div');c.className='tree-children';c.style.display='none';p.appendChild(c);renderTree(x.children,c);} d.onclick=(e)=>{e.stopPropagation();document.querySelectorAll('.tree-item').forEach(el=>el.classList.remove('active'));d.classList.add('active');if(x.is_dir){currentFolder=x.path;baseFolder=x.path;selectFolder(x.path);if(hasC){const s=d.nextElementSibling.style;s.display=s.display==='none'?'block':'none';d.querySelector('.tree-toggle').textContent=s.display==='none'?'▶':'▼';}}else{const pd=x.path.substring(0,x.path.lastIndexOf('/'));currentFile=x.name;selectFile(pd,x.name);}}; }); }
function selectFolder(f, fromHistory = false) {

    // -------------------------
    // 履歴登録（戻る/進むのため）
    // -------------------------
    if (!fromHistory) {
        if (!historyLock) {
            // 今の位置より先の履歴を切り捨て
            folderHistory = folderHistory.slice(0, historyIndex + 1);

            // 新しいフォルダを履歴に追加
            folderHistory.push(f);

            // インデックスを更新
            historyIndex = folderHistory.length - 1;
        }
    }

    // -------------------------
    // ここから元の処理
    // -------------------------
    currentFolder = f;

    const box = document.getElementById('fileList');
    box.innerHTML = 'Loading...';

    document.getElementById('previewBox').innerHTML = 'Select file';
    document.getElementById('fileInfoBar').style.display = 'none';

    fetch(API_PROXY + '?action=list&dir=' + encodeURIComponent(f) + '&t=' + Date.now())
        .then(r => r.json())
        .then(d => {
            box.innerHTML = '';
            loadedFileData = Array.isArray(d) ? d : [];
            sortFiles(fileSortState.key);
            renderFiles();
        })
        .catch(e => box.innerHTML = 'Error');
}
function renderFiles() { 
    // --- サムネイル表示モード ---
    if (viewMode === 'thumb') {
        return renderFilesThumb();
    }
    const box = document.getElementById('fileList');
    box.innerHTML = '';

    // 一覧表示用にスタイル解除
    box.style.display = '';
    box.style.flexWrap = '';
    box.style.gap = '';
    box.style.padding = '';

    // 右クリックメニュー（何も無いところ）
    box.oncontextmenu = (e) => {
        if (e.target === box) {
            e.preventDefault();
            showContextMenu(null, e.pageX, e.pageY);
        }
    };

    // ---- ヘッダー行 ----
    const h = document.createElement('div');
    h.className = 'file-list-header';
    h.innerHTML =
        '<span class="file-name-col" onclick="sortFiles(\'name\')">Name ' + getSortIcon('name') + '</span>' +
        '<span class="file-size-col">Size</span>' +
        '<span class="file-perm-col">Perms</span>' +
        '<span class="file-date-col" onclick="sortFiles(\'mtime\')">Date ' + getSortIcon('mtime') + '</span>';
    box.appendChild(h);

    // ---- Upナビゲーション ----
    // 左ペインで選んだ baseFolder より上には絶対に戻らない
    if (currentFolder !== '' && currentFolder !== baseFolder) {
        const up = document.createElement('div');
        up.className = 'file-list-item';
        up.style.background = '#fafafa';
        up.innerHTML =
            '<span class="file-name-col"><b>⤴️ .. (Up)</b></span>' +
            '<span class="file-size-col"></span>' +
            '<span class="file-perm-col"></span>' +
            '<span class="file-date-col"></span>';

        up.onclick = () => {
            const pos = currentFolder.lastIndexOf('/');
            let parent = pos > -1 ? currentFolder.substring(0, pos) : '';

            // 親が baseFolder より上に行くのを阻止
            if (parent.length < baseFolder.length) {
                parent = baseFolder;
            }

            selectFolder(parent);
        };

        box.appendChild(up);
    }

    // ---- ルートで何もない場合 ----
    if (loadedFileData.length === 0 && currentFolder === baseFolder) {
        box.innerHTML += '<div style="padding:10px;">(Empty)</div>';
        return;
    }

    // ---- ファイル + フォルダ一覧 ----
    loadedFileData.forEach(x => {
        const d = document.createElement('div');
        d.className = 'file-list-item';

        d.innerHTML =
            '<span class="file-name-col">' + (x.is_dir ? (x.protected ? '🔒 ' : '📁 '): '📄 ') + x.name + '</span>' +
            '<span class="file-size-col">' + (x.is_dir ? '-' : (x.size || '-')) + '</span>' +
            '<span class="file-perm-col">' + (x.perms || '') + '</span>' +
            '<span class="file-date-col">' + (x.mtime ? new Date(x.mtime).toLocaleString() : '') + '</span>';

        // 右クリック
        d.oncontextmenu = (e) => {
            e.preventDefault();
            e.stopPropagation();
            selectItemAndMenu(
                currentFolder ? currentFolder + '/' + x.name : x.name,
                x.is_dir,
                e.pageX,
                e.pageY
            );
        };

        // 左クリック
        d.onclick = () => {
            document.querySelectorAll('.file-list-item').forEach(el => el.style.background = '');
            d.style.background = '#eef';

            if (x.is_dir) {
                selectFolder(currentFolder + '/' + x.name);
            } else {
                currentFile = x.name;
                selectFile(currentFolder, x.name);
            }
        };

        box.appendChild(d);
    });
applySearchFilter();
}

function applySearchFilter() {
    const q = document.getElementById('fileSearch').value.toLowerCase().trim();
    const items = document.querySelectorAll('#fileList .file-list-item');

    items.forEach(item => {
        const nameEl = item.querySelector('.file-name-col');
        if (!nameEl) return; // header or up

        const name = nameEl.textContent.toLowerCase();
        if (q === '' || name.includes(q)) {
            item.style.display = '';
            
            // highlight
            const original = nameEl.textContent;
            const regex = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'i');
            nameEl.innerHTML = original.replace(regex, '<span style="background:yellow;">$1</span>');
        } else {
            item.style.display = 'none';
        }
    });
}
function renderFilesThumb() {
    const box = document.getElementById('fileList');
    box.innerHTML = '';

    box.style.display = 'flex';
    box.style.flexWrap = 'wrap';
    box.style.gap = '10px';
    box.style.padding = '10px';

    loadedFileData.forEach(x => {

        const item = document.createElement('div');
        item.style.width = '120px';
        item.style.border = '1px solid #ddd';
        item.style.borderRadius = '6px';
        item.style.padding = '5px';
        item.style.textAlign = 'center';
        item.style.cursor = 'pointer';
        item.style.background = '#fff';
        item.style.boxShadow = '0 1px 2px rgba(0,0,0,0.1)';
        item.className = 'thumb-item';

        let ext = x.name.split('.').pop().toLowerCase();
        let thumb = '';

        if (['jpg','jpeg','png','gif','webp','svg','bmp'].includes(ext)) {
            const url = API_GET_FILE + '&path=' + encodeURIComponent(
                x.path || (currentFolder + '/' + x.name)
            );
            thumb = `<img src="${url}" style="width:100%; height:80px; object-fit:cover; border-radius:4px;">`;
        }
        else if (x.is_dir) {
            thumb = `<div style="font-size:40px;">📁</div>`;
        }
        else if (['txt','md','html','css','js','php','json'].includes(ext)) {
            thumb = `<div style="font-size:40px;">📄</div>`;
        }
        else if (ext === 'pdf') {
            thumb = `<div style="font-size:40px;">📑</div>`;
        }
        else if (ext === 'zip') {
            thumb = `<div style="font-size:40px;">🗜️</div>`;
        }
        else {
            thumb = `<div style="font-size:40px;">📦</div>`;
        }

        item.innerHTML = `
            ${thumb}
            <div style="font-size:11px; margin-top:5px; word-break:break-all;">${x.name}</div>
        `;

        item.onclick = () => {
            if (x.is_dir) selectFolder(currentFolder + '/' + x.name);
            else {
                currentFile = x.name;
                selectFile(currentFolder, x.name);
            }
        };

        box.appendChild(item);
    });
}

// 3. Utils
const resizer=document.getElementById('resizer');const leftSide=document.getElementById('files');let xR=0;let lW=0;const mdH=(e)=>{xR=e.clientX;lW=leftSide.getBoundingClientRect().width;resizer.classList.add('active');document.body.classList.add('resizing');document.addEventListener('mousemove',mmH);document.addEventListener('mouseup',muH);};const mmH=(e)=>{const dx=e.clientX-xR;const nw=lW+dx;if(nw>150&&nw<(document.body.clientWidth-200))leftSide.style.flex=`0 0 ${nw}px`;};const muH=()=>{resizer.classList.remove('active');document.body.classList.remove('resizing');document.removeEventListener('mousemove',mmH);document.removeEventListener('mouseup',muH);};if(resizer)resizer.addEventListener('mousedown',mdH);
function apiPost(url,data,cb){const p=new URLSearchParams();for(const k in data)p.append(k,data[k]);fetch(url,{method:'POST',body:p}).then(r=>r.json()).then(j=>{if(j.status==='ok'){if(cb)cb();}else alert('Error: '+(j.message||'Unknown'));}).catch(e=>alert('Comm Error'));}
function escapeHtml(s){return String(s).replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));}
function getSortIcon(k,s=fileSortState){return s.key!==k?'':(s.order==='asc'?'▲':'▼');}
function sortFiles(k){if(fileSortState.key===k)fileSortState.order=fileSortState.order==='asc'?'desc':'asc';else{fileSortState.key=k;fileSortState.order='asc';}loadedFileData.sort((a,b)=>compareValues(a,b,fileSortState.key,fileSortState.order));renderFiles();}
function sortFolders(k){if(folderSortState.key===k)folderSortState.order=folderSortState.order==='asc'?'desc':'asc';else{folderSortState.key=k;folderSortState.order='asc';}sortTreeRecursive(loadedTreeData,folderSortState.key,folderSortState.order);const box=document.getElementById('folderList');box.innerHTML='';const h=document.createElement('div');h.className='file-list-header';h.innerHTML=`<span class="tree-toggle"></span><span class="file-name-col" onclick="sortFolders('name')">Name ${getSortIcon('name',folderSortState)}</span><span class="file-date-col" onclick="sortFolders('mtime')">Date ${getSortIcon('mtime',folderSortState)}</span>`;box.appendChild(h);renderTree(loadedTreeData,box);}
function sortTreeRecursive(n,k,o){if(!Array.isArray(n))return;n.sort((a,b)=>compareValues(a,b,k,o));n.forEach(x=>{if(x.children)sortTreeRecursive(x.children,k,o)});}
function compareValues(a,b,k,o){let vA=a[k],vB=b[k];if(k==='mtime'){vA=vA?new Date(vA).getTime():0;vB=vB?new Date(vB).getTime():0;}else if(typeof vA==='string'){vA=vA.toLowerCase();vB=vB.toLowerCase();}if(a.is_dir!==undefined&&b.is_dir!==undefined){if(a.is_dir&&!b.is_dir)return -1;if(!a.is_dir&&b.is_dir)return 1;}if(vA<vB)return o==='asc'?-1:1;if(vA>vB)return o==='asc'?1:-1;return 0;}

function selectItemAndMenu(p,d,x,y){ ctxTarget=p; ctxIsDir=d; const m=document.getElementById('contextMenu'); 
    let h = d ? '<div class="ctx-item" onclick="selectFolder(ctxTarget)">📂 Open</div>' : ''; // ファイルの場合はプレビュー不要(クリックで自動)


// 👉 フォルダの場合（Open メニューを追加）
if (d) {
    h += `<div class="ctx-item" onclick="selectFolder('${p}')">📂 Open</div>`;
}
if (d) {

    h += `
    <div class="ctx-item" onclick="protectFolder('${p}')">🔒 保護 (.protected)</div>
    <div class="ctx-item" onclick="unprotectFolder('${p}')">🔓 保護解除</div>
    `;
}
// 👉 ZIP 圧縮（ファイル・フォルダ両方 OK）
h += `<div class="ctx-item" onclick="zipCompress('${p}')">📦 ZIP Compress</div>`;

// 👉 ZIP の場合：解凍を追加
if (p.toLowerCase().endsWith('.zip')) {
    h += `<div class="ctx-item" onclick="zipExtract('${p}')">🗜️ Extract ZIP</div>`;
}

h += `
    <div class="ctx-sep"></div>
    <div class="ctx-item" onclick="openSystemDialog('rename', ctxTarget)">🏷️ Rename</div>
    <div class="ctx-item" onclick="openSystemDialog('chmod', ctxTarget)">🔧 Permission</div>
    <div class="ctx-sep"></div>
    <div class="ctx-item" style="color:red" onclick="openSystemDialog('delete', ctxTarget)">🗑️ Delete</div>
`;

    m.innerHTML=h; m.style.left=x+'px'; m.style.top=y+'px'; m.style.display='block';
}
document.onclick=()=>{document.getElementById('contextMenu').style.display='none';};
function execCtx(act) { document.getElementById('contextMenu').style.display = 'none'; if (act==='reload'){ loadFolders(); selectFolder(currentFolder); } else if (act==='mkdir') openSystemDialog('mkdir'); else if (act==='mkfile') openSystemDialog('mkfile'); else if (act==='upload') document.getElementById('btnUpload').click(); else if (act==='open') selectFolder(ctxTarget); else if (act==='rename') openSystemDialog('rename', ctxTarget); else if (act==='delete') openSystemDialog('delete', ctxTarget); else if (act==='chmod') openSystemDialog('chmod', ctxTarget); else if (act==='dl') { const l=document.createElement('a'); l.href=API_PROXY+'?action=download&path='+encodeURIComponent(ctxTarget); document.body.appendChild(l); l.click(); l.remove(); } }

function openSystemDialog(m,t=''){const d=document.getElementById('systemModal');sysDialogMode=m;sysOriginalPath=t;sysTargetDir=currentFolder;if(['rename','delete','chmod'].includes(m))sysTargetDir=t.substring(0,t.lastIndexOf('/'))||'';updateSysTreePath();d.style.display='block';document.getElementById('sysModalInputGroup').style.display='block';document.getElementById('sysModalTreeGroup').style.display='block';document.getElementById('sysModalMessage').style.display='none';document.getElementById('sysModalInput').value='';const ti=document.getElementById('sysModalTitle');const bt=document.getElementById('sysModalSubmit');if(m==='mkdir'){ti.textContent='New Folder';bt.textContent='Create';renderSysTree(loadedTreeData,document.getElementById('sysModalTree'));document.getElementById('sysModalInput').focus();}else if(m==='mkfile'){ti.textContent='New File';document.getElementById('sysModalInput').value='new.txt';bt.textContent='Create';renderSysTree(loadedTreeData,document.getElementById('sysModalTree'));document.getElementById('sysModalInput').focus();}else if(m==='rename'){ti.textContent='Rename';document.getElementById('sysModalInput').value=t.split('/').pop();bt.textContent='Change';renderSysTree(loadedTreeData,document.getElementById('sysModalTree'));document.getElementById('sysModalInput').focus();}else if(m==='delete'){ti.textContent='Delete';document.getElementById('sysModalInputGroup').style.display='none';document.getElementById('sysModalTreeGroup').style.display='none';document.getElementById('sysModalMessage').style.display='block';document.getElementById('sysModalMessage').innerHTML=`Delete: <b>${t}</b>`;bt.textContent='Delete';}else if(m==='chmod'){ti.textContent='Permission';document.getElementById('sysModalLabel').textContent='e.g. 755';document.getElementById('sysModalInput').value='0755';document.getElementById('sysModalTreeGroup').style.display='none';bt.textContent='Change';document.getElementById('sysModalInput').focus();}bt.onclick=executeSysAction;}
function closeSystemModal(){document.getElementById('systemModal').style.display='none';}
function executeSysAction(){const n=document.getElementById('sysModalInput').value.trim();if(sysDialogMode!=='delete'&&!n)return alert('Input name');if(sysDialogMode==='mkdir')apiPost(API_MKDIR,{action:'mkdir',dir:sysTargetDir,name:n},()=>finishSysAction());else if(sysDialogMode==='mkfile')apiPost(API_CREATE_FILE,{action:'create_file',dir:sysTargetDir,name:n},()=>finishSysAction());else if(sysDialogMode==='rename')apiPost(API_RENAME,{action:'rename',src:sysOriginalPath,dst:(sysTargetDir?sysTargetDir+'/':'')+n},()=>finishSysAction());else if(sysDialogMode==='delete')apiPost(API_DELETE,{action:'delete',path:sysOriginalPath},()=>{currentFile='';if(currentFolder===sysOriginalPath)currentFolder='';finishSysAction();});else if(sysDialogMode==='chmod')apiPost(API_CHMOD,{action:'chmod',path:sysOriginalPath,mode:n},()=>finishSysAction());}
function finishSysAction(){closeSystemModal();loadFolders();try{selectFolder(currentFolder);}catch(e){selectFolder('');}}
function renderSysTree(n,c){c.innerHTML='';const tr=(l,p)=>{l.forEach(x=>{if(!x.is_dir)return;const d=document.createElement('div');d.style.padding='2px 5px';d.style.cursor='pointer';if(x.path===sysTargetDir)d.style.fontWeight='bold';d.innerHTML='📁 '+x.name;d.onclick=(e)=>{e.stopPropagation();sysTargetDir=x.path;document.getElementById('sysModalPath').textContent=sysTargetDir||'/';renderSysTree(loadedTreeData,document.getElementById('sysModalTree'));};p.appendChild(d);if(x.children){const cc=document.createElement('div');cc.style.paddingLeft='15px';p.appendChild(cc);tr(x.children,cc);}});};const r=document.createElement('div');r.innerHTML='🏠 Root';r.style.padding='2px 5px';r.style.cursor='pointer';r.onclick=()=>{sysTargetDir='';document.getElementById('sysModalPath').textContent='/';renderSysTree(loadedTreeData,document.getElementById('sysModalTree'));};c.appendChild(r);tr(n,c);}
function updateSysTreePath(){document.getElementById('sysModalPath').textContent=sysTargetDir?sysTargetDir:'(Root)';}

document.getElementById('btnReload').onclick=()=>{loadFolders();selectFolder(currentFolder);};
document.getElementById('btnBack').onclick = () => {
    if (historyIndex > 0) {
        historyLock = true;
        historyIndex--;
        selectFolder(folderHistory[historyIndex], true);
        historyLock = false;
    }
};

document.getElementById('btnForward').onclick = () => {
    if (historyIndex < folderHistory.length - 1) {
        historyLock = true;
        historyIndex++;
        selectFolder(folderHistory[historyIndex], true);
        historyLock = false;
    }
};

document.getElementById('btnNewFolder').onclick=()=>openSystemDialog('mkdir');
document.getElementById('btnNewFile').onclick=()=>openSystemDialog('mkfile');
document.getElementById('btnRename').onclick=()=>{const t=currentFile?(currentFolder?currentFolder+'/'+currentFile:currentFile):currentFolder;if(!t)return alert('Select item');openSystemDialog('rename',t);};
document.getElementById('btnDelete').onclick=()=>{const t=currentFile?(currentFolder?currentFolder+'/'+currentFile:currentFile):currentFolder;if(!t)return alert('Select item');openSystemDialog('delete',t);};
document.getElementById('btnProtect').onclick = () => {

    if (!currentFolder)
        return alert('Select folder');

    protectFolder(currentFolder);
};

document.getElementById('btnUnprotect').onclick = () => {

    if (!currentFolder)
        return alert('Select folder');

    unprotectFolder(currentFolder);
};
document.getElementById('btnPassword').onclick = () => {
    const pass = prompt('理事会共通パスワード（閲覧用）を設定', '');
    if (pass === null) return;
    if (!pass) { alert('空は設定できません'); return; }
    const fd = new FormData();
    fd.append('action', 'set_boardroom_password');
    fd.append('password', pass);
    fetch(API_PROXY, { method: 'POST', body: fd })
        .then(r => r.json())
        .then(j => {
            if (j.status === 'ok') alert('理事会パスワードを保存しました\nconfig/boardroom.json');
            else alert('エラー: ' + (j.message || ''));
        });
};
document.getElementById('btnUpload').onclick=()=>{
    const i=document.createElement('input');
    i.type='file';
    i.multiple=true;
    i.onchange=async()=>{
        if(!i.files.length)return;
        const list=[];
        for(let n=0;n<i.files.length;n++){
            const f=i.files[n];
            list.push({ file:f, relativePath:(f.webkitRelativePath||f.name).replace(/\\/g,'/') });
        }
        if(window.rscopeUploadFileList) await window.rscopeUploadFileList(list,'アップロード');
    };
    i.click();
};
document.getElementById('btnUploadFolder').onclick=()=>{
    const i=document.createElement('input');
    i.type='file';
    i.multiple=true;
    i.webkitdirectory=true;
    i.onchange=async()=>{
        if(!i.files.length)return;
        const list=[];
        for(let n=0;n<i.files.length;n++){
            const f=i.files[n];
            list.push({ file:f, relativePath:(f.webkitRelativePath||f.name).replace(/\\/g,'/') });
        }
        if(window.rscopeUploadFileList) await window.rscopeUploadFileList(list,'フォルダコピー');
    };
    i.click();
};
document.getElementById('btnListView').onclick = () => {
    viewMode = 'list';
    renderFiles();
};
function zipCompress(path) {
    if (!confirm("Compress to ZIP?\n\n" + path)) return;

    const fd = new FormData();
    fd.append('action','zip_compress');
    fd.append('path', path);

    document.getElementById('previewBox').innerHTML = "Zipping...";

    fetch(API_PROXY, { method:'POST', body: fd })
        .then(r=>r.json())
        .then(j=>{
            if(j.status==='ok'){
                alert("ZIP created: " + j.zip);
                selectFolder(currentFolder);
            } else {
                alert("Error: " + j.message);
            }
        });
}

function protectFolder(path)
{
    const fd = new FormData();

    fd.append('action', 'protect');
    fd.append('path', path);

    fetch(API_PROXY, {
        method: 'POST',
        body: fd
    })
    .then(r => r.json())
    .then(j => {
        if (j.status === 'ok') alert('保護しました（.protected）\n理事会パスワードで閲覧できます');
        else alert('エラー: ' + (j.message || ''));
        loadFolders();
        selectFolder(currentFolder);
    })
    .catch(err => alert(err));
}

function unprotectFolder(path)
{
    const fd = new FormData();

    fd.append('action', 'unprotect');
    fd.append('path', path);

    fetch(API_PROXY, {
        method: 'POST',
        body: fd
    })
    .then(r => r.json())
    .then(j => {
        if (j.status === 'ok') alert('保護を解除しました');
        else alert('エラー: ' + (j.message || ''));
        loadFolders();
        selectFolder(currentFolder);
    })
    .catch(err => alert(err));
}

function zipExtract(path) {
    if (!confirm("Extract ZIP?\n\n" + path)) return;

    const fd = new FormData();
    fd.append('action','zip_extract');
    fd.append('path', path);

    document.getElementById('previewBox').innerHTML = "Extracting...";

    fetch(API_PROXY, { method:'POST', body: fd })
        .then(r=>r.json())
        .then(j=>{
            if(j.status==='ok'){
                alert("Extracted into folder: " + j.folder);
                selectFolder(currentFolder);
            } else {
                alert("Error: " + j.message);
            }
        });
}

document.getElementById('btnThumbView').onclick = () => {
    viewMode = 'thumb';
    renderFiles();
};

// =======================
// Image Editor
// =======================
function imageMimeFromExt(ext) {
    const map = { jpg:'image/jpeg', jpeg:'image/jpeg', png:'image/png', gif:'image/gif', webp:'image/webp', bmp:'image/png' };
    return map[ext] || 'image/png';
}

function initImageEditor(box, actions, path, fileUrl) {
    const ext = path.split('.').pop().toLowerCase();
    const mime = imageMimeFromExt(ext);
    const dir = path.includes('/') ? path.substring(0, path.lastIndexOf('/')) : '';
    const baseName = path.split('/').pop();

    box.innerHTML = '';
    actions.innerHTML = '';

    const wrap = document.createElement('div');
    wrap.className = 'image-editor-wrap';
    const toolbar = document.createElement('div');
    toolbar.className = 'image-editor-toolbar';
    const resizePanel = document.createElement('div');
    resizePanel.className = 'image-editor-panel';
    const qualityPanel = document.createElement('div');
    qualityPanel.className = 'image-editor-panel';
    const viewport = document.createElement('div');
    viewport.className = 'image-editor-viewport';
    const modeBar = document.createElement('div');
    modeBar.className = 'image-editor-mode-bar';
    const annotPanel = document.createElement('div');
    annotPanel.className = 'image-annot-panel open';
    const stage = document.createElement('div');
    stage.className = 'image-editor-stage annotating';
    const photoCanvas = document.createElement('canvas');
    photoCanvas.className = 'image-editor-canvas photo-layer';
    const annotCanvas = document.createElement('canvas');
    annotCanvas.className = 'image-editor-canvas annot-layer';
    const cropOverlay = document.createElement('div');
    cropOverlay.className = 'image-crop-overlay';
    const status = document.createElement('div');
    status.className = 'image-editor-status';

    stage.appendChild(photoCanvas);
    stage.appendChild(annotCanvas);
    stage.appendChild(cropOverlay);
    viewport.appendChild(stage);
    wrap.appendChild(modeBar);
    wrap.appendChild(annotPanel);
    wrap.appendChild(toolbar);
    wrap.appendChild(resizePanel);
    wrap.appendChild(qualityPanel);
    wrap.appendChild(viewport);
    wrap.appendChild(status);
    box.appendChild(wrap);

    const photoCtx = photoCanvas.getContext('2d');
    const annotCtx = annotCanvas.getContext('2d');
    const original = document.createElement('canvas');
    const originalCtx = original.getContext('2d');

    let editorMode = 'annotate';
    let annotations = [];
    let annotDraft = null;
    let annotTool = 'arrow';
    let annotStep = 0;
    let annotTemp = null;
    let stampChoice = '要確認';
    const strokeColor = '#e53935';
    const STAMPS = ['要確認', '済', 'NG', '危険', 'OK', '再撮', '保留', 'No.'];

    const work = document.createElement('canvas');
    const ctx = work.getContext('2d', { willReadFrequently: true });
    const base = document.createElement('canvas');
    const baseCtx = base.getContext('2d', { willReadFrequently: true });

    let viewScale = 1;
    let cropMode = false;
    let cropDragging = false;
    let cropStart = null;
    let cropRect = null;
    let jpegQuality = 0.92;
    let adj = { brightness:0, contrast:0, saturation:0, sharpen:0, blur:0, grayscale:false, sepia:false };

    const inpW = document.createElement('input');
    inpW.type = 'number';
    inpW.min = 1;
    const inpH = document.createElement('input');
    inpH.type = 'number';
    inpH.min = 1;
    const chkLock = document.createElement('input');
    chkLock.type = 'checkbox';
    chkLock.checked = true;

    function commitBase() {
        base.width = work.width;
        base.height = work.height;
        baseCtx.drawImage(work, 0, 0);
        resetAdjSliders();
    }

    function resetAdjSliders() {
        adj = { brightness:0, contrast:0, saturation:0, sharpen:0, blur:0, grayscale:false, sepia:false };
        syncAdjInputs();
    }

    function syncAdjInputs() {
        if (rngBright) rngBright.value = adj.brightness;
        if (rngContrast) rngContrast.value = adj.contrast;
        if (rngSat) rngSat.value = adj.saturation;
        if (rngSharp) rngSharp.value = adj.sharpen;
        if (rngBlur) rngBlur.value = adj.blur;
        if (lblBright) lblBright.textContent = adj.brightness;
        if (lblContrast) lblContrast.textContent = adj.contrast;
        if (lblSat) lblSat.textContent = adj.saturation;
        if (lblSharp) lblSharp.textContent = adj.sharpen;
        if (lblBlur) lblBlur.textContent = adj.blur;
    }

    let rngBright, rngContrast, rngSat, rngSharp, rngBlur;
    let lblBright, lblContrast, lblSat, lblSharp, lblBlur;

    function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }

    function applyContrast(v, c) {
        const f = (259 * (c + 255)) / (255 * (259 - c));
        return clamp(f * (v - 128) + 128, 0, 255);
    }

    function renderAdjustments() {
        if (!base.width) return;
        const w = base.width;
        const h = base.height;
        const tmp = document.createElement('canvas');
        tmp.width = w;
        tmp.height = h;
        const tctx = tmp.getContext('2d');
        if (adj.blur > 0) {
            tctx.filter = 'blur(' + adj.blur + 'px)';
            tctx.drawImage(base, 0, 0);
            tctx.filter = 'none';
        } else {
            tctx.drawImage(base, 0, 0);
        }
        const img = tctx.getImageData(0, 0, w, h);
        const d = img.data;
        const br = adj.brightness * 2.55;
        const ct = adj.contrast * 2.55;
        const sat = adj.saturation / 100;
        for (let i = 0; i < d.length; i += 4) {
            let r = d[i], g = d[i + 1], b = d[i + 2];
            r = applyContrast(r + br, ct);
            g = applyContrast(g + br, ct);
            b = applyContrast(b + br, ct);
            const gray = 0.299 * r + 0.587 * g + 0.114 * b;
            if (adj.saturation !== 0) {
                r = gray + (r - gray) * (1 + sat);
                g = gray + (g - gray) * (1 + sat);
                b = gray + (b - gray) * (1 + sat);
            }
            if (adj.grayscale) {
                r = g = b = gray;
            } else if (adj.sepia) {
                const tr = 0.393 * r + 0.769 * g + 0.189 * b;
                const tg = 0.349 * r + 0.686 * g + 0.168 * b;
                const tb = 0.272 * r + 0.534 * g + 0.131 * b;
                r = tr; g = tg; b = tb;
            }
            d[i] = clamp(r, 0, 255);
            d[i + 1] = clamp(g, 0, 255);
            d[i + 2] = clamp(b, 0, 255);
        }
        tctx.putImageData(img, 0, 0);

        if (adj.sharpen > 0) {
            const sImg = tctx.getImageData(0, 0, w, h);
            const out = new Uint8ClampedArray(sImg.data);
            const amount = adj.sharpen / 100;
            const k = [0, -1, 0, -1, 5, -1, 0, -1, 0];
            for (let y = 1; y < h - 1; y++) {
                for (let x = 1; x < w - 1; x++) {
                    for (let c = 0; c < 3; c++) {
                        let sum = 0;
                        let ki = 0;
                        for (let ky = -1; ky <= 1; ky++) {
                            for (let kx = -1; kx <= 1; kx++) {
                                const idx = ((y + ky) * w + (x + kx)) * 4 + c;
                                sum += sImg.data[idx] * k[ki++];
                            }
                        }
                        const idx = (y * w + x) * 4 + c;
                        const orig = sImg.data[idx];
                        out[idx] = clamp(orig + (sum - orig) * amount, 0, 255);
                    }
                }
            }
            sImg.data.set(out);
            tctx.putImageData(sImg, 0, 0);
        }

        work.width = w;
        work.height = h;
        ctx.drawImage(tmp, 0, 0);
        drawToDisplay();
    }

    function updateStatus() {
        const rnoteName = baseName + '.rnote';
        let msg = work.width + ' × ' + work.height + ' px  |  表示 ' + Math.round(viewScale * 100) + '%';
        if (editorMode === 'annotate') {
            msg += '  |  現場注釈 ' + annotations.length + '件 → ' + rnoteName;
            if (annotTool) msg += '  |  ツール: ' + annotToolLabel(annotTool);
        } else if (cropMode) {
            msg += '  |  ドラッグで範囲指定 → トリミング適用';
        } else {
            msg += '  |  ピクセル編集モード';
        }
        status.textContent = msg;
    }

    function annotToolLabel(t) {
        const m = { arrow:'矢印', text:'テキスト', circle:'○囲み', dimension:'寸法線', stamp:'スタンプ', select:'選択' };
        return m[t] || t;
    }

    function applyViewScale() {
        const scale = 'scale(' + viewScale + ')';
        photoCanvas.style.transform = scale;
        annotCanvas.style.transform = scale;
        photoCanvas.style.transformOrigin = 'center center';
        annotCanvas.style.transformOrigin = 'center center';
        updateStatus();
        syncCropOverlay();
    }

    function drawToDisplay() {
        if (editorMode === 'annotate') {
            photoCanvas.width = original.width;
            photoCanvas.height = original.height;
            photoCtx.drawImage(original, 0, 0);
            annotCanvas.width = original.width;
            annotCanvas.height = original.height;
            annotCanvas.style.display = 'block';
            renderAnnotations();
        } else {
            photoCanvas.width = work.width;
            photoCanvas.height = work.height;
            photoCtx.drawImage(work, 0, 0);
            annotCanvas.style.display = 'none';
        }
        applyViewScale();
        inpW.value = work.width;
        inpH.value = work.height;
    }

    function setEditorMode(mode) {
        editorMode = mode;
        stage.classList.toggle('annotating', mode === 'annotate');
        annotPanel.classList.toggle('open', mode === 'annotate');
        toolbar.style.display = mode === 'pixel' ? 'flex' : 'none';
        resizePanel.classList.remove('open');
        qualityPanel.classList.remove('open');
        if (mode === 'annotate') {
            setCropMode(false);
            drawToDisplay();
        } else {
            drawToDisplay();
        }
        updateStatus();
    }

    function newAnnotId() { return 'a' + Date.now() + '_' + Math.random().toString(36).slice(2, 7); }

    function drawArrow(ctx, x1, y1, x2, y2, color, lw) {
        const dx = x2 - x1, dy = y2 - y1;
        const ang = Math.atan2(dy, dx);
        const len = Math.hypot(dx, dy);
        if (len < 2) return;
        const head = Math.min(18, len * 0.25);
        ctx.strokeStyle = color;
        ctx.fillStyle = color;
        ctx.lineWidth = lw;
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(x2, y2);
        ctx.lineTo(x2 - head * Math.cos(ang - 0.4), y2 - head * Math.sin(ang - 0.4));
        ctx.lineTo(x2 - head * Math.cos(ang + 0.4), y2 - head * Math.sin(ang + 0.4));
        ctx.closePath();
        ctx.fill();
    }

    function drawDimension(ctx, a) {
        const c = a.color || strokeColor;
        const lw = a.lineWidth || 2;
        const x1 = a.x1, y1 = a.y1, x2 = a.x2, y2 = a.y2;
        const ang = Math.atan2(y2 - y1, x2 - x1);
        const perp = ang + Math.PI / 2;
        const ext = 8;
        ctx.strokeStyle = c;
        ctx.fillStyle = c;
        ctx.lineWidth = lw;
        ctx.beginPath();
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
        ctx.stroke();
        [[x1, y1], [x2, y2]].forEach(([x, y]) => {
            ctx.beginPath();
            ctx.moveTo(x + ext * Math.cos(perp), y + ext * Math.sin(perp));
            ctx.lineTo(x - ext * Math.cos(perp), y - ext * Math.sin(perp));
            ctx.stroke();
        });
        const label = a.label || '';
        if (label) {
            const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
            ctx.font = 'bold ' + (a.fontSize || 14) + 'px sans-serif';
            const tw = ctx.measureText(label).width;
            ctx.fillStyle = 'rgba(255,255,255,0.92)';
            ctx.fillRect(mx - tw / 2 - 4, my - 12, tw + 8, 20);
            ctx.fillStyle = c;
            ctx.fillText(label, mx - tw / 2, my + 4);
        }
    }

    function drawStamp(ctx, a) {
        const text = a.stamp || '要確認';
        const fs = a.fontSize || 18;
        ctx.font = 'bold ' + fs + 'px sans-serif';
        const tw = ctx.measureText(text).width;
        const pad = 6;
        const w = tw + pad * 2, h = fs + pad;
        const x = a.x - w / 2, y = a.y - h / 2;
        ctx.fillStyle = a.fill || 'rgba(255,235,59,0.85)';
        ctx.strokeStyle = a.color || '#c62828';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.rect(x, y, w, h);
        ctx.fill();
        ctx.stroke();
        ctx.fillStyle = a.color || '#c62828';
        ctx.fillText(text, x + pad, y + fs);
    }

    function drawOneAnnotation(ctx, a) {
        const c = a.color || strokeColor;
        const lw = a.lineWidth || 2;
        ctx.save();
        switch (a.type) {
            case 'arrow':
                drawArrow(ctx, a.x1, a.y1, a.x2, a.y2, c, lw);
                break;
            case 'circle':
                ctx.strokeStyle = c;
                ctx.lineWidth = lw;
                ctx.beginPath();
                ctx.arc(a.cx, a.cy, a.r, 0, Math.PI * 2);
                ctx.stroke();
                break;
            case 'text':
                ctx.font = (a.fontSize || 16) + 'px sans-serif';
                ctx.fillStyle = c;
                ctx.fillText(a.text || '', a.x, a.y);
                break;
            case 'dimension':
                drawDimension(ctx, a);
                break;
            case 'stamp':
                drawStamp(ctx, a);
                break;
        }
        ctx.restore();
    }

    function renderAnnotations() {
        if (!annotCanvas.width) return;
        annotCtx.clearRect(0, 0, annotCanvas.width, annotCanvas.height);
        annotations.forEach(a => drawOneAnnotation(annotCtx, a));
        if (annotDraft) drawOneAnnotation(annotCtx, annotDraft);
    }

    function loadRnote() {
        fetch(API_PROXY + '?action=get_rnote&path=' + encodeURIComponent(path) + '&t=' + Date.now())
            .then(r => r.json())
            .then(j => {
                if (j.status === 'ok' && j.data && Array.isArray(j.data.annotations)) {
                    annotations = j.data.annotations;
                } else {
                    annotations = [];
                }
                renderAnnotations();
                updateStatus();
            })
            .catch(() => { annotations = []; });
    }

    function saveRnote() {
        const doc = {
            format: 'rnote',
            version: 1,
            scope: 'R-Scope',
            image: baseName,
            imagePath: path,
            imageWidth: original.width,
            imageHeight: original.height,
            updated: new Date().toISOString(),
            annotations: annotations
        };
        const fd = new FormData();
        fd.append('action', 'save_rnote');
        fd.append('path', path);
        fd.append('content', JSON.stringify(doc));
        status.textContent = '注釈を保存中...';
        fetch(API_PROXY, { method: 'POST', body: fd })
            .then(r => r.json())
            .then(j => {
                if (j.status === 'ok') {
                    alert('注釈を保存しました: ' + baseName + '.rnote\n（写真本体は変更していません）');
                    selectFolder(currentFolder);
                } else {
                    alert('保存エラー: ' + (j.message || ''));
                }
                updateStatus();
            })
            .catch(() => { alert('保存エラー'); updateStatus(); });
    }

    function imagePointFromEvent(e) {
        const rect = annotCanvas.getBoundingClientRect();
        return {
            x: Math.max(0, Math.min(annotCanvas.width, (e.clientX - rect.left) / viewScale)),
            y: Math.max(0, Math.min(annotCanvas.height, (e.clientY - rect.top) / viewScale))
        };
    }

    function finishDraft() {
        if (!annotDraft) return;
        annotDraft.id = newAnnotId();
        annotations.push(annotDraft);
        annotDraft = null;
        annotStep = 0;
        annotTemp = null;
        renderAnnotations();
        updateStatus();
    }

    function onAnnotMouseDown(e) {
        if (editorMode !== 'annotate') return;
        e.preventDefault();
        const p = imagePointFromEvent(e);
        if (annotTool === 'text') {
            const t = prompt('テキストを入力', '');
            if (t) {
                annotations.push({ id: newAnnotId(), type: 'text', x: p.x, y: p.y, text: t, color: strokeColor, fontSize: 16 });
                renderAnnotations();
                updateStatus();
            }
            return;
        }
        if (annotTool === 'stamp') {
            annotations.push({ id: newAnnotId(), type: 'stamp', x: p.x, y: p.y, stamp: stampChoice, color: '#c62828' });
            renderAnnotations();
            updateStatus();
            return;
        }
        if (annotTool === 'dimension') {
            if (annotStep === 0) {
                annotTemp = { x1: p.x, y1: p.y };
                annotStep = 1;
                annotDraft = { type: 'dimension', x1: p.x, y1: p.y, x2: p.x, y2: p.y, label: '', color: strokeColor };
            } else if (annotStep === 1) {
                annotDraft.x2 = p.x;
                annotDraft.y2 = p.y;
                const label = prompt('寸法・距離のラベル（例: 3500mm）', '');
                annotDraft.label = label || '';
                finishDraft();
            }
            return;
        }
        annotDragging = true;
        annotStart = p;
        if (annotTool === 'arrow') {
            annotDraft = { type: 'arrow', x1: p.x, y1: p.y, x2: p.x, y2: p.y, color: strokeColor, lineWidth: 2 };
        } else if (annotTool === 'circle') {
            annotDraft = { type: 'circle', cx: p.x, cy: p.y, r: 0, color: strokeColor, lineWidth: 2 };
        }
    }

    let annotDragging = false;
    let annotStart = null;

    function onAnnotMouseMove(e) {
        if (editorMode !== 'annotate') return;
        const p = imagePointFromEvent(e);
        if (annotTool === 'dimension' && annotStep === 1 && annotDraft) {
            annotDraft.x2 = p.x;
            annotDraft.y2 = p.y;
            renderAnnotations();
            return;
        }
        if (!annotDragging || !annotDraft) return;
        if (annotTool === 'arrow') {
            annotDraft.x2 = p.x;
            annotDraft.y2 = p.y;
        } else if (annotTool === 'circle') {
            annotDraft.r = Math.hypot(p.x - annotStart.x, p.y - annotStart.y);
        }
        renderAnnotations();
    }

    function onAnnotMouseUp(e) {
        if (editorMode !== 'annotate') return;
        if (annotTool === 'dimension') return;
        if (!annotDragging) return;
        annotDragging = false;
        if (annotTool === 'arrow' && annotDraft) {
            const len = Math.hypot(annotDraft.x2 - annotDraft.x1, annotDraft.y2 - annotDraft.y1);
            if (len >= 5) finishDraft();
            else { annotDraft = null; renderAnnotations(); }
        } else if (annotTool === 'circle' && annotDraft && annotDraft.r >= 3) {
            finishDraft();
        } else {
            annotDraft = null;
            renderAnnotations();
        }
    }

    function syncCropOverlay() {
        if (!cropRect || !cropMode) {
            cropOverlay.style.display = 'none';
            return;
        }
        cropOverlay.style.display = 'block';
        const s = viewScale;
        cropOverlay.style.left = (cropRect.x * s) + 'px';
        cropOverlay.style.top = (cropRect.y * s) + 'px';
        cropOverlay.style.width = (cropRect.w * s) + 'px';
        cropOverlay.style.height = (cropRect.h * s) + 'px';
    }

    function displayToImageCoords(clientX, clientY) {
        const rect = photoCanvas.getBoundingClientRect();
        return {
            x: Math.max(0, Math.min(work.width, (clientX - rect.left) / viewScale)),
            y: Math.max(0, Math.min(work.height, (clientY - rect.top) / viewScale))
        };
    }

    function setCropMode(on) {
        cropMode = on;
        photoCanvas.classList.toggle('crop-mode', on);
        btnCrop.classList.toggle('active', on);
        btnApplyCrop.style.display = on ? '' : 'none';
        if (!on) { cropRect = null; syncCropOverlay(); }
        updateStatus();
    }

    function togglePanel(panel, btn) {
        const open = !panel.classList.contains('open');
        resizePanel.classList.remove('open');
        qualityPanel.classList.remove('open');
        btnResize.classList.remove('active');
        btnQuality.classList.remove('active');
        if (open) {
            panel.classList.add('open');
            btn.classList.add('active');
        }
    }

    function loadFromUrl(url) {
        status.textContent = '読み込み中...';
        const img = new Image();
        img.onload = function() {
            original.width = img.naturalWidth;
            original.height = img.naturalHeight;
            originalCtx.drawImage(img, 0, 0);
            work.width = img.naturalWidth;
            work.height = img.naturalHeight;
            ctx.drawImage(img, 0, 0);
            commitBase();
            viewScale = 1;
            cropRect = null;
            setCropMode(false);
            loadRnote();
            setEditorMode(editorMode);
            fitView();
        };
        img.onerror = function() { status.textContent = '画像の読み込みに失敗しました'; };
        img.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 't=' + Date.now();
    }

    function fitView() {
        const pad = 24;
        const vw = viewport.clientWidth - pad;
        const vh = viewport.clientHeight - pad;
        if (work.width && work.height && vw > 0 && vh > 0) {
            viewScale = Math.min(1, vw / work.width, vh / work.height);
            viewScale = Math.max(0.1, Math.round(viewScale * 100) / 100);
        }
        applyViewScale();
    }

    function rotate90() {
        const w = work.width, h = work.height;
        const tmp = document.createElement('canvas');
        tmp.width = h; tmp.height = w;
        const tctx = tmp.getContext('2d');
        tctx.translate(h / 2, w / 2);
        tctx.rotate(Math.PI / 2);
        tctx.drawImage(work, -w / 2, -h / 2);
        work.width = h; work.height = w;
        ctx.drawImage(tmp, 0, 0);
        commitBase();
        cropRect = null;
        drawToDisplay();
        fitView();
    }

    function applyCrop() {
        if (!cropRect || Math.abs(cropRect.w) < 2 || Math.abs(cropRect.h) < 2) {
            alert('トリミング範囲を指定してください');
            return;
        }
        const x = Math.round(Math.min(cropRect.x, cropRect.x + cropRect.w));
        const y = Math.round(Math.min(cropRect.y, cropRect.y + cropRect.h));
        const w = Math.round(Math.abs(cropRect.w));
        const h = Math.round(Math.abs(cropRect.h));
        const tmp = document.createElement('canvas');
        tmp.width = w; tmp.height = h;
        tmp.getContext('2d').drawImage(work, x, y, w, h, 0, 0, w, h);
        work.width = w; work.height = h;
        ctx.drawImage(tmp, 0, 0);
        commitBase();
        setCropMode(false);
        drawToDisplay();
        fitView();
    }

    function applyResize() {
        const nw = parseInt(inpW.value, 10);
        const nh = parseInt(inpH.value, 10);
        if (!nw || !nh || nw < 1 || nh < 1) {
            alert('幅・高さを正しく入力してください');
            return;
        }
        const tmp = document.createElement('canvas');
        tmp.width = nw; tmp.height = nh;
        const tctx = tmp.getContext('2d');
        tctx.imageSmoothingEnabled = true;
        tctx.imageSmoothingQuality = 'high';
        tctx.drawImage(work, 0, 0, nw, nh);
        work.width = nw; work.height = nh;
        ctx.drawImage(tmp, 0, 0);
        commitBase();
        drawToDisplay();
        fitView();
        resizePanel.classList.remove('open');
        btnResize.classList.remove('active');
    }

    function getDataUrl() {
        const q = (mime === 'image/jpeg') ? jpegQuality : undefined;
        return work.toDataURL(mime, q);
    }

    function postImage(action, targetPath, onOk) {
        const fd = new FormData();
        fd.append('action', action);
        fd.append('path', targetPath);
        fd.append('image', getDataUrl());
        status.textContent = '保存中...';
        fetch(API_PROXY, { method: 'POST', body: fd })
            .then(r => r.json())
            .then(j => {
                if (j.status === 'ok') {
                    alert('保存しました');
                    if (onOk) onOk(j);
                } else {
                    alert('保存エラー: ' + (j.message || ''));
                }
                updateStatus();
            })
            .catch(() => { alert('保存エラー'); updateStatus(); });
    }

    function saveImage() {
        if (!confirm('元ファイルを上書きしますか？')) return;
        postImage('save_image', path, () => loadFromUrl(fileUrl));
    }

    function saveImageAs() {
        const def = baseName.replace(/(\.[^.]+)$/, '_edit$1');
        const raw = prompt('別名で保存するファイル名', def);
        if (!raw || !raw.trim()) return;
        const fname = raw.trim().replace(/\\/g, '/').split('/').pop();
        if (!fname) return;
        const newPath = (dir ? dir + '/' : '') + fname;
        postImage('save_image_as', newPath, () => {
            selectFolder(currentFolder);
            selectFile(dir, fname);
        });
    }

    function addToolBtn(label, title, onClick) {
        const b = document.createElement('button');
        b.type = 'button';
        b.textContent = label;
        b.title = title;
        b.onclick = onClick;
        toolbar.appendChild(b);
        return b;
    }

    function addSlider(panel, label, key, min, max, step) {
        const lbl = document.createElement('label');
        const span = document.createElement('span');
        span.textContent = '0';
        const rng = document.createElement('input');
        rng.type = 'range';
        rng.min = min; rng.max = max; rng.step = step || 1; rng.value = 0;
        lbl.appendChild(document.createTextNode(label + ' '));
        lbl.appendChild(rng);
        lbl.appendChild(span);
        panel.appendChild(lbl);
        rng.addEventListener('input', () => {
            adj[key] = parseFloat(rng.value);
            if (key === 'brightness') { adj.grayscale = false; adj.sepia = false; }
            if (key === 'contrast' || key === 'saturation') { /* keep presets */ }
            span.textContent = rng.value;
            renderAdjustments();
        });
        return { rng, span };
    }

    function addQualityCmd(label, fn) {
        const b = document.createElement('button');
        b.type = 'button';
        b.textContent = label;
        b.onclick = fn;
        return b;
    }

    // --- Resize panel ---
    resizePanel.appendChild(document.createTextNode('幅'));
    resizePanel.appendChild(inpW);
    resizePanel.appendChild(document.createTextNode('高さ'));
    resizePanel.appendChild(inpH);
    const lblLock = document.createElement('label');
    lblLock.appendChild(chkLock);
    lblLock.appendChild(document.createTextNode('比率固定'));
    resizePanel.appendChild(lblLock);
    inpW.addEventListener('input', () => {
        if (chkLock.checked && work.height) {
            inpH.value = Math.max(1, Math.round(parseInt(inpW.value, 10) / (work.width / work.height)));
        }
    });
    inpH.addEventListener('input', () => {
        if (chkLock.checked && work.width) {
            inpW.value = Math.max(1, Math.round(parseInt(inpH.value, 10) * (work.width / work.height)));
        }
    });
    const btnResizeApply = document.createElement('button');
    btnResizeApply.textContent = 'リサイズ適用';
    btnResizeApply.onclick = applyResize;
    resizePanel.appendChild(btnResizeApply);

    // --- Quality panel ---
    const s1 = addSlider(qualityPanel, '明るさ', 'brightness', -100, 100);
    rngBright = s1.rng; lblBright = s1.span;
    const s2 = addSlider(qualityPanel, 'コントラスト', 'contrast', -100, 100);
    rngContrast = s2.rng; lblContrast = s2.span;
    const s3 = addSlider(qualityPanel, '彩度', 'saturation', -100, 100);
    rngSat = s3.rng; lblSat = s3.span;
    const s4 = addSlider(qualityPanel, 'シャープ', 'sharpen', 0, 100);
    rngSharp = s4.rng; lblSharp = s4.span;
    const s5 = addSlider(qualityPanel, 'ぼかし', 'blur', 0, 20);
    rngBlur = s5.rng; lblBlur = s5.span;

    if (mime === 'image/jpeg') {
        const lblJq = document.createElement('label');
        const rngJq = document.createElement('input');
        rngJq.type = 'range'; rngJq.min = 50; rngJq.max = 100; rngJq.value = 92;
        const spJq = document.createElement('span');
        spJq.textContent = '92';
        lblJq.appendChild(document.createTextNode('JPEG品質 '));
        lblJq.appendChild(rngJq);
        lblJq.appendChild(spJq);
        qualityPanel.appendChild(lblJq);
        rngJq.addEventListener('input', () => {
            jpegQuality = parseInt(rngJq.value, 10) / 100;
            spJq.textContent = rngJq.value;
        });
    }

    const sep = document.createElement('div');
    sep.className = 'image-editor-sep';
    qualityPanel.appendChild(sep);

    const cmdRow = document.createElement('div');
    cmdRow.className = 'image-quality-cmds';

    function bump(key, delta, min, max) {
        adj[key] = clamp((adj[key] || 0) + delta, min, max);
        adj.grayscale = false;
        adj.sepia = false;
        syncAdjInputs();
        renderAdjustments();
    }

    [
        ['明るさ＋', () => bump('brightness', 10, -100, 100)],
        ['明るさ－', () => bump('brightness', -10, -100, 100)],
        ['コントラスト＋', () => bump('contrast', 10, -100, 100)],
        ['コントラスト－', () => bump('contrast', -10, -100, 100)],
        ['彩度＋', () => bump('saturation', 10, -100, 100)],
        ['彩度－', () => bump('saturation', -10, -100, 100)],
        ['シャープ＋', () => bump('sharpen', 10, 0, 100)],
        ['シャープ－', () => bump('sharpen', -10, 0, 100)],
        ['ぼかし＋', () => bump('blur', 1, 0, 20)],
        ['ぼかし－', () => bump('blur', -1, 0, 20)],
        ['モノクロ', () => { adj.grayscale = true; adj.sepia = false; renderAdjustments(); }],
        ['セピア', () => { adj.sepia = true; adj.grayscale = false; renderAdjustments(); }],
        ['カラーに戻す', () => { adj.grayscale = false; adj.sepia = false; renderAdjustments(); }]
    ].forEach(([label, fn]) => {
        cmdRow.appendChild(addQualityCmd(label, fn));
    });
    qualityPanel.appendChild(cmdRow);

    const btnAdjApply = document.createElement('button');
    btnAdjApply.textContent = '画質を確定';
    btnAdjApply.title = '現在のプレビューを編集結果として確定';
    btnAdjApply.onclick = () => { commitBase(); alert('画質編集を確定しました'); };
    qualityPanel.appendChild(btnAdjApply);

    const btnAdjReset = document.createElement('button');
    btnAdjReset.textContent = '画質リセット';
    btnAdjReset.onclick = () => { resetAdjSliders(); renderAdjustments(); };
    qualityPanel.appendChild(btnAdjReset);

    // --- Toolbar ---
    addToolBtn('🔍＋', '拡大', () => { viewScale = Math.min(5, Math.round((viewScale + 0.1) * 100) / 100); applyViewScale(); });
    addToolBtn('🔍－', '縮小', () => { viewScale = Math.max(0.1, Math.round((viewScale - 0.1) * 100) / 100); applyViewScale(); });
    addToolBtn('⊡ 合わせる', '画面に合わせる', fitView);

    const btnResize = addToolBtn('📐 リサイズ', 'ピクセルサイズ変更', () => togglePanel(resizePanel, btnResize));
    const btnQuality = addToolBtn('🎨 画質編集', '明るさ・コントラスト等', () => togglePanel(qualityPanel, btnQuality));

    const btnCrop = addToolBtn('✂ トリミング', '範囲選択', () => setCropMode(!cropMode));
    const btnApplyCrop = addToolBtn('✓ 切抜適用', 'トリミング適用', applyCrop);
    btnApplyCrop.style.display = 'none';

    addToolBtn('↻ 回転', '90°右回転', rotate90);
    addToolBtn('💾 上書き保存', '元ファイルを上書き', saveImage);
    addToolBtn('📄 別名保存', '新しいファイル名で保存', saveImageAs);
    addToolBtn('↺ リセット', '元画像を再読込', () => {
        if (!confirm('編集を破棄しますか？')) return;
        loadFromUrl(fileUrl);
    });

    // --- Mode bar ---
    const btnModeAnnot = document.createElement('button');
    btnModeAnnot.type = 'button';
    btnModeAnnot.textContent = '📍 現場注釈（原本そのまま）';
    btnModeAnnot.className = 'active';
    btnModeAnnot.onclick = () => {
        btnModeAnnot.classList.add('active');
        btnModePixel.classList.remove('active');
        setEditorMode('annotate');
    };
    modeBar.appendChild(btnModeAnnot);

    const btnModePixel = document.createElement('button');
    btnModePixel.type = 'button';
    btnModePixel.textContent = '🎨 ピクセル編集';
    btnModePixel.onclick = () => {
        if (!confirm('ピクセル編集は写真ファイル自体を変更します。注釈モードに戻すと原本＋.rnoteで表示されます。続けますか？')) return;
        btnModePixel.classList.add('active');
        btnModeAnnot.classList.remove('active');
        setEditorMode('pixel');
    };
    modeBar.appendChild(btnModePixel);

    // --- Annotation tools ---
    function setAnnotTool(tool, btn) {
        annotTool = tool;
        annotPanel.querySelectorAll('button[data-annot-tool]').forEach(b => b.classList.remove('active'));
        if (btn) btn.classList.add('active');
        annotStep = 0;
        annotDraft = null;
        annotTemp = null;
        updateStatus();
    }

    function addAnnotBtn(label, tool) {
        const b = document.createElement('button');
        b.type = 'button';
        b.textContent = label;
        b.dataset.annotTool = tool;
        if (tool === 'arrow') b.classList.add('active');
        b.onclick = () => setAnnotTool(tool, b);
        annotPanel.appendChild(b);
        return b;
    }

    addAnnotBtn('➡ 矢印', 'arrow');
    addAnnotBtn('📝 テキスト', 'text');
    addAnnotBtn('○ 囲み', 'circle');
    addAnnotBtn('📏 寸法線', 'dimension');
    addAnnotBtn('🏷 スタンプ', 'stamp');

    const stampSel = document.createElement('select');
    stampSel.style.padding = '4px';
    STAMPS.forEach(s => {
        const o = document.createElement('option');
        o.value = s; o.textContent = s;
        stampSel.appendChild(o);
    });
    stampSel.value = stampChoice;
    stampSel.onchange = () => { stampChoice = stampSel.value; };
    annotPanel.appendChild(stampSel);

    const btnSaveRnote = document.createElement('button');
    btnSaveRnote.textContent = '💾 注釈保存 (.rnote)';
    btnSaveRnote.onclick = saveRnote;
    annotPanel.appendChild(btnSaveRnote);

    const btnUndoAnnot = document.createElement('button');
    btnUndoAnnot.textContent = '↩ 1つ戻す';
    btnUndoAnnot.onclick = () => {
        if (annotations.length) {
            annotations.pop();
            renderAnnotations();
            updateStatus();
        }
    };
    annotPanel.appendChild(btnUndoAnnot);

    const btnClearAnnot = document.createElement('button');
    btnClearAnnot.textContent = '🗑 注釈全削除';
    btnClearAnnot.onclick = () => {
        if (!annotations.length) return;
        if (!confirm('画面上の注釈をすべて削除しますか？（.rnoteは保存するまで残ります）')) return;
        annotations = [];
        renderAnnotations();
        updateStatus();
    };
    annotPanel.appendChild(btnClearAnnot);

    toolbar.style.display = 'none';

    annotCanvas.addEventListener('mousedown', onAnnotMouseDown);
    annotCanvas.addEventListener('mousemove', onAnnotMouseMove);
    annotCanvas.addEventListener('mouseup', onAnnotMouseUp);

    photoCanvas.addEventListener('mousedown', (e) => {
        if (editorMode !== 'pixel' || !cropMode) return;
        e.preventDefault();
        cropDragging = true;
        cropStart = displayToImageCoords(e.clientX, e.clientY);
        cropRect = { x: cropStart.x, y: cropStart.y, w: 0, h: 0 };
        syncCropOverlay();
    });
    const onMove = (e) => {
        if (!cropDragging || !cropStart) return;
        const p = displayToImageCoords(e.clientX, e.clientY);
        cropRect = { x: cropStart.x, y: cropStart.y, w: p.x - cropStart.x, h: p.y - cropStart.y };
        syncCropOverlay();
    };
    const onUp = () => { cropDragging = false; };
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);

    setEditorMode('annotate');

    const dlBtn = document.createElement('button');
    dlBtn.innerHTML = '⬇ Download';
    dlBtn.onclick = () => {
        const a = document.createElement('a');
        a.href = API_PROXY + '?action=download&path=' + encodeURIComponent(path);
        document.body.appendChild(a);
        a.click();
        a.remove();
    };
    actions.appendChild(dlBtn);

    loadFromUrl(fileUrl);
    window.addEventListener('resize', () => { if (!cropMode) fitView(); });
}

loadFolders();
document.getElementById('fileSearch').addEventListener('input', () => {
    applySearchFilter();
});
</script>
</body>
</html>
