OASIS_MOBILE/src/views/gallery_view.js
oasis-bot c332afd6ea upstream: integrar Oasis 1.3.1
Base del merge a tres bandas: git 0.9.5 de epsylon, esta rama y el src de la
APK oficial 1.3.1 (la variante movil: OASIS_MOBILE, regex sin \p{}, audio y
video de Android). Corre en el mismo nodejs-mobile 18.20.4 que ya usabamos.

Se conserva de la rama: Karvan, identidades y multicuenta, topbar y barra
inferior, borrado de EXIF sin sharp, filtro hot del foro, RTC sin STUN y los
modulos apagados en movil (wallet, banking, torrents).

Se toma de upstream lo que ya hace de serie: respuesta citada en chats, ssb-lan
en movil, configuracion en ~/.ssb/oasis (sobrevive a reinstalar el backend).
El asistente de bienvenida deja elegir workflow tambien en movil.
2026-10-18 02:52:17 +02:00

121 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const { div, a, span, img, video, input, button, label, br } = require("../server/node_modules/hyperaxe")
const { i18n } = require("./main_views")
const { MAX_IMAGES } = require("../models/media_gallery")
const safeArr = (v) => (Array.isArray(v) ? v : [])
const safeText = (v) => String(v == null ? "" : v).trim()
const downloadHref = (src) => `${src}${String(src).includes("?") ? "&" : "?"}download=1`
const blobIdOf = (entry) => {
const value = safeText(entry)
const m = value.match(/\(([^)\s]+)\)/)
return m ? m[1] : value
}
const blobUrl = (id, size) => size
? `/image/${size}/${encodeURIComponent(id)}`
: `/blob/${encodeURIComponent(id)}`
const isVideoEntry = (entry) => /\[video:[^\]]*\]\(/.test(String(entry || ""))
const imagesOf = (item) => {
const list = safeArr(item && item.images).filter(Boolean)
if (list.length) return list
return item && item.image ? [item.image] : []
}
const videoOf = (item) => safeText(item && item.video)
const renderMediaThumb = (entry, alt = "") => isVideoEntry(entry)
? video({ controls: true, class: "gallery-image", src: blobUrl(blobIdOf(entry)), preload: 'metadata' })
: img({ src: blobUrl(blobIdOf(entry), 256), class: "gallery-image", alt })
const lightboxId = (scope, itemId, index) => `${scope}-photo-${encodeURIComponent(itemId)}-${index}`
let zoomSeq = 0
const renderZoomableImage = (src, { alt = "", imgClass = "", linkClass = "" } = {}) => {
if (!src) return null
zoomSeq = (zoomSeq + 1) % 1000000
const id = `zoom-${zoomSeq}-${String(src).replace(/[^a-zA-Z0-9]/g, "").slice(-10)}`
return [
a({ href: `#${id}`, id: `${id}-src`, class: linkClass ? `zoom-link ${linkClass}` : "zoom-link" }, img({ src, class: imgClass, alt })),
div({ id, class: "lightbox" },
a({ href: downloadHref(src), class: "lightbox-download", title: i18n.fileShareDownload || "Download" }, "⤓"),
a({ href: `#${id}-src`, class: "lightbox-close" }, "×"),
img({ src, class: "lightbox-image", alt })
)
]
}
const renderPhotoGallery = (item, scope = "media") => {
const list = imagesOf(item)
const clip = videoOf(item)
if (!list.length && !clip) return null
const id = (item && (item.id || item.key || item.rootId)) || ""
return div({ class: "media-gallery" },
list.length
? div({ class: "gallery" },
list.map((entry, i) =>
isVideoEntry(entry)
? span({ class: "gallery-item" }, renderMediaThumb(entry))
: a({ href: `#${lightboxId(scope, id, i)}`, id: `${lightboxId(scope, id, i)}-src`, class: "gallery-item" }, renderMediaThumb(entry))
)
)
: null,
list.filter(entry => !isVideoEntry(entry)).map((entry) =>
div({ id: lightboxId(scope, id, list.indexOf(entry)), class: "lightbox" },
a({ href: downloadHref(blobUrl(blobIdOf(entry))), class: "lightbox-download", title: i18n.fileShareDownload || "Download" }, "⤓"),
a({ href: `#${lightboxId(scope, id, list.indexOf(entry))}-src`, class: "lightbox-close" }, "×"),
img({ src: blobUrl(blobIdOf(entry)), class: "lightbox-image", alt: "" })
)
),
clip
? div({ class: "media-video media-video-centered" },
video({ controls: true, class: "media-video-player", src: blobUrl(blobIdOf(clip)), preload: 'metadata' })
)
: null
)
}
const renderGalleryFields = (item = {}, isEdit = false, maxImages = MAX_IMAGES) => {
const list = imagesOf(item)
return [
label(i18n.galleryImages),
br(),
...list.map(entry => input({ type: "hidden", name: "keepImages", value: entry })),
list.length < maxImages
? div({ class: "media-file-row" },
input({ type: "file", name: "images", accept: "image/*", multiple: true }),
button({ type: "submit", name: "action", value: "addPhoto", attrs: { formnovalidate: "formnovalidate" }, class: "filter-btn media-add-btn" }, i18n.galleryAddPhoto)
)
: null,
br(),
list.length
? [
div({ class: "gallery media-form-gallery" },
list.map((entry, i) =>
span({ class: "gallery-item" },
renderMediaThumb(entry),
button({ type: "submit", name: "removePhoto", value: String(i), attrs: { formnovalidate: "formnovalidate" }, class: "media-remove-btn" }, i18n.galleryRemovePhoto)
)
)
),
br()
]
: null,
br()
]
}
module.exports = {
MAX_IMAGES,
blobIdOf,
blobUrl,
isVideoEntry,
imagesOf,
videoOf,
renderMediaThumb,
renderPhotoGallery,
renderGalleryFields,
renderZoomableImage
}