Pinafore-Web-Client-Frontend/src/routes/_components/dialog/components/MediaAltEditor.html

232 lines
6.6 KiB
HTML

<div class="media-alt-editor {className}">
<textarea
id="the-media-alt-input-{realm}-{index}"
class="media-alt-input"
placeholder="{intl.altLabel}"
ref:textarea
bind:value=rawText
></textarea>
<label for="the-media-alt-input-{realm}-{index}" class="sr-only">
{intl.altLabel}
</label>
<LengthGauge
{length}
{overLimit}
max={mediaAltCharLimit}
/>
<LengthIndicator
{length}
{overLimit}
max={mediaAltCharLimit}
style="width: 100%; text-align: right;"
/>
<button class="extract-text-button" type="button"
on:click="onClick()"
disabled={extracting}
aria-label={extractButtonLabel}
>
<SvgIcon href="{extracting ? '#fa-spinner' : '#fa-magic'}"
className="extract-text-svg {extracting ? 'spin' : ''}"
/>
<span>{extractButtonText}</span>
</button>
<LengthGauge
length={extractionProgress}
overLimit={false}
max={100}
/>
</div>
<style>
.media-alt-editor {
display: flex;
flex-direction: column;
}
.media-alt-input {
padding: 5px;
border: 1px solid var(--input-border);
min-height: 40px;
resize: none;
overflow: hidden;
word-wrap: break-word;
/* Text must be at least 16px or else iOS Safari zooms in */
font-size: 1.2em;
max-height: 70vh;
}
.extract-text-button {
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
}
.extract-text-button span {
margin-left: 15px;
}
:global(.extract-text-svg) {
fill: var(--button-text);
width: 18px;
height: 18px;
}
@media (max-height: 767px) {
.media-alt-input {
max-height: 40vh;
width: 100%;
overflow: auto;
}
.extract-text-button {
margin-top: 0;
}
button.extract-text-button {
padding: 7px 10px;
}
}
@media (min-height: 768px) {
.media-alt-input {
min-width: 250px;
min-height: 75px;
}
}
</style>
<script>
import { requestPostAnimationFrame } from '../../../_utils/requestPostAnimationFrame'
import { mark, stop } from '../../../_utils/marks'
import { autosize } from '../../../_thirdparty/autosize/autosize'
import { observe } from 'svelte-extras'
import { throttleTimer } from '../../../_utils/throttleTimer'
import { get } from '../../../_utils/lodash-lite'
import { store } from '../../../_store/store'
import { MEDIA_ALT_CHAR_LIMIT } from '../../../_static/media'
import LengthGauge from '../../LengthGauge.html'
import LengthIndicator from '../../LengthIndicator.html'
import { length } from 'stringz'
import { runTesseract } from '../../../_utils/runTesseract'
import SvgIcon from '../../SvgIcon.html'
import { toast } from '../../toast/toast'
import { getCachedMediaFile } from '../../../_utils/mediaUploadFileCache'
import { formatIntl } from '../../../_utils/formatIntl'
const updateRawTextInStore = throttleTimer(requestPostAnimationFrame)
export default {
oncreate () {
this.setupAutosize()
this.setupSyncFromStore()
this.setupSyncToStore()
},
ondestroy () {
this.teardownAutosize()
},
store: () => store,
data: () => ({
rawText: '',
mediaAltCharLimit: MEDIA_ALT_CHAR_LIMIT,
extracting: false,
className: '',
extractionProgress: 0
}),
computed: {
length: ({ rawText }) => length(rawText || ''),
overLimit: ({ mediaAltCharLimit, length }) => length > mediaAltCharLimit,
url: ({ media, index }) => get(media, [index, 'data', 'url']),
mediaId: ({ media, index }) => get(media, [index, 'data', 'id']),
extractButtonText: ({ extracting }) => extracting ? 'intl.extractingText' : 'intl.extractText',
extractButtonLabel: ({ extractButtonText, extractionProgress, extracting }) => {
if (extracting) {
return formatIntl('intl.extractingTextCompletion', { percent: Math.round(extractionProgress) })
}
return extractButtonText
}
},
methods: {
observe,
setupSyncFromStore () {
this.observe('media', media => {
media = media || []
const { index, rawText } = this.get()
const text = get(media, [index, 'description'], '')
if (rawText !== text) {
this.set({ rawText: text })
}
})
},
setupSyncToStore () {
this.observe('rawText', rawText => {
updateRawTextInStore(() => {
const { realm, index, media } = this.get()
if (media[index].description !== rawText) {
media[index].description = rawText
this.store.setComposeData(realm, { media })
this.store.save()
}
this.fire('resize')
})
}, { init: false })
},
setupAutosize () {
const textarea = this.refs.textarea
requestPostAnimationFrame(() => {
mark('autosize()')
autosize(textarea)
stop('autosize()')
})
},
teardownAutosize () {
mark('autosize.destroy()')
autosize.destroy(this.refs.textarea)
stop('autosize.destroy()')
},
measure () {
autosize.update(this.refs.textarea)
},
async onClick () {
this.set({ extracting: true })
try {
const { url, mediaId } = this.get()
const onProgress = progress => {
requestAnimationFrame(() => {
this.set({ extractionProgress: progress * 100 })
})
}
const file = await getCachedMediaFile(mediaId)
let text
if (file) { // Avoid downloading from the network a file that the user *just* uploaded
const fileUrl = URL.createObjectURL(file)
try {
text = await runTesseract(fileUrl, onProgress)
} finally {
URL.revokeObjectURL(fileUrl)
}
} else {
text = await runTesseract(url, onProgress)
}
const { media, index, realm } = this.get()
if (media[index].description !== text) {
media[index].description = text
this.store.setComposeData(realm, { media })
this.store.save()
}
} catch (err) {
console.error(err)
/* no await */ toast.say('intl.unableToExtractText')
} finally {
this.set({ extracting: false })
setTimeout(() => {
requestAnimationFrame(() => {
this.set({ extractionProgress: 0 })
})
}, 400)
}
}
},
components: {
LengthGauge,
LengthIndicator,
SvgIcon
}
}
</script>