Add swipe controls to move back and forth in albums

This commit is contained in:
2024-04-11 18:40:26 +02:00
parent 089fa3ad84
commit c5432c7541
9 changed files with 169 additions and 40 deletions

View File

@@ -23,7 +23,7 @@
font-style: normal; font-style: normal;
font-size: 1.5em; font-size: 1.5em;
display: inline-block; display: inline-block;
line-height: 1; line-height: 1em;
text-transform: none; text-transform: none;
letter-spacing: normal; letter-spacing: normal;
word-wrap: normal; word-wrap: normal;
@@ -496,7 +496,7 @@ select {
border-radius: 6px; border-radius: 6px;
margin: 2px; margin: 2px;
background: var(--input_background); background: var(--input_background);
padding: 3px 4px; padding: 3px;
overflow: hidden; overflow: hidden;
color: var(--input_text); color: var(--input_text);
cursor: pointer; cursor: pointer;
@@ -543,7 +543,7 @@ input[type="color"]:active,
select:active { select:active {
box-shadow: inset 4px 4px 6px var(--shadow_color); box-shadow: inset 4px 4px 6px var(--shadow_color);
/* Exactly 3px offset compared to the inactive padding to give a depth effect */ /* Exactly 3px offset compared to the inactive padding to give a depth effect */
padding: 6px 1px 0px 7px; padding: 6px 0px 0px 6px;
} }
.button_highlight { .button_highlight {
@@ -654,7 +654,7 @@ input[type="datetime-local"] {
border: none; border: none;
border-radius: 5px; border-radius: 5px;
background: var(--input_background); background: var(--input_background);
padding: 3px 4px; padding: 3px;
box-shadow: inset 1px 1px 0px 0px var(--shadow_color); box-shadow: inset 1px 1px 0px 0px var(--shadow_color);
/* override user-agent style */ /* override user-agent style */
min-width: 100px; min-width: 100px;

View File

@@ -372,8 +372,9 @@ const keyboard_event = evt => {
{#if !disable_menu} {#if !disable_menu}
<button <button
on:click={toolbar_toggle} on:click={toolbar_toggle}
class="button_toggle_toolbar round" class="round"
class:button_highlight={toolbar_visible} class:button_highlight={toolbar_visible}
style="line-height: 1em;"
title="Open or close the toolbar"> title="Open or close the toolbar">
<i class="icon">menu</i> <i class="icon">menu</i>
</button> </button>

View File

@@ -94,7 +94,6 @@ export const set_item = idx => {
.nav_button{ .nav_button{
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
margin: 4px;
} }
.list_navigator { .list_navigator {
@@ -107,9 +106,9 @@ export const set_item = idx => {
.file_button { .file_button {
position: relative; position: relative;
height: 48px; height: 2.6em;
width: 220px; width: 220px;
margin: 4px 3px; margin: 2px;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
border-radius: 6px; border-radius: 6px;
@@ -124,7 +123,7 @@ export const set_item = idx => {
text-decoration: none; text-decoration: none;
vertical-align: top; vertical-align: top;
cursor: pointer; cursor: pointer;
border-width: 2px; border-width: 1px;
border-style: solid; border-style: solid;
border-color: var(--input_background); border-color: var(--input_background);
box-shadow: 1px 1px 0px 0px var(--shadow_color); box-shadow: 1px 1px 0px 0px var(--shadow_color);

View File

@@ -182,9 +182,12 @@ const report_description = () => {
{#each abuse_categories as cat} {#each abuse_categories as cat}
{#if cat.filter === undefined || cat.filter(file.mime_type) } {#if cat.filter === undefined || cat.filter(file.mime_type) }
<label class="type_label" for="type_{cat.type}"> <label for="type_{cat.type}">
<input class="type_button" type="radio" bind:group={abuse_type} id="type_{cat.type}" name="abuse_type" value="{cat.type}"> <input type="radio" bind:group={abuse_type} id="type_{cat.type}" name="abuse_type" value="{cat.type}">
<span class="type_desc"><b>{cat.name}</b>: {cat.desc}</span> <div>
<b>{cat.name}</b><br/>
{cat.desc}
</div>
</label> </label>
{/if} {/if}
{/each} {/each}
@@ -193,11 +196,11 @@ const report_description = () => {
<h3>Report multiple files?</h3> <h3>Report multiple files?</h3>
<label for="report_single"> <label for="report_single">
<input type="radio" bind:group={single_or_all} id="report_single" name="single_or_all" value="single"> <input type="radio" bind:group={single_or_all} id="report_single" name="single_or_all" value="single">
Report only the selected file ({file.name}) <div>Report only the selected file ({file.name})</div>
</label> </label>
<label for="report_all" style="border-bottom: none;"> <label for="report_all" style="border-bottom: none;">
<input type="radio" bind:group={single_or_all} id="report_all" name="single_or_all" value="all"> <input type="radio" bind:group={single_or_all} id="report_all" name="single_or_all" value="all">
Report all {list.files.length} files in this album <div>Report all {list.files.length} files in this album</div>
</label> </label>
{/if} {/if}
@@ -270,9 +273,22 @@ const report_description = () => {
overflow: hidden; overflow: hidden;
} }
label { label {
display: block; padding: 0.2em;
border-bottom: 1px var(--separator) solid; display: flex;
padding: 0.5em; flex-direction: row;
}
label > input {
flex: 0 0 auto;
margin-right: 0.5em;
}
label > div {
flex: 1 1 auto;
padding: 0 0.2em;
border-radius: 6px;
border: 1px solid var(--separator);
}
input[type="radio"]:checked+div {
border-color: var(--highlight_color);
} }
.spinner_container { .spinner_container {
position: absolute; position: absolute;
@@ -294,15 +310,4 @@ label {
.report_form > textarea { .report_form > textarea {
height: 5em; height: 5em;
} }
.type_label {
display: flex;
flex-direction: row;
}
.type_button {
flex: 0 0 auto;
margin-right: 1em;
}
.type_label {
flex: 1 1 auto;
}
</style> </style>

View File

@@ -59,7 +59,7 @@ export const set_file = async file => {
{:else if viewer_type === "rate_limit"} {:else if viewer_type === "rate_limit"}
<RateLimit bind:this={viewer} on:download></RateLimit> <RateLimit bind:this={viewer} on:download></RateLimit>
{:else if viewer_type === "image"} {:else if viewer_type === "image"}
<Image bind:this={viewer} on:loading></Image> <Image bind:this={viewer} is_list={is_list} on:prev on:next on:loading></Image>
{:else if viewer_type === "video"} {:else if viewer_type === "video"}
<Video bind:this={viewer} is_list={is_list} on:loading on:download on:prev on:next on:reload></Video> <Video bind:this={viewer} is_list={is_list} on:loading on:download on:prev on:next on:reload></Video>
{:else if viewer_type === "audio"} {:else if viewer_type === "audio"}

View File

@@ -1,20 +1,36 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { swipe_nav } from "./SwipeNavigate.svelte";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export const set_file = f => file = f export const set_file = f => {
file = f
dispatch("loading", true)
}
let file = { let file = {
id: "", id: "",
name: "", name: "",
mime_type: "", mime_type: "",
get_href: "", get_href: "",
} }
export let is_list = false
let container let container
let zoom = false let zoom = false
let x, y = 0 let x, y = 0
let dragging = false let dragging = false
// For some reason the dblclick event is firing twice during testing.. So here's
// an event debouncer
let last_dblclick = 0
const double_click = e => {
let now = Date.now()
if (now - last_dblclick > 500) {
zoom = !zoom
}
last_dblclick = now
}
const mousedown = (e) => { const mousedown = (e) => {
if (!dragging && e.which === 1 && zoom) { if (!dragging && e.which === 1 && zoom) {
x = e.pageX x = e.pageX
@@ -48,20 +64,35 @@ const mouseup = (e) => {
return false return false
} }
} }
let container_style = ""
const on_load = () => {
dispatch("loading", false)
container_style = ""
}
</script> </script>
<svelte:window on:mousemove={mousemove} on:mouseup={mouseup} /> <svelte:window on:mousemove={mousemove} on:mouseup={mouseup} />
<div bind:this={container} class="container" class:zoom> <div
bind:this={container}
class="container"
class:zoom
use:swipe_nav={!zoom && is_list}
on:style={e => container_style = e.detail}
on:prev
on:next
style={container_style}
>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<img <img
on:loadstart={() => {dispatch("loading", true)}} on:load={on_load}
on:load={() => {dispatch("loading", false)}} on:error={on_load}
on:error={() => {dispatch("loading", false)}} on:dblclick={double_click}
on:dblclick={() => {zoom = !zoom}} on:doubletap={double_click}
on:doubletap={() => {zoom = !zoom}}
on:mousedown={mousedown} on:mousedown={mousedown}
class="image" class:zoom class="image"
class:zoom
src={file.get_href} src={file.get_href}
alt={file.name} /> alt={file.name} />
</div> </div>

View File

@@ -0,0 +1,76 @@
<script context="module">
export const swipe_nav = (node, swipe_enabled) => {
let start_x = 0
let start_y = 0
let render_offset = 0
let enabled = swipe_enabled
const touchstart = e => {
start_x = e.touches[0].clientX
start_y = e.touches[0].clientY
render_offset = 0
}
const touchmove = e => {
if (!enabled) {
return
}
let offset_x = e.touches[0].clientX - start_x
let abs_x = Math.abs(offset_x)
let abs_y = Math.abs(e.touches[0].clientY - start_y)
let neg = offset_x < 0 ? -1 : 1
if (abs_x > 100 && abs_y < abs_x/3) {
set_offset((abs_x-100)*neg, false)
} else {
set_offset(0, true)
}
}
const touchend = e => {
if (!enabled) {
return
}
if (render_offset > 100) {
set_offset(1000, true)
node.dispatchEvent(new CustomEvent("prev"))
} else if (render_offset < -100) {
set_offset(-1000, true)
node.dispatchEvent(new CustomEvent("next"))
} else {
set_offset(0, true)
}
}
const set_offset = (off, animate) => {
render_offset = off
let detail = "transform: translateX("+off+"px);"
if (animate) {
detail += "transition: transform 500ms;"
}
node.dispatchEvent(new CustomEvent("style", {detail: detail}))
}
node.addEventListener("touchstart", touchstart)
node.addEventListener("touchmove", touchmove)
node.addEventListener("touchend", touchend)
return {
update(swipe_enabled) {
enabled = swipe_enabled
if (!enabled) {
render_offset = 0
}
},
destroy() {
node.removeEventListener("touchstart", touchstart)
node.removeEventListener("touchmove", touchmove)
node.removeEventListener("touchend", touchend)
}
}
}
</script>

View File

@@ -56,11 +56,11 @@ const state_update = async (base) => {
<CustomBanner path={state.path}/> <CustomBanner path={state.path}/>
</FileManager> </FileManager>
{:else if viewer_type === "audio"} {:else if viewer_type === "audio"}
<Audio bind:this={viewer} fs_navigator={fs_navigator} state={state}> <Audio state={state} bind:this={viewer} fs_navigator={fs_navigator}>
<CustomBanner path={state.path}/> <CustomBanner path={state.path}/>
</Audio> </Audio>
{:else if viewer_type === "image"} {:else if viewer_type === "image"}
<Image state={state} on:open_sibling/> <Image state={state} bind:this={viewer} on:open_sibling/>
{:else if viewer_type === "video"} {:else if viewer_type === "video"}
<Video state={state} bind:this={viewer} on:open_sibling/> <Video state={state} bind:this={viewer} on:open_sibling/>
{:else if viewer_type === "pdf"} {:else if viewer_type === "pdf"}

View File

@@ -1,12 +1,20 @@
<script> <script>
import { createEventDispatcher } from "svelte";
import { swipe_nav } from "../../file_viewer/viewers/SwipeNavigate.svelte";
import { fs_path_url } from "../FilesystemUtil"; import { fs_path_url } from "../FilesystemUtil";
let dispatch = createEventDispatcher()
export let state export let state
let container let container
let zoom = false let zoom = false
let x, y = 0 let x, y = 0
let dragging = false let dragging = false
let container_style = ""
export const update = () => {
container_style = ""
}
const mousedown = (e) => { const mousedown = (e) => {
if (!dragging && e.which === 1 && zoom) { if (!dragging && e.which === 1 && zoom) {
@@ -45,7 +53,16 @@ const mouseup = (e) => {
<svelte:window on:mousemove={mousemove} on:mouseup={mouseup} /> <svelte:window on:mousemove={mousemove} on:mouseup={mouseup} />
<div bind:this={container} class="container" class:zoom> <div
bind:this={container}
class="container"
class:zoom
use:swipe_nav={!zoom}
on:style={e => container_style = e.detail}
on:prev={() => dispatch("open_sibling", -1)}
on:next={() => dispatch("open_sibling", 1)}
style={container_style}
>
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<img <img
on:dblclick={() => {zoom = !zoom}} on:dblclick={() => {zoom = !zoom}}