archiveTile.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  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="my-2 px-4 md:px-0">
  29. <div class="checkbox inline-block mr-3">
  30. <input
  31. id="add-password"
  32. type="checkbox"
  33. ${state.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.password
  44. ? ''
  45. : 'invisible'} border rounded-sm focus:border-blue leading-normal my-2 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.password || ''}"
  53. />
  54. <label
  55. id="password-msg"
  56. for="password-input"
  57. class="block text-xs text-grey-darker mt-1"
  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.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.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 overflow-y-scroll"
  116. ${archive.open ? 'open' : ''}
  117. ontoggle="${toggled}"
  118. >
  119. <summary
  120. >${translate('fileCount', {
  121. num: archive.manifest.files.length
  122. })}</summary
  123. >
  124. ${list(
  125. archive.manifest.files.map(f => fileInfo(f)),
  126. 'list-reset h-full'
  127. )}
  128. </details>
  129. `;
  130. }
  131. function toggled(event) {
  132. event.stopPropagation();
  133. archive.open = event.target.open;
  134. }
  135. }
  136. module.exports = function(state, emit, archive) {
  137. const copyOrShare =
  138. platform() !== 'android'
  139. ? html`
  140. <button
  141. class="text-blue hover:text-blue-dark focus:text-blue-darker self-end font-medium flex items-center"
  142. onclick=${copy}
  143. >
  144. <img src="${assets.get('copy-16.svg')}" class="mr-2" />
  145. ${state.translate('copyUrlHover')}
  146. </button>
  147. `
  148. : html`
  149. <button
  150. class="text-blue hover:text-blue-dark focus:text-blue-darker self-end font-medium flex items-center"
  151. onclick=${share}
  152. >
  153. <img src="${assets.get('share-24.svg')}" class="mr-2" /> Share
  154. </button>
  155. `;
  156. const dl =
  157. platform() === 'web'
  158. ? html`
  159. <a
  160. class="flex items-end text-blue hover:text-blue-dark focus:text-blue-darker font-medium"
  161. href="${archive.url}"
  162. >
  163. <img src="${assets.get('download.svg')}" class="mr-1" />
  164. ${state.translate('downloadButtonLabel')}
  165. </a>
  166. `
  167. : html`
  168. <div></div>
  169. `;
  170. return html`
  171. <send-archive
  172. id="archive-${archive.id}"
  173. class="flex flex-col items-start border border-grey-light bg-white p-4 w-full">
  174. <p class="w-full">
  175. <img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
  176. <input
  177. type="image"
  178. class="float-right self-center text-white delete"
  179. alt="Delete"
  180. src="${assets.get('close-16.svg')}"
  181. onclick=${del}/>
  182. <h1 class="text-sm font-medium word-break-all">${archive.name}</h1>
  183. <div class="text-xs font-normal opacity-75 pt-1">${bytes(
  184. archive.size
  185. )}</div>
  186. </p>
  187. <div class="text-xs text-grey-dark w-full mt-2 mb-2">
  188. ${expiryInfo(state.translate, archive)}
  189. </div>
  190. ${archiveDetails(state.translate, archive)}
  191. <hr class="w-full border-t my-4">
  192. <div class="flex justify-between w-full">
  193. ${dl}
  194. ${copyOrShare}
  195. </div>
  196. </send-archive>`;
  197. function copy(event) {
  198. event.stopPropagation();
  199. copyToClipboard(archive.url);
  200. const text = event.target.lastChild;
  201. text.textContent = state.translate('copiedUrl');
  202. setTimeout(
  203. () => (text.textContent = state.translate('copyUrlHover')),
  204. 1000
  205. );
  206. }
  207. function del(event) {
  208. event.stopPropagation();
  209. emit('delete', { file: archive, location: 'success-screen' });
  210. }
  211. function share(event) {
  212. event.stopPropagation();
  213. Android.shareUrl(archive.url);
  214. }
  215. };
  216. module.exports.wip = function(state, emit) {
  217. return html`
  218. <send-upload-area class="flex flex-col bg-white md:h-full w-full" id="wip">
  219. ${list(
  220. Array.from(state.archive.files)
  221. .reverse()
  222. .map(f => fileInfo(f, remove(f))),
  223. 'list-reset overflow-y-scroll px-4 bg-blue-lightest md:h-full md:max-h-half-screen',
  224. 'bg-white px-2 mt-3 border border-grey-light rounded'
  225. )}
  226. <div class="flex-grow p-4 bg-blue-lightest mb-6 font-medium">
  227. <input
  228. id="file-upload"
  229. class="hidden"
  230. type="file"
  231. multiple
  232. onchange="${add}"
  233. />
  234. <label
  235. for="file-upload"
  236. class="flex flex-row items-center justify-between w-full p-2 cursor-pointer"
  237. title="${state.translate('addFilesButton')}"
  238. >
  239. <div class="flex items-center">
  240. <img src="${assets.get('addfiles.svg')}" class="w-6 h-6 mr-2" />
  241. ${state.translate('addFilesButton')}
  242. </div>
  243. <div class="font-normal text-sm text-grey-darker">
  244. ${state.translate('totalSize', { size: bytes(state.archive.size) })}
  245. </div>
  246. </label>
  247. </div>
  248. ${expiryOptions(state, emit)} ${password(state, emit)}
  249. <button
  250. id="upload-btn"
  251. class="btn md:rounded flex-no-shrink"
  252. title="${state.translate('uploadFilesButton')}"
  253. onclick="${upload}"
  254. >
  255. ${state.translate('uploadFilesButton')}
  256. </button>
  257. </send-upload-area>
  258. `;
  259. function upload(event) {
  260. window.scrollTo(0, 0);
  261. event.preventDefault();
  262. event.target.disabled = true;
  263. if (!state.uploading) {
  264. emit('upload', {
  265. type: 'click',
  266. dlimit: state.downloadCount || 1,
  267. password: state.password
  268. });
  269. }
  270. }
  271. function add(event) {
  272. event.preventDefault();
  273. const newFiles = Array.from(event.target.files);
  274. emit('addFiles', { files: newFiles });
  275. setTimeout(() => {
  276. document
  277. .querySelector('#wip > ul > li:first-child')
  278. .scrollIntoView({ block: 'center' });
  279. });
  280. }
  281. function remove(file) {
  282. return html`
  283. <input
  284. type="image"
  285. class="self-center text-white ml-4 delete"
  286. alt="Delete"
  287. src="${assets.get('close-16.svg')}"
  288. onclick="${del}"
  289. />
  290. `;
  291. function del(event) {
  292. event.stopPropagation();
  293. emit('removeUpload', file);
  294. }
  295. }
  296. };
  297. module.exports.uploading = function(state, emit) {
  298. const progress = state.transfer.progressRatio;
  299. const progressPercent = percent(progress);
  300. const archive = state.archive;
  301. return html`
  302. <send-upload-area
  303. id="${archive.id}"
  304. class="flex flex-col items-start border border-grey-light bg-white p-4 w-full">
  305. <p class="w-full">
  306. <img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
  307. <h1 class="text-sm font-medium word-break-all">${archive.name}</h1>
  308. <div class="text-xs font-normal opacity-75 pt-1">${bytes(
  309. archive.size
  310. )}</div>
  311. </p>
  312. <div class="text-xs text-grey-dark w-full mt-2 mb-2">
  313. ${expiryInfo(state.translate, {
  314. dlimit: state.downloadCount || 1,
  315. dtotal: 0,
  316. expiresAt: Date.now() + 500 + state.timeLimit * 1000
  317. })}
  318. </div>
  319. <div class="text-blue text-sm font-medium mt-2">${progressPercent}</div>
  320. <progress class="my-3" value="${progress}">${progressPercent}</progress>
  321. <button
  322. class="text-blue hover:text-blue-dark focus:text-blue-darker self-end font-medium"
  323. onclick=${cancel}>
  324. ${state.translate('uploadingPageCancel')}
  325. </button>
  326. </send-upload-area>`;
  327. function cancel(event) {
  328. event.stopPropagation();
  329. event.target.disabled = true;
  330. emit('cancel');
  331. }
  332. };
  333. module.exports.empty = function(state, emit) {
  334. return html`
  335. <send-upload-area
  336. class="flex flex-col items-center justify-center border-2 border-dashed border-blue-light px-6 py-16 h-full w-full"
  337. onclick="${e => {
  338. if (e.target.tagName !== 'LABEL') {
  339. document.getElementById('file-upload').click();
  340. }
  341. }}"
  342. >
  343. <img src="${assets.get('addfiles.svg')}" width="48" height="48" />
  344. <div
  345. class="pt-6 pb-2 text-center text-lg font-bold uppercase tracking-wide"
  346. >
  347. ${state.translate('uploadDropDragMessage')}
  348. </div>
  349. <div class="pb-6 text-center text-base italic">
  350. ${state.translate('uploadDropClickMessage')}
  351. </div>
  352. <input
  353. id="file-upload"
  354. class="hidden"
  355. type="file"
  356. multiple
  357. onchange="${add}"
  358. onclick="${e => e.stopPropagation()}"
  359. />
  360. <label
  361. for="file-upload"
  362. role="button"
  363. class="btn rounded flex items-center mt-4"
  364. title="${state.translate('addFilesButton')}"
  365. >
  366. ${state.translate('addFilesButton')}
  367. </label>
  368. </send-upload-area>
  369. `;
  370. function add(event) {
  371. event.preventDefault();
  372. const newFiles = Array.from(event.target.files);
  373. emit('addFiles', { files: newFiles });
  374. }
  375. };
  376. module.exports.preview = function(state, emit) {
  377. const archive = state.fileInfo;
  378. if (archive.open === undefined) {
  379. archive.open = true;
  380. }
  381. return html`
  382. <send-archive class="flex flex-col max-h-full bg-white border border-grey-light p-4 w-full">
  383. <p class="w-full mb-4">
  384. <img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
  385. <h1 class="text-sm font-medium word-break-all">${archive.name}</h1>
  386. <div class="text-xs font-normal opacity-75 pt-1">${bytes(
  387. archive.size
  388. )}</div>
  389. </p>
  390. ${archiveDetails(state.translate, archive)}
  391. <button
  392. id="download-btn"
  393. class="btn rounded mt-4 w-full flex-no-shrink"
  394. title="${state.translate('downloadButtonLabel')}"
  395. onclick=${download}>
  396. ${state.translate('downloadButtonLabel')}
  397. </button>
  398. </send-archive>`;
  399. function download(event) {
  400. event.preventDefault();
  401. event.target.disabled = true;
  402. emit('download', archive);
  403. }
  404. };
  405. module.exports.downloading = function(state, emit) {
  406. const archive = state.fileInfo;
  407. const progress = state.transfer.progressRatio;
  408. const progressPercent = percent(progress);
  409. return html`
  410. <send-archive class="flex flex-col bg-white border border-grey-light p-4 w-full">
  411. <p class="w-full mb-4">
  412. <img class="float-left mr-3" src="${assets.get('blue_file.svg')}"/>
  413. <h1 class="text-sm font-medium word-break-all">${archive.name}</h1>
  414. <div class="text-xs font-normal opacity-75 pt-1">${bytes(
  415. archive.size
  416. )}</div>
  417. </p>
  418. <div class="text-blue text-sm font-medium mt-2">${progressPercent}</div>
  419. <progress class="my-3" value="${progress}">${progressPercent}</progress>
  420. <button
  421. class="border rounded bg-grey-dark text-white mt-2 text-center py-2 px-6 h-12 w-full flex flex-no-shrink items-center justify-center font-semibold"
  422. title="${state.translate('downloadCancel')}"
  423. onclick=${cancel}>
  424. ${state.translate('downloadCancel')}
  425. </button>
  426. </send-archive>`;
  427. function cancel(event) {
  428. event.preventDefault();
  429. event.target.disabled = true;
  430. emit('cancel');
  431. }
  432. };