diff --git a/bindery/static/js/stories.js b/bindery/static/js/stories.js index 2c38eb7..cd488c3 100644 --- a/bindery/static/js/stories.js +++ b/bindery/static/js/stories.js @@ -106,7 +106,7 @@ function libraryImport(config) { // Upload a FanFicFare ePub. Bindery stores its chapters as if it had downloaded // them, so chapters that the site stubbed or removed stay in the book. -export function epubImport(storyId) { +export function epubImport(storyId, title = 'Import an ePub file') { let check = true; const file = h('input', { type: 'file', accept: '.epub,application/epub+zip', 'aria-label': 'Epub file' }); const upload = button( @@ -132,9 +132,22 @@ export function epubImport(storyId) { 'Chapters that Bindery has already keep their stored text.' : 'Starts tracking the story of an ePub made by FanFicFare, with all of its chapters. ' + 'If Bindery tracks the story already, it adds the chapters that it does not have.'; - return h( - 'div', - {}, + const card = h( + 'details', + { + class: 'card', + // A file dropped on the closed card opens it and puts the file in the picker, ready to import. + ondragover: (e) => e.preventDefault(), + ondrop: (e) => { + e.preventDefault(); + const dropped = e.dataTransfer?.files; + if (dropped && dropped.length) { + file.files = dropped; + card.open = true; + } + }, + }, + h('summary', {}, h('h2', {}, title)), h('p', { class: 'hint' }, hint, ' The next update adds the chapters that the site published after the ePub.'), h( 'div', @@ -144,6 +157,7 @@ export function epubImport(storyId) { h('label', {}, checkbox(true, (on) => (check = on)), ' Check the site for new chapters after import'), ), ); + return card; } function storyRow(s) { @@ -268,7 +282,7 @@ export function storiesPage(main, _params, config) { table, ), h('details', { class: 'card', open: true }, h('summary', {}, h('h2', {}, 'Jobs')), jobs.el), - h('details', { class: 'card' }, h('summary', {}, h('h2', {}, 'Import an epub file')), epubImport()), + epubImport(), h('details', { class: 'card' }, h('summary', {}, h('h2', {}, 'Import from the library')), libraryImport(config)), ); diff --git a/bindery/static/js/story.js b/bindery/static/js/story.js index cf1eb27..47d2c71 100644 --- a/bindery/static/js/story.js +++ b/bindery/static/js/story.js @@ -613,7 +613,7 @@ export function storyPage(main, { storyId }, config) { ), chapters, ), - h('details', { class: 'card' }, h('summary', {}, h('h2', {}, 'Import chapters from an epub')), epubImport(id)), + epubImport(id, 'Import chapters from an ePub'), h('details', { class: 'card', open: true }, h('summary', {}, h('h2', {}, 'Recent jobs')), jobs.el), h( 'details', diff --git a/tests/js/flows.test.mjs b/tests/js/flows.test.mjs index 664c262..ff145fb 100644 --- a/tests/js/flows.test.mjs +++ b/tests/js/flows.test.mjs @@ -144,6 +144,16 @@ for (const storyId of [undefined, 1]) { }); } +test('dropping a file on a closed epub import card opens it and fills the file field', () => { + const root = epubImport(1); + assert.equal(root.open, undefined); + const file = field(root, 'Epub file'); + const book = new File(['book'], 'story.epub', { type: 'application/epub+zip' }); + root.listeners.drop({ preventDefault() {}, dataTransfer: { files: [book] } }); + assert.equal(root.open, true); + assert.equal(file.files[0], book); +}); + test('library import sends only selected books and handles an empty candidate list', async () => { await mountStories(); api.on('GET', 'library/candidates', []);