diff --git a/package-lock.json b/package-lock.json index be11d3290fd232d29cd0a8f6856cd9637dc185d2..b1afc0f861ef12d7327b6791b3a2325fd2121ec6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1104,6 +1104,32 @@ "to-fast-properties": "^2.0.0" } }, + "@fortawesome/fontawesome-common-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.2.tgz", + "integrity": "sha512-wBaAPGz1Awxg05e0PBRkDRuTsy4B3dpBm+zreTTyd9TH4uUM27cAL4xWyWR0rLJCrRwzVsQ4hF3FvM6rqydKPA==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.2.tgz", + "integrity": "sha512-853G/Htp0BOdXnPoeCPTjFrVwyrJHpe8MhjB/DYE9XjwhnNDfuBCd3aKc2YUYbEfHEcBws4UAA0kA9dymZKGjA==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.1.2" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.1.2.tgz", + "integrity": "sha512-lTgZz+cMpzjkHmCwOG3E1ilUZrnINYdqMmrkv30EC3XbRsGlbIOL8H9LaNp5SV4g0pNJDfQ4EdTWWaMvdwyLiQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.1.2" + } + }, + "@fortawesome/vue-fontawesome": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-2.0.8.tgz", + "integrity": "sha512-SRmP0q9Ox4zq8ydDR/hrH+23TVU1bdwYVnugLVaAIwklOHbf56gx6JUGlwES7zjuNYqzKgl8e39iYf6ph8qSQw==" + }, "@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -1154,6 +1180,27 @@ "postcss": "^7.0.0" } }, + "@kouts/vue-modal": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@kouts/vue-modal/-/vue-modal-2.1.9.tgz", + "integrity": "sha512-VuW/4o2Gdrm67fKIJk7L9HIy/G/3fBJE+ZXus0mVqlLUdua+u1MQIPI9lsEm64zyYCSrgRKcsQrn5jnbanX4wA==", + "dev": true, + "requires": { + "vue": "^2.6.14" + }, + "dependencies": { + "vue": { + "version": "2.7.9", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.9.tgz", + "integrity": "sha512-GeWCvAUkjzD5q4A3vgi8ka5r9bM6g8fmNmx/9VnHDKCaEzBcoVw+7UcQktZHrJ2jhlI+Zv8L57pMCIwM4h4MWg==", + "dev": true, + "requires": { + "@vue/compiler-sfc": "2.7.9", + "csstype": "^3.1.0" + } + } + } + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -1170,6 +1217,20 @@ "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, + "@revolist/revogrid": { + "version": "4.2.0-next.1", + "resolved": "https://registry.npmjs.org/@revolist/revogrid/-/revogrid-4.2.0-next.1.tgz", + "integrity": "sha512-eW9xGbX/mauVpdB2qgDHj4JHqG7vB9Vc2Y9pkEP21J7nygmdgSJnAIlRGVyFez0fuzQgrCiVS2wckfVb/h+WCQ==" + }, + "@revolist/vue-datagrid": { + "version": "4.2.0-next.1", + "resolved": "https://registry.npmjs.org/@revolist/vue-datagrid/-/vue-datagrid-4.2.0-next.1.tgz", + "integrity": "sha512-Xaj40sOY9+C6jkwb6156xccf7QqNrC9l1rt6/grdhyCTxdOQObIn5p6eY8WSnYrzju/06tOC8pLLPdrmKPDCmA==", + "requires": { + "@revolist/revogrid": "^4.2.0-next.1", + "@stencil/core": "^2.17.3" + } + }, "@soda/friendly-errors-webpack-plugin": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.0.tgz", @@ -1240,6 +1301,11 @@ "integrity": "sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==", "dev": true }, + "@stencil/core": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.17.3.tgz", + "integrity": "sha512-qw2DZzOpyaltLLEfYRTj3n+XbvRtkmv4QQimYDJubC6jMY0NXK9r6H2+VyszdbbVmvK1D9GqZtyvY0NmOrztsg==" + }, "@types/anymatch": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", @@ -1779,63 +1845,6 @@ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "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" - } - }, - "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" - } - }, "ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -1844,28 +1853,6 @@ "requires": { "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" - } - }, - "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" - } } } }, @@ -1889,6 +1876,42 @@ "strip-ansi": "^6.0.0" } }, + "@vue/compiler-sfc": { + "version": "2.7.9", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.9.tgz", + "integrity": "sha512-TD2FvT0fPUezw5RVP4tfwTZnKHP0QjeEUb39y7tORvOJQTjbOuHJEk4GPHUPsRaTeQ8rjuKjntyrYcEIx+ODxg==", + "dev": true, + "requires": { + "@babel/parser": "^7.18.4", + "postcss": "^8.4.14", + "source-map": "^0.6.1" + }, + "dependencies": { + "@babel/parser": { + "version": "7.18.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz", + "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==", + "dev": true + }, + "postcss": { + "version": "8.4.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", + "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "@vue/component-compiler-utils": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.2.0.tgz", @@ -2146,6 +2169,15 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "a11y-dialog": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/a11y-dialog/-/a11y-dialog-7.5.2.tgz", + "integrity": "sha512-zfWtVvrbGbP3AFnEJ1aJFtu7GvedgjOEKbkyEUSeaNWDzmFJk9O5nuolDQrRDyRDE5fqSJRiBJtD5bUYKveoUg==", + "dev": true, + "requires": { + "focusable-selectors": "^0.4.0" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2261,7 +2293,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -3174,7 +3205,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -3531,7 +3561,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -3539,8 +3568,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "color-string": { "version": "1.5.5", @@ -3944,8 +3972,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { "version": "5.2.1", @@ -4259,6 +4286,12 @@ } } }, + "csstype": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", + "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", + "dev": true + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -4283,6 +4316,11 @@ "assert-plus": "^1.0.0" } }, + "date-fns": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.1.tgz", + "integrity": "sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw==" + }, "de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -4729,6 +4767,14 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "dev": true }, + "downloadify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/downloadify/-/downloadify-1.0.2.tgz", + "integrity": "sha512-7WrGWRhiUGA3SwuRHfFaDoKYnSugUw0tqKKZcS4JOL9fkMReRoIZ24aVHncBTC/72ZuXpQcVQRVNUjidgZo1FA==", + "requires": { + "mime": "^2.4.6" + } + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -4937,8 +4983,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { "version": "6.8.0", @@ -5886,6 +5931,11 @@ "schema-utils": "^2.5.0" } }, + "file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -6019,6 +6069,12 @@ "readable-stream": "^2.3.6" } }, + "focusable-selectors": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/focusable-selectors/-/focusable-selectors-0.4.0.tgz", + "integrity": "sha512-tc/236hUU3xemsRLu1RKhRQ5UWHjRM9iJTli1zdac43h7b1biRSgG0mILM0qrcsKaGCHcOPJ6NKbk12ouKHLpw==", + "dev": true + }, "follow-redirects": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", @@ -6388,8 +6444,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.2", @@ -6522,6 +6577,11 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "howler": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", + "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==" + }, "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -6832,6 +6892,11 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "import-cwd": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", @@ -6961,8 +7026,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "inquirer": { "version": "7.3.3", @@ -7457,8 +7521,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -7605,6 +7668,17 @@ "verror": "1.10.0" } }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -7646,6 +7720,14 @@ "type-check": "~0.3.2" } }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -7788,6 +7870,11 @@ "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", "dev": true }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "lodash.kebabcase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", @@ -7888,6 +7975,11 @@ "object-visit": "^1.0.0" } }, + "material-icons": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/material-icons/-/material-icons-1.11.10.tgz", + "integrity": "sha512-1nGlVYwH98BQGSoxHf1QCCDll6AmnkxuATjTHREvy8BSEc6EfRkvcsQwEmSgAY7qcKLV3Ip81u9YEiH/sjQgkg==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -8112,8 +8204,7 @@ "mime": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", - "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", - "dev": true + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" }, "mime-db": { "version": "1.47.0", @@ -8324,6 +8415,12 @@ "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", "dev": true }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -8959,8 +9056,7 @@ "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "parallel-transform": { "version": "1.2.0", @@ -9163,6 +9259,12 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "picomatch": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", @@ -9916,8 +10018,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "progress": { "version": "2.0.3", @@ -10185,7 +10286,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -10429,6 +10529,12 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true + }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -10540,8 +10646,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -10927,8 +11032,7 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "setprototypeof": { "version": "1.1.1", @@ -11211,6 +11315,12 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", @@ -11489,7 +11599,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -11569,7 +11678,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -12310,8 +12418,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.1", @@ -12393,6 +12500,11 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz", "integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg==" }, + "vue-class-component": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz", + "integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==" + }, "vue-eslint-parser": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.6.0.tgz", @@ -12419,12 +12531,30 @@ } } }, + "vue-good-table": { + "version": "2.21.11", + "resolved": "https://registry.npmjs.org/vue-good-table/-/vue-good-table-2.21.11.tgz", + "integrity": "sha512-OpVPdxbBTahtfq1aXxEa5P1CMy1wiLcBg4mo7k6Qs537l9v8KVrvF+fXqbnxqNrAfmd1Mw9LidcjgTErjmVU8g==", + "requires": { + "date-fns": "^2.17.0", + "lodash.isequal": "^4.5.0" + } + }, "vue-hot-reload-api": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", "dev": true }, + "vue-js-modal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/vue-js-modal/-/vue-js-modal-2.0.1.tgz", + "integrity": "sha512-5FUwsH2zoxRKX4a7wkFAqX0eITCcIMunJDEfIxzHs2bHw9o20+Iqm+uQvBcg1jkzyo1+tVgThR/7NGU8djbD8Q==", + "dev": true, + "requires": { + "resize-observer-polyfill": "^1.5.1" + } + }, "vue-loader": { "version": "15.9.7", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.7.tgz", @@ -12446,11 +12576,117 @@ } } }, + "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" + }, + "dependencies": { + "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" + } + }, + "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" + } + }, + "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" + } + } + } + }, + "vue-property-decorator": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-8.5.1.tgz", + "integrity": "sha512-O6OUN2OMsYTGPvgFtXeBU3jPnX5ffQ9V4I1WfxFQ6dqz6cOUbR3Usou7kgFpfiXDvV7dJQSFcJ5yUPgOtPPm1Q==", + "requires": { + "vue-class-component": "^7.1.0" + } + }, + "vue-rate": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/vue-rate/-/vue-rate-2.5.0.tgz", + "integrity": "sha512-JMP1hm7fX+PO3fUdB+RPqadabflIOMqo/waZ5LL0HI1sRcF7Cj8rvJHNfv6eH3AQPUwvSNm7rZwDVwqMwIlcMw==", + "requires": { + "vue": "^2.5.21" + } + }, "vue-router": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.1.tgz", "integrity": "sha512-RRQNLT8Mzr8z7eL4p7BtKvRaTSGdCbTy2+Mm5HTJvLGYSSeG9gDzNasJPP/yOYKLy+/cLG/ftrqq5fvkFwBJEw==" }, + "vue-slider-component": { + "version": "3.2.20", + "resolved": "https://registry.npmjs.org/vue-slider-component/-/vue-slider-component-3.2.20.tgz", + "integrity": "sha512-S5+4d6zdL+/ClpDQoIgImIdXRv2b+75PIy3cDGsZsakhroJD6cSFA0juY/AblGqhvIkNcBIU354eOw6T26DWbA==", + "requires": { + "core-js": "^3.6.5", + "vue-property-decorator": "^8.0.0" + } + }, "vue-style-loader": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", @@ -12485,6 +12721,15 @@ "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "dev": true }, + "vuesax": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/vuesax/-/vuesax-3.12.2.tgz", + "integrity": "sha512-fdzcTPsrVklhWXtC8o07n9+SCmdQ8rxD9cZxNUx8KnbSH/Ss5azIHLLwMoNQuwT6kBFUcRzxVTfDh8jHzoSXaw==", + "requires": { + "chalk": "^2.4.2", + "vue": "^2.6.9" + } + }, "watchpack": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", @@ -13299,6 +13544,12 @@ "async-limiter": "~1.0.0" } }, + "xmodal-vue": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/xmodal-vue/-/xmodal-vue-1.0.5.tgz", + "integrity": "sha512-XqqqARzjXhmvwNSJ6qDQD74tI8TvNE9+rwMwfeXx/ZATW4cwvf9ncFjS9GRtPGm6R/t+v5Ne0hmyhWBml0cIZQ==", + "dev": true + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index f8b6f290c662e1dce90f81bbdfdf3f685591f32a..67e72ce20e24071b1d63e9d43c42f144cf746c4b 100644 --- a/package.json +++ b/package.json @@ -8,20 +8,35 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.1.2", + "@fortawesome/free-solid-svg-icons": "^6.1.2", + "@fortawesome/vue-fontawesome": "^2.0.8", + "@revolist/vue-datagrid": "^4.2.0-next.1", "axios": "^0.21.1", "core-js": "^3.6.5", + "downloadify": "^1.0.2", + "file-saver": "^2.0.5", + "howler": "^2.2.3", + "jszip": "^3.10.1", + "material-icons": "^1.11.10", "vue": "^2.6.11", + "vue-good-table": "^2.21.11", + "vue-rate": "^2.5.0", "vue-router": "^3.2.0", + "vue-slider-component": "^3.2.20", + "vuesax": "^3.12.2", "wavesurfer.js": "^5.1.0", "wavesurfer.js-vue": "^1.0.0" }, "devDependencies": { + "@kouts/vue-modal": "^2.1.9", "@types/wavesurfer.js": "^5.1.0", "@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-plugin-router": "~4.5.0", "@vue/cli-service": "~4.5.0", "@vue/eslint-config-standard": "^5.1.2", + "a11y-dialog": "^7.5.2", "babel-eslint": "^10.1.0", "eslint": "^6.7.2", "eslint-plugin-import": "^2.20.2", @@ -31,7 +46,9 @@ "eslint-plugin-vue": "^6.2.2", "node-sass": "^4.12.0", "sass-loader": "^8.0.2", - "vue-template-compiler": "^2.6.11" + "vue-js-modal": "^2.0.1", + "vue-template-compiler": "^2.6.11", + "xmodal-vue": "^1.0.5" }, "eslintConfig": { "root": true, diff --git a/src/App.vue b/src/App.vue index 404b9f24af82e9b245cc23121a48bf2b396a5ddb..8c9acdb9b36810282776b08481cb10c22af6d4fa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,15 +1,15 @@ <template> <div id="orchive3"> - <aside> - <SearchBar id="SearchContainer"> - </SearchBar> + <aside id="side-id" class="side"> + <NavigationMenu class="side-content" v-if="!isSearch" id="NavigationContainer"></NavigationMenu> + <SearchBar class="side-content" v-else id="SearchBarContainer"></SearchBar> <div class="dividers"> <div class="dividerSmall"></div> <div class="divider"></div> <div class="dividerSmall"></div> </div> </aside> - <main> + <main v-bind:style="{marginLeft: (this.asideWidth) + 'px'}"> <transition name="fade" mode="out-in"> <router-view :key="$route.path"/> </transition> @@ -18,13 +18,36 @@ </template> <script> -import SearchBar from './components/SearchBar.vue' + +import NavigationMenu from '@/components/NavigationMenu' +import SearchBar from '@/components/SearchBar' export default { components: { - SearchBar + SearchBar, + NavigationMenu + }, + data () { + return { + asideWidth: 0, + audio: null, + isSearch: false + } + }, + mounted () { + this.resizeObserver = new ResizeObserver(this.onResize) + this.resizeObserver.observe(document.getElementById('side-id')) + this.onResize() + }, + watch: { + $route (to, from) { + this.isSearch = (to.name === 'SearchContextView') + } }, methods: { + onResize () { + this.asideWidth = document.getElementById('side-id').offsetWidth + } } } </script> @@ -50,29 +73,25 @@ export default { body { overflow-x: hidden; height: 100vh; - margin: 0px + margin: 0 } #orchive3 { font-family: Avenir, Helvetica, Arial, sans-serif; height: 100vh; - /* -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-align: center; - color: #2c3e50; */ display: flex; >aside { height: 100vh; - width: 30%; - margin-left: 20px; display: inline-flex; - overflow: hidden; + position: fixed; + } + >main { + width: 100%; } } -#SearchContainer{ - margin: auto 0; - width: 95%; - margin-top: calc(5vh + 20px); + +.side-content { + margin-left: 25px; } .dividers { @@ -94,7 +113,7 @@ body { background: $dark-blue; } -#timelineOverviewContainer{ +#timelineOverviewContainer { width: 13%; min-width: 80px; // padding-right: 20px; @@ -105,30 +124,12 @@ body { margin: auto; } -main { - width: 70%; - // overflow: scroll; - height: 100vh -} - #mainContainer { width: 87%; overflow-y: scroll; height: 100vh; article { - padding: 20px; + padding: 0px; } } -/*#nav { - padding: 30px; -} - -#nav a { - font-weight: bold; - color: #2c3e50; -} - -#nav a.router-link-exact-active { - color: #42b983; -}*/ </style> diff --git a/src/assets/audio/001A.mp3 b/src/assets/audio/001A.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..30ea094d2d361d2c4006a8ddb4c08df43e8ee984 Binary files /dev/null and b/src/assets/audio/001A.mp3 differ diff --git a/src/assets/audio/001A1.mp3 b/src/assets/audio/001A1.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3a87fbaf3a8ad0515d525437001cd7b6142a3efa Binary files /dev/null and b/src/assets/audio/001A1.mp3 differ diff --git a/src/assets/cancel_icon.svg b/src/assets/cancel_icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..448555e98e806791943212ae6acac30269cae4d2 --- /dev/null +++ b/src/assets/cancel_icon.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="17.828" height="17.828" viewBox="0 0 17.828 17.828"> + <polygon points="2.828 17.828 8.914 11.742 15 17.828 17.828 15 11.742 8.914 17.828 2.828 15 0 8.914 6.086 2.828 0 0 2.828 6.085 8.914 0 15 2.828 17.828"/> +</svg> diff --git a/src/assets/check_icon.svg b/src/assets/check_icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..2c16674c8895159aff27a1f4b36b284934cd28ef --- /dev/null +++ b/src/assets/check_icon.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="22.903" height="19.395" viewBox="0 0 22.903 19.395"> + <polygon points="22.903 2.828 20.075 0 6.641 13.435 3.102 9.09 0 11.616 6.338 19.395 22.903 2.828"/> +</svg> diff --git a/src/assets/download.svg b/src/assets/download.svg new file mode 100644 index 0000000000000000000000000000000000000000..63c84661311eafda0aa6d37214be405a00427066 --- /dev/null +++ b/src/assets/download.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="19.799" viewBox="0 0 20 19.799"> + <path d="M8,0a10,10,0,1,0,4,0V8.8h2l-4,4-4-4H8Z"/> +</svg> diff --git a/src/assets/minus_icon.svg b/src/assets/minus_icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..6eace0add1de65d52bef33cdee88d9bae0848b2e --- /dev/null +++ b/src/assets/minus_icon.svg @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve"> +<g id="_x32_"> + <path style="fill:#303030;" d="M125.61,71.238H2.39c-1.32,0-2.39-1.07-2.39-2.39v-9.787c0-1.32,1.07-2.39,2.39-2.39h123.22 + c1.32,0,2.39,1.07,2.39,2.39v9.787C128,70.168,126.93,71.238,125.61,71.238z"/> +</g> +<g id="Layer_1"> +</g> +</svg> diff --git a/src/assets/plus_icon.svg b/src/assets/plus_icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..220f3f26da58cafb2fc9a222561bb294343a46d5 --- /dev/null +++ b/src/assets/plus_icon.svg @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="iso-8859-1"?> +<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve"> +<g id="_x33_"> + <path style="fill:#303030;" d="M128,63.954c0,2.006-0.797,3.821-2.136,5.127c-1.308,1.337-3.125,2.133-5.166,2.133H71.302v49.356 + c0,4.012-3.284,7.292-7.302,7.292c-2.009,0-3.827-0.828-5.166-2.134c-1.308-1.337-2.136-3.152-2.136-5.159V71.214H7.302 + c-4.05,0-7.302-3.248-7.302-7.26c0-2.006,0.797-3.853,2.136-5.159c1.308-1.306,3.125-2.134,5.166-2.134h49.395V7.306 + c0-4.012,3.284-7.26,7.302-7.26c2.009,0,3.827,0.828,5.166,2.133c1.308,1.306,2.136,3.121,2.136,5.127v49.356h49.395 + C124.747,56.662,128,59.91,128,63.954z"/> +</g> +<g id="Layer_1"> +</g> +</svg> diff --git a/src/classes/AlphaColor.js b/src/classes/AlphaColor.js new file mode 100644 index 0000000000000000000000000000000000000000..8dc5124aea46b27ea6a1a5fbf57cf2126f915651 --- /dev/null +++ b/src/classes/AlphaColor.js @@ -0,0 +1,53 @@ +class AlphaColor { + constructor (color) { + this.color = color + } + + parseAlphaColor () { + if (/^rgba\((\d{1,3}%?\s*,\s*){3}(\d*(?:\.\d+)?)\)$/.test(this.color)) { + return this.parseRgba() + } else if (/^hsla\(\d+\s*,\s*([\d.]+%\s*,\s*){2}(\d*(?:\.\d+)?)\)$/.test(this.color)) { + return this.parseHsla() + } else if (/^#([0-9A-Fa-f]{4}|[0-9A-Fa-f]{8})$/.test(this.color)) { + return this.parseAlphaHex() + } else if (/^transparent$/.test(this.color)) { + return this.parseTransparent() + } + + return { + color: this.color, + opacity: '1' + } + } + + parseRgba () { + return { + color: this.color.replace(/,(?!.*,).*(?=\))|a/g, ''), + opacity: this.color.match(/\.\d+|[01](?=\))/)[0] + } + } + + parseHsla () { + return { + color: this.color.replace(/,(?!.*,).*(?=\))|a/g, ''), + opacity: this.color.match(/\.\d+|[01](?=\))/)[0] + } + } + + parseAlphaHex () { + return { + color: this.color.length === 5 ? this.color.substring(0, 4) : this.color.substring(0, 7), + opacity: this.color.length === 5 ? (parseInt(this.color.substring(4, 5) + this.color.substring(4, 5), 16) / 255).toFixed(2) : (parseInt(this.color.substring(7, 9), 16) / 255).toFixed(2) + + } + } + + parseTransparent () { + return { + color: '#fff', + opacity: 0 + } + } +} + +module.exports = AlphaColor diff --git a/src/components/AudioAndFrequencyPlayer.vue b/src/components/AudioAndFrequencyPlayer.vue new file mode 100644 index 0000000000000000000000000000000000000000..0c40a438e36eac247a9489d7eb4e282d78293cac --- /dev/null +++ b/src/components/AudioAndFrequencyPlayer.vue @@ -0,0 +1,330 @@ +<template> + <table> + <tr class="row1"> + <td class="column1"> + <div class="audioTitle"> Audio Spectrum </div> + </td> + <td class="column2"> + <div></div> + </td> + <td class="audio"> + <div v-bind:id="wavesurfer_spectrum_id"> + </div> + </td> + </tr> + <tr class="row1"> + <td class="column1"> + <div class="audioTitle"> Audio Waveform </div> + </td> + <td class="column2"> + <div class="playOrca" @click="playPauseSegment" :class="{noAudio: isError || isLoading, isError: isError}"> + <img class="playIcon" v-if="!isPlaying" src="../assets/play-circle-solid.svg" alt=""> + <img class="pauseIcon" v-else src="../assets/pause-circle-solid.svg" alt=""> + </div> + <div class="loading" v-if="isLoading && !isError"> + <div class="loader"></div> + </div> + <div class="loading error" v-if="isError"> + audio could not be loaded! + </div> + </td> + <td class="audio"> + <div v-bind:id="wavesurfer_audio_id"> + </div> + </td> + </tr> + </table> +</template> + +<script> +import WaveSurfer from 'wavesurfer.js' +import RegionsPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions' +import SpectrogramPlugin from 'wavesurfer.js/src/plugin/spectrogram' +import { Bus } from '@/main' +import axios from 'axios' + +export default { + name: 'AudioAndFrequencyPlayer', + props: { + tape_name: String, + tape_year: Number, + tape_channel: String, + timestamp_start_ms: Number, + timestamp_end_ms: Number, + sequences: { type: Array, required: true }, + is_editable: Boolean, + component_id: String + }, + data () { + return { + wavesurfer_audio_id: 'waveSegment' + this.component_id, + wavesurfer_spectrum_id: 'spectrumSegment' + this.component_id, + bufferedAudioTapes: {}, + isPlaying: false, + isError: false, + isLoading: true, + wavesurfer: null, + playAll: false, + setup: true, + panChannel: 0 + } + }, + async mounted () { + this.wavesurfer = WaveSurfer.create({ + container: '#waveSegment' + this.component_id, + fillParent: true, + barWidth: 7, + interact: this.is_editable, + waveColor: '#912', + plugins: [ + RegionsPlugin.create({ + }), + SpectrogramPlugin.create({ + container: '#spectrumSegment' + this.component_id, + fftSamples: 256, + labels: true, + height: 128, + frequencyMax: 10000 + }) + ] + }) + // add channel support + this.wavesurfer.panner = this.wavesurfer.backend.ac.createStereoPanner() + this.panChannel = 0 + if (localStorage.isLive !== undefined && localStorage.isLive === 'true') { + this.panChannel = 1 + } + this.wavesurfer.panner.pan.value = Number(this.panChannel) + this.wavesurfer.backend.setFilter(this.wavesurfer.panner) + this.wavesurfer.on('ready', await (() => { + this.isLoading = false + this.wavesurfer.clearRegions() + if (this.is_editable) { + this.wavesurfer.enableDragSelection({}) + } + for (let i = 0; i < this.sequences.length; i++) { + const regionToAdd = { + start: ((this.sequences[i].timestamp_start_ms - this.timestamp_start_ms) / 1000), + end: ((this.sequences[i].timestamp_end_ms - this.timestamp_start_ms) / 1000), + id: this.sequences[i].orca_sequence_id, + drag: this.is_editable, + resize: this.is_editable + } + this.wavesurfer.addRegion(regionToAdd) + } + this.setup = false + })) + this.wavesurfer.on('finish', await (() => { + this.isPlaying = false + })) + this.wavesurfer.on('region-click', await ((region, e) => { + this.playAll = false + e.stopPropagation() + region.wavesurfer.play(region.start, region.end) + this.isPlaying = true + Bus.$emit('selected-sequence', region) + })) + this.wavesurfer.on('region-out', await ((region, e) => { + if (!this.playAll) { + this.isPlaying = false + } + })) + this.wavesurfer.on('region-created', await ((region, e) => { + if (!this.setup) { + Bus.$emit('created-sequence', region) + } + })) + this.wavesurfer.on('region-update-end', await ((region, e) => { + if (!this.setup) { + Bus.$emit('updated-sequence', region) + } + })) + this.wavesurfer.on('error', await ((e) => { + })) + }, + destroyed () { + this.$destroy() + Bus.$off() + this.wavesurfer.unAll() + this.wavesurfer.destroy() + if (this.wavesurfer.backend !== null) { + delete this.wavesurfer.backend.buffer + } + }, + created () { + Bus.$on('segment_index_changed', (actualIndex) => { + this.setup = true + this.isLoading = true + this.isError = false + this.isPlaying = false + }) + Bus.$on('player-stop', (stop) => { + this.isPlaying = false + this.wavesurfer.pause() + }) + this.$eventHub.$on('live_queue_settings', (queueSettings) => { + if (this.panChannel === 0 && queueSettings.isLive) { + this.panChannel = 1 + this.wavesurfer.panner.pan.value = Number(this.panChannel) + this.wavesurfer.backend.setFilter(this.wavesurfer.panner) + this.wavesurfer.reload() + } else if (this.panChannel === 1 && !queueSettings.isLive) { + this.panChannel = 0 + this.wavesurfer.panner.pan.value = Number(this.panChannel) + this.wavesurfer.backend.setFilter(this.wavesurfer.panner) + this.wavesurfer.reload() + } + }) + }, + beforeDestroy () { + this.wavesurfer.destroy() + this.wavesurfer.unAll() + }, + methods: { + async loadAudioFile (year, name, channel) { + if (this.bufferedAudioTapes[this.tape_name] === undefined) { + // buffer is buggy + } + let orcaPath = '' + if (this.component_id === 'mark') { + orcaPath = '/files/tapes/' + this.tape_year + '/' + this.tape_name + '/' + this.tape_channel + '.mp3' + } else { + orcaPath = '/files/tapes/' + year + '/' + name + '/' + channel + '.mp3' + } + console.log(orcaPath) + // const orcaPath = require('../assets/audio/001A1.mp3') + const config = { url: orcaPath, method: 'get', responseType: 'blob' } + const response = await axios.request(config) + this.bufferedAudioTapes[this.tape_name] = response.data + // buffer is buggy + const blobAudio = this.bufferedAudioTapes[this.tape_name] + const size = blobAudio.size + const duration = 48 * 60 * 1000 + const bytePerMillisecond = size / duration + const startBytes = bytePerMillisecond * this.timestamp_start_ms + const endBytes = bytePerMillisecond * this.timestamp_end_ms + const blobAudioSliced = blobAudio.slice(startBytes, endBytes) + const sizeSliced = blobAudioSliced.size + console.log( + ' size: ' + size + + '\n duration: ' + duration + + '\n bytePerMillisecond: ' + bytePerMillisecond + + '\n startBytes: ' + startBytes + + '\n endBytes: ' + endBytes + + '\n sizeSliced: ' + sizeSliced + ) + this.wavesurfer.loadBlob(blobAudioSliced) + // this.wavesurfer.load(orcaPath) + }, + playPauseSegment () { + if (this.wavesurfer.isPlaying()) { + this.isPlaying = false + this.wavesurfer.pause() + } else { + Bus.$emit('player-stop', this.isPlaying) + this.isPlaying = true + this.playAll = true + this.wavesurfer.play() + } + }, + removeSequence (id) { + const regions = this.wavesurfer.regions.list + regions[id].remove() + } + } +} +</script> + +<style lang="scss"> +@import '@/assets/color.scss'; +.isError { + top: 44px !important; +} +.loading { + left: 140%; + position: relative; + top: -28px; + width: 100%; + &.error { + color: $dark-blue; + font-weight: bold; + text-transform: uppercase; + color: lightcoral; + } + // class loader: https://www.w3schools.com/howto/howto_css_loader.asp, besucht am 07.10.21 + .loader { + border: 5px solid $light-grey-2; + border-top: 5px solid $dark-blue; + border-radius: 50%; + width: 25px; + height: 25px; + animation: spin 2s linear infinite; + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + } +} + +.noAudio { + top: 17.5px; + position: relative; + cursor: unset; + pointer-events: none; + .playIcon { + filter: $filter-light-grey-2; + cursor: unset; + pointer-events: none; + } +} + +.row1{ + padding-bottom: 28.2px; +} + +.column1{ + width: 95px; + text-align: right; + padding-right: 17.5px; +} + +.column2{ + height: 50px; + width: 50px; + padding-right: 17.5px; +} + +.playIcon{ + width: 50px; + height: 50px; + filter: $filter-dark-blue; + cursor: pointer; +} + +.pauseIcon{ + width: 50px; + height: 50px; + filter: $filter-light-blue; + cursor: pointer; +} + +.audio { + width: 80%; + height: 50px; +} + +marker { + &:hover { + span { + display: block; + } + } + + span { + display: none; + z-index: 100; + background: white; + opacity: 0.8; + } +} +</style> diff --git a/src/components/DetailHeader.vue b/src/components/DetailHeader.vue index 989f4a950f2c8d82d1fc46243d5dc7ae415c6e6b..d949b8cd857d5890f292dee0e232ba0b0e1270f3 100644 --- a/src/components/DetailHeader.vue +++ b/src/components/DetailHeader.vue @@ -16,10 +16,9 @@ }" class="previous" > - < </router-link> <div class="noPrevious" v-else> < </div> - <p> + <p class="next"> {{indexThisYear}} / {{countThisYear}} Tape </p> <router-link v-if="tapes.nextTape != null" @click="sendNextTape" diff --git a/src/components/EditSequenceModel.vue b/src/components/EditSequenceModel.vue new file mode 100644 index 0000000000000000000000000000000000000000..a0e214aec69ac0dc7a9ac93a0eebe0d9a010cd97 --- /dev/null +++ b/src/components/EditSequenceModel.vue @@ -0,0 +1,79 @@ +<template> + <div class="modal-overlay"> + <div class="modal"> + <img class="check" alt="" /> + <h6>Saved!</h6> + <p>Your Details have been saved Successfully</p> + <button>Go Home</button> + </div> + <div class="close"> + <img class="close-img" alt=""/> + </div> + </div> +</template> + +<script> + +export default { + name: 'EditSequenceModel', + methods: { + } +} +</script> + +<style scoped> + +.modal-overlay { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: center; + background-color: #000000da; +} + +.modal { + text-align: center; + background-color: white; + height: 500px; + width: 500px; + margin-top: 10%; + padding: 60px 0; + border-radius: 20px; +} +.close { + margin: 10% 0 0 16px; + cursor: pointer; +} + +.close-img { + width: 25px; +} + +.check { + width: 150px; +} + +h6 { + font-weight: 500; + font-size: 28px; + margin: 20px 0; +} + +p { + font-size: 16px; + margin: 20px 0; +} + +button { + background-color: #ac003e; + width: 150px; + height: 40px; + color: white; + font-size: 14px; + border-radius: 16px; + margin-top: 50px; +} +</style> diff --git a/src/components/FilterSegments.vue b/src/components/FilterSegments.vue new file mode 100644 index 0000000000000000000000000000000000000000..a893f6f09c9ae26048b43c7e23d4715c9d3f4f66 --- /dev/null +++ b/src/components/FilterSegments.vue @@ -0,0 +1,171 @@ +<template> + <div> + <div v-if="!loading"> + <div v-if="canFilterId"> + Filter Year: + <SearchAutoComplete ref="sYear" :clear="false" class="content" :items="allYears" ></SearchAutoComplete> + Filter Tape: + <SearchAutoComplete ref="sTape" :clear="false" class="content" :items="allTapes" ></SearchAutoComplete> + Filter Duration: Max: {{this.maxMin}}s Use: < 3 (shorter), > 5 (longer) or 3 < 5 (between) + <SearchAutoComplete :suggest="false" ref="sDuration" :clear="false" class="content" :items="[]"></SearchAutoComplete> + </div> + <div v-if="canSearchId"> + Find Id: + <SearchAutoComplete :suggest="false" ref="sId" :clear="false" class="content" :items="[]" ></SearchAutoComplete> + </div> + </div> + <div v-else> + loading... + </div> + </div> +</template> + +<script> +import SearchAutoComplete from '../components/SearchAutocomplete' + +export default { + components: { + SearchAutoComplete + }, + name: 'FilterSegments', + props: ['info', 'loading'], + data () { + return { + rows: [], + allYears: [], + allTapes: [], + maxMin: 0, + filtered: [], + canSearchId: true, + canFilterId: true + } + }, + mounted () { + this.$refs.sTape.$on('onkeychange', this.setOptions) + this.$refs.sDuration.$on('onkeychange', this.setOptions) + this.$refs.sYear.$on('onkeychange', this.setOptions) + this.$refs.sId.$on('onkeychange', this.setOptions) + this.$refs.sTape.$on('enter', this.setOptions) + this.$refs.sDuration.$on('enter', this.setOptions) + this.$refs.sYear.$on('enter', this.setOptions) + this.$refs.sId.$on('enter', this.setOptions) + }, + methods: { + onOpenPopup () { + this.setOptions() + }, + setOptions () { + const it = this.getFilter() + this.canSearchId = true + this.canFilterId = true + let filterIt = {} + Object.assign(filterIt, this.info) + if (it.id !== '') { + this.canFilterId = true + } + if (it.year !== '' && this.info[it.year] !== undefined) { + filterIt = {} + filterIt[it.year] = this.info[it.year] + this.canSearchId = true + } + if (it.tape !== '') { + const tmpFilter = {} + Object.assign(tmpFilter, filterIt) + const allKeys = Object.keys(filterIt) + for (let i = 0; i < allKeys.length; i++) { + if (!filterIt[allKeys[i]].tapes.includes(it.tape)) { + delete filterIt[allKeys[i]] + } + } + if (Object.keys(filterIt).length === 0) { + filterIt = tmpFilter + } + this.canSearchId = true + } + this.allYears = Object.keys(filterIt) + this.allTapes = [] + this.maxMin = 0 + // console.log('keys: ' + this.allYears) + + for (let i = 0; i < this.allYears.length; i++) { + const tapes = filterIt[this.allYears[i]].tapes + for (let i = 0; i < tapes.length; i++) { + const tape = (tapes[i]).toString() + // console.log(tape) + this.allTapes.push(tape) + } + // console.log('duration: ' + filterIt[this.allYears[i]].maxMin) + if (this.maxMin < filterIt[this.allYears[i]].maxMin) { + this.maxMin = filterIt[this.allYears[i]].maxMin + } + } + this.allTapes = [...new Set(this.allTapes)] + }, + resetValues () { + this.$refs.sYear.setValue('') + this.$refs.sTape.setValue('') + this.$refs.sId.setValue('') + this.$refs.sDuration.setValue('') + }, + setFilter (filter) { + this.$refs.sYear.setValue(filter.year) + this.$refs.sTape.setValue(filter.tape) + this.$refs.sId.setValue(filter.id) + this.$refs.sDuration.setValue(filter.duration) + }, + getFilter () { + const it = { + year: '', + id: '', + tape: '', + duration: '' + } + if (this.$refs.sYear !== undefined) { + it.year = this.$refs.sYear.getValue() + } + if (this.$refs.sTape !== undefined) { + it.tape = this.$refs.sTape.getValue() + } + if (this.$refs.sDuration !== undefined) { + it.duration = this.$refs.sDuration.getValue() + } + if (this.$refs.sId !== undefined) { + it.id = this.$refs.sId.getValue() + } + return it + }, + getDurationFromString (duration) { + const durations = { + min: 0, + max: Infinity + } + + if (duration !== '') { + if (duration.includes('<')) { + const split = duration.split('<') + if (split[0].replace(' ', '') === '') { + durations.max = parseInt(split[1]) + } else { + durations.min = parseInt(split[0]) + durations.max = parseInt(split[1]) + } + } else if (duration.includes('>')) { + const split = duration.split('>') + durations.min = parseInt(split[1]) + } + } + return durations + } + } +} +</script> + +<style scoped> +.search { + width: 100%; + border-radius: 10px; + padding: 5px; + margin-right: 20px; + margin-top: 10px; +} +</style> diff --git a/src/components/NavigationMenu.vue b/src/components/NavigationMenu.vue new file mode 100644 index 0000000000000000000000000000000000000000..c36f6827aeff8a2fe0e83cd961980afd80b556f0 --- /dev/null +++ b/src/components/NavigationMenu.vue @@ -0,0 +1,439 @@ +<template> + <div class="navigationMenu"> + <router-link :to="{ name: 'AnnualOverview'}"> + <img class="OrchiveLogo" src="../assets/Orchive.svg"> + </router-link> + <div class="navigationContainer"> + <div class="containerItem" v-for="(item, index) in this.sites" :key=index> + <button class="navigationButton" v-bind:class="{ selected: actualIndex === index }" @click="navigateTo(index)" >{{item}}</button> + </div> + <RandomOrcaPlayer + class="randomOrcaPlayer" + :is-playing="false" + :length="10" + :speed="5"> + </RandomOrcaPlayer> + <div class="sequenceCount"> + <p v-if="sequenceQueue.length > 0">{{sequenceQueue.length}} orca sequence(s) added</p> + <p v-else>No orca sequence added</p> + </div> + <div class="containerItemUserChange"> + <vs-button @click="onOpenUserUpdate" radius color="danger" type="gradient" icon="person"></vs-button> + </div> + <div class="containerItemUserChange1"> + <vs-button @click="onOpenSequencesList" radius color="primary" type="gradient" icon="list"></vs-button> + </div> + <div style="display: none" v-if="queueSettings.isLive"> + <audio autoplay v-for="sequence in sequenceQueue.filter(x => canPlay.includes(x.orca_sequence_id))" + @ended="sequenceEnded(sequence.orca_sequence_id)" + v-bind:id="sequence.orca_sequence_id" + v-on:canplay="canPlaySequence(sequence.orca_sequence_id)" + v-bind:key="sequence.orca_sequence_id"> + <source v-bind:src="sequence.url" type="audio/mp3" /> + </audio> + </div> + </div> + <vs-popup class="holamundo" title="User" :active.sync="userPopupActive"> + <SearchAutoComplete @enter="onUserChange" @onkeychange="onUserChange" :search="userName" :clear="false" class="content" :items="allUsers" ></SearchAutoComplete> + <div class="content"> + <vs-button class="popupButton" @click="onUserUpdate" color="success" type="filled">Change User</vs-button> + <vs-button class="popupButton" @click="userPopupActive=false" color="danger" type="filled">Discard</vs-button> + </div> + </vs-popup> + <vs-popup class="homeland" title="Sequences" :active.sync="sequencesListPopupActive"> + <vs-list v-for="item in sequenceQueue" v-bind:key="item.orca_sequence_id"> + {{ item.orca_sequence_id }} + </vs-list> + </vs-popup> + </div> +</template> + +<script> +import SearchAutoComplete from '../components/SearchAutocomplete' +import RandomOrcaPlayer from '@/components/RandomOrcaPlayer' +import { Bus } from '@/main' +import axios from 'axios' + +export default { + components: { + SearchAutoComplete, + RandomOrcaPlayer + }, + data () { + return { + audioContext: {}, + audioElement: {}, + stereoNode: {}, + track: {}, + userName: '', + userPopupActive: false, + sequencesListPopupActive: false, + allUsers: [], + actualIndex: 0, + isRandomPlayerPlaying: false, + sites: [ + 'Overview', + 'Search', + 'Mark sequences', + 'Find sequences', + 'Organize sequences' + ], + navigation: [ + 'AnnualOverview', + 'SearchContextView', + 'MarkSequencesView', + 'FindSequencesView', + 'OrganizeSequencesView' + ], + queueSettings: { + length: 10, + speed: [2, 10], + isLive: false + }, + mock: false, + audio: {}, + sequenceQueue: [], + canPlay: [] + } + }, + created () { + Bus.$on('set-user', () => { + this.userPopupActive = true + }) + this.$eventHub.$on('live_queue_settings', (queueSettingsTemp) => { + localStorage.isLive = queueSettingsTemp.isLive + console.log('New queue settings: ' + JSON.stringify(queueSettingsTemp)) + const elemsToDelete = this.sequenceQueue.length - queueSettingsTemp.length + // const tempSequenceQueue = [] + + if (elemsToDelete > 0) { + /* for (let i = elemsToDelete; i < this.sequenceQueue.length; i++) { + tempSequenceQueue.push(this.sequenceQueue[i]) + } */ + + this.sequenceQueue.splice(0, elemsToDelete) + + const data = [] + for (let i = 0; i < this.sequenceQueue.length; i++) { + data.push(this.sequenceQueue[i].orca_sequence_id) + } + this.$eventHub.$emit('change-queue-event', data) + } + if (!queueSettingsTemp.isLive) { + console.log('Stop audio player') + this.canPlay = [] + } else if (queueSettingsTemp.isLive && !this.queueSettings.isLive) { + console.log('Start audio player') + this.queueSettings = Object.assign({}, queueSettingsTemp) + this.startSequences() + } + // this.queueSettings = Object.create(queueSettingsTemp) + this.queueSettings = Object.assign({}, queueSettingsTemp) + }) + this.$eventHub.$on('live_add_to_queue', async (sequence) => { + console.log('Add request get') + if (this.sequenceQueue.length > this.queueSettings.length - 1) { + this.sequenceQueue.splice(0, 1) + } + // first we have to load before adding + sequence.url = await this.audioSourceForSequence(sequence) + if (this.queueSettings.isLive) { + this.canPlay.push(sequence.orca_sequence_id) + } + + this.sequenceQueue.push(sequence) + this.updateStorage() + }) + this.$eventHub.$on('live_remove_from_queue', (id) => { + this.sequenceQueue = this.sequenceQueue.filter(x => x.orca_sequence_id !== id) + + if (this.canPlay.includes(id)) { + this.canPlay.splice(this.canPlay.indexOf(id), 1) + } + + this.updateStorage() + }) + this.$eventHub.$on('live_set_queue', async (sequences) => { + this.canPlay = [] + this.sequenceQueue.length = 0 + if (sequences.length > this.queueSettings.length) { + confirm('Too much sequences selected. The current maximum size of the queue is ' + this.queueSettings.length) + return + } + for (let i = 0; i < sequences.length; i++) { + sequences[i].url = await this.audioSourceForSequence(sequences[i]) + this.sequenceQueue.push(sequences[i]) + } + this.updateStorage() + if (this.queueSettings.isLive) { + await this.startSequences() + } + }) + }, + beforeDestroy () { + Bus.$off() + this.$destroy() + }, + async mounted () { + if (this.mock) { + const sequences = this.mockSequences() + for (let i = 0; i < sequences.length; i++) { + sequences[i].url = await this.audioSourceForSequence(sequences[i]) + if (this.queueSettings.isLive) { + this.canPlay.push(sequences[i].orca_sequence_id) + } + this.sequenceQueue.push(sequences[i]) + } + } + + this.userName = localStorage.user + if (this.userName === undefined || this.userName === 'null' || this.userName === '') { + this.userPopupActive = true + } + + try { + const result = await axios.get('/api/browse/mark/orca_sequences/user_names') + this.allUsers = Object.keys(result.data) + } catch (error) { + console.log(error) + } + if (localStorage.sequenceQueue) { + localStorage.sequenceQueue = '' + } + }, + methods: { + async audioSourceForSequence (sequence) { + console.log(sequence) + let orcaPath = '/files/tapes/' + sequence.tape_year + '/' + sequence.tape_name + '/' + sequence.tape_channel + '.mp3' + + if (this.mock) { + orcaPath = require('../assets/audio/001A1.mp3') + } + const config = { url: orcaPath, method: 'get', responseType: 'blob' } + const response = await axios.request(config) + const blobAudio = response.data + const size = blobAudio.size + const duration = 48 * 60 * 1000 + const bytePerMillisecond = size / duration + const startBytes = bytePerMillisecond * sequence.timestamp_start_ms + const endBytes = bytePerMillisecond * sequence.timestamp_end_ms + const blobAudioSliced = blobAudio.slice(startBytes, endBytes) + const url = window.URL.createObjectURL(blobAudioSliced) + console.log(url) + return url + }, + async sequenceEnded (id) { + console.log('ended ' + id) + this.canPlay.splice(this.canPlay.indexOf(id), 1) + console.log('removed from can play: ' + this.canPlay) + + if (this.sequenceQueue.filter(sequence => sequence.orca_sequence_id === id).length > 0) { + await this.delay(this.getRandomValue(this.queueSettings.speed)) + this.canPlay.push(id) + console.log('added to can play: ' + this.canPlay) + } + + for (let i = 0; i < this.canPlay.length; i++) { + if (this.sequenceQueue.filter(sequence => sequence.orca_sequence_id === this.canPlay[i]).length <= 0) { + this.canPlay.splice(i, 1) + } + } + console.log('actual can play: ' + this.canPlay) + }, + onUserChange (name) { + this.userName = name + }, + navigateTo (viewId) { + this.$router.push({ + name: this.navigation[viewId] + }).catch((error) => { + console.log(error) + }) + }, + onOpenUserUpdate () { + this.userPopupActive = true + }, + onOpenSequencesList () { + this.sequencesListPopupActive = true + }, + onUserUpdate () { + if (this.userName === '') { + return + } + this.userPopupActive = false + localStorage.user = this.userName + Bus.$emit('user-set') + }, + updateStorage () { + const data = [] + for (let i = 0; i < this.sequenceQueue.length; i++) { + data.push(this.sequenceQueue[i].orca_sequence_id) + } + localStorage.sequenceQueue = data + console.log('Local storage updated:' + JSON.stringify(localStorage.sequenceQueue)) + this.$eventHub.$emit('change-queue-event', data) + }, + mockSequences () { + return [ + { + orca_sequence_id: '0', + timestamp_start_ms: 100000, + timestamp_end_ms: 102000 + }, + { + orca_sequence_id: '1', + timestamp_start_ms: 102000, + timestamp_end_ms: 104000 + }, + { + orca_sequence_id: '2', + timestamp_start_ms: 106000, + timestamp_end_ms: 108000 + }, + { + orca_sequence_id: '3', + timestamp_start_ms: 38000, + timestamp_end_ms: 40000 + }, + { + orca_sequence_id: '4', + timestamp_start_ms: 200000, + timestamp_end_ms: 210000 + } + ] + }, + async startSequences () { + const times = {} + for (let i = 0; i < this.sequenceQueue.length; i++) { + times[this.sequenceQueue[i].orca_sequence_id] = this.getRandomValue(this.queueSettings.speed) + } + + for (let i = 0; i < this.queueSettings.speed[1]; i++) { + for (let j = 0; j < this.sequenceQueue.length; j++) { + if (times[this.sequenceQueue[j].orca_sequence_id] === i) { + this.canPlay.push(this.sequenceQueue[j].orca_sequence_id) + } + } + if (!this.queueSettings.isLive) { + return + } + await this.delay(1) + if (!this.queueSettings.isLive) { + return + } + } + }, + getRandomValue (range) { + return Math.floor(Math.random() * (range[1] - range[0] + 1) + range[0]) + }, + async delay (n) { + return new Promise(function (resolve) { + setTimeout(resolve, n * 1000) + }) + }, + canPlaySequence (id) { + console.log('can play adjusted') + + if (this.audioContext[id] !== undefined) { + return + } + const audio = document.getElementById(id) + console.log(audio) + if (audio === undefined || audio === null) { + return + } + this.audioContext[id] = new AudioContext() + this.track[id] = this.audioContext[id].createMediaElementSource(audio) + this.stereoNode[id] = new StereoPannerNode(this.audioContext[id], { pan: -1 }) + this.track[id].connect(this.stereoNode[id]).connect(this.audioContext[id].destination) + + // remove other keys + const keys = Object.keys(this.track) + for (let i = 0; i < keys.length; i++) { + this.audioContext[keys[i]] = undefined + this.track[keys[i]] = undefined + this.stereoNode[keys[i]] = undefined + } + } + }, + watch: { + $route (to, from) { + const index = this.navigation.indexOf(to.name) + if (index === -1) { + this.actualIndex = 0 + } else { + this.actualIndex = index + } + } + } +} +</script> + +<style lang="scss"> +@import '@/assets/color.scss'; + +.OrchiveLogo{ + width: 65%; + margin-bottom: 1.5em; +} + +::placeholder{ + color: white; + font-size: 15pt; +} + +.navigationContainer{ + margin-right: 30px; +} + +.navigationButton{ + color: white; + background-color: rgba(38, 106, 133, 0.49); + border: 1px solid $dark-blue; + border-radius: 12px; + width: 100%; + font-size: 12pt; + margin-top: 5px; + margin-bottom: 5px; + outline: 0; + box-sizing: border-box; +} + +.selected{ + background-color: #266A85; +} + +.containerItemUserChange { + position: absolute; + bottom: 10px; +} + +.containerItemUserChange1 { + position: absolute; + bottom: 10px; + left: 80px; +} + +.popupButton { + margin: 5px; +} + +.content { + margin-left: 5px; + margin-right: 5px; + padding-top: 10px; + padding-bottom: 10px; + margin-top: 5px; +} + +.sequenceCount { + font-size: 0.8em; + padding-top: 20px; +} + +.test { + align-items: center; + font-size: 1.2em; +} + +</style> diff --git a/src/components/RandomOrcaPlayer.vue b/src/components/RandomOrcaPlayer.vue new file mode 100644 index 0000000000000000000000000000000000000000..51973731520c72105d50229b66151e1647267cb0 --- /dev/null +++ b/src/components/RandomOrcaPlayer.vue @@ -0,0 +1,136 @@ +<template> + <div class="randomPlayerContainer"> + <div class="titlePlayer">Orca Player</div> + <div class="randomPlayerItem"> + <div class="playOrcaRandomPlayer" @click="playRandomPlayer"> + <img class="playIconRandomPlayer" v-if="!isPlayerPlaying" src="../assets/play-circle-solid.svg" alt=""> + <img class="pauseIconRandomPlayer" v-else src="../assets/pause-circle-solid.svg" alt=""> + </div> + </div> + <div class="titleSlider">Choose range of delay between noise:</div> + <vue-slider + class="sliderSpeed" + ref="sliderSpeed" + v-model="valueRange" + :enable-cross="false" + :min="2" + :max="60" + :min-range="3" + :max-range="58" + :interval="1" + :tooltip-placement="'bottom'" + @drag-end="this.onSliderSpeedChanged"> + </vue-slider> + <div class="titleSlider">Choose length of queue:</div> + <vue-slider + class="sliderAmount" + ref="sliderAmount" + v-model="valueSliderAmount" + :min="2" + :max="15" + :interval="1" + :tooltip-placement="'bottom'" + @drag-end="this.onSliderAmountChanged"> + </vue-slider> + </div> +</template> + +<script> +import VueSlider from 'vue-slider-component' +import 'vue-slider-component/theme/default.css' +import { Bus } from '@/main' + +export default { + components: { + VueSlider + }, + name: 'RandomOrcaPlayer', + props: { + length: Number, + speed: Number, + isPlaying: Boolean + }, + data () { + return { + valueRange: [2, 10], + valueSliderSpeed: 1, + valueSliderAmount: 2, + isPlayerPlaying: false, + queueSettings: { + length: 10, + speed: [2, 10], + isLive: false + } + } + }, + mounted () { + this.queueSettings.isLive = this.isPlaying + this.queueSettings.length = this.length + this.$refs.sliderAmount.setValue(this.queueSettings.length) + this.$eventHub.$emit('live_queue_settings', this.queueSettings) + }, + destroyed () { + this.$destroy() + Bus.$off() + }, + methods: { + onSliderSpeedChanged (index) { + this.queueSettings.speed = this.$refs.sliderSpeed.getValue() + this.$eventHub.$emit('live_queue_settings', this.queueSettings) + }, + onSliderAmountChanged (index) { + this.queueSettings.length = this.$refs.sliderAmount.getValue() + this.$eventHub.$emit('live_queue_settings', this.queueSettings) + }, + playRandomPlayer () { + this.isPlayerPlaying = !this.isPlayerPlaying + this.queueSettings.isLive = this.isPlayerPlaying + this.$eventHub.$emit('live_queue_settings', this.queueSettings) + } + } +} +</script> + +<style lang="scss"> +@import '@/assets/color.scss'; + +.playIconRandomPlayer{ + width: 100px; + height: 100px; + filter: $filter-dark-blue; + display: block; + margin-top: 10px; + margin-left: auto; + margin-right: auto; +} + +.pauseIconRandomPlayer{ + width: 100px; + height: 100px; + filter: $filter-light-blue; + cursor: pointer; + display: block; + margin-top: 10px; + margin-left: auto; + margin-right: auto; +} + +.sliderSpeed { + margin-top: 10px; +} + +.sliderAmount { + margin-top: 10px; +} + +.titleSlider { + font-size: 0.8em; + padding-top: 10px; +} + +.titlePlayer { + font-size: 1.2em; + margin-top: 10px; + text-align: center; +} +</style> diff --git a/src/components/SearchAutocomplete.vue b/src/components/SearchAutocomplete.vue new file mode 100644 index 0000000000000000000000000000000000000000..37c93f297f921c053719ad73692b731ffa794945 --- /dev/null +++ b/src/components/SearchAutocomplete.vue @@ -0,0 +1,190 @@ +<template> + <div class="autocomplete"> + <input + type="text" + class="search" + autocomplete="off" + @input="onChange" + v-model="search" + @keyup="onChangeText" + @keydown.down="onArrowDown" + @keydown.up="onArrowUp" + @keydown.enter="onEnter" + /> + <ul + id="autocomplete-results" + v-show="isOpen && suggest" + class="autocomplete-results" + > + <li + class="loading" + v-if="isLoading" + > + Loading results... + </li> + <li + v-else + v-for="(result, i) in results" + :key="i" + @click="setResult(result)" + class="autocomplete-result" + :class="{ 'is-active': i === arrowCounter }" + > + {{ result }} + </li> + </ul> + </div> +</template> + +<script> +export default { + name: 'SearchAutocomplete', + props: { + search: { + type: String, + required: false, + default: () => '' + }, + clear: { + type: Boolean, + required: false, + default: () => true + }, + items: { + type: Array, + required: false, + default: () => [] + }, + isAsync: { + type: Boolean, + required: false, + default: false + }, + suggest: { + type: Boolean, + required: false, + default: true + } + }, + data () { + return { + isOpen: false, + results: [], + isLoading: false, + arrowCounter: -1 + } + }, + watch: { + items: function (value, oldValue) { + if (value.length !== oldValue.length) { + this.results = value + this.isLoading = false + } + } + }, + mounted () { + document.addEventListener('click', this.handleClickOutside) + }, + destroyed () { + document.removeEventListener('click', this.handleClickOutside) + }, + methods: { + setValue (value) { + this.search = value + }, + getValue () { + return this.search + }, + onChangeText () { + this.$emit('onkeychange', this.search) + }, + setResult (result) { + this.$emit('enter', result) + if (this.clear) { + this.search = '' + } else { + this.search = result + } + this.isOpen = false + }, + filterResults () { + this.results = this.items.filter((item) => { + return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1 + }) + }, + onChange () { + if (this.isAsync) { + this.isLoading = true + } else { + this.filterResults() + this.isOpen = true + } + }, + handleClickOutside (event) { + if (!this.$el.contains(event.target)) { + this.isOpen = false + this.arrowCounter = -1 + } + }, + onArrowDown () { + if (this.arrowCounter < this.results.length) { + this.arrowCounter = this.arrowCounter + 1 + } + }, + onArrowUp () { + if (this.arrowCounter > 0) { + this.arrowCounter = this.arrowCounter - 1 + } + }, + onEnter () { + if (this.arrowCounter === -1) { + this.$emit('enter', this.search) + } else { + this.$emit('enter', this.results[this.arrowCounter]) + } + if (this.clear) { + this.search = '' + } else if (this.arrowCounter === -1) { + } else { + this.search = this.results[this.arrowCounter] + } + this.isOpen = false + this.arrowCounter = -1 + } + } +} +</script> + +<style> +.autocomplete { + position: relative; +} + +.autocomplete-results { + padding: 0; + margin: 0; + border: 1px solid #eeeeee; + height: 120px; + overflow: auto; + border-radius: 10px; +} + +.autocomplete-result { + list-style: none; + text-align: left; + padding: 5px; + cursor: pointer; +} + +.autocomplete-result.is-active, +.autocomplete-result:hover { + background-color: #4AAE9B; + color: white; +} + +.search { + width: 100%; + border-radius: 10px; + padding: 5px; +} +</style> diff --git a/src/components/SearchBar.vue b/src/components/SearchBar.vue index d1b62bc8c10fa30aae0508d4f0049c6364cb3052..5a31962c6b251f5891545186a5bf73b4c65c8461 100644 --- a/src/components/SearchBar.vue +++ b/src/components/SearchBar.vue @@ -3,8 +3,7 @@ <router-link :to="{ name: 'AnnualOverview'}" @click.native="deleteQuery"> <img class="OrchiveLogo" src="../assets/Orchive.svg"> </router-link> - - <div class="searchContainer"> + <div class="searchContainer"> <div class="searchHeader"> <h2 class="searchH1">Search</h2> <router-link @click.native="deleteQuery" :to="{ name: 'AnnualOverview'}" > diff --git a/src/components/SegmentHeader.vue b/src/components/SegmentHeader.vue new file mode 100644 index 0000000000000000000000000000000000000000..603b6938d320b133d683acbe67ec8f0546b93efa --- /dev/null +++ b/src/components/SegmentHeader.vue @@ -0,0 +1,172 @@ +<template> + <div class="detailHeader"> + <div class="change"> + <div class="PDFIconContainer"> + <img class="detailPDFIcon" src="@/assets/tape.svg"> + </div> + <div class="selectPreviousOrNextLabbook"> + <div style="cursor: pointer;" class="previous" v-if="this.actualIndex > 0" @click="changeSegment(-1)"> < </div> + <div class="noPrevious" v-else> < </div> + <p class="next"> + {{actualIndex + 1}} / {{segments_count}} Segment + </p> + <div style="cursor: pointer;" class="next" v-if="this.actualIndex < this.segments_count - 1" @click="changeSegment(1)"> > </div> + <div class="noNext" v-else> > </div> + </div> + </div> + <div class="infoLabbook"> + <h1> {{segment_id}} </h1> + <p class="labbookDate"> Orca probability: {{segment_probability}} </p> + <p class="labbookDate"> Already showed: {{segment_assessed}} </p> + </div> + <div class="infoLabbook"> + <router-link + :to="{ + name: 'DetailView', + params: { + year: this.tape_year, + type: 'Tape', + name: this.tape_name + }, + }" + > + <h1> {{tape_name}} </h1> + </router-link> + <p class="labbookDate"> {{tape_year}} </p> + </div> + </div> +</template> + +<script> +import { Bus } from '@/main' + +export default { + name: 'SegmentHeader', + props: { + segments_count: { + type: Number, + required: true + }, + tape_name: { + type: String, + required: true + }, + tape_year: { + type: Number, + required: true + }, + segment_id: { + type: String + }, + segment_probability: { + type: String, + required: true + }, + segment_assessed: { + type: Boolean, + required: true + } + }, + data () { + return { + actualIndex: 0 + } + }, + mounted () { + }, + methods: { + changeSegment (direction) { + this.actualIndex = this.actualIndex + direction + Bus.$emit('segment_index_changed', this.actualIndex) + }, + resetIndex () { + this.actualIndex = 0 + Bus.$emit('segment_index_changed', this.actualIndex) + } + } +} +</script> + +<style lang="scss"> +@import '@/assets/color.scss'; + +.detailHeader { + display: flex; + justify-content: space-between; +} + +.detailHeader button { + cursor: pointer; + border: none; + background: none; + color: $dark-blue; + font-weight: bold; +} + +.changeLabbook { + display: flex; + flex-direction: column; +} + +.change { + padding-top: 18px; +} + +.detailPDFIcon { + filter: invert(36%) sepia(5%) saturate(5710%) hue-rotate(152deg) brightness(94%) contrast(84%); + display: block; + margin-left: auto; + margin-right: auto; + width: 35px; +} + +.selectPreviousOrNextLabbook { + display: flex; + color: $dark-blue; + font-weight: bold; + margin-top: -6px; +} + +.detailHeader h1 { + text-align: right; + margin-bottom: 6px; + margin-top: 8px; +} + +.labbookDate { + margin-top: 0px; + text-align: right; +} + +.previous { + margin-top: 16px; + margin-bottom: 16px; + margin-right: 5px; + text-decoration: none; + color: $dark-blue; +} + +.noPrevious { + margin-top: 16px; + margin-bottom: 16px; + margin-right: 5px; + text-decoration: none; + color: $light-grey-2; +} + +.next { + margin-top: 16px; + margin-bottom: 16px; + margin-left: 5px; + text-decoration: none; + color: $dark-blue; +} + +.noNext { + margin-top: 16px; + margin-bottom: 16px; + margin-left: 5px; + text-decoration: none; + color: $light-grey-2; +} +</style> diff --git a/src/components/SequenceHeader.vue b/src/components/SequenceHeader.vue new file mode 100644 index 0000000000000000000000000000000000000000..eacccf885b6e0eaa522c6c180e82761902bc1f2b --- /dev/null +++ b/src/components/SequenceHeader.vue @@ -0,0 +1,171 @@ +<template> + <div class="detailHeader"> + <div class="sequenceInfo"> + <h1> {{orca_sequence_id}} </h1> + <p class="detailedInfo"> Timestamp: {{Math.trunc(timestamp_start/60000)}}min {{((timestamp_start % 60000)/1000).toFixed(0)}}s - {{Math.trunc(timestamp_end/60000)}}min {{((timestamp_end % 60000)/1000).toFixed(0)}}s </p> + <p v-if="rating > 0" class="detailedInfo"><StarRating :read-only="true" :star-size="20" :rating="rating" :show-rating="false"/></p> + </div> + <div class="infoLabbook"> + <router-link + :to="{ + name: 'DetailView', + params: { + year: this.tape_year, + type: 'Tape', + name: this.tape_name + }, + }" + > + <h1> {{tape_name}} </h1> + </router-link> + <p class="labbookDate"> {{tape_year}} </p> + </div> + </div> +</template> + +<script> +import { Bus } from '@/main' +import StarRating from '../components/StarRating.vue' + +export default { + name: 'SequenceHeader', + components: { + StarRating + }, + props: { + orca_sequence_id: { + type: String, + required: true + }, + tape_name: { + type: String, + required: true + }, + tape_year: { + type: Number, + required: true + }, + timestamp_start: { + type: Number, + required: true + }, + timestamp_end: { + type: Number, + required: true + }, + rating: { + type: Number, + required: true + } + }, + data () { + return { + actualIndex: 0 + } + }, + mounted () { + }, + methods: { + changeSegment (direction) { + this.actualIndex = this.actualIndex + direction + Bus.$emit('segment_index_changed', this.actualIndex) + } + } +} +</script> + +<style lang="scss"> +@import '@/assets/color.scss'; + +.detailHeader { + display: flex; + justify-content: space-between; +} + +.detailHeader button { + cursor: pointer; + border: none; + background: none; + color: $dark-blue; + font-weight: bold; +} + +.changeLabbook { + display: flex; + flex-direction: column; +} + +.change { + padding-top: 18px; +} + +.detailPDFIcon { + filter: invert(36%) sepia(5%) saturate(5710%) hue-rotate(152deg) brightness(94%) contrast(84%); + display: block; + margin-left: auto; + margin-right: auto; + width: 35px; +} + +.selectPreviousOrNextLabbook { + display: flex; + color: $dark-blue; + font-weight: bold; + margin-top: -6px; +} + +.detailHeader h1 { + text-align: right; + margin-bottom: 6px; + margin-top: 8px; +} + +.labbookDate { + margin-top: 0px; + text-align: right; +} + +.sequenceInfo { + margin-top: 0px; + text-align: left; + margin-right: 100px; +} + +.detailedInfo { + margin-top: 0px; + text-align:left; + alignment: left; +} + +.previous { + margin-top: 16px; + margin-bottom: 16px; + margin-right: 5px; + text-decoration: none; + color: $dark-blue; +} + +.noPrevious { + margin-top: 16px; + margin-bottom: 16px; + margin-right: 5px; + text-decoration: none; + color: $light-grey-2; +} + +.next { + margin-top: 16px; + margin-bottom: 16px; + margin-left: 5px; + text-decoration: none; + color: $dark-blue; +} + +.noNext { + margin-top: 16px; + margin-bottom: 16px; + margin-left: 5px; + text-decoration: none; + color: $light-grey-2; +} +</style> diff --git a/src/components/Star.vue b/src/components/Star.vue new file mode 100644 index 0000000000000000000000000000000000000000..d206bdcdaddd279f5b57c48830e8e999c4ecd031 --- /dev/null +++ b/src/components/Star.vue @@ -0,0 +1,259 @@ +<template> + <svg + :class="['vue-star-rating-star', {'vue-star-rating-star-rotate' : shouldAnimate}]" + :height="starSize" + :width="starSize" + :viewBox="viewBox" + @mousemove="mouseMoving" + @click="selected" + @touchstart="touchStart" + @touchend="touchEnd" + > + + <linearGradient + :id="grad" + x1="0" + x2="100%" + y1="0" + y2="0" + > + <stop + :offset="starFill" + :stop-color="(rtl) ? getColor(inactiveColor) : getColor(activeColor)" + :stop-opacity="(rtl) ? getOpacity(inactiveColor) : getOpacity(activeColor)" + /> + <stop + :offset="starFill" + :stop-color="(rtl) ? getColor(activeColor) : getColor(inactiveColor)" + :stop-opacity="(rtl) ? getOpacity(activeColor) : getOpacity(inactiveColor)" + /> + </linearGradient> + + <filter + :id="glowId" + height="130%" + width="130%" + filterUnits="userSpaceOnUse" + > + <feGaussianBlur + :stdDeviation="glow" + result="coloredBlur" + /> + <feMerge> + <feMergeNode in="coloredBlur" /> + <feMergeNode in="SourceGraphic" /> + </feMerge> + </filter> + + <polygon + v-show="glowColor && glow > 0 && fill > 0" + :points="starPointsToString" + :fill="gradId" + :stroke="glowColor" + :filter="'url(#'+glowId+')'" + :stroke-width="border" + /> + + <polygon + :points="starPointsToString" + :fill="gradId" + :stroke="getBorderColor" + :stroke-width="border" + :stroke-linejoin="strokeLinejoin" + /> + <polygon + :points="starPointsToString" + :fill="gradId" + /> + </svg> +</template> + +<script type="text/javascript"> +import AlphaColor from '../classes/AlphaColor' + +export default { + name: 'Star', + props: { + fill: { + type: Number, + default: 0 + }, + points: { + type: Array, + default () { + return [] + } + }, + size: { + type: Number, + default: 50 + }, + starId: { + type: Number, + required: true + }, + activeColor: { + type: String, + required: true + }, + inactiveColor: { + type: String, + required: true + }, + borderColor: { + type: String, + default: '#000' + }, + activeBorderColor: { + type: String, + default: '#000' + }, + borderWidth: { + type: Number, + default: 0 + }, + roundedCorners: { + type: Boolean, + default: false + }, + rtl: { + type: Boolean, + default: false + }, + glow: { + type: Number, + default: 0 + }, + glowColor: { + type: String, + default: null, + required: false + }, + animate: { + type: Boolean, + default: false + } + }, + emits: ['star-mouse-move', 'star-selected'], + data () { + return { + starPoints: [19.8, 2.2, 6.6, 43.56, 39.6, 17.16, 0, 17.16, 33, 43.56], + grad: '', + glowId: '', + isStarActive: true + } + }, + computed: { + starPointsToString () { + return this.starPoints.join(',') + }, + gradId () { + return 'url(#' + this.grad + ')' + }, + starSize () { + // Adjust star size when rounded corners are set with no border, to account for the 'hidden' border + const size = (this.roundedCorners && this.borderWidth <= 0) ? parseInt(this.size) - parseInt(this.border) : this.size + return parseInt(size) + parseInt(this.border) + }, + starFill () { + return (this.rtl) ? 100 - this.fill + '%' : this.fill + '%' + }, + border () { + return (this.roundedCorners && this.borderWidth <= 0) ? 6 : this.borderWidth + }, + getBorderColor () { + if (this.roundedCorners && this.borderWidth <= 0) { + // create a hidden border + return (this.fill <= 0) ? this.inactiveColor : this.activeColor + } + + return (this.fill <= 0) ? this.borderColor : this.activeBorderColor + }, + maxSize () { + return this.starPoints.reduce(function (a, b) { + return Math.max(a, b) + }) + }, + viewBox () { + return '0 0 ' + this.maxSize + ' ' + this.maxSize + }, + shouldAnimate () { + return this.animate && this.isStarActive + }, + strokeLinejoin () { + return this.roundedCorners ? 'round' : 'miter' + } + }, + created () { + this.starPoints = (this.points.length) ? this.points : this.starPoints + this.calculatePoints() + this.grad = this.getRandomId() + this.glowId = this.getRandomId() + }, + methods: { + mouseMoving ($event) { + if ($event.touchAction !== 'undefined') { + this.$emit('star-mouse-move', { + event: $event, + position: this.getPosition($event), + id: this.starId + }) + } + }, + touchStart () { + this.$nextTick(() => { + this.isStarActive = true + }) + }, + touchEnd () { + this.$nextTick(() => { + this.isStarActive = false + }) + }, + getPosition ($event) { + // calculate position in percentage. + const starWidth = (92 / 100) * this.size + const offset = (this.rtl) ? Math.min($event.offsetX, 45) : Math.max($event.offsetX, 1) + const position = Math.round((100 / starWidth) * offset) + + return Math.min(position, 100) + }, + selected ($event) { + this.$emit('star-selected', { + id: this.starId, + position: this.getPosition($event) + }) + }, + getRandomId () { + return Math.random().toString(36).substring(7) + }, + calculatePoints () { + this.starPoints = this.starPoints.map((point, i) => { + const offset = i % 2 === 0 ? this.border * 1.5 : 0 + return ((this.size / this.maxSize) * point) + offset + }) + }, + getColor (color) { + return new AlphaColor(color).parseAlphaColor().color + }, + getOpacity (color) { + return new AlphaColor(color).parseAlphaColor().opacity + } + } +} +</script> + +<style scoped> +.vue-star-rating-star { + overflow: visible !important; +} + +.vue-star-rating-star-rotate { + transition: all .25s; +} + +.vue-star-rating-star-rotate:hover { + transition: transform 0.25s; + transform: rotate(-15deg) scale(1.3) +} +</style> diff --git a/src/components/StarRating.vue b/src/components/StarRating.vue new file mode 100644 index 0000000000000000000000000000000000000000..c1c43282ae11140f627cb191d6650002933acee7 --- /dev/null +++ b/src/components/StarRating.vue @@ -0,0 +1,309 @@ +<template> + <div :class="['vue-star-rating', {'vue-star-rating-rtl':rtl}, {'vue-star-rating-inline': inline}]"> + <div class="sr-only"> + <slot + name="screen-reader" + :rating="selectedRating" + :stars="maxRating" + > + <span>Rated {{ selectedRating }} stars out of {{ maxRating }}</span> + </slot> + </div> + + <div + class="vue-star-rating" + @mouseleave="resetRating" + > + <span + v-for="n in maxRating" + :key="n" + :class="[{'vue-star-rating-pointer': !readOnly }, 'vue-star-rating-star']" + :style="{'margin-right': margin + 'px'}" + > + <star + :fill="fillLevel[n-1]" + :size="starSize" + :points="starPoints" + :star-id="n" + :step="step" + :active-color="currentActiveColor" + :inactive-color="inactiveColor" + :border-color="borderColor" + :active-border-color="currentActiveBorderColor" + :border-width="borderWidth" + :rounded-corners="roundedCorners" + :rtl="rtl" + :glow="glow" + :glow-color="glowColor" + :animate="animate" + @star-selected="setRating($event, true)" + @star-mouse-move="setRating" + /> + </span> + <span + v-if="showRating" + :class="['vue-star-rating-rating-text', textClass]" + > {{ formattedRating }}</span> + </div> + </div> +</template> +<script type="text/javascript"> +import Star from '../components/Star' + +export default { + + name: 'VueStarRating', + components: { + Star + }, + props: { + rowId: { + type: String, + default: '' + }, + increment: { + type: Number, + default: 1 + }, + rating: { + type: Number, + default: 0 + }, + roundStartRating: { + type: Boolean, + default: true + }, + activeColor: { + type: [String, Array], + default: '#ffd055' + }, + inactiveColor: { + type: String, + default: '#d8d8d8' + }, + maxRating: { + type: Number, + default: 5 + }, + starPoints: { + type: Array, + default () { + return [] + } + }, + starSize: { + type: Number, + default: 50 + }, + showRating: { + type: Boolean, + default: true + }, + readOnly: { + type: Boolean, + default: false + }, + textClass: { + type: String, + default: '' + }, + inline: { + type: Boolean, + default: false + }, + borderColor: { + type: String, + default: '#999' + }, + activeBorderColor: { + type: [String, Array], + default: null + }, + borderWidth: { + type: Number, + default: 0 + }, + roundedCorners: { + type: Boolean, + default: false + }, + padding: { + type: Number, + default: 0 + }, + rtl: { + type: Boolean, + default: false + }, + fixedPoints: { + type: Number, + default: null + }, + glow: { + type: Number, + default: 0 + }, + glowColor: { + type: String, + default: '#fff' + }, + clearable: { + type: Boolean, + default: false + }, + activeOnClick: { + type: Boolean, + default: false + }, + animate: { + type: Boolean, + default: false + } + }, + emits: ['update-rating', 'hover-rating'], + + data () { + return { + step: 0, + fillLevel: [], + currentRating: 0, + selectedRating: 0, + ratingSelected: false + } + }, + computed: { + formattedRating () { + return (this.fixedPoints === null) ? this.currentRating : this.currentRating.toFixed(this.fixedPoints) + }, + shouldRound () { + return this.ratingSelected || this.roundStartRating + }, + margin () { + return this.padding + this.borderWidth + }, + activeColors () { + if (Array.isArray(this.activeColor)) { + return this.padColors(this.activeColor, this.maxRating, this.activeColor.slice(-1)[0]) + } + + return new Array(this.maxRating).fill(this.activeColor) + }, + currentActiveColor () { + if (!this.activeOnClick) { + return (this.currentRating > 0) ? this.activeColors[Math.ceil(this.currentRating) - 1] : this.inactiveColor + } + return (this.selectedRating > 0) ? this.activeColors[Math.ceil(this.selectedRating) - 1] : this.inactiveColor + }, + activeBorderColors () { + if (Array.isArray(this.activeBorderColor)) { + return this.padColors(this.activeBorderColor, this.maxRating, this.activeBorderColor.slice(-1)[0]) + } + const borderColor = (this.activeBorderColor) ? this.activeBorderColor : this.borderColor + return new Array(this.maxRating).fill(borderColor) + }, + currentActiveBorderColor () { + if (!this.activeOnClick) { + return (this.currentRating > 0) ? this.activeBorderColors[Math.ceil(this.currentRating) - 1] : this.borderColor + } + return (this.selectedRating > 0) ? this.activeBorderColors[Math.ceil(this.selectedRating) - 1] : this.borderColor + }, + roundedRating () { + const inv = 1.0 / this.increment + return Math.min(this.maxRating, Math.ceil(this.currentRating * inv) / inv) + } + }, + watch: { + rating (val) { + this.currentRating = val + this.selectedRating = val + this.createStars(this.shouldRound) + } + }, + created () { + this.step = this.increment * 100 + this.currentRating = this.rating + this.selectedRating = this.currentRating + this.createStars(this.roundStartRating) + }, + methods: { + setRating ($event, persist) { + if (!this.readOnly) { + const position = (this.rtl) ? (100 - $event.position) / 100 : $event.position / 100 + this.currentRating = (($event.id + position) - 1).toFixed(2) + this.currentRating = (this.currentRating > this.maxRating) ? this.maxRating : this.currentRating + if (persist) { + this.createStars(true, true) + this.selectedRating = (this.clearable && this.currentRating === this.selectedRating) ? 0 : this.currentRating + this.$emit('update-rating', this.selectedRating, this.rowId) + this.ratingSelected = true + } else { + this.createStars(true, !this.activeOnClick) + this.$emit('hover-rating', this.selectedRating, this.rowId) + } + } + }, + resetRating () { + if (!this.readOnly) { + this.currentRating = this.selectedRating + this.createStars(this.shouldRound) + } + }, + createStars (round = true, applyFill = true) { + this.currentRating = (round) ? this.roundedRating : this.currentRating + for (let i = 0; i < this.maxRating; i++) { + let level = 0 + if (i < this.currentRating) { + level = (this.currentRating - i > 1) ? 100 : (this.currentRating - i) * 100 + } + if (applyFill) { + this.fillLevel[i] = Math.round(level) + } + } + }, + padColors (array, minLength, fillValue) { + return Object.assign(new Array(minLength).fill(fillValue), array) + } + } +} +</script> +<style scoped> +.vue-star-rating-star { + display: inline-block; + -webkit-tap-highlight-color: transparent; +} + +.vue-star-rating-pointer { + cursor: pointer; +} + +.vue-star-rating { + display: flex; + align-items: center; +} + +.vue-star-rating-inline { + display: inline-flex; +} + +.vue-star-rating-rating-text { + margin-left: 7px; +} + +.vue-star-rating-rtl { + direction: rtl; +} + +.vue-star-rating-rtl .vue-star-rating-rating-text { + margin-right: 10px; + direction: rtl; +} + +.sr-only { + position: absolute; + left: -10000px; + top: auto; + width: 1px; + height: 1px; + overflow: hidden; +} +</style> diff --git a/src/main.js b/src/main.js index 37ed7008d24bb57a4588b8f1cd1e40dc390a2a62..87f8a5674cf7ebaf7c1c100ea8fb459a7bf2ae15 100644 --- a/src/main.js +++ b/src/main.js @@ -2,12 +2,20 @@ import Vue from 'vue' import App from './App.vue' import router from './router' import WaveSurferVue from 'wavesurfer.js-vue' +import VueGoodTable from 'vue-good-table' +import 'vue-good-table/dist/vue-good-table.css' +import 'vuesax/dist/vuesax.css' +import Vuesax from 'vuesax' +import 'material-icons/iconfont/material-icons.css' Vue.config.productionTip = false - +Vue.config.devtools = true export const Bus = new Vue() +Vue.prototype.$eventHub = new Vue() Vue.use(WaveSurferVue) +Vue.use(VueGoodTable) +Vue.use(Vuesax) new Vue({ router, diff --git a/src/router/index.js b/src/router/index.js index 38bedb032360ec7f43dedbb2c770b67787dcb5e9..348e175393092f6d919e9ab2e62ce22fccbbd769 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -31,6 +31,21 @@ const routes = [ props: route => ({ query: route.query }) }, + { + path: '/mark', + name: 'MarkSequencesView', + component: () => import('../views/MarkSequencesView.vue') + }, + { + path: '/find', + name: 'FindSequencesView', + component: () => import('../views/FindSequencesView.vue') + }, + { + path: '/organize', + name: 'OrganizeSequencesView', + component: () => import('../views/OrganizeSequencesView.vue') + }, { path: '/:year', name: 'MonthlyOverview', diff --git a/src/views/DetailView.vue b/src/views/DetailView.vue index fc7675fcbd0950aaf5e87362e36fa4319f1c5cc0..2d1e58d7671b4129e105f9e03750437a703597cf 100644 --- a/src/views/DetailView.vue +++ b/src/views/DetailView.vue @@ -110,6 +110,11 @@ export default { this.tapeName = this.tapeNames[this.audioNo - 1] }, created () { + Bus.$on('goToTape', (tapeName, year, type) => { + this.name = tapeName + this.year = year + this.type = type + }) Bus.$on('sendPreviousTape', (tapeName, year, type) => { this.name = tapeName this.year = year diff --git a/src/views/ErrorView.vue b/src/views/ErrorView.vue index 9e20ec79fe6de65ca86fd70fda9940ba00c8b0f7..96320332982612de0b2ffe665bb5ac5e84231b6b 100644 --- a/src/views/ErrorView.vue +++ b/src/views/ErrorView.vue @@ -43,6 +43,7 @@ } </style> <script> + export default { name: 'ErrorView', props: { diff --git a/src/views/FindSequencesView.vue b/src/views/FindSequencesView.vue new file mode 100644 index 0000000000000000000000000000000000000000..c2768bc0b047f36e8d48bda5c0d9872bba134755 --- /dev/null +++ b/src/views/FindSequencesView.vue @@ -0,0 +1,535 @@ +<template> + <div class="findSequences"> + <div v-if="this.sequence_count <= 0"> + <p1> No marked sequences found </p1> + </div> + <div v-if="this.sequence_count > 0" class="sequenceHeader"> + <div class="changeSequence"> + <div class="TapeIconContainer"> + <img class="tapeIcon" src="@/assets/tape.svg"> + </div> + <div class="selectPreviousOrNextSequence"> + <div style="cursor: pointer;" class="previous" v-if="this.actual_sequence_index > 0" @click="changeSequence(-1)"> < </div> + <div class="noPrevious" v-else> < </div> + <p class="next"> + {{actual_sequence_index + 1}} / {{sequence_count}} Sequence + </p> + <div style="cursor: pointer;" class="next" v-if="this.actual_sequence_index < this.sequence_count - 1" @click="changeSequence(1)"> > </div> + <div class="noNext" v-else> > </div> + </div> + </div> + <SequenceHeader + ref="sequence_header_1" + :orca_sequence_id="this.actual_sequence.orca_sequence_id" + :tape_name="this.actual_sequence.tape_name" + :tape_year="this.actual_sequence.tape_year" + :timestamp_start="this.actual_sequence.timestamp_start_ms" + :timestamp_end="this.actual_sequence.timestamp_end_ms" + :rating="this.actual_sequence.rating"> + </SequenceHeader> + </div> + <AudioAndFrequencyPlayer + v-if="this.sequence_count > 0" + class="expand-width-first" + :is_editable="false" + :tape_name="this.actual_sequence.tape_name" + :tape_year="this.actual_sequence.tape_year" + :tape_channel="this.actual_sequence.tape_channel" + :timestamp_start_ms="this.actual_sequence.timestamp_start_ms" + :timestamp_end_ms="this.actual_sequence.timestamp_end_ms" + :sequences="this.regions" + :component_id="'first'" + ref="wavesurferComponentFirst"></AudioAndFrequencyPlayer> + <div v-if="this.sequence_count_similar_sequence > 0" class="similarSequenceHeader"> + <div class="changeSimilarSequence"> + <div class="TapeIconContainerSimilar"> + <img class="tapeIconSimilar" src="@/assets/tape.svg"> + </div> + <div class="selectPreviousOrNextSimilarSequence"> + <div style="cursor: pointer;" class="previousSimilar" v-if="this.actual_sequence_index_similar_sequence > 0" @click="changeSequenceSimilar(-1)"> < </div> + <div class="noPreviousSimilar" v-else> < </div> + <p class="next"> + {{actual_sequence_index_similar_sequence + 1}} / {{sequence_count_similar_sequence}} Sequence + </p> + <div style="cursor: pointer;" class="nextSimilar" v-if="this.actual_sequence_index_similar_sequence < this.sequence_count_similar_sequence - 1" @click="changeSequenceSimilar(1)"> > </div> + <div class="noNextSimilar" v-else> > </div> + </div> + </div> + <SequenceHeader + ref="sequence_header_2" + :orca_sequence_id="this.embedding_info.embedding_name" + :tape_name="this.embedding_info.tape_name" + :tape_year="this.embedding_info.year" + :timestamp_start="this.embedding_info.timestamp_start_ms" + :timestamp_end="this.embedding_info.timestamp_end_ms" + :rating="this.embedding_info.rating"> + </SequenceHeader> + </div> + <AudioAndFrequencyPlayer + v-if="this.sequence_count_similar_sequence > 0" + class="expand-width-first" + :is_editable="false" + :origin_tape_name="this.embedding_info.tape_name" + :year="this.embedding_info.year" + :timestamp_start_ms="this.embedding_info.timestamp_start_ms" + :timestamp_end_ms="this.embedding_info.timestamp_end_ms" + :sequences="this.regions" + :component_id="'second'" + ref="wavesurferComponentSecond"></AudioAndFrequencyPlayer> + <div v-if="this.sequence_count_similar_sequence > 0" class="saveSimilarSequence"> + <div> + <vue-good-table + :columns="columns" + :rows="rows" + @on-cell-click="onCellClick"> + <template slot="table-row" slot-scope="props"> + <span title="Similar" @click="similar" v-if="props.column.field === 'similar'"> + <img class="similarIcon" src="../assets/check_icon.svg" alt=""> + </span> + <div v-if="props.column.field === 'rating'"> + <StarRating :read-only="false" @update-rating="onRatingChanged" :row-id="props.row.orca_sequence_id" :star-size="20" :rating="props.row.rating" :show-rating="false"></StarRating> + </div> + <span v-else> + {{props.formattedRow[props.column.field]}} + </span> + </template> + </vue-good-table> + </div> + <vs-popup class="holamundo" title="Tags" :active.sync="tagPopupActive"> + <div class="content"> + <vs-chip v-for="tag in embedding_info.tags" v-bind:key="{tag}"> + <div class="chip-content">{{tag}}</div> + <div class="chip-content" title="Delete" @click="onTagRemove(tag)"> + <img class="deleteIcon" src="../assets/trash-alt-solid.svg" alt=""> + </div> + </vs-chip> + </div> + <SearchAutoComplete @enter="onEnter" class="content" :items="allTags" ></SearchAutoComplete> + <div class="content"> + <vs-button class="popupButton" @click="onTagsUpdate" color="success" type="filled">Update</vs-button> + <vs-button class="popupButton" @click="tagPopupActive=false" color="danger" type="filled">Discard</vs-button> + </div> + </vs-popup> + </div> + <div v-if="(this.sequence_count_similar_sequence <= 0) && this.sequence_count > 0" class="bottom"> + <div class="infoLabbook"> + <p1> {{this.message}} </p1> + </div> + </div> + </div> +</template> + +<script> +import AudioAndFrequencyPlayer from '@/components/AudioAndFrequencyPlayer' +import StarRating from '../components/StarRating.vue' +import SearchAutoComplete from '../components/SearchAutocomplete' +import SequenceHeader from '@/components/SequenceHeader' +import axios from 'axios' +import { Bus } from '@/main' + +export default ({ + name: 'FindSequencesView', + components: { + AudioAndFrequencyPlayer, + StarRating, + SearchAutoComplete, + SequenceHeader + }, + data () { + return { + sequence_count: 0, + actual_sequence_index: 0, + sequences: [], + message: 'An error occured.', + actual_sequence: { + orca_sequence_id: '', + tape_year: 0, + tape_name: '', + tape_channel: '', + audio_segment: '', + timestamp_start_ms: 0, + timestamp_end_ms: 0, + rating: 0, + tags: [], + user_name: '' + }, + sequence_count_similar_sequence: 0, + actual_sequence_index_similar_sequence: 0, + similar_sequences: [], + embedding_info: { + embedding_name: '', + orca_sequence_id: '', + origin_audio_segment: '', + tape_name: '', + tape_channel: '', + year: 0, + timestamp_start_ms: 0, + timestamp_end_ms: 0, + rating: 0, + tags: [], + user_name: '' + }, + columns: [ + { + label: 'Similar', + field: 'similar', + sortable: false + }, + { + label: 'ID', + field: 'orca_sequence_id', + sortable: false + }, + { + label: 'Tags', + field: 'tags', + sortable: false + }, + { + label: 'Rating', + field: 'rating', + type: 'number', + sortable: false + }, + { + label: 'Duration', + field: 'duration', + sortable: false + }, + { + label: 'Username', + field: 'user_name', + sortable: false + } + + ], + rows: [ + { orca_sequence_id: '', tags: '', rating: 0, duration: '', user_name: '' } + ], + regions: [], + allTags: [], + tagPopupActive: false, + channelRequestBody: { + _id: '', + name: '', + year: 0, + left: '', + right: '' + } + } + }, + beforeDestroy () { + Bus.$off() + }, + async mounted () { + // Waveform für das Orca Audio + try { + const response = await axios.get('/api/browse/mark/orca_sequences/embeddings_available/true') + this.sequences = response.data + this.sequence_count = response.data.length + const responseTags = await axios.get('/api/browse/mark/orca_sequences/tags') + this.allTags = Object.keys(responseTags.data) + await this.loadSequence() + await this.loadSimilarSequences(true) + } catch (error) { + console.log(error) + } + }, + updated () { + }, + + methods: { + async similar () { + const url = this.createSequence() + await axios.post('/api/orca_sequence/', url) + .then((response) => { + console.log('Following data was saved:') + console.log(response.data) + this.$vs.notify({ + title: 'Sequence saved', + text: 'Sequence ID: ' + this.embedding_info.orca_sequence_id, + color: 'success', + icon: 'verified_user' + }) + this.loadSimilarSequences(true) + }) + .catch((error) => { + console.log(error) + confirm(error) + this.$vs.notify({ + title: 'Error while saving', + text: error, + color: 'warning', + icon: 'error' + }) + }) + }, + onTagRemove (tag) { + const index = this.embedding_info.tags.indexOf(tag) + if (index !== -1) { + this.embedding_info.tags.splice(index, 1) + } + this.createRow() + console.log(JSON.stringify(this.embedding_info.tags)) + }, + async onTagsUpdate (tag) { + this.tagPopupActive = false + this.createRow() + }, + onEnter (tag) { + if (tag === '' || this.embedding_info.tags.includes(tag)) { + return + } + if (!this.allTags.includes(tag)) { + this.allTags.push(tag) + } + this.embedding_info.tags.push(tag) + this.createRow() + }, + onCellClick (params) { + if (params.column.label === 'Tags') { + this.tagPopupActive = true + } + }, + onRatingChanged (selectedRating, rowId) { + this.embedding_info.rating = selectedRating + this.createRow() + }, + getTimestampInSeconds () { + return Math.floor(Date.now() / 1000) + }, + createSequence () { + return { + orca_sequence_id: this.embedding_info.orca_sequence_id, + tape_year: parseInt(this.embedding_info.year), + tape_name: this.embedding_info.tape_name, + tape_channel: this.embedding_info.tape_channel, + audio_segment: this.embedding_info.origin_audio_segment, + timestamp_start_ms: parseInt(this.embedding_info.timestamp_start_ms), + timestamp_end_ms: parseInt(this.embedding_info.timestamp_end_ms), + rating: this.embedding_info.rating, + tags: this.embedding_info.tags, + user_name: localStorage.user + } + }, + createRow () { + console.log('Create row:') + console.log(JSON.stringify(this.embedding_info.tags)) + if (Array.isArray(this.embedding_info.tags)) { + let tagsString = '' + for (let i = 0; i < this.embedding_info.tags.length; i++) { + tagsString += this.embedding_info.tags[i] + if (i < this.embedding_info.tags.length - 1) { + tagsString += ', ' + } + } + this.rows[0].tags = tagsString + } else { + this.rows[0].tags = '' + } + this.rows[0].orca_sequence_id = this.embedding_info.orca_sequence_id + this.rows[0].rating = this.embedding_info.rating + const durationInt = (this.embedding_info.timestamp_end_ms - this.embedding_info.timestamp_start_ms) / 1000 + this.rows[0].duration = String(durationInt) + 's' + this.rows[0].user_name = localStorage.user + }, + getPosition (string, subString, index) { + return string.split(subString, index).join(subString).length + }, + async createSimilarSequenceInfo (embeddingName) { + this.embedding_info.embedding_name = embeddingName + this.embedding_info.orca_sequence_id = 'orca_sequence_' + this.getTimestampInSeconds() + this.embedding_info.tape_name = embeddingName.substring(this.getPosition(embeddingName, '_', 3) + 1, this.getPosition(embeddingName, '_', 4)) + const timestampStart = embeddingName.substring(this.getPosition(embeddingName, '_', 4) + 1, this.getPosition(embeddingName, '_', 5)) + this.embedding_info.timestamp_start_ms = parseInt(timestampStart) + const timestampEnd = embeddingName.substring(this.getPosition(embeddingName, '_', 5) + 1, this.getPosition(embeddingName, '_', 6)) + this.embedding_info.timestamp_end_ms = parseInt(timestampEnd) + this.embedding_info.user_name = localStorage.user + this.embedding_info.origin_audio_segment = 'audio_segment_' + this.embedding_info.year + '_' + this.embedding_info.tape_name + '_' + timestampStart + '_' + timestampEnd + this.embedding_info.year = parseInt(embeddingName.substring(this.getPosition(embeddingName, '_', 2) + 1, this.getPosition(embeddingName, '_', 3))) + this.embedding_info.tags = [] + this.embedding_info.rating = 0 + await this.getChannel(this.embedding_info.tape_name, this.embedding_info.year) + }, + async getChannel (name, year) { + try { + const url = '/api/search-audio_tape/tape_name/' + name + '/tape_year/' + year + const response = await axios.get(url) + this.channelRequestBody = response.data + if (this.channelRequestBody[0].left === 'orca') { + this.embedding_info.tape_channel = 'left' + } else if (this.channelRequestBody[0].right === 'orca') { + this.embedding_info.tape_channel = 'right' + } else { + this.embedding_info.tape_channel = 'None' + } + } catch (error) { + console.log(error) + confirm(error) + } + }, + async loadSequence () { + const id = String(this.sequences[this.actual_sequence_index]) + try { + await axios.get('/api/browse/mark/orca_sequence/' + id).then((response) => { + this.actual_sequence = response.data + console.log(JSON.stringify(this.actual_sequence)) + this.$refs.wavesurferComponentFirst.loadAudioFile(this.actual_sequence.tape_year, this.actual_sequence.tape_name, this.actual_sequence.tape_channel) + }) + } catch (error) { + console.log(error) + } + }, + async loadSimilarSequences (reloadIndex) { + const id = String(this.sequences[this.actual_sequence_index]) + try { + await axios.get('/api/search-audio_segments/' + id).then(async (responseEmbedding) => { + this.sequence_count_similar_sequence = responseEmbedding.data.length + if (reloadIndex) { + this.actual_sequence_index_similar_sequence = 0 + } + console.log('Following sequence data was received:') + console.log(responseEmbedding.data[this.actual_sequence_index_similar_sequence]) + await this.createSimilarSequenceInfo(responseEmbedding.data[this.actual_sequence_index_similar_sequence]).then((dummy) => { + this.createRow() + this.$refs.wavesurferComponentSecond.loadAudioFile(this.embedding_info.year, this.embedding_info.tape_name, this.embedding_info.tape_channel) + this.$vs.notify({ + title: 'New sequence', + text: 'New sequence loaded: ' + this.embedding_info.orca_sequence_id, + color: 'success', + icon: 'verified_user' + }) + }) + }) + } catch (error) { + this.sequence_count_similar_sequence = 0 + this.similar_sequences = [] + this.message = error.response.data.message + console.log(error.response) + } + }, + changeSequence (direction) { + this.actual_sequence_index = this.actual_sequence_index + direction + this.loadSequence() + this.loadSimilarSequences(true) + }, + changeSequenceSimilar (direction) { + this.actual_sequence_index_similar_sequence = this.actual_sequence_index_similar_sequence + direction + this.loadSimilarSequences(false) + } + } +}) +</script> + +<style lang="scss"> +@import '@/assets/color.scss'; + +.sequenceHeader { + display: flex; + justify-content: space-between; +} + +.similarSequenceHeader { + display: flex; + justify-content: space-between; + clear:both; +} + +.changeSequence { + padding-top: 18px; +} + +.tapeIcon { + filter: invert(36%) sepia(5%) saturate(5710%) hue-rotate(152deg) brightness(94%) contrast(84%); + display: block; + margin-left: auto; + margin-right: auto; + width: 35px; +} + +.tapeIconSimilar { + filter: invert(36%) sepia(5%) saturate(5710%) hue-rotate(152deg) brightness(94%) contrast(84%); + display: block; + margin-left: auto; + margin-right: auto; + width: 35px; +} + +.selectPreviousOrNextSequence { + display: flex; + color: $dark-blue; + font-weight: bold; + margin-top: -6px; +} + +.selectPreviousOrNextSimilarSequence { + display: flex; + color: $dark-blue; + font-weight: bold; + margin-top: -6px; +} + +.saveSimilarSequence { + float: left; + margin: 5px; + width:70%; +} + +.top{ + float:top +} + +.bottom{ + float:bottom; +} + +li { + display: inline-block; + vertical-align: top; + margin: 5px; +} + +.expand-width-first { + width: 95%; + padding-right: 25px; +} + +.findSequences { + margin: 25px; +} + +.similarIcon{ + margin-left: 5px; + margin-right: 5px; + width: 20px; + height: 20px; + filter: invert(35%) sepia(25%) saturate(1262%) hue-rotate(151deg) brightness(94%) contrast(85%); + cursor: pointer; +} + +.deleteIcon{ + margin-left: 5px; + margin-right: 5px; + width: 20px; + height: 20px; + filter: invert(35%) sepia(25%) saturate(1262%) hue-rotate(151deg) brightness(94%) contrast(85%); + cursor: pointer; +} + +.popupButton { + margin: 5px; +} + +.content { + margin-left: 5px; + margin-right: 5px; + padding-top: 10px; + padding-bottom: 10px; + margin-top: 5px; +} + +.chip-content { + padding: 5px; +} + +</style> diff --git a/src/views/MarkSequencesView.vue b/src/views/MarkSequencesView.vue new file mode 100644 index 0000000000000000000000000000000000000000..9f7a49f5ed1bfbdf75c6f9ebdbd10d798c4ea961 --- /dev/null +++ b/src/views/MarkSequencesView.vue @@ -0,0 +1,703 @@ +<template> + <div class="markSequences"> + <SegmentHeader + ref="segmentHeader" + :segments_count="this.segments_count" + :tape_name="this.actual_segment.tape_name" + :tape_year="this.actual_segment.tape_year" + :segment_probability="this.actual_segment.orca_probability.toFixed(2)" + :segment_id="this.actual_segment.segment_id" + :segment_assessed="this.actual_segment.already_assessed" + ></SegmentHeader> + <vs-button class="popupButton" @click="onFilterOpen" color="success" type="filled">Filter</vs-button> + <AudioAndFrequencyPlayer + class="expand-width" + :is_editable="this.isEditable" + :tape_name="this.actual_segment.tape_name" + :tape_year="this.actual_segment.tape_year" + :tape_channel="this.actual_segment.tape_channel" + :timestamp_start_ms="this.actual_segment.timestamp_start_ms" + :timestamp_end_ms="this.actual_segment.timestamp_end_ms" + :sequences="this.rows" + :component_id="'mark'" + ref="wavesurferComponent"></AudioAndFrequencyPlayer> + <vue-good-table + id="table_id" + class="table" + title="Sequences" + :paginate="true" + :columns="columns" + :fixed-header="true" + :row-style-class="rowStyleClassFn" + @on-cell-click="onCellClick" + :sort-options="{ + enabled: false + }" + :select-options="{ + enabled: false + }" + :filter-options="{ + enabled: false + }" + :pagination-options="{ + enabled: false, + mode: 'records' + }" + :rows="rows" + > + <template slot="table-row" slot-scope="props"> + <span title="Delete" @click="deleteCommand(props.row.orca_sequence_id)" v-if="props.column.field === 'delete'"> + <img class="deleteIcon" src="../assets/trash-alt-solid.svg" alt=""> + </span> + <div v-else-if="props.column.field === 'rating'"> + <StarRating :read-only="!isEditable" @update-rating="onRatingChanged" :row-id="props.row.orca_sequence_id" :star-size="20" :rating="props.row.rating" :show-rating="false"></StarRating> + </div> + <span v-else-if="props.column.field === 'add_remove'" @click="addRandomPlayerCommand(props.row.orca_sequence_id)"> + <img v-if="randomPlayerQueue && randomPlayerQueue.includes(props.row.orca_sequence_id)" class="removeIcon" src="../assets/minus_icon.svg" alt=""> + <img v-else class="addIcon" src="../assets/plus_icon.svg" alt=""> + </span> + <div v-else-if="props.column.field === 'add_remove'" @click="addRandomPlayerCommand(props.row.orca_sequence_id)"> + <img v-if="randomPlayerQueue && randomPlayerQueue.includes(props.row.orca_sequence_id)" class="removeIcon" src="../assets/minus_icon.svg" alt=""> + <img v-else class="addIcon" src="../assets/plus_icon.svg" alt=""> + </div> + <span v-else> + {{props.formattedRow[props.column.field]}} + </span> + </template> + <div slot="emptystate"> + No sequences available + </div> + </vue-good-table> + <vs-popup class="holamundo" title="Tags" :active.sync="tagPopupActive"> + <div class="content"> + <vs-chip v-for="tag in actualTags" v-bind:key="{tag}"> + <div class="chip-content">{{tag}}</div> + <div class="chip-content" title="Delete" @click="onTagRemove(tag)"> + <img class="deleteIcon" src="../assets/trash-alt-solid.svg" alt=""> + </div> + </vs-chip> + </div> + <SearchAutoComplete @enter="onEnter" class="content" :items="allTags" ></SearchAutoComplete> + <div class="content"> + <vs-button class="popupButton" @click="onTagsUpdate" color="success" type="filled">Update</vs-button> + <vs-button class="popupButton" @click="tagPopupActive=false" color="danger" type="filled">Discard</vs-button> + </div> + </vs-popup> + <vs-popup class="holamundo" title="Filter" :active.sync="filterPopupActive"> + <FilterSegments ref="filterSeg" :loading="requested" :info="this.idInformation"></FilterSegments> + <div class="content"> + <vs-button class="popupButton" @click="onFilterChange" color="success" type="filled">Use Filter</vs-button> + <vs-button class="popupButton" @click="onFilterDiscard" color="warning" type="filled">Discard Filter</vs-button> + <vs-button class="popupButton" @click="onFilterReset" color="danger" type="filled">Reset Filter</vs-button> + </div> + </vs-popup> + </div> +</template> + +<script> +import axios from 'axios' +import AudioAndFrequencyPlayer from '@/components/AudioAndFrequencyPlayer' +import SegmentHeader from '@/components/SegmentHeader' +import { Bus } from '@/main' +import StarRating from '../components/StarRating.vue' +import SearchAutoComplete from '../components/SearchAutocomplete' +import FilterSegments from '@/components/FilterSegments' + +export default ({ + name: 'MarkSequencesView', + components: { + AudioAndFrequencyPlayer, + SegmentHeader, + StarRating, + SearchAutoComplete, + FilterSegments + }, + data () { + return { + filterPopupActive: false, + regionIdUploading: '', + isEditable: false, + tagPopupActive: false, + active: true, + actualIndex: 0, + isPlayingOrca: false, + displayRegion: false, + playAll: true, + segments: [], + tmpFilterOld: {}, + filteredSegments: [], + segments_count: 0, + actual_segment: { + tape_year: 0, + tape_name: '', + tape_channel: '', + timestamp_start_ms: 0, + timestamp_end_ms: 0, + already_assessed: true, + orca_probability: 0 + }, + selected_sequence_id: -1, + actualTags: [], + setup: true, + user: '', + allTags: [], + columns: [ + { + label: 'Delete', + field: 'delete', + sortable: false + }, + { + label: 'Add or remove', + field: 'add_remove', + sortable: false + }, + { + label: 'ID', + field: 'orca_sequence_id' + }, + { + label: 'Tags', + field: 'tags' + }, + { + label: 'Rating', + field: 'rating', + type: 'number' + }, + { + label: 'Duration', + field: 'duration' + }, + { + label: 'Username', + field: 'user_name' + } + ], + rows: [], + idInformation: {}, + requested: false, + randomPlayerQueue: ['dummy'] + } + }, + created () { + Bus.$on('segment_index_changed', (actualIndex) => { + this.onIndexChanged(actualIndex) + }) + Bus.$on('created-sequence', (region) => { + if (this.regionIdUploading === region.id) { + return + } + this.regionIdUploading = region.id + this.saveSequenceRegion(region) + }) + Bus.$on('updated-sequence', (region) => { + const sequence = this.rows.find(sequence => sequence.orca_sequence_id === region.id) + + if (sequence === undefined || sequence.timestamp_end_ms === undefined) { + return + } + const endTime = (this.actual_segment.timestamp_start_ms + region.end * 1000).toFixed(2) + const startTime = (this.actual_segment.timestamp_start_ms + region.start * 1000).toFixed(2) + if ( + sequence.timestamp_end_ms !== endTime || + sequence.timestamp_start_ms !== startTime + ) { + const sequenceToUpdate = this.rows.find(sequence => sequence.orca_sequence_id === region.id) + sequenceToUpdate.timestamp_end_ms = endTime + sequenceToUpdate.timestamp_start_ms = startTime + this.updateSequence(sequenceToUpdate) + } + }) + Bus.$on('selected-sequence', (region) => { + this.selected_sequence_id = region.id + }) + Bus.$on('user-set', () => { + this.isEditable = true + }) + this.$eventHub.$on('change-queue-event', (sequences) => { + console.log(JSON.stringify(sequences)) + this.randomPlayerQueue = sequences + console.log(JSON.stringify(this.randomPlayerQueue)) + }) + }, + async mounted () { + try { + /* + this.idInformation = {} + this.idInformation['2017'] = { tapes: ['001A', '001B'], maxMin: 10 } + this.idInformation['2018'] = { tapes: ['005A', '005B'], maxMin: 5 } + this.idInformation['2019'] = { tapes: ['0010A', '001B'], maxMin: 20 } + */ + const response = await axios.get('/api/browse/tapes') + this.idInformation = response.data + } catch (error) { + console.log(error) + } + if (localStorage.user === undefined || localStorage.user === 'null' || localStorage.user === '') { + Bus.$emit('set-user') + } else { + this.isEditable = true + } + try { + const response = await axios.get('/api/browse/mark/audio_segments') + this.segments = response.data + this.filteredSegments = this.segments + this.segments_count = response.data.length + await this.loadSegment() + const responseTags = await axios.get('/api/browse/mark/orca_sequences/tags') + this.allTags = Object.keys(responseTags.data) + } catch (error) { + console.log(error) + } + /* + this.segments = [ + 'asdads_asdasd_tape1_1998_10000_20000', + 'asdads_asda22_tape2_1998_15000_20000', + 'asdads_asda33_tape3_1997_5000_30000', + 'asdads_asda44_tape4_1997_20000_25000' + ] + this.filteredSegments = this.segments + this.segments_count = this.filteredSegments.length + */ + if (localStorage.sequenceQueue !== undefined) { + this.randomPlayerQueue = localStorage.sequenceQueue.split(',') + } + }, + beforeDestroy () { + Bus.$off() + }, + methods: { + onIndexChanged (actualIndex) { + this.actualIndex = actualIndex + this.updateSegment() + this.loadSegment() + /* + if (this.bufferedRows[actualIndex] === undefined || + this.bufferedSegments[actualIndex] === undefined) { + this.updateSegment() + this.loadSegment() + } else { + this.setup = true + this.rows = this.bufferedRows[actualIndex] + this.actual_segment = this.bufferedSegments[actualIndex] + this.$refs.wavesurferComponent.loadAudioFile() + } + */ + }, + async onRatingChanged (selectedRating, rowId) { + await this.updateRating(rowId, selectedRating) + }, + async updateRating (rowId, selectedRating) { + try { + const uploadSequence = { + orca_sequence_id: rowId, + rating: selectedRating + } + await axios.put('/api/orca_sequence/rating/' + uploadSequence.orca_sequence_id, uploadSequence) + this.sequence = this.rows.find(sequence => sequence.orca_sequence_id === uploadSequence.orca_sequence_id) + this.sequence.rating = selectedRating + // this.bufferedRows[this.actualIndex] = this.rows + this.$vs.notify({ + title: 'Successfully updated', + text: 'Sequence ID: ' + uploadSequence.orca_sequence_id, + color: 'success', + icon: 'verified_user' + }) + } catch (error) { + console.log(error) + this.$vs.notify({ + title: 'Update with error', + text: error, + color: 'warning', + icon: 'error' + }) + return false + } + return true + }, + deleteCommand (id) { + if (!this.isEditable) { + return + } + axios.delete('/api/orca_sequence/' + id) + .then((response) => { + this.rows = this.rows.filter(sequence => sequence.orca_sequence_id !== id) + this.$refs.wavesurferComponent.removeSequence(id) + // this.bufferedRows[this.actualIndex] = this.rows + this.$vs.notify({ + title: 'Successfully deleted', + text: 'Sequence ID: ' + id, + color: 'success', + icon: 'verified_user' + }) + this.randomPlayerQueue = this.randomPlayerQueue.filter(e => e !== id) + this.$eventHub.$emit('live_remove_from_queue', id) + }) + .catch((error) => { + console.log(error) + this.$vs.notify({ + title: 'Delete with error', + text: error, + color: 'warning', + icon: 'error' + }) + }) + this.wavesurfer.pause() + this.isPlaying = false + }, + async addRandomPlayerCommand (id) { + if (localStorage.sequenceQueue !== undefined) { + this.randomPlayerQueue = localStorage.sequenceQueue.split(',') + } + if (this.randomPlayerQueue.includes(id)) { + this.randomPlayerQueue = this.randomPlayerQueue.filter(e => e !== id) + this.$eventHub.$emit('live_remove_from_queue', id) + console.log('remove') + console.log(JSON.stringify(this.randomPlayerQueue)) + } else { + const sequence = this.rows.find(sequence => sequence.orca_sequence_id === id) + this.$eventHub.$emit('live_add_to_queue', sequence) + this.randomPlayerQueue.push(id) + } + }, + onCellClick (params) { + if (!this.isEditable) { + return + } + if (params.column.label === 'Tags') { + this.selected_sequence_id = params.row.orca_sequence_id + if (params.row.tags.length === 0) { + this.actualTags = [] + } else { + this.actualTags = params.row.tags.split(', ') + } + this.tagPopupActive = true + } + }, + rowStyleClassFn (row) { + return row.orca_sequence_id === this.selected_sequence_id ? 'isSelected' : 'isNormal' + }, + onTagRemove (tag) { + const index = this.actualTags.indexOf(tag) + if (index !== -1) { + this.actualTags.splice(index, 1) + } + }, + async onTagsUpdate () { + this.tagPopupActive = false + const sequence = this.rows.find(sequence => sequence.orca_sequence_id === this.selected_sequence_id) + const sequenceToUpdate = Object.assign({}, sequence) + sequenceToUpdate.tags = '' + for (let i = 0; i < this.actualTags.length; i++) { + sequenceToUpdate.tags += this.actualTags[i] + if (i < (this.actualTags.length - 1)) { + sequenceToUpdate.tags += ', ' + } + } + await this.updateSequence(sequenceToUpdate) + }, + onEnter (tag) { + if (tag === '' || this.actualTags.includes(tag)) { + return + } + if (!this.allTags.includes(tag)) { + this.allTags.push(tag) + } + this.actualTags.push(tag) + }, + async saveSequenceRegion (region) { + if (this.isUploading === true) { + return + } + this.isUploading = true + const ids = this.rows.find(sequence => sequence.orca_sequence_id === region.id) + if (ids !== undefined) { + return + } + const sequenceToSave = this.createSequenceByRegion(region) + try { + console.log('TRY: ' + JSON.stringify(sequenceToSave)) + await axios.post('/api/orca_sequence/', sequenceToSave) + const sequenceToDisplay = this.createSequenceToDisplay(sequenceToSave) + this.rows.push(sequenceToDisplay) + this.rows.sort(this.objectComparisonCallback) + // this.bufferedRows[this.actualIndex] = this.rows + // this.bufferedSegments[this.actualIndex] = this.actual_segment + console.log('SUCCESS: ' + JSON.stringify(sequenceToSave)) + this.$vs.notify({ + title: 'Successfully saved', + text: 'Sequence ID: ' + sequenceToSave.orca_sequence_id, + color: 'success', + icon: 'verified_user' + }) + } catch (error) { + console.log(error) + console.log('ERROR: ' + JSON.stringify(sequenceToSave)) + this.$vs.notify({ + title: 'Save with error', + text: error, + color: 'warning', + icon: 'error' + }) + } + this.isUploading = false + }, + async updateSegment () { + try { + const updateSequence = { + audio_segment_id: this.actual_segment.segment_id, + already_assessed: true + } + await axios.put('/api/audio_segment/' + this.actual_segment.segment_id, updateSequence) + this.actual_segment.already_assessed = true + // this.bufferedSegments[this.actualIndex] = this.actual_segment + } catch (error) { + console.log(error) + return false + } + return true + }, + async updateSequence (sequenceToUpdate) { + try { + const uploadSequence = this.createSequenceToUpload(sequenceToUpdate) + await axios.put('/api/orca_sequence/' + uploadSequence.orca_sequence_id, uploadSequence) + this.rows = this.rows.filter(sequence => sequence.orca_sequence_id !== sequenceToUpdate.orca_sequence_id) + const sequenceToDisplay = this.createSequenceToDisplay(sequenceToUpdate) + this.rows.push(sequenceToDisplay) + this.rows.sort(this.objectComparisonCallback) + // this.bufferedRows[this.actualIndex] = this.rows + // this.bufferedSegments[this.actualIndex] = this.actual_segment + this.$vs.notify({ + title: 'Successfully updated', + text: 'Sequence ID: ' + uploadSequence.orca_sequence_id, + color: 'success', + icon: 'verified_user' + }) + } catch (error) { + console.log(error) + this.$vs.notify({ + title: 'Update with error', + text: error, + color: 'warning', + icon: 'error' + }) + return false + } + return true + }, + createSequenceByRegion (region) { + return { + orca_sequence_id: region.id, + tape_year: this.actual_segment.tape_year, + tape_name: this.actual_segment.tape_name, + tape_channel: this.actual_segment.tape_channel, + audio_segment: this.actual_segment.segment_id, + timestamp_start_ms: parseInt((this.actual_segment.timestamp_start_ms + region.start * 1000).toFixed(2)), + timestamp_end_ms: parseInt((this.actual_segment.timestamp_start_ms + region.end * 1000).toFixed(2)), + rating: 0, + tags: [], + user_name: localStorage.user + } + }, + clearSequences () { + this.rows = [] + }, + async loadSegment () { + this.setup = true + this.clearSequences() + const tempIndex = this.actualIndex + const id = String(this.filteredSegments[this.actualIndex]) + try { + const response = await axios.get('/api/browse/mark/audio_segment/' + id) + this.actual_segment = response.data + this.actual_segment.segment_id = id + const responseSequences = await axios.get('/api/browse/mark/orca_sequences/audio_segment/' + id) + for (let i = 0; i < responseSequences.data.length; i++) { + const sequenceId = responseSequences.data[i] + const responseSequence = await axios.get('/api/browse/mark/orca_sequence/' + sequenceId) + const sequence = responseSequence.data + sequence.orca_sequence_id = sequenceId + const displaySequence = this.createSequenceToDisplay(sequence) + this.rows.push(displaySequence) + this.rows.sort(this.objectComparisonCallback) + if (this.actualIndex !== tempIndex) { + this.rows = [] + return + } + } + // this.bufferedRows[this.actualIndex] = this.rows + // this.bufferedSegments[this.actualIndex] = this.actual_segment + } catch (error) { + console.log(error) + } + this.$refs.wavesurferComponent.loadAudioFile() + }, + createSequenceToDisplay (sequence) { + const sequenceToDisplay = Object.assign({}, sequence) + if (Array.isArray(sequence.tags)) { + let tagsString = '' + for (let i = 0; i < sequence.tags.length; i++) { + tagsString += sequence.tags[i] + if (i < sequence.tags.length - 1) { + tagsString += ', ' + } + } + sequenceToDisplay.tags = tagsString + } + sequenceToDisplay.duration = ((sequence.timestamp_end_ms - sequence.timestamp_start_ms) / 1000).toFixed(2) + sequenceToDisplay.add_remove = false + return sequenceToDisplay + }, + createSequenceToUpload (sequence) { + return { + orca_sequence_id: sequence.orca_sequence_id, + tape_year: sequence.tape_year, + tape_name: sequence.tape_name, + tape_channel: sequence.tape_channel, + audio_segment: sequence.audio_segment, + timestamp_start_ms: parseInt(sequence.timestamp_start_ms), + timestamp_end_ms: parseInt(sequence.timestamp_end_ms), + rating: sequence.rating, + tags: sequence.tags.split(', '), + user_name: localStorage.user + } + }, + objectComparisonCallback (a, b) { + const textA = a.orca_sequence_id.toUpperCase() + const textB = b.orca_sequence_id.toUpperCase() + return (textA < textB) ? -1 : (textA > textB) ? 1 : 0 + }, + async onFilterChange () { + const actualFilter = this.$refs.filterSeg.getFilter() + this.requested = true + this.filteredSegments = [] + if (actualFilter.id !== '') { + // search id + this.filteredSegments = [actualFilter.id] + } else if (actualFilter.year !== '' || actualFilter.tape !== '' || actualFilter.duration !== '') { + // filter ids + if (actualFilter.year === '') { + actualFilter.year = '*' + } + if (actualFilter.tape === '') { + actualFilter.tape = '*' + } + if (actualFilter.duration === '') { + actualFilter.duration = '*' + } + try { + const response = await axios.get('/api/browse/mark/audio_segments/year/' + actualFilter.year + '/tape/' + actualFilter.tape + '/timequery/' + actualFilter.duration) + this.filteredSegments = response.data + this.segments_count = response.data.length + } catch (error) { + console.log(error) + } + } else { + // load random ones + this.filteredSegments = this.segments + } + this.segments_count = this.filteredSegments.length + if (this.segments_count === 0) { + this.requested = false + this.$vs.notify({ + title: 'No segments found', + text: 'Change filter parameters', + color: 'warning', + icon: 'error' + }) + this.$refs.filterSeg.setFilter(actualFilter) + } else { + this.filterPopupActive = false + this.$refs.segmentHeader.resetIndex() + } + }, + onFilterDiscard () { + this.filterPopupActive = false + this.$refs.filterSeg.setFilter(this.tmpFilterOld) + }, + onFilterReset () { + this.$refs.filterSeg.resetValues() + }, + onFilterOpen () { + this.requested = false + this.filterPopupActive = true + this.$refs.filterSeg.onOpenPopup() + this.tmpFilterOld = this.$refs.filterSeg.getFilter() + } + } +}) +</script> + +<style lang="scss"> +@import '@/assets/color.scss'; +@import '@/assets/grid.scss'; + +.markSequences { + margin: 25px; +} + +.expand-width { + width: 95%; + padding-right: 25px; +} + +.table { + margin-top: 50px; + padding-bottom: 50px; + padding-left: 25px; + padding-right: 25px; + width: 95%; +} + +.isSelected { + background: #87B394; +} + +.isNormal { +} + +.deleteIcon{ + margin-left: 5px; + margin-right: 5px; + width: 20px; + height: 20px; + filter: invert(35%) sepia(25%) saturate(1262%) hue-rotate(151deg) brightness(94%) contrast(85%); + cursor: pointer; +} + +.addIcon{ + margin-left: 5px; + margin-right: 5px; + width: 20px; + height: 20px; + filter: invert(35%) sepia(25%) saturate(1262%) hue-rotate(151deg) brightness(94%) contrast(85%); + cursor: pointer; +} + +.removeIcon{ + margin-left: 5px; + margin-right: 5px; + width: 20px; + height: 20px; + filter: invert(35%) sepia(25%) saturate(1262%) hue-rotate(151deg) brightness(94%) contrast(85%); + cursor: pointer; +} + +.popupButton { + margin: 5px; +} + +.content { + margin-left: 5px; + margin-right: 5px; + padding-top: 10px; + padding-bottom: 10px; + margin-top: 5px; +} + +.chip-content { + padding: 5px; +} + +</style> diff --git a/src/views/OrganizeSequencesView.vue b/src/views/OrganizeSequencesView.vue new file mode 100644 index 0000000000000000000000000000000000000000..aae111091e666a723f15f06498b35686cb7ab92a --- /dev/null +++ b/src/views/OrganizeSequencesView.vue @@ -0,0 +1,521 @@ +<template> + <div id='app'> + <vue-good-table + ref="table_ref" + id="table_id" + class="table" + title="Sequences" + styleClass="vgt-table striped" + @on-selected-rows-change="selectionChanged" + :paginate="true" + :columns="columns" + :fixed-header="true" + :select-options="{ + enabled: true, + selectOnCheckboxOnly: true, + selectionInfoClass: 'selected', + selectionText: 'rows selected', + clearSelectionText: 'clear', + disableSelectInfo: true, + selectAllByGroup: true + }" + :pagination-options="{ + enabled: true, + mode: 'records' + }" + :rows="rows" + > + <template slot="table-row" slot-scope="props"> + <div class="row" v-if="props.column.field === 'action'"> + <div title="Play/Pause" @click="playPauseRow(props.row)"> + <img class="playIcon" v-if="rowIdPlaying !== props.row.orca_sequence_id" src="../assets/play-circle-solid.svg"> + <img class="pauseIcon" v-else src="../assets/pause-circle-solid.svg"> + </div> + <div title="Download" @click="downloadCommand(props.row)"> + <img class="deleteIcon" src="../assets/download.svg"> + </div> + <div title="Delete" @click="deleteCommand(props.row.orca_sequence_id)"> + <img class="deleteIcon" src="../assets/trash-alt-solid.svg"> + </div> + <div title="Add or remove random player" @click="addRandomPlayerCommand(props.row.orca_sequence_id)"> + <img v-if="randomPlayerQueue && randomPlayerQueue.includes(props.row.orca_sequence_id)" class="removeIcon" src="../assets/minus_icon.svg" alt=""> + <img v-else class="addIcon" src="../assets/plus_icon.svg" alt=""> + </div> + </div> + <div v-else-if="props.column.field === 'rating'"> + <StarRating :read-only="!isEditable" @update-rating="onRatingChanged" :row-id="props.row.orca_sequence_id" :star-size="20" :rating="props.row.rating" :show-rating="false"></StarRating> + </div> + <span v-else> + {{props.formattedRow[props.column.field]}} + </span> + </template> + <div slot="emptystate"> + No sequences available + </div> + </vue-good-table> + <div class="allAction" v-if="this.currentSelection.length > 0"> + <span title="Download Selected" @click="downloadCommand(-1)"> + <img class="deleteIcon" src="../assets/download.svg"> + </span> + <span title="Delete Selected" @click="deleteCommand(-1)"> + <img class="deleteIcon" src="../assets/trash-alt-solid.svg"> + </span> + <span title="Add or remove selected to random player" @click="addRandomPlayerCommand(-1)"> + <img class="addIcon" src="../assets/plus_icon.svg" alt=""> + </span> + </div> + </div> +</template> + +<script> +import axios from 'axios' +import StarRating from '../components/StarRating.vue' +import JSZip from 'jszip' +import { saveAs } from 'file-saver' +import { Bus } from '@/main' + +export default { + name: 'OrganizeSequencesView', + components: { + StarRating + }, + data () { + return { + audio: null, + panChannel: 0, + track: null, + stereoNode: null, + audioContext: new AudioContext(), + isEditable: false, + bufferedAudioTapes: {}, + rowIdPlaying: -1, + windowHeight: 250, + randomPlayerQueue: ['dummy'], + currentSelection: [], + columns: [ + { + label: 'Action', + field: 'action', + sortable: false, + filterOptions: { + enabled: false // enable filter for this column + } + }, + { + label: 'Username', + field: 'user_name', + filterOptions: { + enabled: true, // enable filter for this column + placeholder: 'Filter users', // placeholder for filter input + filterValue: '', // initial populated value for this filter + filterDropdownItems: [], // dropdown (with selected values) instead of text input + filterFn: function (data, filterString) { + const stringData = data.toString() + return stringData.includes(filterString) + } + } + }, + { + label: 'Tags', + field: 'tags', + filterOptions: { + enabled: true, // enable filter for this column + placeholder: 'Filter tags', // placeholder for filter input + filterDropdownItems: [], // dropdown (with selected values) instead of text input + filterFn: function (data, filterString) { + const stringData = data.toString() + return stringData.includes(filterString) + } + } + }, + { + label: 'Rating', + field: 'rating', + type: 'number', + filterOptions: { + enabled: true, // enable filter for this column + placeholder: 'Filter rating', // placeholder for filter input + filterValue: '', // initial populated value for this filter + filterDropdownItems: [ + { value: 1, text: '1 Rated' }, + { value: 2, text: '2 Rated' }, + { value: 3, text: '3 Rated' }, + { value: 4, text: '4 Rated' }, + { value: 5, text: '5 Rated' } + ] + } + }, + { + label: 'Tape', + field: 'tape_name', + filterOptions: { + enabled: true, // enable filter for this column + placeholder: 'Filter tape', // placeholder for filter input + filterValue: '', // initial populated value for this filter + filterDropdownItems: [], // dropdown (with selected values) instead of text input + filterFn: function (data, filterString) { + const stringData = data.toString() + return stringData.includes(filterString) + } + } + }, + { + label: 'Year', + field: 'tape_year', + type: 'number', + filterOptions: { + enabled: true, // enable filter for this column + placeholder: 'Filter year', // placeholder for filter input + filterValue: '', // initial populated value for this filter + filterDropdownItems: [], // dropdown (with selected values) instead of text input + filterFn: function (data, filterString) { + const stringData = data.toString() + return stringData.includes(filterString) + } + } + }, + { + label: 'Duration', + field: 'duration', + filterOptions: { + enabled: true, // enable filter for this column + placeholder: 'Filter duration', // placeholder for filter input + filterValue: '', // initial populated value for this filter + filterDropdownItems: ['< 5 sec.', '5 - 10 sec.', '10 - 20 sec.', '>= 20 sec.'], // dropdown (with selected values) instead of text input + filterFn: function (data, filterString) { + return (filterString === '< 5 sec.' && data < 5) || + (filterString === '5 - 10 sec.' && data >= 5 && data < 10) || + (filterString === '10 - 20 sec.' && data >= 10 && data < 20) || + (filterString === '>= 20 sec.' && data >= 20) + } + } + }, + { + label: 'Sequence Id', + field: 'orca_sequence_id', + filterOptions: { + enabled: true, // enable filter for this column + placeholder: 'Filter id', // placeholder for filter input + filterValue: '', // initial populated value for this filter + filterDropdownItems: [], // dropdown (with selected values) instead of text input + filterFn: function (data, filterString) { + const stringData = data.toString() + return stringData.includes(filterString) + } + } + }, + { + label: 'Segment Id', + field: 'audio_segment', + filterOptions: { + enabled: true, // enable filter for this column + placeholder: 'Filter id', // placeholder for filter input + filterValue: '', // initial populated value for this filter + filterDropdownItems: [], // dropdown (with selected values) instead of text input + filterFn: function (data, filterString) { + const stringData = data.toString() + return stringData.includes(filterString) + } + } + } + ], + rows: [] + } + }, + beforeDestroy () { + Bus.$off() + }, + created () { + Bus.$on('user-set', () => { + this.isEditable = true + }) + this.$eventHub.$on('change-queue-event', (sequences) => { + this.randomPlayerQueue = sequences + }) + }, + async mounted () { + if (localStorage.user !== undefined) { + this.isEditable = true + } + window.addEventListener('resize', () => { + this.windowHeight = window.innerHeight + }) + try { + const response = await axios.get('/api/browse/mark/orca_sequences/') + for (let i = 0; i < response.data.length; i++) { + const sequenceId = response.data[i] + const responseSequence = await axios.get('/api/browse/mark/orca_sequence/' + sequenceId) + const sequence = responseSequence.data + sequence.orca_sequence_id = sequenceId + sequence.duration = ((parseInt(sequence.timestamp_end_ms) - parseInt(sequence.timestamp_start_ms)) / 1000).toFixed(2) + const tags = sequence.tags + sequence.tags = '' + + if (!(this.columns[6].filterOptions.filterDropdownItems.includes(sequence.user_name))) { + this.columns[6].filterOptions.filterDropdownItems.push(sequence.user_name) + } + + for (let i = 0; i < tags.length; i++) { + sequence.tags += tags[i] + if (i < tags.length - 1) { + sequence.tags += ',' + } + } + console.log(sequence) + this.rows.push(sequence) + } + const responseTags = await axios.get('/api/browse/mark/orca_sequences/tags') + this.columns[3].filterOptions.filterDropdownItems = Object.keys(responseTags.data) + } catch (error) { + console.log(error) + } + if (localStorage.sequenceQueue !== undefined) { + this.randomPlayerQueue = localStorage.sequenceQueue.split(',') + console.log('mounted: ' + JSON.stringify(this.randomPlayerQueue)) + } + }, + methods: { + async onRatingChanged (selectedRating, rowId) { + await this.updateRating(rowId, selectedRating) + }, + async updateRating (rowId, selectedRating) { + try { + const uploadSequence = { + orca_sequence_id: rowId, + rating: selectedRating + } + await axios.put('/api/orca_sequence/rating/' + uploadSequence.orca_sequence_id, uploadSequence) + this.$vs.notify({ + title: 'Successfully updated', + text: 'Sequence ID: ' + uploadSequence.orca_sequence_id, + color: 'success', + icon: 'verified_user' + }) + } catch (error) { + console.log(error) + this.$vs.notify({ + title: 'Update with error', + text: error, + color: 'warning', + icon: 'error' + }) + return false + } + return true + }, + async playPauseRow (row) { + if (this.rowIdPlaying !== row.orca_sequence_id) { + this.rowIdPlaying = row.orca_sequence_id + if (this.audio !== null) { + this.audio.pause() + } + if (this.bufferedAudioTapes[row.tape_name] === undefined) { + // const tapData = row.tape_name.split('_') + const orcaPath = '/files/tapes/' + row.tape_year + '/' + row.tape_name + '/' + row.tape_channel + '.mp3' + // const orcaPath = require('../assets/audio/001A1.mp3') + const config = { url: orcaPath, method: 'get', responseType: 'blob' } + const response = await axios.request(config) + this.bufferedAudioTapes[row.tape_name] = response.data + } + const blobAudio = this.bufferedAudioTapes[row.tape_name] + const size = blobAudio.size + const duration = 48 * 60 * 1000 + const bytePerMillisecond = size / duration + const startBytes = bytePerMillisecond * row.timestamp_start_ms + const endBytes = bytePerMillisecond * row.timestamp_end_ms + const blobAudioSliced = blobAudio.slice(startBytes, endBytes) + const blobURL = window.URL.createObjectURL(blobAudioSliced) + this.audio = new Audio(blobURL) + this.track = this.audioContext.createMediaElementSource(this.audio) + this.panChannel = 0 + if (localStorage.isLive !== undefined && localStorage.isLive === 'true') { + this.panChannel = 1 + } + this.stereoNode = new StereoPannerNode(this.audioContext, { pan: this.panChannel }) + this.track.connect(this.stereoNode).connect(this.audioContext.destination) + this.audio.addEventListener('canplaythrough', (event) => { + this.audio.play() + }) + this.audio.addEventListener('ended', (event) => { + this.rowIdPlaying = -1 + }) + } else { + this.rowIdPlaying = -1 + this.audio.pause() + } + }, + selectionChanged (item) { + this.showSelectionOptions = true + this.currentSelection = [] + for (let i = 0; i < item.selectedRows.length; i++) { + this.currentSelection.push(item.selectedRows[i]) + } + }, + async downloadCommand (row) { + const d = new Date() + let metaData = 'Downloaded on ' + d.getDate() + '. ' + d.getMonth() + ' ' + d.getFullYear() + ' at ' + d.getHours() + ':' + d.getMinutes() + metaData += '\nFilter options: ' + JSON.stringify(this.$refs.table_ref.columnFilters) + if (row !== -1) { + this.currentSelection.push(row) + console.log(JSON.stringify(this.currentSelection)) + } + const zip = new JSZip() + for (let i = 0; i < this.currentSelection.length; i++) { + console.log(JSON.stringify(this.currentSelection)) + metaData += '\n' + JSON.stringify(this.currentSelection[i]) + const blobAudioSliced = await this.createBlobSequence(this.currentSelection[i]) + zip.file(this.currentSelection[i].orca_sequence_id + '.mp3', blobAudioSliced) + } + zip.file('meta_information.txt', metaData) + zip.generateAsync({ + type: 'blob' + }).then(function (content) { + saveAs(content, 'orca_sequences.zip') + }) + }, + async createBlobSequence (row) { + if (this.bufferedAudioTapes[row.tape_name] === undefined) { + const orcaPath = require('../assets/audio/001A1.mp3') + const config = { url: orcaPath, method: 'get', responseType: 'blob' } + const response = await axios.request(config) + this.bufferedAudioTapes[row.tape_name] = response.data + } + const blobAudio = this.bufferedAudioTapes[row.tape_name] + const size = blobAudio.size + const duration = 48 * 60 * 1000 + const bytePerMillisecond = size / duration + const startBytes = bytePerMillisecond * row.timestamp_start_ms + const endBytes = bytePerMillisecond * row.timestamp_end_ms + return blobAudio.slice(startBytes, endBytes) + }, + async deleteCommand (id) { + if (!this.isEditable) { + return + } + if (id === -1) { + const isExecuted = confirm('Are you sure to delete ' + this.currentSelection.length + ' sequence(s)?') + if (!isExecuted) { + return + } + const ids = this.currentSelection + for (let i = 0; i < ids.length; i++) { + await this.deleteSequence(ids[i].orca_sequence_id) + } + } else { + /* const isExecuted = confirm('Are you sure to delete ' + 1 + ' sequence?') + if (!isExecuted) { + return + } */ + await this.deleteSequence(id) + } + }, + async addRandomPlayerCommand (id) { + console.log('Add to player:' + String(id)) + if (id === -1) { + const isExecuted = confirm('Are you sure to add ' + this.currentSelection.length + ' sequence(s) to the orca player?') + if (!isExecuted) { + return + } + const ids = this.currentSelection + this.$eventHub.$emit('live_set_queue', ids) + } else { + if (localStorage.sequenceQueue !== undefined) { + this.randomPlayerQueue = localStorage.sequenceQueue.split(',') + } + if (this.randomPlayerQueue.includes(id)) { + this.randomPlayerQueue = this.randomPlayerQueue.filter(e => e !== id) + this.$eventHub.$emit('live_remove_from_queue', id) + console.log('remove from orca player: ' + JSON.stringify(this.randomPlayerQueue)) + } else { + const sequence = this.rows.find(sequence => sequence.orca_sequence_id === id) + this.$eventHub.$emit('live_add_to_queue', sequence) + this.randomPlayerQueue.push(id) + } + } + }, + async deleteSequence (sequenceId) { + if (!this.isEditable) { + return + } + try { + const response = await axios.delete('/api/orca_sequence/' + sequenceId) + if (response.status === 200) { + this.rows = this.rows.filter(sequence => sequence.orca_sequence_id !== sequenceId) + this.randomPlayerQueue = this.randomPlayerQueue.filter(e => e !== sequenceId) + Bus.$emit('live_remove_from_queue', sequenceId) + } + } catch (error) { + console.log(error) + } + } + } +} +</script> + +<style scoped> + +.table { + margin-top: 50px; + padding-bottom: 20px; + padding-left: 25px; + margin-right: 25px; +} + +.row { + width: 100%; + display: flex; + flex-direction: row; + justify-content: center; +} + +.playIcon{ + margin-left: 5px; + margin-right: 5px; + width: 25px; + height: 25px; + filter: invert(35%) sepia(25%) saturate(1262%) hue-rotate(151deg) brightness(94%) contrast(85%); + cursor: pointer; +} + +.deleteIcon{ + margin-left: 5px; + margin-right: 5px; + width: 25px; + height: 25px; + filter: invert(35%) sepia(25%) saturate(1262%) hue-rotate(151deg) brightness(94%) contrast(85%); + cursor: pointer; +} + +.pauseIcon{ + margin-left: 5px; + margin-right: 5px; + width: 25px; + height: 25px; + filter: invert(61%) sepia(14%) saturate(1125%) hue-rotate(155deg) brightness(84%) contrast(86%); + cursor: pointer; +} + +.addIcon{ + margin-left: 5px; + margin-right: 5px; + width: 25px; + height: 25px; + filter: invert(35%) sepia(25%) saturate(1262%) hue-rotate(151deg) brightness(94%) contrast(85%); + cursor: pointer; +} + +.removeIcon{ + margin-left: 5px; + margin-right: 5px; + width: 25px; + height: 25px; + filter: invert(35%) sepia(25%) saturate(1262%) hue-rotate(151deg) brightness(94%) contrast(85%); + cursor: pointer; +} + +.allAction { + margin-left: 25px; + padding-bottom: 20px; +} + +</style>