Eluza133 commited on
Commit
defacb9
·
verified ·
1 Parent(s): 886f11b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +294 -1
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 = '&times;';
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