diff --git a/.gitignore b/.gitignore
index 0152b6e..ddbf3f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
__pycache__
node_modules
.mypy_cache
+.pytest_cache
+.ruff_cache
.venv
+data/
diff --git a/static/components/item-card.js b/static/components/item-card.js
index cce5ae0..73258e2 100644
--- a/static/components/item-card.js
+++ b/static/components/item-card.js
@@ -38,6 +38,8 @@ window.app.component('item-card', {
return 'In Stock'
},
itemImgUrl() {
+ if (!this.item.images || !this.item.images.length) return null
+ if (isURLimg(this.item.images[0])) return this.item.images[0]
return isBase64String(this.item.images[0])
? this.item.images[0]
: `/api/v1/assets/${this.item.images[0]}/thumbnail`
diff --git a/static/components/photo-gallery-form.js b/static/components/photo-gallery-form.js
index 21bf9c5..9404837 100644
--- a/static/components/photo-gallery-form.js
+++ b/static/components/photo-gallery-form.js
@@ -99,6 +99,39 @@ window.app.component('photo-gallery-form', {
}
},
+ addFromUrl() {
+ if (!this.canAdd) {
+ LNbits.utils.notifyError(`Maximum ${this.max} photos allowed`)
+ return
+ }
+
+ this.$q
+ .dialog({
+ title: 'Add Photo from URL',
+ color: 'secondary',
+ prompt: {
+ model: '',
+ type: 'text',
+ isValid: isURLimg,
+ label: 'Image URL',
+ attrs: {
+ placeholder: 'https://example.com/photo.jpg'
+ }
+ },
+ cancel: true,
+ persistent: true
+ })
+ .onOk(url => {
+ const preview = url.trim()
+ this.gallery.push({
+ file: null,
+ preview,
+ assetId: null,
+ isNew: true
+ })
+ })
+ },
+
remove(index) {
URL.revokeObjectURL(this.gallery[index].preview)
this.gallery.splice(index, 1)
@@ -111,6 +144,14 @@ window.app.component('photo-gallery-form', {
+ {
return {
assetId: id,
- preview: isBase64String(id) ? id : `/api/v1/assets/${id}/thumbnail`,
+ preview:
+ isBase64String(id) || isURLimg(id)
+ ? id
+ : `/api/v1/assets/${id}/thumbnail`,
file: null,
isNew: false
}
@@ -531,9 +534,9 @@ window.app = Vue.createApp({
this.itemDialog.data.inventory_id = this.inventory.id
try {
const assetIds = await Promise.all(
- this.itemDialog.gallery
- .filter(p => p.file)
- .map(p => this.uploadPhoto(p.file))
+ this.itemDialog.gallery.map(p =>
+ p.file ? this.uploadPhoto(p.file) : p.preview
+ )
)
if (assetIds.includes(null)) {
LNbits.utils.notifyError('One or more photo uploads failed')
@@ -555,40 +558,20 @@ window.app = Vue.createApp({
}
},
async updateItem(data) {
- const filesToUpload = this.itemDialog.gallery
- .filter(
- p =>
- (p.isNew && p.file) ||
- (!p.isNew && p.assetId && isBase64String(p.assetId))
- )
- .map(p => {
- if (p.isNew && p.file) return p.file
- if (!p.isNew && p.assetId && isBase64String(p.assetId))
- return base64ToFile(p.assetId)
- return null
+ const assetIds = await Promise.all(
+ this.itemDialog.gallery.map(p => {
+ if (p.file) return this.uploadPhoto(p.file)
+ if (p.assetId && isBase64String(p.assetId)) {
+ return this.uploadPhoto(base64ToFile(p.assetId))
+ }
+ return p.assetId || p.preview
})
- .filter(Boolean)
-
- let uploadedAssetIds = []
- if (filesToUpload.length > 0) {
- try {
- uploadedAssetIds = await Promise.all(
- filesToUpload.map(file => this.uploadPhoto(file))
- )
- } catch (error) {
- LNbits.utils.notifyError('Failed to upload photos')
- return
- }
+ )
+ if (assetIds.includes(null)) {
+ LNbits.utils.notifyError('One or more photo uploads failed')
+ return
}
-
- // Asset IDs from gallery that are not new and not base64
- const existingAssetIds = this.itemDialog.gallery
- .filter(p => !p.isNew && p.assetId && !isBase64String(p.assetId))
- .map(p => p.assetId)
-
- const finalIds = [...existingAssetIds, ...uploadedAssetIds]
-
- data.images = toCsv(finalIds)
+ data.images = toCsv(assetIds)
try {
const {data: updatedItem} = await LNbits.api.request(
diff --git a/static/js/utils.js b/static/js/utils.js
index fc1c1b1..ebf0231 100644
--- a/static/js/utils.js
+++ b/static/js/utils.js
@@ -21,9 +21,10 @@ function mapItems(obj) {
}
obj.tags = fromCsv(obj.tags)
obj.omit_tags = fromCsv(obj.omit_tags)
- obj.images = isBase64String(obj.images)
- ? fromCsv(obj.images, '|||')
- : fromCsv(obj.images)
+ obj.images =
+ isBase64String(obj.images) || obj.images?.includes('|||')
+ ? fromCsv(obj.images, '|||')
+ : fromCsv(obj.images)
return obj
}
@@ -51,3 +52,11 @@ function isBase64String(str) {
if (typeof str !== 'string') return false
return str.includes('data:') && str.includes('base64')
}
+function isURLimg(str) {
+ if (typeof str !== 'string') return false
+ try {
+ return ['http:', 'https:'].includes(new URL(str.trim()).protocol)
+ } catch {
+ return false
+ }
+}
diff --git a/templates/inventory/manager.html b/templates/inventory/manager.html
index fe05623..582e4b7 100644
--- a/templates/inventory/manager.html
+++ b/templates/inventory/manager.html
@@ -358,11 +358,17 @@
},
async addItem() {
this.itemDialog.data.inventory_id = this.inventory.id
- const base64List = await Promise.all(
- this.itemDialog.gallery.map(p => fileToBase64(p.file))
+ const newPhotos = await Promise.all(
+ this.itemDialog.gallery.map(p =>
+ p.file ? fileToBase64(p.file) : p.preview
+ )
)
+
try {
- this.itemDialog.data.images = toCsv(base64List, '|||')
+ this.itemDialog.data.images = toCsv(
+ newPhotos,
+ newPhotos.some(isBase64String) ? '|||' : ','
+ )
const {data} = await LNbits.api.request(
'POST',
`/inventory/api/v1/managers/${this.manager.id}/item`,
@@ -385,21 +391,16 @@
}
},
async updateItem(data) {
- const newPhotos = this.itemDialog.gallery.filter(p => p.isNew && p.file)
- let newBase64 = []
-
- if (newPhotos.length > 0) {
- newBase64 = await Promise.all(
- newPhotos.map(p => fileToBase64(p.file))
+ const finalImages = await Promise.all(
+ this.itemDialog.gallery.map(p =>
+ p.file ? fileToBase64(p.file) : p.preview
)
- }
-
- const finalImages = [
- ...this.itemDialog.gallery.filter(p => !p.isNew).map(p => p.file),
- ...newBase64
- ]
+ )
- data.images = toCsv(finalImages, '|||')
+ data.images = toCsv(
+ finalImages,
+ finalImages.some(isBase64String) ? '|||' : ','
+ )
try {
const {data: updatedItem} = await LNbits.api.request(