download.html 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Download - {{ filename }} - HokoriTemp</title>
  7. <!-- SVG Favicon -->
  8. <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
  9. <link rel="stylesheet" href="/static/css/styles.css">
  10. <!-- Fancybox CSS and JS -->
  11. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css">
  12. <script src="https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js"></script>
  13. <!-- GLightbox for videos -->
  14. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/glightbox/dist/css/glightbox.min.css">
  15. <script src="https://cdn.jsdelivr.net/npm/glightbox/dist/js/glightbox.min.js"></script>
  16. <!-- PDF.js -->
  17. <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
  18. <!-- HEIC2ANY for iPhone images -->
  19. <script src="https://cdn.jsdelivr.net/npm/heic2any@0.0.4/dist/heic2any.min.js"></script>
  20. <script src="https://cdn.tailwindcss.com"></script>
  21. <script>
  22. (function() {
  23. const savedTheme = localStorage.getItem('theme') || 'light';
  24. if (savedTheme === 'dark') {
  25. document.documentElement.classList.add('dark');
  26. }
  27. })();
  28. tailwind.config = {
  29. darkMode: 'class',
  30. theme: {
  31. extend: {
  32. animation: {
  33. 'fade-in': 'fadeIn 0.3s ease-in-out',
  34. 'slide-up': 'slideUp 0.3s ease-out',
  35. 'pulse-slow': 'pulse 2s infinite'
  36. }
  37. }
  38. }
  39. }
  40. </script>
  41. </head>
  42. <body class="bg-gray-50 dark:bg-gray-900 min-h-screen transition-colors duration-300">
  43. <div class="container mx-auto px-4 py-8 max-w-4xl">
  44. <!-- Header -->
  45. <div class="flex justify-between items-center mb-8">
  46. <div class="text-center flex-1">
  47. <h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">HokoriTemp</h1>
  48. <p class="text-gray-600 dark:text-gray-400">File Download</p>
  49. </div>
  50. <!-- Theme Toggle Button -->
  51. <button id="themeToggle" class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors ml-4">
  52. <!-- Sun Icon (Light Mode) -->
  53. <svg id="sunIcon" class="w-5 h-5 text-gray-800 dark:text-gray-200 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  54. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
  55. </svg>
  56. <!-- Moon Icon (Dark Mode) -->
  57. <svg id="moonIcon" class="w-5 h-5 text-gray-800 dark:text-gray-200 block dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  58. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
  59. </svg>
  60. </button>
  61. </div>
  62. <!-- File Info Section -->
  63. <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-6 transition-colors duration-300">
  64. <div class="flex items-center space-x-4 mb-4">
  65. <div class="flex-shrink-0">
  66. <svg class="w-12 h-12 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  67. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
  68. </svg>
  69. </div>
  70. <div class="flex-1">
  71. <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-1">{{ filename }}</h2>
  72. <div class="flex flex-wrap gap-4 text-sm text-gray-600 dark:text-gray-400">
  73. <span>Size: {{ "%.2f"|format(file_size / 1024 / 1024) }} MB</span>
  74. {% if mime_type %}
  75. <span>Type: {{ mime_type }}</span>
  76. {% endif %}
  77. <span>Expires: {{ expiration_date[:19].replace('T', ' ') }}</span>
  78. </div>
  79. </div>
  80. </div>
  81. <!-- Download Button -->
  82. <div class="flex gap-3">
  83. <a href="/download/{{ file_id }}/direct"
  84. class="inline-flex items-center px-6 py-3 bg-blue-600 hover:bg-blue-700 dark:bg-blue-700 dark:hover:bg-blue-600 text-white font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
  85. <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  86. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
  87. </svg>
  88. Download File
  89. </a>
  90. <a href="/"
  91. class="inline-flex items-center px-6 py-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
  92. <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  93. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
  94. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5a2 2 0 012-2h2a2 2 0 012 2v0H8v0z"></path>
  95. </svg>
  96. Upload More Files
  97. </a>
  98. </div>
  99. </div>
  100. <!-- Text File Preview -->
  101. {% if is_text_file and file_content %}
  102. <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
  103. <div class="flex items-center justify-between mb-4">
  104. <h3 class="text-lg font-semibold text-gray-900 dark:text-white">File Preview</h3>
  105. <div class="flex items-center space-x-2">
  106. <button onclick="copyTextToClipboard()" class="inline-flex items-center px-3 py-1 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors duration-200">
  107. <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  108. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
  109. </svg>
  110. Copy Text
  111. </button>
  112. <span class="text-sm text-gray-500 dark:text-gray-400">Text file preview</span>
  113. </div>
  114. </div>
  115. <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 overflow-auto max-h-96">
  116. <pre id="file-content" class="text-sm text-gray-800 dark:text-gray-200 whitespace-pre-wrap font-mono">{{ file_content }}</pre>
  117. </div>
  118. <div class="mt-4 text-xs text-gray-500 dark:text-gray-400">
  119. <p>Preview shows the first part of the file content. Download the file to view the complete content.</p>
  120. </div>
  121. </div>
  122. {% elif is_text_file %}
  123. <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
  124. <div class="text-center py-8">
  125. <svg class="w-12 h-12 mx-auto mb-4 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  126. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
  127. </svg>
  128. <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Preview Not Available</h3>
  129. <p class="text-gray-600 dark:text-gray-400">This text file couldn't be previewed. Please download it to view the content.</p>
  130. </div>
  131. </div>
  132. <!-- Media File Preview -->
  133. {% elif is_media_file %}
  134. <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
  135. <div class="flex items-center justify-between mb-4">
  136. <h3 class="text-lg font-semibold text-gray-900 dark:text-white">Media Preview</h3>
  137. <span class="text-sm text-gray-500 dark:text-gray-400">{{ media_type|title }} file preview</span>
  138. </div>
  139. <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 flex justify-center">
  140. {% if media_type == 'image' %}
  141. <div class="relative group">
  142. <a href="/preview/{{ file_id }}" data-fancybox="gallery" data-caption="{{ filename }}" id="image-link-{{ file_id }}">
  143. <img src="/preview/{{ file_id }}" alt="{{ filename }}" class="max-w-full max-h-96 rounded-lg shadow-sm cursor-pointer transition-transform duration-200 hover:scale-105" loading="lazy" id="preview-image-{{ file_id }}">
  144. </a>
  145. <div class="mt-3 flex flex-col items-center space-y-2">
  146. <p class="text-xs text-gray-500 dark:text-gray-400">Click to view with zoom, fullscreen & advanced controls (supports HEIC/HEIF)</p>
  147. <div class="flex space-x-2">
  148. <button onclick="downloadImage('{{ file_id }}', '{{ filename }}')" class="inline-flex items-center px-2 py-1 text-xs font-medium text-white bg-green-600 hover:bg-green-700 rounded transition-colors duration-200">
  149. <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  150. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2z"></path>
  151. </svg>
  152. Save Image
  153. </button>
  154. <button onclick="showImageInfo('{{ filename }}', '{{ file_size }}', '{{ mime_type }}')" class="inline-flex items-center px-2 py-1 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors duration-200">
  155. <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  156. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
  157. </svg>
  158. Info
  159. </button>
  160. </div>
  161. </div>
  162. </div>
  163. {% elif media_type == 'video' %}
  164. <div class="w-full max-w-2xl mx-auto">
  165. <video controls class="w-full rounded-lg shadow-lg" preload="metadata">
  166. <source src="/preview/{{ file_id }}" type="{{ mime_type }}">
  167. Your browser does not support the video tag.
  168. </video>
  169. <div class="mt-3 text-center">
  170. <p class="text-sm font-medium text-gray-900 dark:text-white">{{ filename }}</p>
  171. <p class="text-xs text-gray-500 dark:text-gray-400">{{ mime_type }}</p>
  172. </div>
  173. </div>
  174. {% elif media_type == 'audio' %}
  175. <div class="w-full max-w-md">
  176. <div class="flex items-center space-x-4 mb-4">
  177. <svg class="w-12 h-12 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
  178. <path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM15.657 6.343a1 1 0 011.414 0A9.972 9.972 0 0119 12a9.972 9.972 0 01-1.929 5.657 1 1 0 11-1.414-1.414A7.971 7.971 0 0017 12c0-2.21-.895-4.21-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 12a5.983 5.983 0 01-.757 2.829 1 1 0 01-1.415-1.415A3.987 3.987 0 0013 12a3.987 3.987 0 00-.172-1.414 1 1 0 010-1.415z" clip-rule="evenodd"></path>
  179. </svg>
  180. <div>
  181. <h4 class="font-medium text-gray-900 dark:text-white">{{ filename }}</h4>
  182. <p class="text-sm text-gray-500 dark:text-gray-400">Audio file</p>
  183. </div>
  184. </div>
  185. <audio controls class="w-full">
  186. <source src="/preview/{{ file_id }}" type="{{ mime_type }}">
  187. Your browser does not support the audio tag.
  188. </audio>
  189. </div>
  190. {% elif media_type == 'pdf' %}
  191. <div class="relative group w-full">
  192. <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
  193. <div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
  194. <div class="flex items-center space-x-3">
  195. <svg class="w-8 h-8 text-red-500" fill="currentColor" viewBox="0 0 20 20">
  196. <path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"></path>
  197. </svg>
  198. <div>
  199. <h4 class="font-medium text-gray-900 dark:text-white">{{ filename }}</h4>
  200. <p class="text-sm text-gray-500 dark:text-gray-400">PDF Document</p>
  201. </div>
  202. </div>
  203. <button onclick="openPDFViewer('/preview/{{ file_id }}', '{{ filename }}')" class="inline-flex items-center px-3 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors duration-200">
  204. <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  205. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
  206. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
  207. </svg>
  208. View PDF
  209. </button>
  210. </div>
  211. <div id="pdf-preview-{{ file_id }}" class="p-4 bg-gray-50 dark:bg-gray-900">
  212. <div class="flex items-center justify-center h-48 text-gray-500 dark:text-gray-400">
  213. <div class="text-center">
  214. <svg class="w-12 h-12 mx-auto mb-2 text-red-500" fill="currentColor" viewBox="0 0 20 20">
  215. <path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"></path>
  216. </svg>
  217. <p class="text-sm">Click "View PDF" to open with full viewer controls</p>
  218. </div>
  219. </div>
  220. </div>
  221. </div>
  222. <div class="mt-3 flex flex-col items-center space-y-2">
  223. <p class="text-xs text-gray-500 dark:text-gray-400">PDF viewer with zoom, search, navigation & print support</p>
  224. </div>
  225. </div>
  226. {% endif %}
  227. </div>
  228. <div class="mt-4 text-xs text-gray-500 dark:text-gray-400">
  229. <p>Media preview is displayed above. Download the file for full quality or offline viewing.</p>
  230. </div>
  231. </div>
  232. <!-- Archive File Preview -->
  233. {% elif is_archive_file and archive_contents %}
  234. <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
  235. <div class="flex items-center justify-between mb-4">
  236. <h3 class="text-lg font-semibold text-gray-900 dark:text-white">Archive Contents</h3>
  237. <span class="text-sm text-gray-500 dark:text-gray-400">{{ archive_contents.type|upper }} archive preview</span>
  238. </div>
  239. <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 max-h-96 overflow-auto">
  240. <div class="space-y-2">
  241. {% for file in archive_contents.files %}
  242. <div class="flex items-center justify-between py-2 px-3 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700">
  243. <div class="flex items-center space-x-3">
  244. {% if file.type == 'directory' %}
  245. <svg class="w-4 h-4 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
  246. <path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"></path>
  247. </svg>
  248. {% else %}
  249. <svg class="w-4 h-4 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
  250. <path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"></path>
  251. </svg>
  252. {% endif %}
  253. <span class="text-sm font-medium text-gray-900 dark:text-white truncate">{{ file.name }}</span>
  254. </div>
  255. <div class="flex items-center space-x-4 text-xs text-gray-500 dark:text-gray-400">
  256. {% if file.type != 'directory' %}
  257. <span>{{ "%.1f"|format(file.size / 1024) }} KB</span>
  258. {% if file.compressed_size %}
  259. <span class="text-green-600 dark:text-green-400">{{ "%.1f%%"|format((1 - file.compressed_size / file.size) * 100) }} compressed</span>
  260. {% endif %}
  261. {% endif %}
  262. {% if file.date_time %}
  263. <span>{{ "%04d-%02d-%02d"|format(file.date_time[0], file.date_time[1], file.date_time[2]) }}</span>
  264. {% endif %}
  265. </div>
  266. </div>
  267. {% endfor %}
  268. </div>
  269. </div>
  270. <div class="mt-4 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
  271. <p>Archive contains {{ archive_contents.files|length }} items. Download to extract files.</p>
  272. <div class="flex space-x-4">
  273. {% set total_files = archive_contents.files|selectattr('type', 'equalto', 'file')|list|length %}
  274. {% set total_dirs = archive_contents.files|selectattr('type', 'equalto', 'directory')|list|length %}
  275. <span>{{ total_files }} files</span>
  276. {% if total_dirs > 0 %}
  277. <span>{{ total_dirs }} folders</span>
  278. {% endif %}
  279. </div>
  280. </div>
  281. </div>
  282. {% elif is_archive_file %}
  283. <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
  284. <div class="text-center py-8">
  285. <svg class="w-12 h-12 mx-auto mb-4 text-yellow-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  286. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
  287. </svg>
  288. <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Archive Preview Not Available</h3>
  289. <p class="text-gray-600 dark:text-gray-400">This archive file couldn't be previewed. Please download it to view the contents.</p>
  290. </div>
  291. </div>
  292. {% else %}
  293. <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
  294. <div class="text-center py-8">
  295. <svg class="w-12 h-12 mx-auto mb-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  296. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
  297. </svg>
  298. <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Binary File</h3>
  299. <p class="text-gray-600 dark:text-gray-400">This file cannot be previewed. Click the download button above to get the file.</p>
  300. </div>
  301. </div>
  302. {% endif %}
  303. </div>
  304. <!-- Toast Notification -->
  305. <div id="toast" class="fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg transform translate-x-full transition-transform duration-300 z-50">
  306. <p id="toastMessage"></p>
  307. </div>
  308. <script>
  309. // Theme management
  310. function initTheme() {
  311. const savedTheme = localStorage.getItem('theme') || 'light';
  312. if (savedTheme === 'dark') {
  313. document.documentElement.classList.add('dark');
  314. } else {
  315. document.documentElement.classList.remove('dark');
  316. }
  317. }
  318. function toggleTheme() {
  319. const isDark = document.documentElement.classList.contains('dark');
  320. if (isDark) {
  321. document.documentElement.classList.remove('dark');
  322. localStorage.setItem('theme', 'light');
  323. } else {
  324. document.documentElement.classList.add('dark');
  325. localStorage.setItem('theme', 'dark');
  326. }
  327. }
  328. // Toast notification
  329. function showToast(message, type = 'success') {
  330. const toast = document.getElementById('toast');
  331. const toastMessage = document.getElementById('toastMessage');
  332. toastMessage.textContent = message;
  333. toast.classList.remove('bg-green-500', 'bg-red-500', 'bg-blue-500');
  334. if (type === 'success') {
  335. toast.classList.add('bg-green-500');
  336. } else if (type === 'error') {
  337. toast.classList.add('bg-red-500');
  338. } else if (type === 'info') {
  339. toast.classList.add('bg-blue-500');
  340. }
  341. toast.classList.remove('translate-x-full');
  342. setTimeout(() => {
  343. toast.classList.add('translate-x-full');
  344. }, 3000);
  345. }
  346. // Copy text to clipboard
  347. function copyTextToClipboard() {
  348. const fileContent = document.getElementById('file-content');
  349. if (!fileContent) {
  350. showToast('No content to copy', 'error');
  351. return;
  352. }
  353. const textToCopy = fileContent.textContent || fileContent.innerText;
  354. if (navigator.clipboard && window.isSecureContext) {
  355. // Use modern clipboard API
  356. navigator.clipboard.writeText(textToCopy).then(() => {
  357. showToast('Text copied to clipboard!', 'success');
  358. }).catch(err => {
  359. console.error('Failed to copy text: ', err);
  360. fallbackCopyTextToClipboard(textToCopy);
  361. });
  362. } else {
  363. // Fallback for older browsers
  364. fallbackCopyTextToClipboard(textToCopy);
  365. }
  366. }
  367. function fallbackCopyTextToClipboard(text) {
  368. const textArea = document.createElement('textarea');
  369. textArea.value = text;
  370. textArea.style.position = 'fixed';
  371. textArea.style.left = '-999999px';
  372. textArea.style.top = '-999999px';
  373. document.body.appendChild(textArea);
  374. textArea.focus();
  375. textArea.select();
  376. try {
  377. const successful = document.execCommand('copy');
  378. if (successful) {
  379. showToast('Text copied to clipboard!', 'success');
  380. } else {
  381. showToast('Failed to copy text', 'error');
  382. }
  383. } catch (err) {
  384. console.error('Fallback: Could not copy text: ', err);
  385. showToast('Failed to copy text', 'error');
  386. }
  387. document.body.removeChild(textArea);
  388. }
  389. // Image functions
  390. function downloadImage(fileId, filename) {
  391. const link = document.createElement('a');
  392. link.href = `/download/${fileId}/direct`;
  393. link.download = filename;
  394. document.body.appendChild(link);
  395. link.click();
  396. document.body.removeChild(link);
  397. showToast('Image download started!', 'success');
  398. }
  399. function showImageInfo(filename, fileSize, mimeType) {
  400. const sizeInKB = Math.round(fileSize / 1024);
  401. const sizeInMB = (fileSize / (1024 * 1024)).toFixed(2);
  402. const sizeText = fileSize > 1024 * 1024 ? `${sizeInMB} MB` : `${sizeInKB} KB`;
  403. const infoMessage = `File: ${filename}\nSize: ${sizeText}\nType: ${mimeType}`;
  404. // Create a custom modal for image info
  405. const modal = document.createElement('div');
  406. modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
  407. modal.innerHTML = `
  408. <div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md mx-4 shadow-xl">
  409. <div class="flex items-center justify-between mb-4">
  410. <h3 class="text-lg font-semibold text-gray-900 dark:text-white">Image Information</h3>
  411. <button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
  412. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  413. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
  414. </svg>
  415. </button>
  416. </div>
  417. <div class="space-y-3">
  418. <div>
  419. <span class="text-sm font-medium text-gray-500 dark:text-gray-400">Filename:</span>
  420. <p class="text-gray-900 dark:text-white break-all">${filename}</p>
  421. </div>
  422. <div>
  423. <span class="text-sm font-medium text-gray-500 dark:text-gray-400">File Size:</span>
  424. <p class="text-gray-900 dark:text-white">${sizeText}</p>
  425. </div>
  426. <div>
  427. <span class="text-sm font-medium text-gray-500 dark:text-gray-400">MIME Type:</span>
  428. <p class="text-gray-900 dark:text-white">${mimeType}</p>
  429. </div>
  430. </div>
  431. <div class="mt-6 flex justify-end">
  432. <button onclick="this.closest('.fixed').remove()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors duration-200">
  433. Close
  434. </button>
  435. </div>
  436. </div>
  437. `;
  438. document.body.appendChild(modal);
  439. // Close modal when clicking outside
  440. modal.addEventListener('click', function(e) {
  441. if (e.target === modal) {
  442. modal.remove();
  443. }
  444. });
  445. }
  446. // Initialize
  447. document.addEventListener('DOMContentLoaded', function() {
  448. initTheme();
  449. document.getElementById('themeToggle').addEventListener('click', toggleTheme);
  450. });
  451. </script>
  452. <!-- Fancybox Configuration -->
  453. <script>
  454. // Initialize Fancybox
  455. document.addEventListener('DOMContentLoaded', function() {
  456. if (typeof Fancybox !== 'undefined') {
  457. Fancybox.bind('[data-fancybox]', {
  458. // UI options
  459. animated: true,
  460. showClass: 'f-fadeIn',
  461. hideClass: 'f-fadeOut',
  462. // Toolbar options
  463. Toolbar: {
  464. display: {
  465. left: ['infobar'],
  466. middle: ['zoomIn', 'zoomOut', 'toggle1to1', 'rotateCCW', 'rotateCW', 'flipX', 'flipY'],
  467. right: ['slideshow', 'fullscreen', 'thumbs', 'close']
  468. }
  469. },
  470. // Image options
  471. Images: {
  472. zoom: true,
  473. wheel: 'zoom'
  474. },
  475. // Thumbs options
  476. Thumbs: {
  477. showOnStart: false
  478. },
  479. // Slideshow options
  480. Slideshow: {
  481. speed: 3000
  482. }
  483. });
  484. console.log('Fancybox initialized successfully');
  485. }
  486. });
  487. </script>
  488. <!-- HEIC/HEIF Support -->
  489. <script>
  490. // Function to handle HEIC/HEIF images
  491. async function handleHEICImage(imgElement, fileId) {
  492. try {
  493. const response = await fetch(`/preview/${fileId}`);
  494. const blob = await response.blob();
  495. if (blob.type === 'image/heic' || blob.type === 'image/heif') {
  496. // Convert HEIC to JPEG using heic2any
  497. const convertedBlob = await heic2any({
  498. blob: blob,
  499. toType: 'image/jpeg',
  500. quality: 0.8
  501. });
  502. const convertedUrl = URL.createObjectURL(convertedBlob);
  503. imgElement.src = convertedUrl;
  504. // Update the Fancybox link as well
  505. const linkElement = imgElement.closest('a[data-fancybox]');
  506. if (linkElement) {
  507. linkElement.href = convertedUrl;
  508. }
  509. console.log('HEIC image converted successfully');
  510. }
  511. } catch (error) {
  512. console.error('Error converting HEIC image:', error);
  513. // Fallback: show error message
  514. imgElement.alt = 'HEIC image conversion failed';
  515. }
  516. }
  517. // Check for HEIC images on page load
  518. document.addEventListener('DOMContentLoaded', function() {
  519. const images = document.querySelectorAll('img[id^="preview-image-"]');
  520. images.forEach(img => {
  521. img.addEventListener('error', function() {
  522. // If image fails to load, it might be HEIC
  523. const fileId = this.id.replace('preview-image-', '');
  524. handleHEICImage(this, fileId);
  525. });
  526. });
  527. });
  528. </script>
  529. <!-- PDF.js Support -->
  530. <script>
  531. // PDF viewer function
  532. function openPDFViewer(pdfUrl, filename) {
  533. // Create a modal for PDF viewing
  534. const modal = document.createElement('div');
  535. modal.className = 'fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50';
  536. modal.innerHTML = `
  537. <div class="bg-white dark:bg-gray-800 rounded-lg w-11/12 h-5/6 max-w-6xl flex flex-col">
  538. <div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
  539. <h3 class="text-lg font-semibold text-gray-900 dark:text-white">${filename}</h3>
  540. <div class="flex items-center space-x-2">
  541. <button id="pdf-download" class="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors duration-200">
  542. Download
  543. </button>
  544. <button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
  545. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  546. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
  547. </svg>
  548. </button>
  549. </div>
  550. </div>
  551. <div class="flex-1 p-4">
  552. <iframe src="${pdfUrl}" class="w-full h-full border-0 rounded" title="${filename}"></iframe>
  553. </div>
  554. </div>
  555. `;
  556. document.body.appendChild(modal);
  557. // Add download functionality
  558. modal.querySelector('#pdf-download').addEventListener('click', function() {
  559. const link = document.createElement('a');
  560. link.href = pdfUrl;
  561. link.download = filename;
  562. document.body.appendChild(link);
  563. link.click();
  564. document.body.removeChild(link);
  565. });
  566. // Close modal when clicking outside
  567. modal.addEventListener('click', function(e) {
  568. if (e.target === modal) {
  569. modal.remove();
  570. }
  571. });
  572. // Close modal with Escape key
  573. const handleEscape = function(e) {
  574. if (e.key === 'Escape') {
  575. modal.remove();
  576. document.removeEventListener('keydown', handleEscape);
  577. }
  578. };
  579. document.addEventListener('keydown', handleEscape);
  580. }
  581. </script>
  582. </body>
  583. </html>