Update app.py
Browse files
app.py
CHANGED
|
@@ -45,9 +45,10 @@ BASE_STYLE = '''
|
|
| 45 |
--glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
| 46 |
--delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3);
|
| 47 |
--note-color: #6a5acd; --share-color: #4caf50; --archive-color: #78909c;
|
| 48 |
-
--todolist-color: #29b6f6; --shoppinglist-color: #ffa726; --business-color: #fd7e14;
|
| 49 |
}
|
| 50 |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
|
|
|
| 51 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 52 |
html { scroll-behavior: smooth; }
|
| 53 |
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--background-dark); color: var(--text-dark); line-height: 1.6; -webkit-tap-highlight-color: transparent; }
|
|
@@ -126,6 +127,7 @@ label { padding: 0; margin: 0; border: none; background: none; }
|
|
| 126 |
#fab-option-todolist i { color: var(--todolist-color); }
|
| 127 |
#fab-option-shoppinglist i { color: var(--shoppinglist-color); }
|
| 128 |
#fab-option-business i { color: var(--business-color); }
|
|
|
|
| 129 |
#create-folder-form { display: none; margin-top: 15px; }
|
| 130 |
.shared-link-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #333; }
|
| 131 |
.shared-link-item:last-child { border-bottom: none; }
|
|
@@ -149,6 +151,22 @@ label { padding: 0; margin: 0; border: none; background: none; }
|
|
| 149 |
.public-list-item .quantity { font-weight: bold; color: var(--secondary); background: #2a2a2a; padding: 2px 8px; border-radius: 6px; }
|
| 150 |
.form-group { margin-bottom: 15px; text-align: left; }
|
| 151 |
.form-group small { color: var(--text-muted); font-size: 0.8em; margin-top: 4px; display: block; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
'''
|
| 153 |
|
| 154 |
PUBLIC_SHARE_PAGE_HTML = '''
|
|
@@ -1043,6 +1061,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 1043 |
<h4>Добавить в "{{ current_folder.name if current_folder_id != 'root' else 'Главная' }}"</h4>
|
| 1044 |
<div class="fab-options">
|
| 1045 |
<label for="file-input" class="fab-option" id="fab-option-upload"><i class="fa-solid fa-upload"></i><span>Файлы</span></label>
|
|
|
|
| 1046 |
<div class="fab-option" id="fab-option-note" onclick="openNoteModal()"><i class="fa-solid fa-note-sticky"></i><span>Заметку</span></div>
|
| 1047 |
<div class="fab-option" id="fab-option-folder"><i class="fa-solid fa-folder-plus"></i><span>Папку</span></div>
|
| 1048 |
<div class="fab-option" id="fab-option-todolist" onclick="openListEditorModal(null, 'todolist')"><i class="fa-solid fa-list-check"></i><span>Список дел</span></div>
|
|
@@ -1062,6 +1081,26 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 1062 |
<button class="btn" style="background: #555; width: 100%; margin-top: 10px;" onclick="closeFabModal()">Закрыть</button>
|
| 1063 |
</div></div>
|
| 1064 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1065 |
<div class="modal" id="note-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
|
| 1066 |
<h4 id="note-modal-title">Новая заметка</h4>
|
| 1067 |
<input type="hidden" id="note-id-input">
|
|
@@ -1485,6 +1524,7 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 1485 |
backButton.show();
|
| 1486 |
backButton.onClick(() => { haptic.impactOccurred('light'); window.location.href = `{{ url_for('tma_dashboard') }}?folder_id=${parentFolderId}`; });
|
| 1487 |
} else { window.Telegram.WebApp.BackButton.hide(); }
|
|
|
|
| 1488 |
});
|
| 1489 |
function closeListEditorModal() { document.getElementById('list-editor-modal').style.display = 'none'; }
|
| 1490 |
async function openListEditorModal(listId = null, listType) {
|
|
@@ -1585,6 +1625,259 @@ TMA_DASHBOARD_HTML_TEMPLATE = '''
|
|
| 1585 |
if (result.status === 'success') { haptic.notificationOccurred('success'); window.location.reload(); }
|
| 1586 |
else { haptic.notificationOccurred('error'); Telegram.WebApp.showAlert(result.message || 'Ошибка сохранения.'); }
|
| 1587 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1588 |
</script></body></html>
|
| 1589 |
'''
|
| 1590 |
|
|
|
|
| 45 |
--glass-bg: rgba(30, 30, 30, 0.7); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
| 46 |
--delete-color: #ff4444; --folder-color: #ffc107; --selection-color: rgba(139, 92, 246, 0.3);
|
| 47 |
--note-color: #6a5acd; --share-color: #4caf50; --archive-color: #78909c;
|
| 48 |
+
--todolist-color: #29b6f6; --shoppinglist-color: #ffa726; --business-color: #fd7e14; --camera-color: #42a5f5;
|
| 49 |
}
|
| 50 |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 51 |
+
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(255, 68, 68, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(255, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(255, 68, 68, 0); } }
|
| 52 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 53 |
html { scroll-behavior: smooth; }
|
| 54 |
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--background-dark); color: var(--text-dark); line-height: 1.6; -webkit-tap-highlight-color: transparent; }
|
|
|
|
| 127 |
#fab-option-todolist i { color: var(--todolist-color); }
|
| 128 |
#fab-option-shoppinglist i { color: var(--shoppinglist-color); }
|
| 129 |
#fab-option-business i { color: var(--business-color); }
|
| 130 |
+
#fab-option-camera i { color: var(--camera-color); }
|
| 131 |
#create-folder-form { display: none; margin-top: 15px; }
|
| 132 |
.shared-link-item { display: flex; justify-content: space-between; align-items: center; padding: 8px; border-bottom: 1px solid #333; }
|
| 133 |
.shared-link-item:last-child { border-bottom: none; }
|
|
|
|
| 151 |
.public-list-item .quantity { font-weight: bold; color: var(--secondary); background: #2a2a2a; padding: 2px 8px; border-radius: 6px; }
|
| 152 |
.form-group { margin-bottom: 15px; text-align: left; }
|
| 153 |
.form-group small { color: var(--text-muted); font-size: 0.8em; margin-top: 4px; display: block; }
|
| 154 |
+
#camera-modal { background: #000; z-index: 2500; }
|
| 155 |
+
.camera-container { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; }
|
| 156 |
+
#camera-view { width: 100%; height: 100%; object-fit: cover; }
|
| 157 |
+
.camera-top-bar { position: absolute; top: 0; left: 0; right: 0; padding: 15px; display: flex; justify-content: space-between; align-items: center; background: linear-gradient(to bottom, rgba(0,0,0,0.6), transparent); }
|
| 158 |
+
.camera-top-bar button { background: none; border: none; color: white; font-size: 24px; cursor: pointer; text-shadow: 0 1px 3px rgba(0,0,0,0.5); }
|
| 159 |
+
.camera-zoom-slider { position: absolute; top: 80px; right: 10px; writing-mode: vertical-bt; width: 40px; display: none; }
|
| 160 |
+
.camera-zoom-slider input { width: 150px; }
|
| 161 |
+
.camera-controls { position: absolute; bottom: 0; left: 0; right: 0; padding: 20px; background: linear-gradient(to top, rgba(0,0,0,0.6), transparent); display: flex; justify-content: space-around; align-items: center; }
|
| 162 |
+
#capture-btn { width: 70px; height: 70px; border-radius: 50%; border: 4px solid white; background-color: rgba(255,255,255,0.3); transition: all 0.2s; }
|
| 163 |
+
#capture-btn.video.recording { background-color: var(--delete-color); border-radius: 15%; animation: pulse 1.5s infinite; }
|
| 164 |
+
.camera-controls button { background: none; border: none; color: white; font-size: 28px; cursor: pointer; width: 70px; text-align: center; }
|
| 165 |
+
#camera-mode-toggle { font-size: 0.8em; font-weight: bold; padding: 5px; border-radius: 10px; background: rgba(0,0,0,0.4); }
|
| 166 |
+
#photo-preview-bar { position: absolute; bottom: 110px; left: 10px; right: 10px; height: 70px; display: flex; gap: 10px; overflow-x: auto; align-items: center; }
|
| 167 |
+
#photo-preview-bar .preview-item { width: 60px; height: 60px; border-radius: 10px; border: 2px solid white; object-fit: cover; flex-shrink: 0; position: relative; }
|
| 168 |
+
#photo-preview-bar .preview-item .remove-preview { position: absolute; top: -5px; right: -5px; background: var(--delete-color); color: white; border-radius: 50%; width: 20px; height: 20px; font-size: 14px; border: none; line-height: 20px; text-align: center; cursor: pointer; }
|
| 169 |
+
.video-timer { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); background: rgba(0,0,0,0.5); color: white; padding: 5px 10px; border-radius: 8px; font-family: monospace; display: none; }
|
| 170 |
'''
|
| 171 |
|
| 172 |
PUBLIC_SHARE_PAGE_HTML = '''
|
|
|
|
| 1061 |
<h4>Добавить в "{{ current_folder.name if current_folder_id != 'root' else 'Главная' }}"</h4>
|
| 1062 |
<div class="fab-options">
|
| 1063 |
<label for="file-input" class="fab-option" id="fab-option-upload"><i class="fa-solid fa-upload"></i><span>Файлы</span></label>
|
| 1064 |
+
<div class="fab-option" id="fab-option-camera" onclick="cameraManager.open()"><i class="fa-solid fa-camera"></i><span>Камера</span></div>
|
| 1065 |
<div class="fab-option" id="fab-option-note" onclick="openNoteModal()"><i class="fa-solid fa-note-sticky"></i><span>Заметку</span></div>
|
| 1066 |
<div class="fab-option" id="fab-option-folder"><i class="fa-solid fa-folder-plus"></i><span>Папку</span></div>
|
| 1067 |
<div class="fab-option" id="fab-option-todolist" onclick="openListEditorModal(null, 'todolist')"><i class="fa-solid fa-list-check"></i><span>Список дел</span></div>
|
|
|
|
| 1081 |
<button class="btn" style="background: #555; width: 100%; margin-top: 10px;" onclick="closeFabModal()">Закрыть</button>
|
| 1082 |
</div></div>
|
| 1083 |
|
| 1084 |
+
<div class="modal" id="camera-modal">
|
| 1085 |
+
<div class="camera-container">
|
| 1086 |
+
<video id="camera-view" autoplay playsinline></video>
|
| 1087 |
+
<div class="camera-top-bar">
|
| 1088 |
+
<button id="camera-close-btn"><i class="fa-solid fa-xmark"></i></button>
|
| 1089 |
+
<div class="video-timer" id="video-timer">00:00</div>
|
| 1090 |
+
<button id="upload-captured-btn" style="font-size: 1rem; display: none;"><i class="fa-solid fa-upload"></i> <span id="upload-counter"></span></button>
|
| 1091 |
+
</div>
|
| 1092 |
+
<div class="camera-zoom-slider">
|
| 1093 |
+
<input type="range" id="zoom-slider" min="1" max="10" step="0.1" value="1" orient="vertical">
|
| 1094 |
+
</div>
|
| 1095 |
+
<div id="photo-preview-bar"></div>
|
| 1096 |
+
<div class="camera-controls">
|
| 1097 |
+
<button id="camera-mode-toggle">PHOTO</button>
|
| 1098 |
+
<button id="capture-btn"></button>
|
| 1099 |
+
<button id="switch-camera-btn"><i class="fa-solid fa-rotate"></i></button>
|
| 1100 |
+
</div>
|
| 1101 |
+
</div>
|
| 1102 |
+
</div>
|
| 1103 |
+
|
| 1104 |
<div class="modal" id="note-modal"><div class="modal-content" style="padding: 20px; max-width: 500px; width: 90%;">
|
| 1105 |
<h4 id="note-modal-title">Новая заметка</h4>
|
| 1106 |
<input type="hidden" id="note-id-input">
|
|
|
|
| 1524 |
backButton.show();
|
| 1525 |
backButton.onClick(() => { haptic.impactOccurred('light'); window.location.href = `{{ url_for('tma_dashboard') }}?folder_id=${parentFolderId}`; });
|
| 1526 |
} else { window.Telegram.WebApp.BackButton.hide(); }
|
| 1527 |
+
cameraManager.init();
|
| 1528 |
});
|
| 1529 |
function closeListEditorModal() { document.getElementById('list-editor-modal').style.display = 'none'; }
|
| 1530 |
async function openListEditorModal(listId = null, listType) {
|
|
|
|
| 1625 |
if (result.status === 'success') { haptic.notificationOccurred('success'); window.location.reload(); }
|
| 1626 |
else { haptic.notificationOccurred('error'); Telegram.WebApp.showAlert(result.message || 'Ошибка сохранения.'); }
|
| 1627 |
}
|
| 1628 |
+
const cameraManager = {
|
| 1629 |
+
modal: null, video: null, stream: null, facingMode: 'environment',
|
| 1630 |
+
capturedBlobs: [], mode: 'photo', isRecording: false,
|
| 1631 |
+
recorder: null, recordedChunks: [], timerInterval: null,
|
| 1632 |
+
|
| 1633 |
+
init() {
|
| 1634 |
+
this.modal = document.getElementById('camera-modal');
|
| 1635 |
+
this.video = document.getElementById('camera-view');
|
| 1636 |
+
document.getElementById('camera-close-btn').addEventListener('click', () => this.close());
|
| 1637 |
+
document.getElementById('switch-camera-btn').addEventListener('click', () => this.switchCamera());
|
| 1638 |
+
document.getElementById('capture-btn').addEventListener('click', () => this.capture());
|
| 1639 |
+
document.getElementById('camera-mode-toggle').addEventListener('click', () => this.toggleMode());
|
| 1640 |
+
document.getElementById('upload-captured-btn').addEventListener('click', () => this.uploadCaptured());
|
| 1641 |
+
document.getElementById('zoom-slider').addEventListener('input', (e) => this.setZoom(e.target.value));
|
| 1642 |
+
},
|
| 1643 |
+
|
| 1644 |
+
async open() {
|
| 1645 |
+
closeFabModal();
|
| 1646 |
+
haptic.impactOccurred('medium');
|
| 1647 |
+
this.modal.style.display = 'flex';
|
| 1648 |
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
| 1649 |
+
Telegram.WebApp.showAlert('Камера не поддерживается на этом устройстве.');
|
| 1650 |
+
return;
|
| 1651 |
+
}
|
| 1652 |
+
await this.startCamera();
|
| 1653 |
+
},
|
| 1654 |
+
|
| 1655 |
+
close() {
|
| 1656 |
+
haptic.impactOccurred('light');
|
| 1657 |
+
this.stopCamera();
|
| 1658 |
+
this.modal.style.display = 'none';
|
| 1659 |
+
this.capturedBlobs = [];
|
| 1660 |
+
this.updatePreview();
|
| 1661 |
+
},
|
| 1662 |
+
|
| 1663 |
+
async startCamera() {
|
| 1664 |
+
this.stopCamera();
|
| 1665 |
+
const constraints = {
|
| 1666 |
+
video: {
|
| 1667 |
+
facingMode: this.facingMode,
|
| 1668 |
+
width: { ideal: 4096 },
|
| 1669 |
+
height: { ideal: 2160 }
|
| 1670 |
+
},
|
| 1671 |
+
audio: (this.mode === 'video')
|
| 1672 |
+
};
|
| 1673 |
+
try {
|
| 1674 |
+
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
| 1675 |
+
this.video.srcObject = this.stream;
|
| 1676 |
+
this.video.play();
|
| 1677 |
+
setTimeout(() => this.setupZoom(), 500);
|
| 1678 |
+
} catch (err) {
|
| 1679 |
+
Telegram.WebApp.showAlert(`Ошибка доступа к камере: ${err.name}`);
|
| 1680 |
+
this.close();
|
| 1681 |
+
}
|
| 1682 |
+
},
|
| 1683 |
+
|
| 1684 |
+
stopCamera() {
|
| 1685 |
+
if (this.stream) {
|
| 1686 |
+
this.stream.getTracks().forEach(track => track.stop());
|
| 1687 |
+
this.stream = null;
|
| 1688 |
+
}
|
| 1689 |
+
if (this.isRecording) {
|
| 1690 |
+
this.stopRecording();
|
| 1691 |
+
}
|
| 1692 |
+
},
|
| 1693 |
+
|
| 1694 |
+
switchCamera() {
|
| 1695 |
+
haptic.impactOccurred('light');
|
| 1696 |
+
this.facingMode = (this.facingMode === 'user') ? 'environment' : 'user';
|
| 1697 |
+
this.startCamera();
|
| 1698 |
+
},
|
| 1699 |
+
|
| 1700 |
+
setupZoom() {
|
| 1701 |
+
const zoomSliderContainer = document.querySelector('.camera-zoom-slider');
|
| 1702 |
+
const zoomSlider = document.getElementById('zoom-slider');
|
| 1703 |
+
if (this.stream) {
|
| 1704 |
+
const track = this.stream.getVideoTracks()[0];
|
| 1705 |
+
const capabilities = track.getCapabilities();
|
| 1706 |
+
if (capabilities.zoom) {
|
| 1707 |
+
zoomSlider.min = capabilities.zoom.min;
|
| 1708 |
+
zoomSlider.max = capabilities.zoom.max;
|
| 1709 |
+
zoomSlider.step = capabilities.zoom.step;
|
| 1710 |
+
zoomSlider.value = track.getSettings().zoom || capabilities.zoom.min;
|
| 1711 |
+
zoomSliderContainer.style.display = 'block';
|
| 1712 |
+
} else {
|
| 1713 |
+
zoomSliderContainer.style.display = 'none';
|
| 1714 |
+
}
|
| 1715 |
+
}
|
| 1716 |
+
},
|
| 1717 |
+
|
| 1718 |
+
setZoom(value) {
|
| 1719 |
+
if (this.stream) {
|
| 1720 |
+
const track = this.stream.getVideoTracks()[0];
|
| 1721 |
+
track.applyConstraints({ advanced: [{ zoom: value }] });
|
| 1722 |
+
}
|
| 1723 |
+
},
|
| 1724 |
+
|
| 1725 |
+
toggleMode() {
|
| 1726 |
+
this.mode = (this.mode === 'photo') ? 'video' : 'photo';
|
| 1727 |
+
document.getElementById('camera-mode-toggle').textContent = this.mode.toUpperCase();
|
| 1728 |
+
document.getElementById('capture-btn').classList.toggle('video', this.mode === 'video');
|
| 1729 |
+
this.startCamera(); // Restart with/without audio
|
| 1730 |
+
},
|
| 1731 |
+
|
| 1732 |
+
capture() {
|
| 1733 |
+
haptic.impactOccurred('heavy');
|
| 1734 |
+
if (this.mode === 'photo') {
|
| 1735 |
+
this.takePhoto();
|
| 1736 |
+
} else {
|
| 1737 |
+
this.toggleRecording();
|
| 1738 |
+
}
|
| 1739 |
+
},
|
| 1740 |
+
|
| 1741 |
+
takePhoto() {
|
| 1742 |
+
const canvas = document.createElement('canvas');
|
| 1743 |
+
canvas.width = this.video.videoWidth;
|
| 1744 |
+
canvas.height = this.video.videoHeight;
|
| 1745 |
+
const ctx = canvas.getContext('2d');
|
| 1746 |
+
ctx.drawImage(this.video, 0, 0, canvas.width, canvas.height);
|
| 1747 |
+
canvas.toBlob(blob => {
|
| 1748 |
+
this.capturedBlobs.push({blob: blob, type: 'image/jpeg'});
|
| 1749 |
+
this.updatePreview();
|
| 1750 |
+
}, 'image/jpeg', 0.95);
|
| 1751 |
+
},
|
| 1752 |
+
|
| 1753 |
+
toggleRecording() {
|
| 1754 |
+
if (this.isRecording) {
|
| 1755 |
+
this.stopRecording();
|
| 1756 |
+
} else {
|
| 1757 |
+
this.startRecording();
|
| 1758 |
+
}
|
| 1759 |
+
},
|
| 1760 |
+
|
| 1761 |
+
startRecording() {
|
| 1762 |
+
if (!this.stream) return;
|
| 1763 |
+
this.isRecording = true;
|
| 1764 |
+
this.recordedChunks = [];
|
| 1765 |
+
this.recorder = new MediaRecorder(this.stream);
|
| 1766 |
+
this.recorder.ondataavailable = e => this.recordedChunks.push(e.data);
|
| 1767 |
+
this.recorder.onstop = () => {
|
| 1768 |
+
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
|
| 1769 |
+
this.capturedBlobs.push({blob: blob, type: 'video/webm'});
|
| 1770 |
+
this.updatePreview();
|
| 1771 |
+
};
|
| 1772 |
+
this.recorder.start();
|
| 1773 |
+
document.getElementById('capture-btn').classList.add('recording');
|
| 1774 |
+
this.startTimer();
|
| 1775 |
+
},
|
| 1776 |
+
|
| 1777 |
+
stopRecording() {
|
| 1778 |
+
if (this.recorder && this.isRecording) {
|
| 1779 |
+
this.recorder.stop();
|
| 1780 |
+
this.isRecording = false;
|
| 1781 |
+
document.getElementById('capture-btn').classList.remove('recording');
|
| 1782 |
+
this.stopTimer();
|
| 1783 |
+
}
|
| 1784 |
+
},
|
| 1785 |
+
|
| 1786 |
+
startTimer() {
|
| 1787 |
+
const timerEl = document.getElementById('video-timer');
|
| 1788 |
+
timerEl.style.display = 'block';
|
| 1789 |
+
let seconds = 0;
|
| 1790 |
+
this.timerInterval = setInterval(() => {
|
| 1791 |
+
seconds++;
|
| 1792 |
+
const mins = Math.floor(seconds / 60).toString().padStart(2, '0');
|
| 1793 |
+
const secs = (seconds % 60).toString().padStart(2, '0');
|
| 1794 |
+
timerEl.textContent = `${mins}:${secs}`;
|
| 1795 |
+
}, 1000);
|
| 1796 |
+
},
|
| 1797 |
+
|
| 1798 |
+
stopTimer() {
|
| 1799 |
+
clearInterval(this.timerInterval);
|
| 1800 |
+
document.getElementById('video-timer').style.display = 'none';
|
| 1801 |
+
document.getElementById('video-timer').textContent = '00:00';
|
| 1802 |
+
},
|
| 1803 |
+
|
| 1804 |
+
updatePreview() {
|
| 1805 |
+
const previewBar = document.getElementById('photo-preview-bar');
|
| 1806 |
+
const uploadBtn = document.getElementById('upload-captured-btn');
|
| 1807 |
+
const uploadCounter = document.getElementById('upload-counter');
|
| 1808 |
+
previewBar.innerHTML = '';
|
| 1809 |
+
this.capturedBlobs.forEach((item, index) => {
|
| 1810 |
+
const previewItem = document.createElement('div');
|
| 1811 |
+
previewItem.style.position = 'relative';
|
| 1812 |
+
const url = URL.createObjectURL(item.blob);
|
| 1813 |
+
let el;
|
| 1814 |
+
if (item.type.startsWith('image')) {
|
| 1815 |
+
el = document.createElement('img');
|
| 1816 |
+
} else {
|
| 1817 |
+
el = document.createElement('video');
|
| 1818 |
+
el.muted = true;
|
| 1819 |
+
}
|
| 1820 |
+
el.src = url;
|
| 1821 |
+
el.className = 'preview-item';
|
| 1822 |
+
|
| 1823 |
+
const removeBtn = document.createElement('button');
|
| 1824 |
+
removeBtn.innerHTML = '×';
|
| 1825 |
+
removeBtn.className = 'remove-preview';
|
| 1826 |
+
removeBtn.onclick = (e) => {
|
| 1827 |
+
e.stopPropagation();
|
| 1828 |
+
this.capturedBlobs.splice(index, 1);
|
| 1829 |
+
this.updatePreview();
|
| 1830 |
+
};
|
| 1831 |
+
|
| 1832 |
+
previewItem.appendChild(el);
|
| 1833 |
+
previewItem.appendChild(removeBtn);
|
| 1834 |
+
previewBar.appendChild(previewItem);
|
| 1835 |
+
});
|
| 1836 |
+
|
| 1837 |
+
if (this.capturedBlobs.length > 0) {
|
| 1838 |
+
uploadBtn.style.display = 'inline-block';
|
| 1839 |
+
uploadCounter.textContent = this.capturedBlobs.length;
|
| 1840 |
+
} else {
|
| 1841 |
+
uploadBtn.style.display = 'none';
|
| 1842 |
+
}
|
| 1843 |
+
},
|
| 1844 |
+
|
| 1845 |
+
uploadCaptured() {
|
| 1846 |
+
if (this.capturedBlobs.length === 0) return;
|
| 1847 |
+
haptic.notificationOccurred('success');
|
| 1848 |
+
const formData = new FormData();
|
| 1849 |
+
formData.append('current_folder_id', '{{ current_folder_id }}');
|
| 1850 |
+
this.capturedBlobs.forEach((item, index) => {
|
| 1851 |
+
const extension = item.type.startsWith('image') ? '.jpg' : '.webm';
|
| 1852 |
+
const filename = `capture_${Date.now()}_${index}${extension}`;
|
| 1853 |
+
formData.append('files', item.blob, filename);
|
| 1854 |
+
});
|
| 1855 |
+
|
| 1856 |
+
this.close();
|
| 1857 |
+
|
| 1858 |
+
const progressContainer = document.getElementById('progress-container');
|
| 1859 |
+
const progressBar = document.getElementById('progress-bar');
|
| 1860 |
+
progressContainer.style.display = 'block';
|
| 1861 |
+
progressBar.style.width = '0%';
|
| 1862 |
+
|
| 1863 |
+
const xhr = new XMLHttpRequest();
|
| 1864 |
+
xhr.upload.addEventListener('progress', e => {
|
| 1865 |
+
if (e.lengthComputable) {
|
| 1866 |
+
progressBar.style.width = Math.round((e.loaded / e.total) * 100) + '%';
|
| 1867 |
+
}
|
| 1868 |
+
});
|
| 1869 |
+
xhr.addEventListener('load', () => {
|
| 1870 |
+
haptic.notificationOccurred('success');
|
| 1871 |
+
window.location.reload();
|
| 1872 |
+
});
|
| 1873 |
+
xhr.addEventListener('error', () => {
|
| 1874 |
+
haptic.notificationOccurred('error');
|
| 1875 |
+
Telegram.WebApp.showAlert('Ошибка загрузки.');
|
| 1876 |
+
});
|
| 1877 |
+
xhr.open('POST', '{{ url_for("tma_dashboard") }}', true);
|
| 1878 |
+
xhr.send(formData);
|
| 1879 |
+
}
|
| 1880 |
+
};
|
| 1881 |
</script></body></html>
|
| 1882 |
'''
|
| 1883 |
|