Add digital badge tab for round-display photo prep

Adds a new "Цифровой бейдж" tab for preparing 240×240 PNG circles for
round digital displays. Photos come from device upload or the existing
Flickr selection. Each photo can be drag-cropped and zoomed (mouse,
wheel, or pinch on touch), have a yellow price tag (arc band or
rectangular plate, with auto thousands separator and ₽), and a curved
nickname signature — which auto-moves to the top of the circle when a
price tag occupies the bottom. Saves are routed through the Web Share
API on phones so the badge lands in the iOS/Android Photos app
directly; history of saved badges is kept under data/badges/. Default
nickname is stored in Settings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-05-14 21:16:37 +03:00
parent a0dd7b1ed4
commit 68f4560bd5
4 changed files with 1445 additions and 0 deletions
+133
View File
@@ -976,6 +976,139 @@ try {
}
break;
// ============ DIGITAL BADGE (round display) ============
case 'get_badge_settings':
$settingsFile = __DIR__ . '/data/badge_settings.json';
if (file_exists($settingsFile)) {
$settings = json_decode(file_get_contents($settingsFile), true) ?: [];
echo json_encode(['success' => true, 'settings' => $settings]);
} else {
echo json_encode(['success' => true, 'settings' => ['nickname' => '']]);
}
break;
case 'save_badge_settings':
$dataDir = __DIR__ . '/data';
if (!is_dir($dataDir)) {
mkdir($dataDir, 0755, true);
}
$settingsFile = $dataDir . '/badge_settings.json';
$settings = [
'nickname' => trim((string)($_POST['nickname'] ?? '')),
];
if (file_put_contents($settingsFile, json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['error' => 'Не удалось сохранить настройки']);
}
break;
case 'badge_save':
// Save generated badge PNG (sent as base64 data URL) to /data/badges/.
$dataUrl = $_POST['image'] ?? '';
if (strpos($dataUrl, 'data:image/png;base64,') !== 0) {
echo json_encode(['error' => 'Некорректный формат изображения']);
exit;
}
$base64 = substr($dataUrl, strlen('data:image/png;base64,'));
$binary = base64_decode($base64, true);
if ($binary === false) {
echo json_encode(['error' => 'Не удалось декодировать изображение']);
exit;
}
if (strlen($binary) > 5 * 1024 * 1024) {
echo json_encode(['error' => 'Файл слишком большой']);
exit;
}
$badgesDir = __DIR__ . '/data/badges';
if (!is_dir($badgesDir)) {
mkdir($badgesDir, 0755, true);
}
$filename = 'badge_' . date('Ymd_His') . '_' . substr(bin2hex(random_bytes(4)), 0, 8) . '.png';
$filepath = $badgesDir . '/' . $filename;
if (file_put_contents($filepath, $binary) === false) {
echo json_encode(['error' => 'Не удалось сохранить файл']);
exit;
}
$indexFile = $badgesDir . '/index.json';
$index = [];
if (file_exists($indexFile)) {
$index = json_decode(file_get_contents($indexFile), true) ?: [];
}
array_unshift($index, [
'filename' => $filename,
'created_at' => date('Y-m-d H:i:s'),
'has_price' => !empty($_POST['has_price']),
'has_nickname' => !empty($_POST['has_nickname']),
'label' => trim((string)($_POST['label'] ?? '')),
]);
// Keep at most 100 entries
$index = array_slice($index, 0, 100);
file_put_contents($indexFile, json_encode($index, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$path = dirname($_SERVER['REQUEST_URI']);
$url = $protocol . '://' . $host . $path . '/data/badges/' . $filename;
echo json_encode(['success' => true, 'filename' => $filename, 'url' => $url]);
break;
case 'badge_list':
$indexFile = __DIR__ . '/data/badges/index.json';
if (!file_exists($indexFile)) {
echo json_encode(['success' => true, 'items' => []]);
break;
}
$index = json_decode(file_get_contents($indexFile), true) ?: [];
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$path = dirname($_SERVER['REQUEST_URI']);
$items = [];
foreach ($index as $entry) {
$fname = $entry['filename'] ?? '';
if (!$fname) continue;
$absPath = __DIR__ . '/data/badges/' . $fname;
if (!file_exists($absPath)) continue;
$items[] = [
'filename' => $fname,
'created_at' => $entry['created_at'] ?? '',
'has_price' => !empty($entry['has_price']),
'has_nickname' => !empty($entry['has_nickname']),
'label' => $entry['label'] ?? '',
'url' => $protocol . '://' . $host . $path . '/data/badges/' . $fname,
];
}
echo json_encode(['success' => true, 'items' => $items]);
break;
case 'badge_delete':
$filename = basename($_POST['filename'] ?? '');
if (!$filename || strpos($filename, 'badge_') !== 0) {
echo json_encode(['error' => 'Некорректное имя файла']);
exit;
}
$badgesDir = __DIR__ . '/data/badges';
$filepath = $badgesDir . '/' . $filename;
if (file_exists($filepath)) {
@unlink($filepath);
}
$indexFile = $badgesDir . '/index.json';
if (file_exists($indexFile)) {
$index = json_decode(file_get_contents($indexFile), true) ?: [];
$index = array_values(array_filter($index, function($e) use ($filename) {
return ($e['filename'] ?? '') !== $filename;
}));
file_put_contents($indexFile, json_encode($index, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
echo json_encode(['success' => true]);
break;
default:
echo json_encode(['error' => 'Unknown action']);
}