mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(stats): save reading history share images
Wait for the canvas export callback before starting the download so the browser can complete the PNG export. Closes #1967
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import type { ReadingHistoryItem } from '@bookorbit/types'
|
import type { ReadingHistoryItem } from '@bookorbit/types'
|
||||||
import { filtersFromQuery, queryFromFilters } from '../composables/useReadingHistoryFilters'
|
import { filtersFromQuery, queryFromFilters } from '../composables/useReadingHistoryFilters'
|
||||||
import { exportFileName, toCsv, toMarkdown } from '../lib/history-export'
|
import { canvasToBlob, exportFileName, toCsv, toMarkdown } from '../lib/history-export'
|
||||||
import { readSpanDays } from '../lib/history-format'
|
import { readSpanDays } from '../lib/history-format'
|
||||||
import { historyRouteForChartClick } from '../lib/chart-month-link'
|
import { historyRouteForChartClick } from '../lib/chart-month-link'
|
||||||
import { fitTiles, withAlpha } from '../lib/share-poster'
|
import { fitTiles, withAlpha } from '../lib/share-poster'
|
||||||
@@ -117,6 +117,24 @@ describe('reading history export', () => {
|
|||||||
expect(exportFileName('Q3 2026', 'md')).toBe('reading-history-q3-2026.md')
|
expect(exportFileName('Q3 2026', 'md')).toBe('reading-history-q3-2026.md')
|
||||||
expect(exportFileName('All time', 'csv')).toBe('reading-history-all-time.csv')
|
expect(exportFileName('All time', 'csv')).toBe('reading-history-all-time.csv')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('waits for the canvas export callback when toBlob returns undefined', async () => {
|
||||||
|
const blob = new Blob(['poster'], { type: 'image/png' })
|
||||||
|
const canvas = {
|
||||||
|
toBlob(callback: BlobCallback) {
|
||||||
|
queueMicrotask(() => callback(blob))
|
||||||
|
},
|
||||||
|
} as HTMLCanvasElement
|
||||||
|
|
||||||
|
await expect(canvasToBlob(canvas, 'image/png')).resolves.toBe(blob)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects when the canvas is unavailable or produces no data', async () => {
|
||||||
|
const canvas = { toBlob: (callback: BlobCallback) => callback(null) } as HTMLCanvasElement
|
||||||
|
|
||||||
|
await expect(canvasToBlob(null, 'image/png')).rejects.toThrow('Canvas is unavailable')
|
||||||
|
await expect(canvasToBlob(canvas, 'image/png')).rejects.toThrow('Canvas export returned no data')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('reading history helpers', () => {
|
describe('reading history helpers', () => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useAuth } from '@/features/auth/composables/useAuth'
|
|||||||
import { useCoverVersions } from '@/features/book/composables/useCoverVersions'
|
import { useCoverVersions } from '@/features/book/composables/useCoverVersions'
|
||||||
import { fetchReadingHistoryPage, fetchReadingHistorySummary } from '../api/reading-history.api'
|
import { fetchReadingHistoryPage, fetchReadingHistorySummary } from '../api/reading-history.api'
|
||||||
import { formatHours, formatMonthName } from '../lib/history-format'
|
import { formatHours, formatMonthName } from '../lib/history-format'
|
||||||
import { saveBlob } from '../lib/history-export'
|
import { canvasToBlob, saveBlob } from '../lib/history-export'
|
||||||
import { POSTER_SIZES, drawPoster, type PosterLook, type PosterShape, type PosterTile } from '../lib/share-poster'
|
import { POSTER_SIZES, drawPoster, type PosterLook, type PosterShape, type PosterTile } from '../lib/share-poster'
|
||||||
|
|
||||||
type SharePeriod = 'year' | 'quarter' | 'month'
|
type SharePeriod = 'year' | 'quarter' | 'month'
|
||||||
@@ -240,10 +240,7 @@ watch(books, (list) => {
|
|||||||
watch([books, shape, look, showTitles, showRereads, showRatings, imageVersion, canvas, loading], () => void nextTick(scheduleDraw))
|
watch([books, shape, look, showTitles, showRereads, showRatings, imageVersion, canvas, loading], () => void nextTick(scheduleDraw))
|
||||||
|
|
||||||
function posterBlob(): Promise<Blob> {
|
function posterBlob(): Promise<Blob> {
|
||||||
return new Promise(
|
return canvasToBlob(canvas.value, 'image/png')
|
||||||
(resolve, reject) =>
|
|
||||||
canvas.value?.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('empty'))), 'image/png') ?? reject(new Error('no canvas')),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileName() {
|
function fileName() {
|
||||||
|
|||||||
@@ -123,3 +123,11 @@ export function saveBlob(blob: Blob, fileName: string) {
|
|||||||
link.remove()
|
link.remove()
|
||||||
setTimeout(() => URL.revokeObjectURL(url), 30_000)
|
setTimeout(() => URL.revokeObjectURL(url), 30_000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canvasToBlob(canvas: HTMLCanvasElement | null, type: string): Promise<Blob> {
|
||||||
|
if (!canvas) return Promise.reject(new Error('Canvas is unavailable'))
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error('Canvas export returned no data'))), type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user