archiveTile.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. /* global Android */
  2. const html = require('choo/html');
  3. const raw = require('choo/html/raw');
  4. const assets = require('../../common/assets');
  5. const {
  6. bytes,
  7. copyToClipboard,
  8. list,
  9. percent,
  10. platform,
  11. timeLeft
  12. } = require('../utils');
  13. const expiryOptions = require('./expiryOptions');
  14. function expiryInfo(translate, archive) {
  15. const l10n = timeLeft(archive.expiresAt - Date.now());
  16. return raw(
  17. translate('frontPageExpireInfo', {
  18. downloadCount: translate('downloadCount', {
  19. num: archive.dlimit - archive.dtotal
  20. }),
  21. timespan: translate(l10n.id, l10n)
  22. })
  23. );
  24. }
  25. function password(state) {
  26. const MAX_LENGTH = 32;
  27. return html`
  28. <div class="mb-2 px-1">
  29. <div class="checkbox inline-block mr-3">
  30. <input
  31. id="add-password"
  32. type="checkbox"
  33. ${state.archive.password ? 'checked' : ''}
  34. autocomplete="off"
  35. onchange="${togglePasswordInput}"
  36. />
  37. <label for="add-password">
  38. ${state.translate('addPasswordMessage')}
  39. </label>
  40. </div>
  41. <input
  42. id="password-input"
  43. class="${state.archive.password
  44. ? ''
  45. : 'invisible'} border rounded focus:border-blue-dark leading-normal my-1 py-1 px-2 h-8"
  46. autocomplete="off"
  47. maxlength="${MAX_LENGTH}"
  48. type="password"
  49. oninput="${inputChanged}"
  50. onfocus="${focused}"
  51. placeholder="${state.translate('unlockInputPlaceholder')}"
  52. value="${state.archive.password || ''}"
  53. />
  54. <label
  55. id="password-msg"
  56. for="password-input"
  57. class="block text-xs text-grey-darker"
  58. ></label>
  59. </div>
  60. `;
  61. function togglePasswordInput(event) {
  62. event.stopPropagation();
  63. const checked = event.target.checked;
  64. const input = document.getElementById('password-input');
  65. if (checked) {
  66. input.classList.remove('invisible');
  67. input.focus();
  68. } else {
  69. input.classList.add('invisible');
  70. input.value = '';
  71. document.getElementById('password-msg').textContent = '';
  72. state.archive.password = null;
  73. }
  74. }
  75. function inputChanged() {
  76. const passwordInput = document.getElementById('password-input');
  77. const pwdmsg = document.getElementById('password-msg');
  78. const password = passwordInput.value;
  79. const length = password.length;
  80. if (length === MAX_LENGTH) {
  81. pwdmsg.textContent = state.translate('maxPasswordLength', {
  82. length: MAX_LENGTH
  83. });
  84. } else {
  85. pwdmsg.textContent = '';
  86. }
  87. state.archive.password = password;
  88. }
  89. function focused(event) {
  90. event.preventDefault();
  91. const el = document.getElementById('password-input');
  92. if (el.placeholder !== state.translate('unlockInputPlaceholder')) {
  93. el.placeholder = '';
  94. }
  95. }
  96. }
  97. function fileInfo(file, action) {
  98. return html`
  99. <send-file class="flex flex-row items-center p-3 w-full">
  100. <img class="" src="${assets.get('blue_file.svg')}"/>
  101. <p class="ml-4 w-full">
  102. <h1 class="text-sm font-medium word-break-all">${file.name}</h1>
  103. <div class="text-xs font-normal opacity-75 pt-1">${bytes(
  104. file.size
  105. )}</div>
  106. <div class="hidden">${file.type}</div>
  107. </p>
  108. ${action}
  109. </send-file>`;
  110. }
  111. function archiveDetails(translate, archive) {
  112. if (archive.manifest.files.length > 1) {
  113. return html`
  114. <details
  115. class="w-full pb-1"
  116. ${archive.open ? 'open' : ''}
  117. ontoggle="${toggled}"
  118. >
  119. <summary
  120. class="flex items-center text-blue-dark text-sm cursor-pointer outline-none"
  121. >
  122. <svg
  123. class="fill-current w-4 h-4 mr-1"
  124. xmlns="http://www.w3.org/2000/svg"
  125. viewBox="0 0 20 20"
  126. >
  127. <path
  128. d="M12.95 10.707l.707-.707L8 4.343 6.586 5.757 10.828 10l-4.242 4.243L8 15.657l4.95-4.95z"
  129. />
  130. </svg>
  131. ${translate('fileCount', {
  132. num: archive.manifest.files.length
  133. })}
  134. </summary>
  135. ${list(
  136. archive.manifest.files.map(f => fileInfo(f)),
  137. 'list-reset h-full'
  138. )}
  139. </details>
  140. `;
  141. }
  142. function toggled(event) {
  143. event.stopPropagation();
  144. archive.open = event.target.open;
  145. }
  146. }
  147. module.exports = function(state, emit, archive) {
  148. const copyOrShare =
  149. platform() !== 'android'
  150. ? html`
  151. <button
  152. class="text-blue-dark hover:text-blue-darker focus:text-blue-darker self-end font-medium flex items-center"
  153. onclick=${copy}
  154. >
  155. <img src="${assets.get('copy-16.svg')}" class="mr-2" />
  156. ${state.translate('copyUrlHover')}
  157. </button>
  158. `
  159. : html`
  160. <button
  161. class="text-blue-dark hover:text-blue-darker focus:text-blue-darker self-end font-medium flex items-center"
  162. onclick=${share}
  163. >
  164. <img src="${assets.get('share-24.svg')}" class="mr-2" /> Share
  165. </button>
  166. `;
  167. const dl =
  168. platform() === 'web'
  169. ? html`
  170. <a
  171. class="flex items-baseline text-blue-dark hover:text-blue-darker focus:text-blue-darker font-medium"
  172. href="${archive.url}"
  173. >
  174. <img src="${assets.get('dl.svg')}" class="mr-2" />
  175. ${state.translate('downloadButtonLabel')}
  176. </a>
  177. `
  178. : html`
  179. <div></div>
  180. `;
  181. return html`
  182. <send-archive
  183. id="archive-${archive.id}"
  184. class="flex flex-col items-start rounded shadow-light bg-white p-4 w-full">
  185. <p class="w-full">
  186. <img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
  187. <input
  188. type="image"
  189. class="float-right self-center text-white delete"
  190. alt="Delete"
  191. src="${assets.get('close-16.svg')}"
  192. onclick=${del}/>
  193. <h1 class="text-sm font-medium word-break-all">${archive.name}</h1>
  194. <div class="text-xs font-normal opacity-75 pt-1">${bytes(
  195. archive.size
  196. )}</div>
  197. </p>
  198. <div class="text-xs text-grey-dark w-full mt-2 mb-2">
  199. ${expiryInfo(state.translate, archive)}
  200. </div>
  201. ${archiveDetails(state.translate, archive)}
  202. <hr class="w-full border-t my-4">
  203. <div class="flex justify-between w-full">
  204. ${dl}
  205. ${copyOrShare}
  206. </div>
  207. </send-archive>`;
  208. function copy(event) {
  209. event.stopPropagation();
  210. copyToClipboard(archive.url);
  211. const text = event.target.lastChild;
  212. text.textContent = state.translate('copiedUrl');
  213. setTimeout(
  214. () => (text.textContent = state.translate('copyUrlHover')),
  215. 1000
  216. );
  217. }
  218. function del(event) {
  219. event.stopPropagation();
  220. emit('delete', archive);
  221. }
  222. function share(event) {
  223. event.stopPropagation();
  224. Android.shareUrl(archive.url);
  225. }
  226. };
  227. module.exports.wip = function(state, emit) {
  228. return html`
  229. <send-upload-area class="flex flex-col bg-white md:h-full w-full" id="wip">
  230. ${list(
  231. Array.from(state.archive.files)
  232. .reverse()
  233. .map(f => fileInfo(f, remove(f))),
  234. 'bg-grey-lightest rounded-t list-reset overflow-y-auto px-6 py-4 md:h-full md:max-h-half-screen',
  235. 'bg-white px-2 my-2 shadow-light rounded'
  236. )}
  237. <div
  238. class="flex-grow flex items-end p-4 bg-grey-lightest rounded-b mb-1 font-medium"
  239. >
  240. <input
  241. id="file-upload"
  242. class="hidden"
  243. type="file"
  244. multiple
  245. onchange="${add}"
  246. />
  247. <label
  248. for="file-upload"
  249. class="flex flex-row items-center justify-between w-full p-2 cursor-pointer"
  250. title="${state.translate('addFilesButton')}"
  251. >
  252. <div class="flex items-center">
  253. <img src="${assets.get('addfiles.svg')}" class="w-6 h-6 mr-2" />
  254. ${state.translate('addFilesButton')}
  255. </div>
  256. <div class="font-normal text-sm text-grey-darker">
  257. ${state.translate('totalSize', { size: bytes(state.archive.size) })}
  258. </div>
  259. </label>
  260. </div>
  261. ${expiryOptions(state, emit)} ${password(state, emit)}
  262. <button
  263. id="upload-btn"
  264. class="btn rounded-lg flex-no-shrink"
  265. title="${state.translate('uploadFilesButton')}"
  266. onclick="${upload}"
  267. >
  268. ${state.translate('uploadFilesButton')}
  269. </button>
  270. </send-upload-area>
  271. `;
  272. function upload(event) {
  273. window.scrollTo(0, 0);
  274. event.preventDefault();
  275. event.target.disabled = true;
  276. if (!state.uploading) {
  277. emit('upload');
  278. }
  279. }
  280. function add(event) {
  281. event.preventDefault();
  282. const newFiles = Array.from(event.target.files);
  283. emit('addFiles', { files: newFiles });
  284. setTimeout(() => {
  285. document
  286. .querySelector('#wip > ul > li:first-child')
  287. .scrollIntoView({ block: 'center' });
  288. });
  289. }
  290. function remove(file) {
  291. return html`
  292. <input
  293. type="image"
  294. class="self-center text-white ml-4 delete"
  295. alt="Delete"
  296. src="${assets.get('close-16.svg')}"
  297. onclick="${del}"
  298. />
  299. `;
  300. function del(event) {
  301. event.stopPropagation();
  302. emit('removeUpload', file);
  303. }
  304. }
  305. };
  306. module.exports.uploading = function(state, emit) {
  307. const progress = state.transfer.progressRatio;
  308. const progressPercent = percent(progress);
  309. const archive = state.archive;
  310. return html`
  311. <send-upload-area
  312. id="${archive.id}"
  313. class="flex flex-col items-start rounded shadow-light bg-white p-4 w-full">
  314. <p class="w-full">
  315. <img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
  316. <h1 class="text-sm font-medium word-break-all">${archive.name}</h1>
  317. <div class="text-xs font-normal opacity-75 pt-1">${bytes(
  318. archive.size
  319. )}</div>
  320. </p>
  321. <div class="text-xs text-grey-dark w-full mt-2 mb-2">
  322. ${expiryInfo(state.translate, {
  323. dlimit: state.archive.dlimit,
  324. dtotal: 0,
  325. expiresAt: Date.now() + 500 + state.archive.timeLimit * 1000
  326. })}
  327. </div>
  328. <div class="text-blue-dark text-sm font-medium mt-2">${progressPercent}</div>
  329. <progress class="my-3" value="${progress}">${progressPercent}</progress>
  330. <button
  331. class="text-blue-dark hover:text-blue-darker focus:text-blue-darker self-end font-medium"
  332. onclick=${cancel}>
  333. ${state.translate('uploadingPageCancel')}
  334. </button>
  335. </send-upload-area>`;
  336. function cancel(event) {
  337. event.stopPropagation();
  338. event.target.disabled = true;
  339. emit('cancel');
  340. }
  341. };
  342. module.exports.empty = function(state, emit) {
  343. return html`
  344. <send-upload-area
  345. class="flex flex-col items-center justify-center border-2 border-dashed border-grey rounded px-6 py-16 h-full w-full"
  346. onclick="${e => {
  347. if (e.target.tagName !== 'LABEL') {
  348. document.getElementById('file-upload').click();
  349. }
  350. }}"
  351. >
  352. <img src="${assets.get('addfiles.svg')}" width="48" height="48" />
  353. <div
  354. class="pt-6 pb-2 text-center text-lg font-bold capitalize tracking-wide"
  355. >
  356. ${state.translate('uploadDropDragMessage')}
  357. </div>
  358. <div class="pb-6 text-center text-base italic">
  359. ${state.translate('uploadDropClickMessage')}
  360. </div>
  361. <input
  362. id="file-upload"
  363. class="hidden"
  364. type="file"
  365. multiple
  366. onchange="${add}"
  367. onclick="${e => e.stopPropagation()}"
  368. />
  369. <label
  370. for="file-upload"
  371. role="button"
  372. class="btn rounded-lg flex items-center mt-4"
  373. title="${state.translate('addFilesButton')}"
  374. >
  375. ${state.translate('addFilesButton')}
  376. </label>
  377. </send-upload-area>
  378. `;
  379. function add(event) {
  380. event.preventDefault();
  381. const newFiles = Array.from(event.target.files);
  382. emit('addFiles', { files: newFiles });
  383. }
  384. };
  385. module.exports.preview = function(state, emit) {
  386. const archive = state.fileInfo;
  387. if (archive.open === undefined) {
  388. archive.open = true;
  389. }
  390. return html`
  391. <send-archive class="flex flex-col max-h-full bg-white rounded shadow-light p-4 w-full md:w-4/5">
  392. <p class="w-full mb-4">
  393. <img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
  394. <h1 class="text-sm font-medium word-break-all">${archive.name}</h1>
  395. <div class="text-xs font-normal opacity-75 pt-1">${bytes(
  396. archive.size
  397. )}</div>
  398. </p>
  399. <div class="h-full md:h-48 overflow-y-auto">
  400. ${archiveDetails(state.translate, archive)}
  401. </div>
  402. <button
  403. id="download-btn"
  404. class="btn rounded-lg mt-4 w-full flex-no-shrink"
  405. title="${state.translate('downloadButtonLabel')}"
  406. onclick=${download}>
  407. ${state.translate('downloadButtonLabel')}
  408. </button>
  409. </send-archive>`;
  410. function download(event) {
  411. event.preventDefault();
  412. event.target.disabled = true;
  413. emit('download', archive);
  414. }
  415. };
  416. module.exports.downloading = function(state) {
  417. const archive = state.fileInfo;
  418. const progress = state.transfer.progressRatio;
  419. const progressPercent = percent(progress);
  420. return html`
  421. <send-archive class="flex flex-col bg-white rounded shadow-light p-4 w-full md:w-4/5">
  422. <p class="w-full mb-4">
  423. <img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
  424. <h1 class="text-sm font-medium word-break-all">${archive.name}</h1>
  425. <div class="text-xs font-normal opacity-75 pt-1">${bytes(
  426. archive.size
  427. )}</div>
  428. </p>
  429. <div class="text-blue-dark text-sm font-medium mt-2">${progressPercent}</div>
  430. <progress class="my-3" value="${progress}">${progressPercent}</progress>
  431. </send-archive>`;
  432. };