mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
web/lightbox: add control buttons
This commit is contained in:
parent
26346df920
commit
6bb1d4477c
13 changed files with 1082 additions and 21 deletions
|
@ -20,7 +20,7 @@ export default tseslint.config(
|
||||||
"import": pluginImport,
|
"import": pluginImport,
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
"import/extensions": [".ts", ".tsx", ".css"],
|
"import/extensions": [".ts", ".tsx", ".css", ".svg"],
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
|
@ -34,6 +34,11 @@ export default tseslint.config(
|
||||||
"named": true,
|
"named": true,
|
||||||
"warnOnUnassignedImports": true,
|
"warnOnUnassignedImports": true,
|
||||||
"pathGroups": [{
|
"pathGroups": [{
|
||||||
|
"pattern": "*.svg?react",
|
||||||
|
"patternOptions": {"matchBase": true},
|
||||||
|
"group": "sibling",
|
||||||
|
"position": "after",
|
||||||
|
}, {
|
||||||
"pattern": "*.css",
|
"pattern": "*.css",
|
||||||
"patternOptions": {"matchBase": true},
|
"patternOptions": {"matchBase": true},
|
||||||
"group": "sibling",
|
"group": "sibling",
|
||||||
|
|
975
web/package-lock.json
generated
975
web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -30,7 +30,8 @@
|
||||||
"globals": "^15.9.0",
|
"globals": "^15.9.0",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.7.0",
|
"typescript-eslint": "^8.7.0",
|
||||||
"vite": "^5.4.8"
|
"vite": "^5.4.8",
|
||||||
|
"vite-plugin-svgr": "^4.2.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"@types/react": "npm:types-react@rc",
|
"@types/react": "npm:types-react@rc",
|
||||||
|
|
1
web/src/icons/close.svg
Normal file
1
web/src/icons/close.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
|
After Width: | Height: | Size: 223 B |
1
web/src/icons/download.svg
Normal file
1
web/src/icons/download.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z"/></svg>
|
After Width: | Height: | Size: 279 B |
1
web/src/icons/rotate-left.svg
Normal file
1
web/src/icons/rotate-left.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M520-80q-51 0-100-14t-92-42l58-58q31 17 65 25.5t69 8.5q117 0 198.5-81.5T800-440q0-117-81.5-198.5T520-720h-6l62 62-56 58-160-160 160-160 56 58-62 62h6q150 0 255 105t105 255q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T520-80ZM280-200 40-440l240-240 240 240-240 240Zm0-114 126-126-126-126-126 126 126 126Zm0-126Z"/></svg>
|
After Width: | Height: | Size: 433 B |
1
web/src/icons/rotate-right.svg
Normal file
1
web/src/icons/rotate-right.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M440-80q-75 0-140.5-28.5t-114-77q-48.5-48.5-77-114T80-440q0-150 105-255t255-105h6l-62-62 56-58 160 160-160 160-56-58 62-62h-6q-117 0-198.5 81.5T160-440q0 117 81.5 198.5T440-160q35 0 69-8.5t65-25.5l58 58q-43 28-92 42T440-80Zm240-120L440-440l240-240 240 240-240 240Zm0-114 126-126-126-126-126 126 126 126Zm0-126Z"/></svg>
|
After Width: | Height: | Size: 436 B |
1
web/src/icons/zoom-in.svg
Normal file
1
web/src/icons/zoom-in.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Zm-40-60v-80h-80v-80h80v-80h80v80h80v80h-80v80h-80Z"/></svg>
|
After Width: | Height: | Size: 427 B |
1
web/src/icons/zoom-out.svg
Normal file
1
web/src/icons/zoom-out.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M784-120 532-372q-30 24-69 38t-83 14q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l252 252-56 56ZM380-400q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400ZM280-540v-80h200v80H280Z"/></svg>
|
After Width: | Height: | Size: 401 B |
|
@ -6,8 +6,35 @@ div.overlay {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
&.lightbox > div.controls {
|
||||||
|
position: fixed;
|
||||||
|
top: .5rem;
|
||||||
|
right: .5rem;
|
||||||
|
display: flex;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
> button, > a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #ccc;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.lightbox > img {
|
&.lightbox > img {
|
||||||
max-width: 75%;
|
max-width: 75%;
|
||||||
max-height: 75%;
|
max-height: 75%;
|
||||||
|
|
||||||
|
transition: rotate 0.2s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,12 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { Component, RefObject, createContext, createRef, useCallback, useState } from "react"
|
import React, { Component, RefObject, createContext, createRef, useCallback, useState } from "react"
|
||||||
|
import DownloadIcon from "../icons/download.svg?react"
|
||||||
|
import CloseIcon from "../icons/close.svg?react"
|
||||||
|
import RotateLeftIcon from "../icons/rotate-left.svg?react"
|
||||||
|
import RotateRightIcon from "../icons/rotate-right.svg?react"
|
||||||
|
import ZoomInIcon from "../icons/zoom-in.svg?react"
|
||||||
|
import ZoomOutIcon from "../icons/zoom-out.svg?react"
|
||||||
import "./Lightbox.css"
|
import "./Lightbox.css"
|
||||||
|
|
||||||
const isTouchDevice = window.ontouchstart !== undefined
|
const isTouchDevice = window.ontouchstart !== undefined
|
||||||
|
@ -56,7 +62,9 @@ export interface LightboxProps extends LightboxParams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Lightbox extends Component<LightboxProps> {
|
export class Lightbox extends Component<LightboxProps> {
|
||||||
transform = { zoom: 1, x: 0, y: 0 }
|
translate = { x: 0, y: 0 }
|
||||||
|
zoom = 1
|
||||||
|
rotate = 0
|
||||||
maybePanning = false
|
maybePanning = false
|
||||||
readonly ref: RefObject<HTMLImageElement | null>
|
readonly ref: RefObject<HTMLImageElement | null>
|
||||||
|
|
||||||
|
@ -65,8 +73,12 @@ export class Lightbox extends Component<LightboxProps> {
|
||||||
this.ref = createRef<HTMLImageElement>()
|
this.ref = createRef<HTMLImageElement>()
|
||||||
}
|
}
|
||||||
|
|
||||||
transformString = () => {
|
get style() {
|
||||||
return `translate(${this.transform.x}px, ${this.transform.y}px) scale(${this.transform.zoom})`
|
return {
|
||||||
|
translate: `${this.translate.x}px ${this.translate.y}px`,
|
||||||
|
rotate: `${this.rotate}deg`,
|
||||||
|
scale: `${this.zoom}`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick = () => {
|
onClick = () => {
|
||||||
|
@ -77,7 +89,9 @@ export class Lightbox extends Component<LightboxProps> {
|
||||||
this.ref.current.style.cursor = "auto"
|
this.ref.current.style.cursor = "auto"
|
||||||
this.maybePanning = false
|
this.maybePanning = false
|
||||||
} else {
|
} else {
|
||||||
this.transform = { zoom: 1, x: 0, y: 0 }
|
this.translate = { x: 0, y: 0 }
|
||||||
|
this.rotate = 0
|
||||||
|
this.zoom = 1
|
||||||
this.props.onClose()
|
this.props.onClose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,14 +101,16 @@ export class Lightbox extends Component<LightboxProps> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
const oldZoom = this.transform.zoom
|
const oldZoom = this.zoom
|
||||||
const delta = -evt.deltaY / 1000
|
const delta = -evt.deltaY / 1000
|
||||||
const newDelta = this.transform.zoom + delta * this.transform.zoom
|
const newDelta = this.zoom + delta * this.zoom
|
||||||
this.transform.zoom = Math.min(Math.max(newDelta, 0.01), 10)
|
this.zoom = Math.min(Math.max(newDelta, 0.01), 10)
|
||||||
const zoomDelta = this.transform.zoom - oldZoom
|
const zoomDelta = this.zoom - oldZoom
|
||||||
this.transform.x += zoomDelta * (this.ref.current.clientWidth / 2 - evt.nativeEvent.offsetX)
|
this.translate.x += zoomDelta * (this.ref.current.clientWidth / 2 - evt.nativeEvent.offsetX)
|
||||||
this.transform.y += zoomDelta * (this.ref.current.clientHeight / 2 - evt.nativeEvent.offsetY)
|
this.translate.y += zoomDelta * (this.ref.current.clientHeight / 2 - evt.nativeEvent.offsetY)
|
||||||
this.ref.current.style.transform = this.transformString()
|
const style = this.style
|
||||||
|
this.ref.current.style.translate = style.translate
|
||||||
|
this.ref.current.style.scale = style.scale
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseDown = (evt: React.MouseEvent) => {
|
onMouseDown = (evt: React.MouseEvent) => {
|
||||||
|
@ -114,24 +130,45 @@ export class Lightbox extends Component<LightboxProps> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
this.transform.x += evt.movementX
|
this.translate.x += evt.movementX
|
||||||
this.transform.y += evt.movementY
|
this.translate.y += evt.movementY
|
||||||
this.ref.current.style.transform = this.transformString()
|
this.ref.current.style.translate = this.style.translate
|
||||||
this.ref.current.style.cursor = "grabbing"
|
this.ref.current.style.cursor = "grabbing"
|
||||||
}
|
}
|
||||||
|
|
||||||
get style() {
|
transformer = (callback: () => void)=> (evt: React.MouseEvent) => {
|
||||||
return {
|
evt.stopPropagation()
|
||||||
transform: this.transformString(),
|
if (!this.ref.current) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
callback()
|
||||||
|
const style = this.style
|
||||||
|
this.ref.current.style.rotate = style.rotate
|
||||||
|
this.ref.current.style.scale = style.scale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopPropagation = (evt: React.MouseEvent) => evt.stopPropagation()
|
||||||
|
zoomIn = this.transformer(() => this.zoom = Math.min(this.zoom * 1.1, 10))
|
||||||
|
zoomOut = this.transformer(() => this.zoom = Math.max(this.zoom / 1.1, 0.01))
|
||||||
|
rotateLeft = this.transformer(() => this.rotate -= 90)
|
||||||
|
rotateRight = this.transformer(() => this.rotate += 90)
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div
|
return <div
|
||||||
className="overlay lightbox"
|
className="overlay lightbox"
|
||||||
onClick={this.onClick}
|
onClick={this.onClick}
|
||||||
onMouseMove={isTouchDevice ? undefined : this.onMouseMove}
|
onMouseMove={isTouchDevice ? undefined : this.onMouseMove}
|
||||||
>
|
>
|
||||||
|
<div className="controls" onClick={this.stopPropagation}>
|
||||||
|
<button onClick={this.zoomOut}><ZoomOutIcon/></button>
|
||||||
|
<button onClick={this.zoomIn}><ZoomInIcon/></button>
|
||||||
|
<button onClick={this.rotateLeft}><RotateLeftIcon/></button>
|
||||||
|
<button onClick={this.rotateRight}><RotateRightIcon/></button>
|
||||||
|
<a href={this.props.src} target="_blank" rel="noopener noreferrer">
|
||||||
|
<DownloadIcon/>
|
||||||
|
</a>
|
||||||
|
<button onClick={this.props.onClose}><CloseIcon/></button>
|
||||||
|
</div>
|
||||||
<img
|
<img
|
||||||
onMouseDown={isTouchDevice ? undefined : this.onMouseDown}
|
onMouseDown={isTouchDevice ? undefined : this.onMouseDown}
|
||||||
onWheel={isTouchDevice ? undefined : this.onWheel}
|
onWheel={isTouchDevice ? undefined : this.onWheel}
|
||||||
|
|
2
web/src/vite-env.d.ts
vendored
Normal file
2
web/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-svgr/client" />
|
|
@ -1,8 +1,18 @@
|
||||||
import react from "@vitejs/plugin-react-swc"
|
import react from "@vitejs/plugin-react-swc"
|
||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
|
import svgr from "vite-plugin-svgr"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
svgr({
|
||||||
|
svgrOptions: {
|
||||||
|
replaceAttrValues: {
|
||||||
|
"#5f6368": "currentColor",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/_gomuks/websocket": {
|
"/_gomuks/websocket": {
|
||||||
|
|
Loading…
Add table
Reference in a new issue