save images

This commit is contained in:
lightling 2025-03-03 20:28:39 -05:00
parent c66914239d
commit 4a6ad11d16
Signed by: lightling
GPG key ID: F1F29650D537C773
7 changed files with 182 additions and 8 deletions

7
src-tauri/Cargo.lock generated
View file

@ -1512,6 +1512,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "http-range"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.10.0" version = "1.10.0"
@ -3770,6 +3776,7 @@ dependencies = [
"gtk", "gtk",
"heck 0.5.0", "heck 0.5.0",
"http", "http",
"http-range",
"jni", "jni",
"libc", "libc",
"log", "log",

View file

@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", features = [] } tauri = { version = "2", features = ["protocol-asset"] }
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"

View file

@ -7,6 +7,7 @@
], ],
"permissions": [ "permissions": [
"core:default", "core:default",
"core:image:allow-from-path",
"opener:default", "opener:default",
"dialog:default", "dialog:default",
"fs:allow-write-text-file", "fs:allow-write-text-file",

View file

@ -18,7 +18,11 @@
} }
], ],
"security": { "security": {
"csp": null "assetProtocol": {
"enable": true,
"scope": ["*/**"]
},
"csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost"
} }
}, },
"bundle": { "bundle": {

View file

@ -8,6 +8,11 @@ export const FieldTypes = [
export type FieldType = typeof FieldTypes[number] export type FieldType = typeof FieldTypes[number]
export interface Image {
src: string
alt?: string
}
export interface Column { export interface Column {
name: string name: string
type: FieldType type: FieldType

View file

@ -11,6 +11,13 @@ import {
type Row as DataRow, type Row as DataRow,
type Column as DataColumn, type Column as DataColumn,
} from 'src/types/data' } from 'src/types/data'
import ImageEditor from './ImageEditor.vue'
interface EditingRow {
data: DataRow
index: number
field: string
}
const props = defineProps<{ const props = defineProps<{
columns: DataColumn[], columns: DataColumn[],
@ -20,6 +27,8 @@ const emits = defineEmits<{
}>() }>()
const model = defineModel<DataRow[]>({ required: true }) const model = defineModel<DataRow[]>({ required: true })
const editingRows = ref([]) const editingRows = ref([])
const editingImage = ref(false)
const editingImageRow = ref(null as EditingRow | null)
const onRowEditSave = (event: { newData: any, index: number }) => { const onRowEditSave = (event: { newData: any, index: number }) => {
let { newData, index } = event let { newData, index } = event
@ -45,9 +54,28 @@ const onDeleteRow = (event: Event, slotProps: { index: number }) => {
model.value.splice(slotProps.index, 1) model.value.splice(slotProps.index, 1)
emits('dirty') emits('dirty')
} }
const onEditImage = (event: Event, slotProps: { data: DataRow, index: number, field: string }) => {
event.preventDefault()
editingImage.value = true
editingImageRow.value = slotProps
}
const onSaveImage = (image: string) => {
const row = { ...model.value[editingImageRow.value!.index] } as any
row[editingImageRow.value!.field] = {
src: image,
}
model.value[editingImageRow.value!.index] = row
}
</script> </script>
<template lang="pug"> <template lang="pug">
ImageEditor(
v-model:visible='editingImage'
v-model:row='editingImageRow'
@saved='onSaveImage'
)
DataTable.data-editor( DataTable.data-editor(
@row-edit-save='onRowEditSave' @row-edit-save='onRowEditSave'
v-model:editingRows='editingRows' v-model:editingRows='editingRows'
@ -70,9 +98,8 @@ DataTable.data-editor(
v-if='col.type === "image"' v-if='col.type === "image"'
) )
Image( Image(
v-if='slotProps.data.image' v-if='slotProps.data[col.name]'
:src='slotProps.data.image.src' :src='slotProps.data[col.name].src'
:alt='slotProps.data.image.alt'
preview preview
) )
span( span(
@ -83,17 +110,20 @@ DataTable.data-editor(
) )
span {{ slotProps.data[col.name] }} span {{ slotProps.data[col.name] }}
template( template(
#editor='{ data, field }' #editor='slotProps'
) )
div( div(
v-if='col.type === "image"' v-if='col.type === "image"'
) )
span Not Implemented Button(
label='Edit Image'
:onClick='(e) => onEditImage(e, slotProps)'
)
div( div(
v-else v-else
) )
InputText( InputText(
v-model='data[field]' v-model='slotProps.data[slotProps.field]'
fluid fluid
) )
Column( Column(

View file

@ -0,0 +1,127 @@
<script setup lang="ts">
import {
computed,
ref,
} from 'vue'
import { convertFileSrc } from '@tauri-apps/api/core'
import { open } from '@tauri-apps/plugin-dialog'
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
import ImageComponent from 'primevue/image'
import InputGroup from 'primevue/inputgroup'
import InputGroupAddon from 'primevue/inputgroupaddon'
import InputText from 'primevue/inputtext'
import {
type Row as DataRow,
} from 'src/types/data'
import { useAppStore } from 'src/store'
const store = useAppStore()
const visible = defineModel<boolean>('visible', { default: false })
const emit = defineEmits<{
(e: 'saved', val: string): void
}>()
const row = defineModel<DataRow>('row')
const filePath = ref(store.currentInventory.filePath.replace('.json', '/'))
const fileName = ref('')
const currentSrc = ref('')
const currentBlob = ref<Blob>()
const onBrowseForImage = async (e: Event) => {
e.preventDefault()
const val = await open({
multiple: false,
directory: false,
filters: [{ name: 'Image (svg, png, jpg, webp)', extensions: ['svg', 'png', 'jpg', 'jpeg', 'webp'] }],
})
if (!!val && !Array.isArray(val)) {
let res = await fetch(convertFileSrc(val), {
method: 'GET',
})
let blob = await res.blob()
if (blob.type.includes('image')){
currentSrc.value = URL.createObjectURL(blob)
currentBlob.value = blob
}
}
}
const onCancel = (e: Event) => {
e.preventDefault()
visible.value = false
}
function convertToDataUrlViaCanvas(url: string, callback: (val: string) => void, outputFormat: string) {
const image = new Image()
image.onload = function() {
let canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let dataUrl
canvas.height = (this as any).height
canvas.width = (this as any).width
ctx!.drawImage(this as any, 0, 0)
dataUrl = canvas.toDataURL(outputFormat)
callback(dataUrl)
canvas = null as any
}
image.src = url
}
const onSave = async (e: Event) => {
e.preventDefault()
convertToDataUrlViaCanvas(currentSrc.value, (val) => {
emit('saved', val)
visible.value = false
}, currentBlob.value!.type)
}
</script>
<template lang="pug">
Dialog(
v-model:visible='visible'
modal
header='Edit Image'
)
.content(
v-if='!currentSrc'
)
Button(
label='Browse For Image'
@click='onBrowseForImage'
)
.content(
v-else
)
InputGroup.image-group
ImageComponent(
:src='currentSrc'
)
template(#footer)
Button(
label='Save'
@click='onSave'
:disabled='!currentSrc'
)
Button(
label='Replace'
@click='onBrowseForImage'
:disabled='!currentSrc'
)
Button(
label='Cancel'
@click='onCancel'
)
</template>
<style scoped lang="sass">
.image-group
display: flex
justify-content: center
:deep(.p-image img)
max-width: 33vw
max-height: 33vh
</style>