Parcourir la source

reworked the label selector a bit

Dimitri Korsch il y a 3 ans
Parent
commit
4f9c26631c

+ 84 - 81
webui/package-lock.json

@@ -1715,6 +1715,16 @@
           "integrity": "sha1-/q7SVZc9LndVW4PbwIhRpsY1IPo=",
           "dev": true
         },
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
         "cacache": {
           "version": "13.0.1",
           "resolved": "https://registry.npm.taobao.org/cacache/download/cacache-13.0.1.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcacache%2Fdownload%2Fcacache-13.0.1.tgz",
@@ -1741,6 +1751,53 @@
             "unique-filename": "^1.1.1"
           }
         },
+        "chalk": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true,
+          "optional": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true,
+          "optional": true
+        },
+        "loader-utils": {
+          "version": "2.0.2",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
+          "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^2.1.2"
+          }
+        },
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npm.taobao.org/source-map/download/source-map-0.6.1.tgz",
@@ -1757,6 +1814,16 @@
             "minipass": "^3.1.1"
           }
         },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
         "terser-webpack-plugin": {
           "version": "2.3.8",
           "resolved": "https://registry.npm.taobao.org/terser-webpack-plugin/download/terser-webpack-plugin-2.3.8.tgz?cache=0&sync_timestamp=1610194258495&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fterser-webpack-plugin%2Fdownload%2Fterser-webpack-plugin-2.3.8.tgz",
@@ -1773,6 +1840,18 @@
             "terser": "^4.6.12",
             "webpack-sources": "^1.4.3"
           }
+        },
+        "vue-loader-v16": {
+          "version": "npm:vue-loader@16.8.3",
+          "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
+          "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "chalk": "^4.1.0",
+            "hash-sum": "^2.0.0",
+            "loader-utils": "^2.0.0"
+          }
         }
       }
     },
@@ -10974,6 +11053,11 @@
       "resolved": "https://registry.npm.taobao.org/vue/download/vue-2.6.12.tgz?cache=0&sync_timestamp=1609359675074&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue%2Fdownload%2Fvue-2.6.12.tgz",
       "integrity": "sha1-9evU+mvShpQD4pqJau1JBEVskSM="
     },
+    "vue-debounce": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/vue-debounce/-/vue-debounce-3.0.2.tgz",
+      "integrity": "sha512-+shuc9Ry+AFqJbN7BMfagazB81/bTiPWvUZ4KBjambgrd3B5EQBojxeGzeNZ21xRflnwB098BG1d0HtWv8WyzA=="
+    },
     "vue-eslint-parser": {
       "version": "7.3.0",
       "resolved": "https://registry.npm.taobao.org/vue-eslint-parser/download/vue-eslint-parser-7.3.0.tgz?cache=0&sync_timestamp=1608031066427&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-eslint-parser%2Fdownload%2Fvue-eslint-parser-7.3.0.tgz",
@@ -11027,87 +11111,6 @@
         }
       }
     },
