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.
121 lines
4.5 KiB
JavaScript
121 lines
4.5 KiB
JavaScript
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
|
||
}
|