archiveTile.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  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('archiveExpiryInfo', {
  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 = 4096;
  27. return html`
  28. <div class="mb-2 px-1">
  29. <input
  30. id="autocomplete-decoy"
  31. class="hidden"
  32. type="password"
  33. value="lol"
  34. />
  35. <div class="checkbox inline-block mr-3">
  36. <input
  37. id="add-password"
  38. type="checkbox"
  39. ${state.archive.password ? 'checked' : ''}
  40. autocomplete="off"
  41. onchange="${togglePasswordInput}"
  42. />
  43. <label for="add-password">
  44. ${state.translate('addPassword')}
  45. </label>
  46. </div>
  47. <div class="relative inline-block my-1">
  48. <input
  49. id="password-input"
  50. class="${state.archive.password
  51. ? ''
  52. : 'invisible'} border-default rounded-default focus:border-primary leading-normal my-1 py-1 px-2 h-8 dark:bg-grey-80"
  53. autocomplete="off"
  54. maxlength="${MAX_LENGTH}"
  55. type="password"
  56. oninput="${inputChanged}"
  57. onfocus="${focused}"
  58. placeholder="${state.translate('unlockInputPlaceholder')}"
  59. value="${state.archive.password || ''}"
  60. />
  61. <button
  62. id="password-preview-button"
  63. type="button"
  64. class="${state.archive.password
  65. ? ''
  66. : 'invisible'} absolute top-0 right-0 w-8 h-8"
  67. onclick="${onPasswordPreviewButtonclicked}"
  68. >
  69. <img
  70. src="${assets.get('eye.svg')}"
  71. width="22"
  72. height="22"
  73. class="m-auto mt-2"
  74. />
  75. </button>
  76. </div>
  77. <label
  78. id="password-msg"
  79. for="password-input"
  80. class="block text-xs text-grey-70"
  81. ></label>
  82. </div>
  83. `;
  84. function onPasswordPreviewButtonclicked(event) {
  85. event.preventDefault();
  86. const input = document.getElementById('password-input');
  87. const eyeIcon = event.currentTarget.querySelector('img');
  88. if (input.type === 'password') {
  89. input.type = 'text';
  90. eyeIcon.src = assets.get('eye-off.svg');
  91. } else {
  92. input.type = 'password';
  93. eyeIcon.src = assets.get('eye.svg');
  94. }
  95. input.focus();
  96. }
  97. function togglePasswordInput(event) {
  98. event.stopPropagation();
  99. const checked = event.target.checked;
  100. const input = document.getElementById('password-input');
  101. const passwordPreviewButton = document.getElementById(
  102. 'password-preview-button'
  103. );
  104. if (checked) {
  105. input.classList.remove('invisible');
  106. passwordPreviewButton.classList.remove('invisible');
  107. input.focus();
  108. } else {
  109. input.classList.add('invisible');
  110. passwordPreviewButton.classList.add('invisible');
  111. input.value = '';
  112. document.getElementById('password-msg').textContent = '';
  113. state.archive.password = null;
  114. }
  115. }
  116. function inputChanged() {
  117. const passwordInput = document.getElementById('password-input');
  118. const pwdmsg = document.getElementById('password-msg');
  119. const password = passwordInput.value;
  120. const length = password.length;
  121. if (length === MAX_LENGTH) {
  122. pwdmsg.textContent = state.translate('maxPasswordLength', {
  123. length: MAX_LENGTH
  124. });
  125. } else {
  126. pwdmsg.textContent = '';
  127. }
  128. state.archive.password = password;
  129. }
  130. function focused(event) {
  131. event.preventDefault();
  132. const el = document.getElementById('password-input');
  133. if (el.placeholder !== state.translate('unlockInputPlaceholder')) {
  134. el.placeholder = '';
  135. }
  136. }
  137. }
  138. function fileInfo(file, action) {
  139. return html`
  140. <send-file class="flex flex-row items-center p-3 w-full">
  141. <svg class="h-8 w-8 text-primary">
  142. <use xlink:href="${assets.get('blue_file.svg')}#icon"/>
  143. </svg>
  144. <p class="ml-4 w-full">
  145. <h1 class="text-base font-medium word-break-all">${file.name}</h1>
  146. <div class="text-sm font-normal opacity-75 pt-1">${bytes(
  147. file.size
  148. )}</div>
  149. </p>
  150. ${action}
  151. </send-file>`;
  152. }
  153. function archiveInfo(archive, action) {
  154. return html`
  155. <p class="w-full flex items-center">
  156. <svg class="h-8 w-6 mr-3 flex-shrink-0 text-primary">
  157. <use xlink:href="${assets.get('blue_file.svg')}#icon"/>
  158. </svg>
  159. <p class="flex-grow">
  160. <h1 class="text-base font-medium word-break-all">${archive.name}</h1>
  161. <div class="text-sm font-normal opacity-75 pt-1">${bytes(
  162. archive.size
  163. )}</div>
  164. </p>
  165. ${action}
  166. </p>`;
  167. }
  168. function archiveDetails(translate, archive) {
  169. if (archive.manifest.files.length > 1) {
  170. return html`
  171. <details
  172. class="w-full pb-1"
  173. ${archive.open ? 'open' : ''}
  174. ontoggle="${toggled}"
  175. >
  176. <summary
  177. class="flex items-center link-primary text-sm cursor-pointer outline-none"
  178. >
  179. <svg
  180. class="fill-current w-4 h-4 mr-1"
  181. xmlns="http://www.w3.org/2000/svg"
  182. viewBox="0 0 20 20"
  183. >
  184. <path
  185. 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"
  186. />
  187. </svg>
  188. ${translate('fileCount', {
  189. num: archive.manifest.files.length
  190. })}
  191. </summary>
  192. ${list(archive.manifest.files.map(f => fileInfo(f)))}
  193. </details>
  194. `;
  195. }
  196. function toggled(event) {
  197. event.stopPropagation();
  198. archive.open = event.target.open;
  199. }
  200. }
  201. module.exports = function(state, emit, archive) {
  202. const copyOrShare =
  203. state.capabilities.share || platform() === 'android'
  204. ? html`
  205. <button
  206. class="link-primary self-end flex items-start"
  207. onclick=${share}
  208. title="Share link"
  209. >
  210. <svg class="h-4 w-4 mr-2">
  211. <use xlink:href="${assets.get('share-24.svg')}#icon" />
  212. </svg>
  213. Share link
  214. </button>
  215. `
  216. : html`
  217. <button
  218. class="link-primary focus:outline self-end flex items-center"
  219. onclick=${copy}
  220. title="${state.translate('copyLinkButton')}"
  221. >
  222. <svg class="h-4 w-4 mr-2">
  223. <use xlink:href="${assets.get('copy-16.svg')}#icon" />
  224. </svg>
  225. ${state.translate('copyLinkButton')}
  226. </button>
  227. `;
  228. const dl =
  229. platform() === 'web'
  230. ? html`
  231. <a
  232. class="flex items-baseline link-primary"
  233. href="${archive.url}"
  234. title="${state.translate('downloadButtonLabel')}"
  235. tabindex="0"
  236. >
  237. <svg class="h-4 w-3 mr-2">
  238. <use xlink:href="${assets.get('dl.svg')}#icon" />
  239. </svg>
  240. ${state.translate('downloadButtonLabel')}
  241. </a>
  242. `
  243. : html`
  244. <div></div>
  245. `;
  246. return html`
  247. <send-archive
  248. id="archive-${archive.id}"
  249. class="flex flex-col items-start rounded-default shadow-light bg-white p-4 w-full dark:bg-grey-90 dark:border-default dark:border-grey-70"
  250. >
  251. ${archiveInfo(
  252. archive,
  253. html`
  254. <input
  255. type="image"
  256. class="self-start flex-shrink-0 text-white hover:opacity-75 focus:outline"
  257. alt="${state.translate('deleteButtonHover')}"
  258. title="${state.translate('deleteButtonHover')}"
  259. src="${assets.get('close-16.svg')}"
  260. onclick=${del}
  261. />
  262. `
  263. )}
  264. <div class="text-sm opacity-75 w-full mt-2 mb-2">
  265. ${expiryInfo(state.translate, archive)}
  266. </div>
  267. ${archiveDetails(state.translate, archive)}
  268. <hr class="w-full border-t my-4 dark:border-grey-70" />
  269. <div class="flex justify-between w-full">
  270. ${dl} ${copyOrShare}
  271. </div>
  272. </send-archive>
  273. `;
  274. function copy(event) {
  275. event.stopPropagation();
  276. copyToClipboard(archive.url);
  277. const text = event.target.lastChild;
  278. text.textContent = state.translate('copiedUrl');
  279. setTimeout(
  280. () => (text.textContent = state.translate('copyLinkButton')),
  281. 1000
  282. );
  283. }
  284. function del(event) {
  285. event.stopPropagation();
  286. emit('delete', archive);
  287. }
  288. async function share(event) {
  289. event.stopPropagation();
  290. if (platform() === 'android') {
  291. Android.shareUrl(archive.url);
  292. } else {
  293. try {
  294. await navigator.share({
  295. title: state.translate('-send-brand'),
  296. text: `Download "${archive.name}" with Send: simple, safe file sharing`,
  297. //state.translate('shareMessage', { name }),
  298. url: archive.url
  299. });
  300. } catch (e) {
  301. // ignore
  302. }
  303. }
  304. }
  305. };
  306. module.exports.wip = function(state, emit) {
  307. return html`
  308. <send-upload-area
  309. class="flex flex-col bg-white h-full w-full dark:bg-grey-90"
  310. id="wip"
  311. >
  312. ${list(
  313. Array.from(state.archive.files)
  314. .reverse()
  315. .map(f =>
  316. fileInfo(f, remove(f, state.translate('deleteButtonHover')))
  317. ),
  318. 'flex-shrink bg-grey-10 rounded-t overflow-y-auto px-6 py-4 md:h-full md:max-h-half-screen dark:bg-black',
  319. 'bg-white px-2 my-2 shadow-light rounded-default dark:bg-grey-90 dark:border-default dark:border-grey-80'
  320. )}
  321. <div
  322. class="flex-shrink-0 flex-grow flex items-end p-4 bg-grey-10 rounded-b mb-1 font-medium dark:bg-grey-90"
  323. >
  324. <input
  325. id="file-upload"
  326. class="opacity-0 w-0 h-0 appearance-none absolute overflow-hidden"
  327. type="file"
  328. multiple
  329. onfocus="${focus}"
  330. onblur="${blur}"
  331. onchange="${add}"
  332. />
  333. <div
  334. for="file-upload"
  335. class="flex flex-row items-center justify-between w-full p-2"
  336. >
  337. <label
  338. for="file-upload"
  339. class="flex items-center cursor-pointer"
  340. title="${state.translate('addFilesButton')}"
  341. >
  342. <svg class="w-6 h-6 mr-2 link-primary">
  343. <use xlink:href="${assets.get('addfiles.svg')}#plus" />
  344. </svg>
  345. ${state.translate('addFilesButton')}
  346. </label>
  347. <div class="font-normal text-sm text-grey-70 dark:text-grey-40">
  348. ${state.translate('totalSize', {
  349. size: bytes(state.archive.size)
  350. })}
  351. </div>
  352. </div>
  353. </div>
  354. ${expiryOptions(state, emit)} ${password(state, emit)}
  355. <button
  356. id="upload-btn"
  357. class="btn rounded-lg flex-shrink-0 focus:outline"
  358. title="${state.translate('uploadButton')}"
  359. onclick="${upload}"
  360. >
  361. ${state.translate('uploadButton')}
  362. </button>
  363. </send-upload-area>
  364. `;
  365. function focus(event) {
  366. event.target.nextElementSibling.firstElementChild.classList.add('outline');
  367. }
  368. function blur(event) {
  369. event.target.nextElementSibling.firstElementChild.classList.remove(
  370. 'outline'
  371. );
  372. }
  373. function upload(event) {
  374. window.scrollTo(0, 0);
  375. event.preventDefault();
  376. event.target.disabled = true;
  377. if (!state.uploading) {
  378. emit('upload');
  379. }
  380. }
  381. function add(event) {
  382. event.preventDefault();
  383. const newFiles = Array.from(event.target.files);
  384. emit('addFiles', { files: newFiles });
  385. setTimeout(() => {
  386. document
  387. .querySelector('#wip > ul > li:first-child')
  388. .scrollIntoView({ block: 'center' });
  389. });
  390. }
  391. function remove(file, desc) {
  392. return html`
  393. <input
  394. type="image"
  395. class="self-center text-white ml-4 h-4 hover:opacity-75 focus:outline"
  396. alt="${desc}"
  397. title="${desc}"
  398. src="${assets.get('close-16.svg')}"
  399. onclick="${del}"
  400. />
  401. `;
  402. function del(event) {
  403. event.stopPropagation();
  404. emit('removeUpload', file);
  405. }
  406. }
  407. };
  408. module.exports.uploading = function(state, emit) {
  409. const progress = state.transfer.progressRatio;
  410. const progressPercent = percent(progress);
  411. const archive = state.archive;
  412. return html`
  413. <send-upload-area
  414. id="${archive.id}"
  415. class="flex flex-col items-start rounded-default shadow-light bg-white p-4 w-full dark:bg-grey-90"
  416. >
  417. ${archiveInfo(archive)}
  418. <div class="text-xs opacity-75 w-full mt-2 mb-2">
  419. ${expiryInfo(state.translate, {
  420. dlimit: state.archive.dlimit,
  421. dtotal: 0,
  422. expiresAt: Date.now() + 500 + state.archive.timeLimit * 1000
  423. })}
  424. </div>
  425. <div class="link-primary text-sm font-medium mt-2">
  426. ${progressPercent}
  427. </div>
  428. <progress class="my-3" value="${progress}">${progressPercent}</progress>
  429. <button
  430. class="link-primary self-end font-medium"
  431. onclick=${cancel}
  432. title="${state.translate('deletePopupCancel')}"
  433. >
  434. ${state.translate('deletePopupCancel')}
  435. </button>
  436. </send-upload-area>
  437. `;
  438. function cancel(event) {
  439. event.stopPropagation();
  440. event.target.disabled = true;
  441. emit('cancel');
  442. }
  443. };
  444. module.exports.empty = function(state, emit) {
  445. const upsell =
  446. state.user.loggedIn || !state.capabilities.account
  447. ? ''
  448. : html`
  449. <button
  450. class="center font-medium text-sm link-primary mt-4 mb-2"
  451. onclick="${event => {
  452. event.stopPropagation();
  453. emit('signup-cta', 'drop');
  454. }}"
  455. >
  456. ${state.translate('signInSizeBump', {
  457. size: bytes(state.LIMITS.MAX_FILE_SIZE)
  458. })}
  459. </button>
  460. `;
  461. return html`
  462. <send-upload-area
  463. class="flex flex-col items-center justify-center border-2 border-dashed border-grey-transparent rounded-default px-6 py-16 h-full w-full dark:border-grey-60"
  464. onclick="${e => {
  465. if (e.target.tagName !== 'LABEL') {
  466. document.getElementById('file-upload').click();
  467. }
  468. }}"
  469. >
  470. <svg class="w-10 h-10 link-primary">
  471. <use xlink:href="/${assets.get('addfiles.svg')}#plus" />
  472. </svg>
  473. <div class="pt-6 pb-2 text-center text-lg font-bold tracking-wide">
  474. ${state.translate('dragAndDropFiles')}
  475. </div>
  476. <div class="pb-6 text-center text-base">
  477. ${state.translate('orClickWithSize', {
  478. size: bytes(state.user.maxSize)
  479. })}
  480. </div>
  481. <input
  482. id="file-upload"
  483. class="opacity-0 w-0 h-0 appearance-none absolute overflow-hidden"
  484. type="file"
  485. multiple
  486. onfocus="${focus}"
  487. onblur="${blur}"
  488. onchange="${add}"
  489. onclick="${e => e.stopPropagation()}"
  490. />
  491. <label
  492. for="file-upload"
  493. role="button"
  494. class="btn rounded-lg flex items-center mt-4"
  495. title="${state.translate('addFilesButton', {
  496. size: bytes(state.user.maxSize)
  497. })}"
  498. >
  499. ${state.translate('addFilesButton')}
  500. </label>
  501. ${upsell}
  502. </send-upload-area>
  503. `;
  504. function focus(event) {
  505. event.target.nextElementSibling.classList.add('bg-primary', 'outline');
  506. }
  507. function blur(event) {
  508. event.target.nextElementSibling.classList.remove('bg-primary', 'outline');
  509. }
  510. function add(event) {
  511. event.preventDefault();
  512. const newFiles = Array.from(event.target.files);
  513. emit('addFiles', { files: newFiles });
  514. }
  515. };
  516. module.exports.preview = function(state, emit) {
  517. const archive = state.fileInfo;
  518. if (archive.open === undefined) {
  519. archive.open = true;
  520. }
  521. const single = archive.manifest.files.length === 1;
  522. const details = single
  523. ? ''
  524. : html`
  525. <div class="mt-4 h-full md:h-48 overflow-y-auto">
  526. ${archiveDetails(state.translate, archive)}
  527. </div>
  528. `;
  529. return html`
  530. <send-archive
  531. class="flex flex-col max-h-full bg-white p-4 w-full md:w-128 dark:bg-grey-90"
  532. >
  533. <div class="border-default rounded-default py-3 px-6 dark:border-grey-70">
  534. ${archiveInfo(archive)} ${details}
  535. </div>
  536. <button
  537. id="download-btn"
  538. class="btn rounded-lg mt-4 w-full flex-shrink-0 focus:outline"
  539. title="${state.translate('downloadButtonLabel')}"
  540. onclick=${download}
  541. >
  542. ${state.translate('downloadButtonLabel')}
  543. </button>
  544. </send-archive>
  545. `;
  546. function download(event) {
  547. event.preventDefault();
  548. event.target.disabled = true;
  549. emit('download');
  550. }
  551. };
  552. module.exports.downloading = function(state) {
  553. const archive = state.fileInfo;
  554. const progress = state.transfer.progressRatio;
  555. const progressPercent = percent(progress);
  556. return html`
  557. <send-archive
  558. class="flex flex-col bg-white rounded-default shadow-light p-4 w-full max-w-sm md:w-128 dark:bg-grey-90"
  559. >
  560. ${archiveInfo(archive)}
  561. <div class="link-primary text-sm font-medium mt-2">
  562. ${progressPercent}
  563. </div>
  564. <progress class="my-3" value="${progress}">${progressPercent}</progress>
  565. </send-archive>
  566. `;
  567. };