This commit is contained in:
Joel Collins 2020-10-28 10:47:15 +00:00
commit ce57d37dc9
44 changed files with 1218 additions and 933 deletions

View file

@ -1,24 +1,30 @@
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
node: true node: true
}, },
extends: [ extends: [
"plugin:vue/recommended", "plugin:vue/recommended",
"eslint:recommended", "eslint:recommended",
"prettier/vue", "prettier/vue",
"plugin:prettier/recommended" "plugin:prettier/recommended",
"plugin:vue/essential",
"@vue/prettier"
], ],
rules: { rules: {
"vue/component-name-in-template-casing": ["error", "PascalCase"], "vue/component-name-in-template-casing": ["error", "PascalCase"],
"no-console": process.env.NODE_ENV === "production" ? "error" : "off", "no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off"
}, },
globals: { globals: {
$nuxt: true $nuxt: true
}, },
parserOptions: { parserOptions: {
parser: "babel-eslint" parser: "babel-eslint"
} }
}; };

View file

@ -1,5 +1,3 @@
module.exports = { module.exports = {
presets: [ presets: ["@vue/app"]
'@vue/app' };
]
}

View file

@ -2618,6 +2618,101 @@
"webpack": "^4.0.0" "webpack": "^4.0.0"
} }
}, },
"@vue/cli-plugin-eslint": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-eslint/-/cli-plugin-eslint-4.5.8.tgz",
"integrity": "sha512-1lg3K2D/harXbN4FvRoA1b0X9947H+G4Ql/43rxJY39OqswfK/d1ck438Fo9M4l1+zhBSUNDmcjn7Q2EH6qWmA==",
"dev": true,
"requires": {
"@vue/cli-shared-utils": "^4.5.8",
"eslint-loader": "^2.2.1",
"globby": "^9.2.0",
"inquirer": "^7.1.0",
"webpack": "^4.0.0",
"yorkie": "^2.0.0"
},
"dependencies": {
"@vue/cli-shared-utils": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.8.tgz",
"integrity": "sha512-pa6oenhBO/5HeDLRSokiwVN01gROACEDy3ESXWuPmragOREGNmmFKtkPHlqeYavGEX6LFp7f0VK3uMX6UYS5mQ==",
"dev": true,
"requires": {
"@hapi/joi": "^15.0.1",
"chalk": "^2.4.2",
"execa": "^1.0.0",
"launch-editor": "^2.2.1",
"lru-cache": "^5.1.1",
"node-ipc": "^9.1.1",
"open": "^6.3.0",
"ora": "^3.4.0",
"read-pkg": "^5.1.1",
"request": "^2.88.2",
"semver": "^6.1.0",
"strip-ansi": "^6.0.0"
}
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"dev": true,
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.0"
}
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"dev": true,
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
}
}
},
"@vue/cli-plugin-router": { "@vue/cli-plugin-router": {
"version": "4.5.6", "version": "4.5.6",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-4.5.6.tgz", "resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-4.5.6.tgz",
@ -3157,6 +3252,15 @@
} }
} }
}, },
"@vue/eslint-config-prettier": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-6.0.0.tgz",
"integrity": "sha512-wFQmv45c3ige5EA+ngijq40YpVcIkAy0Lihupnsnd1Dao5CBbPyfCzqtejFLZX1EwH/kCJdpz3t6s+5wd3+KxQ==",
"dev": true,
"requires": {
"eslint-config-prettier": "^6.0.0"
}
},
"@vue/preload-webpack-plugin": { "@vue/preload-webpack-plugin": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz",
@ -3436,6 +3540,23 @@
"integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==",
"dev": true "dev": true
}, },
"ansi-escapes": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
"integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
"dev": true,
"requires": {
"type-fest": "^0.11.0"
},
"dependencies": {
"type-fest": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
"integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
"dev": true
}
}
},
"ansi-html": { "ansi-html": {
"version": "0.0.7", "version": "0.0.7",
"resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
@ -4576,6 +4697,12 @@
"tslib": "^1.9.0" "tslib": "^1.9.0"
} }
}, },
"ci-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
"integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==",
"dev": true
},
"cipher-base": { "cipher-base": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
@ -6402,6 +6529,19 @@
} }
} }
}, },
"eslint-loader": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-2.2.1.tgz",
"integrity": "sha512-RLgV9hoCVsMLvOxCuNjdqOrUqIj9oJg8hF44vzJaYqsAHuY9G2YAeN3joQ9nxP0p5Th9iFSIpKo+SD8KISxXRg==",
"dev": true,
"requires": {
"loader-fs-cache": "^1.0.0",
"loader-utils": "^1.0.2",
"object-assign": "^4.0.1",
"object-hash": "^1.1.4",
"rimraf": "^2.6.1"
}
},
"eslint-plugin-prettier": { "eslint-plugin-prettier": {
"version": "3.1.4", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz",
@ -6833,6 +6973,15 @@
"integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==",
"dev": true "dev": true
}, },
"figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
"integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
"dev": true,
"requires": {
"escape-string-regexp": "^1.0.5"
}
},
"file-entry-cache": { "file-entry-cache": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
@ -8344,6 +8493,162 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true "dev": true
}, },
"inquirer": {
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz",
"integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==",
"dev": true,
"requires": {
"ansi-escapes": "^4.2.1",
"chalk": "^4.1.0",
"cli-cursor": "^3.1.0",
"cli-width": "^3.0.0",
"external-editor": "^3.0.3",
"figures": "^3.0.0",
"lodash": "^4.17.19",
"mute-stream": "0.0.8",
"run-async": "^2.4.0",
"rxjs": "^6.6.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0",
"through": "^2.3.6"
},
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"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,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"dev": true,
"requires": {
"restore-cursor": "^3.1.0"
}
},
"cli-width": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
"integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
"dev": true
},
"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,
"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
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": 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
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
"onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
"requires": {
"mimic-fn": "^2.1.0"
}
},
"restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"dev": true,
"requires": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
}
},
"run-async": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz",
"integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==",
"dev": true
},
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"dev": true,
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
"ansi-regex": "^5.0.0"
}
},
"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,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"internal-ip": { "internal-ip": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz",
@ -8452,6 +8757,15 @@
"integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
"dev": true "dev": true
}, },
"is-ci": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz",
"integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==",
"dev": true,
"requires": {
"ci-info": "^1.5.0"
}
},
"is-color-stop": { "is-color-stop": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz",
@ -8966,6 +9280,57 @@
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
"dev": true "dev": true
}, },
"loader-fs-cache": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz",
"integrity": "sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA==",
"dev": true,
"requires": {
"find-cache-dir": "^0.1.1",
"mkdirp": "^0.5.1"
},
"dependencies": {
"find-cache-dir": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz",
"integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=",
"dev": true,
"requires": {
"commondir": "^1.0.1",
"mkdirp": "^0.5.1",
"pkg-dir": "^1.0.0"
}
},
"find-up": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
"integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
"dev": true,
"requires": {
"path-exists": "^2.0.0",
"pinkie-promise": "^2.0.0"
}
},
"path-exists": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
"integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
"dev": true,
"requires": {
"pinkie-promise": "^2.0.0"
}
},
"pkg-dir": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz",
"integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=",
"dev": true,
"requires": {
"find-up": "^1.0.0"
}
}
}
},
"loader-runner": { "loader-runner": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
@ -9475,6 +9840,12 @@
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
"dev": true "dev": true
}, },
"mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
"dev": true
},
"mz": { "mz": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@ -9722,6 +10093,12 @@
} }
} }
}, },
"object-hash": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz",
"integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==",
"dev": true
},
"object-inspect": { "object-inspect": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
@ -10891,8 +11268,7 @@
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
"dev": true, "dev": true
"optional": true
}, },
"prettier-linter-helpers": { "prettier-linter-helpers": {
"version": "1.0.0", "version": "1.0.0",
@ -11509,6 +11885,15 @@
"aproba": "^1.1.1" "aproba": "^1.1.1"
} }
}, },
"rxjs": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz",
"integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
}
},
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@ -12415,6 +12800,12 @@
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true "dev": true
}, },
"strip-indent": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz",
"integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=",
"dev": true
},
"stylehacks": { "stylehacks": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",
@ -13292,9 +13683,9 @@
} }
}, },
"vue-loader-v16": { "vue-loader-v16": {
"version": "npm:vue-loader@16.0.0-beta.8", "version": "npm:vue-loader@16.0.0-beta.9",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.8.tgz", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.9.tgz",
"integrity": "sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==", "integrity": "sha512-mu9pg6554GbXDSO8LlxkQM6qUJzUkb/A0FJc9LgRqnU9MCnhzEXwCt1Zx5NObvFpzs2mH2dH/uUCDwL8Qaz9sA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
@ -13446,6 +13837,12 @@
"vue": "^2.6.10" "vue": "^2.6.10"
} }
}, },
"vuejs-paginate": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vuejs-paginate/-/vuejs-paginate-2.1.0.tgz",
"integrity": "sha512-gnwyXlmCiDOu9MLWxN5UJ4PGijKGNOMpHG8ujsrynCzTJljn/rp7Jq0WiDGDAMi5/u0AHuYIHhced+tUW4jblA==",
"dev": true
},
"vuex": { "vuex": {
"version": "3.5.1", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz", "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz",
@ -14263,6 +14660,74 @@
"camelcase": "^5.0.0", "camelcase": "^5.0.0",
"decamelize": "^1.2.0" "decamelize": "^1.2.0"
} }
},
"yorkie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/yorkie/-/yorkie-2.0.0.tgz",
"integrity": "sha512-jcKpkthap6x63MB4TxwCyuIGkV0oYP/YRyuQU5UO0Yz/E/ZAu+653/uov+phdmO54n6BcvFRyyt0RRrWdN2mpw==",
"dev": true,
"requires": {
"execa": "^0.8.0",
"is-ci": "^1.0.10",
"normalize-path": "^1.0.0",
"strip-indent": "^2.0.0"
},
"dependencies": {
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"dev": true,
"requires": {
"lru-cache": "^4.0.1",
"shebang-command": "^1.2.0",
"which": "^1.2.9"
}
},
"execa": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz",
"integrity": "sha1-2NdrvBtVIX7RkP1t1J08d07PyNo=",
"dev": true,
"requires": {
"cross-spawn": "^5.0.1",
"get-stream": "^3.0.0",
"is-stream": "^1.1.0",
"npm-run-path": "^2.0.0",
"p-finally": "^1.0.0",
"signal-exit": "^3.0.0",
"strip-eof": "^1.0.0"
}
},
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
},
"lru-cache": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"dev": true,
"requires": {
"pseudomap": "^1.0.2",
"yallist": "^2.1.2"
}
},
"normalize-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz",
"integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=",
"dev": true
},
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
"dev": true
}
}
} }
} }
} }

