299 lines
9.9 KiB
JavaScript
299 lines
9.9 KiB
JavaScript
css(`
|
|
filepreview- {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 300;
|
|
pointer-events: none;
|
|
font-family: 'Arial';
|
|
}
|
|
`)
|
|
|
|
class FilePreview extends Shadow {
|
|
_file = null
|
|
_url = null
|
|
_panelEl = null
|
|
|
|
open(file, url) {
|
|
this._file = file
|
|
this._url = url
|
|
this.rerender()
|
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
if (this._panelEl) {
|
|
this._panelEl.style.transition = "transform 0.35s cubic-bezier(0.32, 0.72, 0, 1)"
|
|
this._panelEl.style.transform = "translateY(0)"
|
|
}
|
|
}))
|
|
}
|
|
|
|
close() {
|
|
if (!this._panelEl) return
|
|
this._panelEl.style.transition = "transform 0.3s cubic-bezier(0.32, 0.72, 0, 1)"
|
|
this._panelEl.style.transform = "translateY(100%)"
|
|
setTimeout(() => {
|
|
this._file = null
|
|
this._url = null
|
|
this._panelEl = null
|
|
this.rerender()
|
|
}, 300)
|
|
}
|
|
|
|
render() {
|
|
if (!this._file) {
|
|
this.style.pointerEvents = "none"
|
|
return
|
|
}
|
|
this.style.pointerEvents = "all"
|
|
|
|
const type = this._file?.type ?? ""
|
|
const isImage = type.startsWith("image/")
|
|
const isPDF = type === "application/pdf"
|
|
const displayName = this._file?.original_name ?? this._file?.name ?? "File"
|
|
|
|
this._panelEl = VStack(() => {
|
|
this._renderHeader(displayName, isImage)
|
|
|
|
if (isImage) {
|
|
this._renderImageViewer()
|
|
} else if (isPDF) {
|
|
this._renderPDFViewer()
|
|
} else {
|
|
this._renderFileFallback(displayName)
|
|
}
|
|
})
|
|
.position("fixed")
|
|
.inset(0)
|
|
.background(isImage ? "#000" : "var(--main)")
|
|
.transform("translateY(100%)")
|
|
|
|
this._addPanelSwipe()
|
|
}
|
|
|
|
_renderHeader(displayName, isImage) {
|
|
HStack(() => {
|
|
button("⬇")
|
|
.fontSize(1.15, em)
|
|
.color("var(--headertext)")
|
|
.background("transparent")
|
|
.border("none")
|
|
.outline("none")
|
|
.padding(0)
|
|
.paddingLeft(1, rem)
|
|
.flexShrink(0)
|
|
.cursor("pointer")
|
|
.onTap(() => window.open(this._url, "_blank"))
|
|
|
|
p(displayName)
|
|
.margin(0)
|
|
.flex(1)
|
|
.fontSize(0.88, em)
|
|
.fontWeight("600")
|
|
.color("var(--headertext)")
|
|
.overflow("hidden")
|
|
.whiteSpace("nowrap")
|
|
.textOverflow("ellipsis")
|
|
.textAlign("center")
|
|
|
|
button("Done")
|
|
.fontSize(0.88, em)
|
|
.fontWeight("600")
|
|
.color("var(--quillred)")
|
|
.background("transparent")
|
|
.border("none")
|
|
.outline("none")
|
|
.padding(0)
|
|
.paddingRight(1, rem)
|
|
.flexShrink(0)
|
|
.cursor("pointer")
|
|
.onTap(() => this.close())
|
|
})
|
|
.width(100, pct)
|
|
.height(52, px)
|
|
.gap(0.75, rem)
|
|
.alignItems("center")
|
|
.justifyContent("center")
|
|
.background(util.darkMode() ? "var(--darkaccent)" : "var(--sidebottombars)")
|
|
.borderBottom("1px solid var(--divider)")
|
|
.boxSizing("border-box")
|
|
.flexShrink(0)
|
|
}
|
|
|
|
_renderImageViewer() {
|
|
let imgEl = null
|
|
|
|
const container = VStack(() => {
|
|
imgEl = img(this._url, "auto", "auto")
|
|
.display("block")
|
|
.maxWidth(100, pct)
|
|
.maxHeight(100, pct)
|
|
.objectFit("contain")
|
|
.pointerEvents("none")
|
|
})
|
|
.flex(1)
|
|
.width(100, pct)
|
|
.alignItems("center")
|
|
.justifyContent("center")
|
|
.overflow("hidden")
|
|
|
|
if (imgEl) this._setupImageGestures(container, imgEl)
|
|
}
|
|
|
|
_setupImageGestures(container, imgEl) {
|
|
let scale = 1, tx = 0, ty = 0
|
|
let pinchStartDist = null
|
|
let panStartX = 0, panStartY = 0
|
|
let swipeStartY = null, swipeStartTime = null
|
|
let lastTap = 0
|
|
|
|
container.style.touchAction = "none"
|
|
|
|
const applyTransform = (animated = false) => {
|
|
imgEl.style.transition = animated ? "transform 0.3s ease" : ""
|
|
imgEl.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`
|
|
}
|
|
|
|
const resetZoom = () => {
|
|
scale = 1; tx = 0; ty = 0
|
|
applyTransform(true)
|
|
}
|
|
|
|
container.addEventListener("touchstart", (e) => {
|
|
e.stopPropagation()
|
|
if (e.touches.length === 2) {
|
|
pinchStartDist = Math.hypot(
|
|
e.touches[1].clientX - e.touches[0].clientX,
|
|
e.touches[1].clientY - e.touches[0].clientY
|
|
)
|
|
swipeStartY = null
|
|
} else if (e.touches.length === 1) {
|
|
panStartX = e.touches[0].clientX - tx
|
|
panStartY = e.touches[0].clientY - ty
|
|
if (scale <= 1) {
|
|
swipeStartY = e.touches[0].clientY
|
|
swipeStartTime = Date.now()
|
|
}
|
|
}
|
|
}, { passive: true })
|
|
|
|
container.addEventListener("touchmove", (e) => {
|
|
if (e.touches.length === 2 && pinchStartDist !== null) {
|
|
const dist = Math.hypot(
|
|
e.touches[1].clientX - e.touches[0].clientX,
|
|
e.touches[1].clientY - e.touches[0].clientY
|
|
)
|
|
scale = Math.min(Math.max(scale * (dist / pinchStartDist), 1), 6)
|
|
pinchStartDist = dist
|
|
applyTransform()
|
|
} else if (e.touches.length === 1 && scale > 1) {
|
|
tx = e.touches[0].clientX - panStartX
|
|
ty = e.touches[0].clientY - panStartY
|
|
applyTransform()
|
|
} else if (e.touches.length === 1 && swipeStartY !== null) {
|
|
const dy = e.touches[0].clientY - swipeStartY
|
|
if (dy > 5 && this._panelEl) {
|
|
this._panelEl.style.transition = ""
|
|
this._panelEl.style.transform = `translateY(${Math.max(0, dy)}px)`
|
|
}
|
|
}
|
|
}, { passive: true })
|
|
|
|
container.addEventListener("touchend", (e) => {
|
|
if (e.touches.length < 2) pinchStartDist = null
|
|
|
|
if (swipeStartY !== null) {
|
|
const dy = e.changedTouches[0].clientY - swipeStartY
|
|
const vel = dy / Math.max(1, Date.now() - swipeStartTime)
|
|
if (dy > window.innerHeight * 0.25 || vel > 0.5) {
|
|
this.close()
|
|
} else if (this._panelEl) {
|
|
this._panelEl.style.transition = "transform 0.3s ease"
|
|
this._panelEl.style.transform = "translateY(0)"
|
|
}
|
|
swipeStartY = null
|
|
}
|
|
|
|
if (scale < 1) resetZoom()
|
|
|
|
const now = Date.now()
|
|
if (e.changedTouches.length === 1 && now - lastTap < 300) {
|
|
scale > 1 ? resetZoom() : (() => { scale = 2.5; applyTransform(true) })()
|
|
}
|
|
lastTap = now
|
|
}, { passive: true })
|
|
}
|
|
|
|
_addPanelSwipe() {
|
|
let startY = null, startTime = null
|
|
|
|
this._panelEl.addEventListener("touchstart", (e) => {
|
|
if (e.touches.length !== 1) { startY = null; return }
|
|
startY = e.touches[0].clientY
|
|
startTime = Date.now()
|
|
}, { passive: true })
|
|
|
|
this._panelEl.addEventListener("touchmove", (e) => {
|
|
if (startY === null || e.touches.length !== 1) return
|
|
const dy = e.touches[0].clientY - startY
|
|
if (dy > 0 && this._panelEl) {
|
|
this._panelEl.style.transition = ""
|
|
this._panelEl.style.transform = `translateY(${dy}px)`
|
|
}
|
|
}, { passive: true })
|
|
|
|
this._panelEl.addEventListener("touchend", (e) => {
|
|
if (startY === null) return
|
|
const dy = e.changedTouches[0].clientY - startY
|
|
const vel = dy / Math.max(1, Date.now() - startTime)
|
|
if (dy > window.innerHeight * 0.25 || vel > 0.5) {
|
|
this.close()
|
|
} else if (this._panelEl) {
|
|
this._panelEl.style.transition = "transform 0.3s ease"
|
|
this._panelEl.style.transform = "translateY(0)"
|
|
}
|
|
startY = null
|
|
}, { passive: true })
|
|
}
|
|
|
|
_renderPDFViewer() {
|
|
const wrap = div()
|
|
.flex(1)
|
|
.width(100, pct)
|
|
.overflow("hidden")
|
|
wrap.innerHTML = `<iframe src="${this._url}" style="width:100%;height:100%;border:none;display:block;"></iframe>`
|
|
}
|
|
|
|
_renderFileFallback(displayName) {
|
|
const type = this._file?.type ?? ""
|
|
const icon = {
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': "📄",
|
|
'application/msword': "📄",
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': "📊",
|
|
'application/vnd.ms-excel': "📊",
|
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation': "📋",
|
|
'application/vnd.ms-powerpoint': "📋",
|
|
'text/plain': "📄",
|
|
}[type] ?? "📎"
|
|
|
|
VStack(() => {
|
|
p(icon).fontSize(3, em).margin(0)
|
|
p(displayName)
|
|
.margin(0)
|
|
.fontSize(0.9, em)
|
|
.color("var(--headertext)")
|
|
.textAlign("center")
|
|
a(this._url, "Open / Download")
|
|
.attr({ download: displayName })
|
|
.fontSize(0.9, em)
|
|
.color("var(--quillred)")
|
|
.fontWeight("600")
|
|
.textDecoration("none")
|
|
.marginTop(0.5, em)
|
|
})
|
|
.flex(1)
|
|
.alignItems("center")
|
|
.justifyContent("center")
|
|
.gap(0.75, em)
|
|
}
|
|
}
|
|
|
|
register(FilePreview)
|