contacts-pane
Version:
Contacts Pane: Contacts manager for Address Book, Groups, and Individuals.
315 lines (296 loc) • 9.9 kB
JavaScript
import * as UI from 'solid-ui'
import { store } from 'solid-logic'
import * as mime from 'mime-types'
const $rdf = UI.rdf
const ns = UI.ns
const utils = UI.utils
const kb = store
/* Mugshot Gallery
*
* A widget for managing a set of images.
* Make this a form field?
*/
export function renderMugshotGallery (dom, subject) {
function complain (message) {
console.log(message)
galleryDiv.appendChild(UI.widgets.errorMessageBlock(dom, message, 'pink'))
}
async function linkToPicture (subject, pic, remove) {
const link = [
$rdf.st(subject, ns.vcard('hasPhoto'), pic, subject.doc())
]
try {
if (remove) {
await kb.updater.update(link, [])
} else {
await kb.updater.update([], link)
}
} catch (err) {
const msg = ' Write back image link FAIL ' + pic + ', Error: ' + err
console.log(msg)
alert(msg)
}
}
function handleDroppedThing (thing) {
kb.fetcher.nowOrWhenFetched(thing.doc(), function (ok, mess) {
if (!ok) {
console.log('Error looking up dropped thing ' + thing + ': ' + mess)
} else {
const types = kb.findTypeURIs(thing)
for (const ty in types) {
console.log(' drop object type includes: ' + ty) // @@ Allow email addresses and phone numbers to be dropped?
}
console.log('Default: assume web page ' + thing) // icon was: UI.icons.iconBase + 'noun_25830.svg'
kb.add(subject, ns.wf('attachment'), thing, subject.doc())
// @@ refresh UI
}
})
}
function uploadFileToContact (filename, contentType, data) {
// const fileExtension = filename.split('.').pop() // .toLowerCase()
const extension = mime.extension(contentType)
if (contentType !== mime.lookup(filename)) {
filename += '_.' + extension
console.log('MIME TYPE MISMATCH -- adding extension: ' + filename)
}
let prefix, predicate
const isImage = contentType.startsWith('image')
if (isImage) {
prefix = 'image_'
predicate = ns.vcard('hasPhoto')
} else {
prefix = 'attachment_'
predicate = ns.wf('attachment')
}
let n, pic
for (n = 0; ; n++) {
// Check filename is not used or invent new one
pic = kb.sym(subject.dir().uri + filename)
if (!kb.holds(subject, ns.vcard('hasPhoto'), pic)) {
break
}
filename = prefix + n + '.' + extension
}
console.log(
'Putting ' +
data.byteLength +
' bytes of ' +
contentType +
' to ' +
pic
)
kb.fetcher
.webOperation('PUT', pic.uri, {
data: data,
contentType: contentType
})
.then(function (response) {
if (!response.ok) {
complain('Error uploading ' + pic + ':' + response.status)
return
}
console.log(' Upload: put OK: ' + pic)
kb.add(subject, predicate, pic, subject.doc())
kb.fetcher
.putBack(subject.doc(), { contentType: 'text/turtle' })
.then(
function (_response) {
if (isImage) {
mugshotDiv.refresh()
}
},
function (err) {
console.log(
' Write back image link FAIL ' + pic + ', Error: ' + err
)
}
)
})
}
// When a set of URIs are dropped on
async function handleURIsDroppedOnMugshot (uris) {
for (const u of uris) {
let thing = $rdf.sym(u) // Attachment needs text label to disinguish I think not icon.
console.log('Dropped on mugshot thing ' + thing) // icon was: UI.icons.iconBase + 'noun_25830.svg'
if (u.startsWith('http') && u.indexOf('#') < 0) {
// Plain document
// Take a copy of a photo on the web:
if (u.startsWith('http:')) { // because of browser mixed content stuff can't read http:
thing = $rdf.sym('https:' + u.slice(5))
}
const options = { withCredentials: false, credentials: 'omit' }
let result
try {
result = await kb.fetcher.webOperation('GET', thing.uri, options)
} catch (err) {
complain(
`Gallery: fetch error trying to read picture ${thing} data: ${err}`
)
handleDroppedThing(thing)
return
}
const contentType = result.headers.get('Content-Type')
let pathEnd = thing.uri.split('/').slice(-1)[0] // last segment as putative filename
pathEnd = pathEnd.split('?')[0] // chop off any query params
const data = await result.arrayBuffer()
if (!result.ok) {
alert('Cant download, so will link image. ' + thing + ':' + result.status)
handleDroppedThing(thing)
return
}
uploadFileToContact(pathEnd, contentType, data)
return
} else {
alert(
'Not a web document URI, cannot copy as picture: ' + thing
)
}
handleDroppedThing(thing)
}
}
// Drop an image file to set up the mugshot
function droppedFileHandler (files) {
for (let i = 0; i < files.length; i++) {
const f = files[i]
console.log(
' contacts: Filename: ' +
f.name +
', type: ' +
(f.type || 'n/a') +
' size: ' +
f.size +
' bytes, last modified: ' +
(f.lastModifiedDate
? f.lastModifiedDate.toLocaleDateString()
: 'n/a')
) // See e.g. https://www.html5rocks.com/en/tutorials/file/dndfiles/
// @@ Add: progress bar(s)
const reader = new FileReader()
reader.onload = (function (theFile) {
return function (e) {
const data = e.target.result
console.log(' File read byteLength : ' + data.byteLength)
const filename = encodeURIComponent(theFile.name)
const contentType = theFile.type
uploadFileToContact(filename, contentType, data)
}
})(f)
reader.readAsArrayBuffer(f)
}
}
function elementForImage (image) {
const img = dom.createElement('img')
img.setAttribute(
'style',
'max-height: 10em; border-radius: 1em; margin: 0.7em;'
)
UI.widgets.makeDropTarget(
img,
handleURIsDroppedOnMugshot,
droppedFileHandler
)
if (image) {
// img.setAttribute('src', image.uri) use token and works with NSS but not with CSS
// we need to get image with authenticated fetch
store.fetcher._fetch(image.uri)
.then(function(response) {
return response.blob()
})
.then(function(myBlob) {
const objectURL = URL.createObjectURL(myBlob)
img.setAttribute('src', objectURL)
})
UI.widgets.makeDraggable(img, image)
}
return img
}
function syncMugshots () {
let images = kb.each(subject, ns.vcard('hasPhoto')) // Priviledge vcard ones
images.sort() // arbitrary consistency
images = images.slice(0, 5) // max number for the space
if (images.length === 0) {
mugshotDiv.innerHTML = '' // strictly, don't remove it if already there
if (editable) {
mugshotDiv.appendChild(placeholder) // A head image to drop pictures on
} // otherwise leave gallery empty .. nothing to see here folks
} else {
utils.syncTableToArray(mugshotDiv, images, elementForImage)
}
}
// Good URI for a Camera picture
function getImageDoc () {
const imageDoc = kb.sym(
subject.dir().uri + 'Image_' + Date.now() + '.png'
)
return imageDoc
}
// Store picture
async function tookPicture (imageDoc) {
if (imageDoc) {
await linkToPicture(subject, imageDoc)
syncMugshots()
}
}
function trashCan () {
const button = UI.widgets.button(
dom,
UI.icons.iconBase + 'noun_925021.svg',
'Drag here to delete'
)
async function droppedURIHandler (uris) {
const images = kb
.each(subject, ns.vcard('hasPhoto'))
.map(x => x.uri)
for (const uri of uris) {
if (!images.includes(uri)) {
alert('Only drop images in this contact onto this trash can.')
return
}
if (confirm(`Permanently DELETE image ${uri} completely?`)) {
console.log('Unlinking image file ' + uri)
await linkToPicture(subject, kb.sym(uri), true)
try {
console.log('Deleting image file ' + uri)
await kb.fetcher.webOperation('DELETE', uri)
} catch (err) {
alert('Unable to delete picture! ' + err)
}
}
}
syncMugshots()
}
UI.widgets.makeDropTarget(button, droppedURIHandler, null)
return button
}
function renderImageTools () {
const imageToolTable = dom.createElement('table')
const row = imageToolTable.appendChild(dom.createElement('tr'))
const left = row.appendChild(dom.createElement('td'))
const middle = row.appendChild(dom.createElement('td'))
const right = row.appendChild(dom.createElement('td'))
left.appendChild(
UI.media.cameraButton(dom, kb, getImageDoc, tookPicture)
) // 20190812
try {
middle.appendChild(
UI.widgets.fileUploadButtonDiv(dom, droppedFileHandler)
)
} catch (e) {
console.log('ignore fileUploadButtonDiv error for now', e)
}
right.appendChild(trashCan())
return imageToolTable
}
// Body of renderMugshotGallery
const editable = kb.updater.editable(subject.doc().uri, kb)
const galleryDiv = dom.createElement('div')
const mugshotDiv = galleryDiv.appendChild(dom.createElement('div'))
const placeholder = elementForImage()
UI.widgets.setImage(placeholder, subject) // Fallback icon or get from web
syncMugshots()
mugshotDiv.refresh = syncMugshots
if (editable) {
galleryDiv.appendChild(renderImageTools())
}
return galleryDiv
}