2
0

FileManager.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982
  1. <template lang="pug">
  2. q-layout.fileman(view='hHh lpR lFr', container)
  3. q-header.card-header
  4. q-toolbar(dark)
  5. q-icon(name='img:/_assets/icons/fluent-folder.svg', left, size='md')
  6. span {{t(`fileman.title`)}}
  7. q-toolbar(dark)
  8. q-input(
  9. dark
  10. v-model='state.search'
  11. standout='bg-white text-dark'
  12. dense
  13. ref='searchField'
  14. style='width: 100%;'
  15. label='Search folder...'
  16. :debounce='500'
  17. )
  18. template(v-slot:prepend)
  19. q-icon(name='las la-search')
  20. template(v-slot:append)
  21. q-icon.cursor-pointer(
  22. name='las la-times'
  23. @click='state.search=``'
  24. v-if='state.search.length > 0'
  25. :color='$q.dark.isActive ? `blue` : `grey-4`'
  26. )
  27. q-toolbar(dark)
  28. q-space
  29. q-btn(
  30. flat
  31. dense
  32. no-caps
  33. color='red-3'
  34. :aria-label='t(`common.actions.close`)'
  35. icon='las la-times'
  36. @click='close'
  37. )
  38. q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.close`)}}
  39. q-drawer.fileman-left(:model-value='true', :width='350')
  40. .q-px-md.q-pb-sm
  41. tree(
  42. ref='treeComp'
  43. :nodes='state.treeNodes'
  44. :roots='state.treeRoots'
  45. v-model:selected='state.currentFolderId'
  46. @lazy-load='treeLazyLoad'
  47. :use-lazy-load='true'
  48. @context-action='treeContextAction'
  49. :display-mode='state.displayMode'
  50. )
  51. q-drawer.fileman-right(:model-value='$q.screen.gt.md', :width='350', side='right')
  52. .q-pa-md
  53. template(v-if='currentFileDetails')
  54. q-img.rounded-borders.q-mb-md(
  55. src='/_assets/illustrations/fileman-page.svg'
  56. width='100%'
  57. fit='cover'
  58. :ratio='16/10'
  59. no-spinner
  60. )
  61. .fileman-details-row(
  62. v-for='item of currentFileDetails.items'
  63. )
  64. label {{item.label}}
  65. span {{item.value}}
  66. q-page-container
  67. q-page.fileman-center
  68. //- TOOLBAR -----------------------------------------------------
  69. q-toolbar.fileman-toolbar
  70. template(v-if='state.isUploading')
  71. .fileman-progressbar
  72. div(:style='`width: ` + state.uploadPercentage + `%`') {{state.uploadPercentage}}%
  73. q-btn.acrylic-btn.q-ml-sm(
  74. flat
  75. dense
  76. no-caps
  77. color='negative'
  78. :aria-label='t(`common.actions.cancel`)'
  79. icon='las la-square'
  80. @click='uploadCancel'
  81. v-if='state.uploadPercentage < 100'
  82. )
  83. template(v-else)
  84. q-space
  85. q-btn.q-mr-sm(
  86. flat
  87. dense
  88. no-caps
  89. color='grey'
  90. :aria-label='t(`fileman.viewOptions`)'
  91. icon='las la-th-list'
  92. @click=''
  93. )
  94. q-tooltip(anchor='bottom middle', self='top middle') {{t(`fileman.viewOptions`)}}
  95. q-menu(
  96. transition-show='jump-down'
  97. transition-hide='jump-up'
  98. anchor='bottom right'
  99. self='top right'
  100. )
  101. q-card.q-pa-sm
  102. .text-center
  103. small.text-grey {{t(`fileman.viewOptions`)}}
  104. q-list(dense)
  105. q-separator.q-my-sm
  106. q-item(clickable)
  107. q-item-section(side)
  108. q-icon(name='las la-list', color='grey', size='xs')
  109. q-item-section.q-pr-sm Browse using...
  110. q-item-section(side)
  111. q-icon(name='las la-angle-right', color='grey', size='xs')
  112. q-menu(
  113. anchor='top end'
  114. self='top start'
  115. )
  116. q-list.q-pa-sm(dense)
  117. q-item(clickable, @click='state.displayMode = `path`')
  118. q-item-section(side)
  119. q-icon(
  120. :name='state.displayMode === `path` ? `las la-check-circle` : `las la-circle`'
  121. :color='state.displayMode === `path` ? `positive` : `grey`'
  122. size='xs'
  123. )
  124. q-item-section.q-pr-sm Browse Using Paths
  125. q-item(clickable, @click='state.displayMode = `title`')
  126. q-item-section(side)
  127. q-icon(
  128. :name='state.displayMode === `title` ? `las la-check-circle` : `las la-circle`'
  129. :color='state.displayMode === `title` ? `positive` : `grey`'
  130. size='xs'
  131. )
  132. q-item-section.q-pr-sm Browse Using Titles
  133. q-item(clickable, @click='state.isCompact = !state.isCompact')
  134. q-item-section(side)
  135. q-icon(
  136. :name='state.isCompact ? `las la-check-square` : `las la-stop`'
  137. :color='state.isCompact ? `positive` : `grey`'
  138. size='xs'
  139. )
  140. q-item-section.q-pr-sm Compact List
  141. q-item(clickable, @click='state.shouldShowFolders = !state.shouldShowFolders')
  142. q-item-section(side)
  143. q-icon(
  144. :name='state.shouldShowFolders ? `las la-check-square` : `las la-stop`'
  145. :color='state.shouldShowFolders ? `positive` : `grey`'
  146. size='xs'
  147. )
  148. q-item-section.q-pr-sm Show Folders
  149. q-btn.q-mr-sm(
  150. flat
  151. dense
  152. no-caps
  153. color='grey'
  154. :aria-label='t(`common.actions.refresh`)'
  155. icon='las la-redo-alt'
  156. @click='reloadFolder(state.currentFolderId)'
  157. )
  158. q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.refresh`)}}
  159. q-separator.q-mr-sm(inset, vertical)
  160. q-btn.q-mr-sm(
  161. flat
  162. dense
  163. no-caps
  164. color='blue'
  165. :label='t(`common.actions.new`)'
  166. :aria-label='t(`common.actions.new`)'
  167. icon='las la-plus-circle'
  168. @click=''
  169. )
  170. new-menu(
  171. :hide-asset-btn='true'
  172. :show-new-folder='true'
  173. @new-folder='() => newFolder(state.currentFolderId)'
  174. )
  175. q-btn(
  176. flat
  177. dense
  178. no-caps
  179. color='positive'
  180. :label='t(`common.actions.upload`)'
  181. :aria-label='t(`common.actions.upload`)'
  182. icon='las la-cloud-upload-alt'
  183. @click='uploadFile'
  184. )
  185. .fileman-emptylist(v-if='files.length < 1')
  186. template(v-if='state.fileListLoading')
  187. q-spinner.q-mr-sm(color='primary', size='xs', :thickness='3')
  188. span.text-primary Loading...
  189. template(v-else)
  190. q-icon.q-mr-sm(name='las la-exclamation-triangle', size='sm')
  191. span This folder is empty.
  192. q-list.fileman-filelist(v-else)
  193. q-item(
  194. v-for='item of files'
  195. :key='item.id'
  196. clickable
  197. active-class='active'
  198. :active='item.id === state.currentFileId'
  199. @click.native='selectItem(item)'
  200. @dblclick.native='openItem(item)'
  201. )
  202. q-item-section.fileman-filelist-icon(avatar)
  203. q-icon(:name='item.icon', :size='state.isCompact ? `md` : `xl`')
  204. q-item-section.fileman-filelist-label
  205. q-item-label {{item.title}}
  206. q-item-label(caption, v-if='!state.isCompact') {{item.caption}}
  207. q-item-section.fileman-filelist-side(side, v-if='item.side')
  208. .text-caption {{item.side}}
  209. //- RIGHT-CLICK MENU
  210. q-menu(
  211. touch-position
  212. context-menu
  213. auto-close
  214. transition-show='jump-down'
  215. transition-hide='jump-up'
  216. )
  217. q-card.q-pa-sm
  218. q-list(dense, style='min-width: 150px;')
  219. q-item(clickable, v-if='item.type === `page`')
  220. q-item-section(side)
  221. q-icon(name='las la-edit', color='orange')
  222. q-item-section Edit
  223. q-item(clickable, v-if='item.type !== `folder`', @click='openItem(item)')
  224. q-item-section(side)
  225. q-icon(name='las la-eye', color='primary')
  226. q-item-section View
  227. q-item(clickable, v-if='item.type !== `folder`', @click='copyItemURL(item)')
  228. q-item-section(side)
  229. q-icon(name='las la-clipboard', color='primary')
  230. q-item-section Copy URL
  231. q-item(clickable)
  232. q-item-section(side)
  233. q-icon(name='las la-copy', color='teal')
  234. q-item-section Duplicate...
  235. q-item(clickable)
  236. q-item-section(side)
  237. q-icon(name='las la-redo', color='teal')
  238. q-item-section Rename...
  239. q-item(clickable)
  240. q-item-section(side)
  241. q-icon(name='las la-arrow-right', color='teal')
  242. q-item-section Move to...
  243. q-item(clickable, @click='delItem(item)')
  244. q-item-section(side)
  245. q-icon(name='las la-trash-alt', color='negative')
  246. q-item-section.text-negative Delete
  247. q-footer
  248. q-bar.fileman-path
  249. small.text-caption.text-grey-7 {{folderPath}}
  250. input(
  251. type='file'
  252. ref='fileIpt'
  253. multiple
  254. @change='uploadNewFiles'
  255. style='display: none'
  256. )
  257. </template>
  258. <script setup>
  259. import { useI18n } from 'vue-i18n'
  260. import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
  261. import { filesize } from 'filesize'
  262. import { useQuasar } from 'quasar'
  263. import { DateTime } from 'luxon'
  264. import { cloneDeep, find } from 'lodash-es'
  265. import { useRoute, useRouter } from 'vue-router'
  266. import gql from 'graphql-tag'
  267. import Fuse from 'fuse.js/dist/fuse.basic.esm'
  268. import NewMenu from './PageNewMenu.vue'
  269. import Tree from './TreeNav.vue'
  270. import fileTypes from '../helpers/fileTypes'
  271. import { usePageStore } from 'src/stores/page'
  272. import { useSiteStore } from 'src/stores/site'
  273. import FolderCreateDialog from 'src/components/FolderCreateDialog.vue'
  274. import FolderDeleteDialog from 'src/components/FolderDeleteDialog.vue'
  275. // QUASAR
  276. const $q = useQuasar()
  277. // STORES
  278. const pageStore = usePageStore()
  279. const siteStore = useSiteStore()
  280. // ROUTER
  281. const router = useRouter()
  282. const route = useRoute()
  283. // I18N
  284. const { t } = useI18n()
  285. // DATA
  286. const state = reactive({
  287. loading: 0,
  288. search: '',
  289. currentFolderId: null,
  290. currentFileId: null,
  291. treeNodes: {},
  292. treeRoots: [],
  293. displayMode: 'title',
  294. isCompact: false,
  295. shouldShowFolders: true,
  296. isUploading: false,
  297. shouldCancelUpload: false,
  298. uploadPercentage: 0,
  299. fileList: [],
  300. fileListLoading: false
  301. })
  302. // REFS
  303. const fileIpt = ref(null)
  304. const treeComp = ref(null)
  305. // COMPUTED
  306. const folderPath = computed(() => {
  307. if (!state.currentFolderId) {
  308. return '/'
  309. } else {
  310. const folderNode = state.treeNodes[state.currentFolderId] ?? {}
  311. return folderNode.folderPath ? `/${folderNode.folderPath}/${folderNode.fileName}/` : `/${folderNode.fileName}/`
  312. }
  313. })
  314. const filteredFiles = computed(() => {
  315. if (state.search) {
  316. const fuse = new Fuse(state.fileList, {
  317. keys: [
  318. 'title',
  319. 'fileName'
  320. ]
  321. })
  322. return fuse.search(state.search).map(n => n.item)
  323. } else {
  324. return state.fileList
  325. }
  326. })
  327. const files = computed(() => {
  328. return filteredFiles.value.filter(f => {
  329. console.info(f)
  330. // -> Show Folders Filter
  331. if (f.type === 'folder' && !state.shouldShowFolders) {
  332. return false
  333. }
  334. return true
  335. }).map(f => {
  336. switch (f.type) {
  337. case 'folder': {
  338. f.icon = fileTypes.folder.icon
  339. f.caption = t('fileman.folderChildrenCount', f.children, { count: f.children })
  340. break
  341. }
  342. case 'page': {
  343. f.icon = fileTypes.page.icon
  344. f.caption = t(`fileman.${f.pageType}PageType`)
  345. break
  346. }
  347. case 'asset': {
  348. f.icon = fileTypes[f.fileType]?.icon ?? ''
  349. f.side = filesize(f.fileSize)
  350. if (fileTypes[f.fileType]) {
  351. f.caption = t(`fileman.${f.fileType}FileType`)
  352. } else {
  353. f.caption = t('fileman.unknownFileType', { type: f.fileType.toUpperCase() })
  354. }
  355. break
  356. }
  357. }
  358. return f
  359. })
  360. })
  361. const currentFileDetails = computed(() => {
  362. if (state.currentFileId) {
  363. const item = find(state.fileList, ['id', state.currentFileId])
  364. if (item.type === 'folder') {
  365. return null
  366. }
  367. const items = [
  368. {
  369. label: t('fileman.detailsTitle'),
  370. value: item.title
  371. }
  372. ]
  373. switch (item.type) {
  374. case 'page': {
  375. items.push({
  376. label: t('fileman.detailsPageType'),
  377. value: t(`fileman.${item.pageType}PageType`)
  378. })
  379. items.push({
  380. label: t('fileman.detailsPageEditor'),
  381. value: item.pageType
  382. })
  383. items.push({
  384. label: t('fileman.detailsPageUpdated'),
  385. value: DateTime.fromISO(item.updatedAt).toFormat('yyyy-MM-dd \'at\' h:mm ZZZZ')
  386. })
  387. items.push({
  388. label: t('fileman.detailsPageCreated'),
  389. value: DateTime.fromISO(item.updatedAt).toFormat('yyyy-MM-dd \'at\' h:mm ZZZZ')
  390. })
  391. break
  392. }
  393. case 'asset': {
  394. items.push({
  395. label: t('fileman.detailsAssetType'),
  396. value: fileTypes[item.fileType] ? t(`fileman.${item.fileType}FileType`) : t('fileman.unknownFileType', { type: item.fileType.toUpperCase() })
  397. })
  398. items.push({
  399. label: t('fileman.detailsAssetSize'),
  400. value: filesize(item.fileSize)
  401. })
  402. break
  403. }
  404. }
  405. return {
  406. thumbnail: '',
  407. items
  408. }
  409. } else {
  410. return null
  411. }
  412. })
  413. // WATCHERS
  414. watch(() => state.currentFolderId, async (newValue) => {
  415. await loadTree(newValue)
  416. })
  417. // METHODS
  418. function close () {
  419. siteStore.overlay = null
  420. }
  421. async function treeLazyLoad (nodeId, { done, fail }) {
  422. await loadTree(nodeId, ['folder'])
  423. done()
  424. }
  425. async function loadTree (parentId, types) {
  426. if (!parentId) {
  427. parentId = null
  428. }
  429. if (parentId === state.currentFolderId) {
  430. state.fileListLoading = true
  431. state.currentFileId = null
  432. state.fileList = []
  433. }
  434. try {
  435. const resp = await APOLLO_CLIENT.query({
  436. query: gql`
  437. query loadTree (
  438. $siteId: UUID!
  439. $parentId: UUID
  440. $types: [TreeItemType]
  441. ) {
  442. tree (
  443. siteId: $siteId
  444. parentId: $parentId
  445. types: $types
  446. ) {
  447. __typename
  448. ... on TreeItemFolder {
  449. id
  450. folderPath
  451. fileName
  452. title
  453. childrenCount
  454. }
  455. ... on TreeItemPage {
  456. id
  457. folderPath
  458. fileName
  459. title
  460. createdAt
  461. updatedAt
  462. editor
  463. }
  464. ... on TreeItemAsset {
  465. id
  466. folderPath
  467. fileName
  468. title
  469. createdAt
  470. updatedAt
  471. fileSize
  472. }
  473. }
  474. }
  475. `,
  476. variables: {
  477. siteId: siteStore.id,
  478. parentId,
  479. types
  480. },
  481. fetchPolicy: 'network-only'
  482. })
  483. const items = cloneDeep(resp?.data?.tree)
  484. if (items?.length > 0) {
  485. const newTreeRoots = []
  486. for (const item of items) {
  487. switch (item.__typename) {
  488. case 'TreeItemFolder': {
  489. // -> Tree Nodes
  490. if (!state.treeNodes[item.id]) {
  491. state.treeNodes[item.id] = {
  492. folderPath: item.folderPath,
  493. fileName: item.fileName,
  494. title: item.title,
  495. children: state.treeNodes[item.id]?.children ?? []
  496. }
  497. }
  498. // -> Set Ancestors / Tree Roots
  499. if (item.folderPath) {
  500. if (!state.treeNodes[parentId].children.includes(item.id)) {
  501. state.treeNodes[parentId].children.push(item.id)
  502. }
  503. } else {
  504. newTreeRoots.push(item.id)
  505. }
  506. // -> File List
  507. if (parentId === state.currentFolderId) {
  508. state.fileList.push({
  509. id: item.id,
  510. type: 'folder',
  511. title: item.title,
  512. fileName: item.fileName,
  513. children: 0
  514. })
  515. }
  516. break
  517. }
  518. case 'TreeItemAsset': {
  519. if (parentId === state.currentFolderId) {
  520. state.fileList.push({
  521. id: item.id,
  522. type: 'asset',
  523. title: item.title,
  524. fileType: 'pdf',
  525. fileSize: 19000,
  526. folderPath: item.folderPath,
  527. fileName: item.fileName
  528. })
  529. }
  530. break
  531. }
  532. case 'TreeItemPage': {
  533. if (parentId === state.currentFolderId) {
  534. state.fileList.push({
  535. id: item.id,
  536. type: 'page',
  537. title: item.title,
  538. pageType: 'markdown',
  539. updatedAt: '2022-11-24T18:27:00Z',
  540. folderPath: item.folderPath,
  541. fileName: item.fileName
  542. })
  543. }
  544. break
  545. }
  546. }
  547. }
  548. if (newTreeRoots.length > 0) {
  549. state.treeRoots = newTreeRoots
  550. }
  551. }
  552. } catch (err) {
  553. $q.notify({
  554. type: 'negative',
  555. message: 'Failed to load folder tree.',
  556. caption: err.message
  557. })
  558. }
  559. if (parentId === state.currentFolderId) {
  560. nextTick(() => {
  561. state.fileListLoading = false
  562. })
  563. }
  564. if (parentId) {
  565. treeComp.value.setLoaded(parentId)
  566. }
  567. }
  568. function treeContextAction (nodeId, action) {
  569. switch (action) {
  570. case 'newFolder': {
  571. newFolder(nodeId)
  572. break
  573. }
  574. case 'del': {
  575. delFolder(nodeId)
  576. break
  577. }
  578. }
  579. }
  580. // --------------------------------------
  581. // FOLDER METHODS
  582. // --------------------------------------
  583. function newFolder (parentId) {
  584. $q.dialog({
  585. component: FolderCreateDialog,
  586. componentProps: {
  587. parentId
  588. }
  589. }).onOk(() => {
  590. loadTree(parentId)
  591. })
  592. }
  593. function delFolder (folderId, mustReload = false) {
  594. $q.dialog({
  595. component: FolderDeleteDialog,
  596. componentProps: {
  597. folderId,
  598. folderName: state.treeNodes[folderId].title
  599. }
  600. }).onOk(() => {
  601. for (const nodeId in state.treeNodes) {
  602. if (state.treeNodes[nodeId].children.includes(folderId)) {
  603. state.treeNodes[nodeId].children = state.treeNodes[nodeId].children.filter(c => c !== folderId)
  604. }
  605. }
  606. delete state.treeNodes[folderId]
  607. if (state.treeRoots.includes(folderId)) {
  608. state.treeRoots = state.treeRoots.filter(n => n !== folderId)
  609. }
  610. if (mustReload) {
  611. loadTree(state.currentFolderId, null)
  612. }
  613. })
  614. }
  615. function reloadFolder (folderId) {
  616. loadTree(folderId, null)
  617. treeComp.value.resetLoaded()
  618. }
  619. // --------------------------------------
  620. // UPLOAD METHODS
  621. // --------------------------------------
  622. function uploadFile () {
  623. fileIpt.value.click()
  624. }
  625. async function uploadNewFiles () {
  626. if (!fileIpt.value.files?.length) {
  627. return
  628. }
  629. console.info(fileIpt.value.files)
  630. state.isUploading = true
  631. state.uploadPercentage = 0
  632. state.loading++
  633. nextTick(() => {
  634. setTimeout(async () => {
  635. try {
  636. const totalFiles = fileIpt.value.files.length
  637. let idx = 0
  638. for (const fileToUpload of fileIpt.value.files) {
  639. idx++
  640. state.uploadPercentage = totalFiles > 1 ? Math.round(idx / totalFiles * 100) : 90
  641. const resp = await APOLLO_CLIENT.mutate({
  642. mutation: gql`
  643. mutation uploadAssets (
  644. $siteId: UUID!
  645. $files: [Upload!]!
  646. ) {
  647. uploadAssets (
  648. siteId: $siteId
  649. files: $files
  650. ) {
  651. operation {
  652. succeeded
  653. message
  654. }
  655. }
  656. }
  657. `,
  658. variables: {
  659. siteId: siteStore.id,
  660. files: [fileToUpload]
  661. }
  662. })
  663. if (!resp?.data?.uploadAssets?.operation?.succeeded) {
  664. throw new Error(resp?.data?.uploadAssets?.operation?.message || 'An unexpected error occured.')
  665. }
  666. }
  667. state.uploadPercentage = 100
  668. $q.notify({
  669. type: 'positive',
  670. message: t('fileman.uploadSuccess')
  671. })
  672. } catch (err) {
  673. $q.notify({
  674. type: 'negative',
  675. message: 'Failed to upload file.',
  676. caption: err.message
  677. })
  678. }
  679. state.loading--
  680. fileIpt.value.value = null
  681. setTimeout(() => {
  682. state.isUploading = false
  683. state.uploadPercentage = 0
  684. }, 1500)
  685. }, 400)
  686. })
  687. }
  688. function uploadCancel () {
  689. state.isUploading = false
  690. state.uploadPercentage = 0
  691. }
  692. // --------------------------------------
  693. // ITEM LIST ACTIONS
  694. // --------------------------------------
  695. function selectItem (item) {
  696. if (item.type === 'folder') {
  697. state.currentFolderId = item.id
  698. treeComp.value.setOpened(item.id)
  699. } else {
  700. state.currentFileId = item.id
  701. }
  702. }
  703. function openItem (item) {
  704. switch (item.type) {
  705. case 'folder': {
  706. return
  707. }
  708. case 'page': {
  709. const pagePath = item.folderPath ? `${item.folderPath}/${item.fileName}` : item.fileName
  710. router.push(`/${pagePath}`)
  711. close()
  712. break
  713. }
  714. case 'asset': {
  715. // TODO: Open asset
  716. close()
  717. break
  718. }
  719. }
  720. }
  721. async function copyItemURL (item) {
  722. try {
  723. switch (item.type) {
  724. case 'page': {
  725. const pagePath = item.folderPath ? `${item.folderPath}/${item.fileName}` : item.fileName
  726. await navigator.clipboard.writeText(`${window.location.origin}/${pagePath}`)
  727. break
  728. }
  729. case 'asset': {
  730. // TODO: Copy asset URL to clibpard
  731. break
  732. }
  733. default: {
  734. throw new Error('Invalid Item Type')
  735. }
  736. }
  737. $q.notify({
  738. type: 'positive',
  739. message: t('fileman.copyURLSuccess')
  740. })
  741. } catch (err) {
  742. $q.notify({
  743. type: 'negative',
  744. message: 'Failed to copy URL to clipboard.',
  745. caption: err.message
  746. })
  747. }
  748. }
  749. function delItem (item) {
  750. switch (item.type) {
  751. case 'folder': {
  752. delFolder(item.id, true)
  753. break
  754. }
  755. }
  756. }
  757. // MOUNTED
  758. onMounted(() => {
  759. loadTree()
  760. })
  761. </script>
  762. <style lang="scss">
  763. .fileman {
  764. &-left {
  765. @at-root .body--light & {
  766. background-color: $blue-grey-1;
  767. }
  768. @at-root .body--dark & {
  769. background-color: $dark-4;
  770. }
  771. }
  772. &-center {
  773. @at-root .body--light & {
  774. background-color: #FFF;
  775. }
  776. @at-root .body--dark & {
  777. background-color: $dark-6;
  778. }
  779. }
  780. &-right {
  781. @at-root .body--light & {
  782. background-color: $grey-1;
  783. }
  784. @at-root .body--dark & {
  785. background-color: $dark-5;
  786. }
  787. }
  788. &-toolbar {
  789. @at-root .body--light & {
  790. background-color: $grey-1;
  791. }
  792. @at-root .body--dark & {
  793. background-color: $dark-5;
  794. }
  795. }
  796. &-path {
  797. @at-root .body--light & {
  798. background-color: $blue-grey-1 !important;
  799. }
  800. @at-root .body--dark & {
  801. background-color: $dark-4 !important;
  802. }
  803. }
  804. &-emptylist {
  805. padding: 16px;
  806. font-style: italic;
  807. display: flex;
  808. align-items: center;
  809. @at-root .body--light & {
  810. color: $grey-6;
  811. }
  812. @at-root .body--dark & {
  813. color: $dark-4;
  814. }
  815. }
  816. &-filelist {
  817. padding: 8px 12px;
  818. > .q-item {
  819. padding: 8px 6px;
  820. border-radius: 8px;
  821. &.active {
  822. background-color: var(--q-primary);
  823. color: #FFF;
  824. .fileman-filelist-label .q-item__label--caption {
  825. color: rgba(255,255,255,.7);
  826. }
  827. .fileman-filelist-side .text-caption {
  828. color: rgba(255,255,255,.7);
  829. }
  830. }
  831. }
  832. }
  833. &-details-row {
  834. display: flex;
  835. flex-direction: column;
  836. padding: 5px 0;
  837. label {
  838. font-size: .7rem;
  839. font-weight: 500;
  840. @at-root .body--light & {
  841. color: $grey-6;
  842. }
  843. @at-root .body--dark & {
  844. color: $blue-grey-4;
  845. }
  846. }
  847. span {
  848. font-size: .85rem;
  849. @at-root .body--light & {
  850. color: $grey-8;
  851. }
  852. @at-root .body--dark & {
  853. color: $blue-grey-2;
  854. }
  855. }
  856. & + .fileman-details-row {
  857. margin-top: 5px;
  858. }
  859. }
  860. &-progressbar {
  861. width: 100%;
  862. flex: 1;
  863. height: 12px;
  864. border-radius: 3px;
  865. @at-root .body--light & {
  866. background-color: $blue-grey-2;
  867. }
  868. @at-root .body--dark & {
  869. background-color: $dark-4 !important;
  870. }
  871. > div {
  872. height: 12px;
  873. background-color: $positive;
  874. border-radius: 3px 0 0 3px;
  875. background-image: linear-gradient(
  876. -45deg,
  877. rgba(255, 255, 255, 0.3) 25%,
  878. transparent 25%,
  879. transparent 50%,
  880. rgba(255, 255, 255, 0.3) 50%,
  881. rgba(255, 255, 255, 0.3) 75%,
  882. transparent 75%,
  883. transparent
  884. );
  885. background-size: 50px 50px;
  886. background-position: 0 0;
  887. animation: fileman-progress 2s linear infinite;
  888. box-shadow: 0 0 5px 0 $positive;
  889. font-size: 9px;
  890. letter-spacing: 2px;
  891. font-weight: 700;
  892. color: #FFF;
  893. display: flex;
  894. justify-content: center;
  895. align-items: center;
  896. overflow: hidden;
  897. transition: all 1s ease;
  898. }
  899. }
  900. }
  901. @keyframes fileman-progress {
  902. 0% {
  903. background-position: 0 0;
  904. }
  905. 100% {
  906. background-position: -50px -50px;
  907. }
  908. }
  909. </style>