mirror of
https://github.com/simon987/sist2.git
synced 2025-04-21 19:26:45 +00:00
Compare commits
No commits in common. "0acdaab31a141731c83762e36d6baf8cd17b18f2" and "89fd68be8eac4d89be53956655a5ddb14b347a8c" have entirely different histories.
0acdaab31a
...
89fd68be8e
@ -1,30 +1,25 @@
|
||||
.idea/
|
||||
*.sist2
|
||||
docs/
|
||||
test_i/
|
||||
test_i_inc/
|
||||
Testing/
|
||||
.drone.yml
|
||||
**/cmake_install.cmake
|
||||
**/CMakeCache.txt
|
||||
**/CMakeFiles/
|
||||
LICENSE
|
||||
.idea
|
||||
*/thumbs
|
||||
*.cbp
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
cmake-build-debug
|
||||
cmake_install.cmake
|
||||
Makefile
|
||||
**/*.md
|
||||
**/*.cbp
|
||||
VERSION
|
||||
**/node_modules/
|
||||
*.out
|
||||
LOG
|
||||
sist2*
|
||||
index.sist2/
|
||||
bundle*.css
|
||||
bundle.js
|
||||
**/*.a
|
||||
**/vgcore.*
|
||||
build/
|
||||
.git/
|
||||
sist2-*-linux-debug
|
||||
sist2-*-linux
|
||||
sist2_debug
|
||||
sist2
|
||||
**/libscan-test-files
|
||||
**/scan_ub_test
|
||||
**/scan_a_test
|
||||
**/scan_test
|
||||
third-party/libscan/libscan-test-files/
|
||||
**/ext_ffmpeg
|
||||
**/ext_libmobi
|
||||
**/ext_libwpd
|
||||
**/core
|
||||
*.a
|
||||
**/scan_a_test
|
||||
Dockerfile
|
||||
*.idx/
|
||||
VERSION
|
||||
|
16
.drone.yml
16
.drone.yml
@ -70,19 +70,3 @@ 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
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: DOCKER_USER
|
||||
password:
|
||||
from_secret: DOCKER_PASSWORD
|
||||
repo: simon987/sist2
|
||||
context: ./
|
||||
dockerfile: ./Dockerfile
|
||||
auto_tag: true
|
||||
auto_tag_suffix: arm64-linux
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -9,7 +9,6 @@ Makefile
|
||||
*.out
|
||||
LOG
|
||||
sist2*
|
||||
!sist2-vue/
|
||||
index.sist2/
|
||||
bundle*.css
|
||||
bundle.js
|
||||
@ -19,8 +18,3 @@ build/
|
||||
third-party/
|
||||
*.idx/
|
||||
VERSION
|
||||
git_hash.h
|
||||
Testing/
|
||||
test_i
|
||||
test_i_inc
|
||||
node_modules/
|
@ -4,18 +4,8 @@ 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)
|
||||
|
@ -2,12 +2,10 @@ FROM simon987/sist2-build as build
|
||||
MAINTAINER simon987 <me@simon987.net>
|
||||
|
||||
WORKDIR /build/
|
||||
COPY . .
|
||||
RUN cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake .
|
||||
ADD . /build/
|
||||
RUN cmake -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake .
|
||||
RUN make -j$(nproc)
|
||||
RUN strip sist2
|
||||
RUN ls -lh
|
||||
RUN ls -lh sist2-vue/dist/
|
||||
|
||||
FROM ubuntu:20.10
|
||||
|
||||
|
@ -3,7 +3,7 @@ MAINTAINER simon987 <me@simon987.net>
|
||||
|
||||
WORKDIR /build/
|
||||
ADD . /build/
|
||||
RUN cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake .
|
||||
RUN cmake -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE=/vcpkg/scripts/buildsystems/vcpkg.cmake .
|
||||
RUN make -j$(nproc)
|
||||
RUN strip sist2
|
||||
|
||||
|
@ -2,16 +2,18 @@
|
||||
|
||||
VCPKG_ROOT="/vcpkg"
|
||||
|
||||
rm *.gz &>/dev/null
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
rm -rf CMakeFiles CMakeCache.txt
|
||||
cmake -DSIST_PLATFORM=x64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" .
|
||||
cmake -DSIST_DEBUG=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_PLATFORM=x64_linux -DSIST_DEBUG=on -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" .
|
||||
cmake -DSIST_DEBUG=on -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" .
|
||||
make -j $(nproc)
|
||||
mv sist2_debug sist2-x64-linux-debug
|
@ -2,16 +2,12 @@
|
||||
|
||||
VCPKG_ROOT="/vcpkg"
|
||||
|
||||
rm *.gz &>/dev/null
|
||||
|
||||
git submodule update --init --recursive
|
||||
|
||||
rm -rf CMakeFiles CMakeCache.txt
|
||||
cmake -DSIST_PLATFORM=arm64_linux -DSIST_DEBUG=off -DBUILD_TESTS=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" .
|
||||
cmake -DSIST_DEBUG=off -DCMAKE_TOOLCHAIN_FILE="${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" .
|
||||
make -j $(nproc)
|
||||
strip sist2
|
||||
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": "integer",
|
||||
"type": "float",
|
||||
"index": false
|
||||
},
|
||||
"width": {
|
||||
@ -134,9 +134,7 @@
|
||||
}
|
||||
},
|
||||
"tag": {
|
||||
"type": "text",
|
||||
"fielddata": true,
|
||||
"analyzer": "tag_analyzer",
|
||||
"type": "keyword",
|
||||
"copy_to": "suggest-tag"
|
||||
},
|
||||
"suggest-tag": {
|
||||
|
@ -7,12 +7,7 @@
|
||||
"analysis": {
|
||||
"tokenizer": {
|
||||
"path_tokenizer": {
|
||||
"type": "path_hierarchy",
|
||||
"delimiter": "/"
|
||||
},
|
||||
"tag_tokenizer": {
|
||||
"type": "path_hierarchy",
|
||||
"delimiter": "."
|
||||
"type": "path_hierarchy"
|
||||
},
|
||||
"my_nGram_tokenizer": {
|
||||
"type": "nGram",
|
||||
@ -27,12 +22,6 @@
|
||||
"lowercase"
|
||||
]
|
||||
},
|
||||
"tag_analyzer": {
|
||||
"tokenizer": "tag_tokenizer",
|
||||
"filter": [
|
||||
"lowercase"
|
||||
]
|
||||
},
|
||||
"case_insensitive_kw_analyzer": {
|
||||
"tokenizer": "keyword",
|
||||
"filter": [
|
||||
|
@ -2,9 +2,16 @@
|
||||
|
||||
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,6 +157,7 @@ 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,10 +1,12 @@
|
||||
files = [
|
||||
"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",
|
||||
"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",
|
||||
]
|
||||
|
||||
|
||||
@ -13,10 +15,6 @@ def clean(filepath):
|
||||
|
||||
|
||||
for file in files:
|
||||
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
23
sist2-vue/.gitignore
vendored
@ -1,23 +0,0 @@
|
||||
.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
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
"presets": [
|
||||
"@vue/cli-plugin-babel/preset"
|
||||
]
|
||||
}
|
9
sist2-vue/dist/css/chunk-vendors.css
vendored
9
sist2-vue/dist/css/chunk-vendors.css
vendored
File diff suppressed because one or more lines are too long
1
sist2-vue/dist/css/index.css
vendored
1
sist2-vue/dist/css/index.css
vendored
File diff suppressed because one or more lines are too long
3
sist2-vue/dist/index.html
vendored
3
sist2-vue/dist/index.html
vendored
@ -1,3 +0,0 @@
|
||||
<!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
146
sist2-vue/dist/js/chunk-vendors.js
vendored
File diff suppressed because one or more lines are too long
1
sist2-vue/dist/js/index.js
vendored
1
sist2-vue/dist/js/index.js
vendored
File diff suppressed because one or more lines are too long
27265
sist2-vue/package-lock.json
generated
27265
sist2-vue/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,55 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
<!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>
|
@ -1,312 +0,0 @@
|
||||
<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>
|
@ -1,382 +0,0 @@
|
||||
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("");
|
@ -1,228 +0,0 @@
|
||||
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();
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
@ -1,28 +0,0 @@
|
||||
<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>
|
@ -1,129 +0,0 @@
|
||||
<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>
|
@ -1,100 +0,0 @@
|
||||
<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>
|
@ -1,99 +0,0 @@
|
||||
<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>
|
@ -1,126 +0,0 @@
|
||||
<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>
|
@ -1,267 +0,0 @@
|
||||
<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>
|
@ -1,66 +0,0 @@
|
||||
<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>
|
@ -1,22 +0,0 @@
|
||||
<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>
|
@ -1,39 +0,0 @@
|
||||
<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>
|
@ -1,37 +0,0 @@
|
||||
<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>
|
@ -1,232 +0,0 @@
|
||||
<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>
|
@ -1,69 +0,0 @@
|
||||
<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>
|
@ -1,63 +0,0 @@
|
||||
<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>
|
@ -1,32 +0,0 @@
|
||||
<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>
|
@ -1,45 +0,0 @@
|
||||
<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>
|
@ -1,170 +0,0 @@
|
||||
<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>
|
@ -1,33 +0,0 @@
|
||||
<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>
|
@ -1,23 +0,0 @@
|
||||
<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>
|
@ -1,72 +0,0 @@
|
||||
<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>
|
@ -1,30 +0,0 @@
|
||||
<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>
|
@ -1,93 +0,0 @@
|
||||
<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>
|
@ -1,93 +0,0 @@
|
||||
<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>
|
@ -1,30 +0,0 @@
|
||||
<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>
|
@ -1,129 +0,0 @@
|
||||
<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>
|
@ -1,26 +0,0 @@
|
||||
<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>
|
@ -1,61 +0,0 @@
|
||||
<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>
|
@ -1,101 +0,0 @@
|
||||
<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>
|
@ -1,245 +0,0 @@
|
||||
<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>
|
@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<b-progress value="1" max="1" animated></b-progress>
|
||||
</template>
|
@ -1,76 +0,0 @@
|
||||
<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>
|
@ -1,46 +0,0 @@
|
||||
<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>
|
@ -1,40 +0,0 @@
|
||||
<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>
|
@ -1,135 +0,0 @@
|
||||
<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>
|
@ -1,21 +0,0 @@
|
||||
<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>
|
@ -1,59 +0,0 @@
|
||||
<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>
|
@ -1,337 +0,0 @@
|
||||
<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>
|
@ -1,185 +0,0 @@
|
||||
<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>
|
@ -1,296 +0,0 @@
|
||||
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",
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
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
7
sist2-vue/src/plugins/bootstrap-vue.js
vendored
@ -1,7 +0,0 @@
|
||||
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)
|
@ -1,36 +0,0 @@
|
||||
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
17
sist2-vue/src/shims-tsx.d.ts
vendored
@ -1,17 +0,0 @@
|
||||
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
4
sist2-vue/src/shims-vue.d.ts
vendored
@ -1,4 +0,0 @@
|
||||
declare module '*.vue' {
|
||||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|
@ -1,340 +0,0 @@
|
||||
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,
|
||||
}
|
||||
})
|
@ -1,139 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
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-")
|
||||
}
|
@ -1,265 +0,0 @@
|
||||
<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>
|
@ -1,288 +0,0 @@
|
||||
<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>
|
@ -1,79 +0,0 @@
|
||||
<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>
|
@ -1,40 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
filenameHashing: false,
|
||||
productionSourceMap: false,
|
||||
publicPath: "./",
|
||||
pages: {
|
||||
index: {
|
||||
entry: "src/main.js"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
#!/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,15 +4,13 @@
|
||||
|
||||
#define DEFAULT_OUTPUT "index.sist2/"
|
||||
#define DEFAULT_CONTENT_SIZE 32768
|
||||
#define DEFAULT_QUALITY 1
|
||||
#define DEFAULT_SIZE 300
|
||||
#define DEFAULT_QUALITY 5
|
||||
#define DEFAULT_SIZE 500
|
||||
#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
|
||||
@ -93,7 +91,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", LOG_SIST_WARNING, "Could not open original index! Disabled incremental scan feature.");
|
||||
sist_log("main.c", SIST_WARNING, "Could not open original index! Disabled incremental scan feature.");
|
||||
args->incremental = NULL;
|
||||
}
|
||||
}
|
||||
@ -362,15 +360,6 @@ 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) {
|
||||
@ -429,8 +418,6 @@ 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,14 +59,11 @@ 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,8 +1,6 @@
|
||||
#include "ctx.h"
|
||||
|
||||
ScanCtx_t ScanCtx = {
|
||||
.stat_index_size = 0,
|
||||
};
|
||||
ScanCtx_t ScanCtx;
|
||||
WebCtx_t WebCtx;
|
||||
IndexCtx_t IndexCtx;
|
||||
LogCtx_t LogCtx;
|
||||
|
@ -27,8 +27,6 @@ typedef struct {
|
||||
|
||||
tpool_t *pool;
|
||||
|
||||
tpool_t *writer_pool;
|
||||
|
||||
int threads;
|
||||
int depth;
|
||||
|
||||
@ -87,10 +85,7 @@ typedef struct {
|
||||
char *auth_pass;
|
||||
int auth_enabled;
|
||||
int tag_auth_enabled;
|
||||
char *tagline;
|
||||
struct index_t indices[256];
|
||||
char lang[3];
|
||||
int dev;
|
||||
struct index_t indices[64];
|
||||
} WebCtx_t;
|
||||
|
||||
extern ScanCtx_t ScanCtx;
|
||||
|
@ -393,20 +393,12 @@ void elastic_init(int force_reset, const char* user_mappings, const char* user_s
|
||||
|
||||
snprintf(url, sizeof(url), "%s/%s/_settings", IndexCtx.es_url, IndexCtx.es_index);
|
||||
r = web_put(url, user_settings ? user_settings : settings_json);
|
||||
LOG_INFOF("elastic.c", "Update ES settings <%d>", r->status_code);
|
||||
if (r->status_code != 200) {
|
||||
print_error(r);
|
||||
LOG_FATAL("elastic.c", "Could not update user settings")
|
||||
}
|
||||
LOG_INFOF("elastic.c", "Update user_settings <%d>", r->status_code);
|
||||
free_response(r);
|
||||
|
||||
snprintf(url, sizeof(url), "%s/%s/_mappings/_doc?include_type_name=true", IndexCtx.es_url, IndexCtx.es_index);
|
||||
r = web_put(url, user_mappings ? user_mappings : mappings_json);
|
||||
LOG_INFOF("elastic.c", "Update ES mappings <%d>", r->status_code);
|
||||
if (r->status_code != 200) {
|
||||
print_error(r);
|
||||
LOG_FATAL("elastic.c", "Could not update user mappings")
|
||||
}
|
||||
LOG_INFOF("elastic.c", "Update user_mappings <%d>", r->status_code);
|
||||
free_response(r);
|
||||
|
||||
snprintf(url, sizeof(url), "%s/%s/_open", IndexCtx.es_url, IndexCtx.es_index);
|
||||
|
File diff suppressed because one or more lines are too long
@ -3,7 +3,101 @@
|
||||
#include "src/parsing/parse.h"
|
||||
#include "src/parsing/mime.h"
|
||||
|
||||
#include <zstd.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;
|
||||
}
|
||||
|
||||
char *get_meta_key_text(enum metakey meta_key) {
|
||||
|
||||
@ -79,63 +173,135 @@ 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;
|
||||
|
||||
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);
|
||||
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");
|
||||
}
|
||||
cJSON_AddNumberToObject(json, "size", (double) doc->size);
|
||||
cJSON_AddNumberToObject(json, "mtime", doc->mtime);
|
||||
}
|
||||
|
||||
dyn_buffer_t buf = dyn_buffer_create();
|
||||
|
||||
// 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);
|
||||
char *filepath = doc->filepath + ScanCtx.index.desc.root_len;
|
||||
doc->filepath += ScanCtx.index.desc.root_len;
|
||||
|
||||
cJSON_AddStringToObject(json, "extension", filepath + doc->ext);
|
||||
dyn_buffer_write(&buf, doc, sizeof(line_t));
|
||||
dyn_buffer_write_str(&buf, doc->filepath);
|
||||
|
||||
// Remove extension
|
||||
if (*(filepath + doc->ext - 1) == '.') {
|
||||
*(filepath + doc->ext - 1) = '\0';
|
||||
} else {
|
||||
*(filepath + doc->ext) = '\0';
|
||||
}
|
||||
|
||||
char filepath_escaped[PATH_MAX * 3];
|
||||
str_escape(filepath_escaped, filepath + doc->base);
|
||||
|
||||
cJSON_AddStringToObject(json, "name", filepath_escaped);
|
||||
|
||||
if (doc->base > 0) {
|
||||
*(filepath + doc->base - 1) = '\0';
|
||||
|
||||
str_escape(filepath_escaped, filepath);
|
||||
cJSON_AddStringToObject(json, "path", filepath_escaped);
|
||||
} else {
|
||||
cJSON_AddStringToObject(json, "path", "");
|
||||
}
|
||||
|
||||
char md5_str[MD5_STR_LENGTH];
|
||||
buf2hex(doc->path_md5, MD5_DIGEST_LENGTH, md5_str);
|
||||
cJSON_AddStringToObject(json, "_id", md5_str);
|
||||
|
||||
// Metadata
|
||||
meta_line_t *meta = doc->meta_head;
|
||||
while (meta != NULL) {
|
||||
dyn_buffer_write_short(&buf, (uint16_t) meta->key);
|
||||
|
||||
switch (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);
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
ScanCtx.stat_index_size += buf.cur;
|
||||
dyn_buffer_destroy(&buf);
|
||||
}
|
||||
|
||||
void thread_cleanup() {
|
||||
close(index_fd);
|
||||
cleanup_parse();
|
||||
cleanup_font();
|
||||
}
|
||||
|
||||
|
||||
void read_index_bin(const char *path, const char *index_id, index_func func) {
|
||||
line_t line;
|
||||
dyn_buffer_t buf = dyn_buffer_create();
|
||||
|
||||
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 *document = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(document, "index", index_id);
|
||||
|
||||
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:
|
||||
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: {
|
||||
cJSON_AddNumberToObject(json, get_meta_key_text(meta->key), (double) meta->long_val);
|
||||
buffer_size_guess += 20;
|
||||
long value;
|
||||
ret = fread(&value, sizeof(long), 1, file);
|
||||
cJSON_AddNumberToObject(document, get_meta_key_text(key), (double) value);
|
||||
break;
|
||||
}
|
||||
case MetaMediaAudioCodec:
|
||||
@ -166,198 +332,23 @@ char *build_json_string(document_t *doc) {
|
||||
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);
|
||||
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: %x %s", meta->key, get_meta_key_text(meta->key))
|
||||
LOG_FATALF("serialize.c", "Invalid meta key (corrupt index): %x", key)
|
||||
}
|
||||
|
||||
meta_line_t *tmp = meta;
|
||||
meta = meta->next;
|
||||
free(tmp);
|
||||
fread(&key, sizeof(uint16_t), 1, file);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
cleanup_parse();
|
||||
cleanup_font();
|
||||
}
|
||||
|
||||
void read_index_bin_handle_line(const char *line, const char *index_id, index_func func) {
|
||||
|
||||
cJSON *document = cJSON_Parse(line);
|
||||
const char *path_md5_str = cJSON_GetObjectItem(document, "_id")->valuestring;
|
||||
|
||||
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);
|
||||
@ -374,7 +365,6 @@ void read_index_bin_handle_line(const char *line, const char *index_id, index_fu
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@ -385,120 +375,116 @@ void read_index_bin_handle_line(const char *line, const char *index_id, index_fu
|
||||
}
|
||||
|
||||
func(document, path_md5_str);
|
||||
cJSON_DeleteItemFromObject(document, "_id");
|
||||
cJSON_Delete(document);
|
||||
if (meta_obj) {
|
||||
cJSON_Delete(meta_obj);
|
||||
}
|
||||
}
|
||||
|
||||
void read_index_ndjson(const char *path, const char *index_id, index_func func) {
|
||||
dyn_buffer_t buf = dyn_buffer_create();
|
||||
|
||||
// Initialize zstd things
|
||||
FILE *file = fopen(path, "rb");
|
||||
|
||||
size_t const buf_in_size = ZSTD_DStreamInSize();
|
||||
void *const buf_in = malloc(buf_in_size);
|
||||
|
||||
size_t const buf_out_size = ZSTD_DStreamOutSize();
|
||||
void *const buf_out = malloc(buf_out_size);
|
||||
|
||||
ZSTD_DCtx *const dctx = ZSTD_createDCtx();
|
||||
|
||||
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};
|
||||
|
||||
while (input.pos < input.size) {
|
||||
ZSTD_outBuffer output = {buf_out, buf_out_size, 0};
|
||||
|
||||
size_t const ret = ZSTD_decompressStream(dctx, &output, &input);
|
||||
|
||||
for (int i = 0; i < output.pos; i++) {
|
||||
char c = ((char *) output.dst)[i];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const char *json_type_copy_fields[] = {
|
||||
"mime", "name", "path", "extension", "index", "size", "mtime", "parent",
|
||||
|
||||
// Meta
|
||||
"title", "content", "width", "height", "duration", "audioc", "videoc",
|
||||
"bitrate", "artist", "album", "album_artist", "genre", "title", "font_name",
|
||||
|
||||
// Special
|
||||
"tag", "_url"
|
||||
};
|
||||
|
||||
const char *json_type_array_fields[] = {
|
||||
"_keyword", "_text"
|
||||
};
|
||||
|
||||
void read_index_json(const char *path, UNUSED(const char *index_id), index_func func) {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
cJSON *input = cJSON_Parse(line);
|
||||
if (input == NULL) {
|
||||
LOG_FATALF("serialize.c", "Could not parse JSON line: \n%s", line)
|
||||
}
|
||||
if (line) {
|
||||
free(line);
|
||||
}
|
||||
|
||||
cJSON *document = cJSON_CreateObject();
|
||||
const char *id_str = cJSON_GetObjectItem(input, "_id")->valuestring;
|
||||
|
||||
for (int i = 0; i < (sizeof(json_type_copy_fields) / sizeof(json_type_copy_fields[0])); i++) {
|
||||
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 < (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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func(document, id_str);
|
||||
cJSON_Delete(document);
|
||||
cJSON_Delete(input);
|
||||
|
||||
}
|
||||
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_NDJSON) == 0) {
|
||||
read_index_ndjson(path, index_id, 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);
|
||||
}
|
||||
}
|
||||
|
||||
static __thread GHashTable *IncrementalReadTable = NULL;
|
||||
void incremental_read(GHashTable *table, const char *filepath) {
|
||||
FILE *file = fopen(filepath, "rb");
|
||||
line_t line;
|
||||
|
||||
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;
|
||||
LOG_DEBUGF("serialize.c", "Incremental read %s", filepath)
|
||||
|
||||
incremental_put_str(IncrementalReadTable, path_md5_str, mtime);
|
||||
while (1) {
|
||||
size_t ret = fread((void *) &line, sizeof(line_t), 1, file);
|
||||
if (ret != 1 || feof(file)) {
|
||||
break;
|
||||
}
|
||||
|
||||
void incremental_read(GHashTable *table, const char *filepath, index_descriptor_t *desc) {
|
||||
IncrementalReadTable = table;
|
||||
read_index(filepath, desc->id, desc->type, json_put_incremental);
|
||||
}
|
||||
|
||||
static __thread GHashTable *IncrementalCopyTable = NULL;
|
||||
static __thread store_t *IncrementalCopySourceStore = NULL;
|
||||
static __thread store_t *IncrementalCopyDestinationStore = NULL;
|
||||
|
||||
void incremental_copy_handle_doc(cJSON *document, UNUSED(const char id_str[MD5_STR_LENGTH])) {
|
||||
|
||||
const char *path_md5_str = cJSON_GetObjectItem(document, "_id")->valuestring;
|
||||
unsigned char path_md5[MD5_DIGEST_LENGTH];
|
||||
hex2buf(path_md5_str, MD5_STR_LENGTH - 1, path_md5);
|
||||
|
||||
if (cJSON_GetObjectItem(document, "parent") != NULL || incremental_get_str(IncrementalCopyTable, path_md5_str)) {
|
||||
// Copy index line
|
||||
cJSON_DeleteItemFromObject(document, "index");
|
||||
char *json_str = cJSON_PrintUnformatted(document);
|
||||
const size_t json_str_len = strlen(json_str);
|
||||
|
||||
json_str = realloc(json_str, json_str_len + 1);
|
||||
*(json_str + json_str_len) = '\n';
|
||||
|
||||
zstd_write_string(json_str, json_str_len + 1);
|
||||
free(json_str);
|
||||
|
||||
// Copy tn store contents
|
||||
size_t buf_len;
|
||||
char *buf = store_read(IncrementalCopySourceStore, (char *) path_md5, sizeof(path_md5), &buf_len);
|
||||
if (buf_len != 0) {
|
||||
store_write(IncrementalCopyDestinationStore, (char *) path_md5, sizeof(path_md5), buf, buf_len);
|
||||
free(buf);
|
||||
}
|
||||
incremental_put(table, line.path_md5, line.mtime);
|
||||
|
||||
while ((getc(file)) != 0) {}
|
||||
skip_meta(file);
|
||||
}
|
||||
fclose(file);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -507,14 +493,72 @@ void incremental_copy_handle_doc(cJSON *document, UNUSED(const char id_str[MD5_S
|
||||
*/
|
||||
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;
|
||||
|
||||
if (WriterCtx.out_file == NULL) {
|
||||
initialize_writer_ctx(dst_filepath);
|
||||
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;
|
||||
}
|
||||
|
||||
IncrementalCopyTable = copy_table;
|
||||
IncrementalCopySourceStore = store;
|
||||
IncrementalCopyDestinationStore = dst_store;
|
||||
// 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);
|
||||
|
||||
read_index(filepath, "", INDEX_TYPE_NDJSON, incremental_copy_handle_doc);
|
||||
// Copy filepath
|
||||
char filepath_buf[PATH_MAX];
|
||||
char c;
|
||||
char *ptr = filepath_buf;
|
||||
while ((c = (char) getc(file))) {
|
||||
*ptr++ = c;
|
||||
}
|
||||
*ptr = '\0';
|
||||
fwrite(filepath_buf, (ptr - filepath_buf) + 1, 1, dst_file);
|
||||
|
||||
// Copy tn store contents
|
||||
size_t buf_len;
|
||||
char path_md5[MD5_DIGEST_LENGTH];
|
||||
MD5((unsigned char *) filepath_buf, (ptr - filepath_buf), (unsigned char *) path_md5);
|
||||
char *buf = store_read(store, path_md5, sizeof(path_md5), &buf_len);
|
||||
if (buf_len != 0) {
|
||||
store_write(dst_store, path_md5, sizeof(path_md5), buf, buf_len);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
enum metakey key = 0;
|
||||
while (1) {
|
||||
fread(&key, sizeof(uint16_t), 1, file);
|
||||
fwrite(&key, sizeof(uint16_t), 1, dst_file);
|
||||
if (key == META_NEXT) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (IS_META_INT(key)) {
|
||||
int val;
|
||||
ret = fread(&val, sizeof(val), 1, file);
|
||||
fwrite(&val, sizeof(val), 1, dst_file);
|
||||
} else if (IS_META_LONG(key)) {
|
||||
long val;
|
||||
ret = fread(&val, sizeof(val), 1, file);
|
||||
fwrite(&val, sizeof(val), 1, dst_file);
|
||||
} else {
|
||||
while ((c = (char) getc(file))) {
|
||||
fwrite(&c, sizeof(c), 1, dst_file);
|
||||
}
|
||||
fwrite("\0", sizeof(c), 1, dst_file);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while ((getc(file))) {}
|
||||
skip_meta(file);
|
||||
}
|
||||
}
|
||||
fclose(file);
|
||||
fclose(dst_file);
|
||||
}
|
||||
|
@ -16,15 +16,13 @@ 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, index_descriptor_t *desc);
|
||||
void incremental_read(GHashTable *table, const char *filepath);
|
||||
|
||||
/**
|
||||
* 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,10 +1,9 @@
|
||||
#include "store.h"
|
||||
#include "src/ctx.h"
|
||||
|
||||
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);
|
||||
store_t *store_create(char *path, size_t chunk_size) {
|
||||
|
||||
store_t *store = malloc(sizeof(struct store_t));
|
||||
#if (SIST_FAKE_STORE != 1)
|
||||
store->chunk_size = chunk_size;
|
||||
pthread_rwlock_init(&store->lock, NULL);
|
||||
@ -39,7 +38,7 @@ void store_destroy(store_t *store) {
|
||||
|
||||
#if (SIST_FAKE_STORE != 1)
|
||||
pthread_rwlock_destroy(&store->lock);
|
||||
mdb_dbi_close(store->env, store->dbi);
|
||||
mdb_close(store->env, store->dbi);
|
||||
mdb_env_close(store->env);
|
||||
#endif
|
||||
free(store);
|
||||
|
@ -11,8 +11,6 @@
|
||||
#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;
|
||||
@ -20,7 +18,7 @@ typedef struct store_t {
|
||||
pthread_rwlock_t lock;
|
||||
} store_t;
|
||||
|
||||
store_t *store_create(const char *path, size_t chunk_size);
|
||||
store_t *store_create(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 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 SIST_DEBUG 0
|
||||
#define SIST_INFO 1
|
||||
#define SIST_WARNING 2
|
||||
#define SIST_ERROR 3
|
||||
#define SIST_FATAL 4
|
||||
|
||||
#define LOG_DEBUGF(filepath, fmt, ...) \
|
||||
if (LogCtx.very_verbose) {sist_logf(filepath, LOG_SIST_DEBUG, fmt, __VA_ARGS__);}
|
||||
if (LogCtx.very_verbose) {sist_logf(filepath, SIST_DEBUG, fmt, __VA_ARGS__);}
|
||||
#define LOG_DEBUG(filepath, str) \
|
||||
if (LogCtx.very_verbose) {sist_log(filepath, LOG_SIST_DEBUG, str);}
|
||||
if (LogCtx.very_verbose) {sist_log(filepath, SIST_DEBUG, str);}
|
||||
|
||||
#define LOG_INFOF(filepath, fmt, ...) \
|
||||
if (LogCtx.verbose) {sist_logf(filepath, LOG_SIST_INFO, fmt, __VA_ARGS__);}
|
||||
if (LogCtx.verbose) {sist_logf(filepath, SIST_INFO, fmt, __VA_ARGS__);}
|
||||
#define LOG_INFO(filepath, str) \
|
||||
if (LogCtx.verbose) {sist_log(filepath, LOG_SIST_INFO, str);}
|
||||
if (LogCtx.verbose) {sist_log(filepath, SIST_INFO, str);}
|
||||
|
||||
#define LOG_WARNINGF(filepath, fmt, ...) \
|
||||
if (LogCtx.verbose) {sist_logf(filepath, LOG_SIST_WARNING, fmt, __VA_ARGS__);}
|
||||
if (LogCtx.verbose) {sist_logf(filepath, SIST_WARNING, fmt, __VA_ARGS__);}
|
||||
#define LOG_WARNING(filepath, str) \
|
||||
if (LogCtx.verbose) {sist_log(filepath, LOG_SIST_WARNING, str);}
|
||||
if (LogCtx.verbose) {sist_log(filepath, SIST_WARNING, str);}
|
||||
|
||||
#define LOG_ERRORF(filepath, fmt, ...) \
|
||||
if (LogCtx.verbose) {sist_logf(filepath, LOG_SIST_ERROR, fmt, __VA_ARGS__);}
|
||||
if (LogCtx.verbose) {sist_logf(filepath, SIST_ERROR, fmt, __VA_ARGS__);}
|
||||
#define LOG_ERROR(filepath, str) \
|
||||
if (LogCtx.verbose) {sist_log(filepath, LOG_SIST_ERROR, str);}
|
||||
if (LogCtx.verbose) {sist_log(filepath, SIST_ERROR, str);}
|
||||
|
||||
#define LOG_FATALF(filepath, fmt, ...) \
|
||||
sist_logf(filepath, LOG_SIST_FATAL, fmt, __VA_ARGS__);\
|
||||
sist_logf(filepath, SIST_FATAL, fmt, __VA_ARGS__);\
|
||||
exit(-1);
|
||||
#define LOG_FATAL(filepath, str) \
|
||||
sist_log(filepath, LOG_SIST_FATAL, str);\
|
||||
sist_log(filepath, SIST_FATAL, str);\
|
||||
exit(-1);
|
||||
|
||||
#include "sist.h"
|
||||
|
68
src/main.c
68
src/main.c
@ -21,6 +21,7 @@
|
||||
#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",
|
||||
@ -90,21 +91,19 @@ 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_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);
|
||||
strcpy(ScanCtx.index.desc.type, INDEX_TYPE_BIN);
|
||||
|
||||
write_index_descriptor(path, &ScanCtx.index.desc);
|
||||
}
|
||||
@ -158,11 +157,7 @@ void _logf(const char *filepath, int level, char *format, ...) {
|
||||
|
||||
void initialize_scan_context(scan_args_t *args) {
|
||||
|
||||
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
|
||||
// Arc
|
||||
ScanCtx.arc_ctx.mode = args->archive_mode;
|
||||
ScanCtx.arc_ctx.log = _log;
|
||||
ScanCtx.arc_ctx.logf = _logf;
|
||||
@ -173,6 +168,11 @@ 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,7 +192,6 @@ 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;
|
||||
@ -267,15 +266,16 @@ 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", original_desc.version, Version)
|
||||
LOG_FATALF("main.c", "Version mismatch! Index is %s but executable is %s/%s", original_desc.version,
|
||||
Version, INDEX_VERSION_EXTERNAL)
|
||||
}
|
||||
|
||||
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, &original_desc);
|
||||
incremental_read(ScanCtx.original_table, file_path);
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
@ -294,9 +294,11 @@ 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();
|
||||
@ -305,12 +307,8 @@ void sist2_scan(scan_args_t *args) {
|
||||
load_incremental_index(args);
|
||||
}
|
||||
|
||||
ScanCtx.pool = tpool_create(args->threads, thread_cleanup, TRUE, TRUE);
|
||||
ScanCtx.pool = tpool_create(args->threads, thread_cleanup, 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)
|
||||
@ -318,9 +316,6 @@ 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)
|
||||
@ -328,7 +323,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.ndjson.zst", ScanCtx.index.path);
|
||||
snprintf(dst_path, PATH_MAX, "%s_index_original", ScanCtx.index.path);
|
||||
store_t *source = store_create(store_path, STORE_SIZE_TN);
|
||||
|
||||
DIR *dir = opendir(args->incremental);
|
||||
@ -346,10 +341,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);
|
||||
@ -358,7 +353,6 @@ 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) {
|
||||
@ -378,8 +372,9 @@ 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) {
|
||||
LOG_FATALF("main.c", "Version mismatch! Index is %s but executable is %s", desc.version, Version)
|
||||
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)
|
||||
}
|
||||
|
||||
DIR *dir = opendir(args->index_path);
|
||||
@ -389,6 +384,7 @@ 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);
|
||||
|
||||
@ -410,7 +406,7 @@ void sist2_index(index_args_t *args) {
|
||||
cleanup = elastic_cleanup;
|
||||
}
|
||||
|
||||
IndexCtx.pool = tpool_create(args->threads, cleanup, FALSE, FALSE);
|
||||
IndexCtx.pool = tpool_create(args->threads, cleanup, FALSE);
|
||||
tpool_start(IndexCtx.pool);
|
||||
|
||||
struct dirent *de;
|
||||
@ -419,7 +415,6 @@ 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);
|
||||
@ -433,7 +428,6 @@ 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);
|
||||
}
|
||||
@ -464,9 +458,6 @@ 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]);
|
||||
@ -523,7 +514,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=3"),
|
||||
"Thumbnail quality, on a scale of 1.0 to 31.0, 1.0 being the best. DEFAULT=5"),
|
||||
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,
|
||||
@ -551,8 +542,7 @@ 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"),
|
||||
@ -573,8 +563,6 @@ 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,6 +39,8 @@ 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);
|
||||
@ -49,28 +51,26 @@ void set_dbg_current_file(parse_job_t *job) {
|
||||
void parse(void *arg) {
|
||||
|
||||
parse_job_t *job = arg;
|
||||
|
||||
document_t *doc = malloc(sizeof(document_t));
|
||||
doc->filepath = malloc(strlen(job->filepath) + 1);
|
||||
document_t doc;
|
||||
|
||||
set_dbg_current_file(job);
|
||||
|
||||
strcpy(doc->filepath, job->filepath);
|
||||
doc->ext = (short) job->ext;
|
||||
doc->base = (short) job->base;
|
||||
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,19 +83,22 @@ 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 (doc.mime == 0 && !ScanCtx.fast) {
|
||||
if (IS_GIT_OBJ) {
|
||||
goto abort;
|
||||
}
|
||||
|
||||
// Get mime type with libmagic
|
||||
if (!job->vfile.is_fs_file) {
|
||||
@ -126,11 +129,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);
|
||||
}
|
||||
}
|
||||
@ -140,48 +143,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:
|
||||
@ -191,14 +194,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,16 +47,5 @@
|
||||
#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
Normal file
4
src/static/css/autocomplete.min.css
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.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
Normal file
9
src/static/css/bootstrap-colorpicker.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
src/static/css/bootstrap.min.css
vendored
Normal file
6
src/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/static/css/bricklayer.min.css
vendored
Normal file
1
src/static/css/bricklayer.min.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
.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}
|
544
src/static/css/dark.css
Normal file
544
src/static/css/dark.css
Normal file
@ -0,0 +1,544 @@
|
||||
*: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
Normal file
1
src/static/css/inspire-tree-light.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
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