@fiduswriter/image-manager 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +5 -0
  3. package/dist/copyright_dialog/index.d.ts +17 -0
  4. package/dist/copyright_dialog/index.d.ts.map +1 -0
  5. package/dist/copyright_dialog/index.js +148 -0
  6. package/dist/copyright_dialog/index.js.map +1 -0
  7. package/dist/copyright_dialog/templates.d.ts +13 -0
  8. package/dist/copyright_dialog/templates.d.ts.map +1 -0
  9. package/dist/copyright_dialog/templates.js +48 -0
  10. package/dist/copyright_dialog/templates.js.map +1 -0
  11. package/dist/database.d.ts +9 -0
  12. package/dist/database.d.ts.map +1 -0
  13. package/dist/database.js +51 -0
  14. package/dist/database.js.map +1 -0
  15. package/dist/edit_dialog/index.d.ts +24 -0
  16. package/dist/edit_dialog/index.d.ts.map +1 -0
  17. package/dist/edit_dialog/index.js +186 -0
  18. package/dist/edit_dialog/index.js.map +1 -0
  19. package/dist/edit_dialog/model.d.ts +20 -0
  20. package/dist/edit_dialog/model.d.ts.map +1 -0
  21. package/dist/edit_dialog/model.js +153 -0
  22. package/dist/edit_dialog/model.js.map +1 -0
  23. package/dist/edit_dialog/templates.d.ts +5 -0
  24. package/dist/edit_dialog/templates.d.ts.map +1 -0
  25. package/dist/edit_dialog/templates.js +33 -0
  26. package/dist/edit_dialog/templates.js.map +1 -0
  27. package/dist/index.d.ts +5 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +5 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/overview/categories.d.ts +8 -0
  32. package/dist/overview/categories.d.ts.map +1 -0
  33. package/dist/overview/categories.js +94 -0
  34. package/dist/overview/categories.js.map +1 -0
  35. package/dist/overview/index.d.ts +39 -0
  36. package/dist/overview/index.d.ts.map +1 -0
  37. package/dist/overview/index.js +358 -0
  38. package/dist/overview/index.js.map +1 -0
  39. package/dist/overview/menu.d.ts +48 -0
  40. package/dist/overview/menu.d.ts.map +1 -0
  41. package/dist/overview/menu.js +67 -0
  42. package/dist/overview/menu.js.map +1 -0
  43. package/dist/overview/templates.d.ts +4 -0
  44. package/dist/overview/templates.d.ts.map +1 -0
  45. package/dist/overview/templates.js +24 -0
  46. package/dist/overview/templates.js.map +1 -0
  47. package/dist/selection_dialog/index.d.ts +19 -0
  48. package/dist/selection_dialog/index.d.ts.map +1 -0
  49. package/dist/selection_dialog/index.js +149 -0
  50. package/dist/selection_dialog/index.js.map +1 -0
  51. package/package.json +53 -0
  52. package/src/copyright_dialog/index.js +179 -0
  53. package/src/copyright_dialog/templates.js +56 -0
  54. package/src/database.js +57 -0
  55. package/src/edit_dialog/index.js +222 -0
  56. package/src/edit_dialog/model.js +184 -0
  57. package/src/edit_dialog/templates.js +41 -0
  58. package/src/index.js +4 -0
  59. package/src/overview/categories.js +115 -0
  60. package/src/overview/index.js +435 -0
  61. package/src/overview/menu.js +75 -0
  62. package/src/overview/templates.js +30 -0
  63. package/src/selection_dialog/index.js +176 -0
