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(