1
0
mirror of https://git.sr.ht/~cadence/bibliogram synced 2024-11-22 16:17:29 +00:00

Add SPA post overlay

This commit is contained in:
Cadence Fish 2020-02-22 01:35:19 +13:00
parent 13e40259b7
commit aac358cd65
No known key found for this signature in database
GPG Key ID: 81015DF9AA8607E1
9 changed files with 317 additions and 103 deletions

View File

@ -114,6 +114,34 @@ module.exports = [
}) })
} }
}, },
{
route: `/fragment/post/(${constants.external.shortcode_regex})`, methods: ["GET"], code: ({fill}) => {
return getOrFetchShortcode(fill[0]).then(async post => {
await post.fetchChildren()
await post.fetchExtendedOwnerP() // serial await is okay since intermediate fetch result is cached
if (post.isVideo()) await post.fetchVideoURL()
return {
statusCode: 200,
contentType: "application/json",
content: {
title: post.getCaptionIntroduction(),
html: pugCache.get("pug/fragments/post.pug").web({post})
}
}
}).catch(error => {
if (error === constants.symbols.NOT_FOUND) {
return render(404, "pug/friendlyerror.pug", {
statusCode: 404,
title: "Not found",
message: "Somehow, you reached a post that doesn't exist.",
withInstancesLink: false
})
} else {
throw error
}
})
}
},
{ {
route: "/p", methods: ["GET"], code: async ({url}) => { route: "/p", methods: ["GET"], code: async ({url}) => {
if (url.searchParams.has("p")) { if (url.searchParams.has("p")) {
@ -135,7 +163,7 @@ module.exports = [
route: `/p/(${constants.external.shortcode_regex})`, methods: ["GET"], code: ({fill}) => { route: `/p/(${constants.external.shortcode_regex})`, methods: ["GET"], code: ({fill}) => {
return getOrFetchShortcode(fill[0]).then(async post => { return getOrFetchShortcode(fill[0]).then(async post => {
await post.fetchChildren() await post.fetchChildren()
await post.fetchExtendedOwnerP() // parallel await is okay since intermediate fetch result is cached await post.fetchExtendedOwnerP() // serial await is okay since intermediate fetch result is cached
if (post.isVideo()) await post.fetchVideoURL() if (post.isVideo()) await post.fetchVideoURL()
return render(200, "pug/post.pug", {post}) return render(200, "pug/post.pug", {post})
}).catch(error => { }).catch(error => {

View File

@ -15,9 +15,29 @@ class FreezeWidth extends ElemJS {
const intersectionThreshold = 0 const intersectionThreshold = 0
class NextPageController {
constructor() {
this.instance = null
}
add() {
const nextPage = q("#next-page")
if (nextPage) {
this.instance = new NextPage(nextPage, this)
} else {
this.instance = null
}
}
activate() {
if (this.instance) this.instance.activate()
}
}
class NextPage extends FreezeWidth { class NextPage extends FreezeWidth {
constructor(container) { constructor(container, controller) {
super(container) super(container)
this.controller = controller
this.clicked = false this.clicked = false
this.nextPageNumber = +this.element.getAttribute("data-page") this.nextPageNumber = +this.element.getAttribute("data-page")
this.attribute("href", "javascript:void(0)") this.attribute("href", "javascript:void(0)")
@ -54,14 +74,17 @@ class NextPage extends FreezeWidth {
q("#next-page-container").remove() q("#next-page-container").remove()
this.observer.disconnect() this.observer.disconnect()
q("#timeline").insertAdjacentHTML("beforeend", text) q("#timeline").insertAdjacentHTML("beforeend", text)
addNextPageControl() this.controller.add()
}) })
} }
activate() {
if (this.fetching) return
this.class("disabled")
this.fetch()
}
} }
function addNextPageControl() { const controller = new NextPageController()
const nextPage = q("#next-page") controller.add()
if (nextPage) new NextPage(nextPage) export {controller}
}
addNextPageControl()

View File

@ -0,0 +1,125 @@
import {q, ElemJS} from "./elemjs/elemjs.js"
/** @type {PostOverlay[]} */
const postOverlays = []
const titleHistory = []
titleHistory.push(document.title)
const shortcodeDataMap = new Map()
window.addEventListener("popstate", event => {
// console.log(event.state, postOverlays.length)
if (event.state) {
if (event.state.view === "post_overlay") {
loadPostOverlay(event.state.shortcode, false)
}
} else { // event.state === null which means back to originally loaded page, so pop overlay
setTimeout(() => { // make sure document is entirely loaded
if (titleHistory.length === 1) {
document.title = titleHistory[0]
} else if (titleHistory.length >= 2) {
titleHistory.pop()
document.title = titleHistory.slice(-1)[0]
}
if (postOverlays.length) {
popOverlay()
} else {
window.location.reload()
}
})
}
})
function pushOverlay(overlay) {
postOverlays.push(overlay)
document.body.style.overflowY = "hidden"
}
function popOverlay() {
const top = postOverlays.pop()
if (top) {
top.pop()
}
if (postOverlays.length === 0) document.body.style.overflowY = "auto"
}
class PostOverlay extends ElemJS {
constructor() {
super("div")
this.class("post-overlay")
this.event("click", event => {
if (event.target === event.currentTarget) history.back()
})
this.loaded = false
this.available = true
setTimeout(() => {
if (!this.loaded) {
this.class("loading")
this.child(
new ElemJS("div").class("loading-inner").text("Loading...")
)
}
}, 0)
}
setContent(html) {
this.html(html)
this.loaded = true
this.removeClass("loading")
}
showError() {
this.loaded = true
this.class("loading")
this.clearChildren()
this.child(
new ElemJS("div").class("loading-inner").text("Request failed.")
)
}
pop() {
this.element.remove()
this.available = false
}
}
const timeline = q("#timeline")
if (timeline) {
timeline.addEventListener("click", event => {
/** @type {HTMLElement[]} */
//@ts-ignore
const path = event.composedPath()
const postLink = path.find(element => element.classList && element.classList.contains("sized-link") && element.hasAttribute("data-shortcode"))
if (postLink) {
event.preventDefault()
const shortcode = postLink.getAttribute("data-shortcode")
loadPostOverlay(shortcode, true)
}
})
}
function fetchShortcodeFragment(shortcode) {
if (shortcodeDataMap.has(shortcode)) return Promise.resolve(shortcodeDataMap.get(shortcode))
else return fetch(`/fragment/post/${shortcode}`).then(res => res.json())
}
function loadPostOverlay(shortcode, shouldPushState) {
const overlay = new PostOverlay()
document.body.appendChild(overlay.element)
pushOverlay(overlay)
if (shouldPushState) history.pushState({view: "post_overlay", shortcode: shortcode}, "", `/p/${shortcode}`)
const fetcher = fetchShortcodeFragment(shortcode)
fetcher.then(root => {
shortcodeDataMap.set(shortcode, root)
if (overlay.available) {
const {title, html} = root
overlay.setContent(html)
if (overlay.available) {
document.title = title
}
}
})
fetcher.catch(error => {
console.error(error)
overlay.showError()
})
}

View File

@ -0,0 +1,5 @@
//- Needs post
include ../includes/post.pug
+post(post)

View File

@ -0,0 +1,17 @@
include ./display_structured
mixin post(post)
.post-page-divider
section.description-section
header.user-header
img(src=post.ownerPfpCacheP width=150 height=150 alt="").pfp
a.name(href=`/u/${post.getBasicOwner().username}`)= `${post.data.owner.full_name} (@${post.getBasicOwner().username})`
if post.getCaption()
p.structured-text.description
+display_structured(post.getStructuredCaption())
section.images-gallery
for entry in post.children
if entry.isVideo()
video(src=entry.getVideoUrlP() controls preload="auto" width=entry.data.dimensions.width height=entry.data.dimensions.height).sized-video
else
img(src=entry.getDisplayUrlP() alt=entry.getAlt() width=entry.data.dimensions.width height=entry.data.dimensions.height).sized-image

View File

@ -11,5 +11,5 @@ mixin timeline_page(page, pageIndex)
- const suggestedSize = 260 //- from css :( - const suggestedSize = 260 //- from css :(
each image in page each image in page
- const thumbnail = image.getSuggestedThumbnailP(suggestedSize) //- use this as the src in case there are problems with srcset - const thumbnail = image.getSuggestedThumbnailP(suggestedSize) //- use this as the src in case there are problems with srcset
a(href=`/p/${image.data.shortcode}`).sized-link a(href=`/p/${image.data.shortcode}` data-shortcode=image.data.shortcode).sized-link
img(src=thumbnail.src alt=image.getAlt() width=thumbnail.config_width height=thumbnail.config_height srcset=image.getThumbnailSrcsetP() sizes=image.getThumbnailSizes()).sized-image img(src=thumbnail.src alt=image.getAlt() width=thumbnail.config_width height=thumbnail.config_height srcset=image.getThumbnailSrcsetP() sizes=image.getThumbnailSizes()).sized-image

View File

@ -1,6 +1,4 @@
include includes/display_structured include includes/post
- const numberFormat = new Intl.NumberFormat().format
doctype html doctype html
html html
@ -12,18 +10,7 @@ html
=`Post from @${post.getBasicOwner().username}` =`Post from @${post.getBasicOwner().username}`
=` | Bibliogram` =` | Bibliogram`
include includes/head include includes/head
script(type="module" src="/static/js/post_overlay.js")
body.post-page body.post-page
main.post-page-divider main
section.description-section +post(post)
header.user-header
img(src=post.ownerPfpCacheP width=150 height=150 alt="").pfp
a.name(href=`/u/${post.getBasicOwner().username}`)= `${post.data.owner.full_name} (@${post.getBasicOwner().username})`
if post.getCaption()
p.structured-text.description
+display_structured(post.getStructuredCaption())
section.images-gallery
for entry in post.children
if entry.isVideo()
video(src=entry.getVideoUrlP() controls preload="auto" width=entry.data.dimensions.width height=entry.data.dimensions.height).sized-video
else
img(src=entry.getDisplayUrlP() alt=entry.getAlt() width=entry.data.dimensions.width height=entry.data.dimensions.height).sized-image

View File

@ -16,6 +16,7 @@ html
title= `@${user.data.username} | Bibliogram` title= `@${user.data.username} | Bibliogram`
include includes/head include includes/head
script(src="/static/js/pagination.js" type="module") script(src="/static/js/pagination.js" type="module")
script(src="/static/js/post_overlay.js" type="module")
body body
.main-divider .main-divider
header.profile-overview header.profile-overview
@ -46,7 +47,7 @@ html
if constants.settings.rss_enabled if constants.settings.rss_enabled
+feed_link("RSS", "rss", user.data.username, "application/rss+xml", display_feed_validation_buttons) +feed_link("RSS", "rss", user.data.username, "application/rss+xml", display_feed_validation_buttons)
+feed_link("Atom", "atom", user.data.username, "application/atom+xml", display_feed_validation_buttons) +feed_link("Atom", "atom", user.data.username, "application/atom+xml", display_feed_validation_buttons)
a(rel="noreferrer noopener" href=`https://www.instagram.com/${user.data.username}`) instagram.com a(rel="noreferrer noopener" href=`https://www.instagram.com/${user.data.username}` target="_blank") instagram.com
- const hasPosts = !user.data.is_private && user.timeline.pages.length && user.timeline.pages[0].length - const hasPosts = !user.data.is_private && user.timeline.pages.length && user.timeline.pages[0].length
main(class=hasPosts ? "" : "no-posts")#timeline.timeline main(class=hasPosts ? "" : "no-posts")#timeline.timeline

View File

@ -215,95 +215,95 @@ body
@include sized @include sized
.post-page .post-page
background-color: #6a6b71 background-color: #505156
.post-page-divider .post-page-divider
display: grid
grid-template-columns: 360px auto
max-width: 1200px
margin: 0 auto
min-height: 100vh
@media screen and (max-width: $layout-a-max)
display: flex
flex-direction: column
.description-section
display: grid display: grid
grid-template-columns: 360px auto align-items: start
max-width: 1200px align-content: normal
margin: 0 auto background-color: #eee
min-height: 100vh position: sticky
top: 0
height: 100vh
overflow-y: auto
box-sizing: border-box
@media screen and (max-width: $layout-a-max) @media screen and (max-width: $layout-a-max)
position: inherit
top: inherit
height: inherit
.user-header
display: flex display: flex
flex-direction: column
.description-section
display: grid
align-items: start
align-content: normal
background-color: #eee
position: sticky
top: 0
height: 100vh
overflow-y: auto
box-sizing: border-box
@media screen and (max-width: $layout-a-max)
position: inherit
top: inherit
height: inherit
.user-header
display: flex
align-items: center
justify-content: center
background-color: #b3b3b3
padding: 10px
.pfp
$size: 40px
width: $size
height: $size
margin-right: 10px
background-color: rgba(40, 40, 40, 0.25)
.name
font-size: 20px
color: black
text-decoration: none
&:hover
text-decoration: underline
.description
margin: 12px
white-space: pre-line
overflow-wrap: anywhere
font-size: 20px
line-height: 1.4
@media screen and (max-width: $layout-a-max)
font-size: 18px
.images-gallery
display: flex
flex-direction: column
align-items: center align-items: center
justify-content: center justify-content: center
background-color: #262728 background-color: #b3b3b3
padding: 10px padding: 10px
.pfp
$size: 40px
width: $size
height: $size
margin-right: 10px
background-color: rgba(40, 40, 40, 0.25)
.name
font-size: 20px
color: black
text-decoration: none
&:hover
text-decoration: underline
.description
margin: 12px
white-space: pre-line
overflow-wrap: anywhere
font-size: 20px
line-height: 1.4
@media screen and (max-width: $layout-a-max) @media screen and (max-width: $layout-a-max)
flex: 1 font-size: 18px
.sized-image, .sized-video .images-gallery
color: #eee display: flex
background-color: #3b3c3d flex-direction: column
max-height: 94vh align-items: center
max-width: 100% justify-content: center
background-color: #262728
padding: 10px
&:not(:last-child) @media screen and (max-width: $layout-a-max)
margin-bottom: 10px flex: 1
.sized-image .sized-image, .sized-video
width: auto color: #eee
height: auto background-color: #3b3c3d
max-height: 94vh
max-width: 100%
.sized-video &:not(:last-child)
width: 100% margin-bottom: 10px
height: 100%
.sized-image
width: auto
height: auto
.sized-video
width: 100%
height: 100%
.error-page .error-page
box-sizing: border-box box-sizing: border-box
@ -490,3 +490,31 @@ body
margin-top: 45px margin-top: 45px
padding-top: 15px padding-top: 15px
border-top: 1px solid #714141 border-top: 1px solid #714141
.post-overlay
position: fixed
top: 0
left: 0
right: 0
bottom: 0
background: rgba(0, 0, 0, 0.7)
z-index: 10
overflow-y: auto
&:not(.loading) > *
min-height: 100vh
&.loading
display: flex
justify-content: center
align-items: center
.loading-inner
color: white
font-size: 30px
line-height: 1
padding: 26px
border-radius: 20px
border: 2px solid #aaa
font-weight: bold
background-color: #282828