Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3a429d7e1 | ||
|
|
d8179bb066 | ||
|
|
960305dfa9 | ||
|
|
16bbeec5a3 | ||
|
|
39f9c39360 | ||
|
|
011b6d14e4 | ||
|
|
d4661e7f04 |
+4
-1
@@ -15,7 +15,10 @@ RUN --mount=type=cache,target=/root/go/pkg/mod \
|
|||||||
|
|
||||||
# Copy package files and install npm dependencies (cached unless package.json changes)
|
# Copy package files and install npm dependencies (cached unless package.json changes)
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN --mount=type=cache,target=/root/.npm \
|
# npm cache mount renamed from /root/.npm: the old volume held a stale
|
||||||
|
# foliate-js tarball (name@version reuse) that --no-cache and builder prune
|
||||||
|
# could not evict; a new mount id guarantees a clean fetch.
|
||||||
|
RUN --mount=type=cache,target=/root/.npm-cache2 \
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Copy Go mod files (cached unless go.mod changes)
|
# Copy Go mod files (cached unless go.mod changes)
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
"dev": "npm run build:ts:dev && npm run build:css"
|
"dev": "npm run build:ts:dev && npm run build:css"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bookhoard/foliate-js": "git+https://git.linuxhg.com/Bookhoard/foliate-js.git#8a20399",
|
"@bookhoard/foliate-js": "git+https://git.linuxhg.com/Bookhoard/foliate-js.git#1305a52",
|
||||||
"alpinejs": "^3.15.8",
|
"alpinejs": "^3.15.8",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
|||||||
@@ -139,7 +139,13 @@ templ Reader(user User, metadata ReaderMetadata) {
|
|||||||
:style="'background-color:' + c"
|
:style="'background-color:' + c"
|
||||||
:title="selectionPopover.mode === 'create' ? 'Highlight' : 'Set color'"
|
:title="selectionPopover.mode === 'create' ? 'Highlight' : 'Set color'"
|
||||||
:aria-label="'Highlight color ' + c"
|
:aria-label="'Highlight color ' + c"
|
||||||
@click="selectionPopover.mode === 'create' ? createHighlight(c) : (selectionPopover.color = c, saveHighlightChanges())"
|
@click="selectionPopover.noteOpen
|
||||||
|
? (selectionPopover.mode === 'create'
|
||||||
|
? (selectionPopover.color = c)
|
||||||
|
: (selectionPopover.color = c, saveHighlightChanges(true)))
|
||||||
|
: (selectionPopover.mode === 'create'
|
||||||
|
? createHighlight(c)
|
||||||
|
: (selectionPopover.color = c, saveHighlightChanges()))"
|
||||||
></button>
|
></button>
|
||||||
</template>
|
</template>
|
||||||
<div class="w-px h-5 reader-sep"></div>
|
<div class="w-px h-5 reader-sep"></div>
|
||||||
@@ -188,7 +194,7 @@ templ Reader(user User, metadata ReaderMetadata) {
|
|||||||
type="button"
|
type="button"
|
||||||
class="flex-1 py-1 text-xs border rounded hover:opacity-80"
|
class="flex-1 py-1 text-xs border rounded hover:opacity-80"
|
||||||
style="border-color: var(--border);"
|
style="border-color: var(--border);"
|
||||||
@click="selectionPopover.noteOpen = false"
|
@click="hideSelectionPopover()"
|
||||||
>Cancel</button>
|
>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,14 +50,14 @@ func Reader(user User, metadata ReaderMetadata) templ.Component {
|
|||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\"><title>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"overscroll-none\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\"><title>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 30, Col: 26}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 30, Col: 26}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -67,7 +67,7 @@ func Reader(user User, metadata ReaderMetadata) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var3 = []any{"theme-" + user.Theme + " h-screen overflow-hidden"}
|
var templ_7745c5c3_Var3 = []any{"theme-" + user.Theme + " h-screen overflow-hidden overscroll-none"}
|
||||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
@@ -79,7 +79,7 @@ func Reader(user User, metadata ReaderMetadata) templ.Component {
|
|||||||
var templ_7745c5c3_Var4 string
|
var templ_7745c5c3_Var4 string
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(readerInitExpr(metadata))
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(readerInitExpr(metadata))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 40, Col: 36}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 40, Col: 36}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -92,7 +92,7 @@ func Reader(user User, metadata ReaderMetadata) templ.Component {
|
|||||||
var templ_7745c5c3_Var5 string
|
var templ_7745c5c3_Var5 string
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String())
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String())
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 1, Col: 0}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 1, Col: 0}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -122,7 +122,7 @@ func Reader(user User, metadata ReaderMetadata) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><!-- Selection popover (highlights) --><div id=\"selection-popover\" x-show=\"selectionPopover.open\" x-transition:enter=\"transition ease-out duration-150\" x-transition:enter-start=\"opacity-0 scale-95\" x-transition:enter-end=\"opacity-100 scale-100\" x-transition:leave=\"transition ease-in duration-100\" x-transition:leave-start=\"opacity-100 scale-100\" x-transition:leave-end=\"opacity-0 scale-95\" :style=\"'left:' + selectionPopover.x + 'px; top:' + selectionPopover.y + 'px'\" class=\"reader-popover\" role=\"dialog\" aria-label=\"Highlight selection\" @pointerdown.stop><div class=\"flex items-center gap-2\"><template x-for=\"c in highlightColors\" :key=\"c\"><button type=\"button\" class=\"color-dot\" :class=\"selectionPopover.color === c ? 'selected' : ''\" :style=\"'background-color:' + c\" :title=\"selectionPopover.mode === 'create' ? 'Highlight' : 'Set color'\" :aria-label=\"'Highlight color ' + c\" @click=\"selectionPopover.mode === 'create' ? createHighlight(c) : (selectionPopover.color = c, saveHighlightChanges())\"></button></template><div class=\"w-px h-5 reader-sep\"></div><button type=\"button\" class=\"reader-popover-btn\" title=\"Note\" @click=\"selectionPopover.noteOpen = true\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 4 13 L 4 16 L 7 16 L 14.5 8.5 L 11.5 5.5 L 4 13 M 12.5 4.5 L 15.5 7.5\"></path></svg></button> <button type=\"button\" class=\"reader-popover-btn\" title=\"Copy text\" @click=\"copySelectionText()\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 6 6 V 3 H 16 V 13 H 13 M 3 6 H 13 V 16 H 3 Z\"></path></svg></button><template x-if=\"selectionPopover.mode === 'edit'\"><button type=\"button\" class=\"reader-popover-btn danger\" title=\"Delete highlight\" @click=\"deleteHighlightById(selectionPopover.id)\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 5 6 H 15 L 14 17 H 6 Z M 8 6 V 4 H 12 V 6 M 4 6 H 16\"></path></svg></button></template></div><div x-show=\"selectionPopover.noteOpen\" class=\"mt-2\"><textarea x-model=\"selectionPopover.note\" rows=\"3\" class=\"reader-note-input\" placeholder=\"Note…\"></textarea><div class=\"flex gap-2 mt-1\"><button type=\"button\" class=\"flex-1 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700\" @click=\"selectionPopover.mode === 'create' ? createHighlight(selectionPopover.color) : saveHighlightChanges()\" x-text=\"selectionPopover.mode === 'create' ? 'Highlight with note' : 'Save note'\">Save</button> <button type=\"button\" class=\"flex-1 py-1 text-xs border rounded hover:opacity-80\" style=\"border-color: var(--border);\" @click=\"selectionPopover.noteOpen = false\">Cancel</button></div></div></div><!-- Annotations drawer (right): highlights / notes / bookmarks --><div id=\"annotations-drawer\" x-show=\"bookmarksOpen\" x-transition:enter=\"transition-transform duration-200 ease-out\" x-transition:enter-start=\"translate-x-full\" x-transition:enter-end=\"translate-x-0\" x-transition:leave=\"transition-transform duration-150 ease-in\" x-transition:leave-start=\"translate-x-0\" x-transition:leave-end=\"translate-x-full\" class=\"reader-drawer right\" role=\"dialog\" aria-label=\"Annotations\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><!-- Selection popover (highlights) --><div id=\"selection-popover\" x-show=\"selectionPopover.open\" x-transition:enter=\"transition ease-out duration-150\" x-transition:enter-start=\"opacity-0 scale-95\" x-transition:enter-end=\"opacity-100 scale-100\" x-transition:leave=\"transition ease-in duration-100\" x-transition:leave-start=\"opacity-100 scale-100\" x-transition:leave-end=\"opacity-0 scale-95\" :style=\"'left:' + selectionPopover.x + 'px; top:' + selectionPopover.y + 'px'\" class=\"reader-popover\" role=\"dialog\" aria-label=\"Highlight selection\" @pointerdown.stop><div class=\"flex items-center gap-2\"><template x-for=\"c in highlightColors\" :key=\"c\"><button type=\"button\" class=\"color-dot\" :class=\"selectionPopover.color === c ? 'selected' : ''\" :style=\"'background-color:' + c\" :title=\"selectionPopover.mode === 'create' ? 'Highlight' : 'Set color'\" :aria-label=\"'Highlight color ' + c\" @click=\"selectionPopover.noteOpen\n\t\t\t\t\t\t\t? (selectionPopover.mode === 'create'\n\t\t\t\t\t\t\t\t? (selectionPopover.color = c)\n\t\t\t\t\t\t\t\t: (selectionPopover.color = c, saveHighlightChanges(true)))\n\t\t\t\t\t\t\t: (selectionPopover.mode === 'create'\n\t\t\t\t\t\t\t\t? createHighlight(c)\n\t\t\t\t\t\t\t\t: (selectionPopover.color = c, saveHighlightChanges()))\"></button></template><div class=\"w-px h-5 reader-sep\"></div><button type=\"button\" class=\"reader-popover-btn\" title=\"Note\" @click=\"selectionPopover.noteOpen = true\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 4 13 L 4 16 L 7 16 L 14.5 8.5 L 11.5 5.5 L 4 13 M 12.5 4.5 L 15.5 7.5\"></path></svg></button> <button type=\"button\" class=\"reader-popover-btn\" title=\"Copy text\" @click=\"copySelectionText()\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 6 6 V 3 H 16 V 13 H 13 M 3 6 H 13 V 16 H 3 Z\"></path></svg></button><template x-if=\"selectionPopover.mode === 'edit'\"><button type=\"button\" class=\"reader-popover-btn danger\" title=\"Delete highlight\" @click=\"deleteHighlightById(selectionPopover.id)\"><svg class=\"reader-icon\" width=\"16\" height=\"16\" aria-hidden=\"true\"><path d=\"M 5 6 H 15 L 14 17 H 6 Z M 8 6 V 4 H 12 V 6 M 4 6 H 16\"></path></svg></button></template></div><div x-show=\"selectionPopover.noteOpen\" class=\"mt-2\"><textarea x-model=\"selectionPopover.note\" rows=\"3\" class=\"reader-note-input\" placeholder=\"Note…\"></textarea><div class=\"flex gap-2 mt-1\"><button type=\"button\" class=\"flex-1 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700\" @click=\"selectionPopover.mode === 'create' ? createHighlight(selectionPopover.color) : saveHighlightChanges()\" x-text=\"selectionPopover.mode === 'create' ? 'Highlight with note' : 'Save note'\">Save</button> <button type=\"button\" class=\"flex-1 py-1 text-xs border rounded hover:opacity-80\" style=\"border-color: var(--border);\" @click=\"hideSelectionPopover()\">Cancel</button></div></div></div><!-- Annotations drawer (right): highlights / notes / bookmarks --><div id=\"annotations-drawer\" x-show=\"bookmarksOpen\" x-transition:enter=\"transition-transform duration-200 ease-out\" x-transition:enter-start=\"translate-x-full\" x-transition:enter-end=\"translate-x-0\" x-transition:leave=\"transition-transform duration-150 ease-in\" x-transition:leave-start=\"translate-x-0\" x-transition:leave-end=\"translate-x-full\" class=\"reader-drawer right\" role=\"dialog\" aria-label=\"Annotations\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ func ReaderChrome(metadata ReaderMetadata) templ.Component {
|
|||||||
var templ_7745c5c3_Var7 templ.SafeURL
|
var templ_7745c5c3_Var7 templ.SafeURL
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + metadata.MediaItemID)
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + metadata.MediaItemID)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 292, Col: 63}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 298, Col: 63}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -187,7 +187,7 @@ func ReaderChrome(metadata ReaderMetadata) templ.Component {
|
|||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 295, Col: 95}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 301, Col: 95}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -229,7 +229,7 @@ func drawerHeader(title string) templ.Component {
|
|||||||
var templ_7745c5c3_Var10 string
|
var templ_7745c5c3_Var10 string
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 555, Col: 35}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `reader.templ`, Line: 561, Col: 35}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
+129
-77
@@ -724,10 +724,26 @@ document.addEventListener("alpine:init", () => {
|
|||||||
// drags, so the popover's pointerup trigger never fires for
|
// drags, so the popover's pointerup trigger never fires for
|
||||||
// them; a selection that has been stable for a moment is the
|
// them; a selection that has been stable for a moment is the
|
||||||
// settled gesture — open the popover for it.
|
// settled gesture — open the popover for it.
|
||||||
if (!this.isFixedLayout)
|
if (this.isFixedLayout && doc.querySelector(".textLayer"))
|
||||||
|
this.schedulePDFSelectionPopover(doc, index);
|
||||||
|
else if (!this.isFixedLayout)
|
||||||
this.scheduleSelectionPopover(doc, index);
|
this.scheduleSelectionPopover(doc, index);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Clicks in the book dismiss the popover in ANY mode and for ANY
|
||||||
|
// content doc (EPUB sections and fixed-layout pages alike): iframe
|
||||||
|
// events never bubble to the host document (so the host
|
||||||
|
// outside-click dismiss never sees them). Clicking a painted
|
||||||
|
// highlight still works: this hides, then foliate's
|
||||||
|
// show-annotation re-opens it in edit mode.
|
||||||
|
doc.addEventListener(
|
||||||
|
"pointerdown",
|
||||||
|
() => {
|
||||||
|
if (this.selectionPopover.open)
|
||||||
|
this.hideSelectionPopover();
|
||||||
|
},
|
||||||
|
{ passive: true },
|
||||||
|
);
|
||||||
// Text selection → highlight popover (reflowable EPUB only;
|
// Text selection → highlight popover (reflowable EPUB only;
|
||||||
// fixed-layout highlight overlays are a later milestone).
|
// fixed-layout highlight overlays are a later milestone).
|
||||||
if (!this.isFixedLayout) {
|
if (!this.isFixedLayout) {
|
||||||
@@ -757,21 +773,6 @@ document.addEventListener("alpine:init", () => {
|
|||||||
(e: PointerEvent) => setTimeout(() => checkSelection(e), 0),
|
(e: PointerEvent) => setTimeout(() => checkSelection(e), 0),
|
||||||
{ passive: true },
|
{ passive: true },
|
||||||
);
|
);
|
||||||
// Clicks in the book dismiss the popover in ANY mode: iframe
|
|
||||||
// events never bubble to the host document (so the host
|
|
||||||
// outside-click dismiss never sees them), and the collapsed
|
|
||||||
// check above only covers create mode — edit mode had no
|
|
||||||
// outside-click path at all, leaving Esc as the only way out.
|
|
||||||
// Clicking a painted highlight still works: this hides, then
|
|
||||||
// foliate's show-annotation re-opens it in edit mode.
|
|
||||||
doc.addEventListener(
|
|
||||||
"pointerdown",
|
|
||||||
() => {
|
|
||||||
if (this.selectionPopover.open)
|
|
||||||
this.hideSelectionPopover();
|
|
||||||
},
|
|
||||||
{ passive: true },
|
|
||||||
);
|
|
||||||
doc.addEventListener(
|
doc.addEventListener(
|
||||||
"keyup",
|
"keyup",
|
||||||
(ev: KeyboardEvent) => {
|
(ev: KeyboardEvent) => {
|
||||||
@@ -785,65 +786,23 @@ document.addEventListener("alpine:init", () => {
|
|||||||
// spread renders (during view.init), which is after this listener
|
// spread renders (during view.init), which is after this listener
|
||||||
// attaches — the stale copy here would always be falsy.
|
// attaches — the stale copy here would always be falsy.
|
||||||
if (this.isFixedLayout && doc.querySelector(".textLayer")) {
|
if (this.isFixedLayout && doc.querySelector(".textLayer")) {
|
||||||
const checkPDFSelection = () => {
|
|
||||||
const sel = doc.getSelection();
|
|
||||||
if (!sel || sel.isCollapsed || !sel.rangeCount) return;
|
|
||||||
const range = sel.getRangeAt(0);
|
|
||||||
const text = sel.toString().replace(/\s+/g, " ").trim();
|
|
||||||
if (!text) return;
|
|
||||||
// Denominator: the element whose post-transform screen rect IS
|
|
||||||
// the visible page. For PDFs that's the rendered canvas — pdf.js
|
|
||||||
// scales <html> by 1/devicePixelRatio, so documentElement's rect
|
|
||||||
// is dpr× too small and would inflate every fraction (highlight
|
|
||||||
// shifted right/oversized on any dpr != 1 display). The canvas's
|
|
||||||
// rect is in the same transform-inclusive space as the textLayer
|
|
||||||
// span rects, so the dpr scaling cancels exactly.
|
|
||||||
const denom =
|
|
||||||
(doc.querySelector("#canvas canvas") as HTMLElement) ||
|
|
||||||
(doc.querySelector("img") as HTMLElement);
|
|
||||||
let dr = denom?.getBoundingClientRect();
|
|
||||||
if (!dr || !dr.width || !dr.height) {
|
|
||||||
const vw = doc.defaultView;
|
|
||||||
dr = {
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
width: vw?.innerWidth || 1,
|
|
||||||
height: vw?.innerHeight || 1,
|
|
||||||
} as DOMRect;
|
|
||||||
}
|
|
||||||
const rects: number[][] = [];
|
|
||||||
for (const r of range.getClientRects()) {
|
|
||||||
const x = (r.left - dr.left) / dr.width;
|
|
||||||
const y = (r.top - dr.top) / dr.height;
|
|
||||||
const w = r.width / dr.width;
|
|
||||||
const h = r.height / dr.height;
|
|
||||||
if (w > 0 && h > 0) rects.push([x, y, w, h]);
|
|
||||||
}
|
|
||||||
if (!rects.length) return;
|
|
||||||
// Map the first rect to host-space for popover placement.
|
|
||||||
// The canvas's screen rect maps 1:1 onto the host iframe box
|
|
||||||
// (the visible page fills the iframe), so sx/sy are 1 for PDFs;
|
|
||||||
// kept general for the comic img fallback.
|
|
||||||
const frame = doc.defaultView?.frameElement as HTMLElement | null;
|
|
||||||
if (!frame) return;
|
|
||||||
const fr = frame.getBoundingClientRect();
|
|
||||||
const first = range.getBoundingClientRect();
|
|
||||||
const sx = fr.width / dr.width;
|
|
||||||
const sy = fr.height / dr.height;
|
|
||||||
this.pdfSelDoc = doc;
|
|
||||||
this.openSelectionPopover({
|
|
||||||
mode: "create",
|
|
||||||
x: fr.left + first.left * sx + (first.width * sx) / 2,
|
|
||||||
y: fr.top + first.top * sy,
|
|
||||||
text,
|
|
||||||
cfi: "",
|
|
||||||
pdfPage: index,
|
|
||||||
pdfRects: rects,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
doc.addEventListener(
|
doc.addEventListener(
|
||||||
"pointerup",
|
"pointerup",
|
||||||
() => setTimeout(checkPDFSelection, 0),
|
(e: PointerEvent) =>
|
||||||
|
setTimeout(() => {
|
||||||
|
// Chrome on touch selects the word under a quick tap —
|
||||||
|
// that is a page-turn tap, not a selection gesture; clear
|
||||||
|
// it and show nothing (same guard as the reflowable path).
|
||||||
|
if (
|
||||||
|
e.pointerType === "touch" &&
|
||||||
|
this.lastTouchDownT &&
|
||||||
|
Date.now() - this.lastTouchDownT < 300
|
||||||
|
) {
|
||||||
|
doc.getSelection()?.removeAllRanges();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.checkPDFSelectionFor(doc, index);
|
||||||
|
}, 0),
|
||||||
{ passive: true },
|
{ passive: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1332,6 +1291,94 @@ document.addEventListener("alpine:init", () => {
|
|||||||
this.openPopoverForSelection(sel, doc, index);
|
this.openPopoverForSelection(sel, doc, index);
|
||||||
}, 400);
|
}, 400);
|
||||||
},
|
},
|
||||||
|
// PDF mirror of scheduleSelectionPopover: the gesture takeover
|
||||||
|
// swallows pointerup for touch selection drags in the fx iframe too,
|
||||||
|
// so a selection that has been stable for a moment is the settled
|
||||||
|
// gesture — open the popover for it.
|
||||||
|
schedulePDFSelectionPopover(doc: any, index: number) {
|
||||||
|
if (this.selPopTimer) clearTimeout(this.selPopTimer);
|
||||||
|
this.selPopTimer = setTimeout(() => {
|
||||||
|
this.selPopTimer = null;
|
||||||
|
// A tap on a painted highlight re-opens the popover in edit mode
|
||||||
|
// via its own path — never clobber it.
|
||||||
|
if (this.selectionPopover.open && this.selectionPopover.mode === "edit")
|
||||||
|
return;
|
||||||
|
const sel = doc.getSelection();
|
||||||
|
if (!sel || sel.isCollapsed || !sel.rangeCount) {
|
||||||
|
if (
|
||||||
|
this.selectionPopover.open &&
|
||||||
|
this.selectionPopover.mode === "create"
|
||||||
|
)
|
||||||
|
this.hideSelectionPopover();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.checkPDFSelectionFor(doc, index);
|
||||||
|
}, 400);
|
||||||
|
},
|
||||||
|
checkPDFSelectionFor(doc: any, index: number) {
|
||||||
|
const sel = doc.getSelection();
|
||||||
|
if (!sel || sel.isCollapsed || !sel.rangeCount) return;
|
||||||
|
const range = sel.getRangeAt(0);
|
||||||
|
const text = sel.toString().replace(/\s+/g, " ").trim();
|
||||||
|
if (!text) return;
|
||||||
|
// Denominator: the element whose post-transform screen rect IS
|
||||||
|
// the visible page. For PDFs that's the rendered canvas — pdf.js
|
||||||
|
// scales <html> by 1/devicePixelRatio, so documentElement's rect
|
||||||
|
// is dpr× too small and would inflate every fraction (highlight
|
||||||
|
// shifted right/oversized on any dpr != 1 display). The canvas's
|
||||||
|
// rect is in the same transform-inclusive space as the textLayer
|
||||||
|
// span rects, so the dpr scaling cancels exactly.
|
||||||
|
const denom =
|
||||||
|
(doc.querySelector("#canvas canvas") as HTMLElement) ||
|
||||||
|
(doc.querySelector("img") as HTMLElement);
|
||||||
|
let dr = denom?.getBoundingClientRect();
|
||||||
|
if (!dr || !dr.width || !dr.height) {
|
||||||
|
const vw = doc.defaultView;
|
||||||
|
dr = {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: vw?.innerWidth || 1,
|
||||||
|
height: vw?.innerHeight || 1,
|
||||||
|
} as DOMRect;
|
||||||
|
}
|
||||||
|
const rects: number[][] = [];
|
||||||
|
for (const r of range.getClientRects()) {
|
||||||
|
const x = (r.left - dr.left) / dr.width;
|
||||||
|
const y = (r.top - dr.top) / dr.height;
|
||||||
|
const w = r.width / dr.width;
|
||||||
|
const h = r.height / dr.height;
|
||||||
|
if (w > 0 && h > 0) rects.push([x, y, w, h]);
|
||||||
|
}
|
||||||
|
if (!rects.length) return;
|
||||||
|
// Map the first rect to host-space for popover placement.
|
||||||
|
// The canvas's screen rect maps 1:1 onto the host iframe box
|
||||||
|
// (the visible page fills the iframe), so sx/sy are 1 for PDFs;
|
||||||
|
// kept general for the comic img fallback.
|
||||||
|
const frame = doc.defaultView?.frameElement as HTMLElement | null;
|
||||||
|
if (!frame) return;
|
||||||
|
const fr = frame.getBoundingClientRect();
|
||||||
|
const first = range.getBoundingClientRect();
|
||||||
|
const sx = fr.width / dr.width;
|
||||||
|
const sy = fr.height / dr.height;
|
||||||
|
// On touch the browser's own selection menu attaches ABOVE the
|
||||||
|
// selection and its drag handles sit just below it — place ours
|
||||||
|
// below both (same +90 clearance the reflowable path uses). Fine
|
||||||
|
// pointers (desktop) keep the above-the-selection placement.
|
||||||
|
const coarse = window.matchMedia("(pointer: coarse)").matches;
|
||||||
|
const y = coarse
|
||||||
|
? fr.top + first.bottom * sy + 90
|
||||||
|
: fr.top + first.top * sy;
|
||||||
|
this.pdfSelDoc = doc;
|
||||||
|
this.openSelectionPopover({
|
||||||
|
mode: "create",
|
||||||
|
x: fr.left + first.left * sx + (first.width * sx) / 2,
|
||||||
|
y,
|
||||||
|
text,
|
||||||
|
cfi: "",
|
||||||
|
pdfPage: index,
|
||||||
|
pdfRects: rects,
|
||||||
|
});
|
||||||
|
},
|
||||||
// Trim a touch selection to the visible page. The visible column
|
// Trim a touch selection to the visible page. The visible column
|
||||||
// occupies content x [start - size, start]; endpoints beyond it are
|
// occupies content x [start - size, start]; endpoints beyond it are
|
||||||
// re-mapped to the page edge on their own line via caretRangeFromPoint.
|
// re-mapped to the page edge on their own line via caretRangeFromPoint.
|
||||||
@@ -1579,6 +1626,9 @@ document.addEventListener("alpine:init", () => {
|
|||||||
const p = this.selectionPopover;
|
const p = this.selectionPopover;
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token || !this.mediaItemId || (!p.cfi && p.pdfPage < 0)) return;
|
if (!token || !this.mediaItemId || (!p.cfi && p.pdfPage < 0)) return;
|
||||||
|
// The note editor writes into p.note live; "Highlight with note"
|
||||||
|
// creates the highlight AND its note in one POST.
|
||||||
|
const note = p.note.trim();
|
||||||
const pdfAnchor =
|
const pdfAnchor =
|
||||||
p.pdfPage >= 0
|
p.pdfPage >= 0
|
||||||
? JSON.stringify({ v: 1, page: p.pdfPage, rects: p.pdfRects })
|
? JSON.stringify({ v: 1, page: p.pdfPage, rects: p.pdfRects })
|
||||||
@@ -1599,7 +1649,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
epubcfi_start: p.pdfPage >= 0 ? pdfAnchor : p.cfi,
|
epubcfi_start: p.pdfPage >= 0 ? pdfAnchor : p.cfi,
|
||||||
epubcfi_end: p.pdfPage >= 0 ? pdfAnchor : p.cfiEnd,
|
epubcfi_end: p.pdfPage >= 0 ? pdfAnchor : p.cfiEnd,
|
||||||
color,
|
color,
|
||||||
note_text: "",
|
note_text: note,
|
||||||
percentage_start: this.lastRelocateDetail?.fraction ?? 0,
|
percentage_start: this.lastRelocateDetail?.fraction ?? 0,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -1628,7 +1678,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
? ""
|
? ""
|
||||||
: this.toRangeCfi(p.cfi, p.cfiEnd, p.text) || p.cfi,
|
: this.toRangeCfi(p.cfi, p.cfiEnd, p.text) || p.cfi,
|
||||||
color,
|
color,
|
||||||
note: "",
|
note,
|
||||||
id: row.id,
|
id: row.id,
|
||||||
});
|
});
|
||||||
// Clear the DOM selection: the gesture is consumed, and it is
|
// Clear the DOM selection: the gesture is consumed, and it is
|
||||||
@@ -1644,7 +1694,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
/* ignore highlight errors */
|
/* ignore highlight errors */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async saveHighlightChanges() {
|
async saveHighlightChanges(keepNoteOpen = false) {
|
||||||
const p = this.selectionPopover;
|
const p = this.selectionPopover;
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token || !this.mediaItemId || !p.id) return;
|
if (!token || !this.mediaItemId || !p.id) return;
|
||||||
@@ -1708,7 +1758,9 @@ document.addEventListener("alpine:init", () => {
|
|||||||
id: p.id,
|
id: p.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
p.noteOpen = false;
|
// Explicit saves (the Save button) close the whole popover; color
|
||||||
|
// tweaks made while the note editor is open keep everything open.
|
||||||
|
if (!keepNoteOpen) this.hideSelectionPopover();
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
/* ignore highlight errors */
|
/* ignore highlight errors */
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user