mirror of
				https://github.com/simon987/sist2.git
				synced 2025-10-30 23:46:52 +00:00 
			
		
		
		
	
						commit
						4eea376869
					
				| @ -23,3 +23,4 @@ third-party/libscan/libscan-test-files/ | |||||||
| Dockerfile | Dockerfile | ||||||
| *.idx/ | *.idx/ | ||||||
| VERSION | VERSION | ||||||
|  | sist2-vue/node_modules/ | ||||||
| @ -70,3 +70,4 @@ steps: | |||||||
|       target: /files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/arm_${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/ |       target: /files/sist2/${DRONE_REPO_OWNER}_${DRONE_REPO_NAME}/arm_${DRONE_BRANCH}_${DRONE_BUILD_NUMBER}_${DRONE_COMMIT}/ | ||||||
|       source: |       source: | ||||||
|         - ./sist2-arm64-linux |         - ./sist2-arm64-linux | ||||||
|  |         - ./sist2-arm64-linux-debug | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -9,6 +9,7 @@ Makefile | |||||||
| *.out | *.out | ||||||
| LOG | LOG | ||||||
| sist2* | sist2* | ||||||
|  | !sist2-vue/ | ||||||
| index.sist2/ | index.sist2/ | ||||||
| bundle*.css | bundle*.css | ||||||
| bundle.js | bundle.js | ||||||
| @ -17,4 +18,9 @@ vgcore.* | |||||||
| build/ | build/ | ||||||
| third-party/ | third-party/ | ||||||
| *.idx/ | *.idx/ | ||||||
| VERSION | VERSION | ||||||
|  | git_hash.h | ||||||
|  | Testing/ | ||||||
|  | test_i | ||||||
|  | test_i_inc | ||||||
|  | node_modules/ | ||||||
| @ -4,8 +4,18 @@ set(CMAKE_C_STANDARD 11) | |||||||
| project(sist2 C) | project(sist2 C) | ||||||
| 
 | 
 | ||||||
| option(SIST_DEBUG "Build a debug executable" on) | option(SIST_DEBUG "Build a debug executable" on) | ||||||
|  | option(SIST_FAKE_STORE "Disable IO operations of LMDB stores for debugging purposes" 0) | ||||||
|  | 
 | ||||||
|  | add_compile_definitions( | ||||||
|  |         "SIST_PLATFORM=${SIST_PLATFORM}" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | if (SIST_DEBUG) | ||||||
|  |     add_compile_definitions( | ||||||
|  |             "SIST_DEBUG=${SIST_DEBUG}" | ||||||
|  |     ) | ||||||
|  | endif() | ||||||
| 
 | 
 | ||||||
| set(BUILD_TESTS on) |  | ||||||
| add_subdirectory(third-party/libscan) | add_subdirectory(third-party/libscan) | ||||||
| set(ARGPARSE_SHARED off) | set(ARGPARSE_SHARED off) | ||||||
| add_subdirectory(third-party/argparse) | add_subdirectory(third-party/argparse) | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ MAINTAINER simon987 <me@simon987.net> | |||||||
| 
 | 
 | ||||||
| WORKDIR /build/ | WORKDIR /build/ | ||||||
| ADD . /build/ | ADD . /build/ | ||||||
| RUN cmake -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake . | RUN cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake . | ||||||
| RUN make -j$(nproc) | RUN make -j$(nproc) | ||||||
| RUN strip sist2 | RUN strip sist2 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ MAINTAINER simon987 <me@simon987.net> | |||||||
| 
 | 
 | ||||||
| WORKDIR /build/ | WORKDIR /build/ | ||||||
| ADD . /build/ | ADD . /build/ | ||||||
| RUN cmake -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake . | RUN cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake . | ||||||
| RUN make -j$(nproc) | RUN make -j$(nproc) | ||||||
| RUN strip sist2 | RUN strip sist2 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -38,12 +38,12 @@ sist2 (Simple incremental search tool) | |||||||
|     1. *(or)* Run using docker: |     1. *(or)* Run using docker: | ||||||
|         ```bash |         ```bash | ||||||
|        docker run -d --name es1 --net sist2_net -p 9200:9200 \ |        docker run -d --name es1 --net sist2_net -p 9200:9200 \ | ||||||
|             -e "discovery.type=single-node" elasticsearch:7.5.2 |             -e "discovery.type=single-node" elasticsearch:7.14.0 | ||||||
|         ``` |         ``` | ||||||
|     1. *(or)* Run using docker-compose: |     1. *(or)* Run using docker-compose: | ||||||
|         ```yaml |         ```yaml | ||||||
|           elasticsearch: |           elasticsearch: | ||||||
|             image: docker.elastic.co/elasticsearch/elasticsearch:7.5.2 |             image: docker.elastic.co/elasticsearch/elasticsearch:7.14.0 | ||||||
|             environment: |             environment: | ||||||
|               - discovery.type=single-node |               - discovery.type=single-node | ||||||
|               - "ES_JAVA_OPTS=-Xms1G -Xmx2G" |               - "ES_JAVA_OPTS=-Xms1G -Xmx2G" | ||||||
|  | |||||||
| @ -2,18 +2,16 @@ | |||||||
| 
 | 
 | ||||||
| VCPKG_ROOT="/vcpkg" | VCPKG_ROOT="/vcpkg" | ||||||
| 
 | 
 | ||||||
| rm *.gz &>/dev/null |  | ||||||
| 
 |  | ||||||
| git submodule update --init --recursive | git submodule update --init --recursive | ||||||
| 
 | 
 | ||||||
| rm -rf CMakeFiles CMakeCache.txt | rm -rf CMakeFiles CMakeCache.txt | ||||||
| cmake -DSIST_DEBUG=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" . | cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" . | ||||||
| make -j $(nproc) | make -j $(nproc) | ||||||
| strip sist2 | strip sist2 | ||||||
| ./sist2 -v > VERSION | ./sist2 -v > VERSION | ||||||
| mv sist2 sist2-x64-linux | mv sist2 sist2-x64-linux | ||||||
| 
 | 
 | ||||||
| rm -rf CMakeFiles CMakeCache.txt | rm -rf CMakeFiles CMakeCache.txt | ||||||
| cmake -DSIST_DEBUG=on -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" . | cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=on -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" . | ||||||
| make -j  $(nproc) | make -j  $(nproc) | ||||||
| mv sist2_debug sist2-x64-linux-debug | mv sist2_debug sist2-x64-linux-debug | ||||||
| @ -2,12 +2,16 @@ | |||||||
| 
 | 
 | ||||||
| VCPKG_ROOT="/vcpkg" | VCPKG_ROOT="/vcpkg" | ||||||
| 
 | 
 | ||||||
| rm *.gz &>/dev/null |  | ||||||
| 
 |  | ||||||
| git submodule update --init --recursive | git submodule update --init --recursive | ||||||
| 
 | 
 | ||||||
| rm -rf CMakeFiles CMakeCache.txt | rm -rf CMakeFiles CMakeCache.txt | ||||||
| cmake -DSIST_DEBUG=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" . | cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" . | ||||||
| make -j $(nproc) | make -j $(nproc) | ||||||
| strip sist2 | strip sist2 | ||||||
| mv sist2 sist2-arm64-linux | mv sist2 sist2-arm64-linux | ||||||
|  | 
 | ||||||
|  | rm -rf CMakeFiles CMakeCache.txt | ||||||
|  | cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=on -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" . | ||||||
|  | make -j $(nproc) | ||||||
|  | strip sist2 | ||||||
|  | mv sist2 sist2-arm64-linux-debug | ||||||
| @ -47,7 +47,7 @@ | |||||||
|       "index": false |       "index": false | ||||||
|     }, |     }, | ||||||
|     "duration": { |     "duration": { | ||||||
|       "type": "float", |       "type": "integer", | ||||||
|       "index": false |       "index": false | ||||||
|     }, |     }, | ||||||
|     "width": { |     "width": { | ||||||
| @ -134,7 +134,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "tag": { |     "tag": { | ||||||
|       "type": "keyword", |       "type": "text", | ||||||
|  |       "fielddata": true, | ||||||
|  |       "analyzer": "tag_analyzer", | ||||||
|       "copy_to": "suggest-tag" |       "copy_to": "suggest-tag" | ||||||
|     }, |     }, | ||||||
|     "suggest-tag": { |     "suggest-tag": { | ||||||
|  | |||||||
| @ -7,7 +7,12 @@ | |||||||
|   "analysis": { |   "analysis": { | ||||||
|     "tokenizer": { |     "tokenizer": { | ||||||
|       "path_tokenizer": { |       "path_tokenizer": { | ||||||
|         "type": "path_hierarchy" |         "type": "path_hierarchy", | ||||||
|  |         "delimiter": "/" | ||||||
|  |       }, | ||||||
|  |       "tag_tokenizer": { | ||||||
|  |         "type": "path_hierarchy", | ||||||
|  |         "delimiter": "." | ||||||
|       }, |       }, | ||||||
|       "my_nGram_tokenizer": { |       "my_nGram_tokenizer": { | ||||||
|         "type": "nGram", |         "type": "nGram", | ||||||
| @ -22,6 +27,12 @@ | |||||||
|           "lowercase" |           "lowercase" | ||||||
|         ] |         ] | ||||||
|       }, |       }, | ||||||
|  |       "tag_analyzer": { | ||||||
|  |         "tokenizer": "tag_tokenizer", | ||||||
|  |         "filter": [ | ||||||
|  |           "lowercase" | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|       "case_insensitive_kw_analyzer": { |       "case_insensitive_kw_analyzer": { | ||||||
|         "tokenizer": "keyword", |         "tokenizer": "keyword", | ||||||
|         "filter": [ |         "filter": [ | ||||||
|  | |||||||
| @ -2,16 +2,9 @@ | |||||||
| 
 | 
 | ||||||
| rm -rf index.sist2/ | rm -rf index.sist2/ | ||||||
| 
 | 
 | ||||||
| rm src/static/js/bundle.js 2> /dev/null |  | ||||||
| cat `ls src/static/js/*.min.js` > src/static/js/bundle.js |  | ||||||
| cat src/static/js/{util,dom}.js >> src/static/js/bundle.js |  | ||||||
| 
 |  | ||||||
| rm src/static/css/bundle*.css 2> /dev/null |  | ||||||
| cat src/static/css/*.min.css > src/static/css/bundle.css |  | ||||||
| cat src/static/css/light.css >> src/static/css/bundle.css |  | ||||||
| cat src/static/css/*.min.css > src/static/css/bundle_dark.css |  | ||||||
| cat src/static/css/dark.css >> src/static/css/bundle_dark.css |  | ||||||
| 
 |  | ||||||
| python3 scripts/mime.py > src/parsing/mime_generated.c | python3 scripts/mime.py > src/parsing/mime_generated.c | ||||||
| python3 scripts/serve_static.py > src/web/static_generated.c | python3 scripts/serve_static.py > src/web/static_generated.c | ||||||
| python3 scripts/index_static.py > src/index/static_generated.c | python3 scripts/index_static.py > src/index/static_generated.c | ||||||
|  | 
 | ||||||
|  | printf "static const char *const Sist2CommitHash = \"%s\";\n" $(git rev-parse HEAD) > src/git_hash.h | ||||||
|  | printf "static const char *const LibScanCommitHash = \"%s\";\n" $(cd third-party/libscan/ && git rev-parse HEAD) >> src/git_hash.h | ||||||
|  | |||||||
| @ -157,7 +157,6 @@ application/x-livescreen, ivy | |||||||
| application/x-lotus, wq1 | application/x-lotus, wq1 | ||||||
| application/x-lz4+json, jsonlz4 | application/x-lz4+json, jsonlz4 | ||||||
| application/x-lz4, lz4 | application/x-lz4, lz4 | ||||||
| application/x-lz4, lz4 |  | ||||||
| application/x-lzh-compressed, | application/x-lzh-compressed, | ||||||
| application/x-lzh, lzh | application/x-lzh, lzh | ||||||
| application/x-lzip, lz | application/x-lzip, lz | ||||||
|  | |||||||
| 
 | 
| @ -1,12 +1,10 @@ | |||||||
| files = [ | files = [ | ||||||
|     "src/static/css/bundle.css", |     "sist2-vue/src/assets/favicon.ico", | ||||||
|     "src/static/css/bundle_dark.css", |     "sist2-vue/dist/css/chunk-vendors.css", | ||||||
|     "src/static/js/bundle.js", |     "sist2-vue/dist/css/index.css", | ||||||
|     "src/static/js/search.js", |     "sist2-vue/dist/js/chunk-vendors.js", | ||||||
|     "src/static/img/sprite-skin-flat.png", |     "sist2-vue/dist/js/index.js", | ||||||
|     "src/static/img/sprite-skin-flat-dark.png", |     "sist2-vue/dist/index.html", | ||||||
|     "src/static/search.html", |  | ||||||
|     "src/static/stats.html", |  | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -15,6 +13,10 @@ def clean(filepath): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| for file in files: | for file in files: | ||||||
|     with open(file, "rb") as f: |     try: | ||||||
|         data = f.read() |         with open(file, "rb") as f: | ||||||
|  |             data = f.read() | ||||||
|  |     except: | ||||||
|  |         data = bytes([]) | ||||||
|  | 
 | ||||||
|     print("char %s[%d] = {%s};" % (clean(file), len(data), ",".join(str(int(b)) for b in data))) |     print("char %s[%d] = {%s};" % (clean(file), len(data), ",".join(str(int(b)) for b in data))) | ||||||
|  | |||||||
							
								
								
									
										23
									
								
								sist2-vue/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								sist2-vue/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | .DS_Store | ||||||
|  | node_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # local env files | ||||||
|  | .env.local | ||||||
|  | .env.*.local | ||||||
|  | 
 | ||||||
|  | # Log files | ||||||
|  | npm-debug.log* | ||||||
|  | yarn-debug.log* | ||||||
|  | yarn-error.log* | ||||||
|  | pnpm-debug.log* | ||||||
|  | 
 | ||||||
|  | # Editor directories and files | ||||||
|  | .idea | ||||||
|  | .vscode | ||||||
|  | *.suo | ||||||
|  | *.ntvs* | ||||||
|  | *.njsproj | ||||||
|  | *.sln | ||||||
|  | *.sw? | ||||||
|  | *.iml | ||||||
							
								
								
									
										5
									
								
								sist2-vue/babel.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								sist2-vue/babel.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | module.exports = { | ||||||
|  |     "presets": [ | ||||||
|  |         "@vue/cli-plugin-babel/preset" | ||||||
|  |     ] | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								sist2-vue/dist/css/chunk-vendors.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								sist2-vue/dist/css/chunk-vendors.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								sist2-vue/dist/css/index.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								sist2-vue/dist/css/index.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										3
									
								
								sist2-vue/dist/index.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								sist2-vue/dist/index.html
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>sist2</title><link href="css/chunk-vendors.css" rel="preload" as="style"><link href="css/index.css" rel="preload" as="style"><link href="js/chunk-vendors.js" rel="preload" as="script"><link href="js/index.js" rel="preload" as="script"><link href="css/chunk-vendors.css" rel="stylesheet"><link href="css/index.css" rel="stylesheet"></head><body><noscript><style>body { | ||||||
|  |             height: initial; | ||||||
|  |         }</style><div style="text-align: center; margin-top: 100px"><strong>We're sorry but sist2 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong><br><strong>Nous sommes désolés mais sist2 ne fonctionne pas correctement si JavaScript est activé. Veuillez l'activer pour continuer.</strong></div></noscript><div id="app"></div><script src="js/chunk-vendors.js"></script><script src="js/index.js"></script></body></html> | ||||||
							
								
								
									
										146
									
								
								sist2-vue/dist/js/chunk-vendors.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								sist2-vue/dist/js/chunk-vendors.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								sist2-vue/dist/js/index.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								sist2-vue/dist/js/index.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										27265
									
								
								sist2-vue/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										27265
									
								
								sist2-vue/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										55
									
								
								sist2-vue/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								sist2-vue/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | |||||||
|  | { | ||||||
|  |   "name": "sist2", | ||||||
|  |   "version": "2.11.0", | ||||||
|  |   "private": true, | ||||||
|  |   "scripts": { | ||||||
|  |     "serve": "vue-cli-service serve", | ||||||
|  |     "build": "vue-cli-service build --mode production" | ||||||
|  |   }, | ||||||
|  |   "dependencies": { | ||||||
|  |     "@egjs/vue-infinitegrid": "3.3.0", | ||||||
|  |     "axios": "^0.21.1", | ||||||
|  |     "bootstrap-vue": "^2.21.2", | ||||||
|  |     "core-js": "^3.6.5", | ||||||
|  |     "crypto-es": "^1.2.7", | ||||||
|  |     "d3": "^5.16.0", | ||||||
|  |     "date-fns": "^2.21.3", | ||||||
|  |     "dom-to-image": "^2.6.0", | ||||||
|  |     "fslightbox-vue": "file:../../../mnt/Hatchery/main/projects/sist2/fslightbox-vue-pro-1.3.1.tgz", | ||||||
|  |     "nouislider": "^15.2.0", | ||||||
|  |     "underscore": "^1.13.1", | ||||||
|  |     "vue": "^2.6.12", | ||||||
|  |     "vue-color": "^2.8.1", | ||||||
|  |     "vue-i18n": "^8.24.4", | ||||||
|  |     "vue-masonry-wall": "^0.3.2", | ||||||
|  |     "vue-multiselect": "^2.1.6", | ||||||
|  |     "vue-router": "^3.2.0", | ||||||
|  |     "vue-simple-suggest": "^1.11.1", | ||||||
|  |     "vuex": "^3.4.0" | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "@babel/polyfill": "^7.11.5", | ||||||
|  |     "@vue/cli-plugin-babel": "~4.5.0", | ||||||
|  |     "@vue/cli-plugin-router": "~4.5.0", | ||||||
|  |     "@vue/cli-plugin-typescript": "~4.5.0", | ||||||
|  |     "@vue/cli-plugin-vuex": "~4.5.0", | ||||||
|  |     "@vue/cli-service": "~4.5.0", | ||||||
|  |     "@vue/test-utils": "^1.0.3", | ||||||
|  |     "bootstrap": "^4.5.2", | ||||||
|  |     "inspire-tree": "^4.3.1", | ||||||
|  |     "inspire-tree-dom": "^4.0.6", | ||||||
|  |     "mutationobserver-shim": "^0.3.7", | ||||||
|  |     "popper.js": "^1.16.1", | ||||||
|  |     "portal-vue": "^2.1.7", | ||||||
|  |     "sass": "^1.26.11", | ||||||
|  |     "sass-loader": "^10.0.2", | ||||||
|  |     "typescript": "~4.1.5", | ||||||
|  |     "vue-cli-plugin-bootstrap-vue": "~0.7.0", | ||||||
|  |     "vue-template-compiler": "^2.6.11" | ||||||
|  |   }, | ||||||
|  |   "browserslist": [ | ||||||
|  |     "> 1%", | ||||||
|  |     "last 2 versions", | ||||||
|  |     "not dead" | ||||||
|  |   ] | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								sist2-vue/public/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								sist2-vue/public/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  |     <meta charset="utf-8"> | ||||||
|  |     <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||||
|  |     <meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'/> | ||||||
|  | 
 | ||||||
|  |     <title><%= htmlWebpackPlugin.options.title %></title> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  | <noscript> | ||||||
|  |     <style> | ||||||
|  |         body { | ||||||
|  |             height: initial; | ||||||
|  |         } | ||||||
|  |     </style> | ||||||
|  |     <div style="text-align: center; margin-top: 100px"> | ||||||
|  |         <strong> | ||||||
|  |             We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. | ||||||
|  |             Please enable it to continue. | ||||||
|  |         </strong> | ||||||
|  |         <br/> | ||||||
|  |         <strong> | ||||||
|  |             Nous sommes désolés mais <%= htmlWebpackPlugin.options.title %> ne fonctionne pas correctement | ||||||
|  |             si JavaScript est activé. | ||||||
|  |             Veuillez l'activer pour continuer. | ||||||
|  |         </strong> | ||||||
|  |     </div> | ||||||
|  | </noscript> | ||||||
|  | <div id="app"></div> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										312
									
								
								sist2-vue/src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								sist2-vue/src/App.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,312 @@ | |||||||
|  | <template> | ||||||
|  |   <div id="app" :class="getClass()"> | ||||||
|  |     <NavBar></NavBar> | ||||||
|  |     <router-view v-if="!configLoading"/> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import NavBar from "@/components/NavBar"; | ||||||
|  | import {mapGetters} from "vuex"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   components: {NavBar}, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       configLoading: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters(["optTheme"]), | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.$store.dispatch("loadConfiguration").then(() => { | ||||||
|  |       this.$root.$i18n.locale = this.$store.state.optLang; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     this.$store.subscribe((mutation) => { | ||||||
|  |       if (mutation.type === "setOptLang") { | ||||||
|  |         this.$root.$i18n.locale = mutation.payload; | ||||||
|  |         this.configLoading = true; | ||||||
|  |         window.setTimeout(() => this.configLoading = false, 10); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     getClass() { | ||||||
|  |       return { | ||||||
|  |         "theme-light": this.optTheme === "light", | ||||||
|  |         "theme-black": this.optTheme === "black", | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   , | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | html, body { | ||||||
|  |   height: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #app { | ||||||
|  |   /*font-family: Avenir, Helvetica, Arial, sans-serif;*/ | ||||||
|  |   -webkit-font-smoothing: antialiased; | ||||||
|  |   -moz-osx-font-smoothing: grayscale; | ||||||
|  |   /*text-align: center;*/ | ||||||
|  |   color: #2c3e50; | ||||||
|  |   padding-bottom: 1em; | ||||||
|  |   min-height: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /*Black theme*/ | ||||||
|  | .theme-black { | ||||||
|  |   background-color: #000; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .card, .theme-black .modal-content { | ||||||
|  |   background: #212121; | ||||||
|  |   color: #e0e0e0; | ||||||
|  |   border-radius: 1px; | ||||||
|  |   border: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | .theme-black .table { | ||||||
|  |   color: #e0e0e0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .table td, .theme-black .table th { | ||||||
|  |   border: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .table thead th { | ||||||
|  |   border-bottom: 1px solid #646464; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .custom-select { | ||||||
|  |   overflow: auto; | ||||||
|  |   background-color: #37474F; | ||||||
|  |   border: 1px solid #616161; | ||||||
|  |   color: #bdbdbd; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .custom-select:focus { | ||||||
|  |   border-color: #757575; | ||||||
|  |   outline: 0; | ||||||
|  |   box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .inspire-tree .selected > .wholerow, .theme-black .inspire-tree .selected > .title-wrap:hover + .wholerow { | ||||||
|  |   background: none !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .inspire-tree .icon-expand::before, .theme-black .inspire-tree .icon-collapse::before { | ||||||
|  |   background-color: black !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .inspire-tree .title { | ||||||
|  |   color: #eee; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .inspire-tree { | ||||||
|  |   font-weight: 400; | ||||||
|  |   font-size: 14px; | ||||||
|  |   font-family: Helvetica, Nueue, Verdana, sans-serif; | ||||||
|  |   max-height: 350px; | ||||||
|  |   overflow: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .inspire-tree [type=checkbox] { | ||||||
|  |   left: 22px !important; | ||||||
|  |   top: 7px !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .form-control { | ||||||
|  |   background-color: #37474F; | ||||||
|  |   border: 1px solid #616161; | ||||||
|  |   color: #dbdbdb !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .form-control:focus { | ||||||
|  |   background-color: #546E7A; | ||||||
|  |   color: #fff; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .input-group-text, .theme-black .default-input { | ||||||
|  |   background: #37474F !important; | ||||||
|  |   border: 1px solid #616161 !important; | ||||||
|  |   color: #dbdbdb !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black ::placeholder { | ||||||
|  |   color: #BDBDBD !important; | ||||||
|  |   opacity: 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .nav-tabs .nav-link { | ||||||
|  |   color: #e0e0e0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .nav-tabs .nav-item.show .nav-link, .theme-black .nav-tabs .nav-link.active { | ||||||
|  |   background-color: #212121; | ||||||
|  |   border-color: #616161 #616161 #212121; | ||||||
|  |   color: #e0e0e0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .nav-tabs .nav-link:focus, .theme-black .nav-tabs .nav-link:focus { | ||||||
|  |   border-color: #616161 #616161 #212121; | ||||||
|  |   color: #e0e0e0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .nav-tabs .nav-link:focus, .theme-black .nav-tabs .nav-link:hover { | ||||||
|  |   border-color: #e0e0e0 #e0e0e0 #212121; | ||||||
|  |   color: #e0e0e0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .nav-tabs { | ||||||
|  |   border-bottom: #616161; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black a:hover, .theme-black .btn:hover { | ||||||
|  |   color: #fff; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .b-dropdown a:hover { | ||||||
|  |   color: inherit; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .btn { | ||||||
|  |   color: #eee; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .modal-header .close { | ||||||
|  |   color: #e0e0e0; | ||||||
|  |   text-shadow: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .modal-header { | ||||||
|  |   border-bottom: 1px solid #646464; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* -------------------------- */ | ||||||
|  | 
 | ||||||
|  | #nav { | ||||||
|  |   padding: 30px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #nav a { | ||||||
|  |   font-weight: bold; | ||||||
|  |   color: #2c3e50; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #nav a.router-link-exact-active { | ||||||
|  |   color: #42b983; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .mobile { | ||||||
|  |   display: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .container { | ||||||
|  |   padding-top: 1em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media (max-width: 650px) { | ||||||
|  |   .mobile { | ||||||
|  |     display: initial; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .not-mobile { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .grid-single-column .fit { | ||||||
|  |     max-height: none !important; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .container { | ||||||
|  |     padding-left: 0; | ||||||
|  |     padding-right: 0; | ||||||
|  |     padding-top: 0 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .lightbox-caption { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .info-icon { | ||||||
|  |   width: 1rem; | ||||||
|  |   margin-right: 0.2rem; | ||||||
|  |   cursor: pointer; | ||||||
|  |   line-height: 1rem; | ||||||
|  |   height: 1rem; | ||||||
|  |   background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==); | ||||||
|  |   filter: brightness(45%); | ||||||
|  |   display: block; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .tabs { | ||||||
|  |   margin-top: 10px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .modal-title { | ||||||
|  |   text-overflow: ellipsis; | ||||||
|  |   overflow: hidden; | ||||||
|  |   white-space: nowrap; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media screen and (min-width: 1500px) { | ||||||
|  |   .container { | ||||||
|  |     max-width: 1440px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .noUi-connects { | ||||||
|  |   border-radius: 1px !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | mark { | ||||||
|  |   background: #fff217; | ||||||
|  |   border-radius: 0; | ||||||
|  |   padding: 1px 0; | ||||||
|  |   color: inherit; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black mark { | ||||||
|  |   background: rgba(251, 191, 41, 0.25); | ||||||
|  |   border-radius: 0; | ||||||
|  |   padding: 1px 0; | ||||||
|  |   color: inherit; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .content-div mark { | ||||||
|  |   background: rgba(251, 191, 41, 0.40); | ||||||
|  |   color: white; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .content-div { | ||||||
|  |   font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; | ||||||
|  |   font-size: 13px; | ||||||
|  |   padding: 1em; | ||||||
|  |   background-color: #f5f5f5; | ||||||
|  |   border: 1px solid #ccc; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   margin: 3px; | ||||||
|  |   white-space: normal; | ||||||
|  |   color: #000; | ||||||
|  |   overflow: hidden; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .content-div { | ||||||
|  |   background-color: #37474F; | ||||||
|  |   border: 1px solid #616161; | ||||||
|  |   color: #E0E0E0FF; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .graph { | ||||||
|  |   display: inline-block; | ||||||
|  |   width: 40%; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										382
									
								
								sist2-vue/src/Sist2Api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										382
									
								
								sist2-vue/src/Sist2Api.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,382 @@ | |||||||
|  | import axios from "axios"; | ||||||
|  | import {ext, strUnescape, lum} from "./util"; | ||||||
|  | import CryptoES from 'crypto-es'; | ||||||
|  | 
 | ||||||
|  | export interface EsTag { | ||||||
|  |     id: string | ||||||
|  |     count: number | ||||||
|  |     color: string | undefined | ||||||
|  |     isLeaf: boolean | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface Tag { | ||||||
|  |     style: string | ||||||
|  |     text: string | ||||||
|  |     rawText: string | ||||||
|  |     fg: string | ||||||
|  |     bg: string | ||||||
|  |     userTag: boolean | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface Index { | ||||||
|  |     name: string | ||||||
|  |     version: string | ||||||
|  |     id: string | ||||||
|  |     idPrefix: string | ||||||
|  |     timestamp: number | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface EsHit { | ||||||
|  |     _index: string | ||||||
|  |     _id: string | ||||||
|  |     _score: number | ||||||
|  |     _path_md5: string | ||||||
|  |     _type: string | ||||||
|  |     _tags: Tag[] | ||||||
|  |     _seq: number | ||||||
|  |     _source: { | ||||||
|  |         path: string | ||||||
|  |         size: number | ||||||
|  |         mime: string | ||||||
|  |         name: string | ||||||
|  |         extension: string | ||||||
|  |         index: string | ||||||
|  |         _depth: number | ||||||
|  |         mtime: number | ||||||
|  |         videoc: string | ||||||
|  |         audioc: string | ||||||
|  |         parent: string | ||||||
|  |         width: number | ||||||
|  |         height: number | ||||||
|  |         duration: number | ||||||
|  |         tag: string[] | ||||||
|  |     } | ||||||
|  |     _props: { | ||||||
|  |         isSubDocument: boolean | ||||||
|  |         isImage: boolean | ||||||
|  |         isGif: boolean | ||||||
|  |         isVideo: boolean | ||||||
|  |         isPlayableVideo: boolean | ||||||
|  |         isPlayableImage: boolean | ||||||
|  |         isAudio: boolean | ||||||
|  |         hasThumbnail: boolean | ||||||
|  |     } | ||||||
|  |     highlight: { | ||||||
|  |         name: string[] | undefined, | ||||||
|  |         content: string[] | undefined, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getIdPrefix(indices: Index[], id: string): string { | ||||||
|  |     for (let i = 4; i < 32; i++) { | ||||||
|  |         const prefix = id.slice(0, i); | ||||||
|  | 
 | ||||||
|  |         if (indices.filter(idx => idx.id.slice(0, i) == prefix).length == 1) { | ||||||
|  |             return prefix; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return id; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface EsResult { | ||||||
|  |     took: number | ||||||
|  | 
 | ||||||
|  |     hits: { | ||||||
|  |         // TODO: ES 6.X ?
 | ||||||
|  |         total: { | ||||||
|  |             value: number | ||||||
|  |         } | ||||||
|  |         hits: EsHit[] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     aggregations: any | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class Sist2Api { | ||||||
|  | 
 | ||||||
|  |     private baseUrl: string | ||||||
|  | 
 | ||||||
|  |     constructor(baseUrl: string) { | ||||||
|  |         this.baseUrl = baseUrl; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getSist2Info(): Promise<any> { | ||||||
|  |         return axios.get(`${this.baseUrl}i`).then(resp => { | ||||||
|  |             const indices = resp.data.indices as Index[]; | ||||||
|  | 
 | ||||||
|  |             resp.data.indices = indices.map(idx => { | ||||||
|  |                 return { | ||||||
|  |                     id: idx.id, | ||||||
|  |                     name: idx.name, | ||||||
|  |                     timestamp: idx.timestamp, | ||||||
|  |                     version: idx.version, | ||||||
|  |                     idPrefix: getIdPrefix(indices, idx.id) | ||||||
|  |                 } as Index; | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             return resp.data; | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     setHitProps(hit: EsHit): void { | ||||||
|  |         hit["_props"] = {} as any; | ||||||
|  | 
 | ||||||
|  |         const mimeCategory = hit._source.mime == null ? null : hit._source.mime.split("/")[0]; | ||||||
|  | 
 | ||||||
|  |         if ("parent" in hit._source) { | ||||||
|  |             hit._props.isSubDocument = true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ("thumbnail" in hit._source) { | ||||||
|  |             hit._props.hasThumbnail = true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         switch (mimeCategory) { | ||||||
|  |             case "image": | ||||||
|  |                 if (hit._source.videoc === "gif") { | ||||||
|  |                     hit._props.isGif = true; | ||||||
|  |                 } else { | ||||||
|  |                     hit._props.isImage = true; | ||||||
|  |                 } | ||||||
|  |                 if ("width" in hit._source && !hit._props.isSubDocument && hit._source.videoc !== "tiff" | ||||||
|  |                     && hit._source.videoc !== "raw" && hit._source.videoc !== "ppm") { | ||||||
|  |                     hit._props.isPlayableImage = true; | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case "video": | ||||||
|  |                 if ("videoc" in hit._source) { | ||||||
|  |                     hit._props.isVideo = true; | ||||||
|  |                 } | ||||||
|  |                 if (hit._props.isVideo) { | ||||||
|  |                     const videoc = hit._source.videoc; | ||||||
|  |                     const mime = hit._source.mime; | ||||||
|  | 
 | ||||||
|  |                     hit._props.isPlayableVideo = mime != null && | ||||||
|  |                         mime.startsWith("video/") && | ||||||
|  |                         !hit._props.isSubDocument && | ||||||
|  |                         hit._source.extension !== "mkv" && | ||||||
|  |                         hit._source.extension !== "avi" && | ||||||
|  |                         hit._source.extension !== "mov" && | ||||||
|  |                         videoc !== "hevc" && | ||||||
|  |                         videoc !== "mpeg1video" && | ||||||
|  |                         videoc !== "mpeg2video" && | ||||||
|  |                         videoc !== "wmv3"; | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case "audio": | ||||||
|  |                 if ("audioc" in hit._source && !hit._props.isSubDocument) { | ||||||
|  |                     hit._props.isAudio = true; | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     setHitTags(hit: EsHit): void { | ||||||
|  |         const tags = [] as Tag[]; | ||||||
|  | 
 | ||||||
|  |         const mimeCategory = hit._source.mime == null ? null : hit._source.mime.split("/")[0]; | ||||||
|  | 
 | ||||||
|  |         switch (mimeCategory) { | ||||||
|  |             case "image": | ||||||
|  |             case "video": | ||||||
|  |                 if ("videoc" in hit._source && hit._source.videoc) { | ||||||
|  |                     tags.push({ | ||||||
|  |                         style: "video", | ||||||
|  |                         text: hit._source.videoc.replace(" ", ""), | ||||||
|  |                         userTag: false | ||||||
|  |                     } as Tag); | ||||||
|  |                 } | ||||||
|  |                 break | ||||||
|  |             case "audio": | ||||||
|  |                 if ("audioc" in hit._source && hit._source.audioc) { | ||||||
|  |                     tags.push({ | ||||||
|  |                         style: "audio", | ||||||
|  |                         text: hit._source.audioc, | ||||||
|  |                         userTag: false | ||||||
|  |                     } as Tag); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // User tags
 | ||||||
|  |         if ("tag" in hit._source) { | ||||||
|  |             hit._source.tag.forEach(tag => { | ||||||
|  |                 tags.push(this.createUserTag(tag)); | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         hit._tags = tags; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     createUserTag(tag: string): Tag { | ||||||
|  |         const tokens = tag.split("."); | ||||||
|  | 
 | ||||||
|  |         const colorToken = tokens.pop() as string; | ||||||
|  | 
 | ||||||
|  |         const bg = colorToken; | ||||||
|  |         const fg = lum(colorToken) > 50 ? "#000" : "#fff"; | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             style: "user", | ||||||
|  |             fg: fg, | ||||||
|  |             bg: bg, | ||||||
|  |             text: tokens.join("."), | ||||||
|  |             rawText: tag, | ||||||
|  |             userTag: true, | ||||||
|  |         } as Tag; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     esQuery(query: any): Promise<EsResult> { | ||||||
|  |         return axios.post(`${this.baseUrl}es`, query).then(resp => { | ||||||
|  |             const res = resp.data as EsResult; | ||||||
|  | 
 | ||||||
|  |             if (res.hits?.hits) { | ||||||
|  |                 res.hits.hits.forEach((hit: EsHit) => { | ||||||
|  |                     hit["_source"]["name"] = strUnescape(hit["_source"]["name"]); | ||||||
|  |                     hit["_source"]["path"] = strUnescape(hit["_source"]["path"]); | ||||||
|  |                     hit["_path_md5"] = CryptoES.MD5( | ||||||
|  |                         hit["_source"]["path"] + | ||||||
|  |                         (hit["_source"]["path"] ? "/" : "") + | ||||||
|  |                         hit["_source"]["name"] + ext(hit) | ||||||
|  |                     ).toString(); | ||||||
|  | 
 | ||||||
|  |                     this.setHitProps(hit); | ||||||
|  |                     this.setHitTags(hit); | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return res; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getMimeTypes() { | ||||||
|  |         return this.esQuery({ | ||||||
|  |             aggs: { | ||||||
|  |                 mimeTypes: { | ||||||
|  |                     terms: { | ||||||
|  |                         field: "mime", | ||||||
|  |                         size: 10000 | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             size: 0, | ||||||
|  |         }).then(resp => { | ||||||
|  |             const mimeMap: any[] = []; | ||||||
|  |             resp["aggregations"]["mimeTypes"]["buckets"].sort((a: any, b: any) => a.key > b.key).forEach((bucket: any) => { | ||||||
|  |                 const tmp = bucket["key"].split("/"); | ||||||
|  |                 const category = tmp[0]; | ||||||
|  |                 const mime = tmp[1]; | ||||||
|  | 
 | ||||||
|  |                 let category_exists = false; | ||||||
|  | 
 | ||||||
|  |                 const child = { | ||||||
|  |                     "id": bucket["key"], | ||||||
|  |                     "text": `${mime} (${bucket["doc_count"]})` | ||||||
|  |                 }; | ||||||
|  | 
 | ||||||
|  |                 mimeMap.forEach(node => { | ||||||
|  |                     if (node.text === category) { | ||||||
|  |                         node.children.push(child); | ||||||
|  |                         category_exists = true; | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |                 if (!category_exists) { | ||||||
|  |                     mimeMap.push({"text": category, children: [child]}); | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  | 
 | ||||||
|  |             return mimeMap; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     _createEsTag(tag: string, count: number): EsTag { | ||||||
|  |         const tokens = tag.split("."); | ||||||
|  | 
 | ||||||
|  |         if (/.*\.#[0-9a-f]{6}/.test(tag)) { | ||||||
|  |             return { | ||||||
|  |                 id: tokens.slice(0, -1).join("."), | ||||||
|  |                 color: tokens.pop(), | ||||||
|  |                 isLeaf: true, | ||||||
|  |                 count: count | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             id: tag, | ||||||
|  |             count: count, | ||||||
|  |             isLeaf: false, | ||||||
|  |             color: undefined | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getDocInfo(docId: string) { | ||||||
|  |         return axios.get(`${this.baseUrl}d/${docId}`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getTags() { | ||||||
|  |         return this.esQuery({ | ||||||
|  |             aggs: { | ||||||
|  |                 tags: { | ||||||
|  |                     terms: { | ||||||
|  |                         field: "tag", | ||||||
|  |                         size: 10000 | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             size: 0, | ||||||
|  |         }).then(resp => { | ||||||
|  |             const seen = new Set(); | ||||||
|  | 
 | ||||||
|  |             const tags = resp["aggregations"]["tags"]["buckets"] | ||||||
|  |                 .sort((a: any, b: any) => a["key"].localeCompare(b["key"])) | ||||||
|  |                 .map((bucket: any) => this._createEsTag(bucket["key"], bucket["doc_count"])); | ||||||
|  | 
 | ||||||
|  |             // Remove duplicates (same tag with different color)
 | ||||||
|  |             return tags.filter((t: EsTag) => { | ||||||
|  |                 if (seen.has(t.id)) { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |                 seen.add(t.id); | ||||||
|  |                 return true; | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     saveTag(tag: string, hit: EsHit) { | ||||||
|  |         return axios.post(`${this.baseUrl}tag/` + hit["_source"]["index"], { | ||||||
|  |             delete: false, | ||||||
|  |             name: tag, | ||||||
|  |             doc_id: hit["_id"], | ||||||
|  |             path_md5: hit._path_md5 | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     deleteTag(tag: string, hit: EsHit) { | ||||||
|  |         return axios.post(`${this.baseUrl}tag/` + hit["_source"]["index"], { | ||||||
|  |             delete: true, | ||||||
|  |             name: tag, | ||||||
|  |             doc_id: hit["_id"], | ||||||
|  |             path_md5: hit._path_md5 | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getTreemapCsvUrl(indexId: string) { | ||||||
|  |         return `${this.baseUrl}s/${indexId}/1`; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getMimeCsvUrl(indexId: string) { | ||||||
|  |         return `${this.baseUrl}s/${indexId}/2`; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getSizeCsv(indexId: string) { | ||||||
|  |         return `${this.baseUrl}s/${indexId}/3`; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getDateCsv(indexId: string) { | ||||||
|  |         return `${this.baseUrl}s/${indexId}/4`; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default new Sist2Api(""); | ||||||
							
								
								
									
										228
									
								
								sist2-vue/src/Sist2Query.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								sist2-vue/src/Sist2Query.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,228 @@ | |||||||
|  | import store from "./store"; | ||||||
|  | import {EsHit, Index} from "@/Sist2Api"; | ||||||
|  | 
 | ||||||
|  | const SORT_MODES = { | ||||||
|  |     score: { | ||||||
|  |         mode: [ | ||||||
|  |             {_score: {order: "desc"}}, | ||||||
|  |             {_tie: {order: "asc"}} | ||||||
|  |         ], | ||||||
|  |         key: (hit: EsHit) => hit._score | ||||||
|  |     }, | ||||||
|  |     random: { | ||||||
|  |         mode: [ | ||||||
|  |             {_score: {order: "desc"}}, | ||||||
|  |             {_tie: {order: "asc"}} | ||||||
|  |         ], | ||||||
|  |         key: (hit: EsHit) => hit._score | ||||||
|  |     }, | ||||||
|  |     dateAsc: { | ||||||
|  |         mode: [ | ||||||
|  |             {mtime: {order: "asc"}}, | ||||||
|  |             {_tie: {order: "asc"}} | ||||||
|  |         ], | ||||||
|  |         key: (hit: EsHit) => hit._source.mtime | ||||||
|  |     }, | ||||||
|  |     dateDesc: { | ||||||
|  |         mode: [ | ||||||
|  |             {mtime: {order: "desc"}}, | ||||||
|  |             {_tie: {order: "asc"}} | ||||||
|  |         ], | ||||||
|  |         key: (hit: EsHit) => hit._source.mtime | ||||||
|  |     }, | ||||||
|  |     sizeAsc: { | ||||||
|  |         mode: [ | ||||||
|  |             {size: {order: "asc"}}, | ||||||
|  |             {_tie: {order: "asc"}} | ||||||
|  |         ], | ||||||
|  |         key: (hit: EsHit) => hit._source.size | ||||||
|  |     }, | ||||||
|  |     sizeDesc: { | ||||||
|  |         mode: [ | ||||||
|  |             {size: {order: "desc"}}, | ||||||
|  |             {_tie: {order: "asc"}} | ||||||
|  |         ], | ||||||
|  |         key: (hit: EsHit) => hit._source.size | ||||||
|  |     } | ||||||
|  | } as any; | ||||||
|  | 
 | ||||||
|  | interface SortMode { | ||||||
|  |     text: string | ||||||
|  |     mode: any[] | ||||||
|  |     key: (hit: EsHit) => any | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Sist2Query { | ||||||
|  | 
 | ||||||
|  |     searchQuery(): any { | ||||||
|  | 
 | ||||||
|  |         const getters = store.getters; | ||||||
|  | 
 | ||||||
|  |         const searchText = getters.searchText; | ||||||
|  |         const pathText = getters.pathText; | ||||||
|  |         const empty = searchText === ""; | ||||||
|  |         const sizeMin = getters.sizeMin; | ||||||
|  |         const sizeMax = getters.sizeMax; | ||||||
|  |         const dateMin = getters.dateMin; | ||||||
|  |         const dateMax = getters.dateMax; | ||||||
|  |         const fuzzy = getters.fuzzy; | ||||||
|  |         const size = getters.size; | ||||||
|  |         const after = getters.lastDoc; | ||||||
|  |         const selectedIndexIds = getters.selectedIndices.map((idx: Index) => idx.id) | ||||||
|  |         const selectedMimeTypes = getters.selectedMimeTypes; | ||||||
|  |         const selectedTags = getters.selectedTags; | ||||||
|  | 
 | ||||||
|  |         const filters = [ | ||||||
|  |             {terms: {index: selectedIndexIds}} | ||||||
|  |         ] as any[]; | ||||||
|  | 
 | ||||||
|  |         if (sizeMin && sizeMax) { | ||||||
|  |             filters.push({range: {size: {gte: sizeMin, lte: sizeMax}}}) | ||||||
|  |         } else if (sizeMin) { | ||||||
|  |             filters.push({range: {size: {gte: sizeMin}}}) | ||||||
|  |         } else if (sizeMax) { | ||||||
|  |             filters.push({range: {size: {lte: sizeMax}}}) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (dateMin && dateMax) { | ||||||
|  |             filters.push({range: {mtime: {gte: dateMin, lte: dateMax}}}) | ||||||
|  |         } else if (dateMin) { | ||||||
|  |             filters.push({range: {mtime: {gte: dateMin}}}) | ||||||
|  |         } else if (dateMax) { | ||||||
|  |             filters.push({range: {mtime: {lte: dateMax}}}) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const fields = [ | ||||||
|  |             "name^8", | ||||||
|  |             "content^3", | ||||||
|  |             "album^8", "artist^8", "title^8", "genre^2", "album_artist^8", | ||||||
|  |             "font_name^6" | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         if (getters.optSearchInPath) { | ||||||
|  |             fields.push("path.text^5"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (fuzzy) { | ||||||
|  |             fields.push("content.nGram"); | ||||||
|  |             if (getters.optSearchInPath) { | ||||||
|  |                 fields.push("path.nGram"); | ||||||
|  |             } | ||||||
|  |             fields.push("name.nGram^3"); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const path = pathText.replace(/\/$/, "").toLowerCase(); //remove trailing slashes
 | ||||||
|  |         if (path !== "") { | ||||||
|  |             filters.push({term: {path: path}}) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (selectedMimeTypes.length > 0) { | ||||||
|  |             filters.push({terms: {"mime": selectedMimeTypes}}); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (selectedTags.length > 0) { | ||||||
|  |             if (getters.optTagOrOperator) { | ||||||
|  |                 filters.push({terms: {"tag": selectedTags}}); | ||||||
|  |             } else { | ||||||
|  |                 selectedTags.forEach((tag: string) => filters.push({term: {"tag": tag}})); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let query; | ||||||
|  |         if (getters.optQueryMode === "simple") { | ||||||
|  |             query = { | ||||||
|  |                 simple_query_string: { | ||||||
|  |                     query: searchText, | ||||||
|  |                     fields: fields, | ||||||
|  |                     default_operator: "and" | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             query = { | ||||||
|  |                 query_string: { | ||||||
|  |                     query: searchText, | ||||||
|  |                     default_field: "name", | ||||||
|  |                     default_operator: "and" | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const q = { | ||||||
|  |             _source: { | ||||||
|  |                 excludes: ["content", "_tie"] | ||||||
|  |             }, | ||||||
|  |             query: { | ||||||
|  |                 bool: { | ||||||
|  |                     filter: filters, | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             sort: SORT_MODES[getters.sortMode].mode, | ||||||
|  |             aggs: | ||||||
|  |                 { | ||||||
|  |                     total_size: {"sum": {"field": "size"}}, | ||||||
|  |                     total_count: {"value_count": {"field": "size"}} | ||||||
|  |                 }, | ||||||
|  |             size: size, | ||||||
|  |         } as any; | ||||||
|  | 
 | ||||||
|  |         if (!empty) { | ||||||
|  |             q.query.bool.must = query; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (after) { | ||||||
|  |             q.search_after = [SORT_MODES[getters.sortMode].key(after), after["_id"]]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (getters.optHighlight) { | ||||||
|  |             q.highlight = { | ||||||
|  |                 pre_tags: ["<mark>"], | ||||||
|  |                 post_tags: ["</mark>"], | ||||||
|  |                 fragment_size: getters.optFragmentSize, | ||||||
|  |                 number_of_fragments: 1, | ||||||
|  |                 order: "score", | ||||||
|  |                 fields: { | ||||||
|  |                     content: {}, | ||||||
|  |                     name: {}, | ||||||
|  |                     "name.nGram": {}, | ||||||
|  |                     "content.nGram": {}, | ||||||
|  |                     font_name: {}, | ||||||
|  |                 } | ||||||
|  |             }; | ||||||
|  |             if (getters.optSearchInPath) { | ||||||
|  |                 q.highlight.fields["path.text"] = {}; | ||||||
|  |                 q.highlight.fields["path.nGram"] = {}; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (getters.sortMode === "random") { | ||||||
|  |             q.query = { | ||||||
|  |                 function_score: { | ||||||
|  |                     query: { | ||||||
|  |                         bool: { | ||||||
|  |                             must: filters, | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     functions: [ | ||||||
|  |                         { | ||||||
|  |                             random_score: { | ||||||
|  |                                 seed: getters.seed, | ||||||
|  |                                 field: "_seq_no", | ||||||
|  |                             }, | ||||||
|  |                             weight: 1000 | ||||||
|  |                         } | ||||||
|  |                     ], | ||||||
|  |                     boost_mode: "sum" | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!empty) { | ||||||
|  |                 q.query.function_score.query.bool.must.push(query); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return q; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default new Sist2Query(); | ||||||
							
								
								
									
										0
									
								
								sist2-vue/src/assets/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								sist2-vue/src/assets/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										
											BIN
										
									
								
								sist2-vue/src/assets/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								sist2-vue/src/assets/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										28
									
								
								sist2-vue/src/components/ContentDiv.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								sist2-vue/src/components/ContentDiv.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="content-div" v-html="content()" v-if="content()"></div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: "ContentDiv", | ||||||
|  |   props: ["doc"], | ||||||
|  |   methods: { | ||||||
|  |     content() { | ||||||
|  |       if (!this.doc.highlight) { | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (this.doc.highlight["content.nGram"]) { | ||||||
|  |         return this.doc.highlight["content.nGram"][0]; | ||||||
|  |       } | ||||||
|  |       if (this.doc.highlight.content) { | ||||||
|  |         return this.doc.highlight.content[0]; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
							
								
								
									
										129
									
								
								sist2-vue/src/components/D3DateHistogram.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								sist2-vue/src/components/D3DateHistogram.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,129 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="graph"> | ||||||
|  |     <svg id="date-histogram"></svg> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import * as d3 from "d3"; | ||||||
|  | import Sist2Api from "@/Sist2Api"; | ||||||
|  | 
 | ||||||
|  | const formatSI = d3.format("~s"); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | function dateHistogram(data, svg, title) { | ||||||
|  |   let bins = data.map(d => { | ||||||
|  |     return { | ||||||
|  |       length: Number(d.count), | ||||||
|  |       x0: Number(d.bucket), | ||||||
|  |       x1: Number(d.bucket) + 2629800 | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   bins.sort((a, b) => a.length - b.length); | ||||||
|  | 
 | ||||||
|  |   const margin = { | ||||||
|  |     top: 50, | ||||||
|  |     right: 20, | ||||||
|  |     bottom: 70, | ||||||
|  |     left: 40 | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const thresh = d3.quantile(bins, 0.9, d => d.length); | ||||||
|  |   bins = bins.filter(d => d.length > thresh); | ||||||
|  | 
 | ||||||
|  |   const width = 550; | ||||||
|  |   const height = 450; | ||||||
|  | 
 | ||||||
|  |   svg.selectAll("*").remove(); | ||||||
|  |   svg.attr("viewBox", [0, 0, width, height]); | ||||||
|  | 
 | ||||||
|  |   const y = d3.scaleLinear() | ||||||
|  |       .domain([0, d3.max(bins, d => d.length)]).nice() | ||||||
|  |       .range([height - margin.bottom, margin.top]); | ||||||
|  | 
 | ||||||
|  |   const x = d3.scaleLinear() | ||||||
|  |       .domain(d3.extent(bins, d => d.x0)).nice() | ||||||
|  |       .range([margin.left, width - margin.right]); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("fill", "steelblue") | ||||||
|  |       .selectAll("rect") | ||||||
|  |       .data(bins) | ||||||
|  |       .join("rect") | ||||||
|  |       .attr("x", d => x(d.x0) + 1) | ||||||
|  |       .attr("width", d => Math.max(1, x(d.x1) - x(d.x0) - 1)) | ||||||
|  |       .attr("y", d => y(d.length)) | ||||||
|  |       .attr("height", d => y(0) - y(d.length)) | ||||||
|  |       .call(g => g | ||||||
|  |           .append("title") | ||||||
|  |           .text(d => d.length) | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("transform", `translate(0,${height - margin.bottom})`) | ||||||
|  |       .call( | ||||||
|  |           d3.axisBottom(x) | ||||||
|  |               .ticks(width / 30) | ||||||
|  |               .tickSizeOuter(0) | ||||||
|  |               .tickFormat(t => d3.timeFormat("%Y-%m-%d")(d3.utcParse("%s")(t))) | ||||||
|  |       ) | ||||||
|  |       .call(g => g | ||||||
|  |           .selectAll("text") | ||||||
|  |           .style("text-anchor", "end") | ||||||
|  |           .attr("dx", "-.8em") | ||||||
|  |           .attr("dy", ".15em") | ||||||
|  |           .attr("transform", "rotate(-65)") | ||||||
|  |       ) | ||||||
|  |       .call(g => g.append("text") | ||||||
|  |           .attr("x", width - margin.right) | ||||||
|  |           .attr("y", -4) | ||||||
|  |           .attr("fill", "currentColor") | ||||||
|  |           .attr("font-weight", "bold") | ||||||
|  |           .attr("text-anchor", "end") | ||||||
|  |           .text("mtime") | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("transform", `translate(${margin.left},0)`) | ||||||
|  |       .call( | ||||||
|  |           d3.axisLeft(y) | ||||||
|  |               .ticks(height / 40) | ||||||
|  |               .tickFormat(t => formatSI(t)) | ||||||
|  |       ) | ||||||
|  |       .call(g => g.select(".domain").remove()) | ||||||
|  |       .call(g => g.select(".tick:last-of-type text").clone() | ||||||
|  |           .attr("x", 4) | ||||||
|  |           .attr("text-anchor", "start") | ||||||
|  |           .attr("font-weight", "bold") | ||||||
|  |           .text("File count")); | ||||||
|  | 
 | ||||||
|  |   svg.append("text") | ||||||
|  |       .attr("x", (width / 2)) | ||||||
|  |       .attr("y", (margin.top / 2)) | ||||||
|  |       .attr("text-anchor", "middle") | ||||||
|  |       .style("font-size", "16px") | ||||||
|  |       .text(title); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "D3DateHistogram", | ||||||
|  |   props: ["indexId"], | ||||||
|  |   mounted() { | ||||||
|  |     this.update(this.indexId); | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     indexId: function () { | ||||||
|  |       this.update(this.indexId); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     update(indexId) { | ||||||
|  |       const svg = d3.select("#date-histogram"); | ||||||
|  | 
 | ||||||
|  |       d3.csv(Sist2Api.getDateCsv(indexId)).then(tabularData => { | ||||||
|  |         dateHistogram(tabularData.slice(), svg, this.$t("d3.dateHistogram")); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										100
									
								
								sist2-vue/src/components/D3MimeBarCount.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								sist2-vue/src/components/D3MimeBarCount.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="graph"> | ||||||
|  |     <svg id="agg-mime-count"></svg> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import * as d3 from "d3"; | ||||||
|  | import Sist2Api from "@/Sist2Api"; | ||||||
|  | 
 | ||||||
|  | const formatSI = d3.format("~s"); | ||||||
|  | const barHeight = 20; | ||||||
|  | const ordinalColor = d3.scaleOrdinal(d3.schemeCategory10); | ||||||
|  | 
 | ||||||
|  | function mimeBarCount(data, svg, fillOpacity, title) { | ||||||
|  | 
 | ||||||
|  |   const margin = { | ||||||
|  |     top: 50, | ||||||
|  |     right: 0, | ||||||
|  |     bottom: 10, | ||||||
|  |     left: Math.max( | ||||||
|  |         d3.max(data.sort((a, b) => b.count - a.count).slice(0, 15), d => d.mime.length) * 6, | ||||||
|  |         d3.max(data.sort((a, b) => b.size - a.size).slice(0, 15), d => d.mime.length) * 6, | ||||||
|  |     ) | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   data.forEach(d => { | ||||||
|  |     d.name = d.mime; | ||||||
|  |     d.value = Number(d.count); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   data = data.sort((a, b) => b.value - a.value).slice(0, 15); | ||||||
|  | 
 | ||||||
|  |   const width = 550; | ||||||
|  |   const height = Math.ceil((data.length + 0.1) * barHeight) + margin.top + margin.bottom; | ||||||
|  | 
 | ||||||
|  |   svg.selectAll("*").remove(); | ||||||
|  |   svg.attr("viewBox", [0, 0, width, height]); | ||||||
|  | 
 | ||||||
|  |   const y = d3.scaleBand() | ||||||
|  |       .domain(d3.range(data.length)) | ||||||
|  |       .rangeRound([margin.top, height - margin.bottom]); | ||||||
|  | 
 | ||||||
|  |   const x = d3.scaleLinear() | ||||||
|  |       .domain([0, d3.max(data, d => d.value)]) | ||||||
|  |       .range([margin.left, width - margin.right]); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("fill-opacity", fillOpacity) | ||||||
|  |       .selectAll("rect") | ||||||
|  |       .data(data) | ||||||
|  |       .join("rect") | ||||||
|  |       .attr("fill", d => ordinalColor(d.name)) | ||||||
|  |       .attr("x", x(0)) | ||||||
|  |       .attr("y", (d, i) => y(i)) | ||||||
|  |       .attr("width", d => x(d.value) - x(0)) | ||||||
|  |       .attr("height", y.bandwidth()) | ||||||
|  |       .append("title") | ||||||
|  |       .text(d => d3.format(",")(d.value)); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("transform", `translate(0,${margin.top})`) | ||||||
|  |       .call(d3.axisTop(x).ticks(width / 80, data.format).tickFormat(formatSI)) | ||||||
|  |       .call(g => g.select(".domain").remove()); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("transform", `translate(${margin.left},0)`) | ||||||
|  |       .call(d3.axisLeft(y).tickFormat(i => data[i].name).tickSizeOuter(0)); | ||||||
|  | 
 | ||||||
|  |   svg.append("text") | ||||||
|  |       .attr("x", (width / 2)) | ||||||
|  |       .attr("y", (margin.top / 2)) | ||||||
|  |       .attr("text-anchor", "middle") | ||||||
|  |       .style("font-size", "16px") | ||||||
|  |       .text(title); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "D3MimeBarSize", | ||||||
|  |   props: ["indexId"], | ||||||
|  |   mounted() { | ||||||
|  |     this.update(this.indexId); | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     indexId: function () { | ||||||
|  |       this.update(this.indexId); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     update(indexId) { | ||||||
|  |       const mimeSvgCount = d3.select("#agg-mime-count"); | ||||||
|  |       const fillOpacity = this.$store.state.optTheme === "black" ? 0.9 : 0.6; | ||||||
|  | 
 | ||||||
|  |       d3.csv(Sist2Api.getMimeCsvUrl(indexId)).then(tabularData => { | ||||||
|  |         mimeBarCount(tabularData.slice(), mimeSvgCount, fillOpacity, this.$t("d3.mimeCount")); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										99
									
								
								sist2-vue/src/components/D3MimeBarSize.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								sist2-vue/src/components/D3MimeBarSize.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="graph"> | ||||||
|  |     <svg id="agg-mime-size"></svg> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import * as d3 from "d3"; | ||||||
|  | import Sist2Api from "@/Sist2Api"; | ||||||
|  | 
 | ||||||
|  | const formatSI = d3.format("~s"); | ||||||
|  | const barHeight = 20; | ||||||
|  | const ordinalColor = d3.scaleOrdinal(d3.schemeCategory10); | ||||||
|  | 
 | ||||||
|  | function mimeBarSize(data, svg, fillOpacity, title) { | ||||||
|  | 
 | ||||||
|  |   const margin = { | ||||||
|  |     top: 50, | ||||||
|  |     right: 0, | ||||||
|  |     bottom: 10, | ||||||
|  |     left: Math.max( | ||||||
|  |         d3.max(data.sort((a, b) => b.count - a.count).slice(0, 15), d => d.mime.length) * 6, | ||||||
|  |         d3.max(data.sort((a, b) => b.size - a.size).slice(0, 15), d => d.mime.length) * 6, | ||||||
|  |     ) | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   data.forEach(d => { | ||||||
|  |     d.name = d.mime; | ||||||
|  |     d.value = Number(d.size); | ||||||
|  |   }); | ||||||
|  |   data = data.sort((a, b) => b.value - a.value).slice(0, 15); | ||||||
|  | 
 | ||||||
|  |   const width = 550; | ||||||
|  |   const height = Math.ceil((data.length + 0.1) * barHeight) + margin.top + margin.bottom; | ||||||
|  | 
 | ||||||
|  |   svg.selectAll("*").remove(); | ||||||
|  |   svg.attr("viewBox", [0, 0, width, height]); | ||||||
|  | 
 | ||||||
|  |   const y = d3.scaleBand() | ||||||
|  |       .domain(d3.range(data.length)) | ||||||
|  |       .rangeRound([margin.top, height - margin.bottom]); | ||||||
|  | 
 | ||||||
|  |   const x = d3.scaleLinear() | ||||||
|  |       .domain([0, d3.max(data, d => d.value)]) | ||||||
|  |       .range([margin.left, width - margin.right]); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("fill-opacity", fillOpacity) | ||||||
|  |       .selectAll("rect") | ||||||
|  |       .data(data) | ||||||
|  |       .join("rect") | ||||||
|  |       .attr("fill", d => ordinalColor(d.name)) | ||||||
|  |       .attr("x", x(0)) | ||||||
|  |       .attr("y", (d, i) => y(i)) | ||||||
|  |       .attr("width", d => x(d.value) - x(0)) | ||||||
|  |       .attr("height", y.bandwidth()) | ||||||
|  |       .append("title") | ||||||
|  |       .text(d => formatSI(d.value)); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("transform", `translate(0,${margin.top})`) | ||||||
|  |       .call(d3.axisTop(x).ticks(width / 80, data.format).tickFormat(formatSI)) | ||||||
|  |       .call(g => g.select(".domain").remove()); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("transform", `translate(${margin.left},0)`) | ||||||
|  |       .call(d3.axisLeft(y).tickFormat(i => data[i].name).tickSizeOuter(0)); | ||||||
|  | 
 | ||||||
|  |   svg.append("text") | ||||||
|  |       .attr("x", (width / 2)) | ||||||
|  |       .attr("y", (margin.top / 2)) | ||||||
|  |       .attr("text-anchor", "middle") | ||||||
|  |       .style("font-size", "16px") | ||||||
|  |       .text(title); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "D3MimeBarSize", | ||||||
|  |   props: ["indexId"], | ||||||
|  |   mounted() { | ||||||
|  |     this.update(this.indexId); | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     indexId: function () { | ||||||
|  |       this.update(this.indexId); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     update(indexId) { | ||||||
|  |       const mimeSvgSize = d3.select("#agg-mime-size"); | ||||||
|  |       const fillOpacity = this.$store.state.optTheme === "black" ? 0.9 : 0.6; | ||||||
|  | 
 | ||||||
|  |       d3.csv(Sist2Api.getMimeCsvUrl(indexId)).then(tabularData => { | ||||||
|  |         mimeBarSize(tabularData.slice(), mimeSvgSize, fillOpacity, this.$t("d3.mimeSize")); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										126
									
								
								sist2-vue/src/components/D3SizeHistogram.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								sist2-vue/src/components/D3SizeHistogram.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,126 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="graph"> | ||||||
|  |     <svg id="size-histogram"></svg> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import * as d3 from "d3"; | ||||||
|  | import Sist2Api from "@/Sist2Api"; | ||||||
|  | 
 | ||||||
|  | const formatSI = d3.format("~s"); | ||||||
|  | 
 | ||||||
|  | function sizeHistogram(data, svg, title) { | ||||||
|  | 
 | ||||||
|  |   let bins = data.map(d => { | ||||||
|  |     return { | ||||||
|  |       length: Number(d.count), | ||||||
|  |       x0: Number(d.bucket), | ||||||
|  |       x1: Number(d.bucket) + (5 * 1024 * 1024) | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   bins = bins.sort((a, b) => b.length - a.length).slice(0, 25); | ||||||
|  | 
 | ||||||
|  |   const margin = { | ||||||
|  |     top: 50, | ||||||
|  |     right: 20, | ||||||
|  |     bottom: 70, | ||||||
|  |     left: 40 | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const width = 550; | ||||||
|  |   const height = 450; | ||||||
|  | 
 | ||||||
|  |   svg.selectAll("*").remove(); | ||||||
|  |   svg.attr("viewBox", [0, 0, width, height]); | ||||||
|  | 
 | ||||||
|  |   const y = d3.scaleLinear() | ||||||
|  |       .domain([0, d3.max(bins, d => d.length)]) | ||||||
|  |       .range([height - margin.bottom, margin.top]); | ||||||
|  | 
 | ||||||
|  |   const x = d3.scaleLinear() | ||||||
|  |       .domain(d3.extent(bins, d => d.x0)).nice() | ||||||
|  |       .range([margin.left, width - margin.right]); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("fill", "steelblue") | ||||||
|  |       .selectAll("rect") | ||||||
|  |       .data(bins) | ||||||
|  |       .join("rect") | ||||||
|  |       .attr("x", d => x(d.x0) + 1) | ||||||
|  |       .attr("width", d => Math.max(1, x(d.x1) - x(d.x0) - 1)) | ||||||
|  |       .attr("y", d => y(d.length)) | ||||||
|  |       .attr("height", d => y(0) - y(d.length)) | ||||||
|  |       .call(g => g | ||||||
|  |           .append("title") | ||||||
|  |           .text(d => d.length) | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("transform", `translate(0,${height - margin.bottom})`) | ||||||
|  |       .call( | ||||||
|  |           d3.axisBottom(x) | ||||||
|  |               .ticks(width / 30) | ||||||
|  |               .tickSizeOuter(0) | ||||||
|  |               .tickFormat(formatSI) | ||||||
|  |       ) | ||||||
|  |       .call(g => g | ||||||
|  |           .selectAll("text") | ||||||
|  |           .style("text-anchor", "end") | ||||||
|  |           .attr("dx", "-.8em") | ||||||
|  |           .attr("dy", ".15em") | ||||||
|  |           .attr("transform", "rotate(-65)") | ||||||
|  |       ) | ||||||
|  |       .call(g => g.append("text") | ||||||
|  |           .attr("x", width - margin.right) | ||||||
|  |           .attr("y", -4) | ||||||
|  |           .attr("fill", "currentColor") | ||||||
|  |           .attr("font-weight", "bold") | ||||||
|  |           .attr("text-anchor", "end") | ||||||
|  |           .text("size (bytes)") | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |   svg.append("g") | ||||||
|  |       .attr("transform", `translate(${margin.left},0)`) | ||||||
|  |       .call( | ||||||
|  |           d3.axisLeft(y) | ||||||
|  |               .ticks(height / 40) | ||||||
|  |               .tickFormat(t => formatSI(t)) | ||||||
|  |       ) | ||||||
|  |       .call(g => g.select(".domain").remove()) | ||||||
|  |       .call(g => g.select(".tick:last-of-type text").clone() | ||||||
|  |           .attr("x", 4) | ||||||
|  |           .attr("text-anchor", "start") | ||||||
|  |           .attr("font-weight", "bold") | ||||||
|  |           .text("File count")); | ||||||
|  | 
 | ||||||
|  |   svg.append("text") | ||||||
|  |       .attr("x", (width / 2)) | ||||||
|  |       .attr("y", (margin.top / 2)) | ||||||
|  |       .attr("text-anchor", "middle") | ||||||
|  |       .style("font-size", "16px") | ||||||
|  |       .text(title); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "D3SizeHistogram", | ||||||
|  |   props: ["indexId"], | ||||||
|  |   mounted() { | ||||||
|  |     this.update(this.indexId); | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     indexId: function () { | ||||||
|  |       this.update(this.indexId); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     update(indexId) { | ||||||
|  |       const svg = d3.select("#size-histogram"); | ||||||
|  | 
 | ||||||
|  |       d3.csv(Sist2Api.getSizeCsv(indexId)).then(tabularData => { | ||||||
|  |         sizeHistogram(tabularData.slice(), svg, this.$t("d3.sizeHistogram")); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										267
									
								
								sist2-vue/src/components/D3Treemap.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										267
									
								
								sist2-vue/src/components/D3Treemap.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,267 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <b-btn style="float:right;margin-bottom: 10px" @click="downloadTreemap()" variant="primary"> | ||||||
|  |       {{ $t("download") }} | ||||||
|  |     </b-btn> | ||||||
|  |     <svg id="treemap"></svg> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import * as d3 from "d3"; | ||||||
|  | import {burrow} from "@/util-js" | ||||||
|  | import {humanFileSize} from "@/util"; | ||||||
|  | import Sist2Api from "@/Sist2Api"; | ||||||
|  | import domtoimage from "dom-to-image"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | const TILING_MODES = { | ||||||
|  |   "squarify": d3.treemapSquarify, | ||||||
|  |   "binary": d3.treemapBinary, | ||||||
|  |   "sliceDice": d3.treemapSliceDice, | ||||||
|  |   "slice": d3.treemapSlice, | ||||||
|  |   "dice": d3.treemapDice, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const COLORS = { | ||||||
|  |   "PuBuGn": d3.interpolatePuBuGn, | ||||||
|  |   "PuRd": d3.interpolatePuRd, | ||||||
|  |   "PuBu": d3.interpolatePuBu, | ||||||
|  |   "YlOrBr": d3.interpolateYlOrBr, | ||||||
|  |   "YlOrRd": d3.interpolateYlOrRd, | ||||||
|  |   "YlGn": d3.interpolateYlGn, | ||||||
|  |   "YlGnBu": d3.interpolateYlGnBu, | ||||||
|  |   "Plasma": d3.interpolatePlasma, | ||||||
|  |   "Magma": d3.interpolateMagma, | ||||||
|  |   "Inferno": d3.interpolateInferno, | ||||||
|  |   "Viridis": d3.interpolateViridis, | ||||||
|  |   "Turbo": d3.interpolateTurbo, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const SIZES = { | ||||||
|  |   "small": [800, 600], | ||||||
|  |   "medium": [1300, 750], | ||||||
|  |   "large": [1900, 900], | ||||||
|  |   "x-large": [2800, 1700], | ||||||
|  |   "xx-large": [3600, 2000], | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | const uids = {}; | ||||||
|  | 
 | ||||||
|  | function uid(name) { | ||||||
|  |   let id = uids[name] || 0; | ||||||
|  |   uids[name] = id + 1; | ||||||
|  |   return name + id; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function cascade(root, offset) { | ||||||
|  |   const x = new Map; | ||||||
|  |   const y = new Map; | ||||||
|  |   return root.eachAfter(d => { | ||||||
|  |     if (d.children && d.children.length !== 0) { | ||||||
|  |       x.set(d, 1 + d3.max(d.children, c => c.x1 === d.x1 - offset ? x.get(c) : NaN)); | ||||||
|  |       y.set(d, 1 + d3.max(d.children, c => c.y1 === d.y1 - offset ? y.get(c) : NaN)); | ||||||
|  |     } else { | ||||||
|  |       x.set(d, 0); | ||||||
|  |       y.set(d, 0); | ||||||
|  |     } | ||||||
|  |   }).eachBefore(d => { | ||||||
|  |     d.x1 -= 2 * offset * x.get(d); | ||||||
|  |     d.y1 -= 2 * offset * y.get(d); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function cascadeTreemap(data, svg, width, height, tilingMode, treemapColor) { | ||||||
|  |   const root = cascade( | ||||||
|  |       d3.treemap() | ||||||
|  |           .size([width, height]) | ||||||
|  |           .tile(TILING_MODES[tilingMode]) | ||||||
|  |           .paddingOuter(3) | ||||||
|  |           .paddingTop(16) | ||||||
|  |           .paddingInner(1) | ||||||
|  |           .round(true)( | ||||||
|  |               d3.hierarchy(data) | ||||||
|  |                   .sum(d => d.value) | ||||||
|  |                   .sort((a, b) => b.value - a.value) | ||||||
|  |           ), | ||||||
|  |       3 // treemap.paddingOuter | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const maxDepth = Math.max(...root.descendants().map(d => d.depth)); | ||||||
|  |   const color = d3.scaleSequential([maxDepth, -1], COLORS[treemapColor]); | ||||||
|  | 
 | ||||||
|  |   svg.append("filter") | ||||||
|  |       .attr("id", "shadow") | ||||||
|  |       .append("feDropShadow") | ||||||
|  |       .attr("flood-opacity", 0.3) | ||||||
|  |       .attr("dx", 0) | ||||||
|  |       .attr("stdDeviation", 3); | ||||||
|  | 
 | ||||||
|  |   const node = svg.selectAll("g") | ||||||
|  |       .data( | ||||||
|  |           d3.nest() | ||||||
|  |               .key(d => d.depth).sortKeys(d3.ascending) | ||||||
|  |               .entries(root.descendants()) | ||||||
|  |       ) | ||||||
|  |       .join("g") | ||||||
|  |       .attr("filter", "url(#shadow)") | ||||||
|  |       .selectAll("g") | ||||||
|  |       .data(d => d.values) | ||||||
|  |       .join("g") | ||||||
|  |       .attr("transform", d => `translate(${d.x0},${d.y0})`); | ||||||
|  | 
 | ||||||
|  |   node.append("title") | ||||||
|  |       .text(d => `${d.ancestors().reverse().splice(1).map(d => d.data.name).join("/")}\n${humanFileSize(d.value)}`); | ||||||
|  | 
 | ||||||
|  |   node.append("rect") | ||||||
|  |       .attr("id", d => (d.nodeUid = uid("node"))) | ||||||
|  |       .attr("fill", d => color(d.depth)) | ||||||
|  |       .attr("width", d => d.x1 - d.x0) | ||||||
|  |       .attr("height", d => d.y1 - d.y0); | ||||||
|  | 
 | ||||||
|  |   node.append("clipPath") | ||||||
|  |       .attr("id", d => (d.clipUid = uid("clip"))) | ||||||
|  |       .append("use") | ||||||
|  |       .attr("href", d => `#${d.nodeUid}`); | ||||||
|  | 
 | ||||||
|  |   node.append("text") | ||||||
|  |       .attr("fill", d => d3.hsl(color(d.depth)).l > .5 ? "#333" : "#eee") | ||||||
|  |       .attr("clip-path", d => `url(#${d.clipUid})`) | ||||||
|  |       .selectAll("tspan") | ||||||
|  |       .data(d => [d.data.name, humanFileSize(d.value)]) | ||||||
|  |       .join("tspan") | ||||||
|  |       .text(d => d); | ||||||
|  | 
 | ||||||
|  |   node.filter(d => d.children).selectAll("tspan") | ||||||
|  |       .attr("dx", 3) | ||||||
|  |       .attr("y", 13); | ||||||
|  | 
 | ||||||
|  |   node.filter(d => !d.children).selectAll("tspan") | ||||||
|  |       .attr("x", 3) | ||||||
|  |       .attr("y", (d, i) => `${i === 0 ? 1.1 : 2.3}em`); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function flatTreemap(data, svg, width, height, groupingDepth, tilingMode, fillOpacity) { | ||||||
|  |   const ordinalColor = d3.scaleOrdinal(d3.schemeCategory10); | ||||||
|  | 
 | ||||||
|  |   const root = d3.treemap() | ||||||
|  |       .tile(TILING_MODES[tilingMode]) | ||||||
|  |       .size([width, height]) | ||||||
|  |       .padding(1) | ||||||
|  |       .round(true)( | ||||||
|  |           d3.hierarchy(data) | ||||||
|  |               .sum(d => d.value) | ||||||
|  |               .sort((a, b) => b.value - a.value) | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |   const leaf = svg.selectAll("g") | ||||||
|  |       .data(root.leaves()) | ||||||
|  |       .join("g") | ||||||
|  |       .attr("transform", d => `translate(${d.x0},${d.y0})`); | ||||||
|  | 
 | ||||||
|  |   leaf.append("title") | ||||||
|  |       .text(d => `${d.ancestors().reverse().map(d => d.data.name).join("/")}\n${humanFileSize(d.value)}`); | ||||||
|  | 
 | ||||||
|  |   leaf.append("rect") | ||||||
|  |       .attr("id", d => (d.leafUid = uid("leaf"))) | ||||||
|  |       .attr("fill", d => { | ||||||
|  |         while (d.depth > groupingDepth) d = d.parent; | ||||||
|  |         return ordinalColor(d.data.name); | ||||||
|  |       }) | ||||||
|  |       .attr("fill-opacity", fillOpacity) | ||||||
|  |       .attr("width", d => d.x1 - d.x0) | ||||||
|  |       .attr("height", d => d.y1 - d.y0); | ||||||
|  | 
 | ||||||
|  |   leaf.append("clipPath") | ||||||
|  |       .attr("id", d => (d.clipUid = uid("clip"))) | ||||||
|  |       .append("use") | ||||||
|  |       .attr("href", d => `#${d.leafUid}`); | ||||||
|  | 
 | ||||||
|  |   leaf.append("text") | ||||||
|  |       .attr("clip-path", d => `url(#${d.clipUid})`) | ||||||
|  |       .selectAll("tspan") | ||||||
|  |       .data(d => { | ||||||
|  |         if (d.data.name === ".") { | ||||||
|  |           d = d.parent; | ||||||
|  |         } | ||||||
|  |         return [d.data.name, humanFileSize(d.value)] | ||||||
|  |       }) | ||||||
|  |       .join("tspan") | ||||||
|  |       .attr("x", 2) | ||||||
|  |       .attr("y", (d, i) => `${i === 0 ? 1.1 : 2.3}em`) | ||||||
|  |       .text(d => d); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function exportTreemap(indexName, width, height) { | ||||||
|  |   domtoimage.toBlob(document.getElementById("treemap"), {width: width, height: height}) | ||||||
|  |       .then(function (blob) { | ||||||
|  |         let a = document.createElement("a"); | ||||||
|  |         let url = URL.createObjectURL(blob); | ||||||
|  | 
 | ||||||
|  |         a.href = url; | ||||||
|  |         a.download = `${indexName}_treemap.png`; | ||||||
|  |         document.body.appendChild(a); | ||||||
|  |         a.click(); | ||||||
|  |         setTimeout(function () { | ||||||
|  |           document.body.removeChild(a); | ||||||
|  |           window.URL.revokeObjectURL(url); | ||||||
|  |         }, 0); | ||||||
|  |       }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "D3Treemap", | ||||||
|  |   props: ["indexId"], | ||||||
|  |   watch: { | ||||||
|  |     indexId: function () { | ||||||
|  |       this.update(this.indexId); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.update(this.indexId); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     update(indexId) { | ||||||
|  |       const width = SIZES[this.$store.state.optTreemapSize][0]; | ||||||
|  |       const height = SIZES[this.$store.state.optTreemapSize][1]; | ||||||
|  |       const tilingMode = this.$store.state.optTreemapTiling; | ||||||
|  |       const groupingDepth = this.$store.state.optTreemapColorGroupingDepth; | ||||||
|  |       const treemapColor = this.$store.state.optTreemapColor; | ||||||
|  |       const treemapType = this.$store.state.optTreemapType; | ||||||
|  | 
 | ||||||
|  |       const treemapSvg = d3.select("#treemap"); | ||||||
|  | 
 | ||||||
|  |       treemapSvg.selectAll("*").remove(); | ||||||
|  |       treemapSvg.attr("viewBox", [0, 0, width, height]) | ||||||
|  |           .attr("xmlns", "http://www.w3.org/2000/svg") | ||||||
|  |           .attr("xmlns:xlink", "http://www.w3.org/1999/xlink") | ||||||
|  |           .attr("version", "1.1") | ||||||
|  |           .style("overflow", "visible") | ||||||
|  |           .style("font", "10px sans-serif"); | ||||||
|  | 
 | ||||||
|  |       d3.csv(Sist2Api.getTreemapCsvUrl(indexId)).then(tabularData => { | ||||||
|  |         tabularData.forEach(row => { | ||||||
|  |           row.taxonomy = row.path.split("/"); | ||||||
|  |           row.size = Number(row.size); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         if (treemapType === "cascaded") { | ||||||
|  |           const data = burrow(tabularData, false); | ||||||
|  |           cascadeTreemap(data, treemapSvg, width, height, tilingMode, treemapColor); | ||||||
|  |         } else { | ||||||
|  |           const data = burrow(tabularData.sort((a, b) => b.taxonomy.length - a.taxonomy.length), true); | ||||||
|  |           const fillOpacity = this.$store.state.optTheme === "black" ? 0.9 : 0.6; | ||||||
|  |           flatTreemap(data, treemapSvg, width, height, groupingDepth, tilingMode, fillOpacity); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     downloadTreemap() { | ||||||
|  |       const width = SIZES[this.$store.state.optTreemapSize][0]; | ||||||
|  |       const height = SIZES[this.$store.state.optTreemapSize][1]; | ||||||
|  | 
 | ||||||
|  |       exportTreemap(this.indexId, width, height); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										66
									
								
								sist2-vue/src/components/DateSlider.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								sist2-vue/src/components/DateSlider.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | |||||||
|  | <template> | ||||||
|  |   <div id="dateSlider"></div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import noUiSlider from 'nouislider'; | ||||||
|  | import 'nouislider/dist/nouislider.css'; | ||||||
|  | import {humanDate} from "@/util"; | ||||||
|  | import {mergeTooltips} from "@/util-js"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "DateSlider", | ||||||
|  |   mounted() { | ||||||
|  |     this.$store.subscribe((mutation) => { | ||||||
|  |       if (mutation.type === "setDateBoundsMax") { | ||||||
|  |         const elem = document.getElementById("dateSlider"); | ||||||
|  | 
 | ||||||
|  |         if (elem.children.length > 0) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const dateMax = this.$store.state.dateBoundsMax; | ||||||
|  |         const dateMin = this.$store.state.dateBoundsMin; | ||||||
|  | 
 | ||||||
|  |         const slider = noUiSlider.create(elem, { | ||||||
|  |           start: [ | ||||||
|  |             this.$store.state.dateMin ? this.$store.state.dateMin : dateMin, | ||||||
|  |             this.$store.state.dateMax ? this.$store.state.dateMax : dateMax | ||||||
|  |           ], | ||||||
|  | 
 | ||||||
|  |           tooltips: [true, true], | ||||||
|  |           behaviour: "drag-tap", | ||||||
|  |           connect: true, | ||||||
|  |           range: { | ||||||
|  |             "min": dateMin, | ||||||
|  |             "max": dateMax, | ||||||
|  |           }, | ||||||
|  |           format: { | ||||||
|  |             to: x => humanDate(x), | ||||||
|  |             from: x => x | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         mergeTooltips(elem, 10, " - ", true) | ||||||
|  | 
 | ||||||
|  |         elem.querySelectorAll('.noUi-connect')[0].classList.add("slider-color0") | ||||||
|  | 
 | ||||||
|  |         slider.on("set", (values, handle, unencoded) => { | ||||||
|  |           if (handle === 0) { | ||||||
|  |             this.$store.commit("setDateMin", unencoded[0] === dateMin ? undefined : Math.round(unencoded[0])); | ||||||
|  |           } else { | ||||||
|  |             this.$store.commit("setDateMax", unencoded[1] >= dateMax ? undefined : Math.round(unencoded[1])); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | #dateSlider { | ||||||
|  |   margin-top: 34px; | ||||||
|  |   height: 12px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										22
									
								
								sist2-vue/src/components/DebugIcon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								sist2-vue/src/components/DebugIcon.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 309.998 309.998" fill="currentColor"> | ||||||
|  |     <path | ||||||
|  |         d="M294.998,155.03H250v-48.82l39.714-39.715c5.858-5.857,5.858-15.356,0-21.213c-5.857-5.857-15.355-5.857-21.213,0 l-23.7,23.701c-12.885-37.2-48.274-63.984-89.802-63.984c-41.528,0-76.913,26.787-89.797,63.989L41.497,45.282 c-5.856-5.859-15.354-5.857-21.213,0s-5.858,15.355,0,21.213L60,106.212v48.818H15c-8.284,0-15,6.716-15,15c0,8.284,6.716,15,15,15 h45.134c0.855,16.314,5.849,31.551,13.944,44.68l-49.685,49.683c-5.858,5.857-5.858,15.354,0,21.213 c2.929,2.93,6.768,4.394,10.607,4.394c3.838,0,7.678-1.465,10.606-4.394l48.095-48.093c16.558,14.018,37.957,22.486,61.301,22.486 c0.019,0,0.037-0.001,0.057-0.001c0.011,0,0.022,0.002,0.033,0.002c0.019,0,0.037-0.003,0.056-0.003 c23.285-0.035,44.629-8.494,61.15-22.483l48.094,48.092c2.929,2.929,6.768,4.394,10.606,4.394c3.839,0,7.678-1.465,10.607-4.394 c5.858-5.858,5.858-15.355,0-21.213l-49.683-49.681c8.096-13.131,13.089-28.366,13.944-44.682h45.132c8.284,0,15-6.716,15-15 C309.998,161.746,303.282,155.03,294.998,155.03z M154.999,34.999c30.681,0,56.465,21.365,63.254,50H91.747 C98.535,56.364,124.318,34.999,154.999,34.999z M90,179.999v-9.272c0.011-0.232,0.035-0.462,0.035-0.696 c0-0.234-0.024-0.464-0.035-0.695v-54.336h50.092v128.254C111.415,236.494,90,210.708,90,179.999z M170.092,243.212V114.999H220 v54.297c-0.012,0.244-0.037,0.486-0.037,0.734c0,0.248,0.025,0.49,0.037,0.734v9.234C220,210.645,198.676,236.388,170.092,243.212z"/> | ||||||
|  |   </svg> | ||||||
|  | 
 | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: "DebugIcon" | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | svg { | ||||||
|  |   display: inline-block; | ||||||
|  |   width: 20px; | ||||||
|  |   height: 20px; | ||||||
|  |   vertical-align: middle; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										39
									
								
								sist2-vue/src/components/DebugInfo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								sist2-vue/src/components/DebugInfo.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | <template> | ||||||
|  |   <b-card class="mb-4 mt-4"> | ||||||
|  |     <b-card-title><DebugIcon class="mr-1"></DebugIcon>{{ $t("debug") }}</b-card-title> | ||||||
|  |     <p v-html="$t('debugDescription')"></p> | ||||||
|  | 
 | ||||||
|  |     <b-card-body> | ||||||
|  | 
 | ||||||
|  |       <!-- TODO: ES connectivity, Link to GH page --> | ||||||
|  |       <b-table :items="tableItems" small borderless responsive="md" thead-class="hidden" class="mb-0"></b-table> | ||||||
|  | 
 | ||||||
|  |       <hr /> | ||||||
|  |       <IndexDebugInfo v-for="idx of $store.state.sist2Info.indices" :key="idx.id" :index="idx" class="mt-2"></IndexDebugInfo> | ||||||
|  |     </b-card-body> | ||||||
|  |   </b-card> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import IndexDebugInfo from "@/components/IndexDebugInfo"; | ||||||
|  | import DebugIcon from "@/components/DebugIcon"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "DebugInfo.vue", | ||||||
|  |   components: {DebugIcon, IndexDebugInfo}, | ||||||
|  |   computed: { | ||||||
|  |     tableItems() { | ||||||
|  |       return [ | ||||||
|  |         {key: "version", value: this.$store.state.sist2Info.version}, | ||||||
|  |         {key: "platform", value: this.$store.state.sist2Info.platform}, | ||||||
|  |         {key: "debugBinary", value: this.$store.state.sist2Info.debug}, | ||||||
|  |         {key: "sist2CommitHash", value: this.$store.state.sist2Info.sist2Hash}, | ||||||
|  |         {key: "libscanCommitHash", value: this.$store.state.sist2Info.libscanHash}, | ||||||
|  |         {key: "esIndex", value: this.$store.state.sist2Info.esIndex}, | ||||||
|  |         {key: "tagline", value: this.$store.state.sist2Info.tagline}, | ||||||
|  |         {key: "dev", value: this.$store.state.sist2Info.dev}, | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										37
									
								
								sist2-vue/src/components/DisplayModeToggle.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								sist2-vue/src/components/DisplayModeToggle.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | <template> | ||||||
|  |   <b-button-group> | ||||||
|  |     <b-button variant="primary" :title="$t('displayMode.list')" :pressed="optDisplay==='list'" | ||||||
|  |               @click="setOptDisplay('list')"> | ||||||
|  |       <svg width="20px" height="20px" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> | ||||||
|  |         <path fill="currentColor" | ||||||
|  |               d="M80 368H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm0-320H16A16 16 0 0 0 0 64v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16zm0 160H16a16 16 0 0 0-16 16v64a16 16 0 0 0 16 16h64a16 16 0 0 0 16-16v-64a16 16 0 0 0-16-16zm416 176H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-320H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16V80a16 16 0 0 0-16-16zm0 160H176a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h320a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16z"></path> | ||||||
|  |       </svg> | ||||||
|  |     </b-button> | ||||||
|  | 
 | ||||||
|  |     <b-button variant="primary" :title="$t('displayMode.grid')" :pressed="optDisplay==='grid'" | ||||||
|  |               @click="setOptDisplay('grid')"> | ||||||
|  |       <svg width="20px" height="20px" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> | ||||||
|  |         <path fill="currentColor" | ||||||
|  |               d="M149.333 56v80c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24V56c0-13.255 10.745-24 24-24h101.333c13.255 0 24 10.745 24 24zm181.334 240v-80c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24h101.333c13.256 0 24.001-10.745 24.001-24zm32-240v80c0 13.255 10.745 24 24 24H488c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24H386.667c-13.255 0-24 10.745-24 24zm-32 80V56c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24h101.333c13.256 0 24.001-10.745 24.001-24zm-205.334 56H24c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24h101.333c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24zM0 376v80c0 13.255 10.745 24 24 24h101.333c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H24c-13.255 0-24 10.745-24 24zm386.667-56H488c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H386.667c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24zm0 160H488c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H386.667c-13.255 0-24 10.745-24 24v80c0 13.255 10.745 24 24 24zM181.333 376v80c0 13.255 10.745 24 24 24h101.333c13.255 0 24-10.745 24-24v-80c0-13.255-10.745-24-24-24H205.333c-13.255 0-24 10.745-24 24z"></path> | ||||||
|  |       </svg> | ||||||
|  |     </b-button> | ||||||
|  |   </b-button-group> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import {mapGetters, mapMutations} from "vuex"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "DisplayModeToggle", | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters(["optDisplay"]) | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapMutations(["setOptDisplay"]) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
							
								
								
									
										232
									
								
								sist2-vue/src/components/DocCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								sist2-vue/src/components/DocCard.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,232 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="doc-card" :class="{'sub-document': doc._props.isSubDocument}" :style="`width: ${width}px`"> | ||||||
|  |     <b-card | ||||||
|  |         no-body | ||||||
|  |         img-top | ||||||
|  |     > | ||||||
|  |       <!-- Info modal--> | ||||||
|  |       <DocInfoModal :show="showInfo" :doc="doc" @close="showInfo = false"></DocInfoModal> | ||||||
|  | 
 | ||||||
|  |       <ContentDiv :doc="doc"></ContentDiv> | ||||||
|  | 
 | ||||||
|  |       <!-- Thumbnail--> | ||||||
|  |       <div v-if="doc._props.hasThumbnail" class="img-wrapper" @mouseenter="onTnEnter()" @mouseleave="onTnLeave()"> | ||||||
|  |         <div v-if="doc._props.isAudio" class="card-img-overlay" :class="{'small-badge': smallBadge}"> | ||||||
|  |           <span class="badge badge-resolution">{{ humanTime(doc._source.duration) }}</span> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <div v-if="doc._props.isImage && !hover" class="card-img-overlay" :class="{'small-badge': smallBadge}"> | ||||||
|  |           <span class="badge badge-resolution">{{ `${doc._source.width}x${doc._source.height}` }}</span> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <div v-if="(doc._props.isVideo || doc._props.isGif) && doc._source.duration > 0 && !hover" class="card-img-overlay" | ||||||
|  |              :class="{'small-badge': smallBadge}"> | ||||||
|  |           <span class="badge badge-resolution">{{ humanTime(doc._source.duration) }}</span> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <div v-if="doc._props.isPlayableVideo" class="play"> | ||||||
|  |           <svg viewBox="0 0 494.942 494.942" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |             <path d="m35.353 0 424.236 247.471-424.236 247.471z"/> | ||||||
|  |           </svg> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <img v-if="doc._props.isPlayableImage || doc._props.isPlayableVideo" | ||||||
|  |              :src="(doc._props.isGif && hover) ? `f/${doc._id}` : `t/${doc._source.index}/${doc._id}`" | ||||||
|  |              alt="" | ||||||
|  |              class="pointer fit card-img-top" @click="onThumbnailClick()"> | ||||||
|  |         <img v-else :src="`t/${doc._source.index}/${doc._id}`" alt="" | ||||||
|  |              class="fit card-img-top"> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <!-- Audio player--> | ||||||
|  |       <audio v-if="doc._props.isAudio" ref="audio" preload="none" class="audio-fit fit" controls :type="doc._source.mime" | ||||||
|  |              :src="`f/${doc._id}`" | ||||||
|  |              @play="onAudioPlay()"></audio> | ||||||
|  | 
 | ||||||
|  |       <b-card-body class="padding-03"> | ||||||
|  | 
 | ||||||
|  |         <!-- Title line --> | ||||||
|  |         <div style="display: flex"> | ||||||
|  |           <span class="info-icon" @click="onInfoClick()"></span> | ||||||
|  |           <DocFileTitle :doc="doc"></DocFileTitle> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- Tags --> | ||||||
|  |         <div class="card-text"> | ||||||
|  |           <TagContainer :hit="doc"></TagContainer> | ||||||
|  |         </div> | ||||||
|  |       </b-card-body> | ||||||
|  |     </b-card> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import {ext, humanFileSize, humanTime} from "@/util"; | ||||||
|  | import TagContainer from "@/components/TagContainer.vue"; | ||||||
|  | import DocFileTitle from "@/components/DocFileTitle.vue"; | ||||||
|  | import DocInfoModal from "@/components/DocInfoModal.vue"; | ||||||
|  | import ContentDiv from "@/components/ContentDiv.vue"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   components: {ContentDiv, DocInfoModal, DocFileTitle, TagContainer}, | ||||||
|  |   props: ["doc", "width"], | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       ext: ext, | ||||||
|  |       showInfo: false, | ||||||
|  |       hover: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     placeHolderStyle() { | ||||||
|  | 
 | ||||||
|  |       const tokens = this.doc._source.thumbnail.split(","); | ||||||
|  |       const w = Number(tokens[0]); | ||||||
|  |       const h = Number(tokens[1]); | ||||||
|  | 
 | ||||||
|  |       const MAX_HEIGHT = 400; | ||||||
|  | 
 | ||||||
|  |       return { | ||||||
|  |         height: `${Math.min((h / w) * this.width, MAX_HEIGHT)}px`, | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     smallBadge() { | ||||||
|  |       return this.width < 150; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     humanFileSize: humanFileSize, | ||||||
|  |     humanTime: humanTime, | ||||||
|  |     onInfoClick() { | ||||||
|  |       this.showInfo = true; | ||||||
|  |     }, | ||||||
|  |     async onThumbnailClick() { | ||||||
|  |       this.$store.commit("setUiLightboxSlide", this.doc._seq); | ||||||
|  |       await this.$store.dispatch("showLightbox"); | ||||||
|  |     }, | ||||||
|  |     onAudioPlay() { | ||||||
|  |       document.getElementsByTagName("audio").forEach((el) => { | ||||||
|  |         if (el !== this.$refs["audio"]) { | ||||||
|  |           el.pause(); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     onTnEnter() { | ||||||
|  |       this.hover = true; | ||||||
|  |     }, | ||||||
|  |     onTnLeave() { | ||||||
|  |       this.hover = false; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | <style> | ||||||
|  | .img-wrapper { | ||||||
|  |   position: relative; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .img-wrapper:hover svg { | ||||||
|  |   fill: rgba(0, 0, 0, 1); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .pointer { | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .fit { | ||||||
|  |   display: block; | ||||||
|  |   min-width: 64px; | ||||||
|  |   max-width: 100%; | ||||||
|  |   /*max-height: 400px;*/ | ||||||
|  |   margin: 0 auto 0; | ||||||
|  |   width: auto; | ||||||
|  |   height: auto; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | .card-img-top { | ||||||
|  |   border-top-left-radius: 0; | ||||||
|  |   border-top-right-radius: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .padding-03 { | ||||||
|  |   padding: 0.3rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .card { | ||||||
|  |   margin-top: 1em; | ||||||
|  |   margin-left: 0; | ||||||
|  |   margin-right: 0; | ||||||
|  |   box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important; | ||||||
|  |   border-radius: 0; | ||||||
|  |   border: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .card-body { | ||||||
|  |   padding: 0.3rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .thumbnail-placeholder { | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .card-img-overlay { | ||||||
|  |   pointer-events: none; | ||||||
|  |   padding: 0.75rem; | ||||||
|  |   bottom: unset; | ||||||
|  |   top: 0; | ||||||
|  |   left: unset; | ||||||
|  |   right: unset; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .badge-resolution { | ||||||
|  |   color: #212529; | ||||||
|  |   background-color: #FFC107; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .play { | ||||||
|  |   position: absolute; | ||||||
|  |   width: 25px; | ||||||
|  |   height: 25px; | ||||||
|  |   left: 50%; | ||||||
|  |   top: 50%; | ||||||
|  |   transform: translate(-50%, -50%); | ||||||
|  |   pointer-events: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .play svg { | ||||||
|  |   fill: rgba(0, 0, 0, 0.7); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .doc-card { | ||||||
|  |   padding-left: 3px; | ||||||
|  |   padding-right: 3px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .small-badge { | ||||||
|  |   padding: 1px 3px; | ||||||
|  |   font-size: 70%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .audio-fit { | ||||||
|  |   height: 39px; | ||||||
|  |   vertical-align: bottom; | ||||||
|  |   display: inline; | ||||||
|  |   width: 100%; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .sub-document .card { | ||||||
|  |   background: #AB47BC1F !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .sub-document .card { | ||||||
|  |   background: #37474F !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .sub-document .fit { | ||||||
|  |   padding: 4px 4px 0 4px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										69
									
								
								sist2-vue/src/components/DocCardWall.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								sist2-vue/src/components/DocCardWall.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | <template> | ||||||
|  |   <GridLayout | ||||||
|  |       ref="grid-layout" | ||||||
|  |       :options="gridOptions" | ||||||
|  |       @append="append" | ||||||
|  |       @layout-complete="$emit('layout-complete')" | ||||||
|  |   > | ||||||
|  |     <DocCard v-for="doc in docs" :key="doc._id" :doc="doc" :width="width"></DocCard> | ||||||
|  |   </GridLayout> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import Vue from "vue"; | ||||||
|  | import DocCard from "@/components/DocCard"; | ||||||
|  | 
 | ||||||
|  | import VueInfiniteGrid from "@egjs/vue-infinitegrid"; | ||||||
|  | 
 | ||||||
|  | Vue.use(VueInfiniteGrid); | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  |   components: { | ||||||
|  |     DocCard, | ||||||
|  |   }, | ||||||
|  |   props: ["docs", "append"], | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       width: 0, | ||||||
|  |       gridOptions: { | ||||||
|  |         align: "center", | ||||||
|  |         margin: 0, | ||||||
|  |         transitionDuration: 0, | ||||||
|  |         isOverflowScroll: false, | ||||||
|  |         isConstantSize: false, | ||||||
|  |         useFit: false, | ||||||
|  |         // Indicates whether keep the number of DOMs is maintained. If the useRecycle value is 'true', keep the number | ||||||
|  |         //  of DOMs is maintained. If the useRecycle value is 'false', the number of DOMs will increase as card elements | ||||||
|  |         //  are added. | ||||||
|  |         useRecycle: false | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     colCount() { | ||||||
|  |       const columns = this.$store.getters["optColumns"]; | ||||||
|  | 
 | ||||||
|  |       if (columns === "auto") { | ||||||
|  |         return Math.round(this.$refs["grid-layout"].$el.scrollWidth / 300) | ||||||
|  |       } | ||||||
|  |       return columns; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.width = this.$refs["grid-layout"].$el.scrollWidth / this.colCount; | ||||||
|  | 
 | ||||||
|  |     if (this.colCount === 1) { | ||||||
|  |       this.$refs["grid-layout"].$el.classList.add("grid-single-column"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.$store.subscribe((mutation) => { | ||||||
|  |       if (mutation.type === "busUpdateWallItems" && this.$refs["grid-layout"]) { | ||||||
|  |         this.$refs["grid-layout"].updateItems(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | </style> | ||||||
							
								
								
									
										63
									
								
								sist2-vue/src/components/DocFileTitle.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								sist2-vue/src/components/DocFileTitle.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | <template> | ||||||
|  |   <a :href="`f/${doc._id}`" class="file-title-anchor" target="_blank"> | ||||||
|  |     <div class="file-title" :title="doc._source.path + '/' + doc._source.name + ext(doc)" | ||||||
|  |          v-html="fileName() + ext(doc)"></div> | ||||||
|  |   </a> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import {ext} from "@/util"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "DocFileTitle", | ||||||
|  |   props: ["doc"], | ||||||
|  |   methods: { | ||||||
|  |     ext: ext, | ||||||
|  |     fileName() { | ||||||
|  |       if (!this.doc.highlight) { | ||||||
|  |         return this.doc._source.name; | ||||||
|  |       } | ||||||
|  |       if (this.doc.highlight["name.nGram"]) { | ||||||
|  |         return this.doc.highlight["name.nGram"]; | ||||||
|  |       } | ||||||
|  |       if (this.doc.highlight.name) { | ||||||
|  |         return this.doc.highlight.name; | ||||||
|  |       } | ||||||
|  |       return this.doc._source.name; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .file-title-anchor { | ||||||
|  |   max-width: calc(100% - 1.2rem); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .file-title { | ||||||
|  |   width: 100%; | ||||||
|  |   line-height: 1rem; | ||||||
|  |   height: 1.1rem; | ||||||
|  |   white-space: nowrap; | ||||||
|  |   text-overflow: ellipsis; | ||||||
|  |   overflow: hidden; | ||||||
|  |   font-size: 16px; | ||||||
|  |   font-family: "Source Sans Pro", sans-serif; | ||||||
|  |   font-weight: bold; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .file-title { | ||||||
|  |   color: #ddd; | ||||||
|  | } | ||||||
|  | .theme-black .file-title:hover { | ||||||
|  |   color: #fff; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-light .file-title { | ||||||
|  |   color: black; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .doc-card .file-title { | ||||||
|  |   font-size: 12px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										32
									
								
								sist2-vue/src/components/DocInfoModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								sist2-vue/src/components/DocInfoModal.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | <template> | ||||||
|  |   <b-modal :visible="show" size="lg" :hide-footer="true" static lazy @close="$emit('close')" @hide="$emit('close')" | ||||||
|  |   > | ||||||
|  |     <template #modal-title> | ||||||
|  |       <h5 class="modal-title" :title="doc._source.name + ext(doc)">{{ doc._source.name + ext(doc) }}</h5> | ||||||
|  |     </template> | ||||||
|  |     <img :src="`t/${doc._source.index}/${doc._id}`" alt="" class="fit card-img-top"> | ||||||
|  | 
 | ||||||
|  |     <InfoTable :doc="doc"></InfoTable> | ||||||
|  | 
 | ||||||
|  |     <LazyContentDiv :doc-id="doc._id"></LazyContentDiv> | ||||||
|  |   </b-modal> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import {ext} from "@/util"; | ||||||
|  | import InfoTable from "@/components/InfoTable"; | ||||||
|  | import LazyContentDiv from "@/components/LazyContentDiv"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "DocInfoModal", | ||||||
|  |   components: {LazyContentDiv, InfoTable}, | ||||||
|  |   props: ["doc", "show"], | ||||||
|  |   methods: { | ||||||
|  |     ext: ext, | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
							
								
								
									
										45
									
								
								sist2-vue/src/components/DocList.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								sist2-vue/src/components/DocList.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | |||||||
|  | <template> | ||||||
|  |   <b-list-group class="mt-3"> | ||||||
|  |     <DocListItem v-for="doc in docs" :key="doc._id" :doc="doc"></DocListItem> | ||||||
|  |   </b-list-group> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import DocListItem from "@/components/DocListItem.vue"; | ||||||
|  | import Vue from "vue"; | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  |   name: "DocList", | ||||||
|  |   components: {DocListItem}, | ||||||
|  |   props: ["docs", "append"], | ||||||
|  |   mounted() { | ||||||
|  |     window.addEventListener("scroll", () => { | ||||||
|  |       const threshold = 400; | ||||||
|  |       const app = document.getElementById("app"); | ||||||
|  | 
 | ||||||
|  |       if ((window.innerHeight + window.scrollY) >= app.offsetHeight - threshold) { | ||||||
|  |         this.append(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | 
 | ||||||
|  | .theme-black .list-group-item { | ||||||
|  |   background: #212121; | ||||||
|  |   color: #e0e0e0; | ||||||
|  | 
 | ||||||
|  |   border-bottom: none; | ||||||
|  |   border-left: none; | ||||||
|  |   border-right: none; | ||||||
|  |   border-radius: 0; | ||||||
|  |   padding: .25rem 0.5rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .list-group-item:first-child { | ||||||
|  |   border-top: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
							
								
								
									
										170
									
								
								sist2-vue/src/components/DocListItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								sist2-vue/src/components/DocListItem.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,170 @@ | |||||||
|  | <template> | ||||||
|  |   <b-list-group-item class="flex-column align-items-start mb-2"> | ||||||
|  | 
 | ||||||
|  |     <!-- Info modal--> | ||||||
|  |     <DocInfoModal :show="showInfo" :doc="doc" @close="showInfo = false"></DocInfoModal> | ||||||
|  | 
 | ||||||
|  |     <div class="media ml-2"> | ||||||
|  |       <div v-if="doc._props.hasThumbnail" class="align-self-start mr-2 wrapper-sm"> | ||||||
|  |         <div class="img-wrapper"> | ||||||
|  |           <div v-if="doc._props.isPlayableVideo" class="play"> | ||||||
|  |             <svg viewBox="0 0 494.942 494.942" xmlns="http://www.w3.org/2000/svg"> | ||||||
|  |               <path d="m35.353 0 424.236 247.471-424.236 247.471z"/> | ||||||
|  |             </svg> | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <img v-if="doc._props.isPlayableImage || doc._props.isPlayableVideo" | ||||||
|  |                :src="(doc._props.isGif && hover) ? `f/${doc._id}` : `t/${doc._source.index}/${doc._id}`" | ||||||
|  |                alt="" | ||||||
|  |                class="pointer fit-sm" @click="onThumbnailClick()"> | ||||||
|  |           <img v-else :src="`t/${doc._source.index}/${doc._id}`" alt="" | ||||||
|  |                class="fit-sm"> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div v-else class="file-icon-wrapper" style=""> | ||||||
|  |         <FileIcon></FileIcon> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <div class="doc-line ml-3"> | ||||||
|  |         <div style="display: flex"> | ||||||
|  |           <span class="info-icon" @click="showInfo = true"></span> | ||||||
|  |           <DocFileTitle :doc="doc"></DocFileTitle> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- Content highlight --> | ||||||
|  |         <ContentDiv :doc="doc"></ContentDiv> | ||||||
|  | 
 | ||||||
|  |         <div class="path-row"> | ||||||
|  |           <div class="path-line" v-html="path()"></div> | ||||||
|  |           <TagContainer :hit="doc"></TagContainer> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <div v-if="doc._source.pages || doc._source.author" class="path-row text-muted"> | ||||||
|  |           <span v-if="doc._source.pages">{{ doc._source.pages }} {{ doc._source.pages > 1 ? $t("pages") : $t("page") }}</span> | ||||||
|  |           <span v-if="doc._source.author && doc._source.pages" class="mx-1">-</span> | ||||||
|  |           <span v-if="doc._source.author">{{doc._source.author}}</span> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </b-list-group-item> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import TagContainer from "@/components/TagContainer"; | ||||||
|  | import DocFileTitle from "@/components/DocFileTitle"; | ||||||
|  | import DocInfoModal from "@/components/DocInfoModal"; | ||||||
|  | import ContentDiv from "@/components/ContentDiv"; | ||||||
|  | import FileIcon from "@/components/FileIcon"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "DocListItem", | ||||||
|  |   components: {FileIcon, ContentDiv, DocInfoModal, DocFileTitle, TagContainer}, | ||||||
|  |   props: ["doc"], | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       hover: false, | ||||||
|  |       showInfo: false | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     async onThumbnailClick() { | ||||||
|  |       this.$store.commit("setUiLightboxSlide", this.doc._seq); | ||||||
|  |       await this.$store.dispatch("showLightbox"); | ||||||
|  |     }, | ||||||
|  |     path() { | ||||||
|  |       if (!this.doc.highlight) { | ||||||
|  |         return this.doc._source.path + "/" | ||||||
|  |       } | ||||||
|  |       if (this.doc.highlight["path.text"]) { | ||||||
|  |         return this.doc.highlight["path.text"] + "/" | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (this.doc.highlight["path.nGram"]) { | ||||||
|  |         return this.doc.highlight["path.nGram"] + "/" | ||||||
|  |       } | ||||||
|  |       return this.doc._source.path + "/" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .list-group { | ||||||
|  |   margin-top: 1em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .list-group-item { | ||||||
|  |   padding: .25rem 0.5rem; | ||||||
|  | 
 | ||||||
|  |   box-shadow: 0 0.125rem 0.25rem rgb(0 0 0 / 8%) !important; | ||||||
|  |   border-radius: 0; | ||||||
|  |   border: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .path-row { | ||||||
|  |   display: -ms-flexbox; | ||||||
|  |   display: flex; | ||||||
|  |   -ms-flex-align: start; | ||||||
|  |   align-items: flex-start; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .path-line { | ||||||
|  |   color: #808080; | ||||||
|  |   text-overflow: ellipsis; | ||||||
|  |   overflow: hidden; | ||||||
|  |   white-space: nowrap; | ||||||
|  |   margin-right: 0.3em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .path-line { | ||||||
|  |   color: #bbb; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .play { | ||||||
|  |   position: absolute; | ||||||
|  |   width: 18px; | ||||||
|  |   height: 18px; | ||||||
|  |   left: 50%; | ||||||
|  |   top: 50%; | ||||||
|  |   transform: translate(-50%, -50%); | ||||||
|  |   pointer-events: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .play svg { | ||||||
|  |   fill: rgba(0, 0, 0, 0.7); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .list-group-item .img-wrapper { | ||||||
|  |   width: 88px; | ||||||
|  |   height: 88px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .fit-sm { | ||||||
|  |   max-height: 100%; | ||||||
|  |   max-width: 100%; | ||||||
|  |   width: auto; | ||||||
|  |   height: auto; | ||||||
|  |   position: absolute; | ||||||
|  |   top: 0; | ||||||
|  |   bottom: 0; | ||||||
|  |   left: 0; | ||||||
|  |   right: 0; | ||||||
|  |   margin: auto; | ||||||
|  | 
 | ||||||
|  |   /*box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.12);*/ | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .doc-line { | ||||||
|  |   max-width: calc(100% - 88px - 1.5rem); | ||||||
|  |   flex: 1; | ||||||
|  |   vertical-align: middle; | ||||||
|  |   margin-top: auto; | ||||||
|  |   margin-bottom: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .file-icon-wrapper { | ||||||
|  |   width: calc(88px + .5rem); | ||||||
|  |   height: 88px; | ||||||
|  |   position: relative; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										33
									
								
								sist2-vue/src/components/FileIcon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								sist2-vue/src/components/FileIcon.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | |||||||
|  | <template> | ||||||
|  |   <svg class="file-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"> | ||||||
|  |     <path | ||||||
|  |         fill="currentColor" | ||||||
|  |         d="M 7 2 L 7 48 L 43 48 L 43 14.59375 L 42.71875 14.28125 L 30.71875 2.28125 L 30.40625 2 Z M 9 4 L 29 4 L 29 16 L 41 16 L 41 46 L 9 46 Z M 31 5.4375 L 39.5625 14 L 31 14 Z"/> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: "FileIcon" | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .file-icon { | ||||||
|  |   position: absolute; | ||||||
|  |   max-height: 100%; | ||||||
|  |   max-width: 100%; | ||||||
|  |   top: 0; | ||||||
|  |   bottom: 0; | ||||||
|  |   left: 0; | ||||||
|  |   right: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .file-icon { | ||||||
|  |   color: #ffffff50; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-light .file-icon { | ||||||
|  |   color: #000000a0; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										23
									
								
								sist2-vue/src/components/GearIcon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								sist2-vue/src/components/GearIcon.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | <template> | ||||||
|  |   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" fill="currentColor"> | ||||||
|  |     <g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"> | ||||||
|  |       <path | ||||||
|  |           d="M4568.5,5011c-73.2-7.7-154-25-177.1-36.6c-84.7-46.2-102-119.4-159.8-689.2s-65.5-610.3-159.8-670c-23.1-15.4-125.1-55.8-225.3-90.5c-100.1-32.7-290.7-111.7-423.6-175.2c-319.6-152.1-315.8-152.1-619.9,94.3c-718.1,583.3-650.7,535.2-747,535.2c-77,0-104-11.6-184.8-77c-157.9-127.1-410.1-375.4-567.9-558.3c-155.9-177.1-190.6-250.3-159.8-344.6c9.6-27,165.6-227.2,344.6-446.7c181-219.5,342.7-425.5,360-458.2c52-88.6,42.3-150.2-50.1-335c-73.2-148.3-144.4-325.4-252.2-623.8c-17.3-50-57.8-113.6-88.6-138.6c-63.5-53.9-59.7-53.9-695-117.4c-527.5-52-577.6-65.5-627.6-179c-46.2-105.9-46.2-1057,0-1162.9c50-113.6,98.2-127.1,646.9-181c271.5-25,523.7-52,560.2-57.8c111.7-17.3,179.1-107.8,259.9-344.6c38.5-115.5,119.4-310,177.1-431.3c57.8-119.4,104-240.7,104-269.5c0-78.9-42.4-140.5-394.7-568c-179-219.5-335-419.7-344.6-446.6c-30.8-94.3,3.9-167.5,159.8-344.6c157.9-181,410.1-429.3,564.1-554.5c96.3-78.9,188.7-105.9,265.7-75.1c26.9,11.6,234.9,173.3,462.1,360c227.2,188.7,433.2,348.5,458.2,358.1c82.8,30.8,136.7,17.3,354.3-86.6c119.4-57.8,308-136.7,419.7-175.2c111.7-38.5,221.4-82.8,244.5-98.2c94.3-59.7,102-100.1,159.8-670c61.6-606.5,73.2-648.8,188.7-700.8c105.9-46.2,1057-46.2,1162.9,0c115.5,52,127.1,94.3,188.7,700.8c57.8,569.9,65.4,610.3,159.8,670c23.1,15.4,132.9,59.7,244.5,98.2s300.3,117.4,417.8,175.2c219.5,104,273.4,117.5,356.2,86.6c25-9.6,231-169.4,458.2-358.1c227.2-186.8,435.1-348.5,462.1-360c77-28.9,169.4-3.9,265.7,75.1c152.1,121.3,442.8,410.1,583.4,577.6c140.6,163.6,173.3,242.6,136.7,333.1c-11.6,27-173.3,234.9-360,462.1c-188.7,227.2-348.5,433.2-358.1,458.2c-30.8,82.8-17.3,136.7,86.6,356.2c57.8,117.4,138.6,311.9,177.1,427.4c80.9,236.8,148.3,327.3,259.9,344.6c36.6,5.8,288.8,32.7,562.2,59.7c308,28.9,517.9,59.7,550.6,77c30.8,15.4,71.2,59.7,90.5,100.1c32.8,65.4,36.6,123.2,34.7,573.7c0,562.2-11.5,627.6-115.5,687.3c-46.2,27-188.7,48.1-612.2,90.5c-573.7,59.7-614.2,67.4-673.8,161.7c-15.4,23.1-59.7,132.9-98.2,244.5s-117.4,300.3-175.2,417.8c-57.8,119.4-104,240.7-104,271.5c0,80.9,40.4,138.6,394.7,569.9c181,219.5,335,419.7,344.6,446.7c30.8,94.3-3.9,167.5-159.8,344.6c-157.9,181-410.1,429.3-564.1,554.5c-96.3,78.9-188.7,104-265.7,75.1c-27-11.6-234.9-173.3-462.1-360c-227.2-188.7-433.2-348.5-458.2-358.1c-80.9-30.8-130.9-19.2-371.6,96.3c-130.9,61.6-325.4,142.5-431.3,177.1c-217.5,71.2-308,140.5-325.4,250.3c-5.8,36.6-32.7,288.8-57.8,560.3c-53.9,550.6-67.4,596.8-181,645C5502.3,5018.7,4807.3,5036,4568.5,5011z M5463.8,1897.8c502.5-127.1,954.9-494.8,1184-960.7c446.7-914.5,78.9-2011.9-824-2460.5c-1053.1-521.8-2308.4,52-2604.9,1189.8c-71.2,277.2-71.2,629.6,0,904.9c192.5,737.4,814.4,1284.2,1569.1,1376.6C4974.8,1971,5255.9,1949.8,5463.8,1897.8z"/> | ||||||
|  |     </g> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: "GearIcon" | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | svg { | ||||||
|  |   display: inline-block; | ||||||
|  |   width: 20px; | ||||||
|  |   height: 20px; | ||||||
|  |   margin-top: -4px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										72
									
								
								sist2-vue/src/components/HelpDialog.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								sist2-vue/src/components/HelpDialog.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,72 @@ | |||||||
|  | <template> | ||||||
|  |   <b-modal :visible="show" size="lg" :hide-footer="true" static :title="$t('help.help')" | ||||||
|  |            @close="$emit('close')" | ||||||
|  |            @hide="$emit('close')" | ||||||
|  |   > | ||||||
|  |     <h2>{{$t("help.simpleSearch")}}</h2> | ||||||
|  | 
 | ||||||
|  |     <table class="table"> | ||||||
|  |       <tbody> | ||||||
|  |       <tr> | ||||||
|  |         <td><code>+</code></td> | ||||||
|  |         <td>{{$t("help.and")}}</td> | ||||||
|  |       </tr> | ||||||
|  |       <tr> | ||||||
|  |         <td><code>|</code></td> | ||||||
|  |         <td>{{$t("help.or")}}</td> | ||||||
|  |       </tr> | ||||||
|  |       <tr> | ||||||
|  |         <td><code>-</code></td> | ||||||
|  |         <td>{{$t("help.not")}}</td> | ||||||
|  |       </tr> | ||||||
|  |       <tr> | ||||||
|  |         <td><code>""</code></td> | ||||||
|  |         <td>{{$t("help.quotes")}}</td> | ||||||
|  |       </tr> | ||||||
|  |       <tr> | ||||||
|  |         <td><code>{{$t("help.term")}}*</code></td> | ||||||
|  |         <td>{{$t("help.prefix")}}</td> | ||||||
|  |       </tr> | ||||||
|  |       <tr> | ||||||
|  |         <td><code>(</code> {{$t("and")}} <code>)</code></td> | ||||||
|  |         <td>{{$t("help.parens")}}</td> | ||||||
|  |       </tr> | ||||||
|  |       <tr> | ||||||
|  |         <td><code>{{$t("help.term")}}~N</code></td> | ||||||
|  |         <td>{{$t("help.tildeTerm")}}</td> | ||||||
|  |       </tr> | ||||||
|  |       <tr> | ||||||
|  |         <td><code>"..."~N</code></td> | ||||||
|  |         <td>{{$t("help.tildePhrase")}}</td> | ||||||
|  |       </tr> | ||||||
|  |       </tbody> | ||||||
|  |     </table> | ||||||
|  | 
 | ||||||
|  |     <p v-html="$t('help.example1')"></p> | ||||||
|  | 
 | ||||||
|  |     <p v-html="$t('help.defaultOperator')"></p> | ||||||
|  | 
 | ||||||
|  |     <p v-html="$t('help.fuzzy')"></p> | ||||||
|  | 
 | ||||||
|  |     <br> | ||||||
|  | 
 | ||||||
|  |     <p v-html="$t('help.moreInfoSimple')"></p> | ||||||
|  | 
 | ||||||
|  |     <p></p> | ||||||
|  | 
 | ||||||
|  |     <h2>{{$t("help.advancedSearch")}}</h2> | ||||||
|  |     <p v-html="$t('help.moreInfoAdvanced')"></p> | ||||||
|  | 
 | ||||||
|  |   </b-modal> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: "HelpDialog", | ||||||
|  |   props: ["show"] | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
							
								
								
									
										30
									
								
								sist2-vue/src/components/IndexDebugInfo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								sist2-vue/src/components/IndexDebugInfo.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <h4>[{{ index.name }}]</h4> | ||||||
|  |     <b-table :items="tableItems" small borderless responsive="md" thead-class="hidden" class="mb-0"></b-table> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import {humanDate} from "@/util"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "IndexDebugInfo", | ||||||
|  |   props: ["index"], | ||||||
|  |   computed: { | ||||||
|  |     tableItems() { | ||||||
|  |       return [ | ||||||
|  |         {key: this.$t("name"), value: this.index.name}, | ||||||
|  |         {key: this.$t("id"), value: this.index.id}, | ||||||
|  |         {key: this.$t("indexVersion"), value: this.index.version}, | ||||||
|  |         {key: this.$t("rewriteUrl"), value: this.index.rewriteUrl}, | ||||||
|  |         {key: this.$t("timestamp"), value: humanDate(this.index.timestamp)}, | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
							
								
								
									
										93
									
								
								sist2-vue/src/components/IndexPicker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								sist2-vue/src/components/IndexPicker.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,93 @@ | |||||||
|  | <template> | ||||||
|  |   <VueMultiselect | ||||||
|  |       multiple | ||||||
|  |       label="name" | ||||||
|  |       :value="selectedIndices" | ||||||
|  |       :options="indices" | ||||||
|  |       :close-on-select="indices.length <= 1" | ||||||
|  |       :placeholder="$t('indexPickerPlaceholder')" | ||||||
|  |       @select="addItem" | ||||||
|  |       @remove="removeItem"> | ||||||
|  | 
 | ||||||
|  |     <template slot="option" slot-scope="idx"> | ||||||
|  |       <b-row> | ||||||
|  |         <b-col> | ||||||
|  |           <span class="mr-1">{{ idx.option.name }}</span> | ||||||
|  |           <SmallBadge pill :text="idx.option.version"></SmallBadge> | ||||||
|  |         </b-col> | ||||||
|  |       </b-row> | ||||||
|  |       <b-row class="mt-1"> | ||||||
|  |         <b-col> | ||||||
|  |           <span>{{ formatIdxDate(idx.option.timestamp) }}</span> | ||||||
|  |         </b-col> | ||||||
|  |       </b-row> | ||||||
|  |     </template> | ||||||
|  | 
 | ||||||
|  |   </VueMultiselect> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import VueMultiselect from "vue-multiselect" | ||||||
|  | import SmallBadge from "./SmallBadge.vue" | ||||||
|  | import {mapActions, mapGetters} from "vuex"; | ||||||
|  | import {Index} from "@/Sist2Api"; | ||||||
|  | import Vue from "vue"; | ||||||
|  | import {format} from "date-fns"; | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  |   components: { | ||||||
|  |     VueMultiselect, | ||||||
|  |     SmallBadge | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       loading: true | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters([ | ||||||
|  |       "indices", "selectedIndices" | ||||||
|  |     ]), | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapActions({ | ||||||
|  |       setSelectedIndices: "setSelectedIndices" | ||||||
|  |     }), | ||||||
|  |     removeItem(val: Index): void { | ||||||
|  |       this.setSelectedIndices(this.selectedIndices.filter((item: Index) => item !== val)) | ||||||
|  |     }, | ||||||
|  |     addItem(val: Index): void { | ||||||
|  |       this.setSelectedIndices([...this.selectedIndices, val]) | ||||||
|  |     }, | ||||||
|  |     formatIdxDate(timestamp: number): string { | ||||||
|  |       return format(new Date(timestamp * 1000), "yyyy-MM-dd"); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | }) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style src="vue-multiselect/dist/vue-multiselect.min.css"></style> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | .multiselect__option { | ||||||
|  |   padding: 5px 10px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .multiselect__content-wrapper { | ||||||
|  |   overflow: hidden; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .multiselect__tags { | ||||||
|  |   background: #37474F; | ||||||
|  |   border: 1px solid #616161 !important | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .multiselect__input { | ||||||
|  |   color: #dbdbdb; | ||||||
|  |   background: #37474F; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .multiselect__content-wrapper { | ||||||
|  |   border: none | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										93
									
								
								sist2-vue/src/components/InfoTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								sist2-vue/src/components/InfoTable.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,93 @@ | |||||||
|  | <template> | ||||||
|  |   <b-table :items="tableItems" small borderless responsive="md" thead-class="hidden" class="mb-0 mt-4"> | ||||||
|  | 
 | ||||||
|  |     <template #cell(value)="data"> | ||||||
|  |       <span v-if="'html' in data.item" v-html="data.item.html"></span> | ||||||
|  |       <span v-else>{{data.value}}</span> | ||||||
|  |     </template> | ||||||
|  |   </b-table> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import {humanDate, humanFileSize} from "@/util"; | ||||||
|  | 
 | ||||||
|  | function makeGpsLink(latitude, longitude) { | ||||||
|  | 
 | ||||||
|  |   if (isNaN(latitude) || isNaN(longitude)) { | ||||||
|  |     return ""; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return `<a target="_blank" href="https://maps.google.com/?q=${latitude},${longitude}&ll=${latitude},${longitude}&t=k&z=17">${latitude}, ${longitude}</a>`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function dmsToDecimal(dms, ref) { | ||||||
|  |   const tokens = dms.split(",") | ||||||
|  | 
 | ||||||
|  |   const d = Number(tokens[0].trim().split(":")[0]) / Number(tokens[0].trim().split(":")[1]) | ||||||
|  |   const m = Number(tokens[1].trim().split(":")[0]) / Number(tokens[1].trim().split(":")[1]) | ||||||
|  |   const s = Number(tokens[2].trim().split(":")[0]) / Number(tokens[2].trim().split(":")[1]) | ||||||
|  | 
 | ||||||
|  |   return (d + (m / 60) + (s / 3600)) * (ref === "S" || ref === "W" ? -1 : 1) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "InfoTable", | ||||||
|  |   props: ["doc"], | ||||||
|  |   computed: { | ||||||
|  |     tableItems() { | ||||||
|  |       const src = this.doc._source; | ||||||
|  | 
 | ||||||
|  |       const items = [ | ||||||
|  |         {key: "index", value: `[${this.$store.getters.indexMap[src.index].name}]`}, | ||||||
|  |         {key: "mtime", value: humanDate(src.mtime)}, | ||||||
|  |         {key: "mime", value: src.mime}, | ||||||
|  |         {key: "size", value: humanFileSize(src.size)}, | ||||||
|  |         {key: "path", value: src.path}, | ||||||
|  |       ]; | ||||||
|  | 
 | ||||||
|  |       if ("width" in this.doc._source) { | ||||||
|  |         items.push({ | ||||||
|  |           key: "image size", | ||||||
|  |           value: `${src.width}x${src.height}` | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const fields = [ | ||||||
|  |         "title", "duration", "audioc", "videoc", | ||||||
|  |         "bitrate", "artist", "album", "album_artist", "genre", "font_name", "author", | ||||||
|  |         "modified_by", "pages", "tag", | ||||||
|  |         "exif_make", "exif_software", "exif_exposure_time", "exif_fnumber", "exif_focal_length", | ||||||
|  |           "exif_user_comment", "exif_iso_speed_ratings", "exif_model", "exif_datetime", | ||||||
|  |       ]; | ||||||
|  | 
 | ||||||
|  |       fields.forEach(field => { | ||||||
|  |         if (field in src) { | ||||||
|  |           items.push({key: field, value: src[field]}); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       // Exif GPS | ||||||
|  |       if ("exif_gps_longitude_dec" in src) { | ||||||
|  |         items.push({ | ||||||
|  |           key: "Exif GPS", | ||||||
|  |           html: makeGpsLink(src["exif_gps_latitude_dec"], src["exif_gps_longitude_dec"]), | ||||||
|  |         }); | ||||||
|  |       } else if ("exif_gps_longitude_dms" in src) { | ||||||
|  |         items.push({ | ||||||
|  |           key: "Exif GPS", | ||||||
|  |           html: makeGpsLink( | ||||||
|  |                   dmsToDecimal(src["exif_gps_latitude_dms"], src["exif_gps_latitude_ref"]), | ||||||
|  |                   dmsToDecimal(src["exif_gps_longitude_dms"], src["exif_gps_longitude_ref"]), | ||||||
|  |               ), | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return items; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
							
								
								
									
										30
									
								
								sist2-vue/src/components/LazyContentDiv.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								sist2-vue/src/components/LazyContentDiv.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | <template> | ||||||
|  |   <Preloader v-if="loading"></Preloader> | ||||||
|  |   <div v-else-if="content" class="content-div">{{ content }}</div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import Sist2Api from "@/Sist2Api"; | ||||||
|  | import Preloader from "@/components/Preloader"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "LazyContentDiv", | ||||||
|  |   components: {Preloader}, | ||||||
|  |   props: ["docId"], | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       content: "", | ||||||
|  |       loading: true | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     Sist2Api.getDocInfo(this.docId).then(src => { | ||||||
|  |       this.content = src.data.content; | ||||||
|  |       this.loading = false; | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | </style> | ||||||
							
								
								
									
										129
									
								
								sist2-vue/src/components/Lightbox.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								sist2-vue/src/components/Lightbox.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,129 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <!-- TODO: Set slideshowTime as a configurable option--> | ||||||
|  |     <FsLightbox | ||||||
|  |         :key="lightboxKey" | ||||||
|  |         :toggler="showLightbox" | ||||||
|  |         :sources="lightboxSources" | ||||||
|  |         :thumbs="lightboxThumbs" | ||||||
|  |         :captions="lightboxCaptions" | ||||||
|  |         :types="lightboxTypes" | ||||||
|  |         :source-index="lightboxSlide" | ||||||
|  |         :custom-toolbar-buttons="customButtons" | ||||||
|  |         :slideshow-time="1000 * 10" | ||||||
|  |         :zoom-increment="0.5" | ||||||
|  |         :load-only-current-source="$store.getters.optLightboxLoadOnlyCurrent" | ||||||
|  |         :on-close="onClose" | ||||||
|  |         :on-open="onShow" | ||||||
|  |         :on-slide-change="onSlideChange" | ||||||
|  |     ></FsLightbox> | ||||||
|  | 
 | ||||||
|  |     <a id="lightbox-download" style="display: none"></a> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import FsLightbox from "fslightbox-vue"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "Lightbox", | ||||||
|  |   components: {FsLightbox}, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       customButtons: [ | ||||||
|  |         { | ||||||
|  |           viewBox: "0 0 384.928 384.928", | ||||||
|  |           d: "M321.339,245.334c-4.74-4.692-12.439-4.704-17.179,0l-99.551,98.564V12.03 c0-6.641-5.438-12.03-12.151-12.03s-12.151,5.39-12.151,12.03v331.868l-99.551-98.552c-4.74-4.704-12.439-4.704-17.179,0 s-4.74,12.319,0,17.011l120.291,119.088c4.692,4.644,12.499,4.644,17.191,0l120.291-119.088 C326.091,257.653,326.091,250.038,321.339,245.334C316.599,240.642,326.091,250.038,321.339,245.334z", | ||||||
|  |           width: "17px", | ||||||
|  |           height: "17px", | ||||||
|  |           title: "Download", | ||||||
|  |           onClick: this.onDownloadClick | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     showLightbox() { | ||||||
|  |       return this.$store.getters["uiShowLightbox"]; | ||||||
|  |     }, | ||||||
|  |     lightboxSources() { | ||||||
|  |       return this.$store.getters["uiLightboxSources"]; | ||||||
|  |     }, | ||||||
|  |     lightboxThumbs() { | ||||||
|  |       return this.$store.getters["uiLightboxThumbs"]; | ||||||
|  |     }, | ||||||
|  |     lightboxKey() { | ||||||
|  |       return this.$store.getters["uiLightboxKey"]; | ||||||
|  |     }, | ||||||
|  |     lightboxSlide() { | ||||||
|  |       return this.$store.getters["uiLightboxSlide"]; | ||||||
|  |     }, | ||||||
|  |     lightboxCaptions() { | ||||||
|  |       return this.$store.getters["uiLightboxCaptions"]; | ||||||
|  |     }, | ||||||
|  |     lightboxTypes() { | ||||||
|  |       return this.$store.getters["uiLightboxTypes"]; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     onDownloadClick() { | ||||||
|  |       const url = this.lightboxSources[this.lightboxSlide]; | ||||||
|  | 
 | ||||||
|  |       const a = document.getElementById("lightbox-download"); | ||||||
|  |       a.setAttribute("href", url); | ||||||
|  |       a.setAttribute("download", ""); | ||||||
|  |       a.click(); | ||||||
|  |     }, | ||||||
|  |     onShow() { | ||||||
|  |       this.$store.commit("setUiLightboxIsOpen", true); | ||||||
|  |     }, | ||||||
|  |     onClose() { | ||||||
|  |       this.$store.commit("setUiLightboxIsOpen", false); | ||||||
|  |     }, | ||||||
|  |     onSlideChange() { | ||||||
|  |       // Pause all videos when changing slide | ||||||
|  |       document.getElementsByTagName("video").forEach((el) => { | ||||||
|  |         el.pause(); | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | .fslightbox-toolbar-button:nth-child(2) { | ||||||
|  |   order: 1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .fslightbox-toolbar-button:nth-child(1) { | ||||||
|  |   order: 2; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .fslightbox-toolbar-button:nth-child(3) { | ||||||
|  |   order: 3; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .fslightbox-toolbar-button:nth-child(4) { | ||||||
|  |   order: 4; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .fslightbox-toolbar-button:nth-child(5) { | ||||||
|  |   order: 5; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media (max-width: 650px) { | ||||||
|  |   /* Disable fullscreen on mobile because it's buggy */ | ||||||
|  |   .fslightbox-toolbar-button:nth-child(6) { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .fslightbox-toolbar-button:nth-child(6) { | ||||||
|  |   order: 6; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .fslightbox-toolbar-button:nth-child(7) { | ||||||
|  |   order: 7; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										26
									
								
								sist2-vue/src/components/LightboxCaption.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								sist2-vue/src/components/LightboxCaption.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="lightbox-caption"> | ||||||
|  |     <p> | ||||||
|  |       <b>{{ | ||||||
|  |           `[${$store.getters.indices.find(i => i.id === hit._source.index).name}]` | ||||||
|  |         }}</b>{{ `/${hit._source.path}/${hit._source.name}${ext(hit)}` }} | ||||||
|  |     </p> | ||||||
|  |     <p style="margin-top: -1em"> | ||||||
|  |       <span v-if="hit._source.width">{{ `${hit._source.width}x${hit._source.height}`}}</span> | ||||||
|  |       {{ ` (${humanFileSize(hit._source.size)})` }} | ||||||
|  |     </p> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import {ext, humanFileSize} from "@/util"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "LightboxCaption", | ||||||
|  |   props: ["hit"], | ||||||
|  |   methods: { | ||||||
|  |     humanFileSize: humanFileSize, | ||||||
|  |     ext: ext | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										61
									
								
								sist2-vue/src/components/MimePicker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								sist2-vue/src/components/MimePicker.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,61 @@ | |||||||
|  | <template> | ||||||
|  |   <div id="mimeTree"></div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import InspireTree from "inspire-tree"; | ||||||
|  | import InspireTreeDOM from "inspire-tree-dom"; | ||||||
|  | 
 | ||||||
|  | import "inspire-tree-dom/dist/inspire-tree-light.min.css"; | ||||||
|  | import {getSelectedTreeNodes} from "@/util"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "MimePicker", | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       mimeTree: null, | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.$store.subscribe((mutation) => { | ||||||
|  |       if (mutation.type === "setUiMimeMap") { | ||||||
|  |         const mimeMap = mutation.payload.slice(); | ||||||
|  | 
 | ||||||
|  |         this.mimeTree = new InspireTree({ | ||||||
|  |           selection: { | ||||||
|  |             mode: 'checkbox' | ||||||
|  |           }, | ||||||
|  |           data: mimeMap | ||||||
|  |         }); | ||||||
|  |         new InspireTreeDOM(this.mimeTree, { | ||||||
|  |           target: '#mimeTree' | ||||||
|  |         }); | ||||||
|  |         this.mimeTree.on("node.state.changed", this.handleTreeClick); | ||||||
|  |         this.mimeTree.deselect(); | ||||||
|  | 
 | ||||||
|  |         if (this.$store.state._onLoadSelectedMimeTypes.length > 0) { | ||||||
|  |           this.$store.state._onLoadSelectedMimeTypes.forEach(mime => { | ||||||
|  |             this.mimeTree.node(mime).select(); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     handleTreeClick(node, e) { | ||||||
|  |       if (e === "indeterminate" || e === "collapsed") { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this.$store.commit("setSelectedMimeTypes", getSelectedTreeNodes(this.mimeTree)); | ||||||
|  |     }, | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | #mimeTree { | ||||||
|  |   max-height: 350px; | ||||||
|  |   overflow: auto; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										101
									
								
								sist2-vue/src/components/NavBar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								sist2-vue/src/components/NavBar.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | |||||||
|  | <template> | ||||||
|  |   <b-navbar> | ||||||
|  |     <b-navbar-brand v-if="$route.path !== '/'" to="/"> | ||||||
|  |       <Sist2Icon></Sist2Icon> | ||||||
|  |     </b-navbar-brand> | ||||||
|  |     <b-navbar-brand v-else href="."> | ||||||
|  |       <Sist2Icon></Sist2Icon> | ||||||
|  |     </b-navbar-brand> | ||||||
|  | 
 | ||||||
|  |     <span class="badge badge-pill version" v-if="$store && $store.state.sist2Info"> | ||||||
|  |       {{ sist2Version() }}<span v-if="isDebug()">-dbg</span> | ||||||
|  |     </span> | ||||||
|  | 
 | ||||||
|  |     <span v-if="$store && $store.state.sist2Info" class="tagline" v-html="tagline()"></span> | ||||||
|  | 
 | ||||||
|  |     <b-button class="ml-auto" to="stats" variant="link">{{ $t("stats") }}</b-button> | ||||||
|  |     <b-button to="config" variant="link">{{ $t("config") }}</b-button> | ||||||
|  |   </b-navbar> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import Sist2Icon from "@/components/Sist2Icon"; | ||||||
|  | export default { | ||||||
|  |   name: "NavBar", | ||||||
|  |   components: {Sist2Icon}, | ||||||
|  |   methods: { | ||||||
|  |     tagline() { | ||||||
|  |       return this.$store.state.sist2Info.tagline; | ||||||
|  |     }, | ||||||
|  |     sist2Version() { | ||||||
|  |       return this.$store.state.sist2Info.version; | ||||||
|  |     }, | ||||||
|  |     isDebug() { | ||||||
|  |       return this.$store.state.sist2Info.debug; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .navbar { | ||||||
|  |   box-shadow: 0 0.125rem 0.25rem rgb(0 0 0 / 8%) !important; | ||||||
|  |   border-radius: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .navbar { | ||||||
|  |   background: #546b7a30; | ||||||
|  |   border-bottom: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .navbar-brand { | ||||||
|  |   color: #222 !important; | ||||||
|  |   font-size: 1.75rem; | ||||||
|  |   padding: 0; | ||||||
|  |   font-family: Hack; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .navbar-brand:hover { | ||||||
|  |   color: #000 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .version { | ||||||
|  |   color: #222 !important; | ||||||
|  |   margin-left: -18px; | ||||||
|  |   margin-top: -14px; | ||||||
|  |   font-size: 11px; | ||||||
|  |   font-family: monospace; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .version { | ||||||
|  |   color: #f5f5f5 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .navbar-brand { | ||||||
|  |   font-size: 1.75rem; | ||||||
|  |   padding: 0; | ||||||
|  |   color: #f5f5f5 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black a:hover, .theme-black .btn:hover { | ||||||
|  |   color: #fff; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .navbar span { | ||||||
|  |   color: #eee; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media (max-width: 650px) { | ||||||
|  |   .tagline { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .version { | ||||||
|  |     display: none; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-light .btn-link{ | ||||||
|  |   color: #222; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										245
									
								
								sist2-vue/src/components/PathTree.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								sist2-vue/src/components/PathTree.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,245 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <div class="input-group" style="margin-bottom: 0.5em; margin-top: 1em"> | ||||||
|  |       <div class="input-group-prepend"> | ||||||
|  | 
 | ||||||
|  |         <b-button variant="outline-secondary" @click="$refs['path-modal'].show()"> | ||||||
|  |           <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="20px"> | ||||||
|  |             <path | ||||||
|  |                 fill="currentColor" | ||||||
|  |                 d="M288 224h224a32 32 0 0 0 32-32V64a32 32 0 0 0-32-32H400L368 0h-80a32 32 0 0 0-32 32v64H64V8a8 8 0 0 0-8-8H40a8 8 0 0 0-8 8v392a16 16 0 0 0 16 16h208v64a32 32 0 0 0 32 32h224a32 32 0 0 0 32-32V352a32 32 0 0 0-32-32H400l-32-32h-80a32 32 0 0 0-32 32v64H64V128h192v64a32 32 0 0 0 32 32zm0 96h66.74l32 32H512v128H288zm0-288h66.74l32 32H512v128H288z" | ||||||
|  |             /> | ||||||
|  |           </svg> | ||||||
|  |         </b-button> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <VueSimpleSuggest | ||||||
|  |           class="form-control-fix-flex" | ||||||
|  |           @input="setPathText" | ||||||
|  |           :value="getPathText" | ||||||
|  |           :list="suggestPath" | ||||||
|  |           :max-suggestions="0" | ||||||
|  |           :placeholder="$t('pathBar.placeholder')" | ||||||
|  |       > | ||||||
|  |         <!-- Suggestion item template--> | ||||||
|  |         <div slot="suggestion-item" slot-scope="{ suggestion, query }"> | ||||||
|  |           <div class="suggestion-line" :title="suggestion"> | ||||||
|  |             <strong>{{ query }}</strong>{{ getSuggestionWithoutQueryPrefix(suggestion, query) }} | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </VueSimpleSuggest> | ||||||
|  | 
 | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <b-modal ref="path-modal" :title="$t('pathBar.modalTitle')" size="lg" :hide-footer="true" static> | ||||||
|  |       <div id="pathTree"></div> | ||||||
|  |     </b-modal> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import InspireTree from "inspire-tree"; | ||||||
|  | import InspireTreeDOM from "inspire-tree-dom"; | ||||||
|  | 
 | ||||||
|  | import "inspire-tree-dom/dist/inspire-tree-light.min.css"; | ||||||
|  | import Sist2Api from "@/Sist2Api"; | ||||||
|  | import {mapGetters, mapMutations} from "vuex"; | ||||||
|  | import VueSimpleSuggest from 'vue-simple-suggest' | ||||||
|  | import 'vue-simple-suggest/dist/styles.css' // Optional CSS | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "PathTree", | ||||||
|  |   components: { | ||||||
|  |     VueSimpleSuggest | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       mimeTree: null, | ||||||
|  |       pathItems: [], | ||||||
|  |       tmpPath: "" | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters(["getPathText"]) | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.$store.subscribe((mutation) => { | ||||||
|  |       // Wait until indices are loaded to get the root paths | ||||||
|  |       if (mutation.type === "setIndices") { | ||||||
|  |         let pathTree = new InspireTree({ | ||||||
|  |           data: (node, resolve, reject) => { | ||||||
|  |             return this.getNextDepth(node); | ||||||
|  |           }, | ||||||
|  |           sort: "text" | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         this.$store.state.indices.forEach(idx => { | ||||||
|  |           pathTree.addNode({ | ||||||
|  |             id: "/" + idx.id, | ||||||
|  |             values: ["/" + idx.id], | ||||||
|  |             text: `/[${idx.name}]`, | ||||||
|  |             index: idx.id, | ||||||
|  |             depth: 0, | ||||||
|  |             children: true | ||||||
|  |           }) | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         new InspireTreeDOM(pathTree, { | ||||||
|  |           target: "#pathTree" | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         pathTree.on("node.click", this.handleTreeClick); | ||||||
|  |         pathTree.expand(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapMutations(["setPathText"]), | ||||||
|  |     getSuggestionWithoutQueryPrefix(suggestion, query) { | ||||||
|  |       return suggestion.slice(query.length) | ||||||
|  |     }, | ||||||
|  |     async getPathChoices() { | ||||||
|  |       return new Promise(getPaths => { | ||||||
|  |         const q = { | ||||||
|  |           suggest: { | ||||||
|  |             path: { | ||||||
|  |               prefix: this.getPathText, | ||||||
|  |               completion: { | ||||||
|  |                 field: "suggest-path", | ||||||
|  |                 skip_duplicates: true, | ||||||
|  |                 size: 10000 | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         Sist2Api.esQuery(q) | ||||||
|  |             .then(resp => getPaths(resp["suggest"]["path"][0]["options"].map(opt => opt["_source"]["path"]))); | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |     async suggestPath(term) { | ||||||
|  |       if (!this.$store.state.optSuggestPath) { | ||||||
|  |         return [] | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       term = term.toLowerCase(); | ||||||
|  | 
 | ||||||
|  |       const choices = await this.getPathChoices(); | ||||||
|  | 
 | ||||||
|  |       let matches = []; | ||||||
|  |       for (let i = 0; i < choices.length; i++) { | ||||||
|  |         if (~choices[i].toLowerCase().indexOf(term)) { | ||||||
|  |           matches.push(choices[i]); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return matches.sort((a, b) => a.length - b.length); | ||||||
|  |     }, | ||||||
|  |     getNextDepth(node) { | ||||||
|  |       const q = { | ||||||
|  |         query: { | ||||||
|  |           bool: { | ||||||
|  |             filter: [ | ||||||
|  |               {term: {index: node.index}}, | ||||||
|  |               {range: {_depth: {gte: node.depth + 1, lte: node.depth + 3}}}, | ||||||
|  |             ] | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         aggs: { | ||||||
|  |           paths: { | ||||||
|  |             terms: { | ||||||
|  |               field: "path", | ||||||
|  |               size: 10000 | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }, | ||||||
|  |         size: 0 | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       if (node.depth > 0) { | ||||||
|  |         q.query.bool.must = { | ||||||
|  |           prefix: { | ||||||
|  |             path: node.id, | ||||||
|  |           } | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return Sist2Api.esQuery(q).then(resp => { | ||||||
|  |         const buckets = resp["aggregations"]["paths"]["buckets"]; | ||||||
|  |         if (!buckets) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const paths = []; | ||||||
|  | 
 | ||||||
|  |         return buckets | ||||||
|  |             .filter(bucket => bucket.key.length > node.id.length || node.id.startsWith("/")) | ||||||
|  |             .sort((a, b) => a.key > b.key) | ||||||
|  |             .map(bucket => { | ||||||
|  | 
 | ||||||
|  |               if (paths.some(n => bucket.key.startsWith(n))) { | ||||||
|  |                 return null; | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               const name = node.id.startsWith("/") ? bucket.key : bucket.key.slice(node.id.length + 1); | ||||||
|  | 
 | ||||||
|  |               paths.push(bucket.key); | ||||||
|  | 
 | ||||||
|  |               return { | ||||||
|  |                 id: bucket.key, | ||||||
|  |                 text: `${name}/ (${bucket.doc_count})`, | ||||||
|  |                 depth: node.depth + 1, | ||||||
|  |                 index: node.index, | ||||||
|  |                 values: [bucket.key], | ||||||
|  |                 children: true, | ||||||
|  |               } | ||||||
|  |             }).filter(x => x !== null) | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     handleTreeClick(e, node, handler) { | ||||||
|  |       if (node.depth !== 0) { | ||||||
|  |         this.setPathText(node.id); | ||||||
|  |         this.$refs['path-modal'].hide() | ||||||
|  | 
 | ||||||
|  |         this.$emit("search"); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       handler(); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | #mimeTree { | ||||||
|  |   max-height: 350px; | ||||||
|  |   overflow: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .form-control-fix-flex { | ||||||
|  |   flex: 1 1 auto; | ||||||
|  |   width: 1%; | ||||||
|  |   min-width: 0; | ||||||
|  |   margin-bottom: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .suggestion-line { | ||||||
|  |   max-width: 100%; | ||||||
|  |   white-space: nowrap; | ||||||
|  |   overflow: hidden; | ||||||
|  |   text-overflow: ellipsis; | ||||||
|  |   line-height: 1.1; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | </style> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | .suggestions { | ||||||
|  |   max-height: 250px; | ||||||
|  |   overflow-y: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .suggestions { | ||||||
|  |   color: black | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										3
									
								
								sist2-vue/src/components/Preloader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								sist2-vue/src/components/Preloader.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | <template> | ||||||
|  |   <b-progress value="1" max="1" animated></b-progress> | ||||||
|  | </template> | ||||||
							
								
								
									
										76
									
								
								sist2-vue/src/components/ResultsCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								sist2-vue/src/components/ResultsCard.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | |||||||
|  | <template> | ||||||
|  |   <b-card v-if="lastResultsLoaded" id="results"> | ||||||
|  |     <span>{{ hitCount }} {{ hitCount === 1 ? $t("hit") : $t("hits") }}</span> | ||||||
|  | 
 | ||||||
|  |     <div style="float: right"> | ||||||
|  |       <b-button v-b-toggle.collapse-1 variant="primary" class="not-mobile">{{ $t("details") }}</b-button> | ||||||
|  | 
 | ||||||
|  |       <SortSelect class="ml-2"></SortSelect> | ||||||
|  | 
 | ||||||
|  |       <DisplayModeToggle class="ml-2"></DisplayModeToggle> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <b-collapse id="collapse-1" class="pt-2" style="clear:both;"> | ||||||
|  |       <b-card> | ||||||
|  |         <b-table :items="tableItems" small borderless thead-class="hidden" class="mb-0"></b-table> | ||||||
|  |       </b-card> | ||||||
|  |     </b-collapse> | ||||||
|  |   </b-card> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import {EsResult} from "@/Sist2Api"; | ||||||
|  | import Vue from "vue"; | ||||||
|  | import {humanFileSize, humanTime} from "@/util"; | ||||||
|  | import DisplayModeToggle from "@/components/DisplayModeToggle.vue"; | ||||||
|  | import SortSelect from "@/components/SortSelect.vue"; | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  |   name: "ResultsCard", | ||||||
|  |   components: {SortSelect, DisplayModeToggle}, | ||||||
|  |   computed: { | ||||||
|  |     lastResultsLoaded() { | ||||||
|  |       return this.$store.state.lastQueryResults != null; | ||||||
|  |     }, | ||||||
|  |     hitCount() { | ||||||
|  |       return (this.$store.state.lastQueryResults as EsResult).aggregations.total_count.value; | ||||||
|  |     }, | ||||||
|  |     tableItems() { | ||||||
|  |       const items = []; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |       items.push({key: this.$t("queryTime"), value: this.took()}); | ||||||
|  |       items.push({key: this.$t("totalSize"), value: this.totalSize()}); | ||||||
|  | 
 | ||||||
|  |       return items; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     took() { | ||||||
|  |       return (this.$store.state.lastQueryResults as EsResult).took + "ms"; | ||||||
|  |     }, | ||||||
|  |     totalSize() { | ||||||
|  |       return humanFileSize((this.$store.state.lastQueryResults as EsResult).aggregations.total_size.value); | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | #results { | ||||||
|  |   margin-top: 1em; | ||||||
|  | 
 | ||||||
|  |   box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important; | ||||||
|  |   border-radius: 0; | ||||||
|  |   border: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #results .card-body { | ||||||
|  |   padding: 0.7em 1.25em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .hidden { | ||||||
|  |   display: none; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										46
									
								
								sist2-vue/src/components/SearchBar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								sist2-vue/src/components/SearchBar.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <b-input-group> | ||||||
|  |       <b-form-input :value="searchText" | ||||||
|  |                     :placeholder="advanced() ? $t('searchBar.advanced') : $t('searchBar.simple')" | ||||||
|  |                     @input="setSearchText($event)"></b-form-input> | ||||||
|  | 
 | ||||||
|  |       <template #prepend> | ||||||
|  |         <b-input-group-text> | ||||||
|  |           <b-form-checkbox :checked="fuzzy" title="Toggle fuzzy searching" @change="setFuzzy($event)"> | ||||||
|  |             {{ $t("searchBar.fuzzy") }} | ||||||
|  |           </b-form-checkbox> | ||||||
|  |         </b-input-group-text> | ||||||
|  |       </template> | ||||||
|  | 
 | ||||||
|  |       <template #append> | ||||||
|  |         <b-button variant="outline-secondary" @click="$emit('show-help')">{{$t("help.help")}}</b-button> | ||||||
|  |       </template> | ||||||
|  |     </b-input-group> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import {mapGetters, mapMutations} from "vuex"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters({ | ||||||
|  |       optQueryMode: "optQueryMode", | ||||||
|  |       searchText: "searchText", | ||||||
|  |       fuzzy: "fuzzy", | ||||||
|  |     }), | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapMutations({ | ||||||
|  |       setSearchText: "setSearchText", | ||||||
|  |       setFuzzy: "setFuzzy" | ||||||
|  |     }), | ||||||
|  |     advanced() { | ||||||
|  |       return this.optQueryMode === "advanced" | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | <style> | ||||||
|  | </style> | ||||||
							
								
								
									
										40
									
								
								sist2-vue/src/components/Sist2Icon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								sist2-vue/src/components/Sist2Icon.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | <template> | ||||||
|  |   <svg | ||||||
|  |       xmlns="http://www.w3.org/2000/svg" | ||||||
|  |       width="27.868069mm" | ||||||
|  |       height="7.6446671mm" | ||||||
|  |       viewBox="0 0 27.868069 7.6446671" | ||||||
|  |   > | ||||||
|  |     <g transform="translate(-4.5018313,-4.1849793)"> | ||||||
|  |       <g | ||||||
|  |           style="fill: currentColor;fill-opacity:1;stroke:none;stroke-width:0.26458332"> | ||||||
|  |         <path | ||||||
|  |             d="m 6.3153296,11.829646 q -0.7717014,0 -1.8134983,-0.337619 v -0.916395 q 1.0128581,0.511252 1.803852,0.511252 0.5643067,0 0.901926,-0.236334 0.3376194,-0.236333 0.3376194,-0.63183 0,-0.3424428 -0.2845649,-0.5498376 Q 6.980922,9.4566645 6.3635609,9.3264399 L 5.9921796,9.2492698 Q 5.2301245,9.0949295 4.8732126,8.7428407 4.5211238,8.3859288 4.5211238,7.7733908 q 0,-0.7765245 0.5305447,-1.1961372 0.5305447,-0.4196126 1.5096409,-0.4196126 0.829579,0 1.6061036,0.3183268 V 7.3441319 Q 7.4101809,6.9004036 6.5854251,6.9004036 q -1.1671984,0 -1.1671984,0.7958171 0,0.2604492 0.1012858,0.4147895 0.1012858,0.1495171 0.3858507,0.2556261 0.2845649,0.1012858 0.8392253,0.2122179 l 0.3569119,0.067524 q 1.3408312,0.2652724 1.3408312,1.4614098 0,0.80064 -0.5691298,1.263661 -0.5691298,0.458197 -1.5578722,0.458197 z" | ||||||
|  |             style="stroke-width:0.26458332" | ||||||
|  |         /> | ||||||
|  |         <path | ||||||
|  |             d="m 11.943927,5.3087694 q -0.144694,0 -0.144694,-0.144694 V 4.3296733 q 0,-0.144694 0.144694,-0.144694 h 0.694531 q 0.144694,0 0.144694,0.144694 v 0.8344021 q 0,0.144694 -0.144694,0.144694 z M 13.5645,11.728361 q -0.795817,0 -1.234722,-0.511253 -0.434082,-0.516075 -0.434082,-1.4469398 V 6.9823969 H 10.714028 V 6.2878656 h 2.069124 v 3.4823026 q 0,0.5884228 0.221864,0.8971028 0.221865,0.308681 0.6463,0.308681 h 1.036974 v 0.752409 z" | ||||||
|  |             style="stroke-width:0.26458332" | ||||||
|  |         /> | ||||||
|  |         <path | ||||||
|  |             d="m 18.209178,11.829646 q -0.771701,0 -1.813498,-0.337619 v -0.916395 q 1.012858,0.511252 1.803852,0.511252 0.564306,0 0.901926,-0.236334 0.337619,-0.236333 0.337619,-0.63183 0,-0.3424428 -0.284565,-0.5498376 Q 18.87477,9.4566645 18.257409,9.3264399 l -0.371381,-0.07717 Q 17.123973,9.0949295 16.767061,8.7428407 16.414972,8.3859288 16.414972,7.7733908 q 0,-0.7765245 0.530545,-1.1961372 0.530545,-0.4196126 1.509641,-0.4196126 0.829579,0 1.606103,0.3183268 v 0.8681641 q -0.757232,-0.4437283 -1.581988,-0.4437283 -1.167198,0 -1.167198,0.7958171 0,0.2604492 0.101286,0.4147895 0.101286,0.1495171 0.385851,0.2556261 0.284565,0.1012858 0.839225,0.2122179 l 0.356912,0.067524 q 1.340831,0.2652724 1.340831,1.4614098 0,0.80064 -0.56913,1.263661 -0.56913,0.458197 -1.557872,0.458197 z" | ||||||
|  |             style="stroke-width:0.26458332" | ||||||
|  |         /> | ||||||
|  |         <path | ||||||
|  |             d="m 25.207545,11.709068 q -0.993565,0 -1.408355,-0.40032 -0.409966,-0.405143 -0.409966,-1.3794164 V 6.9775737 H 21.947107 V 6.2878656 h 1.442117 V 4.8746874 l 0.887457,-0.3858507 v 1.7990289 h 2.016069 v 0.6897081 h -2.016069 v 2.9517579 q 0,0.5932454 0.226687,0.8344024 0.226687,0.236333 0.790994,0.236333 h 0.998388 v 0.709001 z" | ||||||
|  |             style="stroke-width:0.26458332" | ||||||
|  |         /> | ||||||
|  |         <path | ||||||
|  |             d="m 27.995317,11.043476 q 0,-0.178456 0.120578,-0.299035 0.274919,-0.289388 0.651123,-0.684885 0.376205,-0.4003199 0.805464,-0.8681638 0.327973,-0.356912 0.491959,-0.5353679 0.16881,-0.1832791 0.255626,-0.2845649 0.09164,-0.1012858 0.178456,-0.2073948 0.255626,-0.3086805 0.405144,-0.5257215 0.15434,-0.2170411 0.250803,-0.4292589 0.168809,-0.3762045 0.168809,-0.7524089 0,-0.5980686 -0.352089,-0.935688 -0.356911,-0.3424425 -0.979096,-0.3424425 -0.863341,0 -1.938899,0.6414768 V 4.8361023 q 0.491959,-0.2363335 0.979096,-0.3569119 0.47749,-0.1205783 0.945334,-0.1205783 0.501606,0 0.940511,0.1350477 0.438905,0.1350478 0.766878,0.4244358 0.289388,0.2556261 0.463021,0.6270074 0.173633,0.3665582 0.173633,0.829579 0,0.4726671 -0.212218,0.9501574 -0.106109,0.2411567 -0.274919,0.4726671 -0.163986,0.2266873 -0.424435,0.540191 Q 31.270225,8.501684 31.077299,8.718725 30.884374,8.9357661 30.628748,9.2106847 30.445469,9.4084332 30.286305,9.5675966 30.131965,9.72676 29.958332,9.9003928 29.7847,10.069203 29.558012,10.300713 29.336148,10.5274 29.012998,10.869843 h 3.356901 v 0.819932 h -4.374582 z" | ||||||
|  |             style="stroke-width:0.26458332" | ||||||
|  |         /> | ||||||
|  |       </g> | ||||||
|  |     </g> | ||||||
|  |   </svg> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: "Sist2Icon" | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										135
									
								
								sist2-vue/src/components/SizeSlider.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								sist2-vue/src/components/SizeSlider.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,135 @@ | |||||||
|  | <template> | ||||||
|  |   <div id="sizeSlider"></div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import noUiSlider from 'nouislider'; | ||||||
|  | import 'nouislider/dist/nouislider.css'; | ||||||
|  | import {humanFileSize} from "@/util"; | ||||||
|  | import {mergeTooltips} from "@/util-js"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "SizeSlider", | ||||||
|  |   mounted() { | ||||||
|  |     const elem = document.getElementById("sizeSlider"); | ||||||
|  | 
 | ||||||
|  |     const slider = noUiSlider.create(elem, { | ||||||
|  |       start: [ | ||||||
|  |         this.$store.state.sizeMin ? this.$store.state.sizeMin : 0, | ||||||
|  |         this.$store.state.sizeMax ? this.$store.state.sizeMax: 1000 * 1000 * 50000 | ||||||
|  |       ], | ||||||
|  | 
 | ||||||
|  |       tooltips: [true, true], | ||||||
|  |       behaviour: "drag-tap", | ||||||
|  |       connect: true, | ||||||
|  |       range: { | ||||||
|  |         'min': 0, | ||||||
|  |         "10%": 1000 * 1000, | ||||||
|  |         "20%": 1000 * 1000 * 10, | ||||||
|  |         "50%": 1000 * 1000 * 5000, | ||||||
|  |         "max": 1000 * 1000 * 50000, | ||||||
|  |       }, | ||||||
|  |       format: { | ||||||
|  |         to: x => x >= 1000 * 1000 * 50000 ? "50G+" : humanFileSize(Math.round(x)), | ||||||
|  |         from: x => x | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     mergeTooltips(elem, 10, " - ") | ||||||
|  | 
 | ||||||
|  |     elem.querySelectorAll('.noUi-connect')[0].classList.add("slider-color0") | ||||||
|  | 
 | ||||||
|  |     slider.on("set", (values, handle, unencoded) => { | ||||||
|  | 
 | ||||||
|  |       if (handle === 0) { | ||||||
|  |         this.$store.commit("setSizeMin", unencoded[0] === 0 ? undefined : Math.round(unencoded[0])) | ||||||
|  |       } else { | ||||||
|  |         this.$store.commit("setSizeMax", unencoded[1] >= 1000 * 1000 * 50000 ? undefined : Math.round(unencoded[1])) | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | #sizeSlider { | ||||||
|  |   margin-top: 34px; | ||||||
|  |   height: 12px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .slider-color0 { | ||||||
|  |   background: #2196f3; | ||||||
|  |   box-shadow: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .slider-color0 { | ||||||
|  |   background: #00bcd4; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .noUi-horizontal .noUi-handle { | ||||||
|  |   width: 2px; | ||||||
|  |   height: 18px; | ||||||
|  |   right: -1px; | ||||||
|  |   top: -3px; | ||||||
|  |   border: none; | ||||||
|  |   background-color: #1976d2; | ||||||
|  |   box-shadow: none; | ||||||
|  |   cursor: ew-resize; | ||||||
|  |   border-radius: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .noUi-horizontal .noUi-handle { | ||||||
|  |   background-color: #2168ac; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .noUi-handle:before { | ||||||
|  |   top: -6px; | ||||||
|  |   left: 3px; | ||||||
|  | 
 | ||||||
|  |   width: 10px; | ||||||
|  |   height: 28px; | ||||||
|  |   background-color: transparent; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .noUi-handle:after { | ||||||
|  |   top: -6px; | ||||||
|  |   left: -10px; | ||||||
|  | 
 | ||||||
|  |   width: 10px; | ||||||
|  |   height: 28px; | ||||||
|  |   background-color: transparent; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .noUi-draggable { | ||||||
|  |   cursor: move; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .noUi-tooltip { | ||||||
|  |   color: #fff; | ||||||
|  |   font-size: 13px; | ||||||
|  |   line-height: 1.333; | ||||||
|  |   text-shadow: none; | ||||||
|  |   padding: 0 2px; | ||||||
|  |   background: #2196F3; | ||||||
|  |   border-radius: 4px; | ||||||
|  |   border: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .noUi-tooltip { | ||||||
|  |   background: #00bcd4; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .theme-black .noUi-connects { | ||||||
|  |   background-color: #37474f; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .noUi-horizontal .noUi-origin > .noUi-tooltip { | ||||||
|  |   bottom: 7px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .noUi-target { | ||||||
|  |   background-color: #e1e4e9; | ||||||
|  |   border: none; | ||||||
|  |   box-shadow: none; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										21
									
								
								sist2-vue/src/components/SmallBadge.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								sist2-vue/src/components/SmallBadge.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | <template> | ||||||
|  |   <b-badge variant="secondary" :pill="pill">{{ text }}</b-badge> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from "vue"; | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  |   props: { | ||||||
|  |     text: String, | ||||||
|  |     pill: Boolean | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | .badge-pill { | ||||||
|  |   padding: 0.3em .4em 0.1em; | ||||||
|  |   border-radius: 6rem; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										59
									
								
								sist2-vue/src/components/SortSelect.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								sist2-vue/src/components/SortSelect.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | <template> | ||||||
|  |   <b-dropdown variant="primary"> | ||||||
|  |     <b-dropdown-item :class="{'dropdown-active': sort === 'score'}" @click="onSelect('score')">{{ | ||||||
|  |         $t("sort.relevance") | ||||||
|  |       }} | ||||||
|  |     </b-dropdown-item> | ||||||
|  |     <b-dropdown-item :class="{'dropdown-active': sort === 'dateAsc'}" @click="onSelect('dateAsc')">{{ | ||||||
|  |         $t("sort.dateAsc") | ||||||
|  |       }} | ||||||
|  |     </b-dropdown-item> | ||||||
|  |     <b-dropdown-item :class="{'dropdown-active': sort === 'dateDesc'}" @click="onSelect('dateDesc')"> | ||||||
|  |       {{ $t("sort.dateDesc") }} | ||||||
|  |     </b-dropdown-item> | ||||||
|  |     <b-dropdown-item :class="{'dropdown-active': sort === 'sizeAsc'}" @click="onSelect('sizeAsc')">{{ | ||||||
|  |         $t("sort.sizeAsc") | ||||||
|  |       }} | ||||||
|  |     </b-dropdown-item> | ||||||
|  |     <b-dropdown-item :class="{'dropdown-active': sort === 'sizeDesc'}" @click="onSelect('sizeDesc')"> | ||||||
|  |       {{ $t("sort.sizeDesc") }} | ||||||
|  |     </b-dropdown-item> | ||||||
|  | 
 | ||||||
|  |     <b-dropdown-item :class="{'dropdown-active': sort === 'random'}" @click="onSelect('random')"> | ||||||
|  |       {{ $t("sort.random") }} | ||||||
|  |     </b-dropdown-item> | ||||||
|  | 
 | ||||||
|  |     <template #button-content> | ||||||
|  |       <svg aria-hidden="true" width="20px" height="20px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"> | ||||||
|  |         <path | ||||||
|  |             fill="currentColor" | ||||||
|  |             d="M41 288h238c21.4 0 32.1 25.9 17 41L177 448c-9.4 9.4-24.6 9.4-33.9 0L24 329c-15.1-15.1-4.4-41 17-41zm255-105L177 64c-9.4-9.4-24.6-9.4-33.9 0L24 183c-15.1 15.1-4.4 41 17 41h238c21.4 0 32.1-25.9 17-41z"></path> | ||||||
|  |       </svg> | ||||||
|  |     </template> | ||||||
|  |   </b-dropdown> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   name: "SortSelect", | ||||||
|  |   computed: { | ||||||
|  |     sort() { | ||||||
|  |       return this.$store.state.sortMode; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     onSelect(sortMode) { | ||||||
|  |       if (sortMode === "random") { | ||||||
|  |         this.$store.commit("setSeed", Math.round(Math.random() * 100000)); | ||||||
|  |       } | ||||||
|  |       this.$store.commit("setSortMode", sortMode); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | .dropdown-active a { | ||||||
|  |   font-weight: bold; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										337
									
								
								sist2-vue/src/components/TagContainer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										337
									
								
								sist2-vue/src/components/TagContainer.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,337 @@ | |||||||
|  | <template> | ||||||
|  |   <div @mouseenter="showAddButton = true" @mouseleave="showAddButton = false"> | ||||||
|  | 
 | ||||||
|  |     <b-modal v-model="showModal" :title="$t('saveTagModalTitle')" hide-footer no-fade centered size="lg" static lazy> | ||||||
|  |       <b-row> | ||||||
|  |         <b-col style="flex-grow: 2" sm> | ||||||
|  |           <VueSimpleSuggest | ||||||
|  |               ref="suggest" | ||||||
|  |               :value="tagText" | ||||||
|  |               @select="setTagText($event)" | ||||||
|  |               @input="setTagText($event)" | ||||||
|  |               class="form-control-fix-flex" | ||||||
|  |               style="margin-top: 17px" | ||||||
|  |               :list="suggestTag" | ||||||
|  |               :max-suggestions="0" | ||||||
|  |               :placeholder="$t('saveTagPlaceholder')" | ||||||
|  |           > | ||||||
|  |             <!-- Suggestion item template--> | ||||||
|  |             <div slot="suggestion-item" slot-scope="{ suggestion, query}" | ||||||
|  |             > | ||||||
|  |               <div class="suggestion-line"> | ||||||
|  |                 <span | ||||||
|  |                     class="badge badge-suggestion" | ||||||
|  |                     :style="{background: getBg(suggestion), color: getFg(suggestion)}" | ||||||
|  |                 > | ||||||
|  |                 <strong>{{ query }}</strong>{{ getSuggestionWithoutQueryPrefix(suggestion, query) }} | ||||||
|  |                 </span> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  |           </VueSimpleSuggest> | ||||||
|  |         </b-col> | ||||||
|  |         <b-col class="mt-4"> | ||||||
|  |           <TwitterColorPicker v-model="color" triangle="hide" :width="252" class="mr-auto ml-auto"></TwitterColorPicker> | ||||||
|  |         </b-col> | ||||||
|  |       </b-row> | ||||||
|  | 
 | ||||||
|  |       <b-button variant="primary" style="float: right" class="mt-2" @click="saveTag()">{{ $t("confirm") }} | ||||||
|  |       </b-button> | ||||||
|  |     </b-modal> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     <template v-for="tag in hit._tags"> | ||||||
|  |       <div v-if="tag.userTag" :key="tag.rawText" style="display: inline-block"> | ||||||
|  |         <span | ||||||
|  |             :id="hit._id+tag.rawText" | ||||||
|  |             :title="tag.text" | ||||||
|  |             tabindex="-1" | ||||||
|  |             class="badge pointer" | ||||||
|  |             :style="badgeStyle(tag)" :class="badgeClass(tag)" | ||||||
|  |             @click.right="onTagRightClick(tag, $event)" | ||||||
|  |         >{{ tag.text.split(".").pop() }}</span> | ||||||
|  | 
 | ||||||
|  |         <b-popover :target="hit._id+tag.rawText" triggers="focus blur" placement="top"> | ||||||
|  |           <b-button variant="danger" @click="onTagDeleteClick(tag, $event)">Delete</b-button> | ||||||
|  |         </b-popover> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <span | ||||||
|  |           v-else :key="tag.text" | ||||||
|  |           class="badge" | ||||||
|  |           :style="badgeStyle(tag)" :class="badgeClass(tag)" | ||||||
|  |       >{{ tag.text.split(".").pop() }}</span> | ||||||
|  |     </template> | ||||||
|  | 
 | ||||||
|  |     <!-- Add button --> | ||||||
|  |     <small v-if="showAddButton" class="badge add-tag-button" @click="tagAdd()">Add</small> | ||||||
|  | 
 | ||||||
|  |     <!-- Size tag--> | ||||||
|  |     <small v-else class="text-muted badge-size">{{ | ||||||
|  |         humanFileSize(hit._source.size) | ||||||
|  |       }}</small> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import {humanFileSize, lum} from "@/util"; | ||||||
|  | import Vue from "vue"; | ||||||
|  | import {Twitter} from 'vue-color' | ||||||
|  | import Sist2Api from "@/Sist2Api"; | ||||||
|  | import VueSimpleSuggest from 'vue-simple-suggest' | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  |   components: { | ||||||
|  |     "TwitterColorPicker": Twitter, | ||||||
|  |     VueSimpleSuggest | ||||||
|  |   }, | ||||||
|  |   props: ["hit"], | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       showAddButton: false, | ||||||
|  |       showModal: false, | ||||||
|  |       tagText: null, | ||||||
|  |       color: { | ||||||
|  |         hex: "#e0e0e0", | ||||||
|  |       }, | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     tagHover() { | ||||||
|  |       return this.$store.getters["uiTagHover"]; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     humanFileSize: humanFileSize, | ||||||
|  |     getSuggestionWithoutQueryPrefix(suggestion, query) { | ||||||
|  |       return suggestion.id.slice(query.length, -8) | ||||||
|  |     }, | ||||||
|  |     getBg(suggestion) { | ||||||
|  |       return suggestion.id.slice(-7); | ||||||
|  |     }, | ||||||
|  |     getFg(suggestion) { | ||||||
|  |       return lum(suggestion.id.slice(-7)) > 50 ? "#000" : "#fff"; | ||||||
|  |     }, | ||||||
|  |     setTagText(value) { | ||||||
|  |       this.$refs.suggest.clearSuggestions(); | ||||||
|  | 
 | ||||||
|  |       if (typeof value === "string") { | ||||||
|  |         this.tagText = { | ||||||
|  |           id: value, | ||||||
|  |           title: value | ||||||
|  |         }; | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this.color = { | ||||||
|  |         hex: "#" + value.id.split("#")[1] | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this.tagText = value; | ||||||
|  |     }, | ||||||
|  |     badgeClass(tag) { | ||||||
|  |       return `badge-${tag.style}`; | ||||||
|  |     }, | ||||||
|  |     badgeStyle(tag) { | ||||||
|  |       return { | ||||||
|  |         background: tag.bg, | ||||||
|  |         color: tag.fg, | ||||||
|  |       }; | ||||||
|  |     }, | ||||||
|  |     onTagHover(tag) { | ||||||
|  |       if (tag.userTag) { | ||||||
|  |         this.$store.commit("setUiTagHover", tag); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     onTagLeave() { | ||||||
|  |       this.$store.commit("setUiTagHover", null); | ||||||
|  |     }, | ||||||
|  |     onTagDeleteClick(tag, e) { | ||||||
|  |       this.hit._tags = this.hit._tags.filter(t => t !== tag); | ||||||
|  | 
 | ||||||
|  |       Sist2Api.deleteTag(tag.rawText, this.hit).then(() => { | ||||||
|  |         //toast | ||||||
|  |         this.$store.commit("busUpdateWallItems"); | ||||||
|  |         this.$store.commit("busUpdateTags"); | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     tagAdd() { | ||||||
|  |       this.showModal = true; | ||||||
|  |     }, | ||||||
|  |     saveTag() { | ||||||
|  |       if (this.tagText.id.includes("#")) { | ||||||
|  |         this.$bvToast.toast( | ||||||
|  |             this.$t("toast.invalidTag"), | ||||||
|  |             { | ||||||
|  |               title: this.$t("toast.invalidTagTitle"), | ||||||
|  |               noAutoHide: true, | ||||||
|  |               toaster: "b-toaster-bottom-right", | ||||||
|  |               headerClass: "toast-header-error", | ||||||
|  |               bodyClass: "toast-body-error", | ||||||
|  |             }); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       let tag = this.tagText.id + this.color.hex.replace("#", ".#"); | ||||||
|  |       const userTags = this.hit._tags.filter(t => t.userTag); | ||||||
|  | 
 | ||||||
|  |       if (userTags.find(t => t.rawText === tag) != null) { | ||||||
|  |         this.$bvToast.toast( | ||||||
|  |             this.$t("toast.dupeTag"), | ||||||
|  |             { | ||||||
|  |               title: this.$t("toast.dupeTagTitle"), | ||||||
|  |               noAutoHide: true, | ||||||
|  |               toaster: "b-toaster-bottom-right", | ||||||
|  |               headerClass: "toast-header-error", | ||||||
|  |               bodyClass: "toast-body-error", | ||||||
|  |             }); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this.hit._tags.push(Sist2Api.createUserTag(tag)); | ||||||
|  | 
 | ||||||
|  |       Sist2Api.saveTag(tag, this.hit).then(() => { | ||||||
|  |         this.tagText = null; | ||||||
|  |         this.showModal = false; | ||||||
|  |         this.$store.commit("busUpdateWallItems"); | ||||||
|  |         this.$store.commit("busUpdateTags"); | ||||||
|  |         // TODO: toast | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     async suggestTag(term) { | ||||||
|  |       term = term.toLowerCase(); | ||||||
|  | 
 | ||||||
|  |       const choices = await this.getTagChoices(term); | ||||||
|  | 
 | ||||||
|  |       let matches = []; | ||||||
|  |       for (let i = 0; i < choices.length; i++) { | ||||||
|  |         if (~choices[i].toLowerCase().indexOf(term)) { | ||||||
|  |           matches.push(choices[i]); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return matches.sort().map(match => { | ||||||
|  |         return { | ||||||
|  |           title: match.split(".").slice(0,-1).join("."), | ||||||
|  |           id: match | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     getTagChoices(prefix) { | ||||||
|  |       return new Promise(getPaths => { | ||||||
|  |         Sist2Api.esQuery({ | ||||||
|  |           suggest: { | ||||||
|  |             tag: { | ||||||
|  |               prefix: prefix, | ||||||
|  |               completion: { | ||||||
|  |                 field: "suggest-tag", | ||||||
|  |                 skip_duplicates: true, | ||||||
|  |                 size: 10000 | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         }).then(resp => { | ||||||
|  |           const result = []; | ||||||
|  |           resp["suggest"]["tag"][0]["options"].map(opt => opt["_source"]["tag"]).forEach(tags => { | ||||||
|  |             tags.forEach(tag => { | ||||||
|  |               const t = tag.slice(0, -8); | ||||||
|  |               if (!result.find(x => x.slice(0, -8) === t)) { | ||||||
|  |                 result.push(tag); | ||||||
|  |               } | ||||||
|  |             }); | ||||||
|  |           }); | ||||||
|  |           getPaths(result); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | .badge-video { | ||||||
|  |   color: #FFFFFF; | ||||||
|  |   background-color: #F27761; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .badge-image { | ||||||
|  |   color: #FFFFFF; | ||||||
|  |   background-color: #AA99C9; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .badge-audio { | ||||||
|  |   color: #FFFFFF; | ||||||
|  |   background-color: #00ADEF; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .badge-user { | ||||||
|  |   color: #212529; | ||||||
|  |   background-color: #e0e0e0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .badge-user:hover, .add-tag-button:hover { | ||||||
|  |   box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .badge-text { | ||||||
|  |   color: #FFFFFF; | ||||||
|  |   background-color: #FAAB3C; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .badge { | ||||||
|  |   margin-right: 3px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .badge-delete { | ||||||
|  |   margin-right: -2px; | ||||||
|  |   margin-left: 2px; | ||||||
|  |   margin-top: -1px; | ||||||
|  |   font-family: monospace; | ||||||
|  |   font-size: 90%; | ||||||
|  |   background: rgba(0, 0, 0, 0.2); | ||||||
|  |   padding: 0.1em 0.4em; | ||||||
|  |   color: white; | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .badge-size { | ||||||
|  |   width: 50px; | ||||||
|  |   display: inline-block; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .add-tag-button { | ||||||
|  |   cursor: pointer; | ||||||
|  |   color: #212529; | ||||||
|  |   background-color: #e0e0e0; | ||||||
|  |   width: 50px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .badge { | ||||||
|  |   user-select: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .badge-suggestion { | ||||||
|  |   font-size: 90%; | ||||||
|  |   font-weight: normal; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | .vc-twitter-body { | ||||||
|  |   padding: 0 !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .vc-twitter { | ||||||
|  |   box-shadow: none !important; | ||||||
|  |   background: none !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .tooltip { | ||||||
|  |   user-select: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .toast { | ||||||
|  |   border: none; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										185
									
								
								sist2-vue/src/components/TagPicker.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								sist2-vue/src/components/TagPicker.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,185 @@ | |||||||
|  | <template> | ||||||
|  |   <div id="tagTree"></div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import InspireTree from "inspire-tree"; | ||||||
|  | import InspireTreeDOM from "inspire-tree-dom"; | ||||||
|  | 
 | ||||||
|  | import "inspire-tree-dom/dist/inspire-tree-light.min.css"; | ||||||
|  | import {getSelectedTreeNodes} from "@/util"; | ||||||
|  | import Sist2Api from "@/Sist2Api"; | ||||||
|  | 
 | ||||||
|  | function resetState(node) { | ||||||
|  |   node._tree.defaultState.forEach(function (val, prop) { | ||||||
|  |     node.state(prop, val); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return node; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function baseStateChange(prop, value, verb, node, deep) { | ||||||
|  |   if (node.state(prop) !== value) { | ||||||
|  |     node._tree.batch(); | ||||||
|  | 
 | ||||||
|  |     if (node._tree.config.nodes.resetStateOnRestore && verb === 'restored') { | ||||||
|  |       resetState(node); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     node.state(prop, value); | ||||||
|  | 
 | ||||||
|  |     node._tree.emit('node.' + verb, node, false); | ||||||
|  | 
 | ||||||
|  |     if (deep && node.hasChildren()) { | ||||||
|  |       node.children.recurseDown(function (child) { | ||||||
|  |         baseStateChange(prop, value, verb, child); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     node.markDirty(); | ||||||
|  |     node._tree.end(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return node; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function addTag(map, tag, id, count) { | ||||||
|  |   const tags = tag.split("."); | ||||||
|  | 
 | ||||||
|  |   const child = { | ||||||
|  |     id: id, | ||||||
|  |     count: count, | ||||||
|  |     text: tags.length !== 1 ? tags[0] : `${tags[0]} (${count})`, | ||||||
|  |     name: tags[0], | ||||||
|  |     children: [], | ||||||
|  |     // Overwrite base functions | ||||||
|  |     blur: function () { | ||||||
|  |       // noop | ||||||
|  |     }, | ||||||
|  |     select: function () { | ||||||
|  |       this.state("selected", true); | ||||||
|  |       return this.check() | ||||||
|  |     }, | ||||||
|  |     deselect: function () { | ||||||
|  |       this.state("selected", false); | ||||||
|  |       return this.uncheck() | ||||||
|  |     }, | ||||||
|  |     uncheck: function () { | ||||||
|  |       baseStateChange('checked', false, 'unchecked', this, false); | ||||||
|  |       this.state('indeterminate', false); | ||||||
|  | 
 | ||||||
|  |       if (this.hasParent()) { | ||||||
|  |         this.getParent().refreshIndeterminateState(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this._tree.end(); | ||||||
|  |       return this; | ||||||
|  |     }, | ||||||
|  |     check: function () { | ||||||
|  |       baseStateChange('checked', true, 'checked', this, false); | ||||||
|  | 
 | ||||||
|  |       if (this.hasParent()) { | ||||||
|  |         this.getParent().refreshIndeterminateState(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this._tree.end(); | ||||||
|  |       return this; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   let found = false; | ||||||
|  |   map.forEach(node => { | ||||||
|  |     if (node.name === child.name) { | ||||||
|  |       found = true; | ||||||
|  |       if (tags.length !== 1) { | ||||||
|  |         addTag(node.children, tags.slice(1).join("."), id, count); | ||||||
|  |       } else { | ||||||
|  |         // Same name, different color | ||||||
|  |         console.error("FIXME: Duplicate tag?") | ||||||
|  |         console.trace(node) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |   if (!found) { | ||||||
|  |     if (tags.length !== 1) { | ||||||
|  |       addTag(child.children, tags.slice(1).join("."), id, count); | ||||||
|  |       map.push(child); | ||||||
|  |     } else { | ||||||
|  |       map.push(child); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   name: "TagPicker", | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       tagTree: null, | ||||||
|  |       loadedFromArgs: false, | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.$store.subscribe((mutation) => { | ||||||
|  |       if (mutation.type === "setUiMimeMap") { | ||||||
|  |         this.initializeTree(); | ||||||
|  |         this.updateTree(); | ||||||
|  |       } else if (mutation.type === "busUpdateTags") { | ||||||
|  |         window.setTimeout(this.updateTree, 2000); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     initializeTree() { | ||||||
|  |       const tagMap = []; | ||||||
|  |       this.tagTree = new InspireTree({ | ||||||
|  |         selection: { | ||||||
|  |           mode: "checkbox", | ||||||
|  |           autoDeselect: false, | ||||||
|  |         }, | ||||||
|  |         checkbox: { | ||||||
|  |           autoCheckChildren: false, | ||||||
|  |         }, | ||||||
|  |         data: tagMap | ||||||
|  |       }); | ||||||
|  |       new InspireTreeDOM(this.tagTree, { | ||||||
|  |         target: '#tagTree' | ||||||
|  |       }); | ||||||
|  |       this.tagTree.on("node.state.changed", this.handleTreeClick); | ||||||
|  |     }, | ||||||
|  |     updateTree() { | ||||||
|  |       const tagMap = []; | ||||||
|  |       Sist2Api.getTags().then(tags => { | ||||||
|  |         tags.forEach(tag => addTag(tagMap, tag.id, tag.id, tag.count)); | ||||||
|  |         this.tagTree.removeAll(); | ||||||
|  |         this.tagTree.addNodes(tagMap); | ||||||
|  | 
 | ||||||
|  |         if (this.$store.state._onLoadSelectedTags.length > 0 && !this.loadedFromArgs) { | ||||||
|  |           this.$store.state._onLoadSelectedTags.forEach(mime => { | ||||||
|  |             this.tagTree.node(mime).select(); | ||||||
|  |             this.loadedFromArgs = true; | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     handleTreeClick(node, e) { | ||||||
|  |       if (e === "indeterminate" || e === "collapsed" || e === 'rendered') { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       this.$store.commit("setSelectedTags", getSelectedTreeNodes(this.tagTree)); | ||||||
|  |     }, | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style scoped> | ||||||
|  | #mimeTree { | ||||||
|  |   max-height: 350px; | ||||||
|  |   overflow: auto; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  | <style> | ||||||
|  | .inspire-tree .focused>.wholerow { | ||||||
|  |   border: none; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										296
									
								
								sist2-vue/src/i18n/messages.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								sist2-vue/src/i18n/messages.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,296 @@ | |||||||
|  | export default { | ||||||
|  |     en: { | ||||||
|  |         searchBar: { | ||||||
|  |             simple: "Search", | ||||||
|  |             advanced: "Advanced search", | ||||||
|  |             fuzzy: "Fuzzy" | ||||||
|  |         }, | ||||||
|  |         download: "Download", | ||||||
|  |         and: "and", | ||||||
|  |         page: "page", | ||||||
|  |         pages: "pages", | ||||||
|  |         mimeTypes: "Media types", | ||||||
|  |         tags: "Tags", | ||||||
|  |         help: { | ||||||
|  |             simpleSearch: "Simple search", | ||||||
|  |             advancedSearch: "Advanced search", | ||||||
|  |             help: "Help", | ||||||
|  |             term: "<TERM>", | ||||||
|  |             and: "AND operator", | ||||||
|  |             or: "OR operator", | ||||||
|  |             not: "negates a single term", | ||||||
|  |             quotes: "will match the enclosed sequence of terms in that specific order", | ||||||
|  |             prefix: "will match any term with a given prefix when used at the end of a word", | ||||||
|  |             parens: "used to group expressions", | ||||||
|  |             tildeTerm: "match a term with a given edit distance", | ||||||
|  |             tildePhrase: "match a phrase with a given number of allowed intervening unmatched words", | ||||||
|  |             example1: | ||||||
|  |                 "For example: <code>\"fried eggs\" +(eggplant | potato) -frittata</code> will match the " + | ||||||
|  |                 "phrase <i>fried eggs</i> and either <i>eggplant</i> or <i>potato</i>, but will ignore results " + | ||||||
|  |                 "containing <i>frittata</i>.", | ||||||
|  |             defaultOperator: | ||||||
|  |                 "When neither <code>+</code> or <code>|</code> is specified, the default operator is " + | ||||||
|  |                 "<code>+</code> (and).", | ||||||
|  |             fuzzy: | ||||||
|  |                 "When the <b>Fuzzy</b> option is checked, partial matches based on 3-grams are also returned.", | ||||||
|  |             moreInfoSimple: "For more information, see <a target=\"_blank\" " + | ||||||
|  |                 "rel=\"noreferrer\" href=\"//www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html\">Elasticsearch documentation</a>", | ||||||
|  |             moreInfoAdvanced: "For documentation about the advanced search mode, see <a target=\"_blank\" rel=\"noreferrer\" href=\"//www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax\">Elasticsearch documentation</a>" | ||||||
|  |         }, | ||||||
|  |         config: "Configuration", | ||||||
|  |         configDescription: "Configuration is saved in real time for this browser.", | ||||||
|  |         configReset: "Reset configuration", | ||||||
|  |         searchOptions: "Search options", | ||||||
|  |         treemapOptions: "Treemap options", | ||||||
|  |         displayOptions: "Display options", | ||||||
|  |         opt: { | ||||||
|  |             lang: "Language", | ||||||
|  |             highlight: "Enable highlighting", | ||||||
|  |             fuzzy: "Set fuzzy search by default", | ||||||
|  |             searchInPath: "Enable matching query against document path", | ||||||
|  |             suggestPath: "Enable auto-complete in path filter bar", | ||||||
|  |             fragmentSize: "Highlight context size in characters", | ||||||
|  |             queryMode: "Search mode", | ||||||
|  |             displayMode: "Display", | ||||||
|  |             columns: "Column count", | ||||||
|  |             treemapType: "Treemap type", | ||||||
|  |             treemapTiling: "Treemap tiling", | ||||||
|  |             treemapColorGroupingDepth: "Treemap color grouping depth (flat)", | ||||||
|  |             treemapColor: "Treemap color (cascaded)", | ||||||
|  |             treemapSize: "Treemap size", | ||||||
|  |             theme: "Theme", | ||||||
|  |             lightboxLoadOnlyCurrent: "Do not preload full-size images for adjacent slides in image viewer.", | ||||||
|  |             slideDuration: "Slide duration", | ||||||
|  |             resultSize: "Number of results per page", | ||||||
|  |             tagOrOperator: "Use OR operator when specifying multiple tags." | ||||||
|  |         }, | ||||||
|  |         queryMode: { | ||||||
|  |             simple: "Simple", | ||||||
|  |             advanced: "Advanced", | ||||||
|  |         }, | ||||||
|  |         lang: { | ||||||
|  |             en: "English", | ||||||
|  |             fr: "Français" | ||||||
|  |         }, | ||||||
|  |         displayMode: { | ||||||
|  |             grid: "Grid", | ||||||
|  |             list: "List", | ||||||
|  |         }, | ||||||
|  |         columns: { | ||||||
|  |             auto: "Auto" | ||||||
|  |         }, | ||||||
|  |         treemapType: { | ||||||
|  |             cascaded: "Cascaded", | ||||||
|  |             flat: "Flat (compact)" | ||||||
|  |         }, | ||||||
|  |         treemapSize: { | ||||||
|  |             small: "Small", | ||||||
|  |             medium: "Medium", | ||||||
|  |             large: "Large", | ||||||
|  |             xLarge: "xLarge", | ||||||
|  |             xxLarge: "xxLarge", | ||||||
|  |             custom: "Custom", | ||||||
|  |         }, | ||||||
|  |         treemapTiling: { | ||||||
|  |             binary: "Binary", | ||||||
|  |             squarify: "Squarify", | ||||||
|  |             slice: "Slice", | ||||||
|  |             dice: "Dice", | ||||||
|  |             sliceDice: "Slice & Dice", | ||||||
|  |         }, | ||||||
|  |         theme: { | ||||||
|  |             light: "Light", | ||||||
|  |             black: "Black" | ||||||
|  |         }, | ||||||
|  |         hit: "hit", | ||||||
|  |         hits: "hits", | ||||||
|  |         details: "Details", | ||||||
|  |         stats: "Stats", | ||||||
|  |         queryTime: "Query time", | ||||||
|  |         totalSize: "Total size", | ||||||
|  |         pathBar: { | ||||||
|  |             placeholder: "Filter path", | ||||||
|  |             modalTitle: "Select path" | ||||||
|  |         }, | ||||||
|  |         debug: "Debug information", | ||||||
|  |         debugDescription: "Information useful for debugging. If you encounter bugs or have suggestions for" + | ||||||
|  |             " new features, please submit a new issue <a href='https://github.com/simon987/sist2/issues/new/choose'>here</a>.", | ||||||
|  |         tagline: "Tagline", | ||||||
|  |         toast: { | ||||||
|  |             esConnErrTitle: "Elasticsearch connection error", | ||||||
|  |             esConnErr: "sist2 web module encountered an error while connecting to Elasticsearch." + | ||||||
|  |                 " See server logs for more information.", | ||||||
|  |             esQueryErrTitle: "Query error", | ||||||
|  |             esQueryErr: "Could not parse or execute query, please check the Advanced search documentation. " + | ||||||
|  |                 "See server logs for more information.", | ||||||
|  |             dupeTagTitle: "Duplicate tag", | ||||||
|  |             dupeTag: "This tag already exists for this document." | ||||||
|  |         }, | ||||||
|  |         saveTagModalTitle: "Add tag", | ||||||
|  |         saveTagPlaceholder: "Tag name", | ||||||
|  |         confirm: "Confirm", | ||||||
|  |         indexPickerPlaceholder: "Select indices", | ||||||
|  |         sort: { | ||||||
|  |             relevance: "Relevance", | ||||||
|  |             dateAsc: "Date (Older first)", | ||||||
|  |             dateDesc: "Date (Newer first)", | ||||||
|  |             sizeAsc: "Size (Smaller first)", | ||||||
|  |             sizeDesc: "Size (Larger first)", | ||||||
|  |             random: "Random", | ||||||
|  |         }, | ||||||
|  |         d3: { | ||||||
|  |             mimeCount: "File count distribution by media type", | ||||||
|  |             mimeSize: "Size distribution by media type", | ||||||
|  |             dateHistogram: "File modification time distribution", | ||||||
|  |             sizeHistogram: "File size distribution", | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     fr: { | ||||||
|  |         searchBar: { | ||||||
|  |             simple: "Recherche", | ||||||
|  |             advanced: "Recherche avancée", | ||||||
|  |             fuzzy: "Approximatif" | ||||||
|  |         }, | ||||||
|  |         download: "Télécharger", | ||||||
|  |         and: "et", | ||||||
|  |         page: "page", | ||||||
|  |         pages: "pages", | ||||||
|  |         mimeTypes: "Types de médias", | ||||||
|  |         tags: "Tags", | ||||||
|  |         help: { | ||||||
|  |             simpleSearch: "Recherche simple", | ||||||
|  |             advancedSearch: "Recherche avancée", | ||||||
|  |             help: "Aide", | ||||||
|  |             term: "<TERME>", | ||||||
|  |             and: "opérator ET", | ||||||
|  |             or: "opérator OU", | ||||||
|  |             not: "exclut un terme", | ||||||
|  |             quotes: "recherche la séquence de termes dans cet ordre spécifique.", | ||||||
|  |             prefix: "lorsqu'utilisé à la fin d'un mot, recherche tous les termes avec le préfixe donné.", | ||||||
|  |             parens: "utilisé pour regrouper des expressions", | ||||||
|  |             tildeTerm: "recherche un terme avec une distance d'édition donnée", | ||||||
|  |             tildePhrase: "recherche une phrase avec un nombre donné de mots intermédiaires tolérés", | ||||||
|  |             example1: | ||||||
|  |                 "Par exemple: <code>\"fried eggs\" +(eggplant | potato) -frittata</code> va rechercher la " + | ||||||
|  |                 "phrase <i>fried eggs</i> et soit <i>eggplant</i> ou <i>potato</i>, mais vas exlure les résultats " + | ||||||
|  |                 "qui contiennent <i>frittata</i>.", | ||||||
|  |             defaultOperator: | ||||||
|  |                 "Lorsqu'aucun des opérateurs <code>+</code> ou <code>|</code> sont spécifiés, l'opérateur par défaut " + | ||||||
|  |                 "est <code>+</code> (ET).", | ||||||
|  |             fuzzy: | ||||||
|  |                 "Lorsque l'option <b>Approximatif</b> est activée, les résultats partiels basés sur les trigrammes sont" + | ||||||
|  |                 " également inclus.", | ||||||
|  |             moreInfoSimple: "Pour plus d'information, voir <a target=\"_blank\" " + | ||||||
|  |                 "rel=\"noreferrer\" href=\"//www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html\">documentation Elasticsearch</a>", | ||||||
|  |             moreInfoAdvanced: "Pour plus d'information sur la recherche avancée, voir <a target=\"_blank\" rel=\"noreferrer\" href=\"//www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax\">documentation Elasticsearch</a>" | ||||||
|  |         }, | ||||||
|  |         config: "Configuration", | ||||||
|  |         configDescription: "La configuration est enregistrée en temps réel pour ce navigateur.", | ||||||
|  |         configReset: "Réinitialiser la configuration", | ||||||
|  |         searchOptions: "Options de recherche", | ||||||
|  |         treemapOptions: "Options du Treemap", | ||||||
|  |         displayOptions: "Options d'affichage", | ||||||
|  |         opt: { | ||||||
|  |             lang: "Langue", | ||||||
|  |             highlight: "Activer le surlignage", | ||||||
|  |             fuzzy: "Activer la recherche approximative par défaut", | ||||||
|  |             searchInPath: "Activer la recherche dans le chemin des documents", | ||||||
|  |             suggestPath: "Activer l'autocomplétion dans la barre de filtre de chemin", | ||||||
|  |             fragmentSize: "Longueur du contexte de surlignage, en nombre de caractères", | ||||||
|  |             queryMode: "Mode de recherche", | ||||||
|  |             displayMode: "Affichage", | ||||||
|  |             columns: "Nombre de colonnes", | ||||||
|  |             treemapType: "Type de Treemap", | ||||||
|  |             treemapTiling: "Treemap tiling", | ||||||
|  |             treemapColorGroupingDepth: "Groupage de couleur du Treemap (plat)", | ||||||
|  |             treemapColor: "Couleur du Treemap (en cascade)", | ||||||
|  |             treemapSize: "Taille du Treemap", | ||||||
|  |             theme: "Thème", | ||||||
|  |             lightboxLoadOnlyCurrent: "Désactiver le chargement des diapositives adjacentes pour le visualiseur d'images", | ||||||
|  |             slideDuration: "Durée des diapositives", | ||||||
|  |             resultSize: "Nombre de résultats par page", | ||||||
|  |             tagOrOperator: "Utiliser l'opérateur OU lors de la spécification de plusieurs tags" | ||||||
|  |         }, | ||||||
|  |         queryMode: { | ||||||
|  |             simple: "Simple", | ||||||
|  |             advanced: "Avancé", | ||||||
|  |         }, | ||||||
|  |         lang: { | ||||||
|  |             en: "English", | ||||||
|  |             fr: "Français" | ||||||
|  |         }, | ||||||
|  |         displayMode: { | ||||||
|  |             grid: "Grille", | ||||||
|  |             list: "Liste", | ||||||
|  |         }, | ||||||
|  |         columns: { | ||||||
|  |             auto: "Auto" | ||||||
|  |         }, | ||||||
|  |         treemapType: { | ||||||
|  |             cascaded: "En cascade", | ||||||
|  |             flat: "Plat (compact)" | ||||||
|  |         }, | ||||||
|  |         treemapSize: { | ||||||
|  |             small: "Petit", | ||||||
|  |             medium: "Moyen", | ||||||
|  |             large: "Grand", | ||||||
|  |             xLarge: "xGrand", | ||||||
|  |             xxLarge: "xxGrand", | ||||||
|  |             custom: "Personnalisé", | ||||||
|  |         }, | ||||||
|  |         treemapTiling: { | ||||||
|  |             binary: "Binary", | ||||||
|  |             squarify: "Squarify", | ||||||
|  |             slice: "Slice", | ||||||
|  |             dice: "Dice", | ||||||
|  |             sliceDice: "Slice & Dice", | ||||||
|  |         }, | ||||||
|  |         theme: { | ||||||
|  |             light: "Clair", | ||||||
|  |             black: "Noir" | ||||||
|  |         }, | ||||||
|  |         hit: "résultat", | ||||||
|  |         hits: "résultats", | ||||||
|  |         details: "Détails", | ||||||
|  |         stats: "Stats", | ||||||
|  |         queryTime: "Durée de la requête", | ||||||
|  |         totalSize: "Taille totale", | ||||||
|  |         pathBar: { | ||||||
|  |             placeholder: "Filtrer le chemin", | ||||||
|  |             modalTitle: "Sélectionner le chemin" | ||||||
|  |         }, | ||||||
|  |         debug: "Information de débogage", | ||||||
|  |         debugDescription: "Informations utiles pour le débogage\n" + | ||||||
|  |             "Si vous rencontrez des bogues ou si vous avez des suggestions pour de nouvelles fonctionnalités," + | ||||||
|  |             " veuillez soumettre un nouvel Issue <a href='https://github.com/simon987/sist2/issues/new/choose'>ici</a>.", | ||||||
|  |         tagline: "Tagline", | ||||||
|  |         toast: { | ||||||
|  |             esConnErrTitle: "Erreur de connexion Elasticsearch", | ||||||
|  |             esConnErr: "Le module web a rencontré une erreur lors de la connexion à Elasticsearch." + | ||||||
|  |                 " Consultez les journaux du serveur pour plus d'informations..", | ||||||
|  |             esQueryErrTitle: "Erreur de requête", | ||||||
|  |             esQueryErr: "Impossible d'analyser ou d'exécuter la requête, veuillez consulter la documentation sur la " + | ||||||
|  |                 "recherche avancée. Voir les journaux du serveur pour plus d'informations.", | ||||||
|  |             dupeTagTitle: "Tag en double", | ||||||
|  |             dupeTag: "Ce tag existe déjà pour ce document." | ||||||
|  |         }, | ||||||
|  |         saveTagModalTitle: "Ajouter un tag", | ||||||
|  |         saveTagPlaceholder: "Nom du tag", | ||||||
|  |         confirm: "Confirmer", | ||||||
|  |         indexPickerPlaceholder: "Sélectionner un index", | ||||||
|  |         sort: { | ||||||
|  |             relevance: "Pertinence", | ||||||
|  |             dateAsc: "Date (Plus ancient)", | ||||||
|  |             dateDesc: "Date (Plus récent)", | ||||||
|  |             sizeAsc: "Taille (Plus petit)", | ||||||
|  |             sizeDesc: "Taille (Plus grand)", | ||||||
|  |             random: "Aléatoire", | ||||||
|  |         }, | ||||||
|  |         d3: { | ||||||
|  |             mimeCount: "Distribution du nombre de fichiers par type de média", | ||||||
|  |             mimeSize: "Distribution des tailles de fichiers par type de média", | ||||||
|  |             dateHistogram: "Distribution des dates de modification", | ||||||
|  |             sizeHistogram: "Distribution des tailles de fichier", | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								sist2-vue/src/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								sist2-vue/src/main.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | import '@babel/polyfill' | ||||||
|  | import 'mutationobserver-shim' | ||||||
|  | import Vue from 'vue' | ||||||
|  | import './plugins/bootstrap-vue' | ||||||
|  | import App from './App.vue' | ||||||
|  | import router from './router' | ||||||
|  | import store from './store' | ||||||
|  | import VueI18n from "vue-i18n"; | ||||||
|  | import messages from "@/i18n/messages"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | import VueRouter from "vue-router"; | ||||||
|  | 
 | ||||||
|  | Vue.config.productionTip = false; | ||||||
|  | 
 | ||||||
|  | Vue.use(VueI18n); | ||||||
|  | Vue.use(VueRouter); | ||||||
|  | 
 | ||||||
|  | const i18n = new VueI18n({ | ||||||
|  |     locale: "en", | ||||||
|  |     messages: messages | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | new Vue({ | ||||||
|  |     router, | ||||||
|  |     store, | ||||||
|  |     i18n, | ||||||
|  |     render: h => h(App) | ||||||
|  | }).$mount("#app"); | ||||||
							
								
								
									
										7
									
								
								sist2-vue/src/plugins/bootstrap-vue.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								sist2-vue/src/plugins/bootstrap-vue.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | import Vue from "vue" | ||||||
|  | 
 | ||||||
|  | import BootstrapVue from "bootstrap-vue" | ||||||
|  | import "bootstrap/dist/css/bootstrap.min.css" | ||||||
|  | import "bootstrap-vue/dist/bootstrap-vue.css" | ||||||
|  | 
 | ||||||
|  | Vue.use(BootstrapVue) | ||||||
							
								
								
									
										36
									
								
								sist2-vue/src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								sist2-vue/src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | import Vue from "vue" | ||||||
|  | import VueRouter, {RouteConfig} from "vue-router" | ||||||
|  | import StatsPage from "../views/StatsPage.vue" | ||||||
|  | import Configuration from "../views/Configuration.vue" | ||||||
|  | import SearchPage from "@/views/SearchPage.vue"; | ||||||
|  | 
 | ||||||
|  | Vue.use(VueRouter) | ||||||
|  | 
 | ||||||
|  | const routes: Array<RouteConfig> = [ | ||||||
|  |     { | ||||||
|  |         path: "/", | ||||||
|  |         name: "SearchPage", | ||||||
|  |         component: SearchPage | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         path: "/stats", | ||||||
|  |         name: "Stats", | ||||||
|  |         component: StatsPage | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         path: "/config", | ||||||
|  |         name: "Configuration", | ||||||
|  |         component: Configuration | ||||||
|  |     } | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | const router = new VueRouter({ | ||||||
|  |     mode: "hash", | ||||||
|  |     base: process.env.BASE_URL, | ||||||
|  |     routes, | ||||||
|  |     scrollBehavior (to, from, savedPosition) { | ||||||
|  |         // return desired position
 | ||||||
|  |     } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | export default router | ||||||
							
								
								
									
										17
									
								
								sist2-vue/src/shims-tsx.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								sist2-vue/src/shims-tsx.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | import Vue, {VNode} from 'vue' | ||||||
|  | 
 | ||||||
|  | declare global { | ||||||
|  |     namespace JSX { | ||||||
|  |         // tslint:disable no-empty-interface
 | ||||||
|  |         interface Element extends VNode { | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // tslint:disable no-empty-interface
 | ||||||
|  |         interface ElementClass extends Vue { | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         interface IntrinsicElements { | ||||||
|  |             [elem: string]: any | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								sist2-vue/src/shims-vue.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								sist2-vue/src/shims-vue.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | declare module '*.vue' { | ||||||
|  |   import Vue from 'vue' | ||||||
|  |   export default Vue | ||||||
|  | } | ||||||
							
								
								
									
										340
									
								
								sist2-vue/src/store/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										340
									
								
								sist2-vue/src/store/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,340 @@ | |||||||
|  | import Vue from "vue" | ||||||
|  | import Vuex from "vuex" | ||||||
|  | import VueRouter, {Route} from "vue-router"; | ||||||
|  | import {EsHit, EsResult, EsTag, Index, Tag} from "@/Sist2Api"; | ||||||
|  | import {deserializeMimes, serializeMimes} from "@/util"; | ||||||
|  | 
 | ||||||
|  | Vue.use(Vuex) | ||||||
|  | 
 | ||||||
|  | export default new Vuex.Store({ | ||||||
|  |     state: { | ||||||
|  |         seed: 0, | ||||||
|  |         indices: [] as Index[], | ||||||
|  |         tags: [] as EsTag[], | ||||||
|  |         sist2Info: null as any, | ||||||
|  | 
 | ||||||
|  |         sizeMin: undefined, | ||||||
|  |         sizeMax: undefined, | ||||||
|  |         dateBoundsMin: null, | ||||||
|  |         dateBoundsMax: null, | ||||||
|  |         dateMin: undefined, | ||||||
|  |         dateMax: undefined, | ||||||
|  |         searchText: "", | ||||||
|  |         pathText: "", | ||||||
|  |         sortMode: "score", | ||||||
|  | 
 | ||||||
|  |         fuzzy: false, | ||||||
|  |         size: 60, | ||||||
|  | 
 | ||||||
|  |         optLang: "en", | ||||||
|  |         optTheme: "light", | ||||||
|  |         optDisplay: "grid", | ||||||
|  | 
 | ||||||
|  |         optHighlight: true, | ||||||
|  |         optTagOrOperator: false, | ||||||
|  |         optFuzzy: true, | ||||||
|  |         optFragmentSize: 200, | ||||||
|  |         optQueryMode: "simple", | ||||||
|  |         optSearchInPath: false, | ||||||
|  |         optColumns: "auto", | ||||||
|  |         optSuggestPath: true, | ||||||
|  |         optTreemapType: "cascaded", | ||||||
|  |         optTreemapTiling: "squarify", | ||||||
|  |         optTreemapColorGroupingDepth: 3, | ||||||
|  |         optTreemapSize: "medium", | ||||||
|  |         optTreemapColor: "PuBuGn", | ||||||
|  |         optLightboxLoadOnlyCurrent: false, | ||||||
|  |         optLightboxSlideDuration: 15, | ||||||
|  | 
 | ||||||
|  |         _onLoadSelectedIndices: [] as string[], | ||||||
|  |         _onLoadSelectedMimeTypes: [] as string[], | ||||||
|  |         _onLoadSelectedTags: [] as string[], | ||||||
|  |         selectedIndices: [] as Index[], | ||||||
|  |         selectedMimeTypes: [] as string[], | ||||||
|  |         selectedTags: [] as string[], | ||||||
|  | 
 | ||||||
|  |         lastQueryResults: null, | ||||||
|  | 
 | ||||||
|  |         keySequence: 0, | ||||||
|  |         querySequence: 0, | ||||||
|  | 
 | ||||||
|  |         uiTagHover: null as Tag | null, | ||||||
|  |         uiLightboxIsOpen: false, | ||||||
|  |         uiShowLightbox: false, | ||||||
|  |         uiLightboxSources: [] as string[], | ||||||
|  |         uiLightboxThumbs: [] as string[], | ||||||
|  |         uiLightboxCaptions: [] as any[], | ||||||
|  |         uiLightboxTypes: [] as string[], | ||||||
|  |         uiLightboxKey: 0, | ||||||
|  |         uiLightboxSlide: 0, | ||||||
|  |         uiReachedScrollEnd: false, | ||||||
|  | 
 | ||||||
|  |         uiMimeMap: [] as any[] | ||||||
|  |     }, | ||||||
|  |     mutations: { | ||||||
|  |         setUiReachedScrollEnd: (state, val) => state.uiReachedScrollEnd = val, | ||||||
|  |         setTags: (state, val) => state.tags = val, | ||||||
|  |         setPathText: (state, val) => state.pathText = val, | ||||||
|  |         setSizeMin: (state, val) => state.sizeMin = val, | ||||||
|  |         setSizeMax: (state, val) => state.sizeMax = val, | ||||||
|  |         setSist2Info: (state, val) => state.sist2Info = val, | ||||||
|  |         setSeed: (state, val) => state.seed = val, | ||||||
|  |         setOptLang: (state, val) => state.optLang = val, | ||||||
|  |         setSortMode: (state, val) => state.sortMode = val, | ||||||
|  |         setIndices: (state, val) => { | ||||||
|  |             state.indices = val; | ||||||
|  | 
 | ||||||
|  |             if (state._onLoadSelectedIndices.length > 0) { | ||||||
|  | 
 | ||||||
|  |                 state.selectedIndices = val.filter( | ||||||
|  |                     (idx: Index) => state._onLoadSelectedIndices.some(prefix => idx.id.startsWith(prefix)) | ||||||
|  |                 ); | ||||||
|  |             } else { | ||||||
|  |                 state.selectedIndices = val; | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         setDateMin: (state, val) => state.dateMin = val, | ||||||
|  |         setDateMax: (state, val) => state.dateMax = val, | ||||||
|  |         setDateBoundsMin: (state, val) => state.dateBoundsMin = val, | ||||||
|  |         setDateBoundsMax: (state, val) => state.dateBoundsMax = val, | ||||||
|  |         setSearchText: (state, val) => state.searchText = val, | ||||||
|  |         setFuzzy: (state, val) => state.fuzzy = val, | ||||||
|  |         setLastQueryResult: (state, val) => state.lastQueryResults = val, | ||||||
|  |         _setOnLoadSelectedIndices: (state, val) => state._onLoadSelectedIndices = val, | ||||||
|  |         _setOnLoadSelectedMimeTypes: (state, val) => state._onLoadSelectedMimeTypes = val, | ||||||
|  |         _setOnLoadSelectedTags: (state, val) => state._onLoadSelectedTags = val, | ||||||
|  |         setSelectedIndices: (state, val) => state.selectedIndices = val, | ||||||
|  |         setSelectedMimeTypes: (state, val) => state.selectedMimeTypes = val, | ||||||
|  |         setSelectedTags: (state, val) => state.selectedTags = val, | ||||||
|  |         setUiTagHover: (state, val: Tag | null) => state.uiTagHover = val, | ||||||
|  |         setUiLightboxIsOpen: (state, val: boolean) => state.uiLightboxIsOpen = val, | ||||||
|  |         _setUiShowLightbox: (state, val: boolean) => state.uiShowLightbox = val, | ||||||
|  |         setUiLightboxKey: (state, val: number) => state.uiLightboxKey = val, | ||||||
|  |         _setKeySequence: (state, val: number) => state.keySequence = val, | ||||||
|  |         _setQuerySequence: (state, val: number) => state.querySequence = val, | ||||||
|  |         addLightboxSource: (state, {source, thumbnail, caption, type}) => { | ||||||
|  |             state.uiLightboxSources.push(source); | ||||||
|  |             state.uiLightboxThumbs.push(thumbnail); | ||||||
|  |             state.uiLightboxCaptions.push(caption); | ||||||
|  |             state.uiLightboxTypes.push(type); | ||||||
|  |         }, | ||||||
|  |         setUiLightboxSlide: (state, val: number) => state.uiLightboxSlide = val, | ||||||
|  | 
 | ||||||
|  |         setUiLightboxSources: (state, val) => state.uiLightboxSources = val, | ||||||
|  |         setUiLightboxThumbs: (state, val) => state.uiLightboxThumbs = val, | ||||||
|  |         setUiLightboxTypes: (state, val) => state.uiLightboxTypes = val, | ||||||
|  |         setUiLightboxCaptions: (state, val) => state.uiLightboxCaptions = val, | ||||||
|  | 
 | ||||||
|  |         setOptTheme: (state, val) => state.optTheme = val, | ||||||
|  |         setOptDisplay: (state, val) => state.optDisplay = val, | ||||||
|  |         setOptColumns: (state, val) => state.optColumns = val, | ||||||
|  |         setOptHighlight: (state, val) => state.optHighlight = val, | ||||||
|  |         setOptFuzzy: (state, val) => state.fuzzy = val, | ||||||
|  |         setOptSearchInPath: (state, val) => state.optSearchInPath = val, | ||||||
|  |         setOptSuggestPath: (state, val) => state.optSuggestPath = val, | ||||||
|  |         setOptFragmentSize: (state, val) => state.optFragmentSize = val, | ||||||
|  |         setOptQueryMode: (state, val) => state.optQueryMode = val, | ||||||
|  |         setOptResultSize: (state, val) => state.size = val, | ||||||
|  |         setOptTagOrOperator: (state, val) => state.optTagOrOperator = val, | ||||||
|  | 
 | ||||||
|  |         setOptTreemapType: (state, val) => state.optTreemapType = val, | ||||||
|  |         setOptTreemapTiling: (state, val) => state.optTreemapTiling = val, | ||||||
|  |         setOptTreemapColorGroupingDepth: (state, val) => state.optTreemapColorGroupingDepth = val, | ||||||
|  |         setOptTreemapSize: (state, val) => state.optTreemapSize = val, | ||||||
|  |         setOptTreemapColor: (state, val) => state.optTreemapColor = val, | ||||||
|  | 
 | ||||||
|  |         setOptLightboxLoadOnlyCurrent: (state, val) => state.optLightboxLoadOnlyCurrent = val, | ||||||
|  | 
 | ||||||
|  |         setUiMimeMap: (state, val) => state.uiMimeMap = val, | ||||||
|  | 
 | ||||||
|  |         busUpdateWallItems: () => { | ||||||
|  |             // noop
 | ||||||
|  |         }, | ||||||
|  |         busUpdateTags: () => { | ||||||
|  |             // noop
 | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     actions: { | ||||||
|  |         loadFromArgs({commit}, route: Route) { | ||||||
|  | 
 | ||||||
|  |             if (route.query.q) { | ||||||
|  |                 commit("setSearchText", route.query.q); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (route.query.fuzzy !== undefined) { | ||||||
|  |                 commit("setFuzzy", true); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (route.query.i) { | ||||||
|  |                 commit("_setOnLoadSelectedIndices", Array.isArray(route.query.i) ? route.query.i : [route.query.i,]); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (route.query.dMin) { | ||||||
|  |                 commit("setDateMin", Number(route.query.dMin)) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (route.query.dMax) { | ||||||
|  |                 commit("setDateMax", Number(route.query.dMax)) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (route.query.sMin) { | ||||||
|  |                 commit("setSizeMin", Number(route.query.sMin)) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (route.query.sMax) { | ||||||
|  |                 commit("setSizeMax", Number(route.query.sMax)) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (route.query.path) { | ||||||
|  |                 commit("setPathText", route.query.path) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (route.query.m) { | ||||||
|  |                 commit("_setOnLoadSelectedMimeTypes", deserializeMimes(route.query.m as string)); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (route.query.t) { | ||||||
|  |                 commit("_setOnLoadSelectedTags", (route.query.t as string).split(",")); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (route.query.sort) { | ||||||
|  |                 commit("setSortMode", route.query.sort); | ||||||
|  |                 commit("setSeed", Number(route.query.seed)); | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         async updateArgs({state}, router: VueRouter) { | ||||||
|  |             await router.push({ | ||||||
|  |                 query: { | ||||||
|  |                     q: state.searchText.trim() ? state.searchText.trim().replace(/\s+/g, " ") : undefined, | ||||||
|  |                     fuzzy: state.fuzzy ? null : undefined, | ||||||
|  |                     i: state.selectedIndices ? state.selectedIndices.map((idx: Index) => idx.idPrefix) : undefined, | ||||||
|  |                     dMin: state.dateMin, | ||||||
|  |                     dMax: state.dateMax, | ||||||
|  |                     sMin: state.sizeMin, | ||||||
|  |                     sMax: state.sizeMax, | ||||||
|  |                     path: state.pathText ? state.pathText : undefined, | ||||||
|  |                     m: serializeMimes(state.selectedMimeTypes), | ||||||
|  |                     t: state.selectedTags.length == 0 ? undefined : state.selectedTags.join(","), | ||||||
|  |                     sort: state.sortMode === "score" ? undefined : state.sortMode, | ||||||
|  |                     seed: state.sortMode === "random" ? state.seed.toString() : undefined | ||||||
|  |                 } | ||||||
|  |             }).catch(() => { | ||||||
|  |                 // ignore
 | ||||||
|  |             }); | ||||||
|  |         }, | ||||||
|  |         updateConfiguration({state}) { | ||||||
|  |             const conf = {} as any; | ||||||
|  | 
 | ||||||
|  |             Object.keys(state).forEach((key) => { | ||||||
|  |                 if (key.startsWith("opt")) { | ||||||
|  |                     conf[key] = (state as any)[key]; | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             localStorage.setItem("sist2_configuration", JSON.stringify(conf)); | ||||||
|  |         }, | ||||||
|  |         loadConfiguration({state}) { | ||||||
|  |             const confString = localStorage.getItem("sist2_configuration"); | ||||||
|  |             if (confString) { | ||||||
|  |                 const conf = JSON.parse(confString); | ||||||
|  | 
 | ||||||
|  |                 Object.keys(state).forEach((key) => { | ||||||
|  |                     if (key.startsWith("opt")) { | ||||||
|  |                         (state as any)[key] = conf[key]; | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         setSelectedIndices: ({commit}, val) => commit("setSelectedIndices", val), | ||||||
|  |         getKeySequence({commit, state}) { | ||||||
|  |             const val = state.keySequence; | ||||||
|  |             commit("_setKeySequence", val + 1); | ||||||
|  | 
 | ||||||
|  |             return val | ||||||
|  |         }, | ||||||
|  |         incrementQuerySequence({commit, state}) { | ||||||
|  |             const val = state.querySequence; | ||||||
|  |             commit("_setQuerySequence", val + 1); | ||||||
|  | 
 | ||||||
|  |             return val | ||||||
|  |         }, | ||||||
|  |         remountLightbox({commit, state}) { | ||||||
|  |             // Change key to an arbitrary number to force the lightbox to remount
 | ||||||
|  |             commit("setUiLightboxKey", state.uiLightboxKey + 1); | ||||||
|  |         }, | ||||||
|  |         showLightbox({commit, state}) { | ||||||
|  |             commit("_setUiShowLightbox", !state.uiShowLightbox); | ||||||
|  |         }, | ||||||
|  |         clearResults({commit}) { | ||||||
|  |             commit("setLastQueryResult", null); | ||||||
|  |             commit("_setKeySequence", 0); | ||||||
|  |             commit("_setUiShowLightbox", false); | ||||||
|  |             commit("setUiLightboxSources", []); | ||||||
|  |             commit("setUiLightboxThumbs", []); | ||||||
|  |             commit("setUiLightboxTypes", []); | ||||||
|  |             commit("setUiLightboxCaptions", []); | ||||||
|  |             commit("setUiLightboxKey", 0); | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     modules: {}, | ||||||
|  |     getters: { | ||||||
|  |         seed: (state) => state.seed, | ||||||
|  |         getPathText: (state) => state.pathText, | ||||||
|  |         indices: state => state.indices, | ||||||
|  |         sist2Info: state => state.sist2Info, | ||||||
|  |         indexMap: state => { | ||||||
|  |             const map = {} as any; | ||||||
|  |             state.indices.forEach(idx => map[idx.id] = idx); | ||||||
|  |             return map; | ||||||
|  |         }, | ||||||
|  |         selectedIndices: (state) => state.selectedIndices, | ||||||
|  |         _onLoadSelectedIndices: (state) => state._onLoadSelectedIndices, | ||||||
|  |         selectedMimeTypes: (state) => state.selectedMimeTypes, | ||||||
|  |         selectedTags: (state) => state.selectedTags, | ||||||
|  |         dateMin: state => state.dateMin, | ||||||
|  |         dateMax: state => state.dateMax, | ||||||
|  |         sizeMin: state => state.sizeMin, | ||||||
|  |         sizeMax: state => state.sizeMax, | ||||||
|  |         searchText: state => state.searchText, | ||||||
|  |         pathText: state => state.pathText, | ||||||
|  |         fuzzy: state => state.fuzzy, | ||||||
|  |         size: state => state.size, | ||||||
|  |         sortMode: state => state.sortMode, | ||||||
|  |         lastQueryResult: state => state.lastQueryResults, | ||||||
|  |         lastDoc: function (state): EsHit | null { | ||||||
|  |             if (state.lastQueryResults == null) { | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return (state.lastQueryResults as unknown as EsResult).hits.hits.slice(-1)[0]; | ||||||
|  |         }, | ||||||
|  |         uiTagHover: state => state.uiTagHover, | ||||||
|  |         uiShowLightbox: state => state.uiShowLightbox, | ||||||
|  |         uiLightboxSources: state => state.uiLightboxSources, | ||||||
|  |         uiLightboxThumbs: state => state.uiLightboxThumbs, | ||||||
|  |         uiLightboxCaptions: state => state.uiLightboxCaptions, | ||||||
|  |         uiLightboxTypes: state => state.uiLightboxTypes, | ||||||
|  |         uiLightboxKey: state => state.uiLightboxKey, | ||||||
|  |         uiLightboxSlide: state => state.uiLightboxSlide, | ||||||
|  | 
 | ||||||
|  |         optLang: state => state.optLang, | ||||||
|  |         optTheme: state => state.optTheme, | ||||||
|  |         optDisplay: state => state.optDisplay, | ||||||
|  |         optColumns: state => state.optColumns, | ||||||
|  |         optHighlight: state => state.optHighlight, | ||||||
|  |         optTagOrOperator: state => state.optTagOrOperator, | ||||||
|  |         optFuzzy: state => state.optFuzzy, | ||||||
|  |         optSearchInPath: state => state.optSearchInPath, | ||||||
|  |         optSuggestPath: state => state.optSuggestPath, | ||||||
|  |         optFragmentSize: state => state.optFragmentSize, | ||||||
|  |         optQueryMode: state => state.optQueryMode, | ||||||
|  |         optTreemapType: state => state.optTreemapType, | ||||||
|  |         optTreemapTiling: state => state.optTreemapTiling, | ||||||
|  |         optTreemapSize: state => state.optTreemapSize, | ||||||
|  |         optTreemapColorGroupingDepth: state => state.optTreemapColorGroupingDepth, | ||||||
|  |         optTreemapColor: state => state.optTreemapColor, | ||||||
|  |         optLightboxLoadOnlyCurrent: state => state.optLightboxLoadOnlyCurrent, | ||||||
|  |         optLightboxSlideDuration: state => state.optLightboxSlideDuration, | ||||||
|  |         optResultSize: state => state.size, | ||||||
|  |     } | ||||||
|  | }) | ||||||
							
								
								
									
										139
									
								
								sist2-vue/src/util-js.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								sist2-vue/src/util-js.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,139 @@ | |||||||
|  | export function mergeTooltips(slider, threshold, separator, fixTooltips) { | ||||||
|  | 
 | ||||||
|  |     const isMobile = window.innerWidth <= 650; | ||||||
|  |     if (isMobile) { | ||||||
|  |         threshold = 25; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const textIsRtl = getComputedStyle(slider).direction === 'rtl'; | ||||||
|  |     const isRtl = slider.noUiSlider.options.direction === 'rtl'; | ||||||
|  |     const isVertical = slider.noUiSlider.options.orientation === 'vertical'; | ||||||
|  |     const tooltips = slider.noUiSlider.getTooltips(); | ||||||
|  |     const origins = slider.noUiSlider.getOrigins(); | ||||||
|  | 
 | ||||||
|  |     // Move tooltips into the origin element. The default stylesheet handles this.
 | ||||||
|  |     tooltips.forEach(function (tooltip, index) { | ||||||
|  |         if (tooltip) { | ||||||
|  |             origins[index].appendChild(tooltip); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     slider.noUiSlider.on('update', function (values, handle, unencoded, tap, positions) { | ||||||
|  | 
 | ||||||
|  |         const pools = [[]]; | ||||||
|  |         const poolPositions = [[]]; | ||||||
|  |         const poolValues = [[]]; | ||||||
|  |         let atPool = 0; | ||||||
|  | 
 | ||||||
|  |         // Assign the first tooltip to the first pool, if the tooltip is configured
 | ||||||
|  |         if (tooltips[0]) { | ||||||
|  |             pools[0][0] = 0; | ||||||
|  |             poolPositions[0][0] = positions[0]; | ||||||
|  |             poolValues[0][0] = values[0]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (let i = 1; i < positions.length; i++) { | ||||||
|  |             if (!tooltips[i] || (positions[i] - positions[i - 1]) > threshold) { | ||||||
|  |                 atPool++; | ||||||
|  |                 pools[atPool] = []; | ||||||
|  |                 poolValues[atPool] = []; | ||||||
|  |                 poolPositions[atPool] = []; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (tooltips[i]) { | ||||||
|  |                 pools[atPool].push(i); | ||||||
|  |                 poolValues[atPool].push(values[i]); | ||||||
|  |                 poolPositions[atPool].push(positions[i]); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         pools.forEach(function (pool, poolIndex) { | ||||||
|  |             const handlesInPool = pool.length; | ||||||
|  | 
 | ||||||
|  |             for (let j = 0; j < handlesInPool; j++) { | ||||||
|  |                 const handleNumber = pool[j]; | ||||||
|  | 
 | ||||||
|  |                 if (j === handlesInPool - 1) { | ||||||
|  |                     let offset = 0; | ||||||
|  | 
 | ||||||
|  |                     poolPositions[poolIndex].forEach(function (value) { | ||||||
|  |                         offset += 1000 - 10 * value; | ||||||
|  |                     }); | ||||||
|  | 
 | ||||||
|  |                     const direction = isVertical ? 'bottom' : 'right'; | ||||||
|  |                     const last = isRtl ? 0 : handlesInPool - 1; | ||||||
|  |                     const lastOffset = 1000 - 10 * poolPositions[poolIndex][last]; | ||||||
|  |                     offset = (textIsRtl && !isVertical ? 100 : 0) + (offset / handlesInPool) - lastOffset; | ||||||
|  | 
 | ||||||
|  |                     // Center this tooltip over the affected handles
 | ||||||
|  |                     tooltips[handleNumber].innerHTML = poolValues[poolIndex].join(separator); | ||||||
|  |                     tooltips[handleNumber].style.display = 'block'; | ||||||
|  | 
 | ||||||
|  |                     tooltips[handleNumber].style[direction] = offset + '%'; | ||||||
|  |                 } else { | ||||||
|  |                     // Hide this tooltip
 | ||||||
|  |                     tooltips[handleNumber].style.display = 'none'; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         if (fixTooltips) { | ||||||
|  |             const isMobile = window.innerWidth <= 650; | ||||||
|  |             const len = isMobile ? 20 : 5; | ||||||
|  | 
 | ||||||
|  |             if (positions[0] < len) { | ||||||
|  |                 tooltips[0].style.right = `${(1 - ((positions[0]) / len)) * -35}px` | ||||||
|  |             } else { | ||||||
|  |                 tooltips[0].style.right = "0" | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (positions[1] > (100 - len)) { | ||||||
|  |                 tooltips[1].style.right = `${((positions[1] - (100 - len)) / len) * 35}px` | ||||||
|  |             } else { | ||||||
|  |                 tooltips[1].style.right = "0" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export function burrow(table, addSelfDir, rootName) { | ||||||
|  |     const root = {}; | ||||||
|  |     table.forEach(row => { | ||||||
|  |         let layer = root; | ||||||
|  | 
 | ||||||
|  |         row.taxonomy.forEach(key => { | ||||||
|  |             layer[key] = key in layer ? layer[key] : {}; | ||||||
|  |             layer = layer[key]; | ||||||
|  |         }); | ||||||
|  |         if (Object.keys(layer).length === 0) { | ||||||
|  |             layer["$size$"] = row.size; | ||||||
|  |         } else if (addSelfDir) { | ||||||
|  |             layer["."] = { | ||||||
|  |                 "$size$": row.size, | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const descend = function (obj, depth) { | ||||||
|  |         return Object.keys(obj).filter(k => k !== "$size$").map(k => { | ||||||
|  |             const child = { | ||||||
|  |                 name: k, | ||||||
|  |                 depth: depth, | ||||||
|  |                 value: 0, | ||||||
|  |                 children: descend(obj[k], depth + 1) | ||||||
|  |             }; | ||||||
|  |             if ("$size$" in obj[k]) { | ||||||
|  |                 child.value = obj[k]["$size$"]; | ||||||
|  |             } | ||||||
|  |             return child; | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |         name: rootName, | ||||||
|  |         children: descend(root, 1), | ||||||
|  |         value: 0, | ||||||
|  |         depth: 0, | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										137
									
								
								sist2-vue/src/util.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								sist2-vue/src/util.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,137 @@ | |||||||
|  | import {EsHit} from "@/Sist2Api"; | ||||||
|  | 
 | ||||||
|  | export function ext(hit: EsHit) { | ||||||
|  |     return Object.prototype.hasOwnProperty.call(hit._source, "extension") | ||||||
|  |     && hit["_source"]["extension"] !== "" ? "." + hit["_source"]["extension"] : ""; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function strUnescape(str: string): string { | ||||||
|  |     let result = ""; | ||||||
|  | 
 | ||||||
|  |     for (let i = 0; i < str.length; i++) { | ||||||
|  |         const c = str[i]; | ||||||
|  |         const next = str[i + 1]; | ||||||
|  | 
 | ||||||
|  |         if (c === "]") { | ||||||
|  |             if (next === "]") { | ||||||
|  |                 result += c; | ||||||
|  |                 i += 1; | ||||||
|  |             } else { | ||||||
|  |                 result += String.fromCharCode(parseInt(str.slice(i, i + 2), 16)); | ||||||
|  |                 i += 2; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             result += c; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const thresh = 1000; | ||||||
|  | const units = ["k", "M", "G", "T", "P", "E", "Z", "Y"]; | ||||||
|  | 
 | ||||||
|  | export function humanFileSize(bytes: number): string { | ||||||
|  |     if (bytes === 0) { | ||||||
|  |         return "0 B" | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (Math.abs(bytes) < thresh) { | ||||||
|  |         return bytes + ' B'; | ||||||
|  |     } | ||||||
|  |     let u = -1; | ||||||
|  |     do { | ||||||
|  |         bytes /= thresh; | ||||||
|  |         ++u; | ||||||
|  |     } while (Math.abs(bytes) >= thresh && u < units.length - 1); | ||||||
|  | 
 | ||||||
|  |     return bytes.toFixed(1) + units[u]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function humanTime(sec_num: number): string { | ||||||
|  |     sec_num = Math.floor(sec_num); | ||||||
|  |     const hours = Math.floor(sec_num / 3600); | ||||||
|  |     const minutes = Math.floor((sec_num - (hours * 3600)) / 60); | ||||||
|  |     const seconds = sec_num - (hours * 3600) - (minutes * 60); | ||||||
|  | 
 | ||||||
|  |     return `${hours < 10 ? "0" : ""}${hours}:${minutes < 10 ? "0" : ""}${minutes}:${seconds < 10 ? "0" : ""}${seconds}`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function humanDate(numMilis: number): string { | ||||||
|  |     const date = (new Date(numMilis * 1000)); | ||||||
|  |     return date.getUTCFullYear() + "-" + ("0" + (date.getUTCMonth() + 1)).slice(-2) + "-" + ("0" + date.getUTCDate()).slice(-2) | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function lum(c: string) { | ||||||
|  |     c = c.substring(1); | ||||||
|  |     const rgb = parseInt(c, 16); | ||||||
|  |     const r = (rgb >> 16) & 0xff; | ||||||
|  |     const g = (rgb >> 8) & 0xff; | ||||||
|  |     const b = (rgb >> 0) & 0xff; | ||||||
|  | 
 | ||||||
|  |     return 0.2126 * r + 0.7152 * g + 0.0722 * b; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export function getSelectedTreeNodes(tree: any) { | ||||||
|  |     const selectedNodes = new Set(); | ||||||
|  | 
 | ||||||
|  |     const selected = tree.selected(); | ||||||
|  | 
 | ||||||
|  |     for (let i = 0; i < selected.length; i++) { | ||||||
|  | 
 | ||||||
|  |         if (selected[i].id === "any") { | ||||||
|  |             return ["any"] | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         //Only get children
 | ||||||
|  |         if (selected[i].text.indexOf("(") !== -1) { | ||||||
|  |             if (selected[i].values) { | ||||||
|  |                 selectedNodes.add(selected[i].values.slice(-1)[0]); | ||||||
|  |             } else { | ||||||
|  |                 selectedNodes.add(selected[i].id); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return Array.from(selectedNodes); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function serializeMimes(mimes: string[]): string | undefined { | ||||||
|  |     if (mimes.length == 0) { | ||||||
|  |         return undefined; | ||||||
|  |     } | ||||||
|  |     return mimes.map(mime => compressMime(mime)).join(""); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function deserializeMimes(mimeString: string): string[] { | ||||||
|  |     return mimeString | ||||||
|  |         .replaceAll(/([IVATUF])/g, "$$$&") | ||||||
|  |         .split("$") | ||||||
|  |         .map(mime => decompressMime(mime)) | ||||||
|  |         .slice(1) // Ignore the first (empty) token
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function compressMime(mime: string): string { | ||||||
|  |     return mime | ||||||
|  |         .replace("image/", "I") | ||||||
|  |         .replace("video/", "V") | ||||||
|  |         .replace("application/", "A") | ||||||
|  |         .replace("text/", "T") | ||||||
|  |         .replace("audio/", "U") | ||||||
|  |         .replace("font/", "F") | ||||||
|  |         .replace("+", ",") | ||||||
|  |         .replace("x-", "X") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function decompressMime(mime: string): string { | ||||||
|  |     return mime | ||||||
|  |         .replace("I", "image/") | ||||||
|  |         .replace("V", "video/") | ||||||
|  |         .replace("A", "application/") | ||||||
|  |         .replace("T", "text/") | ||||||
|  |         .replace("U", "audio/") | ||||||
|  |         .replace("F", "font/") | ||||||
|  |         .replace(",", "+") | ||||||
|  |         .replace("X", "x-") | ||||||
|  | } | ||||||
							
								
								
									
										265
									
								
								sist2-vue/src/views/Configuration.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								sist2-vue/src/views/Configuration.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,265 @@ | |||||||
|  | <template> | ||||||
|  |   <!--  <div :style="{width: `${$store.getters.optContainerWidth}px`}"--> | ||||||
|  |   <div | ||||||
|  |       v-if="!configLoading" | ||||||
|  |       style="margin-left: auto; margin-right: auto;" class="container"> | ||||||
|  | 
 | ||||||
|  |     <b-card> | ||||||
|  |       <b-card-title> | ||||||
|  |         <GearIcon></GearIcon> | ||||||
|  |         {{ $t("config") }} | ||||||
|  |       </b-card-title> | ||||||
|  |       <p>{{ $t("configDescription") }}</p> | ||||||
|  | 
 | ||||||
|  |       <b-card-body> | ||||||
|  |         <h4>{{ $t("displayOptions") }}</h4> | ||||||
|  | 
 | ||||||
|  |         <b-card> | ||||||
|  |           <b-form-checkbox :checked="optLightboxLoadOnlyCurrent" @input="setOptLightboxLoadOnlyCurrent"> | ||||||
|  |             {{ $t("opt.lightboxLoadOnlyCurrent") }} | ||||||
|  |           </b-form-checkbox> | ||||||
|  | 
 | ||||||
|  |           <label>{{ $t("opt.lang") }}</label> | ||||||
|  |           <b-form-select :options="langOptions" :value="optLang" @input="setOptLang"></b-form-select> | ||||||
|  | 
 | ||||||
|  |           <label>{{ $t("opt.theme") }}</label> | ||||||
|  |           <b-form-select :options="themeOptions" :value="optTheme" @input="setOptTheme"></b-form-select> | ||||||
|  | 
 | ||||||
|  |           <label>{{ $t("opt.displayMode") }}</label> | ||||||
|  |           <b-form-select :options="displayModeOptions" :value="optDisplay" @input="setOptDisplay"></b-form-select> | ||||||
|  | 
 | ||||||
|  |           <label>{{ $t("opt.columns") }}</label> | ||||||
|  |           <b-form-select :options="columnsOptions" :value="optColumns" @input="setOptColumns"></b-form-select> | ||||||
|  |         </b-card> | ||||||
|  | 
 | ||||||
|  |         <br/> | ||||||
|  |         <h4>{{ $t("searchOptions") }}</h4> | ||||||
|  |         <b-card> | ||||||
|  |           <b-form-checkbox :checked="optHighlight" @input="setOptHighlight">{{ $t("opt.highlight") }}</b-form-checkbox> | ||||||
|  |           <b-form-checkbox :checked="optTagOrOperator" @input="setOptTagOrOperator">{{ | ||||||
|  |               $t("opt.tagOrOperator") | ||||||
|  |             }} | ||||||
|  |           </b-form-checkbox> | ||||||
|  |           <b-form-checkbox :checked="optFuzzy" @input="setOptFuzzy">{{ $t("opt.fuzzy") }}</b-form-checkbox> | ||||||
|  |           <b-form-checkbox :checked="optSearchInPath" @input="setOptSearchInPath">{{ | ||||||
|  |               $t("opt.searchInPath") | ||||||
|  |             }} | ||||||
|  |           </b-form-checkbox> | ||||||
|  |           <b-form-checkbox :checked="optSuggestPath" @input="setOptSuggestPath">{{ | ||||||
|  |               $t("opt.suggestPath") | ||||||
|  |             }} | ||||||
|  |           </b-form-checkbox> | ||||||
|  | 
 | ||||||
|  |           <br/> | ||||||
|  |           <label>{{ $t("opt.fragmentSize") }}</label> | ||||||
|  |           <b-form-input :value="optFragmentSize" step="10" type="number" min="0" | ||||||
|  |                         @input="setOptFragmentSize"></b-form-input> | ||||||
|  | 
 | ||||||
|  |           <label>{{ $t("opt.resultSize") }}</label> | ||||||
|  |           <b-form-input :value="optResultSize" type="number" min="10" | ||||||
|  |                         @input="setOptResultSize"></b-form-input> | ||||||
|  | 
 | ||||||
|  |           <label>{{ $t("opt.queryMode") }}</label> | ||||||
|  |           <b-form-select :options="queryModeOptions" :value="optQueryMode" @input="setOptQueryMode"></b-form-select> | ||||||
|  | 
 | ||||||
|  |           <label>{{ $t("opt.slideDuration") }}</label> | ||||||
|  |           <b-form-input :value="optLightboxSlideDuration" type="number" min="1" | ||||||
|  |                         @input="setOptLightboxSlideDuration"></b-form-input> | ||||||
|  |         </b-card> | ||||||
|  | 
 | ||||||
|  |         <h4 class="mt-3">{{ $t("treemapOptions") }}</h4> | ||||||
|  |         <b-card> | ||||||
|  |           <label>{{ $t("opt.treemapType") }}</label> | ||||||
|  |           <b-form-select :value="optTreemapType" :options="treemapTypeOptions" | ||||||
|  |                          @input="setOptTreemapType"></b-form-select> | ||||||
|  | 
 | ||||||
|  |           <label>{{ $t("opt.treemapTiling") }}</label> | ||||||
|  |           <b-form-select :value="optTreemapTiling" :options="treemapTilingOptions" | ||||||
|  |                          @input="setOptTreemapTiling"></b-form-select> | ||||||
|  | 
 | ||||||
|  |           <label>{{ $t("opt.treemapColorGroupingDepth") }}</label> | ||||||
|  |           <b-form-input :value="optTreemapColorGroupingDepth" type="number" min="1" | ||||||
|  |                         @input="setOptTreemapColorGroupingDepth"></b-form-input> | ||||||
|  | 
 | ||||||
|  |           <label>{{ $t("opt.treemapSize") }}</label> | ||||||
|  |           <b-form-select :value="optTreemapSize" :options="treemapSizeOptions" | ||||||
|  |                          @input="setOptTreemapSize"></b-form-select> | ||||||
|  | 
 | ||||||
|  |           <template v-if="$store.getters.optTreemapSize === 'custom'"> | ||||||
|  |             <!-- TODO Width/Height input --> | ||||||
|  |             <b-form-input type="number" min="0" step="10"></b-form-input> | ||||||
|  |             <b-form-input type="number" min="0" step="10"></b-form-input> | ||||||
|  |           </template> | ||||||
|  | 
 | ||||||
|  |           <label>{{ $t("opt.treemapColor") }}</label> | ||||||
|  |           <b-form-select :value="optTreemapColor" :options="treemapColorOptions" | ||||||
|  |                          @input="setOptTreemapColor"></b-form-select> | ||||||
|  |         </b-card> | ||||||
|  | 
 | ||||||
|  |         <b-button variant="danger" class="mt-4" @click="onResetClick()">{{ $t("configReset") }}</b-button> | ||||||
|  |       </b-card-body> | ||||||
|  |     </b-card> | ||||||
|  | 
 | ||||||
|  |     <b-card v-if="loading" class="mt-4"> | ||||||
|  |       <Preloader></Preloader> | ||||||
|  |     </b-card> | ||||||
|  |     <DebugInfo v-else></DebugInfo> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | import Vue from "vue"; | ||||||
|  | import {mapGetters, mapMutations} from "vuex"; | ||||||
|  | import DebugInfo from "@/components/DebugInfo.vue"; | ||||||
|  | import Preloader from "@/components/Preloader.vue"; | ||||||
|  | import sist2 from "@/Sist2Api"; | ||||||
|  | import GearIcon from "@/components/GearIcon.vue"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   components: {GearIcon, DebugInfo, Preloader}, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       loading: true, | ||||||
|  |       configLoading: false, | ||||||
|  |       langOptions: [ | ||||||
|  |         {value: "en", text: this.$t("lang.en")}, | ||||||
|  |         {value: "fr", text: this.$t("lang.fr")}, | ||||||
|  |       ], | ||||||
|  |       queryModeOptions: [ | ||||||
|  |         {value: "simple", text: this.$t("queryMode.simple")}, | ||||||
|  |         {value: "advanced", text: this.$t("queryMode.advanced")} | ||||||
|  |       ], | ||||||
|  |       displayModeOptions: [ | ||||||
|  |         {value: "grid", text: this.$t("displayMode.grid")}, | ||||||
|  |         {value: "list", text: this.$t("displayMode.list")} | ||||||
|  |       ], | ||||||
|  |       columnsOptions: [ | ||||||
|  |         {value: "auto", text: this.$t("columns.auto")}, | ||||||
|  |         {value: 1, text: "1"}, | ||||||
|  |         {value: 2, text: "2"}, | ||||||
|  |         {value: 3, text: "3"}, | ||||||
|  |         {value: 4, text: "4"}, | ||||||
|  |         {value: 5, text: "5"}, | ||||||
|  |         {value: 6, text: "6"}, | ||||||
|  |         {value: 7, text: "7"}, | ||||||
|  |         {value: 8, text: "8"}, | ||||||
|  |         {value: 9, text: "9"}, | ||||||
|  |         {value: 10, text: "10"}, | ||||||
|  |         {value: 11, text: "11"}, | ||||||
|  |         {value: 12, text: "12"}, | ||||||
|  |       ], | ||||||
|  |       treemapTypeOptions: [ | ||||||
|  |         {value: "cascaded", text: this.$t("treemapType.cascaded")}, | ||||||
|  |         {value: "flat", text: this.$t("treemapType.flat")} | ||||||
|  |       ], | ||||||
|  |       treemapTilingOptions: [ | ||||||
|  |         {value: "binary", text: this.$t("treemapTiling.binary")}, | ||||||
|  |         {value: "squarify", text: this.$t("treemapTiling.squarify")}, | ||||||
|  |         {value: "slice", text: this.$t("treemapTiling.slice")}, | ||||||
|  |         {value: "dice", text: this.$t("treemapTiling.dice")}, | ||||||
|  |         {value: "sliceDice", text: this.$t("treemapTiling.sliceDice")}, | ||||||
|  |       ], | ||||||
|  |       treemapSizeOptions: [ | ||||||
|  |         {value: "small", text: this.$t("treemapSize.small")}, | ||||||
|  |         {value: "medium", text: this.$t("treemapSize.medium")}, | ||||||
|  |         {value: "large", text: this.$t("treemapSize.large")}, | ||||||
|  |         {value: "x-large", text: this.$t("treemapSize.xLarge")}, | ||||||
|  |         {value: "xx-large", text: this.$t("treemapSize.xxLarge")}, | ||||||
|  |         // {value: "custom", text: this.$t("treemapSize.custom")}, | ||||||
|  |       ], | ||||||
|  |       treemapColorOptions: [ | ||||||
|  |         {value: "PuBuGn", text: "Purple-Blue-Green"}, | ||||||
|  |         {value: "PuRd", text: "Purple-Red"}, | ||||||
|  |         {value: "PuBu", text: "Purple-Blue"}, | ||||||
|  |         {value: "YlOrBr", text: "Yellow-Orange-Brown"}, | ||||||
|  |         {value: "YlOrRd", text: "Yellow-Orange-Red"}, | ||||||
|  |         {value: "YlGn", text: "Yellow-Green"}, | ||||||
|  |         {value: "YlGnBu", text: "Yellow-Green-Blue"}, | ||||||
|  |         {value: "Plasma", text: "Plasma"}, | ||||||
|  |         {value: "Magma", text: "Magma"}, | ||||||
|  |         {value: "Inferno", text: "Inferno"}, | ||||||
|  |         {value: "Viridis", text: "Viridis"}, | ||||||
|  |         {value: "Turbo", text: "Turbo"}, | ||||||
|  |       ], | ||||||
|  |       themeOptions: [ | ||||||
|  |         {value: "light", text: this.$t("theme.light")}, | ||||||
|  |         {value: "black", text: this.$t("theme.black")} | ||||||
|  |       ] | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters([ | ||||||
|  |       "optTheme", | ||||||
|  |       "optDisplay", | ||||||
|  |       "optColumns", | ||||||
|  |       "optHighlight", | ||||||
|  |       "optFuzzy", | ||||||
|  |       "optSearchInPath", | ||||||
|  |       "optSuggestPath", | ||||||
|  |       "optFragmentSize", | ||||||
|  |       "optQueryMode", | ||||||
|  |       "optTreemapType", | ||||||
|  |       "optTreemapTiling", | ||||||
|  |       "optTreemapColorGroupingDepth", | ||||||
|  |       "optTreemapColor", | ||||||
|  |       "optTreemapSize", | ||||||
|  |       "optLightboxLoadOnlyCurrent", | ||||||
|  |       "optLightboxSlideDuration", | ||||||
|  |       "optContainerWidth", | ||||||
|  |       "optResultSize", | ||||||
|  |       "optTagOrOperator", | ||||||
|  |       "optLang" | ||||||
|  |     ]), | ||||||
|  |     clientWidth() { | ||||||
|  |       return window.innerWidth; | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     sist2.getSist2Info().then(data => { | ||||||
|  |       this.$store.commit("setSist2Info", data) | ||||||
|  |       this.loading = false; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     this.$store.subscribe((mutation) => { | ||||||
|  |       if (mutation.type.startsWith("setOpt")) { | ||||||
|  |         this.$store.dispatch("updateConfiguration"); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapMutations([ | ||||||
|  |       "setOptTheme", | ||||||
|  |       "setOptDisplay", | ||||||
|  |       "setOptColumns", | ||||||
|  |       "setOptHighlight", | ||||||
|  |       "setOptFuzzy", | ||||||
|  |       "setOptSearchInPath", | ||||||
|  |       "setOptSuggestPath", | ||||||
|  |       "setOptFragmentSize", | ||||||
|  |       "setOptQueryMode", | ||||||
|  |       "setOptTreemapType", | ||||||
|  |       "setOptTreemapTiling", | ||||||
|  |       "setOptTreemapColorGroupingDepth", | ||||||
|  |       "setOptTreemapColor", | ||||||
|  |       "setOptTreemapSize", | ||||||
|  |       "setOptLightboxLoadOnlyCurrent", | ||||||
|  |       "setOptLightboxSlideDuration", | ||||||
|  |       "setOptContainerWidth", | ||||||
|  |       "setOptResultSize", | ||||||
|  |       "setOptTagOrOperator", | ||||||
|  |       "setOptLang" | ||||||
|  |     ]), | ||||||
|  |     onResetClick() { | ||||||
|  |       localStorage.removeItem("sist2_configuration"); | ||||||
|  |       window.location.reload(); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | .shrink { | ||||||
|  |   flex-grow: inherit; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										288
									
								
								sist2-vue/src/views/SearchPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								sist2-vue/src/views/SearchPage.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,288 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="container"> | ||||||
|  |     <Lightbox></Lightbox> | ||||||
|  |     <HelpDialog :show="showHelp" @close="showHelp = false"></HelpDialog> | ||||||
|  | 
 | ||||||
|  |     <b-card v-if="uiLoading"> | ||||||
|  |       <Preloader></Preloader> | ||||||
|  |     </b-card> | ||||||
|  | 
 | ||||||
|  |     <b-card v-show="!uiLoading" id="search-panel"> | ||||||
|  |       <SearchBar @show-help="showHelp=true"></SearchBar> | ||||||
|  |       <b-row> | ||||||
|  |         <b-col style="height: 70px;" sm="6"> | ||||||
|  |           <SizeSlider></SizeSlider> | ||||||
|  |         </b-col> | ||||||
|  |         <b-col> | ||||||
|  |           <PathTree @search="search(true)"></PathTree> | ||||||
|  |         </b-col> | ||||||
|  |       </b-row> | ||||||
|  |       <b-row> | ||||||
|  |         <b-col sm="6"> | ||||||
|  |           <b-row> | ||||||
|  |             <b-col style="height: 70px;"> | ||||||
|  |               <DateSlider></DateSlider> | ||||||
|  |             </b-col> | ||||||
|  |           </b-row> | ||||||
|  |           <b-row> | ||||||
|  |             <b-col> | ||||||
|  |               <IndexPicker></IndexPicker> | ||||||
|  |             </b-col> | ||||||
|  |           </b-row> | ||||||
|  |         </b-col> | ||||||
|  |         <b-col> | ||||||
|  |           <b-tabs> | ||||||
|  |             <b-tab :title="$t('mimeTypes')"> | ||||||
|  |               <MimePicker></MimePicker> | ||||||
|  |             </b-tab> | ||||||
|  |             <b-tab :title="$t('tags')"> | ||||||
|  |               <TagPicker></TagPicker> | ||||||
|  |             </b-tab> | ||||||
|  |           </b-tabs> | ||||||
|  |         </b-col> | ||||||
|  |       </b-row> | ||||||
|  |     </b-card> | ||||||
|  | 
 | ||||||
|  |     <Preloader v-if="searchBusy && docs.length === 0" class="mt-3"></Preloader> | ||||||
|  | 
 | ||||||
|  |     <div v-else-if="docs.length > 0"> | ||||||
|  |       <ResultsCard></ResultsCard> | ||||||
|  | 
 | ||||||
|  |       <DocCardWall v-if="optDisplay==='grid'" :docs="docs" :append="appendFunc"></DocCardWall> | ||||||
|  |       <DocList v-else :docs="docs" :append="appendFunc"></DocList> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts"> | ||||||
|  | import Preloader from "@/components/Preloader.vue"; | ||||||
|  | import {mapGetters, mapMutations} from "vuex"; | ||||||
|  | import sist2 from "../Sist2Api"; | ||||||
|  | import Sist2Api, {EsHit, EsResult} from "../Sist2Api"; | ||||||
|  | import SearchBar from "@/components/SearchBar.vue"; | ||||||
|  | import IndexPicker from "@/components/IndexPicker.vue"; | ||||||
|  | import Vue from "vue"; | ||||||
|  | import Sist2Query from "@/Sist2Query"; | ||||||
|  | import _debounce from "lodash/debounce"; | ||||||
|  | import DocCardWall from "@/components/DocCardWall.vue"; | ||||||
|  | import Lightbox from "@/components/Lightbox.vue"; | ||||||
|  | import LightboxCaption from "@/components/LightboxCaption.vue"; | ||||||
|  | import MimePicker from "../components/MimePicker.vue"; | ||||||
|  | import ResultsCard from "@/components/ResultsCard.vue"; | ||||||
|  | import PathTree from "@/components/PathTree.vue"; | ||||||
|  | import SizeSlider from "@/components/SizeSlider.vue"; | ||||||
|  | import DateSlider from "@/components/DateSlider.vue"; | ||||||
|  | import TagPicker from "@/components/TagPicker.vue"; | ||||||
|  | import DocList from "@/components/DocList.vue"; | ||||||
|  | import HelpDialog from "@/components/HelpDialog.vue"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export default Vue.extend({ | ||||||
|  |   components: { | ||||||
|  |     HelpDialog, | ||||||
|  |     DocList, | ||||||
|  |     TagPicker, | ||||||
|  |     DateSlider, | ||||||
|  |     SizeSlider, PathTree, ResultsCard, MimePicker, Lightbox, DocCardWall, IndexPicker, SearchBar, Preloader | ||||||
|  |   }, | ||||||
|  |   data: () => ({ | ||||||
|  |     loading: false, | ||||||
|  |     uiLoading: true, | ||||||
|  |     search: undefined as any, | ||||||
|  |     docs: [] as EsHit[], | ||||||
|  |     docIds: new Set(), | ||||||
|  |     searchBusy: false, | ||||||
|  |     Sist2Query: Sist2Query, | ||||||
|  |     showHelp: false | ||||||
|  |   }), | ||||||
|  |   computed: { | ||||||
|  |     ...mapGetters(["indices", "optDisplay"]), | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.search = _debounce(async (clear: boolean) => { | ||||||
|  |       if (clear) { | ||||||
|  |         await this.clearResults(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       await this.searchNow(Sist2Query.searchQuery()); | ||||||
|  | 
 | ||||||
|  |     }, 350, {leading: false}); | ||||||
|  | 
 | ||||||
|  |     Sist2Api.getMimeTypes().then(mimeMap => { | ||||||
|  |       this.$store.commit("setUiMimeMap", mimeMap); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     this.$store.dispatch("loadFromArgs", this.$route).then(() => { | ||||||
|  |       this.$store.subscribe(() => this.$store.dispatch("updateArgs", this.$router)); | ||||||
|  |       this.$store.subscribe((mutation) => { | ||||||
|  |         if ([ | ||||||
|  |           "setSizeMin", "setSizeMax", "setDateMin", "setDateMax", "setSearchText", "setPathText", | ||||||
|  |           "setSortMode", "setOptHighlight", "setOptFragmentSize", "setFuzzy", "setSize", "setSelectedIndices", | ||||||
|  |           "setSelectedMimeTypes", "setSelectedTags", "setOptQueryMode", "setOptSearchInPath", | ||||||
|  |         ].includes(mutation.type)) { | ||||||
|  |           if (this.searchBusy) { | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           this.search(true); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     this.getDateRange().then((range: { min: number, max: number }) => { | ||||||
|  |       this.setDateBoundsMin(range.min); | ||||||
|  |       this.setDateBoundsMax(range.max); | ||||||
|  | 
 | ||||||
|  |       sist2.getSist2Info().then(data => { | ||||||
|  |         this.setSist2Info(data); | ||||||
|  |         this.setIndices(data.indices); | ||||||
|  |         this.uiLoading = false; | ||||||
|  | 
 | ||||||
|  |         this.search(true); | ||||||
|  |       }).catch(() => { | ||||||
|  |         this.showErrorToast(); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapMutations({ | ||||||
|  |       setSist2Info: "setSist2Info", | ||||||
|  |       setIndices: "setIndices", | ||||||
|  |       setDateBoundsMin: "setDateBoundsMin", | ||||||
|  |       setDateBoundsMax: "setDateBoundsMax", | ||||||
|  |       setTags: "setTags", | ||||||
|  |     }), | ||||||
|  |     showErrorToast() { | ||||||
|  |       this.$bvToast.toast( | ||||||
|  |           this.$t("toast.esConnErr"), | ||||||
|  |           { | ||||||
|  |             title: this.$t("toast.esConnErrTitle"), | ||||||
|  |             noAutoHide: true, | ||||||
|  |             toaster: "b-toaster-bottom-right", | ||||||
|  |             headerClass: "toast-header-error", | ||||||
|  |             bodyClass: "toast-body-error", | ||||||
|  |           }); | ||||||
|  |     }, | ||||||
|  |     showSyntaxErrorToast: function (): void { | ||||||
|  |       this.$bvToast.toast( | ||||||
|  |           this.$t("toast.esQueryErr"), | ||||||
|  |           { | ||||||
|  |             title: this.$t("toast.esQueryErrTitle"), | ||||||
|  |             noAutoHide: true, | ||||||
|  |             toaster: "b-toaster-bottom-right", | ||||||
|  |             headerClass: "toast-header-warning", | ||||||
|  |             bodyClass: "toast-body-warning", | ||||||
|  |           }); | ||||||
|  |     }, | ||||||
|  |     async searchNow(q: any) { | ||||||
|  |       this.searchBusy = true; | ||||||
|  |       await this.$store.dispatch("incrementQuerySequence"); | ||||||
|  | 
 | ||||||
|  |       Sist2Api.esQuery(q).then(async (resp: EsResult) => { | ||||||
|  |         await this.handleSearch(resp); | ||||||
|  |         this.searchBusy = false; | ||||||
|  |       }).catch(err => { | ||||||
|  |         if (err.response.status === 500 && this.$store.state.optQueryMode === "advanced") { | ||||||
|  |           this.showSyntaxErrorToast(); | ||||||
|  |         } else { | ||||||
|  |           this.showErrorToast(); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }, | ||||||
|  |     async clearResults() { | ||||||
|  |       this.docs = []; | ||||||
|  |       this.docIds.clear(); | ||||||
|  |       await this.$store.dispatch("clearResults"); | ||||||
|  |       this.$store.commit("setUiReachedScrollEnd", false); | ||||||
|  |     }, | ||||||
|  |     async handleSearch(resp: EsResult) { | ||||||
|  |       if (resp.hits.hits.length == 0) { | ||||||
|  |         this.$store.commit("setUiReachedScrollEnd", true); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       resp.hits.hits = resp.hits.hits.filter(hit => !this.docIds.has(hit._id)); | ||||||
|  |       resp.hits.hits.forEach(hit => this.docIds.add(hit._id)); | ||||||
|  | 
 | ||||||
|  |       for (const hit of resp.hits.hits) { | ||||||
|  |         if (hit._props.isPlayableImage || hit._props.isPlayableVideo) { | ||||||
|  |           hit._seq = await this.$store.dispatch("getKeySequence"); | ||||||
|  |           this.$store.commit("addLightboxSource", { | ||||||
|  |             source: `f/${hit._id}`, | ||||||
|  |             thumbnail: hit._props.hasThumbnail | ||||||
|  |                 ? `t/${hit._source.index}/${hit._id}` | ||||||
|  |                 : null, | ||||||
|  |             caption: { | ||||||
|  |               component: LightboxCaption, | ||||||
|  |               props: {hit: hit} | ||||||
|  |             }, | ||||||
|  |             type: hit._props.isVideo ? "video" : "image" | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       await this.$store.dispatch("remountLightbox"); | ||||||
|  |       this.$store.commit("setLastQueryResult", resp); | ||||||
|  | 
 | ||||||
|  |       this.docs.push(...resp.hits.hits); | ||||||
|  |     }, | ||||||
|  |     getDateRange(): Promise<{ min: number, max: number }> { | ||||||
|  |       return sist2.esQuery({ | ||||||
|  |         // TODO: filter current selected indices | ||||||
|  |         aggs: { | ||||||
|  |           dateMin: {min: {field: "mtime"}}, | ||||||
|  |           dateMax: {max: {field: "mtime"}}, | ||||||
|  |         }, | ||||||
|  |         size: 0 | ||||||
|  |       }).then(res => { | ||||||
|  |         return { | ||||||
|  |           min: res.aggregations.dateMin.value, | ||||||
|  |           max: res.aggregations.dateMax.value, | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |     appendFunc() { | ||||||
|  |       if (!this.$store.state.uiReachedScrollEnd && this.search && !this.searchBusy) { | ||||||
|  |         this.searchNow(Sist2Query.searchQuery()); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   beforeRouteUpdate(to, from, next) { | ||||||
|  |     if (this.$store.state.uiLightboxIsOpen) { | ||||||
|  |       this.$store.commit("_setUiShowLightbox", false); | ||||||
|  |       next(false); | ||||||
|  |     } else { | ||||||
|  |       next(); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | }) | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | 
 | ||||||
|  | #search-panel { | ||||||
|  |   box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important; | ||||||
|  |   border-radius: 0; | ||||||
|  |   border: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .toast-header-error, .toast-body-error { | ||||||
|  |   background: #a94442; | ||||||
|  |   color: #f2dede !important; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .toast-header-error { | ||||||
|  |   color: #fff !important; | ||||||
|  |   border-bottom: none; | ||||||
|  |   margin-bottom: -1em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .toast-header-error .close { | ||||||
|  |   text-shadow: none; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .toast-header-warning, .toast-body-warning { | ||||||
|  |   background: #FF8F00; | ||||||
|  |   color: #FFF3E0 !important; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										79
									
								
								sist2-vue/src/views/StatsPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								sist2-vue/src/views/StatsPage.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | |||||||
|  | <template> | ||||||
|  |   <b-container> | ||||||
|  | 
 | ||||||
|  |     <b-card v-if="loading"> | ||||||
|  |       <Preloader></Preloader> | ||||||
|  |     </b-card> | ||||||
|  | 
 | ||||||
|  |     <template v-else> | ||||||
|  |       <b-card> | ||||||
|  |         <b-card-body> | ||||||
|  |           <b-select v-model="selectedIndex" :options="indexOptions"> | ||||||
|  |             <template #first> | ||||||
|  |               <b-form-select-option :value="null" disabled>{{ $t("indexPickerPlaceholder") }}</b-form-select-option> | ||||||
|  |             </template> | ||||||
|  |           </b-select> | ||||||
|  |         </b-card-body> | ||||||
|  |       </b-card> | ||||||
|  | 
 | ||||||
|  |       <b-card v-if="selectedIndex !== null" class="mt-3"> | ||||||
|  |         <b-card-body> | ||||||
|  |           <D3Treemap :index-id="selectedIndex"></D3Treemap> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         </b-card-body> | ||||||
|  |       </b-card> | ||||||
|  | 
 | ||||||
|  |       <b-card v-if="selectedIndex !== null" class="stats-card mt-3"> | ||||||
|  |         <D3MimeBarCount :index-id="selectedIndex"></D3MimeBarCount> | ||||||
|  |         <D3MimeBarSize :index-id="selectedIndex"></D3MimeBarSize> | ||||||
|  |         <D3DateHistogram :index-id="selectedIndex"></D3DateHistogram> | ||||||
|  |         <D3SizeHistogram :index-id="selectedIndex"></D3SizeHistogram> | ||||||
|  |       </b-card> | ||||||
|  |     </template> | ||||||
|  |   </b-container> | ||||||
|  | </template> | ||||||
|  | <script> | ||||||
|  | import D3Treemap from "@/components/D3Treemap"; | ||||||
|  | import Sist2Api from "@/Sist2Api"; | ||||||
|  | import Preloader from "@/components/Preloader.vue"; | ||||||
|  | import D3MimeBarCount from "@/components/D3MimeBarCount"; | ||||||
|  | import D3MimeBarSize from "@/components/D3MimeBarSize"; | ||||||
|  | import D3DateHistogram from "@/components/D3DateHistogram"; | ||||||
|  | import D3SizeHistogram from "@/components/D3SizeHistogram"; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   components: {D3SizeHistogram, D3DateHistogram, D3MimeBarSize, D3MimeBarCount, D3Treemap, Preloader}, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       loading: true, | ||||||
|  |       selectedIndex: null, | ||||||
|  |       indices: [] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     indexOptions() { | ||||||
|  |       return this.indices.map(idx => { | ||||||
|  |         return { | ||||||
|  |           text: idx.name, | ||||||
|  |           value: idx.id | ||||||
|  |         }; | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     Sist2Api.getSist2Info().then(data => { | ||||||
|  |       this.indices = data.indices; | ||||||
|  |       this.loading = false; | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style> | ||||||
|  | 
 | ||||||
|  | .stats-card { | ||||||
|  |   text-align: center; | ||||||
|  |   padding: 1em; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										40
									
								
								sist2-vue/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								sist2-vue/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | { | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "target": "esnext", | ||||||
|  |     "module": "esnext", | ||||||
|  |     "strict": false, | ||||||
|  |     "jsx": "preserve", | ||||||
|  |     "importHelpers": true, | ||||||
|  |     "moduleResolution": "node", | ||||||
|  |     "experimentalDecorators": true, | ||||||
|  |     "skipLibCheck": true, | ||||||
|  |     "esModuleInterop": true, | ||||||
|  |     "allowSyntheticDefaultImports": true, | ||||||
|  |     "sourceMap": false, | ||||||
|  |     "baseUrl": ".", | ||||||
|  |     "types": [ | ||||||
|  |       "webpack-env", | ||||||
|  |     ], | ||||||
|  |     "paths": { | ||||||
|  |       "@/*": [ | ||||||
|  |         "src/*" | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "lib": [ | ||||||
|  |       "esnext", | ||||||
|  |       "dom", | ||||||
|  |       "dom.iterable", | ||||||
|  |       "scripthost" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   "include": [ | ||||||
|  |     "src/**/*.ts", | ||||||
|  |     "src/**/*.tsx", | ||||||
|  |     "src/**/*.vue", | ||||||
|  |     "tests/**/*.ts", | ||||||
|  |     "tests/**/*.tsx" | ||||||
|  |   ], | ||||||
|  |   "exclude": [ | ||||||
|  |     "node_modules" | ||||||
|  |   ] | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								sist2-vue/vue.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								sist2-vue/vue.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | module.exports = { | ||||||
|  |     filenameHashing: false, | ||||||
|  |     productionSourceMap: false, | ||||||
|  |     publicPath: "./", | ||||||
|  |     pages: { | ||||||
|  |         index: { | ||||||
|  |             entry: "src/main.js" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								sist2-vue/watch.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								sist2-vue/watch.sh
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | #!/usr/bin/env bash | ||||||
|  | 
 | ||||||
|  | ./node_modules/@vue/cli-service/bin/vue-cli-service.js build --watch | ||||||
							
								
								
									
										19
									
								
								src/cli.c
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								src/cli.c
									
									
									
									
									
								
							| @ -4,13 +4,15 @@ | |||||||
| 
 | 
 | ||||||
| #define DEFAULT_OUTPUT "index.sist2/" | #define DEFAULT_OUTPUT "index.sist2/" | ||||||
| #define DEFAULT_CONTENT_SIZE 32768 | #define DEFAULT_CONTENT_SIZE 32768 | ||||||
| #define DEFAULT_QUALITY 5 | #define DEFAULT_QUALITY 1 | ||||||
| #define DEFAULT_SIZE 500 | #define DEFAULT_SIZE 300 | ||||||
| #define DEFAULT_REWRITE_URL "" | #define DEFAULT_REWRITE_URL "" | ||||||
| 
 | 
 | ||||||
| #define DEFAULT_ES_URL "http://localhost:9200"
 | #define DEFAULT_ES_URL "http://localhost:9200"
 | ||||||
| #define DEFAULT_ES_INDEX "sist2" | #define DEFAULT_ES_INDEX "sist2" | ||||||
| #define DEFAULT_BATCH_SIZE 100 | #define DEFAULT_BATCH_SIZE 100 | ||||||
|  | #define DEFAULT_TAGLINE "Lightning-fast file system indexer and search tool" | ||||||
|  | #define DEFAULT_LANG "en" | ||||||
| 
 | 
 | ||||||
| #define DEFAULT_LISTEN_ADDRESS "localhost:4090" | #define DEFAULT_LISTEN_ADDRESS "localhost:4090" | ||||||
| #define DEFAULT_TREEMAP_THRESHOLD 0.0005 | #define DEFAULT_TREEMAP_THRESHOLD 0.0005 | ||||||
| @ -91,7 +93,7 @@ int scan_args_validate(scan_args_t *args, int argc, const char **argv) { | |||||||
|     if (args->incremental != NULL) { |     if (args->incremental != NULL) { | ||||||
|         args->incremental = abspath(args->incremental); |         args->incremental = abspath(args->incremental); | ||||||
|         if (abs_path == NULL) { |         if (abs_path == NULL) { | ||||||
|             sist_log("main.c", SIST_WARNING, "Could not open original index! Disabled incremental scan feature."); |             sist_log("main.c", LOG_SIST_WARNING, "Could not open original index! Disabled incremental scan feature."); | ||||||
|             args->incremental = NULL; |             args->incremental = NULL; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -360,6 +362,15 @@ int web_args_validate(web_args_t *args, int argc, const char **argv) { | |||||||
|         args->es_index = DEFAULT_ES_INDEX; |         args->es_index = DEFAULT_ES_INDEX; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (args->lang == NULL) { | ||||||
|  |         args->lang = DEFAULT_LANG; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (strlen(args->lang) != 2) { | ||||||
|  |         fprintf(stderr, "Invalid --lang value, see usage\n"); | ||||||
|  |         return 1; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (args->credentials != NULL) { |     if (args->credentials != NULL) { | ||||||
|         char *ptr = strstr(args->credentials, ":"); |         char *ptr = strstr(args->credentials, ":"); | ||||||
|         if (ptr == NULL) { |         if (ptr == NULL) { | ||||||
| @ -418,6 +429,8 @@ int web_args_validate(web_args_t *args, int argc, const char **argv) { | |||||||
| 
 | 
 | ||||||
|     LOG_DEBUGF("cli.c", "arg es_url=%s", args->es_url) |     LOG_DEBUGF("cli.c", "arg es_url=%s", args->es_url) | ||||||
|     LOG_DEBUGF("cli.c", "arg es_index=%s", args->es_index) |     LOG_DEBUGF("cli.c", "arg es_index=%s", args->es_index) | ||||||
|  |     LOG_DEBUGF("cli.c", "arg tagline=%s", args->tagline) | ||||||
|  |     LOG_DEBUGF("cli.c", "arg dev=%d", args->dev) | ||||||
|     LOG_DEBUGF("cli.c", "arg listen=%s", args->listen_address) |     LOG_DEBUGF("cli.c", "arg listen=%s", args->listen_address) | ||||||
|     LOG_DEBUGF("cli.c", "arg credentials=%s", args->credentials) |     LOG_DEBUGF("cli.c", "arg credentials=%s", args->credentials) | ||||||
|     LOG_DEBUGF("cli.c", "arg tag_credentials=%s", args->tag_credentials) |     LOG_DEBUGF("cli.c", "arg tag_credentials=%s", args->tag_credentials) | ||||||
|  | |||||||
| @ -59,11 +59,14 @@ typedef struct web_args { | |||||||
|     char *listen_address; |     char *listen_address; | ||||||
|     char *credentials; |     char *credentials; | ||||||
|     char *tag_credentials; |     char *tag_credentials; | ||||||
|  |     char *tagline; | ||||||
|  |     char *lang; | ||||||
|     char auth_user[256]; |     char auth_user[256]; | ||||||
|     char auth_pass[256]; |     char auth_pass[256]; | ||||||
|     int auth_enabled; |     int auth_enabled; | ||||||
|     int tag_auth_enabled; |     int tag_auth_enabled; | ||||||
|     int index_count; |     int index_count; | ||||||
|  |     int dev; | ||||||
|     const char **indices; |     const char **indices; | ||||||
| } web_args_t; | } web_args_t; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,6 +1,8 @@ | |||||||
| #include "ctx.h" | #include "ctx.h" | ||||||
| 
 | 
 | ||||||
| ScanCtx_t ScanCtx; | ScanCtx_t ScanCtx = { | ||||||
|  |         .stat_index_size = 0, | ||||||
|  | }; | ||||||
| WebCtx_t WebCtx; | WebCtx_t WebCtx; | ||||||
| IndexCtx_t IndexCtx; | IndexCtx_t IndexCtx; | ||||||
| LogCtx_t LogCtx; | LogCtx_t LogCtx; | ||||||
|  | |||||||
| @ -27,6 +27,8 @@ typedef struct { | |||||||
| 
 | 
 | ||||||
|     tpool_t *pool; |     tpool_t *pool; | ||||||
| 
 | 
 | ||||||
|  |     tpool_t *writer_pool; | ||||||
|  | 
 | ||||||
|     int threads; |     int threads; | ||||||
|     int depth; |     int depth; | ||||||
| 
 | 
 | ||||||
| @ -85,7 +87,10 @@ typedef struct { | |||||||
|     char *auth_pass; |     char *auth_pass; | ||||||
|     int auth_enabled; |     int auth_enabled; | ||||||
|     int tag_auth_enabled; |     int tag_auth_enabled; | ||||||
|     struct index_t indices[64]; |     char *tagline; | ||||||
|  |     struct index_t indices[256]; | ||||||
|  |     char lang[3]; | ||||||
|  |     int dev; | ||||||
| } WebCtx_t; | } WebCtx_t; | ||||||
| 
 | 
 | ||||||
| extern ScanCtx_t ScanCtx; | extern ScanCtx_t ScanCtx; | ||||||
|  | |||||||
| @ -393,12 +393,20 @@ void elastic_init(int force_reset, const char* user_mappings, const char* user_s | |||||||
| 
 | 
 | ||||||
|         snprintf(url, sizeof(url), "%s/%s/_settings", IndexCtx.es_url, IndexCtx.es_index); |         snprintf(url, sizeof(url), "%s/%s/_settings", IndexCtx.es_url, IndexCtx.es_index); | ||||||
|         r = web_put(url, user_settings ? user_settings : settings_json); |         r = web_put(url, user_settings ? user_settings : settings_json); | ||||||
|         LOG_INFOF("elastic.c", "Update user_settings <%d>", r->status_code); |         LOG_INFOF("elastic.c", "Update ES settings <%d>", r->status_code); | ||||||
|  |         if (r->status_code != 200) { | ||||||
|  |             print_error(r); | ||||||
|  |             LOG_FATAL("elastic.c", "Could not update user settings") | ||||||
|  |         } | ||||||
|         free_response(r); |         free_response(r); | ||||||
| 
 | 
 | ||||||
|         snprintf(url, sizeof(url), "%s/%s/_mappings/_doc?include_type_name=true", IndexCtx.es_url, IndexCtx.es_index); |         snprintf(url, sizeof(url), "%s/%s/_mappings/_doc?include_type_name=true", IndexCtx.es_url, IndexCtx.es_index); | ||||||
|         r = web_put(url, user_mappings ? user_mappings : mappings_json); |         r = web_put(url, user_mappings ? user_mappings : mappings_json); | ||||||
|         LOG_INFOF("elastic.c", "Update user_mappings <%d>", r->status_code); |         LOG_INFOF("elastic.c", "Update ES mappings <%d>", r->status_code); | ||||||
|  |         if (r->status_code != 200) { | ||||||
|  |             print_error(r); | ||||||
|  |             LOG_FATAL("elastic.c", "Could not update user mappings") | ||||||
|  |         } | ||||||
|         free_response(r); |         free_response(r); | ||||||
| 
 | 
 | ||||||
|         snprintf(url, sizeof(url), "%s/%s/_open", IndexCtx.es_url, IndexCtx.es_index); |         snprintf(url, sizeof(url), "%s/%s/_open", IndexCtx.es_url, IndexCtx.es_index); | ||||||
|  | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -3,101 +3,7 @@ | |||||||
| #include "src/parsing/parse.h" | #include "src/parsing/parse.h" | ||||||
| #include "src/parsing/mime.h" | #include "src/parsing/mime.h" | ||||||
| 
 | 
 | ||||||
| static __thread int index_fd = -1; | #include <zstd.h> | ||||||
| 
 |  | ||||||
| typedef struct { |  | ||||||
|     unsigned char path_md5[MD5_DIGEST_LENGTH]; |  | ||||||
|     unsigned long size; |  | ||||||
|     unsigned int mime; |  | ||||||
|     int mtime; |  | ||||||
|     short base; |  | ||||||
|     short ext; |  | ||||||
|     char has_parent; |  | ||||||
| } line_t; |  | ||||||
| 
 |  | ||||||
| #define META_NEXT 0xFFFF |  | ||||||
| 
 |  | ||||||
| void skip_meta(FILE *file) { |  | ||||||
|     enum metakey key = 0; |  | ||||||
|     fread(&key, sizeof(uint16_t), 1, file); |  | ||||||
| 
 |  | ||||||
|     while (key != META_NEXT) { |  | ||||||
|         if (IS_META_INT(key)) { |  | ||||||
|             fseek(file, sizeof(int), SEEK_CUR); |  | ||||||
|         } else if (IS_META_LONG(key)) { |  | ||||||
|             fseek(file, sizeof(long), SEEK_CUR); |  | ||||||
|         } else { |  | ||||||
|             while ((getc(file))) {} |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         fread(&key, sizeof(uint16_t), 1, file); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void write_index_descriptor(char *path, index_descriptor_t *desc) { |  | ||||||
|     cJSON *json = cJSON_CreateObject(); |  | ||||||
|     cJSON_AddStringToObject(json, "id", desc->id); |  | ||||||
|     cJSON_AddStringToObject(json, "version", desc->version); |  | ||||||
|     cJSON_AddStringToObject(json, "root", desc->root); |  | ||||||
|     cJSON_AddStringToObject(json, "name", desc->name); |  | ||||||
|     cJSON_AddStringToObject(json, "type", desc->type); |  | ||||||
|     cJSON_AddStringToObject(json, "rewrite_url", desc->rewrite_url); |  | ||||||
|     cJSON_AddNumberToObject(json, "timestamp", (double) desc->timestamp); |  | ||||||
| 
 |  | ||||||
|     int fd = open(path, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR); |  | ||||||
|     if (fd < 0) { |  | ||||||
|         LOG_FATALF("serialize.c", "Could not open index descriptor: %s", strerror(errno)); |  | ||||||
|     } |  | ||||||
|     char *str = cJSON_Print(json); |  | ||||||
|     int ret = write(fd, str, strlen(str)); |  | ||||||
|     if (ret == -1) { |  | ||||||
|         LOG_FATALF("serialize.c", "Could not write index descriptor: %s", strerror(errno)); |  | ||||||
|     } |  | ||||||
|     free(str); |  | ||||||
|     close(fd); |  | ||||||
| 
 |  | ||||||
|     cJSON_Delete(json); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| index_descriptor_t read_index_descriptor(char *path) { |  | ||||||
| 
 |  | ||||||
|     struct stat info; |  | ||||||
|     stat(path, &info); |  | ||||||
|     int fd = open(path, O_RDONLY); |  | ||||||
| 
 |  | ||||||
|     if (fd == -1) { |  | ||||||
|         LOG_FATALF("serialize.c", "Invalid/corrupt index (Could not find descriptor): %s: %s\n", path, strerror(errno)) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     char *buf = malloc(info.st_size + 1); |  | ||||||
|     size_t ret = read(fd, buf, info.st_size); |  | ||||||
|     if (ret == -1) { |  | ||||||
|         LOG_FATALF("serialize.c", "Could not read index descriptor: %s", strerror(errno)); |  | ||||||
|     } |  | ||||||
|     *(buf + info.st_size) = '\0'; |  | ||||||
|     close(fd); |  | ||||||
| 
 |  | ||||||
|     cJSON *json = cJSON_Parse(buf); |  | ||||||
| 
 |  | ||||||
|     index_descriptor_t descriptor; |  | ||||||
|     descriptor.timestamp = (long) cJSON_GetObjectItem(json, "timestamp")->valuedouble; |  | ||||||
|     strcpy(descriptor.root, cJSON_GetObjectItem(json, "root")->valuestring); |  | ||||||
|     strcpy(descriptor.name, cJSON_GetObjectItem(json, "name")->valuestring); |  | ||||||
|     strcpy(descriptor.rewrite_url, cJSON_GetObjectItem(json, "rewrite_url")->valuestring); |  | ||||||
|     descriptor.root_len = (short) strlen(descriptor.root); |  | ||||||
|     strcpy(descriptor.version, cJSON_GetObjectItem(json, "version")->valuestring); |  | ||||||
|     strcpy(descriptor.id, cJSON_GetObjectItem(json, "id")->valuestring); |  | ||||||
|     if (cJSON_GetObjectItem(json, "type") == NULL) { |  | ||||||
|         strcpy(descriptor.type, INDEX_TYPE_BIN); |  | ||||||
|     } else { |  | ||||||
|         strcpy(descriptor.type, cJSON_GetObjectItem(json, "type")->valuestring); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     cJSON_Delete(json); |  | ||||||
|     free(buf); |  | ||||||
| 
 |  | ||||||
|     return descriptor; |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| char *get_meta_key_text(enum metakey meta_key) { | char *get_meta_key_text(enum metakey meta_key) { | ||||||
| 
 | 
 | ||||||
| @ -173,318 +79,426 @@ char *get_meta_key_text(enum metakey meta_key) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | char *build_json_string(document_t *doc) { | ||||||
|  |     cJSON *json = cJSON_CreateObject(); | ||||||
|  |     int buffer_size_guess = 8192; | ||||||
| 
 | 
 | ||||||
| void write_document(document_t *doc) { |     const char *mime_text = mime_get_mime_text(doc->mime); | ||||||
| 
 |     if (mime_text == NULL) { | ||||||
|     if (index_fd == -1) { |         cJSON_AddNullToObject(json, "mime"); | ||||||
|         char dstfile[PATH_MAX]; |     } else { | ||||||
|         pthread_t self = pthread_self(); |         cJSON_AddStringToObject(json, "mime", mime_text); | ||||||
|         snprintf(dstfile, PATH_MAX, "%s_index_%lu", ScanCtx.index.path, self); |  | ||||||
|         index_fd = open(dstfile, O_CREAT | O_WRONLY | O_APPEND, S_IRUSR | S_IWUSR); |  | ||||||
| 
 |  | ||||||
|         if (index_fd == -1) { |  | ||||||
|             perror("open"); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 |     cJSON_AddNumberToObject(json, "size", (double) doc->size); | ||||||
|     dyn_buffer_t buf = dyn_buffer_create(); |     cJSON_AddNumberToObject(json, "mtime", doc->mtime); | ||||||
| 
 | 
 | ||||||
|     // Ignore root directory in the file path
 |     // Ignore root directory in the file path
 | ||||||
|     doc->ext = (short) (doc->ext - ScanCtx.index.desc.root_len); |     doc->ext = (short) (doc->ext - ScanCtx.index.desc.root_len); | ||||||
|     doc->base = (short) (doc->base - ScanCtx.index.desc.root_len); |     doc->base = (short) (doc->base - ScanCtx.index.desc.root_len); | ||||||
|     doc->filepath += ScanCtx.index.desc.root_len; |     char *filepath = doc->filepath + ScanCtx.index.desc.root_len; | ||||||
| 
 | 
 | ||||||
|     dyn_buffer_write(&buf, doc, sizeof(line_t)); |     cJSON_AddStringToObject(json, "extension", filepath + doc->ext); | ||||||
|     dyn_buffer_write_str(&buf, doc->filepath); |  | ||||||
| 
 | 
 | ||||||
|  |     // Remove extension
 | ||||||
|  |     if (*(filepath + doc->ext - 1) == '.') { | ||||||
|  |         *(filepath + doc->ext - 1) = '\0'; | ||||||
|  |     } else { | ||||||
|  |         *(filepath + doc->ext) = '\0'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     char filepath_escaped[PATH_MAX * 3]; | ||||||
|  |     str_escape(filepath_escaped, filepath + doc->base); | ||||||
|  | 
 | ||||||
|  |     cJSON_AddStringToObject(json, "name", filepath_escaped); | ||||||
|  | 
 | ||||||
|  |     if (doc->base > 0) { | ||||||
|  |         *(filepath + doc->base - 1) = '\0'; | ||||||
|  | 
 | ||||||
|  |         str_escape(filepath_escaped, filepath); | ||||||
|  |         cJSON_AddStringToObject(json, "path", filepath_escaped); | ||||||
|  |     } else { | ||||||
|  |         cJSON_AddStringToObject(json, "path", ""); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     char md5_str[MD5_STR_LENGTH]; | ||||||
|  |     buf2hex(doc->path_md5, MD5_DIGEST_LENGTH, md5_str); | ||||||
|  |     cJSON_AddStringToObject(json, "_id", md5_str); | ||||||
|  | 
 | ||||||
|  |     // Metadata
 | ||||||
|     meta_line_t *meta = doc->meta_head; |     meta_line_t *meta = doc->meta_head; | ||||||
|     while (meta != NULL) { |     while (meta != NULL) { | ||||||
|         dyn_buffer_write_short(&buf, (uint16_t) meta->key); |  | ||||||
| 
 | 
 | ||||||
|         if (IS_META_INT(meta->key)) { |         switch (meta->key) { | ||||||
|             dyn_buffer_write_int(&buf, meta->int_val); |             case MetaPages: | ||||||
|         } else if (IS_META_LONG(meta->key)) { |             case MetaWidth: | ||||||
|             dyn_buffer_write_long(&buf, meta->long_val); |             case MetaHeight: | ||||||
|         } else { |             case MetaMediaDuration: | ||||||
|             dyn_buffer_write_str(&buf, meta->str_val); |             case MetaMediaBitrate: { | ||||||
|  |                 cJSON_AddNumberToObject(json, get_meta_key_text(meta->key), (double) meta->long_val); | ||||||
|  |                 buffer_size_guess += 20; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             case MetaMediaAudioCodec: | ||||||
|  |             case MetaMediaVideoCodec: | ||||||
|  |             case MetaContent: | ||||||
|  |             case MetaArtist: | ||||||
|  |             case MetaAlbum: | ||||||
|  |             case MetaAlbumArtist: | ||||||
|  |             case MetaGenre: | ||||||
|  |             case MetaFontName: | ||||||
|  |             case MetaParent: | ||||||
|  |             case MetaExifMake: | ||||||
|  |             case MetaExifSoftware: | ||||||
|  |             case MetaExifExposureTime: | ||||||
|  |             case MetaExifFNumber: | ||||||
|  |             case MetaExifFocalLength: | ||||||
|  |             case MetaExifUserComment: | ||||||
|  |             case MetaExifIsoSpeedRatings: | ||||||
|  |             case MetaExifDateTime: | ||||||
|  |             case MetaExifModel: | ||||||
|  |             case MetaAuthor: | ||||||
|  |             case MetaModifiedBy: | ||||||
|  |             case MetaThumbnail: | ||||||
|  |             case MetaExifGpsLongitudeDMS: | ||||||
|  |             case MetaExifGpsLongitudeDec: | ||||||
|  |             case MetaExifGpsLongitudeRef: | ||||||
|  |             case MetaExifGpsLatitudeDMS: | ||||||
|  |             case MetaExifGpsLatitudeDec: | ||||||
|  |             case MetaExifGpsLatitudeRef: | ||||||
|  |             case MetaTitle: { | ||||||
|  |                 cJSON_AddStringToObject(json, get_meta_key_text(meta->key), meta->str_val); | ||||||
|  |                 buffer_size_guess += (int) strlen(meta->str_val); | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |             default: | ||||||
|  |             LOG_FATALF("serialize.c", "Invalid meta key: %x %s", meta->key, get_meta_key_text(meta->key)) | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         meta_line_t *tmp = meta; |         meta_line_t *tmp = meta; | ||||||
|         meta = meta->next; |         meta = meta->next; | ||||||
|         free(tmp); |         free(tmp); | ||||||
|     } |     } | ||||||
|     dyn_buffer_write_short(&buf, META_NEXT); |  | ||||||
| 
 | 
 | ||||||
|     int res = write(index_fd, buf.buf, buf.cur); |     char *json_str = cJSON_PrintBuffered(json, buffer_size_guess, FALSE); | ||||||
|     if (res == -1) { |     cJSON_Delete(json); | ||||||
|         LOG_FATALF("serialize.c", "Could not write document: %s", strerror(errno)) | 
 | ||||||
|  |     return json_str; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static struct { | ||||||
|  |     FILE *out_file; | ||||||
|  |     size_t buf_out_size; | ||||||
|  | 
 | ||||||
|  |     void *buf_out; | ||||||
|  | 
 | ||||||
|  |     ZSTD_CCtx *cctx; | ||||||
|  | } WriterCtx = { | ||||||
|  |         .out_file =  NULL | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | #define ZSTD_COMPRESSION_LEVEL 10 | ||||||
|  | 
 | ||||||
|  | void initialize_writer_ctx(const char *file_path) { | ||||||
|  |     WriterCtx.out_file = fopen(file_path, "wb"); | ||||||
|  | 
 | ||||||
|  |     WriterCtx.buf_out_size = ZSTD_CStreamOutSize(); | ||||||
|  |     WriterCtx.buf_out = malloc(WriterCtx.buf_out_size); | ||||||
|  | 
 | ||||||
|  |     WriterCtx.cctx = ZSTD_createCCtx(); | ||||||
|  | 
 | ||||||
|  |     ZSTD_CCtx_setParameter(WriterCtx.cctx, ZSTD_c_compressionLevel, ZSTD_COMPRESSION_LEVEL); | ||||||
|  |     ZSTD_CCtx_setParameter(WriterCtx.cctx, ZSTD_c_checksumFlag, FALSE); | ||||||
|  | 
 | ||||||
|  |     LOG_DEBUGF("serialize.c", "Open index file for writing %s", file_path) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void zstd_write_string(const char *string, const size_t len) { | ||||||
|  |     ZSTD_inBuffer input = {string, len, 0}; | ||||||
|  | 
 | ||||||
|  |     do { | ||||||
|  |         ZSTD_outBuffer output = {WriterCtx.buf_out, WriterCtx.buf_out_size, 0}; | ||||||
|  |         ZSTD_compressStream2(WriterCtx.cctx, &output, &input, ZSTD_e_continue); | ||||||
|  | 
 | ||||||
|  |         if (output.pos > 0) { | ||||||
|  |             ScanCtx.stat_index_size += fwrite(WriterCtx.buf_out, 1, output.pos, WriterCtx.out_file); | ||||||
|  |         } | ||||||
|  |     } while (input.pos != input.size); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void write_document_func(void *arg) { | ||||||
|  | 
 | ||||||
|  |     if (WriterCtx.out_file == NULL) { | ||||||
|  |         char dstfile[PATH_MAX]; | ||||||
|  |         snprintf(dstfile, PATH_MAX, "%s_index_main.ndjson.zst", ScanCtx.index.path); | ||||||
|  |         initialize_writer_ctx(dstfile); | ||||||
|     } |     } | ||||||
|     ScanCtx.stat_index_size += buf.cur; | 
 | ||||||
|     dyn_buffer_destroy(&buf); |     document_t *doc = arg; | ||||||
|  | 
 | ||||||
|  |     char *json_str = build_json_string(doc); | ||||||
|  |     const size_t json_str_len = strlen(json_str); | ||||||
|  | 
 | ||||||
|  |     json_str = realloc(json_str, json_str_len + 1); | ||||||
|  |     *(json_str + json_str_len) = '\n'; | ||||||
|  | 
 | ||||||
|  |     zstd_write_string(json_str, json_str_len + 1); | ||||||
|  | 
 | ||||||
|  |     free(json_str); | ||||||
|  |     free(doc->filepath); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void zstd_close() { | ||||||
|  |     if (WriterCtx.out_file == NULL) { | ||||||
|  |         LOG_DEBUG("serialize.c", "No zstd stream to close, skipping cleanup") | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     size_t remaining; | ||||||
|  |     do { | ||||||
|  |         ZSTD_outBuffer output = {WriterCtx.buf_out, WriterCtx.buf_out_size, 0}; | ||||||
|  |         remaining = ZSTD_endStream(WriterCtx.cctx, &output); | ||||||
|  | 
 | ||||||
|  |         if (output.pos > 0) { | ||||||
|  |             ScanCtx.stat_index_size += fwrite(WriterCtx.buf_out, 1, output.pos, WriterCtx.out_file); | ||||||
|  |         } | ||||||
|  |     } while (remaining != 0); | ||||||
|  | 
 | ||||||
|  |     ZSTD_freeCCtx(WriterCtx.cctx); | ||||||
|  |     free(WriterCtx.buf_out); | ||||||
|  |     fclose(WriterCtx.out_file); | ||||||
|  | 
 | ||||||
|  |     LOG_DEBUG("serialize.c", "End zstd stream & close index file") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void writer_cleanup() { | ||||||
|  |     zstd_close(); | ||||||
|  |     WriterCtx.out_file = NULL; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void write_index_descriptor(char *path, index_descriptor_t *desc) { | ||||||
|  |     cJSON *json = cJSON_CreateObject(); | ||||||
|  |     cJSON_AddStringToObject(json, "id", desc->id); | ||||||
|  |     cJSON_AddStringToObject(json, "version", desc->version); | ||||||
|  |     cJSON_AddStringToObject(json, "root", desc->root); | ||||||
|  |     cJSON_AddStringToObject(json, "name", desc->name); | ||||||
|  |     cJSON_AddStringToObject(json, "type", desc->type); | ||||||
|  |     cJSON_AddStringToObject(json, "rewrite_url", desc->rewrite_url); | ||||||
|  |     cJSON_AddNumberToObject(json, "timestamp", (double) desc->timestamp); | ||||||
|  | 
 | ||||||
|  |     int fd = open(path, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR); | ||||||
|  |     if (fd < 0) { | ||||||
|  |         LOG_FATALF("serialize.c", "Could not open index descriptor: %s", strerror(errno)); | ||||||
|  |     } | ||||||
|  |     char *str = cJSON_Print(json); | ||||||
|  |     size_t ret = write(fd, str, strlen(str)); | ||||||
|  |     if (ret == -1) { | ||||||
|  |         LOG_FATALF("serialize.c", "Could not write index descriptor: %s", strerror(errno)); | ||||||
|  |     } | ||||||
|  |     free(str); | ||||||
|  |     close(fd); | ||||||
|  | 
 | ||||||
|  |     cJSON_Delete(json); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | index_descriptor_t read_index_descriptor(char *path) { | ||||||
|  | 
 | ||||||
|  |     struct stat info; | ||||||
|  |     stat(path, &info); | ||||||
|  |     int fd = open(path, O_RDONLY); | ||||||
|  | 
 | ||||||
|  |     if (fd == -1) { | ||||||
|  |         LOG_FATALF("serialize.c", "Invalid/corrupt index (Could not find descriptor): %s: %s\n", path, strerror(errno)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     char *buf = malloc(info.st_size + 1); | ||||||
|  |     size_t ret = read(fd, buf, info.st_size); | ||||||
|  |     if (ret == -1) { | ||||||
|  |         LOG_FATALF("serialize.c", "Could not read index descriptor: %s", strerror(errno)); | ||||||
|  |     } | ||||||
|  |     *(buf + info.st_size) = '\0'; | ||||||
|  |     close(fd); | ||||||
|  | 
 | ||||||
|  |     cJSON *json = cJSON_Parse(buf); | ||||||
|  | 
 | ||||||
|  |     index_descriptor_t descriptor; | ||||||
|  |     descriptor.timestamp = (long) cJSON_GetObjectItem(json, "timestamp")->valuedouble; | ||||||
|  |     strcpy(descriptor.root, cJSON_GetObjectItem(json, "root")->valuestring); | ||||||
|  |     strcpy(descriptor.name, cJSON_GetObjectItem(json, "name")->valuestring); | ||||||
|  |     strcpy(descriptor.rewrite_url, cJSON_GetObjectItem(json, "rewrite_url")->valuestring); | ||||||
|  |     descriptor.root_len = (short) strlen(descriptor.root); | ||||||
|  |     strcpy(descriptor.version, cJSON_GetObjectItem(json, "version")->valuestring); | ||||||
|  |     strcpy(descriptor.id, cJSON_GetObjectItem(json, "id")->valuestring); | ||||||
|  |     if (cJSON_GetObjectItem(json, "type") == NULL) { | ||||||
|  |         strcpy(descriptor.type, INDEX_TYPE_NDJSON); | ||||||
|  |     } else { | ||||||
|  |         strcpy(descriptor.type, cJSON_GetObjectItem(json, "type")->valuestring); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     cJSON_Delete(json); | ||||||
|  |     free(buf); | ||||||
|  | 
 | ||||||
|  |     return descriptor; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | void write_document(document_t *doc) { | ||||||
|  |     tpool_add_work(ScanCtx.writer_pool, write_document_func, doc); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void thread_cleanup() { | void thread_cleanup() { | ||||||
|     close(index_fd); |  | ||||||
|     cleanup_parse(); |     cleanup_parse(); | ||||||
|     cleanup_font(); |     cleanup_font(); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void read_index_bin_handle_line(const char *line, const char *index_id, index_func func) { | ||||||
| 
 | 
 | ||||||
| void read_index_bin(const char *path, const char *index_id, index_func func) { |     cJSON *document = cJSON_Parse(line); | ||||||
|     line_t line; |     const char *path_md5_str = cJSON_GetObjectItem(document, "_id")->valuestring; | ||||||
|     dyn_buffer_t buf = dyn_buffer_create(); |  | ||||||
| 
 | 
 | ||||||
|     FILE *file = fopen(path, "rb"); |     cJSON_AddStringToObject(document, "index", index_id); | ||||||
|     while (TRUE) { |  | ||||||
|         buf.cur = 0; |  | ||||||
|         size_t _ = fread((void *) &line, sizeof(line_t), 1, file); |  | ||||||
|         if (feof(file)) { |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         cJSON *document = cJSON_CreateObject(); |     // Load meta from sidecar files
 | ||||||
|         cJSON_AddStringToObject(document, "index", index_id); |     cJSON *meta_obj = NULL; | ||||||
|  |     if (IndexCtx.meta != NULL) { | ||||||
|  |         const char *meta_string = g_hash_table_lookup(IndexCtx.meta, path_md5_str); | ||||||
|  |         if (meta_string != NULL) { | ||||||
|  |             meta_obj = cJSON_Parse(meta_string); | ||||||
| 
 | 
 | ||||||
|         char path_md5_str[MD5_STR_LENGTH]; |             cJSON *child; | ||||||
|         buf2hex(line.path_md5, sizeof(line.path_md5), path_md5_str); |             for (child = meta_obj->child; child != NULL; child = child->next) { | ||||||
| 
 |                 char meta_key[4096]; | ||||||
|         const char *mime_text = mime_get_mime_text(line.mime); |                 strcpy(meta_key, child->string); | ||||||
|         if (mime_text == NULL) { |                 cJSON_DeleteItemFromObject(document, meta_key); | ||||||
|             cJSON_AddNullToObject(document, "mime"); |                 cJSON_AddItemReferenceToObject(document, meta_key, child); | ||||||
|         } else { |  | ||||||
|             cJSON_AddStringToObject(document, "mime", mime_get_mime_text(line.mime)); |  | ||||||
|         } |  | ||||||
|         cJSON_AddNumberToObject(document, "size", (double) line.size); |  | ||||||
|         cJSON_AddNumberToObject(document, "mtime", line.mtime); |  | ||||||
| 
 |  | ||||||
|         int c = 0; |  | ||||||
|         while ((c = getc(file)) != 0) { |  | ||||||
|             dyn_buffer_write_char(&buf, (char) c); |  | ||||||
|         } |  | ||||||
|         dyn_buffer_write_char(&buf, '\0'); |  | ||||||
| 
 |  | ||||||
|         cJSON_AddStringToObject(document, "extension", buf.buf + line.ext); |  | ||||||
|         if (*(buf.buf + line.ext - 1) == '.') { |  | ||||||
|             *(buf.buf + line.ext - 1) = '\0'; |  | ||||||
|         } else { |  | ||||||
|             *(buf.buf + line.ext) = '\0'; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         char tmp[PATH_MAX * 3]; |  | ||||||
| 
 |  | ||||||
|         str_escape(tmp, buf.buf + line.base); |  | ||||||
|         cJSON_AddStringToObject(document, "name", tmp); |  | ||||||
| 
 |  | ||||||
|         if (line.base > 0) { |  | ||||||
|             *(buf.buf + line.base - 1) = '\0'; |  | ||||||
| 
 |  | ||||||
|             str_escape(tmp, buf.buf); |  | ||||||
|             cJSON_AddStringToObject(document, "path", tmp); |  | ||||||
|         } else { |  | ||||||
|             cJSON_AddStringToObject(document, "path", ""); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         enum metakey key = 0; |  | ||||||
|         fread(&key, sizeof(uint16_t), 1, file); |  | ||||||
|         size_t ret; |  | ||||||
|         while (key != META_NEXT) { |  | ||||||
|             switch (key) { |  | ||||||
|                 case MetaPages: |  | ||||||
|                 case MetaWidth: |  | ||||||
|                 case MetaHeight: { |  | ||||||
|                     int value; |  | ||||||
|                     ret = fread(&value, sizeof(int), 1, file); |  | ||||||
|                     cJSON_AddNumberToObject(document, get_meta_key_text(key), value); |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|                 case MetaMediaDuration: |  | ||||||
|                 case MetaMediaBitrate: { |  | ||||||
|                     long value; |  | ||||||
|                     ret = fread(&value, sizeof(long), 1, file); |  | ||||||
|                     cJSON_AddNumberToObject(document, get_meta_key_text(key), (double) value); |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|                 case MetaMediaAudioCodec: |  | ||||||
|                 case MetaMediaVideoCodec: |  | ||||||
|                 case MetaContent: |  | ||||||
|                 case MetaArtist: |  | ||||||
|                 case MetaAlbum: |  | ||||||
|                 case MetaAlbumArtist: |  | ||||||
|                 case MetaGenre: |  | ||||||
|                 case MetaFontName: |  | ||||||
|                 case MetaParent: |  | ||||||
|                 case MetaExifMake: |  | ||||||
|                 case MetaExifSoftware: |  | ||||||
|                 case MetaExifExposureTime: |  | ||||||
|                 case MetaExifFNumber: |  | ||||||
|                 case MetaExifFocalLength: |  | ||||||
|                 case MetaExifUserComment: |  | ||||||
|                 case MetaExifIsoSpeedRatings: |  | ||||||
|                 case MetaExifDateTime: |  | ||||||
|                 case MetaExifModel: |  | ||||||
|                 case MetaAuthor: |  | ||||||
|                 case MetaModifiedBy: |  | ||||||
|                 case MetaThumbnail: |  | ||||||
|                 case MetaExifGpsLongitudeDMS: |  | ||||||
|                 case MetaExifGpsLongitudeDec: |  | ||||||
|                 case MetaExifGpsLongitudeRef: |  | ||||||
|                 case MetaExifGpsLatitudeDMS: |  | ||||||
|                 case MetaExifGpsLatitudeDec: |  | ||||||
|                 case MetaExifGpsLatitudeRef: |  | ||||||
|                 case MetaTitle: { |  | ||||||
|                     buf.cur = 0; |  | ||||||
|                     while ((c = getc(file)) != 0) { |  | ||||||
|                         if (SHOULD_KEEP_CHAR(c) || c == ' ') { |  | ||||||
|                             dyn_buffer_write_char(&buf, (char) c); |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                     dyn_buffer_write_char(&buf, '\0'); |  | ||||||
|                     cJSON_AddStringToObject(document, get_meta_key_text(key), buf.buf); |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|                 default: |  | ||||||
|                 LOG_FATALF("serialize.c", "Invalid meta key (corrupt index): %x", key) |  | ||||||
|             } |             } | ||||||
| 
 |  | ||||||
|             fread(&key, sizeof(uint16_t), 1, file); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         cJSON *meta_obj = NULL; |  | ||||||
|         if (IndexCtx.meta != NULL) { |  | ||||||
|             const char *meta_string = g_hash_table_lookup(IndexCtx.meta, path_md5_str); |  | ||||||
|             if (meta_string != NULL) { |  | ||||||
|                 meta_obj = cJSON_Parse(meta_string); |  | ||||||
| 
 |  | ||||||
|                 cJSON *child; |  | ||||||
|                 for (child = meta_obj->child; child != NULL; child = child->next) { |  | ||||||
|                     char meta_key[4096]; |  | ||||||
|                     strcpy(meta_key, child->string); |  | ||||||
|                     cJSON_DeleteItemFromObject(document, meta_key); |  | ||||||
|                     cJSON_AddItemReferenceToObject(document, meta_key, child); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (IndexCtx.tags != NULL) { |  | ||||||
|             const char *tags_string = g_hash_table_lookup(IndexCtx.tags, path_md5_str); |  | ||||||
|             if (tags_string != NULL) { |  | ||||||
|                 cJSON *tags_arr = cJSON_Parse(tags_string); |  | ||||||
|                 cJSON_DeleteItemFromObject(document, "tag"); |  | ||||||
|                 cJSON_AddItemToObject(document, "tag", tags_arr); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         func(document, path_md5_str); |  | ||||||
|         cJSON_Delete(document); |  | ||||||
|         if (meta_obj) { |  | ||||||
|             cJSON_Delete(meta_obj); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     dyn_buffer_destroy(&buf); | 
 | ||||||
|     fclose(file); |     // Load tags from tags DB
 | ||||||
|  |     if (IndexCtx.tags != NULL) { | ||||||
|  |         const char *tags_string = g_hash_table_lookup(IndexCtx.tags, path_md5_str); | ||||||
|  |         if (tags_string != NULL) { | ||||||
|  |             cJSON *tags_arr = cJSON_Parse(tags_string); | ||||||
|  |             cJSON_DeleteItemFromObject(document, "tag"); | ||||||
|  |             cJSON_AddItemToObject(document, "tag", tags_arr); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     func(document, path_md5_str); | ||||||
|  |     cJSON_DeleteItemFromObject(document, "_id"); | ||||||
|  |     cJSON_Delete(document); | ||||||
|  |     if (meta_obj) { | ||||||
|  |         cJSON_Delete(meta_obj); | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const char *json_type_copy_fields[] = { | void read_index_ndjson(const char *path, const char *index_id, index_func func) { | ||||||
|         "mime", "name", "path", "extension", "index", "size", "mtime", "parent", |     dyn_buffer_t buf = dyn_buffer_create(); | ||||||
| 
 | 
 | ||||||
|         // Meta
 |     // Initialize zstd things
 | ||||||
|         "title", "content", "width", "height", "duration", "audioc", "videoc", |     FILE *file = fopen(path, "rb"); | ||||||
|         "bitrate", "artist", "album", "album_artist", "genre", "title", "font_name", |  | ||||||
| 
 | 
 | ||||||
|         // Special
 |     size_t const buf_in_size = ZSTD_DStreamInSize(); | ||||||
|         "tag", "_url" |     void *const buf_in = malloc(buf_in_size); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const char *json_type_array_fields[] = { |     size_t const buf_out_size = ZSTD_DStreamOutSize(); | ||||||
|         "_keyword", "_text" |     void *const buf_out = malloc(buf_out_size); | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| void read_index_json(const char *path, UNUSED(const char *index_id), index_func func) { |     ZSTD_DCtx *const dctx = ZSTD_createDCtx(); | ||||||
| 
 | 
 | ||||||
|     FILE *file = fopen(path, "r"); |     size_t read; | ||||||
|     while (TRUE) { |     size_t last_ret = 0; | ||||||
|         char *line = NULL; |     while ((read = fread(buf_in, 1, buf_in_size, file))) { | ||||||
|         size_t len; |         ZSTD_inBuffer input = {buf_in, read, 0}; | ||||||
|         size_t read = getline(&line, &len, file); |  | ||||||
|         if (read < 0) { |  | ||||||
|             if (line) { |  | ||||||
|                 free(line); |  | ||||||
|             } |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         cJSON *input = cJSON_Parse(line); |         while (input.pos < input.size) { | ||||||
|         if (input == NULL) { |             ZSTD_outBuffer output = {buf_out, buf_out_size, 0}; | ||||||
|             LOG_FATALF("serialize.c", "Could not parse JSON line: \n%s", line) |  | ||||||
|         } |  | ||||||
|         if (line) { |  | ||||||
|             free(line); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         cJSON *document = cJSON_CreateObject(); |             size_t const ret = ZSTD_decompressStream(dctx, &output, &input); | ||||||
|         const char *id_str = cJSON_GetObjectItem(input, "_id")->valuestring; |  | ||||||
| 
 | 
 | ||||||
|         for (int i = 0; i < (sizeof(json_type_copy_fields) / sizeof(json_type_copy_fields[0])); i++) { |             for (int i = 0; i < output.pos; i++) { | ||||||
|             cJSON *value = cJSON_GetObjectItem(input, json_type_copy_fields[i]); |                 char c = ((char *) output.dst)[i]; | ||||||
|             if (value != NULL) { |  | ||||||
|                 cJSON_AddItemReferenceToObject(document, json_type_copy_fields[i], value); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         for (int i = 0; i < (sizeof(json_type_array_fields) / sizeof(json_type_array_fields[0])); i++) { |                 if (c == '\n') { | ||||||
|             cJSON *arr = cJSON_GetObjectItem(input, json_type_array_fields[i]); |                     dyn_buffer_write_char(&buf, '\0'); | ||||||
|             if (arr != NULL) { |                     read_index_bin_handle_line(buf.buf, index_id, func); | ||||||
|                 cJSON *obj; |                     buf.cur = 0; | ||||||
|                 cJSON_ArrayForEach(obj, arr) { |                 } else { | ||||||
|                     char key[1024]; |                     dyn_buffer_write_char(&buf, c); | ||||||
|                     cJSON *k = cJSON_GetObjectItem(obj, "k"); |  | ||||||
|                     cJSON *v = cJSON_GetObjectItem(obj, "v"); |  | ||||||
|                     if (k == NULL || v == NULL || !cJSON_IsString(k) || !cJSON_IsString(v)) { |  | ||||||
|                         char *str = cJSON_Print(obj); |  | ||||||
|                         LOG_FATALF("serialize.c", "Invalid %s member: must contain .k and .v string fields: \n%s", |  | ||||||
|                                    json_type_array_fields[i], str) |  | ||||||
|                     } |  | ||||||
|                     snprintf(key, sizeof(key), "%s.%s", json_type_array_fields[i], k->valuestring); |  | ||||||
|                     cJSON_AddStringToObject(document, key, v->valuestring); |  | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             last_ret = ret; | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         func(document, id_str); |  | ||||||
|         cJSON_Delete(document); |  | ||||||
|         cJSON_Delete(input); |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     if (last_ret != 0) { | ||||||
|  |         /* The last return value from ZSTD_decompressStream did not end on a
 | ||||||
|  |          * frame, but we reached the end of the file! We assume this is an | ||||||
|  |          * error, and the input was truncated. | ||||||
|  |          */ | ||||||
|  |         LOG_FATALF("serialize.c", "EOF before end of stream: %zu", last_ret) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     ZSTD_freeDCtx(dctx); | ||||||
|  |     free(buf_in); | ||||||
|  |     free(buf_out); | ||||||
|  | 
 | ||||||
|  |     dyn_buffer_destroy(&buf); | ||||||
|     fclose(file); |     fclose(file); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void read_index(const char *path, const char index_id[MD5_STR_LENGTH], const char *type, index_func func) { | void read_index(const char *path, const char index_id[MD5_STR_LENGTH], const char *type, index_func func) { | ||||||
| 
 | 
 | ||||||
|     if (strcmp(type, INDEX_TYPE_BIN) == 0) { |     if (strcmp(type, INDEX_TYPE_NDJSON) == 0) { | ||||||
|         read_index_bin(path, index_id, func); |         read_index_ndjson(path, index_id, func); | ||||||
|     } else if (strcmp(type, INDEX_TYPE_JSON) == 0) { |  | ||||||
|         read_index_json(path, index_id, func); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void incremental_read(GHashTable *table, const char *filepath) { | static __thread GHashTable *IncrementalReadTable = NULL; | ||||||
|     FILE *file = fopen(filepath, "rb"); |  | ||||||
|     line_t line; |  | ||||||
| 
 | 
 | ||||||
|     LOG_DEBUGF("serialize.c", "Incremental read %s", filepath) | void json_put_incremental(cJSON *document, UNUSED(const char id_str[MD5_STR_LENGTH])) { | ||||||
|  |     const char *path_md5_str = cJSON_GetObjectItem(document, "_id")->valuestring; | ||||||
|  |     const int mtime = cJSON_GetObjectItem(document, "mtime")->valueint; | ||||||
| 
 | 
 | ||||||
|     while (1) { |     incremental_put_str(IncrementalReadTable, path_md5_str, mtime); | ||||||
|         size_t ret = fread((void *) &line, sizeof(line_t), 1, file); | } | ||||||
|         if (ret != 1 || feof(file)) { | 
 | ||||||
|             break; | void incremental_read(GHashTable *table, const char *filepath, index_descriptor_t *desc) { | ||||||
|  |     IncrementalReadTable = table; | ||||||
|  |     read_index(filepath, desc->id, desc->type, json_put_incremental); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | static __thread GHashTable *IncrementalCopyTable = NULL; | ||||||
|  | static __thread store_t *IncrementalCopySourceStore = NULL; | ||||||
|  | static __thread store_t *IncrementalCopyDestinationStore = NULL; | ||||||
|  | 
 | ||||||
|  | void incremental_copy_handle_doc(cJSON *document, UNUSED(const char id_str[MD5_STR_LENGTH])) { | ||||||
|  | 
 | ||||||
|  |     const char *path_md5_str = cJSON_GetObjectItem(document, "_id")->valuestring; | ||||||
|  |     unsigned char path_md5[MD5_DIGEST_LENGTH]; | ||||||
|  |     hex2buf(path_md5_str, MD5_STR_LENGTH - 1, path_md5); | ||||||
|  | 
 | ||||||
|  |     if (cJSON_GetObjectItem(document, "parent") != NULL || incremental_get_str(IncrementalCopyTable, path_md5_str)) { | ||||||
|  |         // Copy index line
 | ||||||
|  |         cJSON_DeleteItemFromObject(document, "index"); | ||||||
|  |         char *json_str = cJSON_PrintUnformatted(document); | ||||||
|  |         const size_t json_str_len = strlen(json_str); | ||||||
|  | 
 | ||||||
|  |         json_str = realloc(json_str, json_str_len + 1); | ||||||
|  |         *(json_str + json_str_len) = '\n'; | ||||||
|  | 
 | ||||||
|  |         zstd_write_string(json_str, json_str_len + 1); | ||||||
|  |         free(json_str); | ||||||
|  | 
 | ||||||
|  |         // Copy tn store contents
 | ||||||
|  |         size_t buf_len; | ||||||
|  |         char *buf = store_read(IncrementalCopySourceStore, (char *) path_md5, sizeof(path_md5), &buf_len); | ||||||
|  |         if (buf_len != 0) { | ||||||
|  |             store_write(IncrementalCopyDestinationStore, (char *) path_md5, sizeof(path_md5), buf, buf_len); | ||||||
|  |             free(buf); | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         incremental_put(table, line.path_md5, line.mtime); |  | ||||||
| 
 |  | ||||||
|         while ((getc(file)) != 0) {} |  | ||||||
|         skip_meta(file); |  | ||||||
|     } |     } | ||||||
|     fclose(file); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
| @ -493,72 +507,14 @@ void incremental_read(GHashTable *table, const char *filepath) { | |||||||
|  */ |  */ | ||||||
| void incremental_copy(store_t *store, store_t *dst_store, const char *filepath, | void incremental_copy(store_t *store, store_t *dst_store, const char *filepath, | ||||||
|                       const char *dst_filepath, GHashTable *copy_table) { |                       const char *dst_filepath, GHashTable *copy_table) { | ||||||
|     FILE *file = fopen(filepath, "rb"); |  | ||||||
|     FILE *dst_file = fopen(dst_filepath, "ab"); |  | ||||||
|     line_t line; |  | ||||||
| 
 | 
 | ||||||
|     LOG_DEBUGF("serialize.c", "Incremental copy %s", filepath) |     if (WriterCtx.out_file == NULL) { | ||||||
| 
 |         initialize_writer_ctx(dst_filepath); | ||||||
|     while (TRUE) { |  | ||||||
|         size_t ret = fread((void *) &line, sizeof(line_t), 1, file); |  | ||||||
|         if (ret != 1 || feof(file)) { |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Assume that files with parents still exist.
 |  | ||||||
|         //  One way to "fix" this would be to check if the parent is marked for copy but it would consistently
 |  | ||||||
|         //  delete files with grandparents, which is a side-effect worse than having orphaned files
 |  | ||||||
|         if (line.has_parent || incremental_get(copy_table, line.path_md5)) { |  | ||||||
|             fwrite(&line, sizeof(line), 1, dst_file); |  | ||||||
| 
 |  | ||||||
|             // Copy filepath
 |  | ||||||
|             char filepath_buf[PATH_MAX]; |  | ||||||
|             char c; |  | ||||||
|             char *ptr = filepath_buf; |  | ||||||
|             while ((c = (char) getc(file))) { |  | ||||||
|                 *ptr++ = c; |  | ||||||
|             } |  | ||||||
|             *ptr = '\0'; |  | ||||||
|             fwrite(filepath_buf, (ptr - filepath_buf) + 1, 1, dst_file); |  | ||||||
| 
 |  | ||||||
|             // Copy tn store contents
 |  | ||||||
|             size_t buf_len; |  | ||||||
|             char path_md5[MD5_DIGEST_LENGTH]; |  | ||||||
|             MD5((unsigned char *) filepath_buf, (ptr - filepath_buf), (unsigned char *) path_md5); |  | ||||||
|             char *buf = store_read(store, path_md5, sizeof(path_md5), &buf_len); |  | ||||||
|             if (buf_len != 0) { |  | ||||||
|                 store_write(dst_store, path_md5, sizeof(path_md5), buf, buf_len); |  | ||||||
|                 free(buf); |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             enum metakey key = 0; |  | ||||||
|             while (1) { |  | ||||||
|                 fread(&key, sizeof(uint16_t), 1, file); |  | ||||||
|                 fwrite(&key, sizeof(uint16_t), 1, dst_file); |  | ||||||
|                 if (key == META_NEXT) { |  | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (IS_META_INT(key)) { |  | ||||||
|                     int val; |  | ||||||
|                     ret = fread(&val, sizeof(val), 1, file); |  | ||||||
|                     fwrite(&val, sizeof(val), 1, dst_file); |  | ||||||
|                 } else if (IS_META_LONG(key)) { |  | ||||||
|                     long val; |  | ||||||
|                     ret = fread(&val, sizeof(val), 1, file); |  | ||||||
|                     fwrite(&val, sizeof(val), 1, dst_file); |  | ||||||
|                 } else { |  | ||||||
|                     while ((c = (char) getc(file))) { |  | ||||||
|                         fwrite(&c, sizeof(c), 1, dst_file); |  | ||||||
|                     } |  | ||||||
|                     fwrite("\0", sizeof(c), 1, dst_file); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } else { |  | ||||||
|             while ((getc(file))) {} |  | ||||||
|             skip_meta(file); |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
|     fclose(file); | 
 | ||||||
|     fclose(dst_file); |     IncrementalCopyTable = copy_table; | ||||||
|  |     IncrementalCopySourceStore = store; | ||||||
|  |     IncrementalCopyDestinationStore = dst_store; | ||||||
|  | 
 | ||||||
|  |     read_index(filepath, "", INDEX_TYPE_NDJSON, incremental_copy_handle_doc); | ||||||
| } | } | ||||||
|  | |||||||
| @ -16,13 +16,15 @@ void write_document(document_t *doc); | |||||||
| 
 | 
 | ||||||
| void read_index(const char *path, const char[MD5_STR_LENGTH], const char *type, index_func); | void read_index(const char *path, const char[MD5_STR_LENGTH], const char *type, index_func); | ||||||
| 
 | 
 | ||||||
| void incremental_read(GHashTable *table, const char *filepath); | void incremental_read(GHashTable *table, const char *filepath, index_descriptor_t *desc); | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
|  * Must be called after write_document |  * Must be called after write_document | ||||||
|  */ |  */ | ||||||
| void thread_cleanup(); | void thread_cleanup(); | ||||||
| 
 | 
 | ||||||
|  | void writer_cleanup(); | ||||||
|  | 
 | ||||||
| void write_index_descriptor(char *path, index_descriptor_t *desc); | void write_index_descriptor(char *path, index_descriptor_t *desc); | ||||||
| 
 | 
 | ||||||
| index_descriptor_t read_index_descriptor(char *path); | index_descriptor_t read_index_descriptor(char *path); | ||||||
|  | |||||||
| @ -1,9 +1,10 @@ | |||||||
| #include "store.h" | #include "store.h" | ||||||
| #include "src/ctx.h" | #include "src/ctx.h" | ||||||
| 
 | 
 | ||||||
| store_t *store_create(char *path, size_t chunk_size) { | store_t *store_create(const char *path, size_t chunk_size) { | ||||||
| 
 |  | ||||||
|     store_t *store = malloc(sizeof(struct store_t)); |     store_t *store = malloc(sizeof(struct store_t)); | ||||||
|  |     mkdir(path, S_IWUSR | S_IRUSR | S_IXUSR); | ||||||
|  | 
 | ||||||
| #if (SIST_FAKE_STORE != 1) | #if (SIST_FAKE_STORE != 1) | ||||||
|     store->chunk_size = chunk_size; |     store->chunk_size = chunk_size; | ||||||
|     pthread_rwlock_init(&store->lock, NULL); |     pthread_rwlock_init(&store->lock, NULL); | ||||||
| @ -38,7 +39,7 @@ void store_destroy(store_t *store) { | |||||||
| 
 | 
 | ||||||
| #if (SIST_FAKE_STORE != 1) | #if (SIST_FAKE_STORE != 1) | ||||||
|     pthread_rwlock_destroy(&store->lock); |     pthread_rwlock_destroy(&store->lock); | ||||||
|     mdb_close(store->env, store->dbi); |     mdb_dbi_close(store->env, store->dbi); | ||||||
|     mdb_env_close(store->env); |     mdb_env_close(store->env); | ||||||
| #endif | #endif | ||||||
|     free(store); |     free(store); | ||||||
|  | |||||||
| @ -11,6 +11,8 @@ | |||||||
| #define STORE_SIZE_META STORE_SIZE_TAG | #define STORE_SIZE_META STORE_SIZE_TAG | ||||||
| 
 | 
 | ||||||
| typedef struct store_t { | typedef struct store_t { | ||||||
|  |     char *path; | ||||||
|  |     char *tmp_path; | ||||||
|     MDB_dbi dbi; |     MDB_dbi dbi; | ||||||
|     MDB_env *env; |     MDB_env *env; | ||||||
|     size_t size; |     size_t size; | ||||||
| @ -18,7 +20,7 @@ typedef struct store_t { | |||||||
|     pthread_rwlock_t lock; |     pthread_rwlock_t lock; | ||||||
| } store_t; | } store_t; | ||||||
| 
 | 
 | ||||||
| store_t *store_create(char *path, size_t chunk_size); | store_t *store_create(const char *path, size_t chunk_size); | ||||||
| 
 | 
 | ||||||
| void store_destroy(store_t *store); | void store_destroy(store_t *store); | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										30
									
								
								src/log.h
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								src/log.h
									
									
									
									
									
								
							| @ -4,37 +4,37 @@ | |||||||
| 
 | 
 | ||||||
| #define LOG_MAX_LENGTH 8192 | #define LOG_MAX_LENGTH 8192 | ||||||
| 
 | 
 | ||||||
| #define SIST_DEBUG 0 | #define LOG_SIST_DEBUG 0 | ||||||
| #define SIST_INFO 1 | #define LOG_SIST_INFO 1 | ||||||
| #define SIST_WARNING 2 | #define LOG_SIST_WARNING 2 | ||||||
| #define SIST_ERROR 3 | #define LOG_SIST_ERROR 3 | ||||||
| #define SIST_FATAL 4 | #define LOG_SIST_FATAL 4 | ||||||
| 
 | 
 | ||||||
| #define LOG_DEBUGF(filepath, fmt, ...) \ | #define LOG_DEBUGF(filepath, fmt, ...) \ | ||||||
|     if (LogCtx.very_verbose) {sist_logf(filepath, SIST_DEBUG, fmt, __VA_ARGS__);} |     if (LogCtx.very_verbose) {sist_logf(filepath, LOG_SIST_DEBUG, fmt, __VA_ARGS__);} | ||||||
| #define LOG_DEBUG(filepath, str) \ | #define LOG_DEBUG(filepath, str) \ | ||||||
|     if (LogCtx.very_verbose) {sist_log(filepath, SIST_DEBUG, str);} |     if (LogCtx.very_verbose) {sist_log(filepath, LOG_SIST_DEBUG, str);} | ||||||
| 
 | 
 | ||||||
| #define LOG_INFOF(filepath, fmt, ...) \ | #define LOG_INFOF(filepath, fmt, ...) \ | ||||||
|     if (LogCtx.verbose) {sist_logf(filepath, SIST_INFO, fmt, __VA_ARGS__);} |     if (LogCtx.verbose) {sist_logf(filepath, LOG_SIST_INFO, fmt, __VA_ARGS__);} | ||||||
| #define LOG_INFO(filepath, str) \ | #define LOG_INFO(filepath, str) \ | ||||||
|     if (LogCtx.verbose) {sist_log(filepath, SIST_INFO, str);} |     if (LogCtx.verbose) {sist_log(filepath, LOG_SIST_INFO, str);} | ||||||
| 
 | 
 | ||||||
| #define LOG_WARNINGF(filepath, fmt, ...) \ | #define LOG_WARNINGF(filepath, fmt, ...) \ | ||||||
|     if (LogCtx.verbose) {sist_logf(filepath, SIST_WARNING, fmt, __VA_ARGS__);} |     if (LogCtx.verbose) {sist_logf(filepath, LOG_SIST_WARNING, fmt, __VA_ARGS__);} | ||||||
| #define LOG_WARNING(filepath, str) \ | #define LOG_WARNING(filepath, str) \ | ||||||
|     if (LogCtx.verbose) {sist_log(filepath, SIST_WARNING, str);} |     if (LogCtx.verbose) {sist_log(filepath, LOG_SIST_WARNING, str);} | ||||||
| 
 | 
 | ||||||
| #define LOG_ERRORF(filepath, fmt, ...) \ | #define LOG_ERRORF(filepath, fmt, ...) \ | ||||||
|     if (LogCtx.verbose) {sist_logf(filepath, SIST_ERROR, fmt, __VA_ARGS__);} |     if (LogCtx.verbose) {sist_logf(filepath, LOG_SIST_ERROR, fmt, __VA_ARGS__);} | ||||||
| #define LOG_ERROR(filepath, str) \ | #define LOG_ERROR(filepath, str) \ | ||||||
|     if (LogCtx.verbose) {sist_log(filepath, SIST_ERROR, str);} |     if (LogCtx.verbose) {sist_log(filepath, LOG_SIST_ERROR, str);} | ||||||
| 
 | 
 | ||||||
| #define LOG_FATALF(filepath, fmt, ...) \ | #define LOG_FATALF(filepath, fmt, ...) \ | ||||||
|     sist_logf(filepath, SIST_FATAL, fmt, __VA_ARGS__);\ |     sist_logf(filepath, LOG_SIST_FATAL, fmt, __VA_ARGS__);\ | ||||||
|     exit(-1); |     exit(-1); | ||||||
| #define LOG_FATAL(filepath, str) \ | #define LOG_FATAL(filepath, str) \ | ||||||
|     sist_log(filepath, SIST_FATAL, str);\ |     sist_log(filepath, LOG_SIST_FATAL, str);\ | ||||||
|     exit(-1); |     exit(-1); | ||||||
| 
 | 
 | ||||||
| #include "sist.h" | #include "sist.h" | ||||||
|  | |||||||
							
								
								
									
										68
									
								
								src/main.c
									
									
									
									
									
								
							
							
						
						
									
										68
									
								
								src/main.c
									
									
									
									
									
								
							| @ -21,7 +21,6 @@ | |||||||
| #define EPILOG "Made by simon987 <me@simon987.net>. Released under GPL-3.0" | #define EPILOG "Made by simon987 <me@simon987.net>. Released under GPL-3.0" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| static const char *const Version = "2.10.3"; |  | ||||||
| static const char *const usage[] = { | static const char *const usage[] = { | ||||||
|         "sist2 scan [OPTION]... PATH", |         "sist2 scan [OPTION]... PATH", | ||||||
|         "sist2 index [OPTION]... INDEX", |         "sist2 index [OPTION]... INDEX", | ||||||
| @ -91,19 +90,21 @@ void sig_handler(int signum) { | |||||||
|     } else if (signum == SIGABRT && sigabrt_handler != NULL) { |     } else if (signum == SIGABRT && sigabrt_handler != NULL) { | ||||||
|         sigabrt_handler(signum); |         sigabrt_handler(signum); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     exit(-1); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void init_dir(const char *dirpath) { | void init_dir(const char *dirpath) { | ||||||
|     char path[PATH_MAX]; |     char path[PATH_MAX]; | ||||||
|     snprintf(path, PATH_MAX, "%sdescriptor.json", dirpath); |     snprintf(path, PATH_MAX, "%sdescriptor.json", dirpath); | ||||||
| 
 | 
 | ||||||
|     unsigned char index_md5[MD5_DIGEST_LENGTH]; |  | ||||||
|     MD5((unsigned char *) ScanCtx.index.desc.name, strlen(ScanCtx.index.desc.name), index_md5); |  | ||||||
|     buf2hex(index_md5, MD5_DIGEST_LENGTH, ScanCtx.index.desc.id); |  | ||||||
| 
 |  | ||||||
|     time(&ScanCtx.index.desc.timestamp); |     time(&ScanCtx.index.desc.timestamp); | ||||||
|     strcpy(ScanCtx.index.desc.version, Version); |     strcpy(ScanCtx.index.desc.version, Version); | ||||||
|     strcpy(ScanCtx.index.desc.type, INDEX_TYPE_BIN); |     strcpy(ScanCtx.index.desc.type, INDEX_TYPE_NDJSON); | ||||||
|  | 
 | ||||||
|  |     unsigned char index_md5[MD5_DIGEST_LENGTH]; | ||||||
|  |     MD5((unsigned char *) &ScanCtx.index.desc.timestamp, sizeof(ScanCtx.index.desc.timestamp), index_md5); | ||||||
|  |     buf2hex(index_md5, MD5_DIGEST_LENGTH, ScanCtx.index.desc.id); | ||||||
| 
 | 
 | ||||||
|     write_index_descriptor(path, &ScanCtx.index.desc); |     write_index_descriptor(path, &ScanCtx.index.desc); | ||||||
| } | } | ||||||
| @ -157,7 +158,11 @@ void _logf(const char *filepath, int level, char *format, ...) { | |||||||
| 
 | 
 | ||||||
| void initialize_scan_context(scan_args_t *args) { | void initialize_scan_context(scan_args_t *args) { | ||||||
| 
 | 
 | ||||||
|     // Arc
 |     ScanCtx.dbg_current_files = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, NULL); | ||||||
|  |     pthread_mutex_init(&ScanCtx.dbg_current_files_mu, NULL); | ||||||
|  |     pthread_mutex_init(&ScanCtx.dbg_file_counts_mu, NULL); | ||||||
|  | 
 | ||||||
|  |     // Archive
 | ||||||
|     ScanCtx.arc_ctx.mode = args->archive_mode; |     ScanCtx.arc_ctx.mode = args->archive_mode; | ||||||
|     ScanCtx.arc_ctx.log = _log; |     ScanCtx.arc_ctx.log = _log; | ||||||
|     ScanCtx.arc_ctx.logf = _logf; |     ScanCtx.arc_ctx.logf = _logf; | ||||||
| @ -168,11 +173,6 @@ void initialize_scan_context(scan_args_t *args) { | |||||||
|         ScanCtx.arc_ctx.passphrase[0] = 0; |         ScanCtx.arc_ctx.passphrase[0] = 0; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ScanCtx.dbg_current_files = g_hash_table_new_full(g_int64_hash, g_int64_equal, NULL, NULL); |  | ||||||
|     pthread_mutex_init(&ScanCtx.dbg_current_files_mu, NULL); |  | ||||||
| 
 |  | ||||||
|     pthread_mutex_init(&ScanCtx.dbg_file_counts_mu, NULL); |  | ||||||
| 
 |  | ||||||
|     // Comic
 |     // Comic
 | ||||||
|     ScanCtx.comic_ctx.log = _log; |     ScanCtx.comic_ctx.log = _log; | ||||||
|     ScanCtx.comic_ctx.logf = _logf; |     ScanCtx.comic_ctx.logf = _logf; | ||||||
| @ -192,6 +192,7 @@ void initialize_scan_context(scan_args_t *args) { | |||||||
|     ScanCtx.ebook_ctx.logf = _logf; |     ScanCtx.ebook_ctx.logf = _logf; | ||||||
|     ScanCtx.ebook_ctx.store = _store; |     ScanCtx.ebook_ctx.store = _store; | ||||||
|     ScanCtx.ebook_ctx.fast_epub_parse = args->fast_epub; |     ScanCtx.ebook_ctx.fast_epub_parse = args->fast_epub; | ||||||
|  |     ScanCtx.ebook_ctx.tn_qscale = args->quality; | ||||||
| 
 | 
 | ||||||
|     // Font
 |     // Font
 | ||||||
|     ScanCtx.font_ctx.enable_tn = args->size > 0; |     ScanCtx.font_ctx.enable_tn = args->size > 0; | ||||||
| @ -266,16 +267,15 @@ void load_incremental_index(const scan_args_t *args) { | |||||||
|     index_descriptor_t original_desc = read_index_descriptor(descriptor_path); |     index_descriptor_t original_desc = read_index_descriptor(descriptor_path); | ||||||
| 
 | 
 | ||||||
|     if (strcmp(original_desc.version, Version) != 0) { |     if (strcmp(original_desc.version, Version) != 0) { | ||||||
|         LOG_FATALF("main.c", "Version mismatch! Index is %s but executable is %s/%s", original_desc.version, |         LOG_FATALF("main.c", "Version mismatch! Index is %s but executable is %s", original_desc.version, Version) | ||||||
|                    Version, INDEX_VERSION_EXTERNAL) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     struct dirent *de; |     struct dirent *de; | ||||||
|     while ((de = readdir(dir)) != NULL) { |     while ((de = readdir(dir)) != NULL) { | ||||||
|         if (strncmp(de->d_name, "_index_", sizeof("_index_") - 1) == 0) { |         if (strncmp(de->d_name, "_index", sizeof("_index") - 1) == 0) { | ||||||
|             char file_path[PATH_MAX]; |             char file_path[PATH_MAX]; | ||||||
|             snprintf(file_path, PATH_MAX, "%s%s", args->incremental, de->d_name); |             snprintf(file_path, PATH_MAX, "%s%s", args->incremental, de->d_name); | ||||||
|             incremental_read(ScanCtx.original_table, file_path); |             incremental_read(ScanCtx.original_table, file_path, &original_desc); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     closedir(dir); |     closedir(dir); | ||||||
| @ -294,11 +294,9 @@ void sist2_scan(scan_args_t *args) { | |||||||
| 
 | 
 | ||||||
|     char store_path[PATH_MAX]; |     char store_path[PATH_MAX]; | ||||||
|     snprintf(store_path, PATH_MAX, "%sthumbs", ScanCtx.index.path); |     snprintf(store_path, PATH_MAX, "%sthumbs", ScanCtx.index.path); | ||||||
|     mkdir(store_path, S_IWUSR | S_IRUSR | S_IXUSR); |  | ||||||
|     ScanCtx.index.store = store_create(store_path, STORE_SIZE_TN); |     ScanCtx.index.store = store_create(store_path, STORE_SIZE_TN); | ||||||
| 
 | 
 | ||||||
|     snprintf(store_path, PATH_MAX, "%smeta", ScanCtx.index.path); |     snprintf(store_path, PATH_MAX, "%smeta", ScanCtx.index.path); | ||||||
|     mkdir(store_path, S_IWUSR | S_IRUSR | S_IXUSR); |  | ||||||
|     ScanCtx.index.meta_store = store_create(store_path, STORE_SIZE_META); |     ScanCtx.index.meta_store = store_create(store_path, STORE_SIZE_META); | ||||||
| 
 | 
 | ||||||
|     scan_print_header(); |     scan_print_header(); | ||||||
| @ -307,8 +305,12 @@ void sist2_scan(scan_args_t *args) { | |||||||
|         load_incremental_index(args); |         load_incremental_index(args); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ScanCtx.pool = tpool_create(args->threads, thread_cleanup, TRUE); |     ScanCtx.pool = tpool_create(args->threads, thread_cleanup, TRUE, TRUE); | ||||||
|     tpool_start(ScanCtx.pool); |     tpool_start(ScanCtx.pool); | ||||||
|  | 
 | ||||||
|  |     ScanCtx.writer_pool = tpool_create(1, writer_cleanup, TRUE, FALSE); | ||||||
|  |     tpool_start(ScanCtx.writer_pool); | ||||||
|  | 
 | ||||||
|     int walk_ret = walk_directory_tree(ScanCtx.index.desc.root); |     int walk_ret = walk_directory_tree(ScanCtx.index.desc.root); | ||||||
|     if (walk_ret == -1) { |     if (walk_ret == -1) { | ||||||
|         LOG_FATALF("main.c", "walk_directory_tree() failed! %s (%d)", strerror(errno), errno) |         LOG_FATALF("main.c", "walk_directory_tree() failed! %s (%d)", strerror(errno), errno) | ||||||
| @ -316,6 +318,9 @@ void sist2_scan(scan_args_t *args) { | |||||||
|     tpool_wait(ScanCtx.pool); |     tpool_wait(ScanCtx.pool); | ||||||
|     tpool_destroy(ScanCtx.pool); |     tpool_destroy(ScanCtx.pool); | ||||||
| 
 | 
 | ||||||
|  |     tpool_wait(ScanCtx.writer_pool); | ||||||
|  |     tpool_destroy(ScanCtx.writer_pool); | ||||||
|  | 
 | ||||||
|     LOG_DEBUGF("main.c", "Skipped files: %d", ScanCtx.dbg_skipped_files_count) |     LOG_DEBUGF("main.c", "Skipped files: %d", ScanCtx.dbg_skipped_files_count) | ||||||
|     LOG_DEBUGF("main.c", "Excluded files: %d", ScanCtx.dbg_excluded_files_count) |     LOG_DEBUGF("main.c", "Excluded files: %d", ScanCtx.dbg_excluded_files_count) | ||||||
|     LOG_DEBUGF("main.c", "Failed files: %d", ScanCtx.dbg_failed_files_count) |     LOG_DEBUGF("main.c", "Failed files: %d", ScanCtx.dbg_failed_files_count) | ||||||
| @ -323,7 +328,7 @@ void sist2_scan(scan_args_t *args) { | |||||||
|     if (args->incremental != NULL) { |     if (args->incremental != NULL) { | ||||||
|         char dst_path[PATH_MAX]; |         char dst_path[PATH_MAX]; | ||||||
|         snprintf(store_path, PATH_MAX, "%sthumbs", args->incremental); |         snprintf(store_path, PATH_MAX, "%sthumbs", args->incremental); | ||||||
|         snprintf(dst_path, PATH_MAX, "%s_index_original", ScanCtx.index.path); |         snprintf(dst_path, PATH_MAX, "%s_index_original.ndjson.zst", ScanCtx.index.path); | ||||||
|         store_t *source = store_create(store_path, STORE_SIZE_TN); |         store_t *source = store_create(store_path, STORE_SIZE_TN); | ||||||
| 
 | 
 | ||||||
|         DIR *dir = opendir(args->incremental); |         DIR *dir = opendir(args->incremental); | ||||||
| @ -341,10 +346,10 @@ void sist2_scan(scan_args_t *args) { | |||||||
|         } |         } | ||||||
|         closedir(dir); |         closedir(dir); | ||||||
|         store_destroy(source); |         store_destroy(source); | ||||||
|  |         writer_cleanup(); | ||||||
| 
 | 
 | ||||||
|         snprintf(store_path, PATH_MAX, "%stags", args->incremental); |         snprintf(store_path, PATH_MAX, "%stags", args->incremental); | ||||||
|         snprintf(dst_path, PATH_MAX, "%stags", ScanCtx.index.path); |         snprintf(dst_path, PATH_MAX, "%stags", ScanCtx.index.path); | ||||||
|         mkdir(store_path, S_IWUSR | S_IRUSR | S_IXUSR); |  | ||||||
|         store_t *source_tags = store_create(store_path, STORE_SIZE_TAG); |         store_t *source_tags = store_create(store_path, STORE_SIZE_TAG); | ||||||
|         store_copy(source_tags, dst_path); |         store_copy(source_tags, dst_path); | ||||||
|         store_destroy(source_tags); |         store_destroy(source_tags); | ||||||
| @ -353,6 +358,7 @@ void sist2_scan(scan_args_t *args) { | |||||||
|     generate_stats(&ScanCtx.index, args->treemap_threshold, ScanCtx.index.path); |     generate_stats(&ScanCtx.index, args->treemap_threshold, ScanCtx.index.path); | ||||||
| 
 | 
 | ||||||
|     store_destroy(ScanCtx.index.store); |     store_destroy(ScanCtx.index.store); | ||||||
|  |     store_destroy(ScanCtx.index.meta_store); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void sist2_index(index_args_t *args) { | void sist2_index(index_args_t *args) { | ||||||
| @ -372,9 +378,8 @@ void sist2_index(index_args_t *args) { | |||||||
| 
 | 
 | ||||||
|     LOG_DEBUGF("main.c", "descriptor version %s (%s)", desc.version, desc.type) |     LOG_DEBUGF("main.c", "descriptor version %s (%s)", desc.version, desc.type) | ||||||
| 
 | 
 | ||||||
|     if (strcmp(desc.version, Version) != 0 && strcmp(desc.version, INDEX_VERSION_EXTERNAL) != 0) { |     if (strcmp(desc.version, Version) != 0) { | ||||||
|         LOG_FATALF("main.c", "Version mismatch! Index is %s but executable is %s/%s", desc.version, Version, |         LOG_FATALF("main.c", "Version mismatch! Index is %s but executable is %s", desc.version, Version) | ||||||
|                    INDEX_VERSION_EXTERNAL) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     DIR *dir = opendir(args->index_path); |     DIR *dir = opendir(args->index_path); | ||||||
| @ -384,7 +389,6 @@ void sist2_index(index_args_t *args) { | |||||||
| 
 | 
 | ||||||
|     char path_tmp[PATH_MAX]; |     char path_tmp[PATH_MAX]; | ||||||
|     snprintf(path_tmp, sizeof(path_tmp), "%s/tags", args->index_path); |     snprintf(path_tmp, sizeof(path_tmp), "%s/tags", args->index_path); | ||||||
|     mkdir(path_tmp, S_IWUSR | S_IRUSR | S_IXUSR); |  | ||||||
|     IndexCtx.tag_store = store_create(path_tmp, STORE_SIZE_TAG); |     IndexCtx.tag_store = store_create(path_tmp, STORE_SIZE_TAG); | ||||||
|     IndexCtx.tags = store_read_all(IndexCtx.tag_store); |     IndexCtx.tags = store_read_all(IndexCtx.tag_store); | ||||||
| 
 | 
 | ||||||
| @ -406,7 +410,7 @@ void sist2_index(index_args_t *args) { | |||||||
|         cleanup = elastic_cleanup; |         cleanup = elastic_cleanup; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     IndexCtx.pool = tpool_create(args->threads, cleanup, FALSE); |     IndexCtx.pool = tpool_create(args->threads, cleanup, FALSE, FALSE); | ||||||
|     tpool_start(IndexCtx.pool); |     tpool_start(IndexCtx.pool); | ||||||
| 
 | 
 | ||||||
|     struct dirent *de; |     struct dirent *de; | ||||||
| @ -415,6 +419,7 @@ void sist2_index(index_args_t *args) { | |||||||
|             char file_path[PATH_MAX]; |             char file_path[PATH_MAX]; | ||||||
|             snprintf(file_path, PATH_MAX, "%s/%s", args->index_path, de->d_name); |             snprintf(file_path, PATH_MAX, "%s/%s", args->index_path, de->d_name); | ||||||
|             read_index(file_path, desc.id, desc.type, f); |             read_index(file_path, desc.id, desc.type, f); | ||||||
|  |             LOG_DEBUGF("main.c", "Read index file %s (%s)", file_path, desc.type) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     closedir(dir); |     closedir(dir); | ||||||
| @ -428,6 +433,7 @@ void sist2_index(index_args_t *args) { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     store_destroy(IndexCtx.tag_store); |     store_destroy(IndexCtx.tag_store); | ||||||
|  |     store_destroy(IndexCtx.meta_store); | ||||||
|     g_hash_table_remove_all(IndexCtx.tags); |     g_hash_table_remove_all(IndexCtx.tags); | ||||||
|     g_hash_table_destroy(IndexCtx.tags); |     g_hash_table_destroy(IndexCtx.tags); | ||||||
| } | } | ||||||
| @ -458,6 +464,9 @@ void sist2_web(web_args_t *args) { | |||||||
|     WebCtx.auth_pass = args->auth_pass; |     WebCtx.auth_pass = args->auth_pass; | ||||||
|     WebCtx.auth_enabled = args->auth_enabled; |     WebCtx.auth_enabled = args->auth_enabled; | ||||||
|     WebCtx.tag_auth_enabled = args->tag_auth_enabled; |     WebCtx.tag_auth_enabled = args->tag_auth_enabled; | ||||||
|  |     WebCtx.tagline = args->tagline; | ||||||
|  |     WebCtx.dev = args->dev; | ||||||
|  |     strcpy(WebCtx.lang, "en"); | ||||||
| 
 | 
 | ||||||
|     for (int i = 0; i < args->index_count; i++) { |     for (int i = 0; i < args->index_count; i++) { | ||||||
|         char *abs_path = abspath(args->indices[i]); |         char *abs_path = abspath(args->indices[i]); | ||||||
| @ -514,7 +523,7 @@ int main(int argc, const char *argv[]) { | |||||||
|             OPT_GROUP("Scan options"), |             OPT_GROUP("Scan options"), | ||||||
|             OPT_INTEGER('t', "threads", &common_threads, "Number of threads. DEFAULT=1"), |             OPT_INTEGER('t', "threads", &common_threads, "Number of threads. DEFAULT=1"), | ||||||
|             OPT_FLOAT('q', "quality", &scan_args->quality, |             OPT_FLOAT('q', "quality", &scan_args->quality, | ||||||
|                       "Thumbnail quality, on a scale of 1.0 to 31.0, 1.0 being the best. DEFAULT=5"), |                       "Thumbnail quality, on a scale of 1.0 to 31.0, 1.0 being the best. DEFAULT=3"), | ||||||
|             OPT_INTEGER(0, "size", &scan_args->size, |             OPT_INTEGER(0, "size", &scan_args->size, | ||||||
|                         "Thumbnail size, in pixels. Use negative value to disable. DEFAULT=500"), |                         "Thumbnail size, in pixels. Use negative value to disable. DEFAULT=500"), | ||||||
|             OPT_INTEGER(0, "content-size", &scan_args->content_size, |             OPT_INTEGER(0, "content-size", &scan_args->content_size, | ||||||
| @ -542,7 +551,8 @@ int main(int argc, const char *argv[]) { | |||||||
|                         "Maximum memory buffer size per thread in MB for files inside archives " |                         "Maximum memory buffer size per thread in MB for files inside archives " | ||||||
|                         "(see USAGE.md). DEFAULT: 2000"), |                         "(see USAGE.md). DEFAULT: 2000"), | ||||||
|             OPT_BOOLEAN(0, "read-subtitles", &scan_args->read_subtitles, "Read subtitles from media files."), |             OPT_BOOLEAN(0, "read-subtitles", &scan_args->read_subtitles, "Read subtitles from media files."), | ||||||
|             OPT_BOOLEAN(0, "fast-epub", &scan_args->fast_epub, "Faster but less accurate EPUB parsing (no thumbnails, metadata)"), |             OPT_BOOLEAN(0, "fast-epub", &scan_args->fast_epub, | ||||||
|  |                         "Faster but less accurate EPUB parsing (no thumbnails, metadata)"), | ||||||
| 
 | 
 | ||||||
|             OPT_GROUP("Index options"), |             OPT_GROUP("Index options"), | ||||||
|             OPT_INTEGER('t', "threads", &common_threads, "Number of threads. DEFAULT=1"), |             OPT_INTEGER('t', "threads", &common_threads, "Number of threads. DEFAULT=1"), | ||||||
| @ -563,6 +573,8 @@ int main(int argc, const char *argv[]) { | |||||||
|             OPT_STRING(0, "bind", &web_args->listen_address, "Listen on this address. DEFAULT=localhost:4090"), |             OPT_STRING(0, "bind", &web_args->listen_address, "Listen on this address. DEFAULT=localhost:4090"), | ||||||
|             OPT_STRING(0, "auth", &web_args->credentials, "Basic auth in user:password format"), |             OPT_STRING(0, "auth", &web_args->credentials, "Basic auth in user:password format"), | ||||||
|             OPT_STRING(0, "tag-auth", &web_args->tag_credentials, "Basic auth in user:password format for tagging"), |             OPT_STRING(0, "tag-auth", &web_args->tag_credentials, "Basic auth in user:password format for tagging"), | ||||||
|  |             OPT_STRING(0, "tagline", &web_args->tagline, "Tagline in navbar"), | ||||||
|  |             OPT_BOOLEAN(0, "dev", &web_args->dev, "Serve html & js files from disk (for development)"), | ||||||
| 
 | 
 | ||||||
|             OPT_GROUP("Exec-script options"), |             OPT_GROUP("Exec-script options"), | ||||||
|             OPT_STRING(0, "es-url", &common_es_url, "Elasticsearch url. DEFAULT=http://localhost:9200"), |             OPT_STRING(0, "es-url", &common_es_url, "Elasticsearch url. DEFAULT=http://localhost:9200"), | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ int fs_read(struct vfile *f, void *buf, size_t size) { | |||||||
|     return read(f->fd, buf, size); |     return read(f->fd, buf, size); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #define CLOSE_FILE(f) if (f.close != NULL) {f.close(&f);}; | #define CLOSE_FILE(f) if ((f).close != NULL) {(f).close(&(f));}; | ||||||
| 
 | 
 | ||||||
| void fs_close(struct vfile *f) { | void fs_close(struct vfile *f) { | ||||||
|     if (f->fd != -1) { |     if (f->fd != -1) { | ||||||
| @ -39,8 +39,6 @@ void fs_reset(struct vfile *f) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #define IS_GIT_OBJ (strlen(doc.filepath + doc.base) == 38 && (strstr(doc.filepath, "objects") != NULL)) |  | ||||||
| 
 |  | ||||||
| void set_dbg_current_file(parse_job_t *job) { | void set_dbg_current_file(parse_job_t *job) { | ||||||
|     unsigned long long pid = (unsigned long long) pthread_self(); |     unsigned long long pid = (unsigned long long) pthread_self(); | ||||||
|     pthread_mutex_lock(&ScanCtx.dbg_current_files_mu); |     pthread_mutex_lock(&ScanCtx.dbg_current_files_mu); | ||||||
| @ -51,26 +49,28 @@ void set_dbg_current_file(parse_job_t *job) { | |||||||
| void parse(void *arg) { | void parse(void *arg) { | ||||||
| 
 | 
 | ||||||
|     parse_job_t *job = arg; |     parse_job_t *job = arg; | ||||||
|     document_t doc; | 
 | ||||||
|  |     document_t *doc = malloc(sizeof(document_t)); | ||||||
|  |     doc->filepath = malloc(strlen(job->filepath) + 1); | ||||||
| 
 | 
 | ||||||
|     set_dbg_current_file(job); |     set_dbg_current_file(job); | ||||||
| 
 | 
 | ||||||
|     doc.filepath = job->filepath; |     strcpy(doc->filepath, job->filepath); | ||||||
|     doc.ext = (short) job->ext; |     doc->ext = (short) job->ext; | ||||||
|     doc.base = (short) job->base; |     doc->base = (short) job->base; | ||||||
| 
 | 
 | ||||||
|     char *rel_path = doc.filepath + ScanCtx.index.desc.root_len; |     char *rel_path = doc->filepath + ScanCtx.index.desc.root_len; | ||||||
|     MD5((unsigned char *) rel_path, strlen(rel_path), doc.path_md5); |     MD5((unsigned char *) rel_path, strlen(rel_path), doc->path_md5); | ||||||
| 
 | 
 | ||||||
|     doc.meta_head = NULL; |     doc->meta_head = NULL; | ||||||
|     doc.meta_tail = NULL; |     doc->meta_tail = NULL; | ||||||
|     doc.mime = 0; |     doc->mime = 0; | ||||||
|     doc.size = job->vfile.info.st_size; |     doc->size = job->vfile.info.st_size; | ||||||
|     doc.mtime = job->vfile.info.st_mtim.tv_sec; |     doc->mtime = job->vfile.info.st_mtim.tv_sec; | ||||||
| 
 | 
 | ||||||
|     int inc_ts = incremental_get(ScanCtx.original_table, doc.path_md5); |     int inc_ts = incremental_get(ScanCtx.original_table, doc->path_md5); | ||||||
|     if (inc_ts != 0 && inc_ts == job->vfile.info.st_mtim.tv_sec) { |     if (inc_ts != 0 && inc_ts == job->vfile.info.st_mtim.tv_sec) { | ||||||
|         incremental_mark_file_for_copy(ScanCtx.copy_table, doc.path_md5); |         incremental_mark_file_for_copy(ScanCtx.copy_table, doc->path_md5); | ||||||
| 
 | 
 | ||||||
|         pthread_mutex_lock(&ScanCtx.dbg_file_counts_mu); |         pthread_mutex_lock(&ScanCtx.dbg_file_counts_mu); | ||||||
|         ScanCtx.dbg_skipped_files_count += 1; |         ScanCtx.dbg_skipped_files_count += 1; | ||||||
| @ -83,22 +83,19 @@ void parse(void *arg) { | |||||||
| 
 | 
 | ||||||
|     if (LogCtx.very_verbose) { |     if (LogCtx.very_verbose) { | ||||||
|         char path_md5_str[MD5_STR_LENGTH]; |         char path_md5_str[MD5_STR_LENGTH]; | ||||||
|         buf2hex(doc.path_md5, MD5_DIGEST_LENGTH, path_md5_str); |         buf2hex(doc->path_md5, MD5_DIGEST_LENGTH, path_md5_str); | ||||||
|         LOG_DEBUGF(job->filepath, "Starting parse job {%s}", path_md5_str) |         LOG_DEBUGF(job->filepath, "Starting parse job {%s}", path_md5_str) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (job->vfile.info.st_size == 0) { |     if (job->vfile.info.st_size == 0) { | ||||||
|         doc.mime = MIME_EMPTY; |         doc->mime = MIME_EMPTY; | ||||||
|     } else if (*(job->filepath + job->ext) != '\0' && (job->ext - job->base != 1)) { |     } else if (*(job->filepath + job->ext) != '\0' && (job->ext - job->base != 1)) { | ||||||
|         doc.mime = mime_get_mime_by_ext(ScanCtx.ext_table, job->filepath + job->ext); |         doc->mime = mime_get_mime_by_ext(ScanCtx.ext_table, job->filepath + job->ext); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     int bytes_read = 0; |     int bytes_read = 0; | ||||||
| 
 | 
 | ||||||
|     if (doc.mime == 0 && !ScanCtx.fast) { |     if (doc->mime == 0 && !ScanCtx.fast) { | ||||||
|         if (IS_GIT_OBJ) { |  | ||||||
|             goto abort; |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         // Get mime type with libmagic
 |         // Get mime type with libmagic
 | ||||||
|         if (!job->vfile.is_fs_file) { |         if (!job->vfile.is_fs_file) { | ||||||
| @ -129,11 +126,11 @@ void parse(void *arg) { | |||||||
| 
 | 
 | ||||||
|         const char *magic_mime_str = magic_buffer(magic, buf, bytes_read); |         const char *magic_mime_str = magic_buffer(magic, buf, bytes_read); | ||||||
|         if (magic_mime_str != NULL) { |         if (magic_mime_str != NULL) { | ||||||
|             doc.mime = mime_get_mime_by_string(ScanCtx.mime_table, magic_mime_str); |             doc->mime = mime_get_mime_by_string(ScanCtx.mime_table, magic_mime_str); | ||||||
| 
 | 
 | ||||||
|             LOG_DEBUGF(job->filepath, "libmagic: %s", magic_mime_str); |             LOG_DEBUGF(job->filepath, "libmagic: %s", magic_mime_str); | ||||||
| 
 | 
 | ||||||
|             if (doc.mime == 0) { |             if (doc->mime == 0) { | ||||||
|                 LOG_WARNINGF(job->filepath, "Couldn't find mime %s", magic_mime_str); |                 LOG_WARNINGF(job->filepath, "Couldn't find mime %s", magic_mime_str); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @ -143,48 +140,48 @@ void parse(void *arg) { | |||||||
|         magic_close(magic); |         magic_close(magic); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     int mmime = MAJOR_MIME(doc.mime); |     int mmime = MAJOR_MIME(doc->mime); | ||||||
| 
 | 
 | ||||||
|     if (!(SHOULD_PARSE(doc.mime))) { |     if (!(SHOULD_PARSE(doc->mime))) { | ||||||
| 
 | 
 | ||||||
|     } else if (IS_RAW(doc.mime)) { |     } else if (IS_RAW(doc->mime)) { | ||||||
|         parse_raw(&ScanCtx.raw_ctx, &job->vfile, &doc); |         parse_raw(&ScanCtx.raw_ctx, &job->vfile, doc); | ||||||
|     } else if ((mmime == MimeVideo && doc.size >= MIN_VIDEO_SIZE) || |     } else if ((mmime == MimeVideo && doc->size >= MIN_VIDEO_SIZE) || | ||||||
|                (mmime == MimeImage && doc.size >= MIN_IMAGE_SIZE) || mmime == MimeAudio) { |                (mmime == MimeImage && doc->size >= MIN_IMAGE_SIZE) || mmime == MimeAudio) { | ||||||
| 
 | 
 | ||||||
|         parse_media(&ScanCtx.media_ctx, &job->vfile, &doc); |         parse_media(&ScanCtx.media_ctx, &job->vfile, doc); | ||||||
| 
 | 
 | ||||||
|     } else if (IS_PDF(doc.mime)) { |     } else if (IS_PDF(doc->mime)) { | ||||||
|         parse_ebook(&ScanCtx.ebook_ctx, &job->vfile, mime_get_mime_text(doc.mime), &doc); |         parse_ebook(&ScanCtx.ebook_ctx, &job->vfile, mime_get_mime_text(doc->mime), doc); | ||||||
| 
 | 
 | ||||||
|     } else if (mmime == MimeText && ScanCtx.text_ctx.content_size > 0) { |     } else if (mmime == MimeText && ScanCtx.text_ctx.content_size > 0) { | ||||||
|         if (IS_MARKUP(doc.mime)) { |         if (IS_MARKUP(doc->mime)) { | ||||||
|             parse_markup(&ScanCtx.text_ctx, &job->vfile, &doc); |             parse_markup(&ScanCtx.text_ctx, &job->vfile, doc); | ||||||
|         } else { |         } else { | ||||||
|             parse_text(&ScanCtx.text_ctx, &job->vfile, &doc); |             parse_text(&ScanCtx.text_ctx, &job->vfile, doc); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     } else if (IS_FONT(doc.mime)) { |     } else if (IS_FONT(doc->mime)) { | ||||||
|         parse_font(&ScanCtx.font_ctx, &job->vfile, &doc); |         parse_font(&ScanCtx.font_ctx, &job->vfile, doc); | ||||||
| 
 | 
 | ||||||
|     } else if ( |     } else if ( | ||||||
|             ScanCtx.arc_ctx.mode != ARC_MODE_SKIP && ( |             ScanCtx.arc_ctx.mode != ARC_MODE_SKIP && ( | ||||||
|                     IS_ARC(doc.mime) || |                     IS_ARC(doc->mime) || | ||||||
|                     (IS_ARC_FILTER(doc.mime) && should_parse_filtered_file(doc.filepath, doc.ext)) |                     (IS_ARC_FILTER(doc->mime) && should_parse_filtered_file(doc->filepath, doc->ext)) | ||||||
|             )) { |             )) { | ||||||
|         parse_archive(&ScanCtx.arc_ctx, &job->vfile, &doc); |         parse_archive(&ScanCtx.arc_ctx, &job->vfile, doc); | ||||||
|     } else if ((ScanCtx.ooxml_ctx.content_size > 0 || ScanCtx.media_ctx.tn_size > 0) && IS_DOC(doc.mime)) { |     } else if ((ScanCtx.ooxml_ctx.content_size > 0 || ScanCtx.media_ctx.tn_size > 0) && IS_DOC(doc->mime)) { | ||||||
|         parse_ooxml(&ScanCtx.ooxml_ctx, &job->vfile, &doc); |         parse_ooxml(&ScanCtx.ooxml_ctx, &job->vfile, doc); | ||||||
|     } else if (is_cbr(&ScanCtx.comic_ctx, doc.mime) || is_cbz(&ScanCtx.comic_ctx, doc.mime)) { |     } else if (is_cbr(&ScanCtx.comic_ctx, doc->mime) || is_cbz(&ScanCtx.comic_ctx, doc->mime)) { | ||||||
|         parse_comic(&ScanCtx.comic_ctx, &job->vfile, &doc); |         parse_comic(&ScanCtx.comic_ctx, &job->vfile, doc); | ||||||
|     } else if (IS_MOBI(doc.mime)) { |     } else if (IS_MOBI(doc->mime)) { | ||||||
|         parse_mobi(&ScanCtx.mobi_ctx, &job->vfile, &doc); |         parse_mobi(&ScanCtx.mobi_ctx, &job->vfile, doc); | ||||||
|     } else if (doc.mime == MIME_SIST2_SIDECAR) { |     } else if (doc->mime == MIME_SIST2_SIDECAR) { | ||||||
|         parse_sidecar(&job->vfile, &doc); |         parse_sidecar(&job->vfile, doc); | ||||||
|         CLOSE_FILE(job->vfile) |         CLOSE_FILE(job->vfile) | ||||||
|         return; |         return; | ||||||
|     } else if (is_msdoc(&ScanCtx.msdoc_ctx, doc.mime)) { |     } else if (is_msdoc(&ScanCtx.msdoc_ctx, doc->mime)) { | ||||||
|         parse_msdoc(&ScanCtx.msdoc_ctx, &job->vfile, &doc); |         parse_msdoc(&ScanCtx.msdoc_ctx, &job->vfile, doc); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     abort: |     abort: | ||||||
| @ -194,14 +191,14 @@ void parse(void *arg) { | |||||||
|         meta_line_t *meta_parent = malloc(sizeof(meta_line_t) + MD5_STR_LENGTH); |         meta_line_t *meta_parent = malloc(sizeof(meta_line_t) + MD5_STR_LENGTH); | ||||||
|         meta_parent->key = MetaParent; |         meta_parent->key = MetaParent; | ||||||
|         buf2hex(job->parent, MD5_DIGEST_LENGTH, meta_parent->str_val); |         buf2hex(job->parent, MD5_DIGEST_LENGTH, meta_parent->str_val); | ||||||
|         APPEND_META((&doc), meta_parent) |         APPEND_META((doc), meta_parent) | ||||||
| 
 | 
 | ||||||
|         doc.has_parent = TRUE; |         doc->has_parent = TRUE; | ||||||
|     } else { |     } else { | ||||||
|         doc.has_parent = FALSE; |         doc->has_parent = FALSE; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     write_document(&doc); |     write_document(doc); | ||||||
| 
 | 
 | ||||||
|     CLOSE_FILE(job->vfile) |     CLOSE_FILE(job->vfile) | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								src/sist.h
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								src/sist.h
									
									
									
									
									
								
							| @ -47,5 +47,16 @@ | |||||||
| #include <sys/types.h> | #include <sys/types.h> | ||||||
| #include <errno.h> | #include <errno.h> | ||||||
| #include <ctype.h> | #include <ctype.h> | ||||||
|  | #include "git_hash.h" | ||||||
|  | 
 | ||||||
|  | #define VERSION "2.11.0" | ||||||
|  | static const char *const Version = VERSION; | ||||||
|  | 
 | ||||||
|  | #ifndef SIST_PLATFORM | ||||||
|  | #define SIST_PLATFORM unknown | ||||||
|  | #endif | ||||||
|  | 
 | ||||||
|  | #define Q(x) #x | ||||||
|  | #define QUOTE(x) Q(x) | ||||||
| 
 | 
 | ||||||
| #endif | #endif | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								src/static/css/autocomplete.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src/static/css/autocomplete.min.css
									
									
									
									
										vendored
									
									
								
							| @ -1,4 +0,0 @@ | |||||||
| .autocomplete-suggestions { text-align: left; cursor: default; border: 1px solid #ccc; border-top: 0; background: #fff; box-shadow: -1px 1px 3px rgba(0,0,0,.1); position: absolute; display: none; z-index: 9999; max-height: 254px; overflow: hidden; overflow-y: auto; box-sizing: border-box; } |  | ||||||
| .autocomplete-suggestion { position: relative; padding: 0 .6em; line-height: 23px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 1.02em; color: #333; } |  | ||||||
| .autocomplete-suggestion b { font-weight: normal; color: #1f8dd6; } |  | ||||||
| .autocomplete-suggestion.selected { background: #f0f0f0; } |  | ||||||
							
								
								
									
										9
									
								
								src/static/css/bootstrap-colorpicker.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								src/static/css/bootstrap-colorpicker.min.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										6
									
								
								src/static/css/bootstrap.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								src/static/css/bootstrap.min.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								src/static/css/bricklayer.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/static/css/bricklayer.min.css
									
									
									
									
										vendored
									
									
								
							| @ -1 +0,0 @@ | |||||||
| .bricklayer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.bricklayer-column-sizer{width:100%;display:none}@media screen and (min-width:640px){.bricklayer-column-sizer{width:50%}}@media screen and (min-width:980px){.bricklayer-column-sizer{width:33.333%}}@media screen and (min-width:1200px){.bricklayer-column-sizer{width:25%}}.bricklayer-column{-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1;padding-left:5px;padding-right:5px} |  | ||||||
| @ -1,544 +0,0 @@ | |||||||
| *:focus { |  | ||||||
|     outline: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .info-icon { |  | ||||||
|     width: 1rem; |  | ||||||
|     margin-right: 0.2rem; |  | ||||||
|     cursor: pointer; |  | ||||||
|     line-height: 1rem; |  | ||||||
|     height: 1rem; |  | ||||||
|     background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKICAgICB2aWV3Qm94PSIwIDAgNDI2LjY2NyA0MjYuNjY3IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MjYuNjY3IDQyNi42Njc7IiBmaWxsPSIjZmZmIj4KPGc+CiAgICA8Zz4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHJlY3QgeD0iMTkyIiB5PSIxOTIiIHdpZHRoPSI0Mi42NjciIGhlaWdodD0iMTI4Ii8+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0yMTMuMzMzLDBDOTUuNDY3LDAsMCw5NS40NjcsMCwyMTMuMzMzczk1LjQ2NywyMTMuMzMzLDIxMy4zMzMsMjEzLjMzM1M0MjYuNjY3LDMzMS4yLDQyNi42NjcsMjEzLjMzMwogICAgICAgICAgICAgICAgUzMzMS4yLDAsMjEzLjMzMywweiBNMjEzLjMzMywzODRjLTk0LjA4LDAtMTcwLjY2Ny03Ni41ODctMTcwLjY2Ny0xNzAuNjY3UzExOS4yNTMsNDIuNjY3LDIxMy4zMzMsNDIuNjY3CiAgICAgICAgICAgICAgICBTMzg0LDExOS4yNTMsMzg0LDIxMy4zMzNTMzA3LjQxMywzODQsMjEzLjMzMywzODR6Ii8+CiAgICAgICAgICAgIDxyZWN0IHg9IjE5MiIgeT0iMTA2LjY2NyIgd2lkdGg9IjQyLjY2NyIgaGVpZ2h0PSI0Mi42NjciLz4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+Cg==); |  | ||||||
|     filter: brightness(65%); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .info-icon:hover { |  | ||||||
|     color: inherit; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .modal-title { |  | ||||||
|     max-width: calc(100% - 2rem); |  | ||||||
|     overflow: hidden; |  | ||||||
|     text-overflow: ellipsis; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| .path-row { |  | ||||||
|     display: -ms-flexbox; |  | ||||||
|     display: flex; |  | ||||||
|     -ms-flex-align: start; |  | ||||||
|     align-items: flex-start; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .tag-container { |  | ||||||
|     margin-left: 0.3rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .path-line { |  | ||||||
|     color: #BBB; |  | ||||||
|     text-overflow: ellipsis; |  | ||||||
|     overflow: hidden; |  | ||||||
|     white-space: nowrap; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| a { |  | ||||||
|     color: #00BCD4; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| body { |  | ||||||
|     overflow-y: scroll; |  | ||||||
|     background: black; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .progress { |  | ||||||
|     margin-top: 1em; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .card, .modal-content { |  | ||||||
|     margin-top: 1em; |  | ||||||
|     background: #212121; |  | ||||||
|     color: #e0e0e0; |  | ||||||
|     border-radius: 1px; |  | ||||||
|     border: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .table { |  | ||||||
|     color: #e0e0e0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .table td, .table th { |  | ||||||
|     border: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .table thead th { |  | ||||||
|     border-bottom: 1px solid #646464; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .modal-header .close { |  | ||||||
|     color: #e0e0e0; |  | ||||||
|     text-shadow: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .modal-header { |  | ||||||
|     border-bottom: 1px solid #646464; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .sub-document { |  | ||||||
|     background: #37474F !important; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .list-group-item.sub-document { |  | ||||||
|     border-top: 1px solid #646464 !important; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .sub-document .text-muted { |  | ||||||
|     color: #8a949c !important; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| .list-group-item { |  | ||||||
|     background: #212121; |  | ||||||
|     color: #e0e0e0; |  | ||||||
| 
 |  | ||||||
|     border-top: 1px solid #424242; |  | ||||||
|     border-bottom: none; |  | ||||||
|     border-left: none; |  | ||||||
|     border-right: none; |  | ||||||
|     padding: .25rem 0.5rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .list-group-item:first-child { |  | ||||||
|     border-top: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .navbar-brand { |  | ||||||
|     font-size: 1.75rem; |  | ||||||
|     padding: 0; |  | ||||||
|     color: #f5f5f5; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .navbar { |  | ||||||
|     background: #546b7a; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| a:hover, .btn:hover { |  | ||||||
|     color: #fff; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .navbar span { |  | ||||||
|     color: #eee; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .document { |  | ||||||
|     padding: 0.3rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .card-text:last-child { |  | ||||||
|     margin-top: -1px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .document p { |  | ||||||
|     margin-bottom: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .document:hover p { |  | ||||||
|     text-decoration: underline; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .badge-video { |  | ||||||
|     color: #FFFFFF; |  | ||||||
|     background-color: #F27761; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .badge-image { |  | ||||||
|     color: #FFFFFF; |  | ||||||
|     background-color: #AA99C9; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .badge-audio { |  | ||||||
|     color: #FFFFFF; |  | ||||||
|     background-color: #00ADEF; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .badge-resolution { |  | ||||||
|     color: #212529; |  | ||||||
|     background-color: #B0BEC5; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .badge-text { |  | ||||||
|     color: #FFFFFF; |  | ||||||
|     background-color: #FAAB3C; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .add-tag-button { |  | ||||||
|     cursor: pointer; |  | ||||||
|     color: #212529; |  | ||||||
|     background-color: #e0e0e0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .card-img-overlay { |  | ||||||
|     pointer-events: none; |  | ||||||
|     padding: 0.75rem; |  | ||||||
| 
 |  | ||||||
|     bottom: unset; |  | ||||||
|     top: 0; |  | ||||||
|     left: unset; |  | ||||||
|     right: unset; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .file-title { |  | ||||||
|     width: 100%; |  | ||||||
|     line-height: 1rem; |  | ||||||
|     height: 1.1rem; |  | ||||||
|     font-size: 10pt; |  | ||||||
|     white-space: nowrap; |  | ||||||
|     text-overflow: ellipsis; |  | ||||||
|     overflow: hidden; |  | ||||||
|     color: #00BCD4; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .badge { |  | ||||||
|     margin-right: 3px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .badge-delete { |  | ||||||
|     margin-right: -2px; |  | ||||||
|     margin-left: 2px; |  | ||||||
|     margin-top: -1px; |  | ||||||
|     font-family: monospace; |  | ||||||
|     font-size: 90%; |  | ||||||
|     background: rgba(0, 0, 0, 0.2); |  | ||||||
|     padding: 0.1em 0.4em; |  | ||||||
|     color: white; |  | ||||||
|     cursor: pointer; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .badge-user { |  | ||||||
|     color: #212529; |  | ||||||
|     background-color: #e0e0e0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .card-img-top { |  | ||||||
|     border-top-left-radius: 0; |  | ||||||
|     border-top-right-radius: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .fit { |  | ||||||
|     display: block; |  | ||||||
|     min-width: 64px; |  | ||||||
|     max-width: 100%; |  | ||||||
|     max-height: 400px; |  | ||||||
|     margin: 0 auto 0; |  | ||||||
|     width: auto; |  | ||||||
|     height: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .img-padding { |  | ||||||
|     padding: 4px 4px 0 4px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .fit-sm { |  | ||||||
|     display: block; |  | ||||||
|     max-width: 64px; |  | ||||||
|     max-height: 64px; |  | ||||||
|     margin: 0 auto; |  | ||||||
|     width: auto; |  | ||||||
|     height: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .audio-fit { |  | ||||||
|     height: 39px; |  | ||||||
|     vertical-align: bottom; |  | ||||||
|     display: inline; |  | ||||||
|     width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| @media screen and (min-width: 1800px) { |  | ||||||
|     .container { |  | ||||||
|         max-width: 1550px; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| mark { |  | ||||||
|     background: rgba(251, 191, 41, 0.25); |  | ||||||
|     border-radius: 0; |  | ||||||
|     padding: 1px 0; |  | ||||||
|     color: inherit; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .content-div mark { |  | ||||||
|     background: rgba(251, 191, 41, 0.40); |  | ||||||
|     color: white; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| .content-div { |  | ||||||
|     font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |  | ||||||
|     font-size: 13px; |  | ||||||
|     padding: 1em; |  | ||||||
|     background-color: #37474F; |  | ||||||
|     border: 1px solid #616161; |  | ||||||
|     border-radius: 4px; |  | ||||||
|     margin: 3px; |  | ||||||
|     white-space: normal; |  | ||||||
|     color: rgb(224, 224, 224); |  | ||||||
|     overflow: hidden; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .irs-single, .irs-from, .irs-to { |  | ||||||
|     font-size: 13px; |  | ||||||
|     background-color: #00BCD4; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .irs-slider { |  | ||||||
|     cursor: col-resize; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .irs { |  | ||||||
|     margin-top: 1em; |  | ||||||
|     margin-bottom: 1em; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .custom-select { |  | ||||||
|     overflow: auto; |  | ||||||
|     background-color: #37474F; |  | ||||||
|     border: 1px solid #616161; |  | ||||||
|     color: #bdbdbd; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .custom-select:focus { |  | ||||||
|     border-color: #757575; |  | ||||||
|     outline: 0; |  | ||||||
|     box-shadow: 0 0 0 .2rem rgba(0, 123, 255, .25); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| option { |  | ||||||
|     outline: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .form-control { |  | ||||||
|     background-color: #37474F; |  | ||||||
|     border: 1px solid #616161; |  | ||||||
|     color: #fff; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .form-control:focus { |  | ||||||
|     background-color: #546E7A; |  | ||||||
|     color: #fff; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .input-group-text { |  | ||||||
|     background: #263238; |  | ||||||
|     border: 1px solid #616161; |  | ||||||
|     color: #dbdbdb; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| ::placeholder { |  | ||||||
|     color: #BDBDBD !important; |  | ||||||
|     opacity: 1; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| .inspire-tree .selected > .wholerow, .inspire-tree .selected > .title-wrap:hover + .wholerow { |  | ||||||
|     background: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .inspire-tree .icon-expand::before, .inspire-tree .icon-collapse::before { |  | ||||||
|     background-color: black; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .inspire-tree .title { |  | ||||||
|     color: #eee; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .inspire-tree { |  | ||||||
|     font-weight: 400; |  | ||||||
|     font-size: 14px; |  | ||||||
|     font-family: Helvetica, Nueue, Verdana, sans-serif; |  | ||||||
|     max-height: 350px; |  | ||||||
|     overflow: auto; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .page-indicator { |  | ||||||
|     line-height: 1rem; |  | ||||||
|     padding: 0.5rem; |  | ||||||
|     background: #212121; |  | ||||||
|     color: #eee; |  | ||||||
|     margin-top: 1em; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .btn-xs { |  | ||||||
|     padding: .1rem .3rem; |  | ||||||
|     font-size: .875rem; |  | ||||||
|     border-radius: .2rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .btn { |  | ||||||
|     color: #eee; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .nav-tabs .nav-link { |  | ||||||
|     color: #e0e0e0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link.active { |  | ||||||
|     background-color: #212121; |  | ||||||
|     border-color: #616161 #616161 #212121; |  | ||||||
|     color: #e0e0e0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .nav-tabs .nav-link:focus, .nav-tabs .nav-link:focus { |  | ||||||
|     border-color: #616161 #616161 #212121; |  | ||||||
|     color: #e0e0e0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .nav-tabs .nav-link:focus, .nav-tabs .nav-link:hover { |  | ||||||
|     border-color: #e0e0e0 #e0e0e0 #212121; |  | ||||||
|     color: #e0e0e0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .nav-tabs { |  | ||||||
|     border-bottom: #616161; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .nav { |  | ||||||
|     margin-top: 0.5rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| @media (max-width: 800px) { |  | ||||||
|     #treeTabs { |  | ||||||
|         flex-basis: inherit; |  | ||||||
|         flex-grow: inherit; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .list-group { |  | ||||||
|     margin-top: 1em; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .wrapper-sm { |  | ||||||
|     min-width: 64px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .media-expanded { |  | ||||||
|     display: inherit; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .media-expanded .fit { |  | ||||||
|     max-height: 250px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| @media (max-width: 650px) { |  | ||||||
|     .media-expanded .fit { |  | ||||||
|         max-height: none; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .tagline { |  | ||||||
|         display: none; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .version { |  | ||||||
|     color: #00BCD4; |  | ||||||
|     margin-left: -18px; |  | ||||||
|     margin-top: -14px; |  | ||||||
|     font-size: 11px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| @media (min-width: 800px) { |  | ||||||
|     .small-btn { |  | ||||||
|         display: none; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .large-btn { |  | ||||||
|         display: inherit; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| @media (max-width: 801px) { |  | ||||||
|     .small-btn { |  | ||||||
|         display: inherit; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     .large-btn { |  | ||||||
|         display: none; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #searchBar { |  | ||||||
|     border-right: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #pathTree .title { |  | ||||||
|     cursor: pointer; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| svg { |  | ||||||
|     fill: white; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .play { |  | ||||||
|     position: absolute; |  | ||||||
|     width: 50px; |  | ||||||
|     height: 50px; |  | ||||||
|     left: 50%; |  | ||||||
|     top: 50%; |  | ||||||
|     transform: translate(-50%, -50%); |  | ||||||
|     pointer-events: none; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .play svg { |  | ||||||
|     fill: rgba(255, 255, 255, 0.7); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .img-wrapper:hover svg { |  | ||||||
|     fill: rgba(255, 255, 255, 1); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .pointer { |  | ||||||
|     cursor: pointer; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-card { |  | ||||||
|     text-align: center; |  | ||||||
|     margin-top: 1em; |  | ||||||
|     padding: 1em; |  | ||||||
| 
 |  | ||||||
|     box-shadow: 0 .125rem .25rem rgba(0, 0, 0, .08) !important; |  | ||||||
|     border-radius: 0; |  | ||||||
|     border: none; |  | ||||||
| 
 |  | ||||||
|     background: #212121; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .graph { |  | ||||||
|     display: inline-block; |  | ||||||
|     width: 40%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .full-screen { |  | ||||||
|     position: absolute; |  | ||||||
|     left: 0; |  | ||||||
|     width: 100%; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stats-btn { |  | ||||||
|     float: right; |  | ||||||
|     margin-bottom: 10px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #graphs-card svg text { |  | ||||||
|     fill: #eee; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .wholerow { |  | ||||||
|     outline: none !important; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .stat > .card-body { |  | ||||||
|     padding: 0.7em 1.25em; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #modal-body > .img-wrapper { |  | ||||||
|     margin-bottom: 1em; |  | ||||||
| } |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user