@@ -0,0 +1,222 @@
1
+ import {CheckableList, ContentMenu, Dialog, addAlert, gettext} from "fwtoolkit"
2
+ import {E2EEEncryptor} from "fwtoolkit/e2ee/encryptor"
3
+ import {imageEditModel} from "./model"
4
+ import {imageEditTemplate} from "./templates"
5
+ export class ImageEditDialog {
6
+ constructor(imageDB, imageId = false, page) {
7
+ this.imageDB = imageDB
8
+ this.page = page
9
+ this.imageId = imageId
10
+ this.dialog = false
11
+ this.copyright = this.imageId
12
+ ? this.imageDB.db[this.imageId].copyright
13
+ : {
14
+ holder: false,
15
+ year: false,
16
+ freeToRead: true,
17
+ licenses: []
18
+ }
19
+ this.menu = this.page.menu?.imageEditModel || imageEditModel()
20
+ }
21
+
22
+ //open a dialog for uploading an image
23
+ init() {
24
+ if (this.page.app.isOffline()) {
25
+ this.showOffline()
26
+ return Promise.resolve()
27
+ }
28
+ const returnPromise = new Promise(resolve => {
29
+ this.dialog = new Dialog({
30
+ title: this.imageId
31
+ ? gettext("Update Image Information")
32
+ : gettext("Upload Image"),
33
+ id: "editimage",
34
+ classes: "fw-media-uploader",
35
+ body: imageEditTemplate({
36
+ image: this.imageId ? this.imageDB.db[this.imageId] : false,
37
+ cats: this.imageDB.cats
38
+ }),
39
+ buttons: [
40
+ {
41
+ text: this.imageId
42
+ ? gettext("Update")
43
+ : gettext("Upload"),
44
+ click: () => resolve(this.saveImage()),
45
+ classes: "fw-dark"
46
+ },
47
+ {
48
+ type: "cancel",
49
+ classes: "fw-orange",
50
+ click: () => this.dialog.close()
51
+ }
52
+ ]
53
+ })
54
+ this.dialog.open()
55
+ })
56
+
57
+ const image = this.imageId ? this.imageDB.db[this.imageId] : false
58
+ const catsEl = document.getElementById("image-edit-categories")
59
+ if (catsEl) {
60
+ this.catsList = new CheckableList({
61
+ dom: catsEl,
62
+ options: this.imageDB.cats.map(cat => ({
63
+ id: cat.id,
64
+ label: cat.category_title
65
+ })),
66
+ initialValue: image ? image.cats : [],
67
+ multiple: true
68
+ })
69
+ } else {
70
+ this.catsList = {value: []}
71
+ }
72
+
73
+ if (!this.imageId) {
74
+ this.bindMediaUploadEvents()
75
+ }
76
+
77
+ document
78
+ .querySelector(".figure-edit-menu")
79
+ .addEventListener("click", event => {
80
+ event.preventDefault()
81
+ event.stopImmediatePropagation()
82
+
83
+ const contentMenu = new ContentMenu({
84
+ menu: this.menu,
85
+ width: 220,
86
+ page: this,
87
+ menuPos: {X: event.pageX - 50, Y: event.pageY + 50}
88
+ })
89
+ contentMenu.open()
90
+ })
91
+
92
+ return returnPromise
93
+ }
94
+
95
+ //add image upload events
96
+ bindMediaUploadEvents() {
97
+ const selectButton = document.querySelector(
98
+ "#editimage .fw-media-select-button"
99
+ ),
100
+ mediaInputSelector = document.querySelector(
101
+ "#editimage .fw-media-file-input"
102
+ )
103
+ this.mediaPreviewerDiv = document.querySelector(
104
+ "#editimage .figure-preview > div"
105
+ )
106
+ this.rotation = 0
107
+ this.cropped = false
108
+
109
+ selectButton.addEventListener("click", () => {
110
+ mediaInputSelector.click()
111
+ })
112
+
113
+ mediaInputSelector.addEventListener("change", () => {
114
+ this.mediaInput = mediaInputSelector.files[0]
115
+ const fr = new window.FileReader()
116
+ fr.onload = () => {
117
+ this.mediaPreviewerDiv.innerHTML = `<div class="img" style="background-image: url(${fr.result});" />`
118
+ this.mediaPreviewer =
119
+ this.mediaPreviewerDiv.querySelector(".img")
120
+ this.mediaPreviewerDiv.classList.remove("crop-mode")
121
+ this.dialog.centerDialog()
122
+ }
123
+ fr.readAsDataURL(this.mediaInput)
124
+ })
125
+ }
126
+
127
+ displayCreateImageError(errors) {
128
+ Object.keys(errors).forEach(eKey => {
129
+ const eMsg = `<div class="fw-warning">${errors[eKey]}</div>`
130
+ if ("error" == eKey) {
131
+ document
132
+ .getElementById("editimage")
133
+ .insertAdjacentHTML("afterbegin", eMsg)
134
+ } else {
135
+ document
136
+ .getElementById(`id_${eKey}`)
137
+ .insertAdjacentHTML("afterend", eMsg)
138
+ }
139
+ })
140
+ }
141
+
142
+ async saveImage() {
143
+ const imageData = {
144
+ title: document.querySelector("#editimage .fw-media-title").value,
145
+ copyright: this.copyright,
146
+ cats: this.catsList.value
147
+ }
148
+ if (this.imageId) {
149
+ imageData.id = this.imageId
150
+ } else if (!this.rotation && !this.cropped) {
151
+ imageData.image = this.mediaInput
152
+ } else {
153
+ const mediaPreviewerStyle =
154
+ this.mediaPreviewer.currentStyle ||
155
+ window.getComputedStyle(this.mediaPreviewer, false)
156
+ const base64data = mediaPreviewerStyle.backgroundImage
157
+ .slice(4, -1)
158
+ .replace(/"/g, "")
159
+ const bstr = atob(base64data.split(",")[1])
160
+ let n = bstr.length
161
+ const u8arr = new Uint8Array(n)
162
+ while (n--) {
163
+ u8arr[n] = bstr.charCodeAt(n)
164
+ }
165
+ imageData.image = new File([u8arr], this.mediaInput.name, {
166
+ type: this.mediaInput.type
167
+ })
168
+ }
169
+
170
+ // For E2EE documents, encrypt the image and copyright before uploading
171
+ const isE2EE = this.page.e2ee?.encrypted === true
172
+ if (isE2EE && imageData.image) {
173
+ imageData.image = await E2EEEncryptor.encryptImage(
174
+ imageData.image,
175
+ this.page.e2ee.key
176
+ )
177
+ imageData.original_file_type = this.mediaInput?.type || "image/png"
178
+ // Encrypt copyright metadata so the server cannot read it
179
+ imageData.copyright = await E2EEEncryptor.encryptObject(
180
+ imageData.copyright,
181
+ this.page.e2ee.key
182
+ )
183
+ }
184
+
185
+ // Remove old warning messages
186
+ document
187
+ .querySelectorAll("#editimage .fw-warning")
188
+ .forEach(el => el.parentElement.removeChild(el))
189
+ return new Promise(resolve => {
190
+ this.imageDB.saveImage(imageData).then(
191
+ imageId => {
192
+ this.dialog.close()
193
+ addAlert("success", gettext("The image has been updated."))
194
+ this.imageId = imageId
195
+ resolve(imageId)
196
+ },
197
+ errors => {
198
+ if (this.page.app.isOffline()) {
199
+ this.showOffline()
200
+ return
201
+ }
202
+ this.displayCreateImageError(errors)
203
+ addAlert(
204
+ "error",
205
+ gettext(
206
+ "Some errors were found. Please examine the form."
207
+ )
208
+ )
209
+ }
210
+ )
211
+ })
212
+ }
213
+
214
+ showOffline() {
215
+ addAlert(
216
+ "info",
217
+ gettext(
218
+ "You are currently offline. Please try again after going online."
219
+ )
220
+ )
221
+ }
222
+ }
@@ -0,0 +1,184 @@
1
+ import Cropper from "cropperjs"
2
+
3
+ import {gettext} from "fwtoolkit"
4
+ import {CopyrightDialog} from "../../copyright_dialog"
5
+
6
+ let mediaPreviewerImg = false
7
+
8
+ export const imageEditModel = () => ({
9
+ content: [
10
+ {
11
+ title: gettext("Rotate Left"),
12
+ type: "action",
13
+ tooltip: gettext("Rotate-left"),
14
+ order: 0,
15
+ action: dialog => {
16
+ const mediaPreviewerStyle =
17
+ dialog.mediaPreviewer.currentStyle ||
18
+ window.getComputedStyle(dialog.mediaPreviewer, false)
19
+ rotateBase64Image(
20
+ mediaPreviewerStyle.backgroundImage
21
+ .slice(4, -1)
22
+ .replace(/"/g, ""),
23
+ dialog.mediaInput.type,
24
+ "left"
25
+ ).then(response =>
26
+ dialog.mediaPreviewer.setAttribute(
27
+ "style",
28
+ `background-image: url(${response});`
29
+ )
30
+ )
31
+ if (dialog.rotation === 0) {
32
+ dialog.rotation = 270
33
+ } else {
34
+ dialog.rotation -= 90
35
+ }
36
+ },
37
+ disabled: dialog => dialog.imageId,
38
+ icon: "redo fa-rotate-180"
39
+ },
40
+ {
41
+ title: gettext("Rotate Right"),
42
+ type: "action",
43
+ tooltip: gettext("Rotate-right"),
44
+ order: 1,
45
+ action: dialog => {
46
+ const mediaPreviewerStyle =
47
+ dialog.mediaPreviewer.currentStyle ||
48
+ window.getComputedStyle(dialog.mediaPreviewer, false)
49
+ rotateBase64Image(
50
+ mediaPreviewerStyle.backgroundImage
51
+ .slice(4, -1)
52
+ .replace(/"/g, ""),
53
+ dialog.mediaInput.type,
54
+ "right"
55
+ ).then(response =>
56
+ dialog.mediaPreviewer.setAttribute(
57
+ "style",
58
+ `background-image: url(${response});`
59
+ )
60
+ )
61
+ if (dialog.rotation === 270) {
62
+ dialog.rotation = 0
63
+ } else {
64
+ dialog.rotation += 90
65
+ }
66
+ },
67
+ disabled: dialog => dialog.imageId,
68
+ icon: "undo"
69
+ },
70
+ {
71
+ title: gettext("Crop"),
72
+ type: "action",
73
+ tooltip: gettext("Crop image"),
74
+ order: 2,
75
+ action: dialog => {
76
+ const mediaPreviewerStyle =
77
+ dialog.mediaPreviewer.currentStyle ||
78
+ window.getComputedStyle(dialog.mediaPreviewer, false)
79
+ //const base64data = mediaPreviewerStyle.backgroundImage.slice(4, -1).replace(/"/g, "")
80
+ mediaPreviewerImg = document.createElement("img")
81
+ //img.src = `url(${base64data})`
82
+ mediaPreviewerImg.src = mediaPreviewerStyle.backgroundImage
83
+ .slice(4, -1)
84
+ .replace(/"/g, "")
85
+ dialog.mediaPreviewer.parentElement.replaceChild(
86
+ mediaPreviewerImg,
87
+ dialog.mediaPreviewer
88
+ )
89
+ const cropper = new Cropper(mediaPreviewerImg, {
90
+ viewMode: 1,
91
+ responsive: true
92
+ })
93
+ toggleCropMode(true, dialog, cropper)
94
+ },
95
+ disabled: dialog => dialog.imageId,
96
+ icon: "crop"
97
+ },
98
+ {
99
+ title: gettext("Set Copyright"),
100
+ type: "action",
101
+ tooltip: gettext("Specify copyright information"),
102
+ order: 3,
103
+ action: dialog => {
104
+ const crDialog = new CopyrightDialog(dialog.copyright)
105
+ crDialog.init().then(copyright => {
106
+ if (copyright) {
107
+ dialog.copyright = copyright
108
+ }
109
+ })
110
+ }
111
+ }
112
+ ]
113
+ })
114
+
115
+ let oldButtons = false
116
+
117
+ const toggleCropMode = (val, dialog, cropper) => {
118
+ if (val && !oldButtons) {
119
+ dialog.mediaPreviewerDiv.classList.add("crop-mode")
120
+ oldButtons = dialog.dialog.buttons
121
+ dialog.dialog.setButtons([
122
+ {
123
+ text: gettext("Crop"),
124
+ click: () => {
125
+ dialog.mediaPreviewer.setAttribute(
126
+ "style",
127
+ `background-image: url(${cropper
128
+ .getCroppedCanvas()
129
+ .toDataURL(dialog.mediaInput.type)});`
130
+ )
131
+ dialog.cropped = true
132
+ cropper.destroy()
133
+ toggleCropMode(false, dialog, cropper)
134
+ },
135
+ classes: "fw-dark"
136
+ },
137
+ {
138
+ type: "cancel",
139
+ classes: "fw-orange",
140
+ click: () => {
141
+ cropper.destroy()
142
+ toggleCropMode(false, dialog, cropper)
143
+ }
144
+ }
145
+ ])
146
+ } else {
147
+ dialog.mediaPreviewerDiv.classList.remove("crop-mode")
148
+ if (mediaPreviewerImg) {
149
+ mediaPreviewerImg.parentElement.replaceChild(
150
+ dialog.mediaPreviewer,
151
+ mediaPreviewerImg
152
+ )
153
+ mediaPreviewerImg = false
154
+ }
155
+ if (oldButtons) {
156
+ dialog.dialog.buttons = oldButtons
157
+ oldButtons = false
158
+ }
159
+ }
160
+ dialog.dialog.refreshButtons()
161
+ dialog.dialog.centerDialog()
162
+ }
163
+
164
+ const rotateBase64Image = (base64data, type, direction) => {
165
+ return new Promise(resolve => {
166
+ const canvas = document.createElement("canvas")
167
+ const ctx = canvas.getContext("2d")
168
+ const image = new Image()
169
+ image.src = base64data
170
+ image.onload = () => {
171
+ canvas.height = image.width
172
+ canvas.width = image.height
173
+ if (direction == "left") {
174
+ ctx.rotate((90 * Math.PI) / 180)
175
+ ctx.translate(0, -canvas.width)
176
+ } else {
177
+ ctx.rotate((-90 * Math.PI) / 180)
178
+ ctx.translate(-canvas.height, 0)
179
+ }
180
+ ctx.drawImage(image, 0, 0)
181
+ resolve(canvas.toDataURL(type))
182
+ }
183
+ })
184
+ }
@@ -0,0 +1,41 @@
1
+ import {escapeText, gettext} from "fwtoolkit"
2
+
3
+ /* A template for the image category selection of the image selection dialog. */
4
+ const imageEditCategoryTemplate = ({cats}) => {
5
+ if (!cats.length) {
6
+ return ""
7
+ }
8
+ return `<div class="fw-media-category">
9
+ <div>${gettext("Select categories")}</div>
10
+ <div id="image-edit-categories"></div>
11
+ </div>`
12
+ }
13
+
14
+ /* A template for the form for the image upload dialog. */
15
+ export const imageEditTemplate = ({image, cats}) =>
16
+ `<div>
17
+ <input name="title" class="fw-media-title" type="text"
18
+ placeholder="${gettext("Insert a title")}" value="${
19
+ image ? escapeText(image.title) : ""
20
+ }" />
21
+ ${
22
+ image
23
+ ? ""
24
+ : `<button type="button" class="fw-media-select-button fw-button fw-light">
25
+ ${gettext("Select a file")}
26
+ </button>
27
+ <input name="image" type="file" class="fw-media-file-input">`
28
+ }
29
+ </div>
30
+ <div class="figure-preview">
31
+ <button class="figure-edit-menu" title="${gettext("Edit Image")}">
32
+ <span class="dot-menu-icon"><i class="fa fa-ellipsis-v"></i></span>
33
+ </button>
34
+ <div>
35
+ ${
36
+ image && image.image
37
+ ? `<div class="img" style="background-image: url(${image.image});"></div>`
38
+ : ""
39
+ }
40
+ </div></div>
41
+ ${imageEditCategoryTemplate({cats})}`
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export {ImageDB} from "./database.js"
2
+ export {ImageOverview} from "./overview/index.js"
3
+ export {ImageEditDialog} from "./edit_dialog/index.js"
4
+ export {ImageSelectionDialog} from "./selection_dialog/index.js"
@@ -0,0 +1,115 @@
1
+ import {
2
+ Dialog,
3
+ activateWait,
4
+ addAlert,
5
+ deactivateWait,
6
+ gettext,
7
+ postJson
8
+ } from "fwtoolkit"
9
+ import {usermediaEditcategoriesTemplate} from "./templates"
10
+
11
+ export class ImageOverviewCategories {
12
+ constructor(imageOverview) {
13
+ this.imageOverview = imageOverview
14
+ imageOverview.mod.categories = this
15
+ }
16
+
17
+ //save changes or create a new category
18
+ saveCategories(cats) {
19
+ activateWait()
20
+
21
+ postJson("/api/usermedia/save_category/", {
22
+ ids: cats.ids,
23
+ titles: cats.titles
24
+ })
25
+ .catch(error => {
26
+ addAlert("error", gettext("Could not update categories"))
27
+ deactivateWait()
28
+ throw error
29
+ })
30
+ .then(({json}) => {
31
+ this.imageOverview.app.imageDB.cats = json.entries
32
+ this.setImageCategoryList(json.entries)
33
+ addAlert("success", gettext("The categories have been updated"))
34
+ deactivateWait()
35
+ })
36
+ }
37
+
38
+ setImageCategoryList(imageCategories) {
39
+ const catSelector = this.imageOverview.menu.model.content.find(
40
+ menuItem => menuItem.id === "cat_selector"
41
+ )
42
+ catSelector.content = catSelector.content.filter(
43
+ cat => cat.type !== "category"
44
+ )
45
+ catSelector.content = catSelector.content.concat(
46
+ imageCategories.map(cat => ({
47
+ type: "category",
48
+ title: cat.category_title,
49
+ action: _overview => {
50
+ const trs = document.querySelectorAll(
51
+ "#imagelist > tbody > tr"
52
+ )
53
+ trs.forEach(tr => {
54
+ if (
55
+ tr
56
+ .querySelector(".fw-usermedia-image")
57
+ .classList.contains(`cat_${cat.id}`)
58
+ ) {
59
+ tr.style.display = ""
60
+ } else {
61
+ tr.style.display = "none"
62
+ }
63
+ })
64
+ }
65
+ }))
66
+ )
67
+ this.imageOverview.menu.update()
68
+ }
69
+
70
+ //open a dialog for editing categories
71
+ editCategoryDialog() {
72
+ const buttons = [
73
+ {
74
+ text: gettext("Submit"),
75
+ classes: "fw-dark",
76
+ click: () => {
77
+ const cats = {
78
+ ids: [],
79
+ titles: []
80
+ }
81
+ document
82
+ .querySelectorAll("#edit-categories .category-form")
83
+ .forEach(el => {
84
+ const thisVal = el.value.trim()
85
+ let thisId = el.dataset.id
86
+ if ("undefined" == typeof thisId) {
87
+ thisId = 0
88
+ }
89
+ if ("" !== thisVal) {
90
+ cats.ids.push(thisId)
91
+ cats.titles.push(thisVal)
92
+ }
93
+ })
94
+ this.saveCategories(cats)
95
+ dialog.close()
96
+ }
97
+ },
98
+ {
99
+ type: "cancel"
100
+ }
101
+ ]
102
+
103
+ const dialog = new Dialog({
104
+ id: "edit-categories",
105
+ title: gettext("Edit Categories"),
106
+ body: usermediaEditcategoriesTemplate({
107
+ categories: this.imageOverview.app.imageDB.cats
108
+ }),
109
+ width: 350,
110
+ height: 350,
111
+ buttons
112
+ })
113
+ dialog.open()
114
+ }
115
+ }