mirror of
https://git.sr.ht/~cadence/bibliogram
synced 2024-11-22 16:17:29 +00:00
parent
becd2c2ec7
commit
d32a12a134
1
src/site/html/static/img/arrow-circled.svg
Normal file
1
src/site/html/static/img/arrow-circled.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 6.35 6.35"><g transform="translate(0 -290.65)" paint-order="fill markers stroke"><circle cx="3.175" cy="293.825" r="3.175" fill="#505050"/><path d="M2.706 292.277l1.51 1.548-1.51 1.548" fill="none" stroke="#ddd" stroke-width=".529" stroke-linecap="round"/></g></svg>
|
After Width: | Height: | Size: 342 B |
@ -18,6 +18,7 @@ const intersectionThreshold = 0
|
|||||||
class NextPageController {
|
class NextPageController {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.instance = null
|
this.instance = null
|
||||||
|
this.activatedCallbacks = []
|
||||||
}
|
}
|
||||||
|
|
||||||
add() {
|
add() {
|
||||||
@ -27,10 +28,15 @@ class NextPageController {
|
|||||||
} else {
|
} else {
|
||||||
this.instance = null
|
this.instance = null
|
||||||
}
|
}
|
||||||
|
this.activatedCallbacks.forEach(c => c())
|
||||||
}
|
}
|
||||||
|
|
||||||
activate() {
|
addActivatedCallback(callback) {
|
||||||
if (this.instance) this.instance.activate()
|
this.activatedCallbacks.push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
async activate() {
|
||||||
|
if (this.instance) await this.instance.activate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +76,7 @@ class NextPage extends FreezeWidth {
|
|||||||
this.fetching = true
|
this.fetching = true
|
||||||
this.freeze("Loading...")
|
this.freeze("Loading...")
|
||||||
|
|
||||||
fetch(`/fragment/user/${this.element.getAttribute("data-username")}/${this.nextPageNumber}`).then(res => res.text()).then(text => {
|
return fetch(`/fragment/user/${this.element.getAttribute("data-username")}/${this.nextPageNumber}`).then(res => res.text()).then(text => {
|
||||||
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)
|
||||||
@ -81,7 +87,7 @@ class NextPage extends FreezeWidth {
|
|||||||
activate() {
|
activate() {
|
||||||
if (this.fetching) return
|
if (this.fetching) return
|
||||||
this.class("disabled")
|
this.class("disabled")
|
||||||
this.fetch()
|
return this.fetch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {q, ElemJS} from "./elemjs/elemjs.js"
|
import {q, ElemJS} from "./elemjs/elemjs.js"
|
||||||
|
import {timeline} from "./post_series.js"
|
||||||
|
|
||||||
/** @type {PostOverlay[]} */
|
/** @type {PostOverlay[]} */
|
||||||
const postOverlays = []
|
const postOverlays = []
|
||||||
@ -10,23 +11,28 @@ window.addEventListener("popstate", event => {
|
|||||||
// console.log(event.state, postOverlays.length)
|
// console.log(event.state, postOverlays.length)
|
||||||
if (event.state) {
|
if (event.state) {
|
||||||
if (event.state.view === "post_overlay") {
|
if (event.state.view === "post_overlay") {
|
||||||
loadPostOverlay(event.state.shortcode, false)
|
console.log(event.state.shortcode, postOverlays.map(o => o.identifier))
|
||||||
|
/*if (postOverlays.length >= 2 && postOverlays.slice(-2)[0].identifier === event.state.shortcode) {
|
||||||
|
// continue down to actually pop please
|
||||||
|
} else {*/
|
||||||
|
return loadPostOverlay(event.state.shortcode, "none")
|
||||||
|
/*}*/
|
||||||
}
|
}
|
||||||
} 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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
// 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) {
|
function pushOverlay(overlay) {
|
||||||
@ -43,14 +49,17 @@ function popOverlay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class PostOverlay extends ElemJS {
|
class PostOverlay extends ElemJS {
|
||||||
constructor() {
|
constructor(identifier) {
|
||||||
super("div")
|
super("div")
|
||||||
|
this.identifier = identifier
|
||||||
|
this.loaded = false
|
||||||
|
this.available = true
|
||||||
|
this.keyboardListeners = []
|
||||||
|
|
||||||
this.class("post-overlay")
|
this.class("post-overlay")
|
||||||
this.event("click", event => {
|
this.event("click", event => {
|
||||||
if (event.target === event.currentTarget) history.back()
|
if (event.target === event.currentTarget) history.back()
|
||||||
})
|
})
|
||||||
this.loaded = false
|
|
||||||
this.available = true
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.loaded) {
|
if (!this.loaded) {
|
||||||
this.class("loading")
|
this.class("loading")
|
||||||
@ -61,6 +70,13 @@ class PostOverlay extends ElemJS {
|
|||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addKeyboardCallback(callback) {
|
||||||
|
if (this.available) {
|
||||||
|
this.keyboardListeners.push(callback)
|
||||||
|
document.addEventListener("keydown", callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setContent(html) {
|
setContent(html) {
|
||||||
this.html(html)
|
this.html(html)
|
||||||
this.loaded = true
|
this.loaded = true
|
||||||
@ -79,12 +95,15 @@ class PostOverlay extends ElemJS {
|
|||||||
pop() {
|
pop() {
|
||||||
this.element.remove()
|
this.element.remove()
|
||||||
this.available = false
|
this.available = false
|
||||||
|
while (this.keyboardListeners.length) {
|
||||||
|
document.removeEventListener("keydown", this.keyboardListeners.shift())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeline = q("#timeline")
|
const timelineElement = q("#timeline")
|
||||||
if (timeline) {
|
if (timelineElement) {
|
||||||
timeline.addEventListener("click", event => {
|
timelineElement.addEventListener("click", event => {
|
||||||
/** @type {HTMLElement[]} */
|
/** @type {HTMLElement[]} */
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const path = event.composedPath()
|
const path = event.composedPath()
|
||||||
@ -92,7 +111,7 @@ if (timeline) {
|
|||||||
if (postLink) {
|
if (postLink) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const shortcode = postLink.getAttribute("data-shortcode")
|
const shortcode = postLink.getAttribute("data-shortcode")
|
||||||
loadPostOverlay(shortcode, true)
|
loadPostOverlay(shortcode, "push")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -102,24 +121,74 @@ function fetchShortcodeFragment(shortcode) {
|
|||||||
else return fetch(`/fragment/post/${shortcode}`).then(res => res.json())
|
else return fetch(`/fragment/post/${shortcode}`).then(res => res.json())
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPostOverlay(shortcode, shouldPushState) {
|
function loadPostOverlay(shortcode, stateChangeType) {
|
||||||
const overlay = new PostOverlay()
|
const overlay = new PostOverlay(shortcode)
|
||||||
document.body.appendChild(overlay.element)
|
document.body.appendChild(overlay.element)
|
||||||
pushOverlay(overlay)
|
pushOverlay(overlay)
|
||||||
if (shouldPushState) history.pushState({view: "post_overlay", shortcode: shortcode}, "", `/p/${shortcode}`)
|
if (stateChangeType === "push") {
|
||||||
const fetcher = fetchShortcodeFragment(shortcode)
|
history.pushState({view: "post_overlay", shortcode: shortcode}, "", `/p/${shortcode}`)
|
||||||
fetcher.then(root => {
|
} else if (stateChangeType === "replace") {
|
||||||
shortcodeDataMap.set(shortcode, root)
|
history.replaceState({view: "post_overlay", shortcode: shortcode}, "", `/p/${shortcode}`)
|
||||||
if (overlay.available) {
|
} else if (stateChangeType !== "none") {
|
||||||
const {title, html} = root
|
throw new Error("Unknown stateChangeType: "+stateChangeType)
|
||||||
overlay.setContent(html)
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const fetcher = fetchShortcodeFragment(shortcode)
|
||||||
|
fetcher.then(root => {
|
||||||
|
shortcodeDataMap.set(shortcode, root)
|
||||||
if (overlay.available) {
|
if (overlay.available) {
|
||||||
document.title = title
|
const {title, html} = root
|
||||||
|
overlay.setContent(html)
|
||||||
|
if (overlay.available) {
|
||||||
|
document.title = title
|
||||||
|
}
|
||||||
|
while (postOverlays.length >= 2) postOverlays.shift().pop()
|
||||||
|
const entry = timeline.entries.get(shortcode)
|
||||||
|
let canInteractWithNavigation = true
|
||||||
|
overlay.element.querySelectorAll(".navigate-posts").forEach(button => {
|
||||||
|
button.addEventListener("click", async event => {
|
||||||
|
/** @type {HTMLButtonElement} */
|
||||||
|
//@ts-ignore
|
||||||
|
const button = event.currentTarget
|
||||||
|
if (button.classList.contains("next")) {
|
||||||
|
navigate("next")
|
||||||
|
} else {
|
||||||
|
navigate("previous")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
overlay.addKeyboardCallback(event => {
|
||||||
|
if (event.key === "ArrowRight") navigate("next")
|
||||||
|
else if (event.key === "ArrowLeft") navigate("previous")
|
||||||
|
})
|
||||||
|
async function navigate(direction) {
|
||||||
|
if (canInteractWithNavigation) {
|
||||||
|
/** @type {HTMLButtonElement} */
|
||||||
|
//@ts-ignore
|
||||||
|
if (direction === "next") {
|
||||||
|
canInteractWithNavigation = false
|
||||||
|
if (entry.isLastEntry()) await timeline.fetch()
|
||||||
|
if (!overlay.available) return
|
||||||
|
var futureShortcode = entry.getNextShortcode()
|
||||||
|
} else { // "previous"
|
||||||
|
if (entry.isFirstEntry()) return
|
||||||
|
canInteractWithNavigation = false
|
||||||
|
var futureShortcode = entry.getPreviousShortcode()
|
||||||
|
}
|
||||||
|
await loadPostOverlay(futureShortcode, "replace")
|
||||||
|
const newOverlay = postOverlays.slice(-1)[0]
|
||||||
|
if (newOverlay === overlay) { // was cancelled
|
||||||
|
canInteractWithNavigation = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
resolve()
|
||||||
})
|
})
|
||||||
fetcher.catch(error => {
|
fetcher.catch(error => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
overlay.showError()
|
overlay.showError()
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
56
src/site/html/static/js/post_series.js
Normal file
56
src/site/html/static/js/post_series.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import {controller} from "./pagination.js"
|
||||||
|
|
||||||
|
class Timeline {
|
||||||
|
constructor() {
|
||||||
|
this.shortcodes = []
|
||||||
|
/** @type {Map<string, TimelineEntry>} */
|
||||||
|
this.entries = new Map()
|
||||||
|
controller.addActivatedCallback(() => this.update())
|
||||||
|
this.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.shortcodes = []
|
||||||
|
document.querySelectorAll("#timeline .sized-link").forEach(element => {
|
||||||
|
const shortcode = element.getAttribute("data-shortcode")
|
||||||
|
this.shortcodes.push(shortcode)
|
||||||
|
this.entries.set(shortcode, new TimelineEntry(this, shortcode))
|
||||||
|
})
|
||||||
|
console.log(this.shortcodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch() {
|
||||||
|
return controller.activate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Timeline} timeline
|
||||||
|
* @param {string} shortcode
|
||||||
|
*/
|
||||||
|
class TimelineEntry {
|
||||||
|
constructor(timeline, shortcode) {
|
||||||
|
this.timeline = timeline
|
||||||
|
this.shortcode = shortcode
|
||||||
|
}
|
||||||
|
|
||||||
|
getNextShortcode() {
|
||||||
|
return this.timeline.shortcodes[this.timeline.shortcodes.indexOf(this.shortcode)+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
getPreviousShortcode() {
|
||||||
|
return this.timeline.shortcodes[this.timeline.shortcodes.indexOf(this.shortcode)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
isFirstEntry() {
|
||||||
|
return this.timeline.shortcodes.indexOf(this.shortcode) === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
isLastEntry() {
|
||||||
|
return this.timeline.shortcodes.indexOf(this.shortcode) === this.timeline.shortcodes.length-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeline = new Timeline()
|
||||||
|
|
||||||
|
export {timeline}
|
@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
include ../includes/post.pug
|
include ../includes/post.pug
|
||||||
|
|
||||||
+post(post)
|
+post(post, true)
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
include ./display_structured
|
include ./display_structured
|
||||||
|
|
||||||
mixin post(post)
|
mixin post(post, headerWithNavigation)
|
||||||
.post-page-divider
|
.post-page-divider
|
||||||
section.description-section
|
section.description-section
|
||||||
header.user-header
|
.user-header
|
||||||
img(src=post.ownerPfpCacheP width=150 height=150 alt="").pfp
|
header.user-header-inner
|
||||||
a.name(href=`/u/${post.getBasicOwner().username}`)= `${post.data.owner.full_name} (@${post.getBasicOwner().username})`
|
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 headerWithNavigation
|
||||||
|
button.navigate-posts.previous
|
||||||
|
img(src="/static/img/arrow-circled.svg" alt="Previous post." style="transform: rotate(180deg)").icon
|
||||||
|
button.navigate-posts.next
|
||||||
|
img(src="/static/img/arrow-circled.svg" alt="Next post.").icon
|
||||||
if post.getCaption()
|
if post.getCaption()
|
||||||
p.structured-text.description
|
p.structured-text.description
|
||||||
+display_structured(post.getStructuredCaption())
|
+display_structured(post.getStructuredCaption())
|
||||||
|
@ -13,4 +13,4 @@ html
|
|||||||
script(type="module" src="/static/js/post_overlay.js")
|
script(type="module" src="/static/js/post_overlay.js")
|
||||||
body.post-page
|
body.post-page
|
||||||
main
|
main
|
||||||
+post(post)
|
+post(post, false)
|
||||||
|
@ -248,27 +248,47 @@ body
|
|||||||
height: inherit
|
height: inherit
|
||||||
|
|
||||||
.user-header
|
.user-header
|
||||||
display: flex
|
display: grid
|
||||||
align-items: center
|
align-items: center
|
||||||
|
grid-template-columns: auto 1fr auto
|
||||||
justify-content: center
|
justify-content: center
|
||||||
background-color: #b3b3b3
|
background-color: #b3b3b3
|
||||||
padding: 10px
|
padding: 10px
|
||||||
|
|
||||||
.pfp
|
.navigate-posts
|
||||||
$size: 40px
|
-webkit-appearance: none
|
||||||
|
-moz-appearance: none
|
||||||
|
border: none
|
||||||
|
margin: 0
|
||||||
|
padding: 0
|
||||||
|
cursor: pointer
|
||||||
|
background: none
|
||||||
|
|
||||||
width: $size
|
.icon
|
||||||
height: $size
|
display: block
|
||||||
margin-right: 10px
|
|
||||||
background-color: rgba(40, 40, 40, 0.25)
|
|
||||||
|
|
||||||
.name
|
.user-header-inner
|
||||||
font-size: 20px
|
display: flex
|
||||||
color: black
|
align-items: center
|
||||||
text-decoration: none
|
justify-content: center
|
||||||
|
grid-row: 1
|
||||||
|
grid-column: 2
|
||||||
|
|
||||||
&:hover
|
.pfp
|
||||||
text-decoration: underline
|
$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
|
.description
|
||||||
margin: 12px
|
margin: 12px
|
||||||
@ -402,7 +422,7 @@ body
|
|||||||
display: flex
|
display: flex
|
||||||
|
|
||||||
.text, .button
|
.text, .button
|
||||||
appearance: none
|
-webkit-appearance: none
|
||||||
-moz-appearance: none
|
-moz-appearance: none
|
||||||
display: flex
|
display: flex
|
||||||
padding: 9px 8px 7px
|
padding: 9px 8px 7px
|
||||||
|
Loading…
Reference in New Issue
Block a user