View file

@ -4,13 +4,11 @@
"private": true, "private": true,
"description": "User client for the OpenFlexure Microscope Server", "description": "User client for the OpenFlexure Microscope Server",
"author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)", "author": "OpenFlexure <contact@openflexure.org> (https://www.openflexure.org)",
"homepage": "https://gitlab.com/openflexure/openflexure-microscope-jsclient",
"publisher": "Bath Open Instrumentation Group",
"displayName": "OpenFlexure Microscope",
"license": "GNU General Public License v3.0 ",
"scripts": { "scripts": {
"serve": "vue-cli-service serve --mode development",
"build": "vue-cli-service build --mode production", "build": "vue-cli-service build --mode production",
"serve": "vue-cli-service serve --mode development" "lint": "vue-cli-service lint",
"prettier": "vue-cli-service lint --fix"
}, },
"dependencies": { "dependencies": {
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
@ -19,7 +17,9 @@
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^3.12.1", "@vue/cli-plugin-babel": "^3.12.1",
"@vue/cli-plugin-eslint": "^4.5.8",
"@vue/cli-service": "^4.5.6", "@vue/cli-service": "^4.5.6",
"@vue/eslint-config-prettier": "^6.0.0",
"axios": "^0.19.2", "axios": "^0.19.2",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"css-loader": "^3.6.0", "css-loader": "^3.6.0",
@ -30,12 +30,14 @@
"less": "^3.12.2", "less": "^3.12.2",
"less-loader": "^5.0.0", "less-loader": "^5.0.0",
"mdns-js": "^1.0.3", "mdns-js": "^1.0.3",
"prettier": "^1.19.1",
"uikit": "^3.5.7", "uikit": "^3.5.7",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-friendly-iframe": "^0.17.0", "vue-friendly-iframe": "^0.17.0",
"vue-plugin-load-script": "^1.3.2", "vue-plugin-load-script": "^1.3.2",
"vue-template-compiler": "^2.6.12", "vue-template-compiler": "^2.6.12",
"vue-tour": "^1.5.0", "vue-tour": "^1.5.0",
"vuejs-paginate": "^2.1.0",
"vuex": "^3.5.1" "vuex": "^3.5.1"
}, },
"postcss": { "postcss": {
@ -47,5 +49,9 @@
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
"not ie <= 8" "not ie <= 8"
] ],
"displayName": "OpenFlexure Microscope",
"homepage": "https://gitlab.com/openflexure/openflexure-microscope-jsclient",
"license": "GNU General Public License v3.0 ",
"publisher": "Bath Open Instrumentation Group"
} }

View file

