Add header image to branding options

This commit is contained in:
2024-02-16 14:50:34 +01:00
parent b4f6bbb4c8
commit a4c5b97cdf
21 changed files with 298 additions and 154 deletions

View File

@@ -0,0 +1,101 @@
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)
}
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 = properties.brand_highlight_color
}
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)
style.background_pattern_color = properties.brand_background_color
}
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)
style.shadow_color = darken(properties.brand_body_color, 0.8)
}
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 the lightness is less than 40 it is considered a dark colour. This
// threshold is 40 instead of 50 because overall dark text is more legible
if (hsl[2] < 40) {
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
}
// Darken and desaturate. Only used for shadows
const darken = (color, percent) => {
let hsl = rgb2hsl(parse(color)) // Convert hex to hsl
hsl[1] = hsl[1] * percent
hsl[2] = hsl[2] * percent
return rgb2hex(hsl2rgb(hsl)) // Convert back to hex
}
const set_alpha = (color, amt) => {
let rgb = parse(color)
rgb.push(amt)
return "rgba(" + rgb.join(", ") + ")"
}

View File

@@ -0,0 +1,210 @@
<script>
import { createEventDispatcher } from "svelte";
import FilePicker from "../filemanager/FilePicker.svelte";
import { fs_update } from "../FilesystemAPI";
import { fs_node_type } from "../FilesystemUtil";
import CustomBanner from "../viewers/CustomBanner.svelte";
let dispatch = createEventDispatcher()
export let file = {
properties: {
branding_enabled: "",
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: "",
},
}
export let enabled = false
$: update_colors(file)
const update_colors = file => {
if (enabled) {
file.properties.branding_enabled = "true"
dispatch("style_change")
} 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}/>
<p class="span3">
You can choose an image to show above or behind the files in this
directory. The image will be picked from your filesystem. The image will
get a shared file link. You can move and rename the file like normal,
but if you remove the shared file property your branding will stop
working. Recommended dimensions for the header image are 1000x90 px.
</p>
<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>Below is your custom header image, if you chose one.</div>
<CustomBanner path={[file]}/>
<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

@@ -0,0 +1,220 @@
<script>
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";
import PathLink from "../util/PathLink.svelte";
import { branding_from_node } from "./Branding";
let dispatch = createEventDispatcher()
export let fs_navigator
let file = {
path: "",
name: "",
id: "",
mode_octal: "",
properties: {},
};
let custom_css = ""
$: is_root_dir = file.path === "/"+file.id
export let visible
export const edit = (f, oae = false, t = "file") => {
file = f
open_after_edit = oae
tab = t
console.log("Editing file", file)
file_name = file.name
shared = !(file.id === undefined || file.id === "")
if (file.properties === undefined) {
file.properties = {}
}
branding_enabled = file.properties.branding_enabled === "true"
if (branding_enabled) {
custom_css = branding_from_node(file)
} else {
custom_css = ""
}
visible = true
}
let tab = "file"
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)
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)
console.log("Moving", file.path, "to", parent+file_name)
await fs_rename(file.path, parent+file_name)
file.path = parent+file_name
}
} catch (err) {
if (err.message) {
alert(err.message)
} else {
console.error(err)
alert(err)
}
return
} finally {
dispatch("loading", false)
}
if (open_after_edit) {
fs_navigator.navigate(file.path, false)
} else {
fs_navigator.reload()
}
}
const delete_file = async e => {
e.preventDefault()
try {
dispatch("loading", true)
await fs_delete_all(file.path)
} catch (err) {
console.error(err)
alert(err)
return
} finally {
dispatch("loading", false)
}
if (open_after_edit) {
fs_navigator.navigate(file.path, false)
} else {
fs_navigator.reload()
}
visible = false
}
</script>
<Modal bind:visible={visible} title="Edit {file.name}" width="700px" form="edit_form" style="color: var(--body_text_color); {custom_css}">
<div class="tab_bar">
<button class:button_highlight={tab === "file"} on:click={() => tab = "file"}>
<i class="icon">edit</i>
Properties
</button>
<button class:button_highlight={tab === "share"} on:click={() => tab = "share"}>
<i class="icon">share</i>
Sharing
</button>
<button class:button_highlight={tab === "branding"} on:click={() => tab = "branding"}>
<i class="icon">palette</i>
Branding
</button>
</div>
<form id="edit_form" on:submit|preventDefault={save}></form>
{#if tab === "file"}
<div class="tab_content">
<h2>File settings</h2>
{#if is_root_dir}
<div class="highlight_yellow">
Filesystem root cannot be renamed. If this shared directory
is in
<PathLink nav={fs_navigator} path="/me">your filesystem</PathLink>
you can rename it from there
</div>
{/if}
<div class="form_grid">
<label for="file_name">Name</label>
<input form="edit_form" bind:value={file_name} id="file_name" type="text" class="form_input" disabled={is_root_dir}/>
</div>
<h2>Delete</h2>
<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">
<h2>Share this file/directory</h2>
<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 form="edit_form" bind:checked={shared} id="shared" type="checkbox" class="form_input"/>
<label for="shared">Share this file or directory</label>
</div>
</div>
{:else if tab === "branding"}
<div class="tab_content">
<BrandingOptions
bind:enabled={branding_enabled}
bind:colors={branding_colors}
file={file}
on:style_change={e => custom_css = branding_from_node(file)}
/>
</div>
{/if}
</Modal>
<style>
.tab_bar {
border-bottom: 2px solid var(--separator);
}
.tab_content {
display: flex;
flex-direction: column;
padding: 8px;
}
.form_grid {
display: grid;
grid-template-columns: 1fr 10fr;
align-items: center;
}
</style>