Add page branding options to filesystem

This commit is contained in:
2024-02-15 18:52:46 +01:00
parent a399c03869
commit 08676e6071
15 changed files with 730 additions and 104 deletions

View File

@@ -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"
},

View File

@@ -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"
}
}

View File

@@ -39,6 +39,7 @@ export let nobg = false
.nobg {
background: none;
margin: 0;
color: var(--body_text_color);
}
@media (max-width: 600px) {

View 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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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(

View File

@@ -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

View 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>

View File

@@ -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;

View File

@@ -120,6 +120,7 @@ td {
.action_button {
margin: 0;
background: none;
color: var(--body_text_color);
}
.hidden {
display: none;

View File

@@ -66,5 +66,6 @@ export let form = ""
.flat {
background: none;
margin: 0;
color: var(--body_text_color);
}
</style>