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

@@ -4,9 +4,10 @@ import { formatDataVolume, formatDate, formatThousands } from "../util/Formattin
import Modal from "../util/Modal.svelte";
import { fs_timeseries } from "./FilesystemAPI";
import { fs_path_url } from "./FilesystemUtil";
import { generate_share_url } from "./Sharebar.svelte";
import { color_by_name } from "../util/Util.svelte";
import { generate_share_path, generate_share_url } from "./Sharebar.svelte";
import { color_by_name, copy_text } from "../util/Util.svelte";
import { tick } from "svelte";
import Button from "../layout/Button.svelte";
export let state
export let visible = false
@@ -21,6 +22,7 @@ const visibility_change = visible => {
$: direct_url = window.location.origin+fs_path_url(state.base.path)
$: share_url = generate_share_url(state.path)
$: direct_share_url = window.location.origin+fs_path_url(generate_share_path(state.path))
let chart
let chart_timespan = 0
@@ -171,9 +173,28 @@ let update_chart = async (base, timespan, interval) => {
</tr>
<tr><td>SHA256 sum</td><td>{state.base.sha256_sum}</td></tr>
{/if}
<tr><td>Direct URL</td><td><a href="{direct_url}">{direct_url}</a></td></tr>
<tr>
<td>Direct link</td>
<td>
<Button highlight_on_click icon="content_copy" label="Copy" click={e => copy_text(direct_url)}/>
<a href="{direct_url}">{direct_url}</a>
</td>
</tr>
{#if share_url !== ""}
<tr><td>Share URL</td><td><a href="{share_url}">{share_url}</a></td></tr>
<tr>
<td>Sharing link</td>
<td>
<Button highlight_on_click icon="content_copy" label="Copy" click={e => copy_text(share_url)}/>
<a href="{share_url}">{share_url}</a>
</td>
</tr>
<tr>
<td>Direct sharing link</td>
<td>
<Button highlight_on_click icon="content_copy" label="Copy" click={e => copy_text(direct_share_url)}/>
<a href="{direct_share_url}">{direct_share_url}</a>
</td>
</tr>
{/if}
</table>

View File

@@ -1,7 +1,7 @@
<script>
import { onMount } from 'svelte';
import LoadingIndicator from '../util/LoadingIndicator.svelte';
import EditWindow from './EditWindow.svelte';
import EditWindow from './edit_window/EditWindow.svelte';
import Toolbar from './Toolbar.svelte';
import Breadcrumbs from './Breadcrumbs.svelte';
import DetailsWindow from './DetailsWindow.svelte';
@@ -10,8 +10,8 @@ import FilePreview from './viewers/FilePreview.svelte';
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';
import { fs_path_url } from './FilesystemUtil.js';
import { branding_from_path } from './edit_window/Branding.js'
let loading = true
let toolbar
@@ -179,7 +179,6 @@ const update_css = path => document.documentElement.style = branding_from_path(p
bind:visible={edit_visible}
fs_navigator={fs_navigator}
on:loading={loading_evt}
on:style_change={e => document.documentElement.style = branding_from_node(e.detail)}
/>
<UploadWidget

View File

@@ -13,6 +13,9 @@ export const fs_encode_path = path => {
}
export const fs_path_url = path => {
if (path[0] !== "/") {
path = "/" + path
}
return window.api_endpoint + "/filesystem" + fs_encode_path(path)
}

View File

@@ -1,5 +1,12 @@
<script context="module">
export const generate_share_url = path => {
let share_path = generate_share_path(path)
if (share_path !== "") {
share_path = window.location.protocol+"//"+window.location.host+"/d/"+share_path
}
return share_path
}
export const generate_share_path = path => {
let share_url = ""
let bucket_idx = -1
@@ -11,9 +18,7 @@ export const generate_share_url = path => {
}
}
if (bucket_idx !== -1) {
share_url = window.location.protocol+"//"+
window.location.host+"/d/"+
path[bucket_idx].id
share_url = path[bucket_idx].id
// Construct the path starting from the bucket
for (let i = bucket_idx+1; i < path.length; i++) {

View File

@@ -106,10 +106,12 @@ let expand = e => {
</button>
{/if}
<button on:click={share}>
<i class="icon">share</i>
<span>Share</span>
</button>
{#if state.base.id !== "me"}
<button on:click={share}>
<i class="icon">share</i>
<span>Share</span>
</button>
{/if}
<button on:click={() => details_visible = !details_visible} class:button_highlight={details_visible}>
<i class="icon">help</i>

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

@@ -1,124 +1,21 @@
<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)
}
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 30 it is considered a dark colour. This
// threshold is 30 instead of 50 because overall dark text is more legible
if (hsl[2] < 30) {
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(", ")+")"
}
</script>
<script>
import { createEventDispatcher } from "svelte";
import FilePicker from "./filemanager/FilePicker.svelte";
import { fs_update } from "./FilesystemAPI";
import { fs_node_type } from "./FilesystemUtil";
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: "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",
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: "",
@@ -133,7 +30,7 @@ $: update_colors(file)
const update_colors = file => {
if (enabled) {
file.properties.branding_enabled = "true"
dispatch("style_change", file)
dispatch("style_change")
} else {
file.properties.branding_enabled = ""
}
@@ -208,15 +105,13 @@ const handle_picker = async e => {
<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/>
<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.
</div>
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>
@@ -243,7 +138,9 @@ const handle_picker = async e => {
<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">

View File

@@ -1,10 +1,11 @@
<script>
import { fs_delete_all, fs_rename, fs_update } from "./FilesystemAPI";
import Modal from "../util/Modal.svelte";
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 Button from "../../layout/Button.svelte";
import BrandingOptions from "./BrandingOptions.svelte";
import PathLink from "./util/PathLink.svelte";
import PathLink from "../util/PathLink.svelte";
import { branding_from_node } from "./Branding";
let dispatch = createEventDispatcher()
@@ -17,6 +18,7 @@ let file = {
properties: {},
};
let custom_css = ""
$: is_root_dir = file.path === "/"+file.id
export let visible
@@ -33,7 +35,14 @@ export const edit = (f, oae = false, t = "file") => {
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
}
@@ -127,7 +136,7 @@ const delete_file = async e => {
}
</script>
<Modal bind:visible={visible} title="Edit {file.name}" width="700px" form="edit_form">
<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>
@@ -184,7 +193,12 @@ const delete_file = async e => {
</div>
{:else if tab === "branding"}
<div class="tab_content">
<BrandingOptions bind:enabled={branding_enabled} bind:colors={branding_colors} file={file} on:style_change/>
<BrandingOptions
bind:enabled={branding_enabled}
bind:colors={branding_colors}
file={file}
on:style_change={e => custom_css = branding_from_node(file)}
/>
</div>
{/if}
</Modal>

View File

@@ -71,9 +71,9 @@ const node_select = e => {
state.children[index].fm_selected = !state.children[index].fm_selected
}
const node_settings = e => {
edit_window.edit(state.children[e.detail], false)
}
const node_settings = e => edit_window.edit(state.children[e.detail], false)
const node_branding = e => edit_window.edit(state.children[e.detail], false, "branding")
const navigate_up = () => {
creating_dir = false
@@ -352,6 +352,8 @@ onMount(() => {
{/if}
</div>
<slot></slot>
{#if directory_view === "list"}
<ListView
state={state}
@@ -361,6 +363,7 @@ onMount(() => {
on:node_context={node_context}
on:node_share_click={node_share_click}
on:node_settings={node_settings}
on:node_branding={node_branding}
on:node_select={node_select}
/>
{:else if directory_view === "gallery"}

View File

@@ -5,6 +5,7 @@ import GalleryView from './GalleryView.svelte'
import Modal from '../../util/Modal.svelte';
import Navigator from '../Navigator.svelte';
import LoadingIndicator from '../../util/LoadingIndicator.svelte';
import Breadcrumbs from '../Breadcrumbs.svelte'
let fs_navigator
let state
@@ -172,6 +173,9 @@ onMount(() => {
<i class="icon">done</i> Pick
</button>
</div>
<Breadcrumbs state={state} fs_navigator={fs_navigator}/>
{#if directory_view === "list"}
<ListView
state={state}

View File

@@ -47,8 +47,13 @@ export let large_icons = false
<i class="icon" title="This file / directory is shared. Click to open public link">share</i>
</a>
{/if}
{#if child.properties && child.properties.branding_enabled}
<button class="action_button" on:click|preventDefault|stopPropagation={() => dispatch("node_branding", index)}>
<i class="icon">palette</i>
</button>
{/if}
{#if state.permissions.update}
<button class="action_button" on:click|preventDefault|stopPropagation={() => {dispatch("node_settings", index)}}>
<button class="action_button" on:click|preventDefault|stopPropagation={() => dispatch("node_settings", index)}>
<i class="icon">edit</i>
</button>
{/if}

View File

@@ -1,6 +1,7 @@
<script>
import { createEventDispatcher, onMount } from 'svelte'
import { fs_path_url } from '../FilesystemUtil';
import FileTitle from '../../file_viewer/viewers/FileTitle.svelte';
let dispatch = createEventDispatcher()
export let state
@@ -37,7 +38,9 @@ onMount(() => {
})
</script>
<slot></slot>
<div class="container">
<FileTitle title={state.base.name}/>
<button on:click={() => dispatch("open_sibling", -1) }><i class="icon">skip_previous</i></button>
<button on:click={() => player.currentTime -= 10 }><i class="icon">replay_10</i></button>
<button on:click={toggle_play}>
@@ -65,7 +68,6 @@ onMount(() => {
<style>
.container {
margin: 50px 0 0 0;
padding: 0;
overflow-y: auto;
text-align: center;

View File

@@ -0,0 +1,50 @@
<script>
export let path = []
let image_uri
let image_link
$: update_links(path)
const update_links = (path) => {
image_uri = null
image_link = null
for (let node of path) {
if (node.properties && node.properties.branding_enabled === "true") {
if (node.properties.brand_header_image) {
image_uri = "/api/filesystem/"+node.properties.brand_header_image
}
if (node.properties.brand_header_link) {
image_link = node.properties.brand_header_link
}
}
}
}
</script>
{#if image_uri}
<div class="container">
{#if image_link}
<a class="link" href={image_link} target="_blank" rel="noreferrer">
<img class="image" src="{image_uri}" alt="User-provided banner"/>
</a>
{:else}
<img class="image" src="{image_uri}" alt="User-provided banner"/>
{/if}
</div>
{/if}
<style>
.container {
margin: 6px 0;
text-align: center;
}
.link {
display: inline-block;
}
.image {
display: inline-block;
margin: auto;
max-height: 90px;
max-width: 100%;
border-radius: 6px;
}
</style>

View File

@@ -7,6 +7,8 @@ let dispatch = createEventDispatcher()
export let state
</script>
<slot></slot>
<h1>{state.base.name}</h1>
<IconBlock icon_href={fs_thumbnail_url(state.base.path, 256, 256)}>

View File

@@ -11,6 +11,7 @@ import Text from "./Text.svelte";
import Video from "./Video.svelte";
import Torrent from "./Torrent.svelte";
import Zip from "./Zip.svelte";
import CustomBanner from "./CustomBanner.svelte";
export let fs_navigator
export let edit_window
@@ -51,9 +52,13 @@ const state_update = async (base) => {
edit_window={edit_window}
on:loading
on:upload_picker
/>
>
<CustomBanner path={state.path}/>
</FileManager>
{:else if viewer_type === "audio"}
<Audio state={state} on:open_sibling/>
<Audio state={state} on:open_sibling>
<CustomBanner path={state.path}/>
</Audio>
{:else if viewer_type === "image"}
<Image state={state} on:open_sibling/>
{:else if viewer_type === "video"}
@@ -61,13 +66,21 @@ const state_update = async (base) => {
{:else if viewer_type === "pdf"}
<Pdf state={state}/>
{:else if viewer_type === "text"}
<Text state={state}/>
<Text state={state}>
<CustomBanner path={state.path}/>
</Text>
{:else if viewer_type === "torrent"}
<Torrent state={state} bind:this={viewer} on:loading on:download/>
<Torrent state={state} bind:this={viewer} on:loading on:download>
<CustomBanner path={state.path}/>
</Torrent>
{:else if viewer_type === "zip"}
<Zip state={state} bind:this={viewer} on:loading on:download />
<Zip state={state} bind:this={viewer} on:loading on:download>
<CustomBanner path={state.path}/>
</Zip>
{:else}
<File state={state} on:download/>
<File state={state} on:download>
<CustomBanner path={state.path}/>
</File>
{/if}
<style>

View File

@@ -75,6 +75,7 @@ const mouseup = (e) => {
max-width: 100%;
max-height: 100%;
cursor: pointer;
box-shadow: 1px 1px 4px 0px var(--shadow_color);
}
.image.zoom {
max-width: none;

View File

@@ -27,6 +27,7 @@ export const set_file = file => {
<div class="container">
<slot></slot>
<pre bind:this={text_pre}>
Loading...
</pre>

View File

@@ -73,6 +73,8 @@ const copy_magnet = () => {
}
</script>
<slot></slot>
<h1>{state.base.name}</h1>
<IconBlock icon_href={fs_node_icon(state.base, 256, 256)}>

View File

@@ -59,6 +59,8 @@ const recursive_size = (file) => {
}
</script>
<slot></slot>
<h1>{state.base.name}</h1>
<IconBlock icon_href={fs_node_icon(state.base, 256, 256)}>

View File

@@ -1,5 +1,6 @@
<script>
export let highlight = false;
export let highlight_on_click = false
export let red = false;
export let round = false;
export let flat = false;
@@ -14,11 +15,25 @@ export let click = e => {}
export let style = ""
export let type = ""
export let form = ""
let click_int = e => {
if (highlight_on_click) {
try {
click(e)
highlight = true
} catch (err) {
red = true
throw err
}
} else {
click(e)
}
}
</script>
{#if link_target === ""}
<button
on:click={click}
on:click={click_int}
class="button"
class:button_highlight={highlight}
class:button_red={red}

View File

@@ -18,6 +18,7 @@ export let width = "800px";
export let height = "auto";
export let padding = false;
export let visible = false;
export let style = "";
const load_bg = background => {
background.style.zIndex = global_index.valueOf();
@@ -53,6 +54,7 @@ const keydown = e => {
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div
class="background"
style={style}
use:load_bg
on:click={hide}
transition:fade={{duration: 200}}