Add page branding options to filesystem
This commit is contained in:
@@ -120,6 +120,12 @@ p>img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
code {
|
||||
display: inline-block;
|
||||
background: var(--background);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Page layout elements */
|
||||
|
||||
.button_toggle_navigation {
|
||||
@@ -527,7 +533,7 @@ input[type="submit"]:active,
|
||||
input[type="button"]:active,
|
||||
input[type="color"]:active,
|
||||
select:active {
|
||||
box-shadow: inset 4px 4px 8px var(--shadow_color);
|
||||
box-shadow: inset 4px 4px 6px var(--shadow_color);
|
||||
/* Exactly 4px offset compared to the inactive padding to give a depth effect */
|
||||
padding: 9px 1px 1px 9px;
|
||||
}
|
||||
@@ -538,8 +544,8 @@ select:active {
|
||||
}
|
||||
|
||||
.button_red {
|
||||
background: linear-gradient(var(--danger_color), var(--danger_color_dark)) !important;
|
||||
color: var(--highlight_text_color) !important;
|
||||
background: var(--danger_color) !important;
|
||||
color: var(--danger_text_color) !important;
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
@@ -663,7 +669,7 @@ input[type="email"]:focus,
|
||||
input[type="number"]:focus,
|
||||
input[type="date"]:focus,
|
||||
input[type="datetime-local"]:focus {
|
||||
box-shadow: inset 0 0 3px 0 var(--highlight_color);
|
||||
box-shadow: inset 0px 0px 0px 1px var(--highlight_color);
|
||||
}
|
||||
|
||||
textarea:disabled,
|
||||
|
47
svelte/package-lock.json
generated
47
svelte/package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"behave-js": "^1.5.0",
|
||||
"chart.js": "^4.2.0",
|
||||
"svelte-flag-icons": "^0.7.1"
|
||||
"pure-color": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.20",
|
||||
@@ -30,6 +30,7 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
|
||||
"integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
@@ -1680,6 +1681,7 @@
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
||||
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.0.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
@@ -1693,6 +1695,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||
"integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
@@ -1701,6 +1704,7 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
@@ -1718,12 +1722,14 @@
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.19",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
|
||||
"integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
@@ -1857,7 +1863,8 @@
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
|
||||
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA=="
|
||||
"integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/jsmediatags": {
|
||||
"version": "3.9.4",
|
||||
@@ -1875,6 +1882,7 @@
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1911,6 +1919,7 @@
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
@@ -1919,6 +1928,7 @@
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
|
||||
"integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
@@ -2129,6 +2139,7 @@
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
|
||||
"integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
"@types/estree": "^1.0.1",
|
||||
@@ -2141,6 +2152,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
@@ -2195,6 +2207,7 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.30",
|
||||
"source-map-js": "^1.0.1"
|
||||
@@ -2233,6 +2246,7 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -2541,7 +2555,8 @@
|
||||
"node_modules/locate-character": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
@@ -2573,7 +2588,8 @@
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
@@ -2633,6 +2649,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
|
||||
"integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"estree-walker": "^3.0.0",
|
||||
@@ -2643,6 +2660,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
@@ -2651,6 +2669,7 @@
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
||||
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
@@ -2673,6 +2692,11 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pure-color": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz",
|
||||
"integrity": "sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA=="
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
@@ -2906,6 +2930,7 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -2948,6 +2973,7 @@
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.1.tgz",
|
||||
"integrity": "sha512-LpLqY2Jr7cRxkrTc796/AaaoMLF/1ax7cto8Ot76wrvKQhrPmZ0JgajiWPmg9mTSDqO16SSLiD17r9MsvAPTmw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
@@ -2967,18 +2993,11 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-flag-icons": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-flag-icons/-/svelte-flag-icons-0.7.1.tgz",
|
||||
"integrity": "sha512-iyxiQ8y6JwtFJ7OsWs0ZTmLHke2dAP8mHyfUfth8VcJbipRR3u339iM3ofop4lBCcscAik0dySGlGK9n0yAwFw==",
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.54.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte/node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
@@ -2987,6 +3006,7 @@
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
||||
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
@@ -2995,6 +3015,7 @@
|
||||
"version": "0.30.3",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz",
|
||||
"integrity": "sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
},
|
||||
|
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"behave-js": "^1.5.0",
|
||||
"chart.js": "^4.2.0"
|
||||
"chart.js": "^4.2.0",
|
||||
"pure-color": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
@@ -39,6 +39,7 @@ export let nobg = false
|
||||
.nobg {
|
||||
background: none;
|
||||
margin: 0;
|
||||
color: var(--body_text_color);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
|
300
svelte/src/filesystem/BrandingOptions.svelte
Normal file
300
svelte/src/filesystem/BrandingOptions.svelte
Normal file
@@ -0,0 +1,300 @@
|
||||
<script context="module">
|
||||
import parse from "pure-color/parse"
|
||||
import rgb2hsl from "pure-color/convert/rgb2hsl";
|
||||
import hsl2rgb from "pure-color/convert/hsl2rgb";
|
||||
import rgb2hex from "pure-color/convert/rgb2hex";
|
||||
|
||||
// Generate a branding style from a file's properties map
|
||||
export const branding_from_path = path => {
|
||||
let style = {}
|
||||
for (let node of path) {
|
||||
add_styles(style, node.properties)
|
||||
}
|
||||
last_generated_style = style
|
||||
return gen_css(style)
|
||||
}
|
||||
|
||||
// The last style which was generated is cached, when we don't have a complete
|
||||
// path to generate the style with we will use the cached style as a basis
|
||||
let last_generated_style = {}
|
||||
export const branding_from_node = node => {
|
||||
add_styles(last_generated_style, node.properties)
|
||||
return gen_css(last_generated_style)
|
||||
}
|
||||
|
||||
const gen_css = style => {
|
||||
return Object.entries(style).map(([key, value]) => `--${key}:${value}`).join(';');
|
||||
}
|
||||
|
||||
// add_styles adds the styles configured in the properties struct to the
|
||||
// existing style which is passed as the first argument. When navigating to a
|
||||
// path this function is executed on every member of the path so all the styles
|
||||
// get combined
|
||||
const add_styles = (style, properties) => {
|
||||
if (!properties || !properties.branding_enabled || properties.branding_enabled !== "true") {
|
||||
return
|
||||
}
|
||||
|
||||
if (properties.brand_input_color) {
|
||||
style.input_background = properties.brand_input_color
|
||||
style.input_hover_background = properties.brand_input_color
|
||||
style.input_text = add_light(properties.brand_input_color, 70)
|
||||
style.shadow_color = add_light(properties.brand_input_color, 20)
|
||||
}
|
||||
if (properties.brand_highlight_color) {
|
||||
style.highlight_color = properties.brand_highlight_color
|
||||
style.highlight_background = properties.brand_highlight_color
|
||||
style.highlight_text_color = add_light(properties.brand_highlight_color, 70)
|
||||
style.link_color = add_light(properties.brand_highlight_color, 10)
|
||||
}
|
||||
if (properties.brand_danger_color) {
|
||||
style.danger_color = properties.brand_danger_color
|
||||
style.danger_text_color = add_light(properties.brand_danger_color, 70)
|
||||
}
|
||||
if (properties.brand_background_color) {
|
||||
style.background_color = properties.brand_background_color
|
||||
style.background = properties.brand_background_color
|
||||
style.background_text_color = add_light(properties.brand_background_color, 70)
|
||||
}
|
||||
if (properties.brand_body_color) {
|
||||
style.body_color = properties.brand_body_color
|
||||
style.body_background = properties.brand_body_color
|
||||
style.body_text_color = add_light(properties.brand_body_color, 70)
|
||||
style.shaded_background = set_alpha(properties.brand_body_color, 0.8)
|
||||
style.separator = add_light(properties.brand_body_color, 5)
|
||||
}
|
||||
if (properties.brand_card_color) {
|
||||
style.card_color = properties.brand_card_color
|
||||
}
|
||||
if (properties.brand_background_image) {
|
||||
style.background_image = "url('/api/filesystem/"+properties.brand_background_image+"')"
|
||||
style.background_image_size = "cover"
|
||||
style.background_image_position = "center"
|
||||
style.background_image_repeat = "no-repeat"
|
||||
}
|
||||
}
|
||||
|
||||
const add_light = (color, amt) => {
|
||||
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
|
||||
if (hsl[2] < 50) {
|
||||
hsl[2] = hsl[2]+amt // Dark color, add lightness
|
||||
} else {
|
||||
hsl[2] = hsl[2]-amt // Light color, remove lightness
|
||||
}
|
||||
return rgb2hex(hsl2rgb(hsl)) // Convert back to hex
|
||||
}
|
||||
const set_alpha = (color, amt) => {
|
||||
let rgb = parse(color)
|
||||
rgb.push(amt)
|
||||
return "rgba("+rgb.join(", ")+")"
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import FilePicker from "./filemanager/FilePicker.svelte";
|
||||
import { fs_update } from "./FilesystemAPI";
|
||||
import { fs_node_type } from "./FilesystemUtil";
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
export let file = {
|
||||
properties: {
|
||||
branding_enabled: "false",
|
||||
brand_input_color: "#2d2d2d",
|
||||
brand_highlight_color: "#75b72d",
|
||||
brand_danger_color: "#bd5f69",
|
||||
brand_background_color: "#141414",
|
||||
brand_body_color: "#1e1e1e",
|
||||
brand_card_color: "#282828",
|
||||
brand_header_image: "",
|
||||
brand_header_link: "",
|
||||
brand_footer_image: "",
|
||||
brand_footer_link: "",
|
||||
brand_background_image: "",
|
||||
},
|
||||
}
|
||||
|
||||
export let enabled = false
|
||||
|
||||
$: update_colors(file)
|
||||
const update_colors = file => {
|
||||
if (enabled) {
|
||||
file.properties.branding_enabled = "true"
|
||||
dispatch("style_change", file)
|
||||
} else {
|
||||
file.properties.branding_enabled = ""
|
||||
}
|
||||
}
|
||||
|
||||
let picker
|
||||
let picking = ""
|
||||
const pick_image = type => {
|
||||
picking = type
|
||||
picker.open(file.path)
|
||||
}
|
||||
const handle_picker = async e => {
|
||||
if (e.detail.length !== 1) {
|
||||
alert("Please select one file")
|
||||
return
|
||||
}
|
||||
let f = e.detail[0]
|
||||
let file_id = f.id
|
||||
|
||||
if (fs_node_type(f) !== "image") {
|
||||
alert("Please select an image file")
|
||||
return
|
||||
}
|
||||
|
||||
// If this image is not public, it will be made public
|
||||
if (file_id === undefined || file_id === "") {
|
||||
try {
|
||||
let new_file = await fs_update(e.detail[0].path, {shared: true})
|
||||
file_id = new_file.id
|
||||
} catch (err) {
|
||||
alert(err)
|
||||
}
|
||||
}
|
||||
|
||||
if (picking === "brand_header_image") {
|
||||
file.properties.brand_header_image = file_id
|
||||
} else if (picking === "brand_background_image") {
|
||||
file.properties.brand_background_image = file_id
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<p>
|
||||
You can customize how your filesystem looks. The colours chosen here apply
|
||||
to the directory you're currently in and all files and directories in this
|
||||
directory. Colours which you do not want to modify can be left empty. Then
|
||||
the default theme colour will be used.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<input bind:checked={enabled} id="enable_branding" type="checkbox" class="form_input"/>
|
||||
<label for="enable_branding">Enable custom branding options</label>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
<div class="grid" class:disabled={!enabled}>
|
||||
<div>Button</div>
|
||||
<input type="color" bind:value={file.properties.brand_input_color}/>
|
||||
<input type="text" bind:value={file.properties.brand_input_color}/>
|
||||
<div>Highlighted button</div>
|
||||
<input type="color" bind:value={file.properties.brand_highlight_color}/>
|
||||
<input type="text" bind:value={file.properties.brand_highlight_color}/>
|
||||
<div>Danger button</div>
|
||||
<input type="color" bind:value={file.properties.brand_danger_color}/>
|
||||
<input type="text" bind:value={file.properties.brand_danger_color}/>
|
||||
<div>Background</div>
|
||||
<input type="color" bind:value={file.properties.brand_background_color}/>
|
||||
<input type="text" bind:value={file.properties.brand_background_color}/>
|
||||
<div>Body</div>
|
||||
<input type="color" bind:value={file.properties.brand_body_color}/>
|
||||
<input type="text" bind:value={file.properties.brand_body_color}/>
|
||||
<div>Card</div>
|
||||
<input type="color" bind:value={file.properties.brand_card_color}/>
|
||||
<input type="text" bind:value={file.properties.brand_card_color}/>
|
||||
<div class="span3">
|
||||
<hr/>
|
||||
|
||||
You can choose an image to show above or behind the files in this
|
||||
directory. The image will be picked from your filesystem. If the image
|
||||
is not shared yet it will be made shared so it can be publicly
|
||||
downloaded.
|
||||
</div>
|
||||
<div>Header image ID</div>
|
||||
<button on:click={() => pick_image("brand_header_image")}>
|
||||
<i class="icon">folder_open</i>
|
||||
Pick
|
||||
</button>
|
||||
<input type="text" bind:value={file.properties.brand_header_image}/>
|
||||
<div>Header image link</div>
|
||||
<input class="span2" type="text" bind:value={file.properties.brand_header_link}/>
|
||||
<div>Background image ID</div>
|
||||
<button on:click={() => pick_image("brand_background_image")}>
|
||||
<i class="icon">folder_open</i>
|
||||
Pick
|
||||
</button>
|
||||
<input type="text" bind:value={file.properties.brand_background_image}/>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
<p>
|
||||
Below is an example of what the site looks like with these colours:
|
||||
</p>
|
||||
|
||||
<div class="example example_background">
|
||||
<div>The background</div>
|
||||
|
||||
<div class="example example_body">
|
||||
<div>The content body. <a href="/">A link</a>!</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div class="example_button_row">
|
||||
<button type="button"><i class="icon">touch_app</i>Normal</button>
|
||||
<button type="button" class="button_highlight"><i class="icon">priority_high</i>Important</button>
|
||||
<button type="button" class="button_red"><i class="icon">warning</i>Dangerous</button>
|
||||
</div>
|
||||
|
||||
<div class="example_button_row">
|
||||
<input type="text" value="A text field"/>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<div class="example example_card">
|
||||
<div>The top layer, for highlighted things</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FilePicker bind:this={picker} on:files={handle_picker}/>
|
||||
|
||||
<style>
|
||||
input[type="color"] {
|
||||
padding: 0;
|
||||
display: inline;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: auto auto auto;
|
||||
align-items: center;
|
||||
}
|
||||
.span2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.span3 {
|
||||
grid-column: span 3;
|
||||
}
|
||||
.disabled {
|
||||
filter: brightness(50%);
|
||||
}
|
||||
|
||||
.example {
|
||||
margin: 6px 0;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.example_background {
|
||||
background: var(--background);
|
||||
color: var(--background_text_color)
|
||||
}
|
||||
.example_body {
|
||||
background: var(--body_background);
|
||||
color: var(--body_text_color)
|
||||
}
|
||||
.example_button_row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.example_button_row>* {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.example_card {
|
||||
background: var(--card_color);
|
||||
}
|
||||
</style>
|
@@ -3,6 +3,7 @@ import { fs_delete_all, fs_rename, fs_update } from "./FilesystemAPI";
|
||||
import Modal from "../util/Modal.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Button from "../layout/Button.svelte";
|
||||
import BrandingOptions from "./BrandingOptions.svelte";
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
@@ -12,6 +13,7 @@ let file = {
|
||||
name: "",
|
||||
id: "",
|
||||
mode_octal: "",
|
||||
properties: {},
|
||||
};
|
||||
|
||||
export let visible
|
||||
@@ -24,6 +26,11 @@ export const edit = (f, oae = false, t = "file") => {
|
||||
|
||||
file_name = file.name
|
||||
shared = !(file.id === undefined || file.id === "")
|
||||
|
||||
if (file.properties === undefined) {
|
||||
file.properties = {}
|
||||
}
|
||||
branding_enabled = file.properties.branding_enabled === "true"
|
||||
visible = true
|
||||
}
|
||||
|
||||
@@ -33,14 +40,41 @@ let open_after_edit = false
|
||||
let file_name = ""
|
||||
let shared = false
|
||||
|
||||
let branding_enabled = false
|
||||
let branding_colors
|
||||
let branding_fields = [
|
||||
"brand_input_color",
|
||||
"brand_highlight_color",
|
||||
"brand_danger_color",
|
||||
"brand_background_color",
|
||||
"brand_body_color",
|
||||
"brand_card_color",
|
||||
"brand_header_image",
|
||||
"brand_header_link",
|
||||
"brand_footer_image",
|
||||
"brand_footer_link",
|
||||
"brand_background_image",
|
||||
]
|
||||
|
||||
const save = async () => {
|
||||
console.debug("Saving file", file.path)
|
||||
|
||||
try {
|
||||
dispatch("loading", true)
|
||||
await fs_update(
|
||||
file.path,
|
||||
{shared: shared},
|
||||
)
|
||||
let opts = {shared: shared}
|
||||
|
||||
opts.branding_enabled = branding_enabled ? "true" : ""
|
||||
|
||||
if (branding_enabled && file.properties) {
|
||||
for (let field of branding_fields) {
|
||||
if (file.properties[field] !== undefined) {
|
||||
console.log("setting", field, "to", file.properties[field])
|
||||
opts[field] = file.properties[field]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await fs_update(file.path, opts)
|
||||
|
||||
if (file_name !== file.name) {
|
||||
let parent = file.path.slice(0, -file.name.length)
|
||||
@@ -90,7 +124,7 @@ const delete_file = async e => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:visible={visible} title="Edit {file.name}" width="600px" form="file_edit_form">
|
||||
<Modal bind:visible={visible} title="Edit {file.name}" width="700px" form="file_edit_form">
|
||||
<div class="tab_bar">
|
||||
<button class:button_highlight={tab === "file"} on:click={() => tab = "file"}>
|
||||
<i class="icon">edit</i>
|
||||
@@ -98,42 +132,46 @@ const delete_file = async e => {
|
||||
</button>
|
||||
<button class:button_highlight={tab === "share"} on:click={() => tab = "share"}>
|
||||
<i class="icon">share</i>
|
||||
Sharing settings
|
||||
Sharing
|
||||
</button>
|
||||
<button class:button_highlight={tab === "branding"} on:click={() => tab = "branding"}>
|
||||
<i class="icon">palette</i>
|
||||
Branding
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="file_edit_form" on:submit|preventDefault={save} style="display: flex; padding: 8px;">
|
||||
{#if tab === "file"}
|
||||
<div class="form">
|
||||
<span class="header">File settings</span>
|
||||
<label for="file_name">Name:</label>
|
||||
<input bind:value={file_name} id="file_name" type="text" class="form_input"/>
|
||||
<span class="header">Delete</span>
|
||||
<p>
|
||||
Delete this file or directory. If this is a directory then all
|
||||
subfiles will be deleted as well. This action cannot be undone.
|
||||
</p>
|
||||
<Button click={delete_file} red icon="delete" label="Delete" style="align-self: flex-start;"/>
|
||||
<form id="file_edit_form" on:submit|preventDefault={save}></form>
|
||||
{#if tab === "file"}
|
||||
<div class="tab_content">
|
||||
<span class="header">File settings</span>
|
||||
<label for="file_name">Name:</label>
|
||||
<input form="file_edit_form" bind:value={file_name} id="file_name" type="text" class="form_input"/>
|
||||
<span class="header">Delete</span>
|
||||
<p>
|
||||
Delete this file or directory. If this is a directory then all
|
||||
subfiles will be deleted as well. This action cannot be undone.
|
||||
</p>
|
||||
<Button click={delete_file} red icon="delete" label="Delete" style="align-self: flex-start;"/>
|
||||
</div>
|
||||
{:else if tab === "share"}
|
||||
<div class="tab_content">
|
||||
<p>
|
||||
When a file or directory is shared it can be accessed
|
||||
through a unique link. You can get the URL with the 'Copy
|
||||
link' button on the toolbar, or share the link with the
|
||||
'Share' button. If you share a directory all the files
|
||||
within the directory are also accessible from the link.
|
||||
</p>
|
||||
<div>
|
||||
<input bind:checked={shared} id="shared" type="checkbox" class="form_input"/>
|
||||
<label for="shared">Share this file or directory</label>
|
||||
</div>
|
||||
{:else if tab === "share"}
|
||||
<div class="form">
|
||||
<span class="header">
|
||||
Sharing settings
|
||||
</span>
|
||||
<p>
|
||||
When a file or directory is shared it can be accessed
|
||||
through a unique link. You can get the URL with the 'Copy
|
||||
link' button on the toolbar, or share the link with the
|
||||
'Share' button. If you share a directory all the files
|
||||
within the directory are also accessible from the link.
|
||||
</p>
|
||||
<div>
|
||||
<input bind:checked={shared} id="shared" type="checkbox" class="form_input"/>
|
||||
<label for="shared">Share this file or directory</label>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
{:else if tab === "branding"}
|
||||
<div class="tab_content">
|
||||
<BrandingOptions bind:enabled={branding_enabled} bind:colors={branding_colors} file={file} on:style_change/>
|
||||
</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
@@ -145,4 +183,9 @@ const delete_file = async e => {
|
||||
.tab_bar {
|
||||
border-bottom: 2px solid var(--separator);
|
||||
}
|
||||
.tab_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
|
@@ -5,10 +5,11 @@ import { fs_path_url } from "./FilesystemUtil";
|
||||
|
||||
export let state
|
||||
|
||||
let loading = true
|
||||
let downloads = 0
|
||||
let transfer_used = 0
|
||||
let socket = null
|
||||
let error_msg = "Loading..."
|
||||
let error_msg = ""
|
||||
|
||||
let connected_to = ""
|
||||
|
||||
@@ -27,7 +28,7 @@ const update_base = async base => {
|
||||
// If the socket is already active we need to close it
|
||||
close_socket()
|
||||
|
||||
error_msg = "Loading..."
|
||||
loading = true
|
||||
|
||||
let ws_endpoint = location.origin.replace(/^http/, 'ws') +
|
||||
fs_path_url(base.path).replace(/^http/, 'ws') +
|
||||
@@ -40,6 +41,7 @@ const update_base = async base => {
|
||||
console.debug("WS update", j)
|
||||
|
||||
error_msg = ""
|
||||
loading = false
|
||||
downloads = j.downloads
|
||||
transfer_used = j.transfer_free + j.transfer_paid
|
||||
}
|
||||
@@ -83,11 +85,16 @@ onDestroy(close_socket)
|
||||
{:else}
|
||||
<div class="group">
|
||||
<div class="label">Downloads</div>
|
||||
<div class="stat">{formatThousands(downloads)}</div>
|
||||
<div class="stat">
|
||||
{loading ? "Loading..." : formatThousands(downloads)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="label">Transfer used</div>
|
||||
<div class="stat">{formatDataVolume(transfer_used, 3)}</div>
|
||||
<div class="stat">
|
||||
{loading ? "Loading..." : formatDataVolume(transfer_used, 3)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@@ -11,6 +11,7 @@ import SearchView from './SearchView.svelte';
|
||||
import UploadWidget from './upload_widget/UploadWidget.svelte';
|
||||
import HomeButton from '../file_viewer/HomeButton.svelte';
|
||||
import { fs_path_url } from './FilesystemUtil';
|
||||
import { branding_from_node, branding_from_path } from './BrandingOptions.svelte';
|
||||
|
||||
let loading = true
|
||||
let toolbar
|
||||
@@ -103,6 +104,14 @@ const search = async () => {
|
||||
const loading_evt = e => {
|
||||
loading = e.detail
|
||||
}
|
||||
|
||||
// Custom CSS rules for the whole viewer. This is updated by either the
|
||||
// navigation_complete event or the style_change event
|
||||
let custom_css = ""
|
||||
$: update_css(state.path)
|
||||
const update_css = path => {
|
||||
custom_css = branding_from_path(path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={keydown} />
|
||||
@@ -113,7 +122,7 @@ const loading_evt = e => {
|
||||
on:loading={loading_evt}
|
||||
/>
|
||||
|
||||
<div class="file_viewer">
|
||||
<div class="file_viewer" style={custom_css}>
|
||||
<div class="headerbar">
|
||||
<div>
|
||||
<HomeButton nobg/>
|
||||
@@ -135,7 +144,7 @@ const loading_evt = e => {
|
||||
on:search={search}
|
||||
/>
|
||||
|
||||
<div class="file_preview checkers">
|
||||
<div class="file_preview">
|
||||
{#if view === "file"}
|
||||
<FilePreview
|
||||
fs_navigator={fs_navigator}
|
||||
@@ -158,8 +167,7 @@ const loading_evt = e => {
|
||||
</div>
|
||||
|
||||
<div class="highlight_yellow">
|
||||
The filesystem is an experimental feature! Please read <a
|
||||
href="/filesystem">the guide</a> before using it.
|
||||
The filesystem is experimental! <a href="/filesystem">Please read the guide</a>
|
||||
</div>
|
||||
|
||||
<!-- This frame will load the download URL when a download button is pressed -->
|
||||
@@ -168,30 +176,43 @@ const loading_evt = e => {
|
||||
title="Frame for downloading files"
|
||||
style="display: none; width: 1px; height: 1px;">
|
||||
</iframe>
|
||||
|
||||
<DetailsWindow
|
||||
state={state}
|
||||
bind:visible={details_visible}
|
||||
/>
|
||||
|
||||
<EditWindow
|
||||
bind:this={edit_window}
|
||||
bind:visible={edit_visible}
|
||||
fs_navigator={fs_navigator}
|
||||
on:loading={loading_evt}
|
||||
on:style_change={e => {
|
||||
custom_css = branding_from_node(e.detail)
|
||||
}}
|
||||
/>
|
||||
|
||||
<UploadWidget
|
||||
bind:this={upload_widget}
|
||||
fs_state={state}
|
||||
drop_upload
|
||||
on:uploads_finished={() => fs_navigator.reload()}
|
||||
/>
|
||||
|
||||
<LoadingIndicator loading={loading}/>
|
||||
</div>
|
||||
|
||||
<DetailsWindow
|
||||
state={state}
|
||||
bind:visible={details_visible}
|
||||
/>
|
||||
|
||||
<EditWindow
|
||||
bind:this={edit_window}
|
||||
bind:visible={edit_visible}
|
||||
fs_navigator={fs_navigator}
|
||||
on:loading={loading_evt}
|
||||
/>
|
||||
|
||||
<UploadWidget
|
||||
bind:this={upload_widget}
|
||||
fs_state={state}
|
||||
drop_upload
|
||||
on:uploads_finished={() => fs_navigator.reload()}
|
||||
/>
|
||||
|
||||
<LoadingIndicator loading={loading}/>
|
||||
|
||||
<style>
|
||||
:global(*) {
|
||||
transition: background-color 0.5s,
|
||||
border 0.5s,
|
||||
border-top 0.5s,
|
||||
border-right 0.5s,
|
||||
border-bottom 0.5s,
|
||||
border-left 0.5s,
|
||||
color 0.5s;
|
||||
}
|
||||
|
||||
/* Viewer container */
|
||||
.file_viewer {
|
||||
position: absolute;
|
||||
@@ -202,7 +223,10 @@ const loading_evt = e => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
/* Force some variable usage that is normally out of scope */
|
||||
background: var(--body_background);
|
||||
color: var(--body_text_color);
|
||||
}
|
||||
|
||||
/* Headerbar (row 1) */
|
||||
@@ -240,5 +264,10 @@ const loading_evt = e => {
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
border: 2px solid var(--separator);
|
||||
background-image: var(--background_image, var(--background_pattern));
|
||||
background-color: var(--background_pattern_color);
|
||||
background-size: var(--background_image_size, initial);
|
||||
background-position: var(--background_image_position, initial);
|
||||
background-repeat: var(--background_image_repeat, repeat);
|
||||
}
|
||||
</style>
|
||||
|
@@ -57,17 +57,12 @@ export const fs_update = async (path, opts) => {
|
||||
const form = new FormData()
|
||||
form.append("action", "update")
|
||||
|
||||
if (opts.created !== undefined) {
|
||||
form.append("created", opts.created.toISOString())
|
||||
}
|
||||
if (opts.modified !== undefined) {
|
||||
form.append("modified", opts.modified.toISOString())
|
||||
}
|
||||
if (opts.mode !== undefined) {
|
||||
form.append("mode", opts.mode)
|
||||
}
|
||||
if (opts.shared !== undefined) {
|
||||
form.append("shared", opts.shared)
|
||||
for (let key of Object.keys(opts)) {
|
||||
if (key === "created" || key === "modified") {
|
||||
form.append(key, opts[key].toISOString())
|
||||
} else {
|
||||
form.append(key, opts[key])
|
||||
}
|
||||
}
|
||||
|
||||
return await fs_check_response(
|
||||
|
@@ -5,6 +5,8 @@ import { fs_encode_path, fs_split_path } from "./FilesystemUtil";
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
|
||||
export let history_enabled = true
|
||||
|
||||
export let state = {
|
||||
// Parts of the raw API response
|
||||
path: [],
|
||||
@@ -55,12 +57,14 @@ export const open_node = (node, push_history) => {
|
||||
// Update window title and navigation history. If push_history is false we
|
||||
// still replace the URL with replaceState. This way the user is not greeted
|
||||
// to a 404 page when refreshing after renaming a file
|
||||
window.document.title = node.path[node.base_index].name+" ~ pixeldrain"
|
||||
let url = "/d"+ fs_encode_path(node.path[node.base_index].path)
|
||||
if (push_history) {
|
||||
window.history.pushState({}, window.document.title, url)
|
||||
} else {
|
||||
window.history.replaceState({}, window.document.title, url)
|
||||
if (history_enabled) {
|
||||
window.document.title = node.path[node.base_index].name+" ~ pixeldrain"
|
||||
let url = "/d"+ fs_encode_path(node.path[node.base_index].path)
|
||||
if (push_history) {
|
||||
window.history.pushState({}, window.document.title, url)
|
||||
} else {
|
||||
window.history.replaceState({}, window.document.title, url)
|
||||
}
|
||||
}
|
||||
|
||||
// If the new node is a child of the previous node we save the parent's
|
||||
|
217
svelte/src/filesystem/filemanager/FilePicker.svelte
Normal file
217
svelte/src/filesystem/filemanager/FilePicker.svelte
Normal file
@@ -0,0 +1,217 @@
|
||||
<script>
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import ListView from './ListView.svelte'
|
||||
import GalleryView from './GalleryView.svelte'
|
||||
import Modal from '../../util/Modal.svelte';
|
||||
import Navigator from '../Navigator.svelte';
|
||||
import LoadingIndicator from '../../util/LoadingIndicator.svelte';
|
||||
|
||||
let fs_navigator
|
||||
let state
|
||||
let modal
|
||||
let dispatch = createEventDispatcher()
|
||||
let directory_view = ""
|
||||
let loading = false
|
||||
const loading_evt = e => loading = e.detail
|
||||
let large_icons = false
|
||||
let show_hidden = false
|
||||
export let select_multiple = false
|
||||
|
||||
export const open = path => {
|
||||
modal.show()
|
||||
fs_navigator.navigate(path, false)
|
||||
}
|
||||
|
||||
$: selected_files = state && state.children.reduce((acc, file) => {
|
||||
if (file.fm_selected) {
|
||||
acc++
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
|
||||
// Navigation functions
|
||||
|
||||
const node_click = e => {
|
||||
let index = e.detail
|
||||
|
||||
if (state.children[index].type === "dir") {
|
||||
fs_navigator.navigate(state.children[index].path, true)
|
||||
} else {
|
||||
select_node(index)
|
||||
}
|
||||
}
|
||||
let node_context = e => {
|
||||
// If this is a touch event we will select the item
|
||||
if (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) {
|
||||
e.detail.event.preventDefault()
|
||||
node_select({detail: e.detail.index})
|
||||
}
|
||||
}
|
||||
const node_select = e => {
|
||||
let index = e.detail
|
||||
mode = "selecting"
|
||||
state.children[index].fm_selected = !state.children[index].fm_selected
|
||||
}
|
||||
const navigate_up = () => {
|
||||
// Go to the path of the last parent
|
||||
if (state.path.length > 1) {
|
||||
fs_navigator.navigate(state.path[state.path.length-2].path, true)
|
||||
}
|
||||
}
|
||||
const toggle_view = () => {
|
||||
if (directory_view === "list") {
|
||||
directory_view = "gallery"
|
||||
} else {
|
||||
directory_view = "list"
|
||||
}
|
||||
|
||||
localStorage.setItem("directory_view", directory_view)
|
||||
}
|
||||
|
||||
// We need to detect if shift is pressed so we can select multiple items
|
||||
let shift_pressed = false
|
||||
let last_selected_node = -1
|
||||
const detect_shift = (e) => {
|
||||
if (e.key === "Shift") {
|
||||
shift_pressed = e.type === "keydown"
|
||||
}
|
||||
}
|
||||
|
||||
const select_node = index => {
|
||||
if (select_multiple && shift_pressed) {
|
||||
// If shift is pressed we do a range select. We select all files between
|
||||
// the last selected file and the file that is being selected now
|
||||
let id_low = Math.min(last_selected_node, index)
|
||||
let id_high = Math.max(last_selected_node, index)
|
||||
|
||||
for (let i = id_low; i <= id_high; i++) {
|
||||
if (i != last_selected_node) {
|
||||
state.children[i].fm_selected = !state.children[i].fm_selected
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Deselect all other entries first
|
||||
if (!select_multiple) {
|
||||
for (let i = 0; i < state.children.length; i++) {
|
||||
state.children[i].fm_selected = false
|
||||
}
|
||||
}
|
||||
state.children[index].fm_selected = !state.children[index].fm_selected
|
||||
}
|
||||
|
||||
last_selected_node = index
|
||||
}
|
||||
|
||||
let done = () => {
|
||||
let selected_files = []
|
||||
for (let i = 0; i < state.children.length; i++) {
|
||||
if (state.children[i].fm_selected) {
|
||||
selected_files.push(state.children[i])
|
||||
}
|
||||
}
|
||||
|
||||
if (selected_files.length > 0) {
|
||||
dispatch("files", selected_files)
|
||||
}
|
||||
modal.hide()
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if(typeof Storage !== "undefined") {
|
||||
directory_view = localStorage.getItem("directory_view")
|
||||
large_icons = localStorage.getItem("large_icons") === "true"
|
||||
}
|
||||
if (directory_view === "" || directory_view === null) {
|
||||
directory_view = "list"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={detect_shift} on:keyup={detect_shift} />
|
||||
|
||||
<Navigator
|
||||
history_enabled={false}
|
||||
bind:this={fs_navigator}
|
||||
bind:state
|
||||
on:loading={loading_evt}
|
||||
/>
|
||||
|
||||
<Modal bind:this={modal} width="900px">
|
||||
<div class="header" slot="title">
|
||||
<button class="button round" on:click={modal.hide}>
|
||||
<i class="icon">close</i>
|
||||
</button>
|
||||
<button on:click={navigate_up} disabled={state.path.length <= 1} title="Up">
|
||||
<i class="icon">north</i>
|
||||
</button>
|
||||
<button on:click={fs_navigator.reload()} title="Refresh directory listing">
|
||||
<i class="icon">refresh</i>
|
||||
</button>
|
||||
|
||||
<div class="title">
|
||||
Selected {selected_files} files
|
||||
</div>
|
||||
|
||||
<button on:click={() => {show_hidden = !show_hidden}} title="Toggle hidden files">
|
||||
{#if show_hidden}
|
||||
<i class="icon">visibility_off</i>
|
||||
{:else}
|
||||
<i class="icon">visibility</i>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button on:click={() => toggle_view()} title="Switch between gallery view and list view">
|
||||
{#if directory_view === "list"}
|
||||
<i class="icon">collections</i>
|
||||
{:else if directory_view === "gallery"}
|
||||
<i class="icon">list</i>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button class="button button_highlight round" on:click={done}>
|
||||
<i class="icon">done</i> Pick
|
||||
</button>
|
||||
</div>
|
||||
{#if directory_view === "list"}
|
||||
<ListView
|
||||
state={state}
|
||||
show_hidden={show_hidden}
|
||||
large_icons={large_icons}
|
||||
on:node_click={node_click}
|
||||
on:node_context={node_context}
|
||||
on:node_select={node_select}
|
||||
/>
|
||||
{:else if directory_view === "gallery"}
|
||||
<GalleryView
|
||||
state={state}
|
||||
show_hidden={show_hidden}
|
||||
large_icons={large_icons}
|
||||
on:node_click={node_click}
|
||||
on:node_context={node_context}
|
||||
on:node_select={node_select}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<LoadingIndicator loading={loading}/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1em;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.title {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
</style>
|
@@ -46,9 +46,9 @@ export let large_icons = false
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: var(--input_background);
|
||||
color: var(--input_text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--body_text_color);
|
||||
transition: background 0.2s;
|
||||
text-decoration: none;
|
||||
padding: 3px;
|
||||
|
@@ -120,6 +120,7 @@ td {
|
||||
.action_button {
|
||||
margin: 0;
|
||||
background: none;
|
||||
color: var(--body_text_color);
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
|
@@ -66,5 +66,6 @@ export let form = ""
|
||||
.flat {
|
||||
background: none;
|
||||
margin: 0;
|
||||
color: var(--body_text_color);
|
||||
}
|
||||
</style>
|
||||
|
@@ -215,7 +215,7 @@ func (s styleSheet) String() string {
|
||||
--highlight_color: %s;
|
||||
--highlight_text_color: %s;
|
||||
--danger_color: %s;
|
||||
--danger_color_dark: %s;
|
||||
--danger_text_color: %s;
|
||||
--scrollbar_foreground_color: %s;
|
||||
--scrollbar_hover_color: %s;
|
||||
|
||||
@@ -247,7 +247,7 @@ func (s styleSheet) String() string {
|
||||
s.Highlight.CSS(),
|
||||
s.HighlightText.CSS(),
|
||||
s.Danger.CSS(),
|
||||
s.Danger.Add(0, 0, -.02).CSS(),
|
||||
s.HighlightText.CSS(),
|
||||
s.ScrollbarForeground.CSS(),
|
||||
s.ScrollbarHover.CSS(),
|
||||
s.BackgroundColor.CSS(),
|
||||
|
Reference in New Issue
Block a user