Skip to content

Commit

Permalink
OPDS: multiple acquisition links in split buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
johnfactotum committed Dec 2, 2023
1 parent 6a04601 commit 56f94a5
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 25 deletions.
1 change: 1 addition & 0 deletions src/gresource.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<file>foliate-js/vendor/pdfjs/pdf.worker.js</file>
<file>opds/opds.html</file>
<file>opds/opds.js</file>
<file>opds/widgets.js</file>
<file>reader/reader.html</file>
<file>reader/reader.js</file>
<file>reader/markup.js</file>
Expand Down
45 changes: 26 additions & 19 deletions src/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -561,25 +561,32 @@ export const Library = GObject.registerClass({
}
showCatalog(url) {
this._main_stack.visible_child = this._catalog_toolbar_view
this._catalog_toolbar_view.content ??= utils.connect(new WebView({
settings: new WebKit.Settings({
enable_write_console_messages_to_stdout: true,
enable_developer_extras: true,
enable_back_forward_navigation_gestures: false,
enable_hyperlink_auditing: false,
enable_html5_database: false,
enable_html5_local_storage: false,
disable_web_security: true,
}),
}), {
'context-menu': () => false,
'load-changed': (webView, event) => {
if (event === WebKit.LoadEvent.FINISHED) {
webView.run(`globalThis.uiText = ${JSON.stringify(uiText)}`)
.catch(e => console.error(e))
}
},
})
if (!this._catalog_toolbar_view.content) {
const webView = new WebView({
settings: new WebKit.Settings({
enable_write_console_messages_to_stdout: true,
enable_developer_extras: true,
enable_back_forward_navigation_gestures: false,
enable_hyperlink_auditing: false,
enable_html5_database: false,
enable_html5_local_storage: false,
disable_web_security: true,
}),
})
const initFormatMediaType = webView.provide('formatMediaType', type =>
Gio.content_type_get_description(type))
utils.connect(webView, {
'context-menu': () => false,
'load-changed': (webView, event) => {
if (event === WebKit.LoadEvent.FINISHED) {
webView.run(`globalThis.uiText = ${JSON.stringify(uiText)}`)
.catch(e => console.error(e))
initFormatMediaType()
}
},
})
this._catalog_toolbar_view.content = webView
}
const webView = this._catalog_toolbar_view.content
webView.loadURI(`foliate-opds:///opds/opds.html?url=${encodeURIComponent(url)}`)
.catch(e => console.error(e))
Expand Down
75 changes: 72 additions & 3 deletions src/opds/opds.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
:root {
font-size: 11pt;
font-family: system-ui;
--shade: rgba(0, 0, 0, .1);
--raised: rgba(0, 0, 0, .05);
--pressed: rgba(0, 0, 0, .15);
}
@media (prefers-color-scheme: dark) {
:root {
--shade: rgba(255, 255, 255, .1);
--raised: rgba(255, 255, 255, .05);
--pressed: rgba(255, 255, 255, .15);
}
}
hgroup {
margin-bottom: 24px;
Expand Down Expand Up @@ -55,17 +65,76 @@
display: block;
max-width: 42em;
}
button {
button, foliate-menubutton::part(button) {
padding: 9px 18px;
font-weight: 700;
min-width: min(100%, 8em);
border-radius: 6px;
border: 0;
background: var(--shade);
}
opds-pub-full [slot="actions"] {
button:hover, foliate-menubutton::part(button):hover {
background: var(--raised);
}
button:active, foliate-menubutton::part(button):active {
background: var(--pressed);
}
opds-pub-full > [slot="actions"] {
display: flex;
flex-wrap: wrap;
gap: 9px;
width: 100%;
}
opds-pub-full > [slot="actions"] > * {
min-width: min(100%, 8em);
}
.split-button {
display: flex;
}
.split-button button {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
.split-button foliate-menubutton::part(button) {
padding: 9px;
border-start-start-radius: 0;
border-end-start-radius: 0;
border-inline-start: 1px solid color-mix(in hsl, currentColor, transparent 85%);;
}
foliate-menubutton {
display: flex;
}
foliate-menu {
position: absolute;
inset-block-start: 100%;
inset-inline-end: 0;
display: flex;
flex-direction: column;
width: max-content;
background: canvas;
border-radius: 9px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.03),
0 1px 3px 1px rgba(0, 0, 0, 0.07),
0 2px 6px 2px rgba(0, 0, 0, 0.03);
visibility: hidden;
padding: 6px;
}
foliate-menu:not([hidden]) {
visibility: visible;
}
button[role="menuitem"] {
border: 0;
text-align: start;
font: menu;
background: none;
border-radius: 6px;
padding: 9px;
}
button[role="menuitem"]:hover {
background: var(--raised);
}
button[role="menuitem"]:active {
background: var(--pressed);
}
</style>
<template id="opds-nav">
<style>
Expand Down
43 changes: 40 additions & 3 deletions src/opds/opds.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import './widgets.js'

const NS = {
ATOM: 'http://www.w3.org/2005/Atom',
}
Expand All @@ -17,6 +19,17 @@ const REL = {
],
}

const groupBy = (arr, f) => {
const map = new Map()
for (const el of arr) {
const key = f(el)
const group = map.get(key)
if (group) group.push(el)
else map.set(key, [el])
}
return map
}

const resolveURL = (url, relativeTo) => {
try {
if (relativeTo.includes(':')) return new URL(url, relativeTo)
Expand Down Expand Up @@ -229,13 +242,37 @@ const renderFeed = (doc, baseURL) => {
actions.slot = 'actions'
item.append(actions)

for (const link of acqLinks) {
const rel = link.getAttribute('rel').split(/ +/).find(r => r.startsWith(REL.ACQ))
const groups = groupBy(acqLinks, link =>
link.getAttribute('rel').split(/ +/).find(r => r.startsWith(REL.ACQ)))
for (const [rel, links] of groups.entries()) {
const label = globalThis.uiText.acq[rel]
?? globalThis.uiText.acq['http://opds-spec.org/acquisition']
const button = document.createElement('button')
button.innerText = label
actions.append(button)
if (links.length === 1) actions.append(button)
else {
const menuButton = document.createElement('foliate-menubutton')
menuButton.innerText = '▼'
const menu = document.createElement('foliate-menu')
menu.slot = 'menu'
menuButton.append(menu)

for (const link of links) {
const type = link.getAttribute('type')
const title = link.getAttribute('title')
const menuitem = document.createElement('button')
menuitem.role = 'menuitem'
if (title) menuitem.textContent = title
else globalThis.formatMediaType(type).then(text =>
menuitem.textContent = text)
menu.append(menuitem)
}

const div = document.createElement('div')
div.classList.add('split-button')
div.replaceChildren(button, menuButton)
actions.append(div)
}
}

document.querySelector('#entry').replaceChildren(item)
Expand Down
116 changes: 116 additions & 0 deletions src/opds/widgets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
customElements.define('foliate-menu', class extends HTMLElement {
#root = this.attachShadow({ mode: 'closed' })
#internals = this.attachInternals()
#items = []
constructor() {
super()
this.#internals.role = 'menu'
const slot = document.createElement('slot')
this.#root.append(slot)
slot.addEventListener('slotchange', e => {
const els = e.target.assignedElements()
this.#items = els.filter(el => el.matches('[role^="menuitem"]'))
})
this.addEventListener('keydown', e => this.#onKeyDown(e))
}
setFocusPrev(el) {
this.setFocusNext(el, this.#items.slice(0).reverse())
}
setFocusNext(el, items = this.#items) {
let justFound, found
for (const item of items) {
if (justFound) {
item.tabIndex = 0
item.focus()
found = true
justFound = false
}
else {
item.tabIndex = -1
if (item === el) justFound = true
}
}
if (!found) {
items[0].tabIndex = 0
items[0].focus()
}
}
#onKeyDown(e) {
switch (e.key) {
case 'ArrowUp':
e.preventDefault()
e.stopPropagation()
this.setFocusPrev(e.target)
break
case 'ArrowDown':
e.preventDefault()
e.stopPropagation()
this.setFocusNext(e.target)
break
}
}
})

customElements.define('foliate-menubutton', class extends HTMLElement {
#root = this.attachShadow({ mode: 'open' })
#button = document.createElement('button')
#menu
#ariaExpandedObserver = new MutationObserver(list => {
for (const { target } of list)
target.dispatchEvent(new Event('aria-expanded'))
})
#onBlur = () => this.#button.ariaExpanded = 'false'
#onClick = e => {
const target = e.composedPath()[0]
if (!this.#button.contains(target) && !this.#menu.contains(target)) {
this.#button.setAttribute('aria-expanded', 'false')
}
}
constructor() {
super()
const sheet = new CSSStyleSheet()
sheet.replaceSync(':host { position: relative }')
this.#root.adoptedStyleSheets = [sheet]

this.#root.append(this.#button)
this.#button.ariaExpanded = 'false'
this.#button.ariaHasPopup = 'menu'
this.#button.part = 'button'

const slot = document.createElement('slot')
this.#button.append(slot)

this.#ariaExpandedObserver.observe(this.#button,
{ attributes: true, attributeFilter: ['aria-expanded'] })
this.#button.addEventListener('click', () => {
this.#button.ariaExpanded =
this.#button.ariaExpanded === 'true' ? 'false' : 'true'
})
this.#button.addEventListener('aria-expanded', () => {
if (!this.#menu) return
if (this.#button.ariaExpanded === 'true') {
this.#menu.hidden = false
this.#menu.focus()
} else this.#menu.hidden = true
})

const menuSlot = document.createElement('slot')
menuSlot.name = 'menu'
this.#root.append(menuSlot)
menuSlot.addEventListener('slotchange', e => {
this.#menu = e.target.assignedElements()[0]
this.#menu.tabIndex = 0
this.#menu.hidden = true
})
menuSlot.addEventListener('keydown', e => e.key === 'Escape'
? this.#button.ariaExpanded = 'false' : null)
}
connectedCallback() {
addEventListener('blur', this.#onBlur)
addEventListener('click', this.#onClick)
}
disconnectedCallback() {
removeEventListener('blur', this.#onBlur)
removeEventListener('click', this.#onClick)
}
})

0 comments on commit 56f94a5

Please sign in to comment.