From 6dfe2324c1a8c73fdb200fb6a31c20e2082c1d6f Mon Sep 17 00:00:00 2001
From: Fabio Manganiello <fabio@manganiello.tech>
Date: Sun, 12 Nov 2023 15:53:46 +0100
Subject: [PATCH] [UI] Added navigation crumbs to the file browser.

---
 .../webapp/src/components/File/Browser.vue    | 118 +++++++++++++++---
 1 file changed, 100 insertions(+), 18 deletions(-)

diff --git a/platypush/backend/http/webapp/src/components/File/Browser.vue b/platypush/backend/http/webapp/src/components/File/Browser.vue
index 4ff7a2018..437eaef38 100644
--- a/platypush/backend/http/webapp/src/components/File/Browser.vue
+++ b/platypush/backend/http/webapp/src/components/File/Browser.vue
@@ -2,27 +2,46 @@
   <div class="browser-container">
     <Loading v-if="loading" />
 
-    <div class="row item" @click="path = (path || '') + '/..'" v-if="path?.length && path !== '/'">
-      <div class="col-10 left side">
-        <i class="icon fa fa-folder" />
-        <span class="name">..</span>
-      </div>
+    <div class="nav" ref="nav">
+      <span class="path"
+            v-for="(token, i) in pathTokens"
+            :key="i"
+            @click="path = pathTokens.slice(0, i + 1).join('/').slice(1)">
+        <span class="token">
+          {{ token }}
+        </span>
+
+        <span class="separator" v-if="(i > 0 || pathTokens.length > 1) && i < pathTokens.length - 1">
+          <i class="fa fa-chevron-right" />
+        </span>
+      </span>
     </div>
 
-    <div class="row item" v-for="(file, i) in filteredFiles" :key="i" @click="path = file.path">
-      <div class="col-10">
-        <i class="icon fa" :class="{'fa-file': file.type !== 'directory', 'fa-folder': file.type === 'directory'}" />
-        <span class="name">
-          {{ file.name }}
-        </span>
+    <div class="items" ref="items">
+      <div class="row item"
+           @click="onBack"
+           v-if="(path?.length && path !== '/') || hasBack">
+        <div class="col-10 left side">
+          <i class="icon fa fa-folder" />
+          <span class="name">..</span>
+        </div>
       </div>
 
-      <div class="col-2 actions">
-        <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())" />
-        </Dropdown>
+      <div class="row item" v-for="(file, i) in filteredFiles" :key="i" @click="path = file.path">
+        <div class="col-10">
+          <i class="icon fa" :class="{'fa-file': file.type !== 'directory', 'fa-folder': file.type === 'directory'}" />
+          <span class="name">
+            {{ file.name }}
+          </span>
+        </div>
+
+        <div class="col-2 actions">
+          <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())" />
+          </Dropdown>
+        </div>
       </div>
     </div>
   </div>
@@ -39,9 +58,14 @@ export default {
   name: "Browser",
   components: {DropdownItem, Dropdown, Loading},
   mixins: [Utils, MediaUtils],
-  emits: ['path-change'],
+  emits: ['back', 'path-change', 'play'],
 
   props: {
+    hasBack: {
+      type: Boolean,
+      default: false,
+    },
+
     initialPath: {
       type: String,
     },
@@ -71,11 +95,24 @@ export default {
 
       return this.files.filter((file) => (file?.name || '').toLowerCase().indexOf(this.filter.toLowerCase()) >= 0)
     },
+
+    pathTokens() {
+      if (!this.path?.length)
+        return ['/']
+
+      return ['/', ...this.path.split(/(?<!\\)\//).slice(1)]
+    },
   },
 
   methods: {
     async refresh() {
       this.loading = true
+      this.$nextTick(() => {
+        // Scroll to the end of the path navigator
+        this.$refs.nav.scrollLeft = 99999
+        // Scroll to the top of the items list
+        this.$refs.items.scrollTop = 0
+      })
 
       try {
         this.files = await this.request('file.list', {path: this.path})
@@ -84,6 +121,13 @@ export default {
         this.loading = false
       }
     },
+
+    onBack() {
+      if (!this.path?.length || this.path === '/')
+        this.$emit('back')
+      else
+        this.path = [...this.pathTokens].slice(0, -1).join('/').slice(1)
+    },
   },
 
   mounted() {
@@ -96,12 +140,50 @@ export default {
 <style lang="scss" scoped>
 @import "src/style/items";
 
+$nav-height: 2.5em;
+
 .browser-container {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+
   .item {
     .actions {
       display: inline-flex;
       justify-content: right;
     }
   }
+
+  .nav {
+    width: 100%;
+    height: $nav-height;
+    padding: 0.5em 1em;
+    background: $tab-bg;
+    box-shadow: $border-shadow-bottom;
+    white-space: nowrap;
+    overflow: hidden;
+
+    .path {
+      cursor: pointer;
+
+      .token {
+        &:hover {
+          color: $default-hover-fg;
+          text-decoration: underline;
+        }
+      }
+
+      .separator {
+        font-size: 1em;
+        width: 1.2em;
+        padding: 0 1em;
+      }
+    }
+  }
+
+  .items {
+    height: calc(100% - #{$nav-height});
+    overflow: auto;
+  }
 }
 </style>