diff --git a/README.md b/README.md index 0d7e559..6f8ab0c 100644 --- a/README.md +++ b/README.md @@ -188,3 +188,7 @@ N/05_... **Откат вручную.** Перед заменой файлов — копия изменяемых файлов (`index.php`, `components/*`) с хостинга; вернуть их той же загрузкой. Новый преленд требует на сайте `/api/images/public/claim` (api с 2026-10-05): со старым api переход по ключу отдаст 404. + +## Android: фото и файлы (2026-10-19) +Основная кнопка выбирает image/*; отдельная ссылка «Выбрать файлы (PDF и др.)» открывает общий picker без ограничения image/*. PDF в подписи скрывается по runtime API-policy. iOS/iPad/Desktop сохраняют прежний общий выбор. +много сохраняется в одной компактной mobile-строке. components/browser-upload.php и CSS upload-picker-links.php — лёгкие inline-копии соответствующих компонентов API: отдельный PHP-хост не использует Yii AssetManager, новый внешний запрос не добавляется. При изменении общего контракта синхронизировать обе копии; API mobile-picker.cjs покрывает обе реальные страницы. Android byte-copy FileProvider сохраняет исходные байты/имя/MIME и ограничен max_file_size_bytes; до ответа policy используется консервативный15MB предел копирования. UTM/client_key/claim/config.json не меняются. + diff --git a/components/browser-upload.php b/components/browser-upload.php new file mode 100644 index 0000000..fd06a54 --- /dev/null +++ b/components/browser-upload.php @@ -0,0 +1,82 @@ + + diff --git a/components/form.php b/components/form.php index 0ea44e9..c53df4a 100644 --- a/components/form.php +++ b/components/form.php @@ -1,4 +1,5 @@ -
+
@@ -26,7 +27,7 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a
HEIC
WebP
GIF
-
PDF
+
PDF
+много
@@ -37,6 +38,7 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a + @@ -78,14 +80,16 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a // отправляется в content-based Tour gate, где и показывается controlled error. let pickerAccept = ; let maxUploadBytes = 0; + window.ZoomzillaUpload.bindPicker(formEl, fileInput, () => pickerAccept, files => handleSelectedFile(files[0])); if (uploadPolicyApiUrl && window.fetch) { fetch(uploadPolicyApiUrl, { credentials: 'omit', cache: 'no-store' }) .then(response => response.ok ? response.json() : null) .then(policy => { if (typeof policy?.picker_accept === 'string' && policy.picker_accept !== '') { pickerAccept = policy.picker_accept; - fileInput.accept = pickerAccept; + fileInput.accept = /Android/i.test(navigator.userAgent) ? 'image/*' : pickerAccept; } + if (policy?.pdf_observation_enabled === false) formEl.querySelector('[data-files-picker]').textContent = ; if (Number.isSafeInteger(policy?.max_file_size_bytes) && policy.max_file_size_bytes > 0) { maxUploadBytes = policy.max_file_size_bytes; } @@ -124,7 +128,7 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a uploadPending = false; if (uploadDotsTimer !== null) window.clearInterval(uploadDotsTimer); uploadDotsTimer = null; - if (uploadButtonLabel) uploadButtonLabel.textContent = uploadButtonDefaultLabel; + if (uploadButtonLabel) uploadButtonLabel.textContent = uploadButtonLabel.dataset.pickerLabel || uploadButtonDefaultLabel; if (uploadButton) { uploadButton.disabled = false; uploadButton.removeAttribute('aria-busy'); @@ -217,12 +221,18 @@ $basePickerAccept = 'image/jpeg,image/png,image/gif,image/webp,image/bmp,image/a } function uploadFile(file) { + // Тот же обход нестабильного Android FileProvider, что на основном сайте. + window.ZoomzillaUpload.prepare(file, maxUploadBytes || 15 * 1024 * 1024) + .then(bytes => uploadPreparedFile(file, bytes)) + .catch(() => showUploadError(genericErrorMsg)); + } + function uploadPreparedFile(file, bytes) { const xhr = new XMLHttpRequest(); const formData = new FormData(); // Ключ загрузки: по нему сайт передаёт её в сессию посетителя (/api/images/public/claim, годен 30 мин). // randomUUID есть в HTTPS-контексте; где его нет (старый Safari), есть getRandomValues. const clientKey = createClientKey(); - formData.append('file', file); + formData.append('file', bytes, file.name || 'upload'); formData.append('client_key', clientKey); const actionUrl = createApiUrl || formEl.getAttribute('action') || '/api/images/public/create'; diff --git a/components/upload-picker-links.php b/components/upload-picker-links.php new file mode 100644 index 0000000..2fb6796 --- /dev/null +++ b/components/upload-picker-links.php @@ -0,0 +1,22 @@ + + + + + diff --git a/messages/en/app.php b/messages/en/app.php index d3b7148..a608f11 100644 --- a/messages/en/app.php +++ b/messages/en/app.php @@ -137,6 +137,10 @@ return [ 'Загрузите изображение' => 'Upload an image', 'Загрузить' => 'Upload', 'Загрузить изображение' => 'Upload Image', + 'Выбрать фото' => 'Choose photo', + 'или' => 'or', + 'Выбрать файлы (PDF и др.)' => 'Choose files (PDF, etc.)', + 'Выбрать файлы (другие форматы)' => 'Choose files (other formats)', 'Загрузка тарифов...' => 'Loading plans...', 'Загрузка...' => 'Loading...', 'Запись в историю добавлена!' => 'History entry added', diff --git a/tests/check-visible-format-list.php b/tests/check-visible-format-list.php index 1eab42c..057812f 100644 --- a/tests/check-visible-format-list.php +++ b/tests/check-visible-format-list.php @@ -11,7 +11,7 @@ if ($form === false) { exit(1); } -preg_match_all('/
([^<]+)<\\/div>/', $form, $formatMatches); +preg_match_all('/
]*>([^<]+)<\\/div>/', $form, $formatMatches); $visibleFormats = ['JPG', 'PNG', 'HEIC', 'WebP', 'GIF', 'PDF', '+много']; if ($formatMatches[1] !== $visibleFormats) { fwrite(STDERR, "FAIL: Preland не показывает утверждённый компактный список форматов в нужном порядке.\n");