diff --git a/frontend/src/stores/seriesManage.js b/frontend/src/stores/seriesManage.js index 3967b68..9f7e628 100644 --- a/frontend/src/stores/seriesManage.js +++ b/frontend/src/stores/seriesManage.js @@ -16,7 +16,10 @@ export const useSeriesManageStore = defineStore('seriesManage', () => { const tagId = ref(null) const series = ref(null) - const pages = ref([]) // [{image_id, page_number, thumbnail_url}] + const chapters = ref([]) // [{id, chapter_number, title, is_placeholder, stated_page_start/end, pages:[...]}] + const gaps = ref([]) // [{after_chapter_id, start, end}] + const pageCount = ref(0) + const targetChapterId = ref(null) // which chapter the picker adds into const picker = ref([]) // gallery scroll results const pickerCursor = ref(null) const pickerSelection = ref([]) // image ids @@ -28,7 +31,14 @@ export const useSeriesManageStore = defineStore('seriesManage', () => { try { const body = await api.get(`/api/series/${id}/pages`) series.value = body.series - pages.value = body.pages + chapters.value = body.chapters || [] + gaps.value = body.gaps || [] + pageCount.value = (body.pages || []).length + // Keep a valid add-target: the selected chapter, else the first one. + const ids = chapters.value.map(c => c.id) + if (!ids.includes(targetChapterId.value)) { + targetChapterId.value = ids.length ? ids[0] : null + } } finally { loading.value = false } @@ -38,6 +48,68 @@ export const useSeriesManageStore = defineStore('seriesManage', () => { if (tagId.value != null) await load(tagId.value) } + function gapAfter(chapterId) { + return gaps.value.find(g => g.after_chapter_id === chapterId) || null + } + + // ---- chapters ---- + async function createChapter({ title = null, isPlaceholder = false } = {}) { + await api.post(`/api/series/${tagId.value}/chapters`, { + body: { title, is_placeholder: isPlaceholder } + }) + await refresh() + } + + async function renameChapter(chapterId, title) { + await api.patch(`/api/series/${tagId.value}/chapters/${chapterId}`, { + body: { title } + }) + await refresh() + } + + async function setChapterStated(chapterId, start, end) { + await api.patch(`/api/series/${tagId.value}/chapters/${chapterId}`, { + body: { stated_page_start: start, stated_page_end: end } + }) + await refresh() + } + + async function reorderChapters(orderedChapterIds) { + await api.post(`/api/series/${tagId.value}/chapters/reorder`, { + body: { chapter_ids: orderedChapterIds } + }) + await refresh() + } + + async function moveChapter(chapterId, dir) { + const ids = chapters.value.map(c => c.id) + const from = ids.indexOf(chapterId) + const to = from + dir + if (from === -1 || to < 0 || to >= ids.length) return + await reorderChapters(moveItem(ids, from, to)) + } + + async function deleteChapter(chapterId) { + await api.delete(`/api/series/${tagId.value}/chapters/${chapterId}`) + await refresh() + } + + async function mergeChapter(sourceId, targetId) { + await api.post(`/api/series/${tagId.value}/chapters/${sourceId}/merge`, { + body: { target_chapter_id: targetId } + }) + await refresh() + toast({ text: 'Chapters merged', type: 'success' }) + } + + async function reorderPages(chapterId, orderedImageIds) { + await api.post(`/api/series/${tagId.value}/chapters/${chapterId}/reorder`, { + body: { image_ids: orderedImageIds } + }) + await refresh() + } + + // ---- picker / pages ---- async function loadPicker(reset = false) { if (reset) { picker.value = []; pickerCursor.value = null } const params = { limit: 50 } @@ -55,12 +127,13 @@ export const useSeriesManageStore = defineStore('seriesManage', () => { async function addSelected() { if (pickerSelection.value.length === 0) return + if (targetChapterId.value == null) await createChapter() await api.post(`/api/series/${tagId.value}/pages`, { - body: { image_ids: pickerSelection.value } + body: { image_ids: pickerSelection.value, chapter_id: targetChapterId.value } }) pickerSelection.value = [] await refresh() - toast({ text: 'Added to series', type: 'success' }) + toast({ text: 'Added to chapter', type: 'success' }) } async function remove(imageId) { @@ -70,13 +143,6 @@ export const useSeriesManageStore = defineStore('seriesManage', () => { await refresh() } - async function reorder(orderedImageIds) { - await api.post(`/api/series/${tagId.value}/reorder`, { - body: { image_ids: orderedImageIds } - }) - await refresh() - } - async function setCover(imageId) { await api.post(`/api/series/${tagId.value}/cover`, { body: { image_id: imageId } @@ -85,8 +151,11 @@ export const useSeriesManageStore = defineStore('seriesManage', () => { } return { - tagId, series, pages, picker, pickerCursor, pickerSelection, loading, - load, refresh, loadPicker, togglePick, addSelected, remove, - reorder, setCover + tagId, series, chapters, gaps, pageCount, targetChapterId, + picker, pickerCursor, pickerSelection, loading, + load, refresh, gapAfter, + createChapter, renameChapter, setChapterStated, reorderChapters, + moveChapter, deleteChapter, mergeChapter, reorderPages, + loadPicker, togglePick, addSelected, remove, setCover } }) diff --git a/frontend/src/stores/seriesReader.js b/frontend/src/stores/seriesReader.js index beac66d..86f2e98 100644 --- a/frontend/src/stores/seriesReader.js +++ b/frontend/src/stores/seriesReader.js @@ -42,7 +42,27 @@ export const useSeriesReaderStore = defineStore('seriesReader', () => { await run(async () => { const body = await api.get(`/api/series/${tagId}/pages`) series.value = body.series - pages.value = body.pages + // page_number is now WITHIN a chapter, so it can't anchor scroll/jump + // (two chapters both have a page 1). Decorate each page with a global + // `seq` (reading-order position) for anchors, plus chapter-divider info + // so the reader can mark where each chapter begins. + const chapters = body.chapters || [] + const labelById = {} + for (const c of chapters) { + labelById[c.id] = c.title || `Chapter ${c.chapter_number}` + } + let prevChapter = null + pages.value = (body.pages || []).map((p, i) => { + const isChapterStart = + chapters.length > 1 && p.chapter_id !== prevChapter + prevChapter = p.chapter_id + return { + ...p, + seq: i + 1, + isChapterStart, + chapterLabel: labelById[p.chapter_id] || null + } + }) }) } diff --git a/frontend/src/views/SeriesManageView.vue b/frontend/src/views/SeriesManageView.vue index d12ddcc..52feba1 100644 --- a/frontend/src/views/SeriesManageView.vue +++ b/frontend/src/views/SeriesManageView.vue @@ -2,9 +2,11 @@
{{ store.series?.name || 'Series' }} - {{ store.pages.length }} page(s) + + {{ store.chapters.length }} chapter(s) · {{ store.pageCount }} page(s) +
-
-
- {{ p.page_number }} - -
- - + +
+ + +
+ No chapters yet — add one, then add images from the right.
-
- No pages yet — add images from the right. + +
+ Add chapter + + Add placeholder +
+
{{ store.pickerSelection.length }} selected @@ -42,7 +139,7 @@ size="small" color="accent" variant="flat" :disabled="store.pickerSelection.length === 0" @click="store.addSelected()" - >Add to series + >Add to {{ targetLabel }}