@ -45,7 +45,7 @@ import loadingContent from "./components/loadingContent.vue";
import axios from "axios"; import axios from "axios";
var Mousetrap = require("mousetrap"); var Mousetrap = require("mousetrap");
Mousetrap.prototype.stopCallback = function(e, element, combo) { Mousetrap.prototype.stopCallback = function(e, element) {
// if the element has the class "mousetrap" then no need to stop // if the element has the class "mousetrap" then no need to stop
if ((" " + element.className + " ").indexOf(" mousetrap ") > -1) { if ((" " + element.className + " ").indexOf(" mousetrap ") > -1) {
return false; return false;

View file

@ -349,7 +349,7 @@ export default {
} }
#container-left { #container-left {
overflow: hidden; overflow: auto;
background-color: rgba(180, 180, 180, 0.025); background-color: rgba(180, 180, 180, 0.025);
width: 100%; width: 100%;
height: 100%; height: 100%;

View file

@ -1,5 +1,6 @@
<template> <template>
<div> <div>
<!-- eslint-disable-next-line vue/no-v-html -->
<p v-html="content"></p> <p v-html="content"></p>
</div> </div>
</template> </template>

View file

@ -200,13 +200,13 @@ export default {
// If task ends with an error // If task ends with an error
else if (result == "error") { else if (result == "error") {
// Pass the error string back with reject // Pass the error string back with reject
console.log("Rejecting pollTask due to error") console.log("Rejecting pollTask due to error");
reject(new Error(response.data.output)); reject(new Error(response.data.output));
} }
// If task ends with termination // If task ends with termination
else if (result == "cancelled") { else if (result == "cancelled") {
// Pass a generic termination error back with reject // Pass a generic termination error back with reject
console.log("Rejecting pollTask due to cancellation") console.log("Rejecting pollTask due to cancellation");
reject(new Error("Task cancelled")); reject(new Error("Task cancelled"));
} else { } else {
// Since the task is still running, we can update the progress bar // Since the task is still running, we can update the progress bar

View file

@ -17,7 +17,7 @@
</template> </template>
<script> <script>
import devTools from "./controlComponents/devTools.vue"; import devTools from "./tabContentComponents/aboutComponents/devTools.vue";
// Export main app // Export main app
export default { export default {

View file

@ -148,9 +148,9 @@
<script> <script>
import axios from "axios"; import axios from "axios";
import cameraCalibrationSettings from "../viewComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue"; import cameraCalibrationSettings from "../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue";
import CSMCalibrationSettings from "../viewComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue"; import CSMCalibrationSettings from "../tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue";
import miniStreamDisplay from "../viewComponents/miniStreamDisplay.vue"; import miniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
export default { export default {
name: "CalibrationModal", name: "CalibrationModal",

View file

@ -21,8 +21,8 @@
</template> </template>
<script> <script>
import devTools from "../controlComponents/devTools.vue"; import devTools from "./aboutComponents/devTools.vue";
import statusPane from "../viewComponents/statusPane.vue"; import statusPane from "./aboutComponents/statusPane.vue";
export default { export default {
name: "AboutContent", name: "AboutContent",

View file

@ -237,7 +237,6 @@
<option>Number</option> <option>Number</option>
</select> </select>
</div> </div>
</div> </div>
</div> </div>
</li> </li>
@ -269,10 +268,10 @@
<script> <script>
import axios from "axios"; import axios from "axios";
import tagList from "../fieldComponents/tagList"; import tagList from "../../fieldComponents/tagList";
import keyvalList from "../fieldComponents/keyvalList"; import keyvalList from "../../fieldComponents/keyvalList";
import taskSubmitter from "../genericComponents/taskSubmitter"; import taskSubmitter from "../../genericComponents/taskSubmitter";
// Export main app // Export main app
export default { export default {

View file

@ -11,8 +11,8 @@
</template> </template>
<script> <script>
import paneCapture from "../controlComponents/paneCapture"; import paneCapture from "./captureComponents/paneCapture";
import streamDisplay from "../viewComponents/streamDisplay.vue"; import streamDisplay from "./streamContent.vue";
export default { export default {
name: "NavigateContent", name: "NavigateContent",

View file

@ -28,8 +28,8 @@
</div> </div>
</div> </div>
<div class="view-component uk-width-expand"> <div class="view-component uk-width-expand">
<galleryDisplay v-if="viewPanel == 'gallery'" /> <galleryContent v-if="viewPanel == 'gallery'" />
<settingsDisplay v-else-if="viewPanel == 'settings'" /> <settingsContent v-else-if="viewPanel == 'settings'" />
<streamDisplay v-else /> <streamDisplay v-else />
</div> </div>
</div> </div>
@ -38,9 +38,9 @@
<script> <script>
import JsonForm from "../pluginComponents/JsonForm"; import JsonForm from "../pluginComponents/JsonForm";
import WebComponentLoader from "../pluginComponents/WebComponentLoader"; import WebComponentLoader from "../pluginComponents/WebComponentLoader";
import streamDisplay from "../viewComponents/streamDisplay.vue"; import streamDisplay from "./streamContent.vue";
import galleryDisplay from "../viewComponents/galleryDisplay.vue"; import galleryContent from "../tabContentComponents/galleryContent.vue";
import settingsDisplay from "../viewComponents/settingsDisplay.vue"; import settingsContent from "../tabContentComponents/settingsContent.vue";
export default { export default {
name: "ExtensionContent", name: "ExtensionContent",
@ -49,8 +49,8 @@ export default {
JsonForm, JsonForm,
WebComponentLoader, WebComponentLoader,
streamDisplay, streamDisplay,
galleryDisplay, galleryContent,
settingsDisplay settingsContent
}, },
props: { props: {

View file

@ -212,8 +212,9 @@ export default {
}, },
created: function() { created: function() {
this.getTagRequest(); // Get initial metadata from prop
this.getAnnotationsRequest(); this.tags = this.captureState.metadata.image.tags;
this.annotations = this.captureState.metadata.annotations;
}, },
methods: { methods: {

View file

@ -6,12 +6,12 @@
<a href="#"> <a href="#">
<img <img
class="uk-width-1-1" class="uk-width-1-1"
:data-src="thumbnail" :data-src="scanState.thumbnail"
:alt="metadata.image.id" :alt="scanState.metadata.image.id"
width="300" width="300"
height="225" height="225"
uk-img uk-img
@click="$root.$emit('globalUpdateCaptureFolder', metadata.image.id)" @click="onClick"
/> />
</a> </a>
</div> </div>
@ -22,7 +22,8 @@
uk-grid uk-grid
> >
<div class="uk-margin-remove-top uk-padding-remove uk-width-expand"> <div class="uk-margin-remove-top uk-padding-remove uk-width-expand">
<b>{{ metadata.type || "Dataset" }}: </b> {{ metadata.image.name }} <b>{{ scanState.metadata.type || "Dataset" }}: </b>
{{ scanState.metadata.image.name }}
</div> </div>
<div class="uk-margin-remove-top uk-padding-remove uk-width-auto"> <div class="uk-margin-remove-top uk-padding-remove uk-width-auto">
<a href="#" class="uk-icon" @click="delAllConfirm()"> <a href="#" class="uk-icon" @click="delAllConfirm()">
@ -34,7 +35,7 @@
<div <div
class="uk-text-meta uk-margin-remove-top uk-padding-remove uk-width-expand" class="uk-text-meta uk-margin-remove-top uk-padding-remove uk-width-expand"
> >
<time>{{ metadata.image.acquisitionDate }}</time> <time>{{ scanState.metadata.image.acquisitionDate }}</time>
</div> </div>
<div <div
class="uk-text-meta uk-margin-remove-top uk-padding-remove uk-width-auto" class="uk-text-meta uk-margin-remove-top uk-padding-remove uk-width-auto"
@ -45,7 +46,7 @@
<div class="uk-card-footer uk-padding-small"> <div class="uk-card-footer uk-padding-small">
<span <span
v-for="tag in metadata.image.tags" v-for="tag in scanState.metadata.image.tags"
:key="tag" :key="tag"
class="uk-label uk-margin-small-right deletable-label" class="uk-label uk-margin-small-right deletable-label"
> >
@ -56,13 +57,16 @@
<div :id="metadataModalID" uk-modal> <div :id="metadataModalID" uk-modal>
<div class="uk-modal-dialog uk-modal-body"> <div class="uk-modal-dialog uk-modal-body">
<button class="uk-modal-close-default" type="button" uk-close></button> <button class="uk-modal-close-default" type="button" uk-close></button>
<h2 class="uk-modal-title">{{ metadata.image.name }}</h2> <h2 class="uk-modal-title">{{ scanState.metadata.image.name }}</h2>
<p><b>Time: </b>{{ metadata.image.acquisitionDate }}</p> <p><b>Time: </b>{{ scanState.metadata.image.acquisitionDate }}</p>
<p><b>ID: </b>{{ metadata.image.id }}</p> <p><b>ID: </b>{{ scanState.metadata.image.id }}</p>
<hr /> <hr />
<div v-for="(value, key) in metadata.image.annotations" :key="key"> <div
v-for="(value, key) in scanState.metadata.image.annotations"
:key="key"
>
<p> <p>
<b>{{ key }}: </b>{{ value }} <b>{{ key }}: </b>{{ value }}
</p> </p>
@ -80,17 +84,9 @@ export default {
name: "ScanCard", name: "ScanCard",
props: { props: {
metadata: { scanState: {
type: Object, type: Object,
required: true required: true
},
thumbnail: {
type: String,
required: true
},
captures: {
type: Array,
required: true
} }
}, },
@ -109,7 +105,7 @@ export default {
}, },
allURLs: function() { allURLs: function() {
var urls = []; var urls = [];
for (var capture of this.captures) { for (var capture of this.scanState.captures) {
urls.push(capture.links.self.href); urls.push(capture.links.self.href);
} }
return urls; return urls;
@ -117,8 +113,12 @@ export default {
}, },
methods: { methods: {
onClick: function() {
this.$emit("selectFolder", this.scanState.metadata.image.id);
},
makeModalName: function(prefix) { makeModalName: function(prefix) {
return prefix + this.metadata.image.id; return prefix + this.scanState.metadata.image.id;
}, },
delAllConfirm: function() { delAllConfirm: function() {
@ -131,18 +131,10 @@ export default {
}, },
deleteAll: function() { deleteAll: function() {
axios axios.all(this.allURLs.map(l => axios.delete(l))).then(() => {
.all(this.allURLs.map(l => axios.delete(l))) // Emit signal to update capture list
.then this.$root.$emit("globalUpdateCaptures");
//axios.spread(function(...res) { });
// all requests are now complete
//console.log(res);
//})
()
.then(() => {
// Emit signal to update capture list
this.$root.$emit("globalUpdateCaptures");
});
} }
} }
}; };

View file

@ -1,19 +1,437 @@
<template> <template>
<!-- Grid managing tab content --> <div class="galleryDisplay uk-padding uk-padding-remove-top">
<!-- Gallery nav bar -->
<nav
class="gallery-navbar uk-navbar-container uk-navbar-transparent"
uk-navbar="mode: click"
>
<!-- Left side controls -->
<div
class="uk-navbar-left uk-padding-remove-top uk-padding-remove-bottom"
>
<ul class="uk-navbar-nav">
<li :class="[sortDescending ? 'uk-active' : '']">
<a class="uk-icon" href="#" @click="sortDescending = true">
<i class="material-icons">keyboard_arrow_down</i>
</a>
</li>
<li :class="[!sortDescending ? 'uk-active' : '']">
<a class="uk-icon" href="#" @click="sortDescending = false">
<i class="material-icons">keyboard_arrow_up</i>
</a>
</li>
<li>
<a href="#">Filter</a>
<div
:class="{
'uk-light uk-background-secondary':
$store.state.globalSettings.darkMode
}"
class="uk-navbar-dropdown"
>
<ul class="uk-nav uk-navbar-dropdown-nav">
<form class="uk-form-stacked">
<div
v-for="tag in allTags"
:key="tag"
class="uk-margin-small"
>
<label>
<input
:id="tag"
v-model="checkedTags"
:value="tag"
checked
class="uk-checkbox"
type="checkbox"
/>
{{ tag }}
</label>
</div>
</form>
</ul>
</div>
</li>
</ul>
</div>
<div class="view-component uk-height-1-1 uk-width-expand"> <!-- Right side buttons -->
<galleryDisplay /> <div class="uk-navbar-right">
<div class="uk-grid">
<div>
<button
class="uk-button uk-button-default uk-width-1-1"
type="button"
@click="updateCaptures()"
>
Refresh Captures
</button>
</div>
<div>
<ZipDownloader :capture-ids="Object.keys(filteredCaptures)" />
</div>
</div>
</div>
</nav>
<!-- Gallery -->
<div
v-if="$store.getters.ready"
class="uk-padding-remove-top"
uk-lightbox="toggle: .lightbox-link"
>
<!-- Folder heading -->
<div
v-if="galleryFolder"
class="gallery-folder-heading uk-flex uk-flex-middle"
>
<a class="uk-icon uk-margin-remove" href="#" @click="galleryBack()">
<i class="material-icons">arrow_back</i>
</a>
<div class="uk-margin-left">
<h3 class="uk-margin-remove uk-margin-left">
<b>SCAN</b>
{{ allScans[galleryFolder].metadata.image.name }}
</h3>
</div>
</div>
<!-- Gallery capture cards -->
<div class="gallery-grid">
<div v-for="item in pagedItems" :key="item.metadata.id">
<scanCard
v-if="'isScan' in item"
:scan-state="item"
@selectFolder="selectFolder"
/>
<captureCard v-else :capture-state="item" />
</div>
</div>
<Paginate
v-model="page"
:page-count="numberOfPages"
:page-range="3"
:margin-pages="1"
:container-class="'uk-pagination uk-flex-center'"
:prev-text="'Prev'"
:next-text="'Next'"
:page-class="'page-item'"
:disabled-class="'uk-disabled'"
>
</Paginate>
</div>
</div> </div>
</template> </template>
<script> <script>
import galleryDisplay from "../viewComponents/galleryDisplay.vue"; import axios from "axios";
import Paginate from "vuejs-paginate";
import captureCard from "./galleryComponents/captureCard.vue";
import scanCard from "./galleryComponents/scanCard.vue";
import ZipDownloader from "./galleryComponents/zipDownloader";
// Export main app
export default { export default {
name: "NavigateContent", name: "GalleryDisplay",
components: { components: {
galleryDisplay captureCard,
scanCard,
ZipDownloader,
Paginate
},
data: function() {
return {
captures: [],
checkedTags: [],
sortDescending: true,
galleryFolder: "",
scanTag: "scan",
unwatchStoreFunction: null,
maxitems: 10,
page: 1
};
},
computed: {
capturesUri: function() {
return `${this.$store.getters.baseUri}/api/v2/captures`;
},
allTags: function() {
// Return an array of unique tags across all captures
var tags = [];
for (var capture of this.captures) {
for (var tag of capture.metadata.image.tags) {
if (!tags.includes(tag)) {
tags.push(tag);
}
}
}
return tags.sort();
},
noScanCaptures: function() {
// List of captures that are not part of a scan
var captures = [];
for (var capture of this.captures) {
// Add to capture list if matched
if (!capture.metadata.dataset) {
captures.push(capture);
}
}
return captures;
},
allScans: function() {
// List of scans as capture-like objects
var scans = {};
for (var capture of this.captures) {
var annotations = capture.metadata.image.annotations;
var tags = capture.metadata.image.tags;
var dataset = capture.metadata.dataset;
if (dataset) {
var id = dataset["id"];
// If this scan ID hasn't been seen before
if (!(id in scans)) {
scans[id] = {};
scans[id].isScan = true;
scans[id].captures = [];
scans[id].metadata = {
image: {
name: dataset.name,
acquisitionDate: dataset.acquisitionDate,
id: dataset.id,
tags: [],
annotations: {}
},
type: dataset.type
};
}
// Add the capture object to the scan
scans[id].captures.push(capture);
// Add missing scan metadata, prioritising first capture
for (var key of Object.keys(annotations)) {
if (!(key in scans[id].metadata.image.annotations)) {
scans[id].metadata.image.annotations[key] = annotations[key];
}
}
// Append missing tags
for (var tag of tags) {
if (!scans[id].metadata.image.tags.includes(tag)) {
scans[id].metadata.image.tags.push(tag);
}
}
// Create a preview thumbnail
if (!("thumbnail" in scans[id])) {
scans[
id
].thumbnail = `${capture.links.download.href}?thumbnail=true`;
}
}
}
return scans;
},
scanList: function() {
// List of scans, obtained from this.allScans values
return Object.values(this.allScans);
},
itemList: function() {
// Get list of current items to show
// If galleryFolder (ie inside a scan folder), show scan captures
// Otherwise, show root captures and scan cards
if (this.galleryFolder) {
return this.allScans[this.galleryFolder].captures;
} else {
return this.noScanCaptures.concat(this.scanList);
}
},
filteredItems: function() {
// Filter itemList by checkedTags
return this.filterCaptures(this.itemList, this.checkedTags);
},
filteredCaptures: function() {
var captures = {};
for (var item of this.filteredItems) {
// If it's a dataset
if ("captures" in item) {
for (var capture of item.captures) {
// Get the ID of each capture in the set
captures[capture.metadata.image.id] = capture;
}
} else {
// If it's a single capture, get the ID of the capture
captures[item.metadata.image.id] = item;
}
}
return captures;
},
sortedItems: function() {
// Sort filteredItems using sortCaptures function
return this.sortCaptures(this.filteredItems);
},
pagedItems: function() {
let startIndex = (this.page - 1) * this.maxitems;
return this.sortedItems.slice(startIndex, startIndex + this.maxitems);
},
numberOfPages: function() {
return Math.floor(this.sortedItems.length / this.maxitems);
}
},
mounted() {
// Update on mount (does nothing if not connected)
this.updateCaptures();
// A global signal listener to perform a gallery refresh
this.$root.$on("globalUpdateCaptures", () => {
this.updateCaptures();
});
},
created: function() {
// Watch for host 'ready', then update status
this.unwatchStoreFunction = this.$store.watch(
(state, getters) => {
return getters.ready;
},
ready => {
if (ready) {
// If the connection is now ready, update capture list
this.updateCaptures();
} else {
// If the connection is now disconnected, empty capture list
this.captures = {};
}
}
);
},
beforeDestroy() {
// Remove global signal listener to perform a gallery refresh
this.$root.$off("globalUpdateCaptures");
// Then we call that function here to unwatch
if (this.unwatchStoreFunction) {
this.unwatchStoreFunction();
this.unwatchStoreFunction = null;
}
},
methods: {
updateCaptures: function() {
if (this.$store.state.available) {
console.log("Updating capture list...");
axios
.get(this.capturesUri)
.then(response => {
this.captures = response.data;
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
} else {
console.log("Delaying capture update until connection is available");
}
},
filterCaptures: function(list, filterTags) {
// Filter a list of captures by an array of tags
var result = [];
for (var capture of list) {
// Assume exclusion
var includeCapture = false;
// Filter by selected tags
var tags = capture.metadata.image.tags;
let checker = (arr, target) => target.every(v => arr.includes(v));
// True if all tags match
includeCapture = checker(tags, filterTags);
// Add to capture list if matched
if (includeCapture == true) {
result.push(capture);
}
}
return result;
},
sortCaptures: function(list) {
// Sort a list of captures by metadata time
function compare(a, b) {
if (a.metadata.image.acquisitionDate < b.metadata.image.acquisitionDate)
return -1;
if (a.metadata.image.acquisitionDate > b.metadata.image.acquisitionDate)
return 1;
return 0;
}
if (this.sortDescending == true) {
return list.sort(compare).reverse();
} else {
return list.sort(compare);
}
},
galleryBack: function() {
this.galleryFolder = "";
this.page = 1;
},
selectFolder: function(folderID) {
console.log(folderID);
this.galleryFolder = folderID;
}
} }
}; };
</script> </script>
<style lang="less" scoped>
.gallery-navbar {
border-width: 0 0 1px 0;
border-style: solid;
border-color: rgba(180, 180, 180, 0.25);
}
.gallery-navbar,
.gallery-folder-heading {
margin-bottom: 30px;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 320px);
justify-content: center;
overflow-y: auto;
}
.gallery-grid > div {
display: inline-block;
margin-bottom: 20px;
}
/deep/ .capture-card {
width: 300px;
height: 100%; // Used to have all cards in a row match their heights
margin-left: auto;
margin-right: auto;
}
</style>

View file

@ -158,7 +158,7 @@
<script> <script>
import axios from "axios"; import axios from "axios";
import taskSubmitter from "../genericComponents/taskSubmitter"; import taskSubmitter from "../../genericComponents/taskSubmitter";
// Export main app // Export main app
export default { export default {

View file

@ -11,8 +11,8 @@
</template> </template>
<script> <script>
import paneNavigate from "../controlComponents/paneNavigate"; import paneNavigate from "./navigateComponents/paneNavigate";
import streamDisplay from "../viewComponents/streamDisplay.vue"; import streamDisplay from "./streamContent.vue";
export default { export default {
name: "NavigateContent", name: "NavigateContent",

View file

@ -10,7 +10,7 @@
<script> <script>
import axios from "axios"; import axios from "axios";
import CSMCalibrationSettings from "./CSMSettingsComponents/CSMCalibrationSettings.vue"; import CSMCalibrationSettings from "./CSMSettingsComponents/CSMCalibrationSettings.vue";
import miniStreamDisplay from "../miniStreamDisplay.vue"; import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
// Export main app // Export main app
export default { export default {

View file

@ -69,7 +69,7 @@
<script> <script>
import axios from "axios"; import axios from "axios";
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue"; import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
import miniStreamDisplay from "../miniStreamDisplay.vue"; import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
// Export main app // Export main app
export default { export default {

View file

@ -8,11 +8,11 @@
> >
</p> </p>
<p class="uk-margin-small uk-width-xlarge"> <p class="uk-margin-small uk-width-xlarge">
Enabling IHI Slide Scan will add a new tab designed to simplify Enabling IHI Slide Scan will add a new tab designed to simplify acquiring
acquiring blood smear tile scans. </br> blood smear tile scans. <br />
While this may be useful for other applications, it is primarily designed While this may be useful for other applications, it is primarily designed
for use in clinics acquiring blood smear samples using a 100x oil-immersion for use in clinics acquiring blood smear samples using a 100x
objective, with a Raspberry Pi Camera v2. oil-immersion objective, with a Raspberry Pi Camera v2.
</p> </p>
</div> </div>
</template> </template>

View file

@ -4,7 +4,7 @@
<h3>Stage settings</h3> <h3>Stage settings</h3>
<p> <p>
<label class="uk-form-label"> <label class="uk-form-label">
Stage Geometry Stage geometry
<div v-if="stageType != 'MissingStage'"> <div v-if="stageType != 'MissingStage'">
<form @submit.prevent="setStageType"> <form @submit.prevent="setStageType">
<div class="uk-margin-small"> <div class="uk-margin-small">
@ -18,7 +18,7 @@
type="submit" type="submit"
class="uk-button uk-button-primary uk-width-1-1" class="uk-button uk-button-primary uk-width-1-1"
> >
Change stage type Change stage geometry
</button> </button>
</div> </div>
</form> </form>
@ -70,11 +70,15 @@ export default {
setStageType: function() { setStageType: function() {
console.log("Setting stage type"); console.log("Setting stage type");
axios axios
.put(this.stageTypeUri, this.stageType) .put(this.stageTypeUri, this.stageType,{
headers: {
'Content-Type': 'application/json',
}
})
.then(response => { .then(response => {
this.stageType = response.data; this.stageType = response.data;
console.log("Stage type set to " + this.stageType); console.log("Stage type set to " + this.stageType);
this.modalNotify("Stage type changed."); this.modalNotify("Stage geometry changed.");
}) })
.catch(error => { .catch(error => {
this.modalError(error); this.modalError(error);

View file

@ -1,20 +1,224 @@
<template> <template>
<!-- Grid managing tab content --> <!-- Grid managing tab content -->
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove"> <div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
<div class="settings-nav">
<ul class="uk-nav uk-nav-default">
<li class="uk-nav-header">Application settings</li>
<li>
<tabIcon
id="settings-display-icon"
tab-i-d="display"
:show-title="false"
:show-tooltip="false"
:require-connection="false"
:current-tab="currentTab"
@set-tab="setTab"
>
Display
</tabIcon>
</li>
<li>
<tabIcon
id="settings-features-icon"
tab-i-d="features"
:show-title="false"
:show-tooltip="false"
:require-connection="false"
:current-tab="currentTab"
@set-tab="setTab"
>
Features
</tabIcon>
</li>
<li class="uk-nav-header">Microscope settings</li>
<li>
<tabIcon
id="settings-camera-icon"
tab-i-d="camera"
:show-title="false"
:show-tooltip="false"
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
Camera
</tabIcon>
</li>
<li>
<tabIcon
id="settings-stage-icon"
tab-i-d="stage"
:show-title="false"
:show-tooltip="false"
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
Stage
</tabIcon>
</li>
<li>
<tabIcon
id="settings-mapping-icon"
tab-i-d="mapping"
:show-title="false"
:show-tooltip="false"
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
Camera/stage mapping
</tabIcon>
</li>
<li>
<tabIcon
id="settings-microscope-icon"
tab-i-d="microscope"
:show-title="false"
:show-tooltip="false"
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
General
</tabIcon>
</li>
</ul>
</div>
<div class="view-component uk-width-expand uk-padding-small"> <div class="view-component uk-width-expand uk-padding-small">
<settingsDisplay /> <tabContent
tab-i-d="display"
:require-connection="false"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<appSettings />
<streamSettings />
</div>
</tabContent>
<tabContent
tab-i-d="features"
:require-connection="false"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<featuresSettings />
</div>
</tabContent>
<tabContent
tab-i-d="camera"
:require-connection="true"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<cameraSettings />
</div>
</tabContent>
<tabContent
tab-i-d="stage"
:require-connection="true"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<stageSettings />
</div>
</tabContent>
<tabContent
tab-i-d="mapping"
:require-connection="true"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<h3>Camera/stage mapping</h3>
<p>
Camera/stage mapping allows the stage to move relative to the camera
view. This enables functions like click-to-move, and more precise
tile scans.
</p>
<CSMSettings />
</div>
</tabContent>
<tabContent
tab-i-d="microscope"
:require-connection="true"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<h3>Microscope settings</h3>
<microscopeSettings />
</div>
</tabContent>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import settingsDisplay from "../viewComponents/settingsDisplay.vue"; import streamSettings from "./settingsComponents/streamSettings.vue";
import microscopeSettings from "./settingsComponents/microscopeSettings.vue";
import cameraSettings from "./settingsComponents/cameraSettings.vue";
import appSettings from "./settingsComponents/appSettings.vue";
import featuresSettings from "./settingsComponents/featuresSettings.vue";
import CSMSettings from "./settingsComponents/CSMSettings.vue";
import stageSettings from "./settingsComponents/stageSettings.vue";
// Import generic components
import tabIcon from "../genericComponents/tabIcon";
import tabContent from "../genericComponents/tabContent";
// Export main app
export default { export default {
name: "NavigateContent", name: "SettingsContent",
components: { components: {
settingsDisplay streamSettings,
cameraSettings,
stageSettings,
microscopeSettings,
CSMSettings,
appSettings,
featuresSettings,
tabIcon,
tabContent
},
data: function() {
return {
selected: "display",
currentTab: "display"
};
},
methods: {
setTab: function(event, tab) {
if (!(this.currentTab == tab)) {
this.currentTab = tab;
}
}
} }
}; };
</script> </script>
<style lang="less" scoped>
// Custom UIkit CSS modifications
@import "../../assets/less/theme.less";
.settings-nav {
overflow-y: auto;
overflow-x: hidden;
width: 250px;
padding: 10px;
background-color: rgba(180, 180, 180, 0.03);
border-width: 0 1px 0 0;
border-style: solid;
border-color: rgba(180, 180, 180, 0.25);
}
.settings-nav li > a {
padding-left: 6px !important;
border-radius: @button-border-radius;
}
</style>

View file

@ -135,7 +135,7 @@
<script> <script>
import axios from "axios"; import axios from "axios";
import taskSubmitter from "../genericComponents/taskSubmitter"; import taskSubmitter from "../../genericComponents/taskSubmitter";
export default { export default {
components: { components: {

View file

@ -11,8 +11,8 @@
</template> </template>
<script> <script>
import paneSlideScan from "../controlComponents/paneSlideScan"; import paneSlideScan from "./slideScanComponents/paneSlideScan";
import streamDisplay from "../viewComponents/streamDisplay.vue"; import streamDisplay from "./streamContent.vue";
export default { export default {
name: "SlideScanContent", name: "SlideScanContent",

View file

@ -8,7 +8,7 @@
</template> </template>
<script> <script>
import streamDisplay from "../viewComponents/streamDisplay.vue"; import streamDisplay from "./streamContent.vue";
export default { export default {
name: "ViewContent", name: "ViewContent",

View file

@ -1,168 +0,0 @@
<template>
<div
class="hostCard uk-card uk-card-default uk-padding-remove uk-width-medium"
:class="{ 'uk-card-secondary': $store.state.globalSettings.darkMode }"
>
<div class="uk-card-media-top">
<div class="snapshot-container uk-width-1-1 uk-height-auto">
<img
class="uk-width-1-1"
:data-src="snapshotSrc"
:src="snapshotSrc"
width="300"
height="225"
/>
<div
v-if="!snapshotAvailable"
class="uk-position-cover uk-flex uk-flex-center uk-flex-middle"
>
Snapshot unavailable
</div>
</div>
</div>
<div class="uk-card-body uk-padding-small">
<div
class="uk-margin-small uk-margin-remove-left uk-margin-remove-right uk-flex uk-flex-middle"
>
<div class="uk-width-expand host-description">
<div class="host-description">
<b>{{ name }}</b>
</div>
<div class="host-description">{{ hostname }}:{{ port }}</div>
</div>
<a
v-if="deletable"
href="#"
class="uk-icon uk-width-auto host-delete"
@click="$emit('delete')"
><i class="material-icons">delete</i></a
>
</div>
</div>
<div class="uk-card-footer uk-padding-small">
<button
class="uk-button uk-button-default uk-form-small uk-float-right uk-margin uk-margin-remove-top uk-width-1-1"
@click="$emit('connect')"
>
Connect
</button>
</div>
</div>
</template>
<script>
import axios from "axios";
// Export main app
export default {
name: "HostCard",
props: {
name: {
type: String,
required: true
},
hostname: {
type: String,
required: true
},
port: {
type: Number,
required: true
},
deletable: {
type: Boolean,
required: false,
default: true
}
},
data: function() {
return {
snapshotRule: "/api/v2/streams/snapshot",
snapshotSrc: "",
snapshotAvailable: false,
polling: null
};
},
computed: {
routesURL: function() {
return `http://${this.hostname}:${this.port}/routes`;
},
snapshotURL: function() {
return `http://${this.hostname}:${this.port}${this.snapshotRule}`;
}
},
mounted() {
// Try to load the microscope snapshot when mounted
this.checkSnapshotAvailable();
},
beforeDestroy() {
// Clear the polling timer
if (this.polling !== null) {
clearInterval(this.polling);
}
},
methods: {
checkSnapshotAvailable: function() {
// Fetches the list of routes from the host, and starts
// a snapshot timer if the snapshot route is available
// Send tag request
axios
.get(this.routesURL)
.then(response => {
// If the host has a snapshot route available
if (this.snapshotRule in response.data) {
// Tell the view that a snapshot is available
this.snapshotAvailable = true;
// Update the snapshot URL
this.updateSnapshotURL();
// If not already polling, start polling
if (this.polling === null) {
this.updateSnapshotPoll();
}
}
})
.catch(() => {
// Tell the view that a snapshot is not available, and ignore
this.snapshotAvailable = false;
});
},
updateSnapshotURL: function() {
// Set the snapshot image src to the snapshot URL,
// adding a timestamp argument to act as a cache-breaker
this.snapshotSrc = `${this.snapshotURL}?${Date.now()}`;
},
updateSnapshotPoll: function() {
// Start a timer to call updateSnapshotURL periodically
this.polling = setInterval(() => {
this.updateSnapshotURL();
}, 15000);
}
}
};
</script>
<style>
.host-description {
text-overflow: ellipsis;
overflow: hidden;
}
.snapshot-container {
position: relative;
}
img[data-src][src*="data:image"] {
background: rgba(127, 127, 127, 0.1);
}
</style>

View file

@ -1,409 +0,0 @@
<template>
<div class="galleryDisplay uk-padding uk-padding-remove-top">
<!-- Gallery nav bar -->
<nav
class="gallery-navbar uk-navbar-container uk-navbar-transparent"
uk-navbar="mode: click"
>
<!-- Left side controls -->
<div
class="uk-navbar-left uk-padding-remove-top uk-padding-remove-bottom"
>
<ul class="uk-navbar-nav">
<li :class="[sortDescending ? 'uk-active' : '']">
<a class="uk-icon" href="#" @click="sortDescending = true">
<i class="material-icons">keyboard_arrow_down</i>
</a>
</li>
<li :class="[!sortDescending ? 'uk-active' : '']">
<a class="uk-icon" href="#" @click="sortDescending = false">
<i class="material-icons">keyboard_arrow_up</i>
</a>
</li>
<li>
<a href="#">Filter</a>
<div
class="uk-navbar-dropdown"
:class="{
'uk-light uk-background-secondary':
$store.state.globalSettings.darkMode
}"
>
<ul class="uk-nav uk-navbar-dropdown-nav">
<form class="uk-form-stacked">
<div
v-for="tag in allTags"
:key="tag"
class="uk-margin-small"
>
<label>
<input
:id="tag"
v-model="checkedTags"
class="uk-checkbox"
type="checkbox"
:value="tag"
checked
/>
{{ tag }}
</label>
</div>
</form>
</ul>
</div>
</li>
</ul>
</div>
<!-- Right side buttons -->
<div class="uk-navbar-right">
<div class="uk-grid">
<div>
<button
type="button"
class="uk-button uk-button-default uk-width-1-1"
@click="updateCaptures()"
>
Refresh Captures
</button>
</div>
<div>
<ZipDownloader :capture-ids="Object.keys(filteredCaptures)" />
</div>
</div>
</div>
</nav>
<!-- Gallery -->
<div
v-if="$store.getters.ready"
class="uk-padding-remove-top"
uk-lightbox="toggle: .lightbox-link"
>
<!-- Folder heading -->
<div
v-if="galleryFolder"
class="gallery-folder-heading uk-flex uk-flex-middle"
>
<a
href="#"
class="uk-icon uk-margin-remove"
@click="galleryFolder = ''"
>
<i class="material-icons">arrow_back</i>
</a>
<div class="uk-margin-left">
<h3 class="uk-margin-remove uk-margin-left">
<b>SCAN</b>
{{ allScans[galleryFolder].metadata.image.name }}
</h3>
</div>
</div>
<!-- Gallery capture cards -->
<div class="gallery-grid">
<div v-for="item in sortedItems" :key="item.metadata.id">
<scanCard
v-if="'isScan' in item"
:metadata="item.metadata"
:thumbnail="item.thumbnail"
:captures="item.captures"
/>
<captureCard v-else :capture-state="item" />
</div>
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
import captureCard from "./galleryComponents/captureCard.vue";
import scanCard from "./galleryComponents/scanCard.vue";
import ZipDownloader from "./galleryComponents/zipDownloader";
// Export main app
export default {
name: "GalleryDisplay",
components: {
captureCard,
scanCard,
ZipDownloader
},
data: function() {
return {
captures: [],
checkedTags: [],
sortDescending: true,
galleryFolder: "",
scanTag: "scan",
unwatchStoreFunction: null
};
},
computed: {
capturesUri: function() {
return `${this.$store.getters.baseUri}/api/v2/captures`;
},
allTags: function() {
// Return an array of unique tags across all captures
var tags = [];
for (var capture of this.captures) {
for (var tag of capture.metadata.image.tags) {
if (!tags.includes(tag)) {
tags.push(tag);
}
}
}
return tags.sort();
},
noScanCaptures: function() {
// List of captures that are not part of a scan
var captures = [];
for (var capture of this.captures) {
// Add to capture list if matched
if (!capture.metadata.dataset) {
captures.push(capture);
}
}
return captures;
},
allScans: function() {
// List of scans as capture-like objects
var scans = {};
for (var capture of this.captures) {
var annotations = capture.metadata.image.annotations;
var tags = capture.metadata.image.tags;
var dataset = capture.metadata.dataset;
if (dataset) {
var id = dataset["id"];
// If this scan ID hasn't been seen before
if (!(id in scans)) {
scans[id] = {};
scans[id].isScan = true;
scans[id].captures = [];
scans[id].metadata = {
image: {
name: dataset.name,
acquisitionDate: dataset.acquisitionDate,
id: dataset.id,
tags: [],
annotations: {}
},
type: dataset.type
};
}
// Add the capture object to the scan
scans[id].captures.push(capture);
// Add missing scan metadata, prioritising first capture
for (var key of Object.keys(annotations)) {
if (!(key in scans[id].metadata.image.annotations)) {
scans[id].metadata.image.annotations[key] = annotations[key];
}
}
// Append missing tags
for (var tag of tags) {
if (!scans[id].metadata.image.tags.includes(tag)) {
scans[id].metadata.image.tags.push(tag);
}
}
// Create a preview thumbnail
if (!("thumbnail" in scans[id])) {
scans[
id
].thumbnail = `${capture.links.download.href}?thumbnail=true`;
}
}
}
return scans;
},
scanList: function() {
// List of scans, obtained from this.allScans values
return Object.values(this.allScans);
},
itemList: function() {
// Get list of current items to show
// If galleryFolder (ie inside a scan folder), show scan captures
// Otherwise, show root captures and scan cards
if (this.galleryFolder) {
console.log(this.allScans[this.galleryFolder].captures);
return this.allScans[this.galleryFolder].captures;
} else {
return this.noScanCaptures.concat(this.scanList);
}
},
filteredItems: function() {
// Filter itemList by checkedTags
return this.filterCaptures(this.itemList, this.checkedTags);
},
filteredCaptures: function() {
var captures = {};
for (var item of this.filteredItems) {
// If it's a dataset
if ("captures" in item) {
for (var capture of item.captures) {
// Get the ID of each capture in the set
captures[capture.metadata.image.id] = capture;
}
} else {
// If it's a single capture, get the ID of the capture
captures[item.metadata.image.id] = item;
}
}
return captures;
},
sortedItems: function() {
// Sort filteredItems using sortCaptures function
return this.sortCaptures(this.filteredItems);
}
},
mounted() {
// Update on mount (does nothing if not connected)
this.updateCaptures();
// A global signal listener to perform a gallery refresh
this.$root.$on("globalUpdateCaptures", () => {
this.updateCaptures();
});
// A global signal listener to set the gallery folder
this.$root.$on("globalUpdateCaptureFolder", folder => {
this.galleryFolder = folder;
});
},
created: function() {
// Watch for host 'ready', then update status
this.unwatchStoreFunction = this.$store.watch(
(state, getters) => {
return getters.ready;
},
ready => {
if (ready) {
// If the connection is now ready, update capture list
this.updateCaptures();
} else {
// If the connection is now disconnected, empty capture list
this.captures = {};
}
}
);
},
beforeDestroy() {
// Remove global signal listener to perform a gallery refresh
this.$root.$off("globalUpdateCaptures");
// Remove global signal listener to set the gallery folder
this.$root.$off("globalUpdateCaptureFolder");
// Then we call that function here to unwatch
if (this.unwatchStoreFunction) {
this.unwatchStoreFunction();
this.unwatchStoreFunction = null;
}
},
methods: {
updateCaptures: function() {
if (this.$store.state.available) {
console.log("Updating capture list...");
axios
.get(this.capturesUri)
.then(response => {
this.captures = response.data;
})
.catch(error => {
this.modalError(error); // Let mixin handle error
});
} else {
console.log("Delaying capture update until connection is available");
}
},
filterCaptures: function(list, filterTags) {
// Filter a list of captures by an array of tags
var result = [];
for (var capture of list) {
// Assume exclusion
var includeCapture = false;
// Filter by selected tags
var tags = capture.metadata.image.tags;
let checker = (arr, target) => target.every(v => arr.includes(v));
// True if all tags match
includeCapture = checker(tags, filterTags);
// Add to capture list if matched
if (includeCapture == true) {
result.push(capture);
}
}
return result;
},
sortCaptures: function(list) {
// Sort a list of captures by metadata time
function compare(a, b) {
if (a.metadata.image.acquisitionDate < b.metadata.image.acquisitionDate)
return -1;
if (a.metadata.image.acquisitionDate > b.metadata.image.acquisitionDate)
return 1;
return 0;
}
if (this.sortDescending == true) {
return list.sort(compare).reverse();
} else {
return list.sort(compare);
}
}
}
};
</script>
<style scoped lang="less">
.gallery-navbar {
border-width: 0 0 1px 0;
border-style: solid;
border-color: rgba(180, 180, 180, 0.25);
}
.gallery-navbar,
.gallery-folder-heading {
margin-bottom: 30px;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, 320px);
justify-content: center;
}
.gallery-grid > div {
display: inline-block;
margin-bottom: 20px;
}
/deep/ .capture-card {
width: 300px;
height: 100%; // Used to have all cards in a row match their heights
margin-left: auto;
margin-right: auto;
}
</style>

View file

@ -1,224 +0,0 @@
<template>
<!-- Grid managing tab content -->
<div uk-grid class="uk-height-1-1 uk-margin-remove uk-padding-remove">
<div class="settings-nav">
<ul class="uk-nav uk-nav-default">
<li class="uk-nav-header">Application settings</li>
<li>
<tabIcon
id="settings-display-icon"
tab-i-d="display"
:show-title="false"
:show-tooltip="false"
:require-connection="false"
:current-tab="currentTab"
@set-tab="setTab"
>
Display
</tabIcon>
</li>
<li>
<tabIcon
id="settings-features-icon"
tab-i-d="features"
:show-title="false"
:show-tooltip="false"
:require-connection="false"
:current-tab="currentTab"
@set-tab="setTab"
>
Features
</tabIcon>
</li>
<li class="uk-nav-header">Microscope settings</li>
<li>
<tabIcon
id="settings-camera-icon"
tab-i-d="camera"
:show-title="false"
:show-tooltip="false"
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
Camera
</tabIcon>
</li>
<li>
<tabIcon
id="settings-stage-icon"
tab-i-d="stage"
:show-title="false"
:show-tooltip="false"
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
Stage
</tabIcon>
</li>
<li>
<tabIcon
id="settings-mapping-icon"
tab-i-d="mapping"
:show-title="false"
:show-tooltip="false"
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
Camera/stage mapping
</tabIcon>
</li>
<li>
<tabIcon
id="settings-microscope-icon"
tab-i-d="microscope"
:show-title="false"
:show-tooltip="false"
:require-connection="true"
:current-tab="currentTab"
@set-tab="setTab"
>
General
</tabIcon>
</li>
</ul>
</div>
<div class="view-component uk-width-expand uk-padding-small">
<tabContent
tab-i-d="display"
:require-connection="false"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<appSettings />
<streamSettings />
</div>
</tabContent>
<tabContent
tab-i-d="features"
:require-connection="false"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<featuresSettings />
</div>
</tabContent>
<tabContent
tab-i-d="camera"
:require-connection="true"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<cameraSettings />
</div>
</tabContent>
<tabContent
tab-i-d="stage"
:require-connection="true"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<stageSettings />
</div>
</tabContent>
<tabContent
tab-i-d="mapping"
:require-connection="true"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<h3>Camera/stage mapping</h3>
<p>
Camera/stage mapping allows the stage to move relative to the camera
view. This enables functions like click-to-move, and more precise
tile scans.
</p>
<CSMSettings />
</div>
</tabContent>
<tabContent
tab-i-d="microscope"
:require-connection="true"
:current-tab="currentTab"
>
<div class="settings-pane uk-padding-small">
<h3>Microscope settings</h3>
<microscopeSettings />
</div>
</tabContent>
</div>
</div>
</template>
<script>
import streamSettings from "./settingsComponents/streamSettings.vue";
import microscopeSettings from "./settingsComponents/microscopeSettings.vue";
import cameraSettings from "./settingsComponents/cameraSettings.vue";
import appSettings from "./settingsComponents/appSettings.vue";
import featuresSettings from "./settingsComponents/featuresSettings.vue";
import CSMSettings from "./settingsComponents/CSMSettings.vue";
import stageSettings from "./settingsComponents/stageSettings.vue";
// Import generic components
import tabIcon from "../genericComponents/tabIcon";
import tabContent from "../genericComponents/tabContent";
// Export main app
export default {
name: "SettingsDisplay",
components: {
streamSettings,
cameraSettings,
stageSettings,
microscopeSettings,
CSMSettings,
appSettings,
featuresSettings,
tabIcon,
tabContent
},
data: function() {
return {
selected: "display",
currentTab: "display"
};
},
methods: {
setTab: function(event, tab) {
if (!(this.currentTab == tab)) {
this.currentTab = tab;
}
}
}
};
</script>
<style lang="less" scoped>
// Custom UIkit CSS modifications
@import "../../assets/less/theme.less";
.settings-nav {
overflow-y: auto;
overflow-x: hidden;
width: 250px;
padding: 10px;
background-color: rgba(180, 180, 180, 0.03);
border-width: 0 1px 0 0;
border-style: solid;
border-color: rgba(180, 180, 180, 0.25);
}
.settings-nav li > a {
padding-left: 6px !important;
border-radius: @button-border-radius;
}
</style>

View file

@ -484,6 +484,7 @@ class PiCameraStreamer(BaseCamera):
use_video_port: bool = False, use_video_port: bool = False,
resize: Tuple[int, int] = None, resize: Tuple[int, int] = None,
bayer: bool = True, bayer: bool = True,
thumbnail: tuple = None,
): ):
""" """
Capture a still image to a StreamObject. Capture a still image to a StreamObject.
@ -515,6 +516,7 @@ class PiCameraStreamer(BaseCamera):
resize=resize, resize=resize,
bayer=(not use_video_port) and bayer, bayer=(not use_video_port) and bayer,
use_video_port=use_video_port, use_video_port=use_video_port,
thumbnail=thumbnail,
) )
# Set resolution and start stream recording if necessary # Set resolution and start stream recording if necessary

View file

@ -8,15 +8,12 @@ import uuid
from collections import OrderedDict from collections import OrderedDict
import dateutil.parser import dateutil.parser
from PIL import Image
from openflexure_microscope.camera import piexif from openflexure_microscope.camera import piexif
from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError
from openflexure_microscope.config import JSONEncoder from openflexure_microscope.config import JSONEncoder
PIL_FORMATS = ["JPG", "JPEG", "PNG", "TIF", "TIFF"]
EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"] EXIF_FORMATS = ["JPG", "JPEG", "TIF", "TIFF"]
THUMBNAIL_SIZE = (200, 150)
def pull_usercomment_dict(filepath): def pull_usercomment_dict(filepath):
@ -145,9 +142,6 @@ class CaptureObject(object):
# List for storing tags # List for storing tags
self.tags = [] self.tags = []
# Thumbnail (populated only for PIL captures)
self.thumb_bytes = None
def write(self, s): def write(self, s):
logging.debug("Writing to %s", self) logging.debug("Writing to %s", self)
self.stream.write(s) self.stream.write(s)
@ -349,21 +343,12 @@ class CaptureObject(object):
""" """
Returns a thumbnail of the capture data, for supported image formats. Returns a thumbnail of the capture data, for supported image formats.
""" """
# If no thumbnail exists, try and make one exif_dict = piexif.load(self.file)
if not self.thumb_bytes: thumbnail = exif_dict.pop("thumbnail")
logging.info("Building thumbnail") if thumbnail:
self.thumb_bytes = io.BytesIO() return io.BytesIO(thumbnail)
if self.format.upper() in PIL_FORMATS: # If no thumbnail exists, serve the full image
im = Image.open(self.data) return self.data.getvalue()
im.thumbnail(THUMBNAIL_SIZE)
im.save(self.thumb_bytes, self.format)
self.thumb_bytes.seek(0)
else:
self.thumb_bytes.seek(0)
# Copy the buffer, to avoid closing the file
data = io.BytesIO(self.thumb_bytes.getbuffer())
return data
def save(self) -> None: def save(self) -> None:
"""Write stream to file, and save/update metadata file""" """Write stream to file, and save/update metadata file"""

View file

@ -144,6 +144,9 @@ class Microscope:
try: try:
logging.info("Trying SangaStage") logging.info("Trying SangaStage")
self.stage = SangaStage(port=stage_port) self.stage = SangaStage(port=stage_port)
logging.info("Saving new SangaStage type configuration")
configuration["stage"]["type"] = stage_type
self.configuration_file.save(configuration)
except Exception as e: # pylint: disable=W0703 except Exception as e: # pylint: disable=W0703
logging.error(e) logging.error(e)
logging.warning("No compatible Sangaboard hardware found.") logging.warning("No compatible Sangaboard hardware found.")
@ -151,17 +154,15 @@ class Microscope:
try: try:
logging.info("Trying SangaDeltaStage") logging.info("Trying SangaDeltaStage")
self.stage = SangaDeltaStage(port=stage_port) self.stage = SangaDeltaStage(port=stage_port)
logging.info("Saving new SangaDeltaStage type configuration")
configuration["stage"]["type"] = stage_type
self.configuration_file.save(configuration)
except Exception as e: # pylint: disable=W0703 except Exception as e: # pylint: disable=W0703
logging.error(e) logging.error(e)
logging.warning("No compatible Sangaboard hardware found.") logging.warning("No compatible Sangaboard hardware found.")
else: else:
logging.warning("The stage type is incorrectly defined.") logging.warning("The stage type is incorrectly defined.")
logging.info("Saving new stage type configuration")
configuration["stage"]["type"] = stage_type
self.configuration_file.save(configuration)
def has_real_stage(self) -> bool: def has_real_stage(self) -> bool:
""" """
Check if a real (non-mock) stage is currently attached. Check if a real (non-mock) stage is currently attached.
@ -384,6 +385,9 @@ class Microscope:
) )
# Capture to output object # Capture to output object
extras = {}
if fmt == "jpeg":
extras["thumbnail"] = (200, 150, 85)
logging.info("Starting microscope capture %s", output.file) logging.info("Starting microscope capture %s", output.file)
self.camera.capture( self.camera.capture(
output, output,
@ -391,6 +395,7 @@ class Microscope:
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
fmt=fmt, fmt=fmt,
**extras,
) )
output.put_and_save(tags, annotations, full_metadata) output.put_and_save(tags, annotations, full_metadata)

View file

@ -60,7 +60,7 @@ pylint = "^2.3"
black = {version = "^18.3-alpha.0",allow-prereleases = true} black = {version = "^18.3-alpha.0",allow-prereleases = true}
[tool.black] [tool.black]
exclude = '(\.eggs|\.git|\.venv)' exclude = '(\.eggs|\.git|\.venv|node_modules/)'
[tool.isort] [tool.isort]
multi_line_output = 3 multi_line_output = 3