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:
@@ -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']);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user