mirror of
				https://github.com/simon987/sist2.git
				synced 2025-11-04 01:36:51 +00:00 
			
		
		
		
	web UI rewrite, switch to ndjson.zst index format
This commit is contained in:
		
							parent
							
								
									391d8ed9d9
								
							
						
					
					
						commit
						f4e1d90a6b
					
				@ -23,3 +23,4 @@ third-party/libscan/libscan-test-files/
 | 
			
		||||
Dockerfile
 | 
			
		||||
*.idx/
 | 
			
		||||
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}/
 | 
			
		||||
      source:
 | 
			
		||||
        - ./sist2-arm64-linux
 | 
			
		||||
        - ./sist2-arm64-linux-debug
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -9,6 +9,7 @@ Makefile
 | 
			
		||||
*.out
 | 
			
		||||
LOG
 | 
			
		||||
sist2*
 | 
			
		||||
!sist2-vue/
 | 
			
		||||
index.sist2/
 | 
			
		||||
bundle*.css
 | 
			
		||||
bundle.js
 | 
			
		||||
@ -17,4 +18,9 @@ vgcore.*
 | 
			
		||||
build/
 | 
			
		||||
third-party/
 | 
			
		||||
*.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)
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
set(ARGPARSE_SHARED off)
 | 
			
		||||
add_subdirectory(third-party/argparse)
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ MAINTAINER simon987 <me@simon987.net>
 | 
			
		||||
 | 
			
		||||
WORKDIR /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 strip sist2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ MAINTAINER simon987 <me@simon987.net>
 | 
			
		||||
 | 
			
		||||
WORKDIR /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 strip sist2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,18 +2,16 @@
 | 
			
		||||
 | 
			
		||||
VCPKG_ROOT="/vcpkg"
 | 
			
		||||
 | 
			
		||||
rm *.gz &>/dev/null
 | 
			
		||||
 | 
			
		||||
git submodule update --init --recursive
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
strip sist2
 | 
			
		||||
./sist2 -v > VERSION
 | 
			
		||||
mv sist2 sist2-x64-linux
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
mv sist2_debug sist2-x64-linux-debug
 | 
			
		||||
@ -2,12 +2,16 @@
 | 
			
		||||
 | 
			
		||||
VCPKG_ROOT="/vcpkg"
 | 
			
		||||
 | 
			
		||||
rm *.gz &>/dev/null
 | 
			
		||||
 | 
			
		||||
