diff --git a/po/POTFILES b/po/POTFILES index 541292fd..a4308af8 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -18,6 +18,7 @@ src/ui/book-viewer.ui src/ui/bookmark-row.ui src/ui/export-dialog.ui src/ui/image-viewer.ui +src/ui/import-dialog.ui src/ui/library.ui src/ui/library-view.ui src/ui/navbar.ui diff --git a/src/annotations.js b/src/annotations.js index c3350ec5..ff0ab8d5 100644 --- a/src/annotations.js +++ b/src/annotations.js @@ -501,19 +501,83 @@ export const AnnotationPopover = GObject.registerClass({ } }) +const ImportDialog = GObject.registerClass({ + GTypeName: 'FoliateImportDialog', + Template: pkg.moduleuri('ui/import-dialog.ui'), + Children: ['annotation-view'], + InternalChildren: ['cancel-button', 'ok-button', 'banner'], + Properties: utils.makeParams({ + 'identifier-mismatch': 'boolean', + }), + Signals: { + 'response': {}, + }, +}, class extends Adw.Window { + constructor(params) { + super(params) + const respond = () => { + this.emit('response') + this.close() + } + this._ok_button.connect('clicked', respond) + this._banner.connect('button-clicked', respond) + this._cancel_button.connect('clicked', () => this.close()) + this.add_controller(utils.addShortcuts({ 'Escape|w': () => this.close() })) + } + close() { + super.close() + this.run_dispose() + } +}) + +export const importAnnotations = (window, data) => { + const dialog = new Gtk.FileDialog() + const filter = new Gtk.FileFilter({ + name: _('JSON Files'), + mime_types: ['application/json'], + }) + dialog.filters = new Gio.ListStore() + dialog.filters.append(new Gtk.FileFilter({ + name: _('All Files'), + patterns: ['*'], + })) + dialog.filters.append(filter) + dialog.default_filter = filter + dialog.open(window, null, (__, res) => { + try { + const file = dialog.open_finish(res) + const json = utils.readJSONFile(file) + if (!json.annotations?.length) return window.error(_('No Annotations'), + _('The imported file has no annotations')) + const importDialog = new ImportDialog({ + identifier_mismatch: !(json.metadata?.identifier === data.key), + transient_for: window, + }) + const model = new Gio.ListStore() + importDialog.annotation_view.setupModel(model) + importDialog.show() + const annotations = json.annotations.map(item => { + const annotation = new Annotation(item) + model.append(annotation) + return annotation + }) + importDialog.connect('response', () => data.addAnnotations(annotations)) + } catch (e) { + if (e instanceof Gtk.DialogError) console.debug(e) + else { + console.error(e) + window.error(_('Cannot Import Annotations'), + _('An error occurred')) + } + } + }) +} + export const exportAnnotations = (window, data) => { const n = data.annotations?.length - if (!(n > 0)) { - const dialog = new Adw.MessageDialog({ - heading: _('No Annotations'), - body: _('You don’t have any annotations for this book'), - modal: true, - transient_for: window, - }) - dialog.add_response('ok', _('OK')) - dialog.present() - return - } + if (!(n > 0)) return window.error(_('No Annotations'), + _('You don’t have any annotations for this book')) + const path = pkg.modulepath('ui/export-dialog.ui') const builder = pkg.useResource ? Gtk.Builder.new_from_resource(path) @@ -599,4 +663,4 @@ ${text} ${note} `)} `, -} \ No newline at end of file +} diff --git a/src/app.js b/src/app.js index 8b65fa43..ae11394a 100644 --- a/src/app.js +++ b/src/app.js @@ -90,6 +90,15 @@ const ApplicationWindow = GObject.registerClass({ add_toast(toast) { this.content.add_toast(toast) } + error(heading, body) { + const dialog = new Adw.MessageDialog({ + heading, body, + modal: true, + transient_for: this, + }) + dialog.add_response('close', _('Close')) + dialog.present() + } openFile(file) { this.file = file if (!this.#bookViewer) { @@ -274,6 +283,9 @@ export const Application = GObject.registerClass({ .card-sidebar, .card-sidebar row.activatable { background-color: transparent; } + .card-sidebar.flat-list .card { + padding: 12px; + } .book-image-frame { box-shadow: 0 6px 12px rgba(0, 0, 0, .15); diff --git a/src/book-viewer.js b/src/book-viewer.js index 143d868b..c2cac571 100644 --- a/src/book-viewer.js +++ b/src/book-viewer.js @@ -17,7 +17,8 @@ import './toc.js' import './search.js' import './navbar.js' import { - AnnotationPopover, AnnotationModel, BookmarkModel, exportAnnotations, + AnnotationPopover, AnnotationModel, BookmarkModel, + importAnnotations, exportAnnotations, } from './annotations.js' import { SelectionPopover } from './selection-tools.js' import { ImageViewer } from './image-viewer.js' @@ -76,21 +77,25 @@ class BookData { const annotations = init ? this.storage.get('annotations', []) : this.annotations.export() - for (const annotation of annotations) this.addAnnotation(annotation, init) + await this.addAnnotations(annotations, false) return this } - async addAnnotation(annotation, init) { + async addAnnotation(annotation, save = true) { try { const [view, ...views] = this.views const { index, label } = await view.addAnnotation(annotation) this.annotations.add(annotation, index, label) for (const view of views) view.addAnnotation(annotation) - if (!init) this.#saveAnnotations() + if (save) this.#saveAnnotations() return annotation } catch (e) { console.error(e) } } + async addAnnotations(annotations, save = true) { + await Promise.all(annotations.map(x => this.addAnnotation(x, false))) + if (save) this.#saveAnnotations() + } async deleteAnnotation(annotation) { try { const [view, ...views] = this.views @@ -761,7 +766,7 @@ export const BookViewer = GObject.registerClass({ 'toggle-sidebar', 'toggle-search', 'show-location', 'toggle-toc', 'toggle-annotations', 'toggle-bookmarks', 'preferences', 'show-info', 'bookmark', - 'export-annotations', + 'export-annotations', 'import-annotations', ], props: ['fold-sidebar'], }) @@ -1081,7 +1086,10 @@ export const BookViewer = GObject.registerClass({ this._bookmark_view.toggle() } exportAnnotations() { - exportAnnotations(this.get_root(), this.#data.storage.export()) + exportAnnotations(this.root, this.#data.storage.export()) + } + importAnnotations() { + importAnnotations(this.root, this.#data) } vfunc_unroot() { this._navbar.tts_box.kill() diff --git a/src/gresource.xml b/src/gresource.xml index f92afbec..45f8e5bc 100644 --- a/src/gresource.xml +++ b/src/gresource.xml @@ -26,6 +26,7 @@ ui/book-viewer.ui ui/export-dialog.ui ui/image-viewer.ui + ui/import-dialog.ui ui/library.ui ui/library-view.ui ui/media-overlay-box.ui diff --git a/src/ui/import-dialog.ui b/src/ui/import-dialog.ui new file mode 100644 index 00000000..d486d787 --- /dev/null +++ b/src/ui/import-dialog.ui @@ -0,0 +1,52 @@ + + + +