archiveTile.js 17 KB

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