git submodule update --init --recursive
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
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
 | 
			
		||||
    },
 | 
			
		||||
    "duration": {
 | 
			
		||||
      "type": "float",
 | 
			
		||||
      "type": "integer",
 | 
			
		||||
      "index": false
 | 
			
		||||
    },
 | 
			
		||||
    "width": {
 | 
			
		||||
@ -134,7 +134,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "tag": {
 | 
			
		||||
      "type": "keyword",
 | 
			
		||||
      "type": "text",
 | 
			
		||||
      "fielddata": true,
 | 
			
		||||
      "analyzer": "tag_analyzer",
 | 
			
		||||
      "copy_to": "suggest-tag"
 | 
			
		||||
    },
 | 
			
		||||
    "suggest-tag": {
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,12 @@
 | 
			
		||||
  "analysis": {
 | 
			
		||||
    "tokenizer": {
 | 
			
		||||
      "path_tokenizer": {
 | 
			
		||||
        "type": "path_hierarchy"
 | 
			
		||||
        "type": "path_hierarchy",
 | 
			
		||||
        "delimiter": "/"
 | 
			
		||||
      },
 | 
			
		||||
      "tag_tokenizer": {
 | 
			
		||||
        "type": "path_hierarchy",
 | 
			
		||||
        "delimiter": "."
 | 
			
		||||
      },
 | 
			
		||||
      "my_nGram_tokenizer": {
 | 
			
		||||
        "type": "nGram",
 | 
			
		||||
@ -22,6 +27,12 @@
 | 
			
		||||
          "lowercase"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "tag_analyzer": {
 | 
			
		||||
        "tokenizer": "tag_tokenizer",
 | 
			
		||||
        "filter": [
 | 
			
		||||
          "lowercase"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "case_insensitive_kw_analyzer": {
 | 
			
		||||
        "tokenizer": "keyword",
 | 
			
		||||
        "filter": [
 | 
			
		||||
 | 
			
		||||
@ -2,16 +2,9 @@
 | 
			
		||||
 | 
			
		||||
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/serve_static.py > src/web/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-lz4+json, jsonlz4
 | 
			
		||||
application/x-lz4, lz4
 | 
			
		||||
application/x-lz4, lz4
 | 
			
		||||
application/x-lzh-compressed,
 | 
			
		||||
application/x-lzh, lzh
 | 
			
		||||
application/x-lzip, lz
 | 
			
		||||
 | 
			
		||||
		
		
			
  | 
@ -1,12 +1,10 @@
 | 
			
		||||
files = [
 | 
			
		||||
    "src/static/css/bundle.css",
 | 
			
		||||
    "src/static/css/bundle_dark.css",
 | 
			
		||||
    "src/static/js/bundle.js",
 | 
			
		||||
    "src/static/js/search.js",
 | 
			
		||||
    "src/static/img/sprite-skin-flat.png",
 | 
			
		||||
    "src/static/img/sprite-skin-flat-dark.png",
 | 
			
		||||
    "src/static/search.html",
 | 
			
		||||
    "src/static/stats.html",
 | 
			
		||||
    "sist2-vue/src/assets/favicon.ico",
 | 
			
		||||
    "sist2-vue/dist/css/chunk-vendors.css",
 | 
			
		||||
    "sist2-vue/dist/css/index.css",
 | 
			
		||||
    "sist2-vue/dist/js/chunk-vendors.js",
 | 
			
		||||
    "sist2-vue/dist/js/index.js",
 | 
			
		||||
    "sist2-vue/dist/index.html",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,6 +13,10 @@ def clean(filepath):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
for file in files:
 | 
			
		||||
    with open(file, "rb") as f:
 | 
			
		||||
        data = f.read()
 | 
			
		||||
    try:
 | 
			
		||||
        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)))
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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_CONTENT_SIZE 32768
 | 
			
		||||
#define DEFAULT_QUALITY 5
 | 
			
		||||
#define DEFAULT_SIZE 500
 | 
			
		||||
#define DEFAULT_QUALITY 1
 | 
			
		||||
#define DEFAULT_SIZE 300
 | 
			
		||||
#define DEFAULT_REWRITE_URL ""
 | 
			
		||||
 | 
			
		||||
#define DEFAULT_ES_URL "http://localhost:9200"
 | 
			
		||||
#define DEFAULT_ES_INDEX "sist2"
 | 
			
		||||
#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_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) {
 | 
			
		||||
        args->incremental = abspath(args->incremental);
 | 
			
		||||
        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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@ -360,6 +362,15 @@ int web_args_validate(web_args_t *args, int argc, const char **argv) {
 | 
			
		||||
        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) {
 | 
			
		||||
        char *ptr = strstr(args->credentials, ":");
 | 
			
		||||
        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_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 credentials=%s", args->credentials)
 | 
			
		||||
    LOG_DEBUGF("cli.c", "arg tag_credentials=%s", args->tag_credentials)
 | 
			
		||||
 | 
			
		||||
@ -59,11 +59,14 @@ typedef struct web_args {
 | 
			
		||||
    char *listen_address;
 | 
			
		||||
    char *credentials;
 | 
			
		||||
    char *tag_credentials;
 | 
			
		||||
    char *tagline;
 | 
			
		||||
    char *lang;
 | 
			
		||||
    char auth_user[256];
 | 
			
		||||
    char auth_pass[256];
 | 
			
		||||
    int auth_enabled;
 | 
			
		||||
    int tag_auth_enabled;
 | 
			
		||||
    int index_count;
 | 
			
		||||
    int dev;
 | 
			
		||||
    const char **indices;
 | 
			
		||||
} web_args_t;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,8 @@
 | 
			
		||||
#include "ctx.h"
 | 
			
		||||
 | 
			
		||||
ScanCtx_t ScanCtx;
 | 
			
		||||
ScanCtx_t ScanCtx = {
 | 
			
		||||
        .stat_index_size = 0,
 | 
			
		||||
};
 | 
			
		||||
WebCtx_t WebCtx;
 | 
			
		||||
IndexCtx_t IndexCtx;
 | 
			
		||||
LogCtx_t LogCtx;
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,8 @@ typedef struct {
 | 
			
		||||
 | 
			
		||||
    tpool_t *pool;
 | 
			
		||||
 | 
			
		||||
    tpool_t *writer_pool;
 | 
			
		||||
 | 
			
		||||
    int threads;
 | 
			
		||||
    int depth;
 | 
			
		||||
 | 
			
		||||
@ -85,7 +87,10 @@ typedef struct {
 | 
			
		||||
    char *auth_pass;
 | 
			
		||||
    int 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;
 | 
			
		||||
 | 
			
		||||
extern ScanCtx_t ScanCtx;
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -3,101 +3,7 @@
 | 
			
		||||
#include "src/parsing/parse.h"
 | 
			
		||||
#include "src/parsing/mime.h"
 | 
			
		||||
 | 
			
		||||
static __thread int index_fd = -1;
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
}
 | 
			
		||||
#include <zstd.h>
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
 | 
			
		||||
    if (index_fd == -1) {
 | 
			
		||||
        char dstfile[PATH_MAX];
 | 
			
		||||
        pthread_t self = pthread_self();
 | 
			
		||||
        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");
 | 
			
		||||
        }
 | 
			
		||||
    const char *mime_text = mime_get_mime_text(doc->mime);
 | 
			
		||||
    if (mime_text == NULL) {
 | 
			
		||||
        cJSON_AddNullToObject(json, "mime");
 | 
			
		||||
    } else {
 | 
			
		||||
        cJSON_AddStringToObject(json, "mime", mime_text);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dyn_buffer_t buf = dyn_buffer_create();
 | 
			
		||||
    cJSON_AddNumberToObject(json, "size", (double) doc->size);
 | 
			
		||||
    cJSON_AddNumberToObject(json, "mtime", doc->mtime);
 | 
			
		||||
 | 
			
		||||
    // Ignore root directory in the file path
 | 
			
		||||
    doc->ext = (short) (doc->ext - 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));
 | 
			
		||||
    dyn_buffer_write_str(&buf, doc->filepath);
 | 
			
		||||
    cJSON_AddStringToObject(json, "extension", filepath + doc->ext);
 | 
			
		||||
 | 
			
		||||
    // 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;
 | 
			
		||||
    while (meta != NULL) {
 | 
			
		||||
        dyn_buffer_write_short(&buf, (uint16_t) meta->key);
 | 
			
		||||
 | 
			
		||||
        if (IS_META_INT(meta->key)) {
 | 
			
		||||
            dyn_buffer_write_int(&buf, meta->int_val);
 | 
			
		||||
        } else if (IS_META_LONG(meta->key)) {
 | 
			
		||||
            dyn_buffer_write_long(&buf, meta->long_val);
 | 
			
		||||
        } else {
 | 
			
		||||
            dyn_buffer_write_str(&buf, meta->str_val);
 | 
			
		||||
        switch (meta->key) {
 | 
			
		||||
            case MetaPages:
 | 
			
		||||
            case MetaWidth:
 | 
			
		||||
            case MetaHeight:
 | 
			
		||||
            case MetaMediaDuration:
 | 
			
		||||
            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 = meta->next;
 | 
			
		||||
        free(tmp);
 | 
			
		||||
    }
 | 
			
		||||
    dyn_buffer_write_short(&buf, META_NEXT);
 | 
			
		||||
 | 
			
		||||
    int res = write(index_fd, buf.buf, buf.cur);
 | 
			
		||||
    if (res == -1) {
 | 
			
		||||
        LOG_FATALF("serialize.c", "Could not write document: %s", strerror(errno))
 | 
			
		||||
    char *json_str = cJSON_PrintBuffered(json, buffer_size_guess, FALSE);
 | 
			
		||||
    cJSON_Delete(json);
 | 
			
		||||
 | 
			
		||||
    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() {
 | 
			
		||||
    close(index_fd);
 | 
			
		||||
    cleanup_parse();
 | 
			
		||||
    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) {
 | 
			
		||||
    line_t line;
 | 
			
		||||
    dyn_buffer_t buf = dyn_buffer_create();
 | 
			
		||||
    cJSON *document = cJSON_Parse(line);
 | 
			
		||||
    const char *path_md5_str = cJSON_GetObjectItem(document, "_id")->valuestring;
 | 
			
		||||
 | 
			
		||||
    FILE *file = fopen(path, "rb");
 | 
			
		||||
    while (TRUE) {
 | 
			
		||||
        buf.cur = 0;
 | 
			
		||||
        size_t _ = fread((void *) &line, sizeof(line_t), 1, file);
 | 
			
		||||
        if (feof(file)) {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    cJSON_AddStringToObject(document, "index", index_id);
 | 
			
		||||
 | 
			
		||||
        cJSON *document = cJSON_CreateObject();
 | 
			
		||||
        cJSON_AddStringToObject(document, "index", index_id);
 | 
			
		||||
    // Load meta from sidecar files
 | 
			
		||||
    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];
 | 
			
		||||
        buf2hex(line.path_md5, sizeof(line.path_md5), path_md5_str);
 | 
			
		||||
 | 
			
		||||
        const char *mime_text = mime_get_mime_text(line.mime);
 | 
			
		||||
        if (mime_text == NULL) {
 | 
			
		||||
            cJSON_AddNullToObject(document, "mime");
 | 
			
		||||
        } 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)
 | 
			
		||||
            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);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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[] = {
 | 
			
		||||
        "mime", "name", "path", "extension", "index", "size", "mtime", "parent",
 | 
			
		||||
void read_index_ndjson(const char *path, const char *index_id, index_func func) {
 | 
			
		||||
    dyn_buffer_t buf = dyn_buffer_create();
 | 
			
		||||
 | 
			
		||||
        // Meta
 | 
			
		||||
        "title", "content", "width", "height", "duration", "audioc", "videoc",
 | 
			
		||||
        "bitrate", "artist", "album", "album_artist", "genre", "title", "font_name",
 | 
			
		||||
    // Initialize zstd things
 | 
			
		||||
    FILE *file = fopen(path, "rb");
 | 
			
		||||
 | 
			
		||||
        // Special
 | 
			
		||||
        "tag", "_url"
 | 
			
		||||
};
 | 
			
		||||
    size_t const buf_in_size = ZSTD_DStreamInSize();
 | 
			
		||||
    void *const buf_in = malloc(buf_in_size);
 | 
			
		||||
 | 
			
		||||
const char *json_type_array_fields[] = {
 | 
			
		||||
        "_keyword", "_text"
 | 
			
		||||
};
 | 
			
		||||
    size_t const buf_out_size = ZSTD_DStreamOutSize();
 | 
			
		||||
    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");
 | 
			
		||||
    while (TRUE) {
 | 
			
		||||
        char *line = NULL;
 | 
			
		||||
        size_t len;
 | 
			
		||||
        size_t read = getline(&line, &len, file);
 | 
			
		||||
        if (read < 0) {
 | 
			
		||||
            if (line) {
 | 
			
		||||
                free(line);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    size_t read;
 | 
			
		||||
    size_t last_ret = 0;
 | 
			
		||||
    while ((read = fread(buf_in, 1, buf_in_size, file))) {
 | 
			
		||||
        ZSTD_inBuffer input = {buf_in, read, 0};
 | 
			
		||||
 | 
			
		||||
        cJSON *input = cJSON_Parse(line);
 | 
			
		||||
        if (input == NULL) {
 | 
			
		||||
            LOG_FATALF("serialize.c", "Could not parse JSON line: \n%s", line)
 | 
			
		||||
        }
 | 
			
		||||
        if (line) {
 | 
			
		||||
            free(line);
 | 
			
		||||
        }
 | 
			
		||||
        while (input.pos < input.size) {
 | 
			
		||||
            ZSTD_outBuffer output = {buf_out, buf_out_size, 0};
 | 
			
		||||
 | 
			
		||||
        cJSON *document = cJSON_CreateObject();
 | 
			
		||||
        const char *id_str = cJSON_GetObjectItem(input, "_id")->valuestring;
 | 
			
		||||
            size_t const ret = ZSTD_decompressStream(dctx, &output, &input);
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < (sizeof(json_type_copy_fields) / sizeof(json_type_copy_fields[0])); i++) {
 | 
			
		||||
            cJSON *value = cJSON_GetObjectItem(input, json_type_copy_fields[i]);
 | 
			
		||||
            if (value != NULL) {
 | 
			
		||||
                cJSON_AddItemReferenceToObject(document, json_type_copy_fields[i], value);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
            for (int i = 0; i < output.pos; i++) {
 | 
			
		||||
                char c = ((char *) output.dst)[i];
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < (sizeof(json_type_array_fields) / sizeof(json_type_array_fields[0])); i++) {
 | 
			
		||||
            cJSON *arr = cJSON_GetObjectItem(input, json_type_array_fields[i]);
 | 
			
		||||
            if (arr != NULL) {
 | 
			
		||||
                cJSON *obj;
 | 
			
		||||
                cJSON_ArrayForEach(obj, arr) {
 | 
			
		||||
                    char key[1024];
 | 
			
		||||
                    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);
 | 
			
		||||
                if (c == '\n') {
 | 
			
		||||
                    dyn_buffer_write_char(&buf, '\0');
 | 
			
		||||
                    read_index_bin_handle_line(buf.buf, index_id, func);
 | 
			
		||||
                    buf.cur = 0;
 | 
			
		||||
                } else {
 | 
			
		||||
                    dyn_buffer_write_char(&buf, c);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
        read_index_bin(path, index_id, func);
 | 
			
		||||
    } else if (strcmp(type, INDEX_TYPE_JSON) == 0) {
 | 
			
		||||
        read_index_json(path, index_id, func);
 | 
			
		||||
    if (strcmp(type, INDEX_TYPE_NDJSON) == 0) {
 | 
			
		||||
        read_index_ndjson(path, index_id, func);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void incremental_read(GHashTable *table, const char *filepath) {
 | 
			
		||||
    FILE *file = fopen(filepath, "rb");
 | 
			
		||||
    line_t line;
 | 
			
		||||
static __thread GHashTable *IncrementalReadTable = NULL;
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
        size_t ret = fread((void *) &line, sizeof(line_t), 1, file);
 | 
			
		||||
        if (ret != 1 || feof(file)) {
 | 
			
		||||
            break;
 | 
			
		||||
    incremental_put_str(IncrementalReadTable, path_md5_str, mtime);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
                      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)
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
        }
 | 
			
		||||
    if (WriterCtx.out_file == NULL) {
 | 
			
		||||
        initialize_writer_ctx(dst_filepath);
 | 
			
		||||
    }
 | 
			
		||||
    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 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
 | 
			
		||||
 */
 | 
			
		||||
void thread_cleanup();
 | 
			
		||||
 | 
			
		||||
void writer_cleanup();
 | 
			
		||||
 | 
			
		||||
void write_index_descriptor(char *path, index_descriptor_t *desc);
 | 
			
		||||
 | 
			
		||||
index_descriptor_t read_index_descriptor(char *path);
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,10 @@
 | 
			
		||||
#include "store.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));
 | 
			
		||||
    mkdir(path, S_IWUSR | S_IRUSR | S_IXUSR);
 | 
			
		||||
 | 
			
		||||
#if (SIST_FAKE_STORE != 1)
 | 
			
		||||
    store->chunk_size = chunk_size;
 | 
			
		||||
    pthread_rwlock_init(&store->lock, NULL);
 | 
			
		||||
@ -38,7 +39,7 @@ void store_destroy(store_t *store) {
 | 
			
		||||
 | 
			
		||||
#if (SIST_FAKE_STORE != 1)
 | 
			
		||||
    pthread_rwlock_destroy(&store->lock);
 | 
			
		||||
    mdb_close(store->env, store->dbi);
 | 
			
		||||
    mdb_dbi_close(store->env, store->dbi);
 | 
			
		||||
    mdb_env_close(store->env);
 | 
			
		||||
#endif
 | 
			
		||||
    free(store);
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,8 @@
 | 
			
		||||
#define STORE_SIZE_META STORE_SIZE_TAG
 | 
			
		||||
 | 
			
		||||
typedef struct store_t {
 | 
			
		||||
    char *path;
 | 
			
		||||
    char *tmp_path;
 | 
			
		||||
    MDB_dbi dbi;
 | 
			
		||||
    MDB_env *env;
 | 
			
		||||
    size_t size;
 | 
			
		||||
@ -18,7 +20,7 @@ typedef struct store_t {
 | 
			
		||||
    pthread_rwlock_t lock;
 | 
			
		||||
} 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);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										30
									
								
								src/log.h
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								src/log.h
									
									
									
									
									
								
							@ -4,37 +4,37 @@
 | 
			
		||||
 | 
			
		||||
#define LOG_MAX_LENGTH 8192
 | 
			
		||||
 | 
			
		||||
#define SIST_DEBUG 0
 | 
			
		||||
#define SIST_INFO 1
 | 
			
		||||
#define SIST_WARNING 2
 | 
			
		||||
#define SIST_ERROR 3
 | 
			
		||||
#define SIST_FATAL 4
 | 
			
		||||
#define LOG_SIST_DEBUG 0
 | 
			
		||||
#define LOG_SIST_INFO 1
 | 
			
		||||
#define LOG_SIST_WARNING 2
 | 
			
		||||
#define LOG_SIST_ERROR 3
 | 
			
		||||
#define LOG_SIST_FATAL 4
 | 
			
		||||
 | 
			
		||||
#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) \
 | 
			
		||||
    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, ...) \
 | 
			
		||||
    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) \
 | 
			
		||||
    if (LogCtx.verbose) {sist_log(filepath, SIST_INFO, str);}
 | 
			
		||||
    if (LogCtx.verbose) {sist_log(filepath, LOG_SIST_INFO, str);}
 | 
			
		||||
 | 
			
		||||
#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) \
 | 
			
		||||
    if (LogCtx.verbose) {sist_log(filepath, SIST_WARNING, str);}
 | 
			
		||||
    if (LogCtx.verbose) {sist_log(filepath, LOG_SIST_WARNING, str);}
 | 
			
		||||
 | 
			
		||||
#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) \
 | 
			
		||||
    if (LogCtx.verbose) {sist_log(filepath, SIST_ERROR, str);}
 | 
			
		||||
    if (LogCtx.verbose) {sist_log(filepath, LOG_SIST_ERROR, str);}
 | 
			
		||||
 | 
			
		||||
#define LOG_FATALF(filepath, fmt, ...) \
 | 
			
		||||
    sist_logf(filepath, SIST_FATAL, fmt, __VA_ARGS__);\
 | 
			
		||||
    sist_logf(filepath, LOG_SIST_FATAL, fmt, __VA_ARGS__);\
 | 
			
		||||
    exit(-1);
 | 
			
		||||
#define LOG_FATAL(filepath, str) \
 | 
			
		||||
    sist_log(filepath, SIST_FATAL, str);\
 | 
			
		||||
    sist_log(filepath, LOG_SIST_FATAL, str);\
 | 
			
		||||
    exit(-1);
 | 
			
		||||
 | 
			
		||||
#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"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
static const char *const Version = "2.10.3";
 | 
			
		||||
static const char *const usage[] = {
 | 
			
		||||
        "sist2 scan [OPTION]... PATH",
 | 
			
		||||
        "sist2 index [OPTION]... INDEX",
 | 
			
		||||
@ -91,19 +90,21 @@ void sig_handler(int signum) {
 | 
			
		||||
    } else if (signum == SIGABRT && sigabrt_handler != NULL) {
 | 
			
		||||
        sigabrt_handler(signum);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    exit(-1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void init_dir(const char *dirpath) {
 | 
			
		||||
    char path[PATH_MAX];
 | 
			
		||||
    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);
 | 
			
		||||
    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);
 | 
			
		||||
}
 | 
			
		||||
@ -157,7 +158,11 @@ void _logf(const char *filepath, int level, char *format, ...) {
 | 
			
		||||
 | 
			
		||||
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.log = _log;
 | 
			
		||||
    ScanCtx.arc_ctx.logf = _logf;
 | 
			
		||||
@ -168,11 +173,6 @@ void initialize_scan_context(scan_args_t *args) {
 | 
			
		||||
        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
 | 
			
		||||
    ScanCtx.comic_ctx.log = _log;
 | 
			
		||||
    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.store = _store;
 | 
			
		||||
    ScanCtx.ebook_ctx.fast_epub_parse = args->fast_epub;
 | 
			
		||||
    ScanCtx.ebook_ctx.tn_qscale = args->quality;
 | 
			
		||||
 | 
			
		||||
    // Font
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    if (strcmp(original_desc.version, Version) != 0) {
 | 
			
		||||
        LOG_FATALF("main.c", "Version mismatch! Index is %s but executable is %s/%s", original_desc.version,
 | 
			
		||||
                   Version, INDEX_VERSION_EXTERNAL)
 | 
			
		||||
        LOG_FATALF("main.c", "Version mismatch! Index is %s but executable is %s", original_desc.version, Version)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    struct dirent *de;
 | 
			
		||||
    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];
 | 
			
		||||
            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);
 | 
			
		||||
@ -294,11 +294,9 @@ void sist2_scan(scan_args_t *args) {
 | 
			
		||||
 | 
			
		||||
    char store_path[PATH_MAX];
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    scan_print_header();
 | 
			
		||||
@ -307,8 +305,12 @@ void sist2_scan(scan_args_t *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);
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
    if (walk_ret == -1) {
 | 
			
		||||
        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_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", "Excluded files: %d", ScanCtx.dbg_excluded_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) {
 | 
			
		||||
        char dst_path[PATH_MAX];
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
        DIR *dir = opendir(args->incremental);
 | 
			
		||||
@ -341,10 +346,10 @@ void sist2_scan(scan_args_t *args) {
 | 
			
		||||
        }
 | 
			
		||||
        closedir(dir);
 | 
			
		||||
        store_destroy(source);
 | 
			
		||||
        writer_cleanup();
 | 
			
		||||
 | 
			
		||||
        snprintf(store_path, PATH_MAX, "%stags", args->incremental);
 | 
			
		||||
        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_copy(source_tags, dst_path);
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
    store_destroy(ScanCtx.index.store);
 | 
			
		||||
    store_destroy(ScanCtx.index.meta_store);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
    if (strcmp(desc.version, Version) != 0 && strcmp(desc.version, INDEX_VERSION_EXTERNAL) != 0) {
 | 
			
		||||
        LOG_FATALF("main.c", "Version mismatch! Index is %s but executable is %s/%s", desc.version, Version,
 | 
			
		||||
                   INDEX_VERSION_EXTERNAL)
 | 
			
		||||
    if (strcmp(desc.version, Version) != 0) {
 | 
			
		||||
        LOG_FATALF("main.c", "Version mismatch! Index is %s but executable is %s", desc.version, Version)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    DIR *dir = opendir(args->index_path);
 | 
			
		||||
@ -384,7 +389,6 @@ void sist2_index(index_args_t *args) {
 | 
			
		||||
 | 
			
		||||
    char path_tmp[PATH_MAX];
 | 
			
		||||
    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.tags = store_read_all(IndexCtx.tag_store);
 | 
			
		||||
 | 
			
		||||
@ -406,7 +410,7 @@ void sist2_index(index_args_t *args) {
 | 
			
		||||
        cleanup = elastic_cleanup;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    IndexCtx.pool = tpool_create(args->threads, cleanup, FALSE);
 | 
			
		||||
    IndexCtx.pool = tpool_create(args->threads, cleanup, FALSE, FALSE);
 | 
			
		||||
    tpool_start(IndexCtx.pool);
 | 
			
		||||
 | 
			
		||||
    struct dirent *de;
 | 
			
		||||
@ -415,6 +419,7 @@ void sist2_index(index_args_t *args) {
 | 
			
		||||
            char file_path[PATH_MAX];
 | 
			
		||||
            snprintf(file_path, PATH_MAX, "%s/%s", args->index_path, de->d_name);
 | 
			
		||||
            read_index(file_path, desc.id, desc.type, f);
 | 
			
		||||
            LOG_DEBUGF("main.c", "Read index file %s (%s)", file_path, desc.type)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    closedir(dir);
 | 
			
		||||
@ -428,6 +433,7 @@ void sist2_index(index_args_t *args) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    store_destroy(IndexCtx.tag_store);
 | 
			
		||||
    store_destroy(IndexCtx.meta_store);
 | 
			
		||||
    g_hash_table_remove_all(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_enabled = args->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++) {
 | 
			
		||||
        char *abs_path = abspath(args->indices[i]);
 | 
			
		||||
@ -514,7 +523,7 @@ int main(int argc, const char *argv[]) {
 | 
			
		||||
            OPT_GROUP("Scan options"),
 | 
			
		||||
            OPT_INTEGER('t', "threads", &common_threads, "Number of threads. DEFAULT=1"),
 | 
			
		||||
            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,
 | 
			
		||||
                        "Thumbnail size, in pixels. Use negative value to disable. DEFAULT=500"),
 | 
			
		||||
            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 "
 | 
			
		||||
                        "(see USAGE.md). DEFAULT: 2000"),
 | 
			
		||||
            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_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, "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, "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_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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#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) {
 | 
			
		||||
    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) {
 | 
			
		||||
    unsigned long long pid = (unsigned long long) pthread_self();
 | 
			
		||||
    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) {
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    doc.filepath = job->filepath;
 | 
			
		||||
    doc.ext = (short) job->ext;
 | 
			
		||||
    doc.base = (short) job->base;
 | 
			
		||||
    strcpy(doc->filepath, job->filepath);
 | 
			
		||||
    doc->ext = (short) job->ext;
 | 
			
		||||
    doc->base = (short) job->base;
 | 
			
		||||
 | 
			
		||||
    char *rel_path = doc.filepath + ScanCtx.index.desc.root_len;
 | 
			
		||||
    MD5((unsigned char *) rel_path, strlen(rel_path), doc.path_md5);
 | 
			
		||||
    char *rel_path = doc->filepath + ScanCtx.index.desc.root_len;
 | 
			
		||||
    MD5((unsigned char *) rel_path, strlen(rel_path), doc->path_md5);
 | 
			
		||||
 | 
			
		||||
    doc.meta_head = NULL;
 | 
			
		||||
    doc.meta_tail = NULL;
 | 
			
		||||
    doc.mime = 0;
 | 
			
		||||
    doc.size = job->vfile.info.st_size;
 | 
			
		||||
    doc.mtime = job->vfile.info.st_mtim.tv_sec;
 | 
			
		||||
    doc->meta_head = NULL;
 | 
			
		||||
    doc->meta_tail = NULL;
 | 
			
		||||
    doc->mime = 0;
 | 
			
		||||
    doc->size = job->vfile.info.st_size;
 | 
			
		||||
    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) {
 | 
			
		||||
        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);
 | 
			
		||||
        ScanCtx.dbg_skipped_files_count += 1;
 | 
			
		||||
@ -83,22 +83,19 @@ void parse(void *arg) {
 | 
			
		||||
 | 
			
		||||
    if (LogCtx.very_verbose) {
 | 
			
		||||
        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)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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)) {
 | 
			
		||||
        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;
 | 
			
		||||
 | 
			
		||||
    if (doc.mime == 0 && !ScanCtx.fast) {
 | 
			
		||||
        if (IS_GIT_OBJ) {
 | 
			
		||||
            goto abort;
 | 
			
		||||
        }
 | 
			
		||||
    if (doc->mime == 0 && !ScanCtx.fast) {
 | 
			
		||||
 | 
			
		||||
        // Get mime type with libmagic
 | 
			
		||||
        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);
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
            if (doc.mime == 0) {
 | 
			
		||||
            if (doc->mime == 0) {
 | 
			
		||||
                LOG_WARNINGF(job->filepath, "Couldn't find mime %s", magic_mime_str);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -143,48 +140,48 @@ void parse(void *arg) {
 | 
			
		||||
        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)) {
 | 
			
		||||
        parse_raw(&ScanCtx.raw_ctx, &job->vfile, &doc);
 | 
			
		||||
    } else if ((mmime == MimeVideo && doc.size >= MIN_VIDEO_SIZE) ||
 | 
			
		||||
               (mmime == MimeImage && doc.size >= MIN_IMAGE_SIZE) || mmime == MimeAudio) {
 | 
			
		||||
    } else if (IS_RAW(doc->mime)) {
 | 
			
		||||
        parse_raw(&ScanCtx.raw_ctx, &job->vfile, doc);
 | 
			
		||||
    } else if ((mmime == MimeVideo && doc->size >= MIN_VIDEO_SIZE) ||
 | 
			
		||||
               (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)) {
 | 
			
		||||
        parse_ebook(&ScanCtx.ebook_ctx, &job->vfile, mime_get_mime_text(doc.mime), &doc);
 | 
			
		||||
    } else if (IS_PDF(doc->mime)) {
 | 
			
		||||
        parse_ebook(&ScanCtx.ebook_ctx, &job->vfile, mime_get_mime_text(doc->mime), doc);
 | 
			
		||||
 | 
			
		||||
    } else if (mmime == MimeText && ScanCtx.text_ctx.content_size > 0) {
 | 
			
		||||
        if (IS_MARKUP(doc.mime)) {
 | 
			
		||||
            parse_markup(&ScanCtx.text_ctx, &job->vfile, &doc);
 | 
			
		||||
        if (IS_MARKUP(doc->mime)) {
 | 
			
		||||
            parse_markup(&ScanCtx.text_ctx, &job->vfile, doc);
 | 
			
		||||
        } else {
 | 
			
		||||
            parse_text(&ScanCtx.text_ctx, &job->vfile, &doc);
 | 
			
		||||
            parse_text(&ScanCtx.text_ctx, &job->vfile, doc);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    } else if (IS_FONT(doc.mime)) {
 | 
			
		||||
        parse_font(&ScanCtx.font_ctx, &job->vfile, &doc);
 | 
			
		||||
    } else if (IS_FONT(doc->mime)) {
 | 
			
		||||
        parse_font(&ScanCtx.font_ctx, &job->vfile, doc);
 | 
			
		||||
 | 
			
		||||
    } else if (
 | 
			
		||||
            ScanCtx.arc_ctx.mode != ARC_MODE_SKIP && (
 | 
			
		||||
                    IS_ARC(doc.mime) ||
 | 
			
		||||
                    (IS_ARC_FILTER(doc.mime) && should_parse_filtered_file(doc.filepath, doc.ext))
 | 
			
		||||
                    IS_ARC(doc->mime) ||
 | 
			
		||||
                    (IS_ARC_FILTER(doc->mime) && should_parse_filtered_file(doc->filepath, doc->ext))
 | 
			
		||||
            )) {
 | 
			
		||||
        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)) {
 | 
			
		||||
        parse_ooxml(&ScanCtx.ooxml_ctx, &job->vfile, &doc);
 | 
			
		||||
    } else if (is_cbr(&ScanCtx.comic_ctx, doc.mime) || is_cbz(&ScanCtx.comic_ctx, doc.mime)) {
 | 
			
		||||
        parse_comic(&ScanCtx.comic_ctx, &job->vfile, &doc);
 | 
			
		||||
    } else if (IS_MOBI(doc.mime)) {
 | 
			
		||||
        parse_mobi(&ScanCtx.mobi_ctx, &job->vfile, &doc);
 | 
			
		||||
    } else if (doc.mime == MIME_SIST2_SIDECAR) {
 | 
			
		||||
        parse_sidecar(&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)) {
 | 
			
		||||
        parse_ooxml(&ScanCtx.ooxml_ctx, &job->vfile, doc);
 | 
			
		||||
    } else if (is_cbr(&ScanCtx.comic_ctx, doc->mime) || is_cbz(&ScanCtx.comic_ctx, doc->mime)) {
 | 
			
		||||
        parse_comic(&ScanCtx.comic_ctx, &job->vfile, doc);
 | 
			
		||||
    } else if (IS_MOBI(doc->mime)) {
 | 
			
		||||
        parse_mobi(&ScanCtx.mobi_ctx, &job->vfile, doc);
 | 
			
		||||
    } else if (doc->mime == MIME_SIST2_SIDECAR) {
 | 
			
		||||
        parse_sidecar(&job->vfile, doc);
 | 
			
		||||
        CLOSE_FILE(job->vfile)
 | 
			
		||||
        return;
 | 
			
		||||
    } else if (is_msdoc(&ScanCtx.msdoc_ctx, doc.mime)) {
 | 
			
		||||
        parse_msdoc(&ScanCtx.msdoc_ctx, &job->vfile, &doc);
 | 
			
		||||
    } else if (is_msdoc(&ScanCtx.msdoc_ctx, doc->mime)) {
 | 
			
		||||
        parse_msdoc(&ScanCtx.msdoc_ctx, &job->vfile, doc);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abort:
 | 
			
		||||
@ -194,14 +191,14 @@ void parse(void *arg) {
 | 
			
		||||
        meta_line_t *meta_parent = malloc(sizeof(meta_line_t) + MD5_STR_LENGTH);
 | 
			
		||||
        meta_parent->key = MetaParent;
 | 
			
		||||
        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 {
 | 
			
		||||
        doc.has_parent = FALSE;
 | 
			
		||||
        doc->has_parent = FALSE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    write_document(&doc);
 | 
			
		||||
    write_document(doc);
 | 
			
		||||
 | 
			
		||||
    CLOSE_FILE(job->vfile)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								src/sist.h
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								src/sist.h
									
									
									
									
									
								
							@ -47,5 +47,16 @@
 | 
			
		||||
#include <sys/types.h>
 | 
			
		||||
#include <errno.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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/static/css/inspire-tree-light.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/static/css/inspire-tree-light.min.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								src/static/css/ion.rangeSlider.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/static/css/ion.rangeSlider.min.css
									
									
									
									
										vendored
									
									
								
							@ -1 +0,0 @@
 | 
			
		||||
.irs{position:relative;display:block;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.irs-line{position:relative;display:block;overflow:hidden;outline:none !important}.irs-line-left,.irs-line-mid,.irs-line-right{position:absolute;display:block;top:0}.irs-line-left{left:0;width:11%}.irs-line-mid{left:9%;width:82%}.irs-line-right{right:0;width:11%}.irs-bar{position:absolute;display:block;left:0;width:0}.irs-bar-edge{position:absolute;display:block;top:0;left:0}.irs-shadow{position:absolute;display:none;left:0;width:0}.irs-slider{position:absolute;display:block;cursor:default;z-index:1}.irs-slider.single{}.irs-slider.from{}.irs-slider.to{}.irs-slider.type_last{z-index:2}.irs-min{position:absolute;display:block;left:0;cursor:default}.irs-max{position:absolute;display:block;right:0;cursor:default}.irs-from,.irs-single,.irs-to{position:absolute;display:block;top:0;left:0;cursor:default;white-space:nowrap}.irs-grid{position:absolute;display:none;bottom:0;left:0;width:100%;height:20px}.irs-with-grid .irs-grid{display:block}.irs-grid-pol{position:absolute;top:0;left:0;width:1px;height:8px;background:#000}.irs-grid-pol.small{height:4px}.irs-grid-text{position:absolute;bottom:0;left:0;white-space:nowrap;text-align:center;font-size:9px;line-height:9px;padding:0 3px;color:#000}.irs-disable-mask{position:absolute;display:block;top:0;left:-1%;width:102%;height:100%;cursor:default;background:rgba(0,0,0,0.0);z-index:2}.irs-disabled{opacity:0.4}.lt-ie9 .irs-disabled{filter: alpha(opacity=40)}.irs-hidden-input{position:absolute !important;display:block !important;top:0 !important;left:0 !important;width:0 !important;height:0 !important;font-size:0 !important;line-height:0 !important;padding:0 !important;margin:0 !important;outline:none !important;z-index:-9999 !important;background:none !important;border-style:solid !important;border-color:transparent !important}
 | 
			
		||||
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