mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(annotations): preserve source highlight timestamps
Use KOReader highlight times for annotation history while retaining ingestion times for diagnostics. Export all stored location representations and backfill existing rows during sync. Fixes #1300
This commit is contained in:
@@ -137,7 +137,7 @@ const metadataItems = computed(() => {
|
||||
if (props.annotation.chapterTitle) items.push(props.annotation.chapterTitle)
|
||||
if (props.annotation.pageno != null) items.push(t('annotations.listItem.pageNumber', { page: props.annotation.pageno }))
|
||||
if (props.annotation.chapterIndex != null) items.push(t('annotations.listItem.chapterNumber', { chapter: props.annotation.chapterIndex + 1 }))
|
||||
const date = formatDate(props.annotation.createdAt)
|
||||
const date = formatDate(props.annotation.highlightedAt)
|
||||
if (date) items.push(date)
|
||||
return items
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@ function makeAnnotation(overrides: Partial<AnnotationItem> = {}): AnnotationItem
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: 0,
|
||||
highlightedAt: '2026-01-01T00:00:00.000Z',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ const { t } = useI18n()
|
||||
|
||||
const STYLE_ICONS = { highlight: Highlighter, underline: Underline, strikethrough: Strikethrough, squiggly: Waves, invert: Contrast }
|
||||
|
||||
const created = computed(() => new Date(props.annotation.createdAt))
|
||||
const created = computed(() => new Date(props.annotation.highlightedAt))
|
||||
const dayLabel = computed(() => formatDate(created.value, { day: 'numeric', month: 'short' }))
|
||||
const yearLabel = computed(() => formatDate(created.value, { year: 'numeric' }))
|
||||
const hub = computed(() => ('bookTitle' in props.annotation ? props.annotation : null))
|
||||
|
||||
@@ -86,7 +86,7 @@ function isFirstOfDay(groupIndex: number, itemIndex: number): boolean {
|
||||
if (!group) return true
|
||||
const previous = group.items[itemIndex - 1]
|
||||
if (!previous) return true
|
||||
return dayKey(previous.createdAt) !== dayKey(group.items[itemIndex]!.createdAt)
|
||||
return dayKey(previous.highlightedAt) !== dayKey(group.items[itemIndex]!.highlightedAt)
|
||||
}
|
||||
|
||||
function isFirstOfChapter(groupIndex: number, itemIndex: number): boolean {
|
||||
|
||||
@@ -18,6 +18,7 @@ function annotation(overrides: Partial<AnnotationHubItem> = {}): AnnotationHubIt
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: 0,
|
||||
highlightedAt: '2026-07-12T00:00:00.000Z',
|
||||
createdAt: '2026-07-12T00:00:00.000Z',
|
||||
bookTitle: 'A Book',
|
||||
author: 'An Author',
|
||||
|
||||
@@ -17,6 +17,7 @@ function item(overrides: Partial<AnnotationHubItem> = {}): AnnotationHubItem {
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-08-24T10:00:00.000Z',
|
||||
createdAt: '2026-08-24T10:00:00.000Z',
|
||||
bookTitle: 'Tartufo',
|
||||
author: 'Kira Jane Buxton',
|
||||
@@ -46,11 +47,11 @@ describe('HUB_VIEWS', () => {
|
||||
|
||||
describe('hubGroupKey', () => {
|
||||
it('keys each mode off the field the server sorted by', () => {
|
||||
const row = item({ bookId: 42, color: '#38BDF8', origin: 'kobo', createdAt: '2026-03-04T00:00:00.000Z' })
|
||||
const row = item({ bookId: 42, color: '#38BDF8', origin: 'kobo', highlightedAt: '2026-03-04T00:00:00.000Z' })
|
||||
expect(hubGroupKey(row, 'book')).toBe('42')
|
||||
expect(hubGroupKey(row, 'color')).toBe('#38BDF8')
|
||||
expect(hubGroupKey(row, 'source')).toBe('kobo')
|
||||
expect(hubGroupKey(row, 'month')).toBe(monthKey(row.createdAt))
|
||||
expect(hubGroupKey(row, 'month')).toBe(monthKey(row.highlightedAt))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export function hubGroupKey(item: AnnotationHubItem, mode: AnnotationHubGroupMod
|
||||
case 'source':
|
||||
return item.origin
|
||||
default:
|
||||
return monthKey(item.createdAt)
|
||||
return monthKey(item.highlightedAt)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ const groups = computed<StreamGroup[]>(() =>
|
||||
}
|
||||
return {
|
||||
key: group.key,
|
||||
label: formatDate(new Date(lead.createdAt), { month: 'long', year: 'numeric' }),
|
||||
label: formatDate(new Date(lead.highlightedAt), { month: 'long', year: 'numeric' }),
|
||||
count: group.items.length,
|
||||
items: group.items,
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ function exportJson() {
|
||||
color: h.color,
|
||||
style: h.style,
|
||||
chapter: h.chapterTitle,
|
||||
highlightedAt: h.highlightedAt,
|
||||
createdAt: h.createdAt,
|
||||
})),
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ const groups = computed<StreamGroup[]>(() =>
|
||||
const first = group.items[0]
|
||||
return {
|
||||
key: group.key,
|
||||
label: first ? formatDate(new Date(first.createdAt), { day: 'numeric', month: 'long', year: 'numeric' }) : group.key,
|
||||
label: first ? formatDate(new Date(first.highlightedAt), { day: 'numeric', month: 'long', year: 'numeric' }) : group.key,
|
||||
count: group.total,
|
||||
items: group.items,
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ function makeHighlight(overrides: Partial<AnnotationItem> = {}): AnnotationItem
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-01-01T00:00:00.000Z',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ function item(overrides: Partial<AnnotationItem> = {}): AnnotationItem {
|
||||
origin: 'kobo',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-04-02T19:30:00.000Z',
|
||||
createdAt: '2026-04-02T19:30:00.000Z',
|
||||
...overrides,
|
||||
}
|
||||
@@ -157,21 +158,21 @@ describe('buildHighlightGroups by colour', () => {
|
||||
describe('buildHighlightGroups by day', () => {
|
||||
it('groups by the viewer time zone and puts the newest day first', () => {
|
||||
const groups = buildHighlightGroups(
|
||||
[item({ id: 1, createdAt: '2026-04-02T12:00:00.000Z' }), item({ id: 2, createdAt: '2026-04-05T12:00:00.000Z' })],
|
||||
[item({ id: 1, highlightedAt: '2026-04-02T12:00:00.000Z' }), item({ id: 2, highlightedAt: '2026-04-05T12:00:00.000Z' })],
|
||||
stats(),
|
||||
'day',
|
||||
)
|
||||
|
||||
expect(groups.map((g) => g.label)).toEqual([
|
||||
highlightDay(item({ createdAt: '2026-04-05T12:00:00.000Z' })),
|
||||
highlightDay(item({ createdAt: '2026-04-02T12:00:00.000Z' })),
|
||||
highlightDay(item({ highlightedAt: '2026-04-05T12:00:00.000Z' })),
|
||||
highlightDay(item({ highlightedAt: '2026-04-02T12:00:00.000Z' })),
|
||||
])
|
||||
expect(groups[0].total).toBe(1)
|
||||
})
|
||||
|
||||
it('puts highlights made on the same day in one group', () => {
|
||||
const groups = buildHighlightGroups(
|
||||
[item({ id: 1, createdAt: '2026-04-02T09:00:00.000Z' }), item({ id: 2, createdAt: '2026-04-02T11:00:00.000Z' })],
|
||||
[item({ id: 1, highlightedAt: '2026-04-02T09:00:00.000Z' }), item({ id: 2, highlightedAt: '2026-04-02T11:00:00.000Z' })],
|
||||
stats(),
|
||||
'day',
|
||||
)
|
||||
@@ -183,6 +184,6 @@ describe('buildHighlightGroups by day', () => {
|
||||
|
||||
describe('highlightDay', () => {
|
||||
it('returns an empty key for an unparseable timestamp', () => {
|
||||
expect(highlightDay(item({ createdAt: 'not a date' }))).toBe('')
|
||||
expect(highlightDay(item({ highlightedAt: 'not a date' }))).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface HighlightGroup {
|
||||
|
||||
/** The day an annotation was made, in the viewer's own time zone. */
|
||||
export function highlightDay(annotation: AnnotationItem): string {
|
||||
const date = new Date(annotation.createdAt)
|
||||
const date = new Date(annotation.highlightedAt)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
|
||||
@@ -161,6 +161,7 @@ describe('ReaderSidebar', () => {
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-02-14T12:00:00.000Z',
|
||||
createdAt: '2026-02-14T12:00:00.000Z',
|
||||
},
|
||||
{
|
||||
@@ -177,6 +178,7 @@ describe('ReaderSidebar', () => {
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-02-15T12:00:00.000Z',
|
||||
createdAt: '2026-02-15T12:00:00.000Z',
|
||||
},
|
||||
],
|
||||
@@ -237,6 +239,7 @@ describe('ReaderSidebar', () => {
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-02-13T12:00:00.000Z',
|
||||
createdAt: '2026-02-13T12:00:00.000Z',
|
||||
},
|
||||
{
|
||||
@@ -253,6 +256,7 @@ describe('ReaderSidebar', () => {
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-02-16T12:00:00.000Z',
|
||||
createdAt: '2026-02-16T12:00:00.000Z',
|
||||
},
|
||||
],
|
||||
@@ -348,6 +352,7 @@ describe('ReaderSidebar', () => {
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-02-11T12:00:00.000Z',
|
||||
createdAt: '2026-02-11T12:00:00.000Z',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -27,6 +27,7 @@ function makeAnnotation(id: number, cfi = `epubcfi(/6/${id})`): Annotation {
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-01-01T00:00:00.000Z',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ function makeAnnotation(overrides: Partial<Annotation> = {}): Annotation {
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-01-01T00:00:00.000Z',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
}
|
||||
|
||||
@@ -167,6 +167,7 @@ function makeAnnotation(id: number, overrides: Partial<AnnotationItem> = {}): An
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-01-01T00:00:00.000Z',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
pdf: { page: 2, rect: { x: 10, y: 20, width: 30, height: 8 }, rects: [{ x: 10, y: 20, width: 30, height: 8 }] },
|
||||
...overrides,
|
||||
|
||||
@@ -25,6 +25,7 @@ function makeAnnotation(overrides: Partial<AnnotationItem> = {}): AnnotationItem
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-01-01T00:00:00.000Z',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
pdf: { page: 1, rect: { x: 10, y: 20, width: 30, height: 8 }, rects: [{ x: 10, y: 20, width: 30, height: 8 }] },
|
||||
...overrides,
|
||||
|
||||
@@ -37,6 +37,7 @@ function makeAnnotation(id: number): AnnotationItem {
|
||||
origin: 'web',
|
||||
positionStatus: 'exact',
|
||||
chapterIndex: null,
|
||||
highlightedAt: '2026-01-01T00:00:00.000Z',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
pdf: PDF_POSITION,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user