-    "vue-loader-v16": {
-      "version": "npm:vue-loader@16.1.2",
-      "resolved": "https://registry.npm.taobao.org/vue-loader/download/vue-loader-16.1.2.tgz?cache=0&sync_timestamp=1608188078235&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-loader%2Fdownload%2Fvue-loader-16.1.2.tgz",
-      "integrity": "sha1-XAO2xQ0qX5g8fOuhXFDXjKKymPQ=",
-      "dev": true,
-      "optional": true,
-      "requires": {
-        "chalk": "^4.1.0",
-        "hash-sum": "^2.0.0",
-        "loader-utils": "^2.0.0"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "4.3.0",
-          "resolved": "https://registry.npm.taobao.org/ansi-styles/download/ansi-styles-4.3.0.tgz?cache=0&sync_timestamp=1606792436886&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fansi-styles%2Fdownload%2Fansi-styles-4.3.0.tgz",
-          "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npm.taobao.org/chalk/download/chalk-4.1.0.tgz?cache=0&sync_timestamp=1591687018980&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fchalk%2Fdownload%2Fchalk-4.1.0.tgz",
-          "integrity": "sha1-ThSHCmGNni7dl92DRf2dncMVZGo=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz",
-          "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "color-name": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npm.taobao.org/color-name/download/color-name-1.1.4.tgz",
-          "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=",
-          "dev": true,
-          "optional": true
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npm.taobao.org/has-flag/download/has-flag-4.0.0.tgz",
-          "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
-          "dev": true,
-          "optional": true
-        },
-        "loader-utils": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npm.taobao.org/loader-utils/download/loader-utils-2.0.0.tgz?cache=0&sync_timestamp=1598867216219&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Floader-utils%2Fdownload%2Floader-utils-2.0.0.tgz",
-          "integrity": "sha1-5MrOW4FtQloWa18JfhDNErNgZLA=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "big.js": "^5.2.2",
-            "emojis-list": "^3.0.0",
-            "json5": "^2.1.2"
-          }
-        },
-        "supports-color": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npm.taobao.org/supports-color/download/supports-color-7.2.0.tgz?cache=0&sync_timestamp=1608035619713&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsupports-color%2Fdownload%2Fsupports-color-7.2.0.tgz",
-          "integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        }
-      }
-    },
     "vue-style-loader": {
       "version": "4.1.2",
       "resolved": "https://registry.npm.taobao.org/vue-style-loader/download/vue-style-loader-4.1.2.tgz",

+ 2 - 1
webui/package.json

@@ -10,7 +10,8 @@
   "dependencies": {
     "core-js": "^3.6.5",
     "socket.io-client": "^3.0.5",
-    "vue": "^2.6.11"
+    "vue": "^2.6.11",
+    "vue-debounce": "^3.0.2"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "~4.5.0",

+ 1 - 25
webui/src/components/media/annotated-image.vue

@@ -6,7 +6,6 @@
                    @interaction="interaction = $event"
                    :filter="filter"
                    @filter="filter = $event"
-                   :labels="labels"
                    :labelsEnabled="labelsEnabled"
                    @labelSelector="openLabelSelector()"
                    :zoomBox="zoomBox"
@@ -280,7 +279,7 @@ export default {
           return `${tip}: move or resize`;
 
         case "label-box":
-          if (this.label.identifier !== null)
+          if (this.label.identifier === null)
             return `${tip}: remove tag`;
           else
             return `${tip}: tag as "${this.label.name}"`;
@@ -495,27 +494,4 @@ h3 {
 }
 
 
-.label-selector {
-  position: absolute;
-  top: 0;
-  left: 50%;
-  z-index: 101;
-
-  width: 30rem;
-  max-width: 90vw;
-  max-height: 90%;
-  padding-bottom: 0;
-
-  animation: label-selector-animation 0.3s ease-out forwards;
-}
-
-@keyframes label-selector-animation {
-  from {
-    transform: translateX(-50%) translateY(-100%);
-  }
-  to {
-    transform: translateX(-50%) translateY(0%);
-  }
-}
-
 </style>

+ 214 - 62
webui/src/components/media/label-selector.vue

@@ -1,34 +1,57 @@
 <template>
   <div class="label-selector">
-    <input ref="input" type="text"
-           v-model="search" placeholder="Search...">
-
-    <div v-if="results.length > 0" class="labels">
-      <div v-for="(label, index) in results" v-bind:key="label.identifier"
-           class="label selectable"
-           :class="{selected: index === selectedIndex, visible: label.is_visible}"
-           @click="select(label)">
-        <div class="hierarchy" v-if="label.hierarchy_level">
-          {{ label.hierarchy_level }}
-        </div>
-        <div class="name">
-          {{ label.name }}
-        </div>
-      </div>
+    <input v-debounce:400ms="setQuery"
+           class="search-field"
+           ref="input"
+           type="text"
+           placeholder="Search...">
+    <div class="filters">
+      <span @click="resetHierarchyFilter"
+            class="filter active">
+          Show all
+        </span>
+      <span v-for="hierarchy in hierarchies"
+            :key="hierarchy.key"
+            @click="toggleHierarchyFilter(hierarchy.key)"
+            :class="{active: isFilterActive(hierarchy.key)}"
+            class="filter">
+          {{ hierarchy.name }}
+       </span>
     </div>
+    <div class="labels">
+      <div v-for="(label, index) in results"
+           :key="index"
+           @click="select(label)"
+           class="label"
+           :class="{selected: index === selectedIndex}"
+           >
+
+
+          <div class="name">
+            <span v-html="highlight(label.name)"></span>
+            <span v-if="label.hierarchy_level"
+                  class="hierarchy">
+              ({{ label.hierarchy_level }})
+            </span>
+          </div>
+      </div>
+
 
-    <div v-else class="labels">
-      <div class="label">
+      <div v-if="results.length < 1" class="label">
         No results found...
       </div>
     </div>
   </div>
 </template>
 
+
+
 <script>
 export default {
   name: "label-selector",
-  props: ['labels'],
+  props: [
+    "labels"
+  ],
   mounted: function () {
     // register events to prevent global hotkeys while typing
     window.addEventListener('keypress', this.keypress, true);
@@ -45,15 +68,94 @@ export default {
   },
   data: function () {
     return {
-      search: '',
       selectedIndex: 0,
-      minSearchChars: 2,
-      deleteLabel: {identifier: null, name: 'None'}
+      query: "",
+      hierarchy_filters: [],
+      deleteLabel: {
+        hierarchy_level: null,
+        identifier: null,
+        name: 'None',
+      },
     }
   },
+
   computed: {
-    sortedLabels: function () {
-      return [...this.labels].sort((a, b) => {
+    hierarchies: function() {
+      let hierarchies = [...new Set(this.labels.map(l => l.hierarchy_level))]
+      hierarchies.sort((a, b) => {
+        if(a === null)
+          return +1;
+        if(b === null)
+          return -1;
+        if (a < b)
+          return +1;
+        else
+          return -1;
+      });
+      return hierarchies.map(
+        l => {
+          return {
+            key: l,
+            name: (l === null ? "Art" : l)
+          }
+        }
+      );
+    },
+
+    results: function() {
+      const query = this.query;
+      let labels = this.labels;
+      const n_filters = this.hierarchy_filters.length;
+
+      if (n_filters !== 0 && n_filters !== this.hierarchies.length)
+        labels = labels.filter(
+          l => this.hierarchy_filters.indexOf(l.hierarchy_level) > -1
+        )
+
+      labels = labels.filter(
+        l => l.name.toLowerCase().indexOf(query) > -1
+      );
+
+      this.sort(labels);
+      return [this.deleteLabel, ...labels];
+    }
+  },
+
+  methods: {
+
+    resetHierarchyFilter: function() {
+      this.hierarchy_filters = [];
+    },
+
+    isFilterActive: function (key){
+      return this.hierarchy_filters.length == 0 ||
+        this.hierarchy_filters.indexOf(key) != -1;
+    },
+
+    toggleHierarchyFilter: function(key) {
+
+      const idx = this.hierarchy_filters.indexOf(key);
+
+      if (idx == -1)
+        this.hierarchy_filters.push(key)
+      else
+        this.hierarchy_filters.splice(idx, 1)
+
+      console.debug("Current hierarchy filters:",
+        this.hierarchy_filters)
+    },
+
+    setQuery: function(query) {
+      this.query = query.toLowerCase();
+    },
+
+    n_visible: function () {
+      return this.results.length;
+    },
+
+    sort: function(labels) {
+      labels.sort((a, b) => {
+
         if (a.hierarchy_level !== b.hierarchy_level) {
           if (a.hierarchy_level === null)
             return +1;
@@ -71,25 +173,15 @@ export default {
           return +1;
       });
     },
-    results: function () {
-      let labs = [this.deleteLabel, ...this.sortedLabels];
-      labs.forEach(l => {l.is_visible = true });
-      return labs;
-    },
 
-    n_visible: function () {
-      return this.results.filter(l => l.is_visible).length;
-    },
-  },
-  methods: {
     keypress: function (event) {
       event.stopPropagation();
 
       switch (event.key) {
         case 'ArrowDown':
           this.selectedIndex += 1;
-          if (this.selectedIndex >= this.n_visible)
-            this.selectedIndex = this.n_visible - 1;
+          if (this.selectedIndex >= this.n_visible())
+            this.selectedIndex = this.n_visible() - 1;
           break;
 
         case 'ArrowUp':
@@ -107,47 +199,75 @@ export default {
           break;
 
         default:
-          this.selectedIndex = Math.min(1, this.n_visible - 1);
+          this.selectedIndex = Math.min(1, this.n_visible() - 1);
           break;
       }
     },
 
+    highlight: function (name) {
+      const query = this.query
+      if (!query)
+        return name;
 
-    setVisibility: function (search) {
-      console.log(search);
-      this.results.forEach(l => {
-        l.is_visible = !search || !l.identifier || l.name.toLowerCase().includes(search);
-      });
+      const start = name.toLowerCase().indexOf(query);
+      if (start == -1)
+        return name
+
+      const end = start + query.length;
+
+      const pre = name.substring(0, start);
+      const match = name.substring(start, end);
+      const post = name.substring(end);
+      return `${pre}<strong>${match}</strong>${post}`;
     },
+
     select: function (label) {
+      console.debug("selected label", label);
       this.$emit('label', label);
       this.closeDelayed();
     },
+
     closeDelayed: function () {
       setTimeout(() => this.$emit('close', true), 100);
-    }
-  },
-  watch: {
-    search: function(newVal) {
-      let query = "";
-
-      if (newVal.length >= this.minSearchChars)
-        query = newVal.toLowerCase();
-
-      this.setVisibility(query);
-    }
+    },
   }
 }
 </script>
 
+
 <style scoped>
+
 .label-selector {
+  position: absolute;
+  top: 0;
+  left: 50%;
+  z-index: 101;
+
+  width: 30rem;
+  max-width: 90vw;
+  max-height: 90%;
+
   display: flex;
   flex-direction: column;
 
+  border-bottom-right-radius: 0.5rem;
+  border-bottom-left-radius: 0.5rem;
   padding: 1rem;
-  background-color: rgba(0, 0, 0, 0.65);
+
+  background-color: rgba(0, 0, 0, 0.75);
   color: whitesmoke;
+
+  animation: label-selector-animation 0.3s ease-out forwards;
+
+}
+
+@keyframes label-selector-animation {
+  from {
+    transform: translateX(-50%) translateY(-100%);
+  }
+  to {
+    transform: translateX(-50%) translateY(0%);
+  }
 }
 
 input {
@@ -161,27 +281,59 @@ input {
 }
 
 .label {
-  padding: 0.25rem 0;
-  display: none;
-}
-
-.label.visible{
-  display: inherit;
+  padding: 0.25rem 0.5rem;
+  cursor: pointer;
+  border-radius: 0.3rem;
+  margin: 0.2rem;
 }
 
 .label:not(:last-child) {
   border-bottom: 1px solid rgba(255, 255, 255, 0.2);
 }
 
-.label.selectable:hover,
-.label.selected {
+.label:hover, .label.selected {
   background-color: var(--primary);
-  padding: 0.25rem 0.5rem;
 }
 
 .hierarchy {
   font-size: 77%;
   opacity: 0.8;
+  float: right;
+}
+
+.filters {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-evenly;
+  align-items: baseline;
+  padding: 0.25rem;
+  border-bottom: 1px solid rgb(20%, 20%, 20%);
+}
+
+.filter {
+  flex-grow: 1;
+  border-radius: 0.5rem;
+  margin: 0rem 0.5rem;
+  padding: 0.25rem;
+  text-align: center;
+
+  background-color: rgb(50%, 50%, 50%);
+  border: 1px solid rgb(10%,10%,10%);
+  color: rgb(20%, 20%, 20%);
+  cursor: pointer;
+}
+
+.filter.active {
+  background-color: rgb(25%, 25%, 25%);
+  color: whitesmoke;
+}
+
+.filter:hover {
+  background-color: rgb(40%, 40%, 40%);
+}
+
+.filter.active:hover {
+  background-color: rgb(20%, 20%, 20%);
 }
 
 </style>

+ 2 - 0
webui/src/main.js

@@ -7,6 +7,7 @@ import './assets/style/fonts.css'
 import Vue from 'vue'
 import App from './App.vue'
 import io from "socket.io-client";
+import vueDebounce from "vue-debounce";
 
 // establish socket connection
 const host = Vue.config.devtools ? window.location.hostname + ':5000' : window.location.host;
@@ -15,6 +16,7 @@ const sio = io(self);
 
 // initialize vue.js
 Vue.config.productionTip = false
+Vue.use(vueDebounce);
 
 new Vue({
     render: h => h(App),