feat(ui): deleted-annotation history on the book page
Replace the Notes & Highlights 'coming soon' stub with a real modal: active counts plus a 'Recently deleted' section listing every tombstoned highlight, note, and bookmark (type badge, deletion time in the user's timezone, text preview), each with Restore and Delete-permanently actions. Restore returns the annotation to every synced device; Delete permanently is confirmed before purging. The list is server-rendered from MediaDetail.DeletedAnnotations — no fetch on open. Alpine handlers in book-detail.ts call the new restore/purge endpoints and reload on success. style.css picks up the line-clamp utilities used by the text previews.
This commit is contained in:
@@ -116,6 +116,8 @@ interface MetadataEditorState {
|
||||
clearRating(): Promise<void>;
|
||||
toggleRead(read: boolean): Promise<void>;
|
||||
resolveConflict(conflictId: string, winner: string): Promise<void>;
|
||||
restoreDeletedAnnotation(annotationType: string, annotationId: string): Promise<void>;
|
||||
purgeDeletedAnnotation(annotationType: string, annotationId: string): Promise<void>;
|
||||
}
|
||||
|
||||
Alpine.data("bookDetail", () => {
|
||||
@@ -194,6 +196,65 @@ Alpine.data("bookDetail", () => {
|
||||
}
|
||||
},
|
||||
|
||||
async restoreDeletedAnnotation(annotationType: string, annotationId: string) {
|
||||
const mediaId = getMediaId();
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`/api/media-items/${mediaId}/annotations/${annotationId}/restore`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: getAuthHeader(),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ annotation_type: annotationType }),
|
||||
},
|
||||
);
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.error || "Failed to restore annotation");
|
||||
}
|
||||
showToast("Annotation restored", "success");
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
} catch (e) {
|
||||
showToast(
|
||||
e instanceof Error ? e.message : "Failed to restore annotation",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async purgeDeletedAnnotation(annotationType: string, annotationId: string) {
|
||||
if (
|
||||
!confirm(
|
||||
"Permanently delete this annotation? This cannot be undone and it will not reappear on any device.",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const mediaId = getMediaId();
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`/api/media-items/${mediaId}/annotations/${annotationId}?annotation_type=${encodeURIComponent(annotationType)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: { Authorization: getAuthHeader() },
|
||||
},
|
||||
);
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.error || "Failed to delete annotation");
|
||||
}
|
||||
showToast("Annotation permanently deleted", "success");
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
} catch (e) {
|
||||
showToast(
|
||||
e instanceof Error ? e.message : "Failed to delete annotation",
|
||||
"error",
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
init() {
|
||||
const ratingAttr = document.body.getAttribute("data-rating");
|
||||
this.userRating = ratingAttr ? parseInt(ratingAttr, 10) || 0 : 0;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user