ソースを参照

feat: file manager (wip)

Nicolas Giard 2 年 前
コミット
24b1984919

+ 1 - 1
server/graph/resolvers/page.js

@@ -618,7 +618,7 @@ module.exports = {
       return obj.icon || 'las la-file-alt'
     },
     password (obj) {
-      return obj ? '********' : ''
+      return obj.password ? '********' : ''
     },
     async tags (obj) {
       return WIKI.db.pages.relatedQuery('tags').for(obj.id)

+ 1 - 0
ux/public/_assets/icons/color-document.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="96px" height="96px"><path fill="#90CAF9" d="M40 45L8 45 8 3 30 3 40 13z"/><path fill="#E1F5FE" d="M38.5 14L29 14 29 4.5z"/><path fill="#1976D2" d="M16 21H33V23H16zM16 25H29V27H16zM16 29H33V31H16zM16 33H29V35H16z"/></svg>

+ 1 - 0
ux/public/_assets/icons/color-pdf.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="96px" height="96px"><path fill="#FF5722" d="M40 45L8 45 8 3 30 3 40 13z"/><path fill="#FBE9E7" d="M38.5 14L29 14 29 4.5z"/><path fill="#FFEBEE" d="M15.81 29.5V33H13.8v-9.953h3.391c.984 0 1.77.306 2.355.916s.878 1.403.878 2.379-.29 1.745-.868 2.311S18.175 29.5 17.149 29.5H15.81zM15.81 27.825h1.381c.383 0 .679-.125.889-.376s.314-.615.314-1.094c0-.497-.107-.892-.321-1.187-.214-.293-.501-.442-.861-.447H15.81V27.825zM21.764 33v-9.953h2.632c1.162 0 2.089.369 2.778 1.107.691.738 1.043 1.75 1.057 3.035v1.613c0 1.308-.346 2.335-1.035 3.079C26.504 32.628 25.553 33 24.341 33H21.764zM23.773 24.722v6.61h.602c.67 0 1.142-.177 1.415-.53.273-.353.417-.962.431-1.828v-1.729c0-.93-.13-1.578-.39-1.944-.26-.367-.702-.56-1.326-.578H23.773zM34.807 28.939h-3.124V33h-2.01v-9.953h5.51v1.675h-3.5v2.55h3.124V28.939z"/></svg>

+ 1 - 0
ux/public/_assets/icons/fluent-folder.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="WQEfvoQAcpQgQgyjQQ4Hqa" x1="24" x2="24" y1="6.708" y2="14.977" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#eba600"/><stop offset="1" stop-color="#c28200"/></linearGradient><path fill="url(#WQEfvoQAcpQgQgyjQQ4Hqa)" d="M24.414,10.414l-2.536-2.536C21.316,7.316,20.553,7,19.757,7L5,7C3.895,7,3,7.895,3,9l0,30	c0,1.105,0.895,2,2,2l38,0c1.105,0,2-0.895,2-2V13c0-1.105-0.895-2-2-2l-17.172,0C25.298,11,24.789,10.789,24.414,10.414z"/><linearGradient id="WQEfvoQAcpQgQgyjQQ4Hqb" x1="24" x2="24" y1="10.854" y2="40.983" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd869"/><stop offset="1" stop-color="#fec52b"/></linearGradient><path fill="url(#WQEfvoQAcpQgQgyjQQ4Hqb)" d="M21.586,14.414l3.268-3.268C24.947,11.053,25.074,11,25.207,11H43c1.105,0,2,0.895,2,2v26	c0,1.105-0.895,2-2,2H5c-1.105,0-2-0.895-2-2V15.5C3,15.224,3.224,15,3.5,15h16.672C20.702,15,21.211,14.789,21.586,14.414z"/></svg>

ファイルの差分が大きいため隠しています
+ 0 - 0
ux/public/_assets/icons/fluent-spring.svg


+ 169 - 0
ux/src/components/FileManager.vue

@@ -0,0 +1,169 @@
+<template lang="pug">
+q-layout(view='hHh lpR fFf', container)
+  q-header.card-header
+    q-toolbar(dark)
+      q-icon(name='img:/_assets/icons/fluent-folder.svg', left, size='md')
+      span {{t(`fileman.title`)}}
+    q-toolbar(dark)
+      q-input(
+        dark
+        v-model='state.search'
+        standout='bg-white text-dark'
+        dense
+        ref='searchField'
+        style='width: 100%;'
+        label='Search folder...'
+        )
+        template(v-slot:prepend)
+          q-icon(name='las la-search')
+        template(v-slot:append)
+          q-icon.cursor-pointer(
+            name='las la-times'
+            @click='state.search=``'
+            v-if='state.search.length > 0'
+            :color='$q.dark.isActive ? `blue` : `grey-4`'
+            )
+    q-toolbar(dark)
+      q-space
+      q-btn.q-mr-sm(
+        flat
+        dense
+        color='blue-4'
+        :aria-label='t(`common.actions.upload`)'
+        icon='las la-plus-circle'
+        @click=''
+        )
+        q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.upload`)}}
+      q-btn(
+        flat
+        dense
+        color='positive'
+        :aria-label='t(`common.actions.upload`)'
+        icon='las la-cloud-upload-alt'
+        @click=''
+        )
+        q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.upload`)}}
+      q-separator.q-mx-sm(vertical, dark, inset)
+      q-btn.q-mr-sm(
+        flat
+        dense
+        color='blue-grey-4'
+        :aria-label='t(`common.actions.upload`)'
+        icon='las la-check-square'
+        @click=''
+        )
+        q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.upload`)}}
+      q-btn.q-mr-sm(
+        flat
+        dense
+        color='blue-grey-4'
+        :aria-label='t(`common.actions.upload`)'
+        icon='las la-list'
+        @click=''
+        )
+        q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.upload`)}}
+      q-btn(
+        flat
+        dense
+        color='blue-grey-4'
+        :aria-label='t(`common.actions.refresh`)'
+        icon='las la-redo-alt'
+        @click=''
+        :loading='state.loading > 0'
+        )
+        q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.refresh`)}}
+      q-separator.q-mx-sm(vertical, dark, inset)
+      q-btn(
+        flat
+        dense
+        color='white'
+        :aria-label='t(`common.actions.close`)'
+        icon='las la-times'
+        @click='close'
+        )
+        q-tooltip(anchor='bottom middle', self='top middle') {{t(`common.actions.close`)}}
+  q-drawer.bg-blue-grey-1(:model-value='true', :width='350')
+    q-list(padding, v-if='state.loading < 1')
+  q-drawer.bg-grey-1(:model-value='true', :width='350', side='right')
+    q-list(padding, v-if='state.loading < 1')
+  q-page-container
+    q-page.bg-white
+      q-bar.bg-grey-1
+        small.text-caption.text-grey-7 / foo / bar
+      q-list.fileman-filelist
+        q-item(clickable)
+          q-item-section(avatar)
+            q-icon(name='img:/_assets/icons/fluent-folder.svg', size='xl')
+          q-item-section
+            q-item-label Beep Boop
+            q-item-label(caption) 19 Items
+          q-item-section(side)
+            .text-caption 1
+        q-item(clickable)
+          q-item-section(avatar)
+            q-icon(name='img:/_assets/icons/fluent-folder.svg', size='xl')
+          q-item-section
+            q-item-label Beep Boop
+            q-item-label(caption) 19 Items
+          q-item-section(side)
+            .text-caption 1
+        q-item(clickable)
+          q-item-section(avatar)
+            q-icon(name='img:/_assets/icons/color-document.svg', size='xl')
+          q-item-section
+            q-item-label Beep Boop
+            q-item-label(caption) Markdown
+          q-item-section(side)
+            .text-caption 1
+        q-item(clickable)
+          q-item-section(avatar)
+            q-icon(name='img:/_assets/icons/color-pdf.svg', size='xl')
+          q-item-section
+            q-item-label Beep Boop
+            q-item-label(caption) 4 pages
+          q-item-section(side)
+            .text-caption 2022/01/01
+
+</template>
+
+<script setup>
+import { useI18n } from 'vue-i18n'
+import { reactive } from 'vue'
+
+import { useSiteStore } from 'src/stores/site'
+
+// STORES
+
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  loading: 0,
+  search: ''
+})
+
+// METHODS
+
+function close () {
+  siteStore.overlay = null
+}
+
+</script>
+
+<style lang="scss">
+.fileman {
+  &-filelist {
+    padding: 8px 12px;
+
+    > .q-item {
+      padding: 8px 6px;
+      border-radius: 8px;
+    }
+  }
+}
+</style>

+ 19 - 3
ux/src/components/HeaderNav.vue

@@ -80,12 +80,22 @@ q-header.bg-header.text-white.site-header(
         flat
         round
         dense
-        icon='las la-tools'
+        icon='las la-folder-open'
         color='positive'
+        aria-label='File Manager'
+        @click='toggleFileManager'
+        )
+        q-tooltip File Manager
+      q-btn.q-ml-md(
+        flat
+        round
+        dense
+        icon='las la-tools'
+        color='pink'
         to='/_admin'
-        aria-label='Administration'
+        :aria-label='t(`common.header.admin`)'
         )
-        q-tooltip Administration
+        q-tooltip {{ t('common.header.admin') }}
       account-menu
 </template>
 
@@ -116,6 +126,12 @@ const { t } = useI18n()
 const state = reactive({
   search: ''
 })
+
+// METHODS
+
+function toggleFileManager () {
+  siteStore.overlay = 'FileManager'
+}
 </script>
 
 <style lang="scss">

+ 7 - 1
ux/src/components/PageNewMenu.vue

@@ -24,7 +24,7 @@ q-menu(
       blueprint-icon(icon='advance')
       q-item-section.q-pr-sm New Redirection
     q-separator.q-my-sm(inset)
-    q-item(clickable, to='/_assets')
+    q-item(clickable, @click='openFileManager')
       blueprint-icon(icon='add-image')
       q-item-section.q-pr-sm Upload Media Asset
 </template>
@@ -34,6 +34,7 @@ import { useI18n } from 'vue-i18n'
 import { useQuasar } from 'quasar'
 
 import { usePageStore } from 'src/stores/page'
+import { useSiteStore } from 'src/stores/site'
 
 // QUASAR
 
@@ -42,6 +43,7 @@ const $q = useQuasar()
 // STORES
 
 const pageStore = usePageStore()
+const siteStore = useSiteStore()
 
 // I18N
 
@@ -53,4 +55,8 @@ function create (editor) {
   window.location.assign('/_edit/new')
   // pageStore.pageCreate({ editor })
 }
+
+function openFileManager () {
+  siteStore.overlay = 'FileManager'
+}
 </script>

+ 1 - 1
ux/src/components/PageRelationDialog.vue

@@ -205,7 +205,7 @@ function create () {
 function persist () {
   const rels = cloneDeep(pageStore.relations)
   for (const rel of rels) {
-    if (rel.id === state.editId) {
+    if (rel.id === props.editId) {
       rel.position = state.pos
       rel.label = state.label
       rel.caption = state.caption

+ 89 - 62
ux/src/components/PageScriptsDialog.vue

@@ -1,20 +1,20 @@
 <template lang="pug">
 q-card.page-scripts-dialog(style='width: 860px; max-width: 90vw;')
   q-toolbar.bg-primary.text-white
-    .text-subtitle2 {{$t('editor.pageScripts.title')}} - {{$t('editor.props.' + mode)}}
+    .text-subtitle2 {{t('editor.pageScripts.title')}} - {{t('editor.props.' + props.mode)}}
     q-space
     q-chip(
       square
       style='background-color: rgba(0,0,0,.1)'
       text-color='white'
       )
-      .text-caption {{this.languageLabel}}
+      .text-caption {{languageLabel}}
   div(style='min-height: 450px;')
-    q-no-ssr(:placeholder='$t(`common.loading`)')
+    q-no-ssr(:placeholder='t(`common.loading`)')
       util-code-editor(
-        v-if='showEditor'
+        v-if='state.showEditor'
         ref='editor'
-        v-model='content'
+        v-model='state.content'
         :language='language'
         :min-height='450'
       )
@@ -22,7 +22,7 @@ q-card.page-scripts-dialog(style='width: 860px; max-width: 90vw;')
     q-space
     q-btn.acrylic-btn(
       icon='las la-times'
-      :label='$t(`common.actions.discard`)'
+      :label='t(`common.actions.discard`)'
       color='grey-7'
       padding='xs md'
       v-close-popup
@@ -30,7 +30,7 @@ q-card.page-scripts-dialog(style='width: 860px; max-width: 90vw;')
     )
     q-btn(
       icon='las la-check'
-      :label='$t(`common.actions.save`)'
+      :label='t(`common.actions.save`)'
       unelevated
       color='primary'
       padding='xs md'
@@ -39,65 +39,92 @@ q-card.page-scripts-dialog(style='width: 860px; max-width: 90vw;')
     )
 </template>
 
-<script>
+<script setup>
+import { computed, nextTick, onMounted, reactive } from 'vue'
+import { useQuasar } from 'quasar'
+import { useI18n } from 'vue-i18n'
+
 import UtilCodeEditor from './UtilCodeEditor.vue'
 
-export default {
-  components: {
-    UtilCodeEditor
-  },
-  props: {
-    mode: {
-      type: String,
-      default: 'css'
-    }
-  },
-  data () {
-    return {
-      content: '',
-      showEditor: false
-    }
-  },
-  computed: {
-    language () {
-      switch (this.mode) {
-        case 'jsLoad':
-        case 'jsUnload':
-          return 'javascript'
-        case 'styles':
-          return 'css'
-        default:
-          return 'plaintext'
-      }
-    },
-    languageLabel () {
-      switch (this.language) {
-        case 'javascript':
-          return 'Javascript'
-        case 'css':
-          return 'CSS'
-        default:
-          return 'Plain Text'
-      }
-    },
-    contentStoreKey () {
-      return 'script' + this.mode.charAt(0).toUpperCase() + this.mode.slice(1)
-    }
-  },
-  mounted () {
-    this.content = this.$store.get(`page/${this.contentStoreKey}`)
-    this.$nextTick(() => {
-      setTimeout(() => {
-        this.showEditor = true
-      }, 250)
-    })
-  },
-  methods: {
-    persist () {
-      this.$store.set(`page/${this.contentStoreKey}`, this.content)
-    }
+import { usePageStore } from 'src/stores/page'
+import { useSiteStore } from 'src/stores/site'
+
+// PROPS
+
+const props = defineProps({
+  mode: {
+    type: String,
+    default: 'css'
   }
+})
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const pageStore = usePageStore()
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  content: '',
+  showEditor: false
+})
+
+// COMPUTED
+
+const language = computed(() => {
+  switch (props.mode) {
+    case 'jsLoad':
+    case 'jsUnload':
+      return 'javascript'
+    case 'styles':
+      return 'css'
+    default:
+      return 'plaintext'
+  }
+})
+
+const languageLabel = computed(() => {
+  switch (language.value) {
+    case 'javascript':
+      return 'Javascript'
+    case 'css':
+      return 'CSS'
+    default:
+      return 'Plain Text'
+  }
+})
+
+const contentStoreKey = computed(() => {
+  return 'script' + props.mode.charAt(0).toUpperCase() + props.mode.slice(1)
+})
+
+// METHODS
+
+function persist () {
+  pageStore.$patch({
+    [contentStoreKey]: state.content
+  })
 }
+
+// MOUNTED
+
+onMounted(() => {
+  state.content = pageStore[contentStoreKey.value]
+  nextTick(() => {
+    setTimeout(() => {
+      state.showEditor = true
+    }, 250)
+  })
+})
 </script>
 
 <style lang="scss">

+ 26 - 23
ux/src/i18n/locales/en.json

@@ -190,6 +190,8 @@
   "admin.general.defaultTimeFormatHint": "The default time format for new users.",
   "admin.general.defaultTimezone": "Default Timezone",
   "admin.general.defaultTimezoneHint": "The default timezone for new users.",
+  "admin.general.defaultTocDepth": "Default ToC Depth",
+  "admin.general.defaultTocDepthHint": "The default minimum and maximum header levels to show in the table of contents.",
   "admin.general.displaySiteTitle": "Display Site Title",
   "admin.general.displaySiteTitleHint": "Should the site title be displayed next to the logo? If your logo isn't square and contain your brand name, turn this option off.",
   "admin.general.favicon": "Favicon",
@@ -289,6 +291,8 @@
   "admin.groups.users": "Users",
   "admin.groups.usersCount": "0 user | 1 user | {count} users",
   "admin.groups.usersNone": "This group doesn't have any user yet.",
+  "admin.icons.subtitle": "Configure the icon packs available for use",
+  "admin.icons.title": "Icons",
   "admin.instances.activeConnections": "Active Connections",
   "admin.instances.activeListeners": "Active Listeners",
   "admin.instances.firstSeen": "First Seen",
@@ -1177,18 +1181,18 @@
   "common.error.title": "Error",
   "common.error.unauthorized.hint": "You don't have the required permissions to access this page.",
   "common.error.unauthorized.title": "Unauthorized",
+  "common.error.unexpected": "An unexpected error occurred.",
   "common.error.unknownsite.hint": "There's no wiki site at this host.",
   "common.error.unknownsite.title": "Unknown Site",
-  "common.error.unexpected": "An unexpected error occurred.",
   "common.field.createdOn": "Created On",
   "common.field.id": "ID",
   "common.field.lastUpdated": "Last Updated",
   "common.field.name": "Name",
   "common.field.task": "Task",
-  "common.footerGeneric": "Powered by {link}, an open source project.",
-  "common.footerPoweredBy": "Powered by {link}",
   "common.footerCopyright": "© {year} {company}. All rights reserved.",
+  "common.footerGeneric": "Powered by {link}, an open source project.",
   "common.footerLicense": "Content is available under the {license}, by {company}.",
+  "common.footerPoweredBy": "Powered by {link}",
   "common.header.account": "Account",
   "common.header.admin": "Administration",
   "common.header.assets": "Assets",
@@ -1423,6 +1427,7 @@
   "editor.props.dateRangeHint": "Select the start and end date for this page publication. The page will only be accessible to users with read access within the selected date range.",
   "editor.props.draft": "Draft",
   "editor.props.draftHint": "Visible to users with write access only.",
+  "editor.props.icon": "Icon",
   "editor.props.info": "Info",
   "editor.props.jsLoad": "Javascript - On Load",
   "editor.props.jsLoadHint": "Execute javascript once the page is loaded",
@@ -1465,6 +1470,7 @@
   "editor.unsaved.body": "You have unsaved changes. Are you sure you want to leave the editor and discard any modifications you made since the last save?",
   "editor.unsaved.title": "Discard Unsaved Changes?",
   "editor.unsavedWarning": "You have unsaved edits. Are you sure you want to leave the editor?",
+  "fileman.title": "File Manager",
   "history.restore.confirmButton": "Restore",
   "history.restore.confirmText": "Are you sure you want to restore this page content as it was on {date}? This version will be copied on top of the current history. As such, newer versions will still be preserved.",
   "history.restore.confirmTitle": "Restore page version?",
@@ -1476,6 +1482,18 @@
   "profile.appearanceHint": "Use the light or dark theme.",
   "profile.appearanceLight": "Light",
   "profile.auth": "Authentication",
+  "profile.authChangePassword": "Change Password",
+  "profile.authInfo": "Your account is associated with the following authentication methods:",
+  "profile.authLoadingFailed": "Failed to load authentication methods.",
+  "profile.authModifyTfa": "Modify 2FA",
+  "profile.authSetTfa": "Set 2FA",
+  "profile.avatar": "Avatar",
+  "profile.avatarClearFailed": "Failed to clear profile picture.",
+  "profile.avatarClearSuccess": "Profile picture cleared successfully.",
+  "profile.avatarUploadFailed": "Failed to upload user profile picture.",
+  "profile.avatarUploadHint": "For best results, use a 180x180 image of type JPG or PNG.",
+  "profile.avatarUploadSuccess": "Profile picture uploaded successfully.",
+  "profile.avatarUploadTitle": "Upload your user profile picture.",
   "profile.darkMode": "Dark Mode",
   "profile.darkModeHint": "Change the appareance of the site to a dark theme.",
   "profile.dateFormat": "Date Format",
@@ -1485,13 +1503,16 @@
   "profile.email": "Email Address",
   "profile.emailHint": "The email address used for login.",
   "profile.groups": "Groups",
+  "profile.groupsInfo": "You're currently part of the following groups:",
   "profile.groupsLoadingFailed": "Failed to load groups.",
+  "profile.groupsNone": "You're not part of any group.",
   "profile.jobTitle": "Job Title",
   "profile.jobTitleHint": "Your position in your organization; shown on your profile page.",
   "profile.localeDefault": "Locale Default",
   "profile.location": "Location",
   "profile.locationHint": "Your city and country; shown on your profile page.",
   "profile.myInfo": "My Info",
+  "profile.notifications": "Notifications",
   "profile.pages.emptyList": "No pages to display.",
   "profile.pages.headerCreatedAt": "Created",
   "profile.pages.headerPath": "Path",
@@ -1515,6 +1536,7 @@
   "profile.timezone": "Timezone",
   "profile.timezoneHint": "Set your timezone to display local time correctly.",
   "profile.title": "Profile",
+  "profile.uploadNewAvatar": "Upload New Image",
   "profile.viewPublicProfile": "View Public Profile",
   "tags.clearSelection": "Clear Selection",
   "tags.currentSelection": "Current Selection",
@@ -1536,24 +1558,5 @@
   "welcome.admin": "Administration Area",
   "welcome.createHome": "Create the homepage",
   "welcome.subtitle": "Let's get started...",
-  "welcome.title": "Welcome to Wiki.js!",
-  "profile.avatar": "Avatar",
-  "profile.uploadNewAvatar": "Upload New Image",
-  "profile.avatarUploadTitle": "Upload your user profile picture.",
-  "profile.avatarUploadHint": "For best results, use a 180x180 image of type JPG or PNG.",
-  "profile.groupsInfo": "You're currently part of the following groups:",
-  "profile.groupsNone": "You're not part of any group.",
-  "profile.authInfo": "Your account is associated with the following authentication methods:",
-  "profile.authSetTfa": "Set 2FA",
-  "profile.authModifyTfa": "Modify 2FA",
-  "profile.authChangePassword": "Change Password",
-  "profile.authLoadingFailed": "Failed to load authentication methods.",
-  "profile.notifications": "Notifications",
-  "profile.avatarUploadSuccess": "Profile picture uploaded successfully.",
-  "profile.avatarUploadFailed": "Failed to upload user profile picture.",
-  "profile.avatarClearSuccess": "Profile picture cleared successfully.",
-  "profile.avatarClearFailed": "Failed to clear profile picture.",
-  "admin.general.defaultTocDepth": "Default ToC Depth",
-  "admin.general.defaultTocDepthHint": "The default minimum and maximum header levels to show in the table of contents.",
-  "editor.props.icon": "Icon"
+  "welcome.title": "Welcome to Wiki.js!"
 }

+ 4 - 0
ux/src/layouts/AdminLayout.vue

@@ -142,6 +142,10 @@ q-layout.admin(view='hHh Lpr lff')
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-module.svg')
           q-item-section {{ t('admin.extensions.title') }}
+        q-item(to='/_admin/icons', v-ripple, active-class='bg-primary text-white')
+          q-item-section(avatar)
+            q-icon(name='img:/_assets/icons/fluent-spring.svg')
+          q-item-section {{ t('admin.icons.title') }}
         q-item(to='/_admin/instances', v-ripple, active-class='bg-primary text-white')
           q-item-section(avatar)
             q-icon(name='img:/_assets/icons/fluent-network.svg')

+ 42 - 0
ux/src/layouts/MainLayout.vue

@@ -76,6 +76,16 @@ q-layout(view='hHh Lpr lff')
         round
         size='md'
       )
+  q-dialog.main-overlay(
+    v-model='siteStore.overlayIsShown'
+    persistent
+    full-width
+    full-height
+    no-shake
+    transition-show='jump-up'
+    transition-hide='jump-down'
+    )
+    component(:is='overlays[siteStore.overlay]')
   footer-nav
 </template>
 
@@ -91,6 +101,14 @@ import { useSiteStore } from '../stores/site'
 
 import HeaderNav from '../components/HeaderNav.vue'
 import FooterNav from 'src/components/FooterNav.vue'
+import LoadingGeneric from 'src/components/LoadingGeneric.vue'
+
+const overlays = {
+  FileManager: defineAsyncComponent({
+    loader: () => import('../components/FileManager.vue'),
+    loadingComponent: LoadingGeneric
+  })
+}
 
 // QUASAR
 
@@ -150,6 +168,30 @@ body.body--dark {
   background-color: $dark-6;
 }
 
+.main-overlay {
+  > .q-dialog__backdrop {
+    background-color: rgba(0,0,0,.6);
+  }
+  > .q-dialog__inner {
+    padding: 24px 64px;
+
+    @media (max-width: $breakpoint-sm-max) {
+      padding: 0;
+    }
+
+    > .q-layout-container {
+      @at-root .body--light & {
+        background-image: linear-gradient(to bottom, $dark-5 10px, $grey-3 11px, $grey-4);
+      }
+      @at-root .body--dark & {
+        background-image: linear-gradient(to bottom, $dark-4 10px, $dark-4 11px, $dark-3);
+      }
+      border-radius: 6px;
+      box-shadow: 0 0 0 2px rgba(0,0,0,.5);
+    }
+  }
+}
+
 .q-footer {
   .q-bar {
     @at-root .body--light & {

+ 116 - 0
ux/src/pages/AdminIcons.vue

@@ -0,0 +1,116 @@
+<template lang='pug'>
+q-page.admin-icons
+  .row.q-pa-md.items-center
+    .col-auto
+      img.admin-icon.admin-icons-icon.animated.fadeInLeft(src='/_assets/icons/fluent-spring.svg')
+    .col.q-pl-md
+      .text-h5.text-primary.animated.fadeInLeft {{ t('admin.icons.title') }}
+      .text-subtitle1.text-grey.animated.fadeInLeft.wait-p2s {{ t('admin.icons.subtitle') }}
+    .col-auto
+      q-btn.acrylic-btn.q-mr-sm(
+        icon='las la-question-circle'
+        flat
+        color='grey'
+        :href='siteStore.docsBase + `/system/icons`'
+        target='_blank'
+        type='a'
+        )
+      q-btn.acrylic-btn(
+        icon='las la-redo-alt'
+        flat
+        color='secondary'
+        :loading='state.loading > 0'
+        @click='load'
+        )
+  q-separator(inset)
+  .row.q-pa-md.q-col-gutter-md
+    .col-12
+      q-card.shadow-1 Beep boop
+
+</template>
+
+<script setup>
+import gql from 'graphql-tag'
+import { cloneDeep } from 'lodash-es'
+import { useI18n } from 'vue-i18n'
+import { useMeta, useQuasar } from 'quasar'
+import { computed, onMounted, reactive, watch } from 'vue'
+
+import { useAdminStore } from 'src/stores/admin'
+import { useSiteStore } from 'src/stores/site'
+
+// QUASAR
+
+const $q = useQuasar()
+
+// STORES
+
+const adminStore = useAdminStore()
+const siteStore = useSiteStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// META
+
+useMeta({
+  title: t('admin.icons.title')
+})
+
+// DATA
+
+const state = reactive({
+  loading: false,
+  extensions: []
+})
+
+// METHODS
+
+async function load () {
+  state.loading++
+  $q.loading.show()
+  const resp = await APOLLO_CLIENT.query({
+    query: gql`
+      query fetchExtensions {
+        systemExtensions {
+          key
+          title
+          description
+          isInstalled
+          isInstallable
+          isCompatible
+        }
+      }
+    `,
+    fetchPolicy: 'network-only'
+  })
+  state.extensions = cloneDeep(resp?.data?.systemExtensions)
+  $q.loading.hide()
+  state.loading--
+}
+
+// MOUNTED
+
+onMounted(() => {
+  load()
+})
+
+</script>
+
+<style lang='scss'>
+.admin-icons {
+  &-icon {
+    animation: fadeInLeft .6s forwards, flower-rotate 30s linear infinite;
+  }
+}
+
+@keyframes flower-rotate {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+</style>

+ 1 - 0
ux/src/router/routes.js

@@ -48,6 +48,7 @@ const routes = [
       // -> System
       { path: 'api', component: () => import('pages/AdminApi.vue') },
       { path: 'extensions', component: () => import('pages/AdminExtensions.vue') },
+      { path: 'icons', component: () => import('pages/AdminIcons.vue') },
       { path: 'instances', component: () => import('pages/AdminInstances.vue') },
       { path: 'mail', component: () => import('pages/AdminMail.vue') },
       // { path: 'rendering', component: () => import('pages/AdminRendering.vue') },

+ 4 - 1
ux/src/stores/site.js

@@ -25,6 +25,7 @@ export const useSiteStore = defineStore('site', {
     pageDataTemplates: [],
     showSideNav: true,
     showSidebar: true,
+    overlay: 'FileManager',
     theme: {
       dark: false,
       injectCSS: '',
@@ -54,7 +55,9 @@ export const useSiteStore = defineStore('site', {
     },
     docsBase: 'https://next.js.wiki/docs'
   }),
-  getters: {},
+  getters: {
+    overlayIsShown: (state) => Boolean(state.overlay)
+  },
   actions: {
     async loadSite (hostname) {
       try {

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません