From d4f6d174c81d30f6bae46aabf92d8a0b3a624d0d Mon Sep 17 00:00:00 2001
From: Fabio Manganiello <fabio@manganiello.tech>
Date: Tue, 12 Dec 2023 21:13:55 +0100
Subject: [PATCH] Added `FileSelector` UI component.

---
 .../webapp/src/components/File/Browser.vue    |  43 ++++++-
 .../src/components/elements/FileSelector.vue  | 116 ++++++++++++++++++
 2 files changed, 154 insertions(+), 5 deletions(-)
 create mode 100644 platypush/backend/http/webapp/src/components/elements/FileSelector.vue

diff --git a/platypush/backend/http/webapp/src/components/File/Browser.vue b/platypush/backend/http/webapp/src/components/File/Browser.vue
index 538065e2..dc41e1db 100644
--- a/platypush/backend/http/webapp/src/components/File/Browser.vue
+++ b/platypush/backend/http/webapp/src/components/File/Browser.vue
@@ -27,7 +27,7 @@
         </div>
       </div>
 
-      <div class="row item" v-for="(file, i) in filteredFiles" :key="i" @click="path = file.path">
+      <div class="row item" v-for="(file, i) in filteredFiles" :key="i" @click="onItemSelect(file)">
         <div class="col-10">
           <i class="icon fa" :class="{'fa-file': file.type !== 'directory', 'fa-folder': file.type === 'directory'}" />
           <span class="name">
@@ -35,11 +35,11 @@
           </span>
         </div>
 
-        <div class="col-2 actions">
+        <div class="col-2 actions" v-if="fileActions.length">
           <Dropdown>
             <DropdownItem icon-class="fa fa-play" text="Play"
                           @click="$emit('play', {type: 'file', url: `file://${file.path}`})"
-                          v-if="isMedia && mediaExtensions.has(file.name.split('.').pop()?.toLowerCase())" />
+                          v-if="hasPlay && file.type !== 'directory'" />
           </Dropdown>
         </div>
       </div>
@@ -58,7 +58,7 @@ export default {
   name: "Browser",
   components: {DropdownItem, Dropdown, Loading},
   mixins: [Utils, MediaUtils],
-  emits: ['back', 'path-change', 'play'],
+  emits: ['back', 'path-change', 'play', 'input'],
 
   props: {
     hasBack: {
@@ -96,6 +96,23 @@ export default {
       return this.files.filter((file) => (file?.name || '').toLowerCase().indexOf(this.filter.toLowerCase()) >= 0)
     },
 
+    hasPlay() {
+      return this.isMedia && this.files.some((file) => this.mediaExtensions.has(file.name.split('.').pop()?.toLowerCase()))
+    },
+
+    fileActions() {
+      if (!this.hasPlay)
+        return []
+
+      return [
+        {
+          iconClass: 'fa fa-play',
+          text: 'Play',
+          onClick: (file) => this.$emit('play', {type: 'file', url: `file://${file.path}`}),
+        },
+      ]
+    },
+
     pathTokens() {
       if (!this.path?.length)
         return ['/']
@@ -128,10 +145,26 @@ export default {
       else
         this.path = [...this.pathTokens].slice(0, -1).join('/').slice(1)
     },
+
+    onItemSelect(file) {
+      if (file.type === 'directory')
+        this.path = file.path
+      else
+        this.$emit('input', file.path)
+    },
+  },
+
+  watch: {
+    initialPath() {
+      this.path = this.initialPath
+    },
+
+    path() {
+      this.refresh()
+    },
   },
 
   mounted() {
-    this.$watch(() => this.path, () => this.refresh())
     this.refresh()
   },
 }
diff --git a/platypush/backend/http/webapp/src/components/elements/FileSelector.vue b/platypush/backend/http/webapp/src/components/elements/FileSelector.vue
new file mode 100644
index 00000000..175093ad
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/elements/FileSelector.vue
@@ -0,0 +1,116 @@
+<template>
+  <div class="file-selector-container">
+    <div class="input">
+      <input type="text"
+             :value="value"
+             :readonly="strict"
+             @input="$emit('input', $event.target.value)" />
+
+      <button type="button"
+              title="Select a file"
+              @click="$refs.fileSelectorModal.show()">
+        <i class="fa fa-folder-open" />
+      </button>
+    </div>
+
+    <Modal title="Select a file" ref="fileSelectorModal">
+      <Browser :initialPath="path"
+               @input="onValueChange($event)"
+               @path-change="path = $event" />
+    </Modal>
+  </div>
+</template>
+
+<script>
+import Modal from "@/components/Modal";
+import Browser from "@/components/File/Browser";
+
+export default {
+  emits: ['input'],
+  components: {
+    Browser,
+    Modal,
+  },
+
+  props: {
+    value: {
+      type: String,
+    },
+
+    strict: {
+      type: Boolean,
+      default: false,
+    },
+  },
+
+  data() {
+    return {
+      path: '/',
+    }
+  },
+
+  methods: {
+    onValueChange(value) {
+      this.$emit('input', value)
+    },
+
+    onFileSelect(value) {
+      if (value != null && (value.startsWith('/') || value.startsWith('file://')))
+        this.path = value.split('/').slice(0, -1).join('/')
+      else
+        this.path = '/'
+
+      this.$refs.fileSelectorModal.hide()
+    },
+  },
+
+  watch: {
+    value(value) {
+      this.onFileSelect(value)
+    },
+  },
+
+  mounted() {
+    this.onFileSelect(this.value)
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+.file-selector-container {
+  display: flex;
+  flex-direction: column;
+
+  .input {
+    display: flex;
+    flex-direction: row;
+    align-items: stretch;
+
+    input {
+      flex-grow: 1;
+      border-radius: 1em 0 0 1em;
+      border-right: none;
+    }
+
+    button {
+      border-radius: 0 1em 1em 0;
+      border-left: none;
+    }
+  }
+
+  :deep(.modal) {
+    .body {
+      width: 80vw;
+      height: 80vh;
+      max-width: 800px;
+      padding: 0;
+
+      .items {
+        .item {
+          padding: 1em 0.5em;
+        }
+      }
+    }
+  }
+}
+</style>