notes-to-strapi-export-article-ai 1.0.119 → 3.0.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.
- package/.eslintrc +30 -22
- package/README.md +98 -143
- package/images/img.png +0 -0
- package/images/img_1.png +0 -0
- package/images/img_10.png +0 -0
- package/images/img_11.png +0 -0
- package/images/img_12.png +0 -0
- package/images/img_13.png +0 -0
- package/images/img_2.png +0 -0
- package/images/img_3.png +0 -0
- package/images/img_4.png +0 -0
- package/images/img_5.png +0 -0
- package/images/img_6.png +0 -0
- package/images/img_7.png +0 -0
- package/images/img_8.png +0 -0
- package/images/img_9.png +0 -0
- package/manifest.json +2 -2
- package/package.json +29 -26
- package/src/components/APIKeys.ts +219 -0
- package/src/components/Configuration.ts +663 -0
- package/src/components/Dashboard.ts +184 -0
- package/src/components/ImageSelectionModal.ts +58 -0
- package/src/components/Routes.ts +279 -0
- package/src/constants.ts +22 -61
- package/src/main.ts +177 -34
- package/src/services/configuration-generator.ts +172 -0
- package/src/services/field-analyzer.ts +84 -0
- package/src/services/frontmatter.ts +329 -0
- package/src/services/strapi-export.ts +436 -0
- package/src/settings/UnifiedSettingsTab.ts +206 -0
- package/src/types/image.ts +27 -16
- package/src/types/index.ts +3 -0
- package/src/types/route.ts +51 -0
- package/src/types/settings.ts +22 -23
- package/src/utils/analyse-file.ts +94 -0
- package/src/utils/debounce.ts +34 -0
- package/src/utils/image-processor.ts +124 -400
- package/src/utils/preview-modal.ts +265 -0
- package/src/utils/process-file.ts +122 -0
- package/src/utils/strapi-uploader.ts +120 -119
- package/src/settings.ts +0 -404
- package/src/types/article.ts +0 -8
- package/src/utils/openai-generator.ts +0 -139
- package/src/utils/validators.ts +0 -8
- package/version-bump.mjs +0 -14
- package/versions.json +0 -119
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { RouteConfig, AnalyzedContent } from '../types'
|
|
2
|
+
import { StrapiExporterSettings } from '../types/settings'
|
|
3
|
+
import { extractFrontMatterAndContent } from '../utils/analyse-file'
|
|
4
|
+
import { App, TFile } from 'obsidian'
|
|
5
|
+
import { uploadImageToStrapi } from '../utils/strapi-uploader'
|
|
6
|
+
import * as yaml from 'js-yaml'
|
|
7
|
+
|
|
8
|
+
export class StrapiExportService {
|
|
9
|
+
constructor(
|
|
10
|
+
private settings: StrapiExporterSettings,
|
|
11
|
+
private app: App,
|
|
12
|
+
private file: TFile
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
private async sendToStrapi(data: any, route: RouteConfig): Promise<void> {
|
|
16
|
+
const url = `${this.settings.strapiUrl}${route.url}`
|
|
17
|
+
|
|
18
|
+
const response = await fetch(url, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
Authorization: `Bearer ${this.settings.strapiApiToken}`,
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify(data),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const responseText = await response.text()
|
|
28
|
+
let responseData
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
responseData = JSON.parse(responseText)
|
|
32
|
+
} catch {
|
|
33
|
+
responseData = responseText
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Strapi API error (${response.status}): ${
|
|
39
|
+
typeof responseData === 'object'
|
|
40
|
+
? JSON.stringify(responseData)
|
|
41
|
+
: responseData
|
|
42
|
+
}`
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private validateSettings(): void {
|
|
48
|
+
if (!this.settings.strapiUrl) {
|
|
49
|
+
throw new Error('Strapi URL is not configured')
|
|
50
|
+
}
|
|
51
|
+
if (!this.settings.strapiApiToken) {
|
|
52
|
+
throw new Error('Strapi API token is not configured')
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private validateRoute(route: RouteConfig): void {
|
|
57
|
+
if (!route.url) {
|
|
58
|
+
throw new Error('Route URL is not configured')
|
|
59
|
+
}
|
|
60
|
+
if (!route.contentField) {
|
|
61
|
+
throw new Error('Content field is not configured')
|
|
62
|
+
}
|
|
63
|
+
if (!route.fieldMappings) {
|
|
64
|
+
throw new Error('Field mappings are not configured')
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Process and handle all images in the content
|
|
70
|
+
* @param content - The content to process
|
|
71
|
+
* @returns Promise<{ content: string; wasModified: boolean }> - Updated content with processed image links and modification status
|
|
72
|
+
*/
|
|
73
|
+
private async processContentImages(
|
|
74
|
+
content: string
|
|
75
|
+
): Promise<{ content: string; wasModified: boolean }> {
|
|
76
|
+
// Regular expressions for both internal and external images
|
|
77
|
+
const internalImageRegex = /!\[\[([^\]]+\.(png|jpe?g|gif|webp))\]\]/g
|
|
78
|
+
const markdownImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g
|
|
79
|
+
|
|
80
|
+
let processedContent = content
|
|
81
|
+
let wasModified = false
|
|
82
|
+
let match
|
|
83
|
+
|
|
84
|
+
// Process internal images (Obsidian format)
|
|
85
|
+
while ((match = internalImageRegex.exec(content)) !== null) {
|
|
86
|
+
const [fullMatch, imagePath] = match
|
|
87
|
+
const processedUrl = await this.processImage(imagePath, true)
|
|
88
|
+
if (processedUrl) {
|
|
89
|
+
wasModified = true
|
|
90
|
+
processedContent = processedContent.replace(
|
|
91
|
+
fullMatch,
|
|
92
|
+
``
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Process markdown images
|
|
98
|
+
while ((match = markdownImageRegex.exec(content)) !== null) {
|
|
99
|
+
const [fullMatch, altText, imagePath] = match
|
|
100
|
+
const processedUrl = await this.processImage(imagePath, false)
|
|
101
|
+
if (processedUrl) {
|
|
102
|
+
wasModified = true
|
|
103
|
+
processedContent = processedContent.replace(
|
|
104
|
+
fullMatch,
|
|
105
|
+
``
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { content: processedContent, wasModified }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Process individual image
|
|
115
|
+
* @param imagePath - Path or URL of the image
|
|
116
|
+
* @param isInternal - Whether the image is internal to Obsidian
|
|
117
|
+
* @returns Promise<string | null> - Processed image URL
|
|
118
|
+
*/
|
|
119
|
+
private async processImage(
|
|
120
|
+
imagePath: string,
|
|
121
|
+
isInternal: boolean
|
|
122
|
+
): Promise<string | null> {
|
|
123
|
+
if (isInternal) {
|
|
124
|
+
return await this.handleInternalImage(imagePath)
|
|
125
|
+
} else {
|
|
126
|
+
return await this.handleExternalImage(imagePath)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Handle internal Obsidian image
|
|
132
|
+
*/
|
|
133
|
+
private async handleInternalImage(imagePath: string): Promise<string | null> {
|
|
134
|
+
const file = this.app.vault.getAbstractFileByPath(imagePath)
|
|
135
|
+
if (!(file instanceof TFile)) {
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const uploadedImage = await uploadImageToStrapi(
|
|
140
|
+
file,
|
|
141
|
+
file.name,
|
|
142
|
+
this.settings,
|
|
143
|
+
this.app
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return uploadedImage?.url || null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Handle external image URL
|
|
151
|
+
*/
|
|
152
|
+
private async handleExternalImage(imageUrl: string): Promise<string | null> {
|
|
153
|
+
// First, check if image already exists in Strapi
|
|
154
|
+
const existingImage = await this.checkExistingImage(imageUrl)
|
|
155
|
+
if (existingImage) {
|
|
156
|
+
return existingImage.data.url
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// If not, download and upload to Strapi
|
|
160
|
+
const response = await fetch(imageUrl)
|
|
161
|
+
const blob = await response.blob()
|
|
162
|
+
const fileName = this.getFileNameFromUrl(imageUrl)
|
|
163
|
+
|
|
164
|
+
const formData = new FormData()
|
|
165
|
+
formData.append('files', blob, fileName)
|
|
166
|
+
|
|
167
|
+
const uploadResponse = await fetch(
|
|
168
|
+
`${this.settings.strapiUrl}/api/upload`,
|
|
169
|
+
{
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: {
|
|
172
|
+
Authorization: `Bearer ${this.settings.strapiApiToken}`,
|
|
173
|
+
},
|
|
174
|
+
body: formData,
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if (!uploadResponse.ok) {
|
|
179
|
+
throw new Error('Failed to upload image to Strapi')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const uploadResult = await uploadResponse.json()
|
|
183
|
+
return uploadResult[0]?.url || null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if image already exists in Strapi
|
|
188
|
+
*/
|
|
189
|
+
private async checkExistingImage(
|
|
190
|
+
imageUrl: string
|
|
191
|
+
): Promise<{ data: { id: number; url: string } } | null> {
|
|
192
|
+
const response = await fetch(
|
|
193
|
+
`${this.settings.strapiUrl}/api/upload/files?filters[url][$eq]=${encodeURIComponent(imageUrl)}`,
|
|
194
|
+
{
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `Bearer ${this.settings.strapiApiToken}`,
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
return null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const results = await response.json()
|
|
206
|
+
return results.length > 0
|
|
207
|
+
? {
|
|
208
|
+
data: {
|
|
209
|
+
id: results[0].id,
|
|
210
|
+
url: results[0].url,
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
: null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Extract filename from URL
|
|
218
|
+
*/
|
|
219
|
+
private getFileNameFromUrl(url: string): string {
|
|
220
|
+
const urlParts = url.split('/')
|
|
221
|
+
const fileName = urlParts[urlParts.length - 1].split('?')[0]
|
|
222
|
+
return fileName || 'image.jpg'
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Process frontmatter data and handle any image fields
|
|
227
|
+
* @param frontmatter - The frontmatter object to process
|
|
228
|
+
* @returns Promise<{frontmatter: any, wasModified: boolean}> - Processed frontmatter and modification status
|
|
229
|
+
*/
|
|
230
|
+
private async processFrontmatterImages(
|
|
231
|
+
frontmatter: any
|
|
232
|
+
): Promise<{ frontmatter: any; wasModified: boolean }> {
|
|
233
|
+
let wasModified = false
|
|
234
|
+
const processedFrontmatter = { ...frontmatter }
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Recursively process object properties
|
|
238
|
+
* @param obj - Object to process
|
|
239
|
+
* @returns Promise<any> - Processed object
|
|
240
|
+
*/
|
|
241
|
+
const processObject = async (obj: any): Promise<any> => {
|
|
242
|
+
for (const key in obj) {
|
|
243
|
+
const value = obj[key]
|
|
244
|
+
|
|
245
|
+
// Handle arrays
|
|
246
|
+
if (Array.isArray(value)) {
|
|
247
|
+
const processedArray = await Promise.all(
|
|
248
|
+
value.map(async item => {
|
|
249
|
+
if (typeof item === 'object' && item !== null) {
|
|
250
|
+
return await processObject(item)
|
|
251
|
+
}
|
|
252
|
+
if (typeof item === 'string') {
|
|
253
|
+
return await processStringValue(item)
|
|
254
|
+
}
|
|
255
|
+
return item
|
|
256
|
+
})
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if (JSON.stringify(processedArray) !== JSON.stringify(value)) {
|
|
260
|
+
obj[key] = processedArray
|
|
261
|
+
wasModified = true
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Handle nested objects
|
|
265
|
+
else if (typeof value === 'object' && value !== null) {
|
|
266
|
+
obj[key] = await processObject(value)
|
|
267
|
+
}
|
|
268
|
+
// Handle string values
|
|
269
|
+
else if (typeof value === 'string') {
|
|
270
|
+
const processedValue = await processStringValue(value)
|
|
271
|
+
if (processedValue !== value) {
|
|
272
|
+
obj[key] = processedValue
|
|
273
|
+
wasModified = true
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return obj
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Process string values for possible image paths/URLs
|
|
282
|
+
* @param value - String value to process
|
|
283
|
+
* @returns Promise<string> - Processed string value
|
|
284
|
+
*/
|
|
285
|
+
const processStringValue = async (value: string): Promise<string> => {
|
|
286
|
+
if (this.isImagePath(value)) {
|
|
287
|
+
const isInternal =
|
|
288
|
+
value.startsWith('./') ||
|
|
289
|
+
value.startsWith('../') ||
|
|
290
|
+
!value.startsWith('http')
|
|
291
|
+
const processedUrl = await this.processImage(value, isInternal)
|
|
292
|
+
if (processedUrl) {
|
|
293
|
+
return processedUrl
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return value
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
await processObject(processedFrontmatter)
|
|
300
|
+
return { frontmatter: processedFrontmatter, wasModified }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Check if a string value might be an image path
|
|
305
|
+
* @param value - String to check
|
|
306
|
+
* @returns boolean - True if the string appears to be an image path
|
|
307
|
+
*/
|
|
308
|
+
private isImagePath(value: string): boolean {
|
|
309
|
+
const imageExtensions = /\.(jpg|jpeg|png|gif|webp|svg)$/i
|
|
310
|
+
return (
|
|
311
|
+
imageExtensions.test(value) ||
|
|
312
|
+
value.includes('/upload/') || // For existing Strapi URLs
|
|
313
|
+
/^https?:\/\/.*\.(jpg|jpeg|png|gif|webp|svg)/i.test(value)
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Prepare export data by processing both content and frontmatter
|
|
319
|
+
* @param app - Obsidian App instance
|
|
320
|
+
* @param file - Current TFile
|
|
321
|
+
* @param route - Route configuration
|
|
322
|
+
* @returns Promise<any> - Prepared export data
|
|
323
|
+
*/
|
|
324
|
+
private async prepareExportData(
|
|
325
|
+
app: App,
|
|
326
|
+
file: TFile,
|
|
327
|
+
route: RouteConfig
|
|
328
|
+
): Promise<any> {
|
|
329
|
+
const content = await app.vault.read(file)
|
|
330
|
+
const { frontmatter, body } = extractFrontMatterAndContent(content)
|
|
331
|
+
|
|
332
|
+
// Process images in frontmatter
|
|
333
|
+
const {
|
|
334
|
+
frontmatter: processedFrontmatter,
|
|
335
|
+
wasModified: frontmatterModified,
|
|
336
|
+
} = await this.processFrontmatterImages(frontmatter)
|
|
337
|
+
|
|
338
|
+
// Process images in content
|
|
339
|
+
const { content: processedContent, wasModified: contentModified } =
|
|
340
|
+
await this.processContentImages(body)
|
|
341
|
+
|
|
342
|
+
// Update file if modifications were made
|
|
343
|
+
if (frontmatterModified || contentModified) {
|
|
344
|
+
await this.updateObsidianFile(processedContent, processedFrontmatter)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
data: {
|
|
349
|
+
...processedFrontmatter,
|
|
350
|
+
[route.contentField]: processedContent,
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Update Obsidian file with processed content and frontmatter
|
|
357
|
+
* @param newContent - Processed content
|
|
358
|
+
* @param newFrontmatter - Processed frontmatter
|
|
359
|
+
* @returns Promise<void>
|
|
360
|
+
*/
|
|
361
|
+
private async updateObsidianFile(
|
|
362
|
+
newContent: string,
|
|
363
|
+
newFrontmatter: any
|
|
364
|
+
): Promise<void> {
|
|
365
|
+
try {
|
|
366
|
+
let updatedContent = ''
|
|
367
|
+
|
|
368
|
+
// Add updated frontmatter
|
|
369
|
+
if (Object.keys(newFrontmatter).length > 0) {
|
|
370
|
+
updatedContent = '---\n'
|
|
371
|
+
updatedContent += yaml.dump(newFrontmatter)
|
|
372
|
+
updatedContent += '---\n\n'
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Add updated content
|
|
376
|
+
updatedContent += newContent
|
|
377
|
+
|
|
378
|
+
// Update the file
|
|
379
|
+
await this.app.vault.modify(this.file, updatedContent)
|
|
380
|
+
} catch (error) {
|
|
381
|
+
throw new Error('Failed to update Obsidian file' + error.message)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Update the exportContent method to use the new image processing
|
|
386
|
+
async exportContent(
|
|
387
|
+
content: AnalyzedContent,
|
|
388
|
+
route: RouteConfig
|
|
389
|
+
): Promise<void> {
|
|
390
|
+
this.validateSettings()
|
|
391
|
+
this.validateRoute(route)
|
|
392
|
+
|
|
393
|
+
const exportData = await this.prepareExportData(this.app, this.file, route)
|
|
394
|
+
|
|
395
|
+
if (exportData.data[route.contentField]) {
|
|
396
|
+
const { content: processedContent } = await this.processContentImages(
|
|
397
|
+
exportData.data[route.contentField]
|
|
398
|
+
)
|
|
399
|
+
exportData.data[route.contentField] = processedContent
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
exportData.data = await this.convertImageUrlsToIds(exportData.data)
|
|
403
|
+
|
|
404
|
+
await this.sendToStrapi(exportData, route)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Convert image URLs to Strapi IDs in frontmatter
|
|
409
|
+
* @param data - The data object containing frontmatter
|
|
410
|
+
* @returns Promise<any> - Updated data with image IDs
|
|
411
|
+
*/
|
|
412
|
+
private async convertImageUrlsToIds(data: any): Promise<any> {
|
|
413
|
+
const processValue = async (value: any): Promise<any> => {
|
|
414
|
+
if (typeof value === 'string' && this.isImagePath(value)) {
|
|
415
|
+
const imageInfo = await this.checkExistingImage(value)
|
|
416
|
+
return imageInfo?.data?.id || value
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (Array.isArray(value)) {
|
|
420
|
+
return Promise.all(value.map(item => processValue(item)))
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (typeof value === 'object' && value !== null) {
|
|
424
|
+
const processed: any = {}
|
|
425
|
+
for (const [key, val] of Object.entries(value)) {
|
|
426
|
+
processed[key] = await processValue(val)
|
|
427
|
+
}
|
|
428
|
+
return processed
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return value
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return await processValue(data)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { App, PluginSettingTab, Notice } from 'obsidian'
|
|
2
|
+
import StrapiExporterPlugin from '../main'
|
|
3
|
+
import { Dashboard } from '../components/Dashboard'
|
|
4
|
+
import { Configuration } from '../components/Configuration'
|
|
5
|
+
import { APIKeys } from '../components/APIKeys'
|
|
6
|
+
import { Routes } from '../components/Routes'
|
|
7
|
+
|
|
8
|
+
interface TabDefinition {
|
|
9
|
+
id: string
|
|
10
|
+
name: string
|
|
11
|
+
icon?: string
|
|
12
|
+
description?: string
|
|
13
|
+
component?: any
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class UnifiedSettingsTab extends PluginSettingTab {
|
|
17
|
+
plugin: StrapiExporterPlugin
|
|
18
|
+
private components: {
|
|
19
|
+
dashboard?: Dashboard
|
|
20
|
+
configuration?: Configuration
|
|
21
|
+
apiKeys?: APIKeys
|
|
22
|
+
routes?: Routes
|
|
23
|
+
} = {}
|
|
24
|
+
private contentContainer: HTMLElement
|
|
25
|
+
|
|
26
|
+
private readonly tabs: TabDefinition[] = [
|
|
27
|
+
{
|
|
28
|
+
id: 'dashboard',
|
|
29
|
+
name: 'Dashboard',
|
|
30
|
+
icon: 'gauge',
|
|
31
|
+
description: 'Overview and status',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'configuration',
|
|
35
|
+
name: 'Configuration',
|
|
36
|
+
icon: 'settings',
|
|
37
|
+
description: 'Configure export settings',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'apiKeys',
|
|
41
|
+
name: 'API Keys',
|
|
42
|
+
icon: 'key',
|
|
43
|
+
description: 'Manage API credentials',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'routes',
|
|
47
|
+
name: 'Routes',
|
|
48
|
+
icon: 'git-branch',
|
|
49
|
+
description: 'Configure export routes',
|
|
50
|
+
},
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
constructor(app: App, plugin: StrapiExporterPlugin) {
|
|
54
|
+
super(app, plugin)
|
|
55
|
+
this.plugin = plugin
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
display(): void {
|
|
59
|
+
try {
|
|
60
|
+
const { containerEl } = this
|
|
61
|
+
containerEl.empty()
|
|
62
|
+
|
|
63
|
+
this.createHeader()
|
|
64
|
+
this.createTabNavigation()
|
|
65
|
+
this.createContentContainer()
|
|
66
|
+
this.updateContent()
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.showError('Failed to display settings' + error.message)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private createHeader(): void {
|
|
73
|
+
const headerEl = this.containerEl.createDiv('settings-header')
|
|
74
|
+
|
|
75
|
+
headerEl.createEl('h1', {
|
|
76
|
+
text: 'Strapi Exporter Settings',
|
|
77
|
+
cls: 'settings-title',
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
headerEl.createEl('p', {
|
|
81
|
+
text: 'Configure your Strapi export settings and manage your integrations.',
|
|
82
|
+
cls: 'settings-description',
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private createTabNavigation(): void {
|
|
87
|
+
const navContainer = this.containerEl.createDiv('strapi-exporter-nav')
|
|
88
|
+
|
|
89
|
+
this.tabs.forEach(tab => {
|
|
90
|
+
this.createTabButton(navContainer, tab)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private createTabButton(container: HTMLElement, tab: TabDefinition): void {
|
|
95
|
+
const button = container.createEl('button', {
|
|
96
|
+
cls: 'strapi-exporter-nav-button',
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Icon if provided
|
|
100
|
+
if (tab.icon) {
|
|
101
|
+
button.createSpan({
|
|
102
|
+
cls: `nav-icon ${tab.icon}`,
|
|
103
|
+
attr: { 'aria-hidden': 'true' },
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Button text
|
|
108
|
+
button.createSpan({
|
|
109
|
+
text: tab.name,
|
|
110
|
+
cls: 'nav-text',
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// Add tooltip with description
|
|
114
|
+
if (tab.description) {
|
|
115
|
+
button.setAttribute('aria-label', tab.description)
|
|
116
|
+
button.setAttribute('title', tab.description)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Set active state
|
|
120
|
+
if (this.plugin.settings.currentTab === tab.id) {
|
|
121
|
+
button.addClass('is-active')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Add click handler
|
|
125
|
+
button.addEventListener('click', () => {
|
|
126
|
+
this.handleTabChange(tab.id, container, button)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private createContentContainer(): void {
|
|
131
|
+
this.contentContainer = this.containerEl.createDiv(
|
|
132
|
+
'strapi-exporter-content'
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private handleTabChange(
|
|
137
|
+
tabId: string,
|
|
138
|
+
container: HTMLElement,
|
|
139
|
+
button: HTMLElement
|
|
140
|
+
): void {
|
|
141
|
+
try {
|
|
142
|
+
this.plugin.settings.currentTab = tabId
|
|
143
|
+
this.updateActiveButton(container, button)
|
|
144
|
+
this.updateContent()
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.showError('Failed to change tab' + error.message)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private updateActiveButton(
|
|
151
|
+
container: HTMLElement,
|
|
152
|
+
activeButton: HTMLElement
|
|
153
|
+
): void {
|
|
154
|
+
container.findAll('.strapi-exporter-nav-button').forEach(btn => {
|
|
155
|
+
btn.removeClass('is-active')
|
|
156
|
+
})
|
|
157
|
+
activeButton.addClass('is-active')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private updateContent(): void {
|
|
161
|
+
try {
|
|
162
|
+
this.contentContainer.empty()
|
|
163
|
+
const currentTab = this.plugin.settings.currentTab
|
|
164
|
+
|
|
165
|
+
if (!this.components[currentTab]) {
|
|
166
|
+
this.initializeComponent(currentTab)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.components[currentTab]?.display()
|
|
170
|
+
} catch (error) {
|
|
171
|
+
this.showError('Failed to update content' + error.message)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private initializeComponent(tabId: string): void {
|
|
176
|
+
switch (tabId) {
|
|
177
|
+
case 'dashboard':
|
|
178
|
+
this.components.dashboard = new Dashboard(
|
|
179
|
+
this.plugin,
|
|
180
|
+
this.contentContainer
|
|
181
|
+
)
|
|
182
|
+
break
|
|
183
|
+
case 'configuration':
|
|
184
|
+
this.components.configuration = new Configuration(
|
|
185
|
+
this.plugin,
|
|
186
|
+
this.contentContainer
|
|
187
|
+
)
|
|
188
|
+
break
|
|
189
|
+
case 'apiKeys':
|
|
190
|
+
this.components.apiKeys = new APIKeys(
|
|
191
|
+
this.plugin,
|
|
192
|
+
this.contentContainer
|
|
193
|
+
)
|
|
194
|
+
break
|
|
195
|
+
case 'routes':
|
|
196
|
+
this.components.routes = new Routes(this.plugin, this.contentContainer)
|
|
197
|
+
break
|
|
198
|
+
default:
|
|
199
|
+
throw new Error(`Unknown tab: ${tabId}`)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private showError(message: string): void {
|
|
204
|
+
new Notice(`Settings Error: ${message}`)
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/types/image.ts
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
path: string
|
|
6
|
-
blob: Blob
|
|
7
|
-
name: string
|
|
1
|
+
export interface ImageDescription {
|
|
2
|
+
url?: string
|
|
3
|
+
path?: string
|
|
4
|
+
name?: string
|
|
8
5
|
id?: any
|
|
6
|
+
blob?: Blob
|
|
7
|
+
description?: ImageMetadata
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ImageMetadata {
|
|
11
|
+
name: string
|
|
12
|
+
alternativeText: string
|
|
13
|
+
caption: string
|
|
14
|
+
width?: number
|
|
15
|
+
height?: number
|
|
16
|
+
format?: string
|
|
17
|
+
size?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ImageProcessingResult {
|
|
21
|
+
content: string
|
|
22
|
+
processedImages: ImageDescription[]
|
|
23
|
+
stats?: ImageProcessingStats
|
|
9
24
|
}
|
|
10
25
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
name: string
|
|
17
|
-
alternativeText: string
|
|
18
|
-
caption: string
|
|
19
|
-
}
|
|
26
|
+
export interface ImageProcessingStats {
|
|
27
|
+
total: number
|
|
28
|
+
processed: number
|
|
29
|
+
failed: number
|
|
30
|
+
skipped: number
|
|
20
31
|
}
|