- Docker container with rmapi cloud backup, PDF conversion pipeline - Web UI: file browser, multi-select, delete/move, thumbnail preview - Sync: backup from reMarkable cloud, rmdoc→PDF conversion via rmc+Inkscape - Excluded-files mechanism to prevent deleted items from returning after sync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
119 lines
4.9 KiB
PHP
Executable File
119 lines
4.9 KiB
PHP
Executable File
<?php
|
|
declare(strict_types=1);
|
|
require __DIR__ . '/auth.php';
|
|
|
|
$RMAPI = '/usr/local/bin/rmapi';
|
|
$flash = '';
|
|
$flashOk = true;
|
|
|
|
function h(string $v): string {
|
|
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
|
}
|
|
|
|
// rmapi-Status prüfen
|
|
function rmapiStatus(string $bin): array {
|
|
exec($bin . ' ls / 2>&1', $out, $code);
|
|
return ['ok' => $code === 0, 'out' => implode("\n", $out)];
|
|
}
|
|
|
|
// Token einrichten
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['code'])) {
|
|
$code = trim($_POST['code']);
|
|
$code = preg_replace('/[^a-zA-Z0-9]/', '', $code);
|
|
if (strlen($code) === 8) {
|
|
$descriptors = [
|
|
0 => ['pipe', 'r'],
|
|
1 => ['pipe', 'w'],
|
|
2 => ['pipe', 'w'],
|
|
];
|
|
$proc = proc_open(escapeshellarg($RMAPI) . ' ls /', $descriptors, $pipes);
|
|
if (is_resource($proc)) {
|
|
fwrite($pipes[0], $code . "\n");
|
|
fclose($pipes[0]);
|
|
$out = stream_get_contents($pipes[1]);
|
|
fclose($pipes[1]);
|
|
fclose($pipes[2]);
|
|
$exitCode = proc_close($proc);
|
|
$flashOk = $exitCode === 0;
|
|
$flash = $flashOk ? 'Token erfolgreich eingerichtet!' : 'Fehler: Ungültiger Code oder Verbindungsproblem.';
|
|
}
|
|
} else {
|
|
$flash = 'Code muss genau 8 Zeichen lang sein.';
|
|
$flashOk = false;
|
|
}
|
|
}
|
|
|
|
$status = rmapiStatus($RMAPI);
|
|
?>
|
|
<!doctype html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>reMarkable Setup</title>
|
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='18' fill='%23000'/%3E%3Ctext y='.9em' font-size='72' font-family='Georgia,serif' font-weight='bold' fill='white' x='50%25' text-anchor='middle'%3Erm%3C/text%3E%3C/svg%3E">
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f4f4f4;color:#222;min-height:100vh;display:flex;align-items:center;justify-content:center}
|
|
.card{background:#fff;border-radius:12px;padding:32px;width:440px;max-width:95vw;box-shadow:0 4px 24px rgba(0,0,0,.1)}
|
|
h1{font-size:20px;font-weight:700;margin-bottom:8px}
|
|
.subtitle{font-size:14px;color:#666;margin-bottom:24px}
|
|
.status{padding:12px 16px;border-radius:8px;font-size:14px;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
|
.status.ok{background:#dcfce7;border:1px solid #86efac;color:#166534}
|
|
.status.err{background:#fee2e2;border:1px solid #fca5a5;color:#991b1b}
|
|
.flash{padding:10px 16px;border-radius:8px;font-size:14px;margin-bottom:16px}
|
|
.flash.ok{background:#dcfce7;border:1px solid #86efac;color:#166534}
|
|
.flash.err{background:#fee2e2;border:1px solid #fca5a5;color:#991b1b}
|
|
label{display:block;font-size:13px;font-weight:500;margin-bottom:6px;color:#444}
|
|
input[type=text]{width:100%;padding:10px 12px;border:1px solid #ccc;border-radius:8px;font-size:16px;letter-spacing:4px;text-align:center;margin-bottom:12px}
|
|
.btn{width:100%;padding:10px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;border:none;background:#2563eb;color:#fff}
|
|
.btn:hover{background:#1d4ed8}
|
|
.hint{font-size:13px;color:#666;margin-top:16px;line-height:1.6}
|
|
.hint a{color:#2563eb}
|
|
.back{display:block;text-align:center;margin-top:20px;font-size:13px;color:#2563eb;text-decoration:none}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<h1>📄 reMarkable Setup</h1>
|
|
<p class="subtitle">Verbindung zur reMarkable Cloud einrichten</p>
|
|
|
|
<div class="status <?= $status['ok'] ? 'ok' : 'err' ?>">
|
|
<?= $status['ok'] ? '✓ Verbunden mit reMarkable Cloud' : '✗ Nicht verbunden — Token fehlt oder abgelaufen' ?>
|
|
</div>
|
|
|
|
<?php if ($flash !== ''): ?>
|
|
<div class="flash <?= $flashOk ? 'ok' : 'err' ?>"><?= h($flash) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (!$status['ok']): ?>
|
|
<form method="post">
|
|
<label for="code">One-Time-Code</label>
|
|
<input type="text" id="code" name="code" maxlength="8" placeholder="xxxxxxxx" autocomplete="off" autofocus>
|
|
<button type="submit" class="btn">Verbinden</button>
|
|
</form>
|
|
<p class="hint">
|
|
Code abrufen unter:<br>
|
|
<a href="https://my.remarkable.com/device/desktop/connect" target="_blank">
|
|
my.remarkable.com/device/desktop/connect
|
|
</a>
|
|
</p>
|
|
<?php else: ?>
|
|
<form method="post">
|
|
<label for="code">Token erneuern (neuer One-Time-Code)</label>
|
|
<input type="text" id="code" name="code" maxlength="8" placeholder="xxxxxxxx" autocomplete="off">
|
|
<button type="submit" class="btn btn-secondary" style="background:#6b7280">Token erneuern</button>
|
|
</form>
|
|
<p class="hint">
|
|
Neuen Code abrufen unter:<br>
|
|
<a href="https://my.remarkable.com/device/desktop/connect" target="_blank">
|
|
my.remarkable.com/device/desktop/connect
|
|
</a>
|
|
</p>
|
|
<?php endif; ?>
|
|
|
|
<a href="/" class="back">← Zurück zur Übersicht</a>
|
|
</div>
|
|
</body>
|
|
</html>
|