feat: add ability to set focal points for media (#1303)
* feat: add ability to set focal points for media fixes #739 * fix tests * actually fix tests * really really fix tests * really really really fix tests pinkie swear
This commit is contained in:
parent
994dda4806
commit
85b75900c1
|
@ -52,5 +52,6 @@ module.exports = [
|
|||
{ id: 'fa-clock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/clock-o.svg' },
|
||||
{ id: 'fa-refresh', src: 'src/thirdparty/font-awesome-svg-png/white/svg/refresh.svg' },
|
||||
{ id: 'fa-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/plus.svg' },
|
||||
{ id: 'fa-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' }
|
||||
{ id: 'fa-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' },
|
||||
{ id: 'fa-crosshairs', src: 'src/thirdparty/font-awesome-svg-png/white/svg/crosshairs.svg' }
|
||||
]
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
"@babel/core": "^7.5.0",
|
||||
"@gamestdio/websocket": "^0.3.2",
|
||||
"@webcomponents/custom-elements": "^1.2.4",
|
||||
"@wessberg/pointer-events": "^1.0.9",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||
"cheerio": "^1.0.0-rc.2",
|
||||
|
|
|
@ -4,7 +4,7 @@ import { postStatus as postStatusToServer } from '../_api/statuses'
|
|||
import { addStatusOrNotification } from './addStatusOrNotification'
|
||||
import { database } from '../_database/database'
|
||||
import { emit } from '../_utils/eventBus'
|
||||
import { putMediaDescription } from '../_api/media'
|
||||
import { putMediaMetadata } from '../_api/media'
|
||||
|
||||
export async function insertHandleForReply (statusId) {
|
||||
let { currentInstance } = store.get()
|
||||
|
@ -22,7 +22,7 @@ export async function insertHandleForReply (statusId) {
|
|||
|
||||
export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||
sensitive, spoilerText, visibility,
|
||||
mediaDescriptions, inReplyToUuid, poll) {
|
||||
mediaDescriptions, inReplyToUuid, poll, mediaFocalPoints) {
|
||||
let { currentInstance, accessToken, online } = store.get()
|
||||
|
||||
if (!online) {
|
||||
|
@ -31,17 +31,27 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
|||
}
|
||||
|
||||
text = text || ''
|
||||
mediaDescriptions = mediaDescriptions || []
|
||||
|
||||
store.set({
|
||||
postingStatus: true
|
||||
let mediaMetadata = (mediaIds || []).map((mediaId, idx) => {
|
||||
return {
|
||||
description: mediaDescriptions && mediaDescriptions[idx],
|
||||
focalPoint: mediaFocalPoints && mediaFocalPoints[idx]
|
||||
}
|
||||
})
|
||||
|
||||
store.set({ postingStatus: true })
|
||||
try {
|
||||
await Promise.all(mediaDescriptions.map(async (description, i) => {
|
||||
return description && putMediaDescription(currentInstance, accessToken, mediaIds[i], description)
|
||||
await Promise.all(mediaMetadata.map(async ({ description, focalPoint }, i) => {
|
||||
description = description || ''
|
||||
focalPoint = focalPoint || [0, 0]
|
||||
focalPoint[0] = focalPoint[0] || 0
|
||||
focalPoint[1] = focalPoint[1] || 0
|
||||
if (description || focalPoint[0] || focalPoint[1]) {
|
||||
return putMediaMetadata(currentInstance, accessToken, mediaIds[i], description, focalPoint)
|
||||
}
|
||||
}))
|
||||
let status = await postStatusToServer(currentInstance, accessToken, text,
|
||||
inReplyToId, mediaIds, sensitive, spoilerText, visibility, poll)
|
||||
inReplyToId, mediaIds, sensitive, spoilerText, visibility, poll, mediaFocalPoints)
|
||||
addStatusOrNotification(currentInstance, 'home', status)
|
||||
store.clearComposeData(realm)
|
||||
emit('postedStatus', realm, inReplyToUuid)
|
||||
|
|
|
@ -11,7 +11,7 @@ export async function uploadMedia (instanceName, accessToken, file, description)
|
|||
return post(url, formData, auth(accessToken), { timeout: MEDIA_WRITE_TIMEOUT })
|
||||
}
|
||||
|
||||
export async function putMediaDescription (instanceName, accessToken, mediaId, description) {
|
||||
export async function putMediaMetadata (instanceName, accessToken, mediaId, description, focus) {
|
||||
let url = `${basename(instanceName)}/api/v1/media/${mediaId}`
|
||||
return put(url, { description }, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||
return put(url, { description, focus: (focus && focus.join(',')) }, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||
}
|
||||
|
|
102
src/routes/_components/Draggable.html
Normal file
102
src/routes/_components/Draggable.html
Normal file
|
@ -0,0 +1,102 @@
|
|||
<div class="draggable-area {draggableClass}"
|
||||
on:pointermove="onPointerMove(event)"
|
||||
on:pointerleave="onPointerLeave(event)"
|
||||
on:click="onClick(event)"
|
||||
ref:area
|
||||
>
|
||||
<div class="draggable-indicator {indicatorClass}"
|
||||
style={indicatorStyle}
|
||||
on:pointerdown="onPointerDown(event)"
|
||||
on:pointerup="onPointerUp(event)"
|
||||
ref:indicator
|
||||
>
|
||||
<div class="draggable-indicator-inner">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.draggable-area {
|
||||
position: relative;
|
||||
touch-action: none;
|
||||
}
|
||||
.draggable-indicator {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
}
|
||||
.draggable-indicator-inner {
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import { throttleRaf } from '../_utils/throttleRaf'
|
||||
|
||||
const clamp = x => Math.max(0, Math.min(1, x))
|
||||
const throttledRaf = throttleRaf()
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
draggableClass: '',
|
||||
indicatorClass: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
indicatorWidth: 0,
|
||||
indicatorHeight: 0
|
||||
}),
|
||||
computed: {
|
||||
indicatorStyle: ({ x, y, indicatorWidth, indicatorHeight }) => (
|
||||
`left: calc(${x * 100}% - ${indicatorWidth / 2}px); top: calc(${y * 100}% - ${indicatorHeight / 2}px);`
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
onPointerDown (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let rect = this.refs.indicator.getBoundingClientRect()
|
||||
this.set({
|
||||
dragging: true,
|
||||
dragOffsetX: e.clientX - rect.left,
|
||||
dragOffsetY: e.clientY - rect.top
|
||||
})
|
||||
},
|
||||
onPointerMove (e) {
|
||||
if (this.get().dragging) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let { indicatorWidth, indicatorHeight, dragOffsetX, dragOffsetY } = this.get()
|
||||
throttledRaf(() => {
|
||||
let rect = this.refs.area.getBoundingClientRect()
|
||||
let offsetX = dragOffsetX - (indicatorWidth / 2)
|
||||
let offsetY = dragOffsetY - (indicatorHeight / 2)
|
||||
let x = clamp((e.clientX - rect.left - offsetX) / rect.width)
|
||||
let y = clamp((e.clientY - rect.top - offsetY) / rect.height)
|
||||
this.set({ x, y })
|
||||
this.fire('change', { x, y })
|
||||
})
|
||||
}
|
||||
},
|
||||
onPointerUp (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
this.set({ dragging: false })
|
||||
},
|
||||
onPointerLeave (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
this.set({ dragging: false })
|
||||
},
|
||||
onClick (e) {
|
||||
if (!e.target.classList.contains('draggable-indicator')) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let rect = this.refs.area.getBoundingClientRect()
|
||||
let x = clamp((e.clientX - rect.left) / rect.width)
|
||||
let y = clamp((e.clientY - rect.top) / rect.height)
|
||||
this.set({ x, y })
|
||||
this.fire('change', { x, y })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -65,7 +65,9 @@
|
|||
// Here we do a pure css version instead of using
|
||||
// https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
|
||||
|
||||
if (!focus) return 'background-position: center;'
|
||||
if (!focus) {
|
||||
return 'background-position: center;'
|
||||
}
|
||||
return `object-position: ${coordsToPercent(focus.x)}% ${100 - coordsToPercent(focus.y)}%;`
|
||||
},
|
||||
fillFixSize: ({ forceSize, $largeInlineMedia }) => !$largeInlineMedia && !forceSize,
|
||||
|
|
|
@ -193,6 +193,7 @@
|
|||
let sensitive = media.length && !!contentWarning
|
||||
let mediaIds = media.map(_ => _.data.id)
|
||||
let mediaDescriptions = media.map(_ => _.description)
|
||||
let mediaFocalPoints = media.map(_ => [_.focusX, _.focusY])
|
||||
let inReplyTo = inReplyToId || ((realm === 'home' || realm === 'dialog') ? null : realm)
|
||||
|
||||
if (overLimit || (!text && !media.length)) {
|
||||
|
@ -217,7 +218,8 @@
|
|||
/* no await */
|
||||
postStatus(realm, text, inReplyTo, mediaIds,
|
||||
sensitive, contentWarning, postPrivacyKey,
|
||||
mediaDescriptions, inReplyToUuid, pollToPost)
|
||||
mediaDescriptions, inReplyToUuid, pollToPost,
|
||||
mediaFocalPoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
<li class="compose-media compose-media-realm-{realm}">
|
||||
<li class="compose-media compose-media-realm-{realm}" aria-label={shortName}>
|
||||
<img
|
||||
alt=""
|
||||
class="{type === 'audio' ? 'audio-preview' : ''}"
|
||||
style="object-position: {objectPosition};"
|
||||
src={previewSrc}
|
||||
{alt}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="compose-media-delete">
|
||||
<button class="compose-media-delete-button"
|
||||
aria-label="Delete {shortName}"
|
||||
<div class="compose-media-buttons">
|
||||
<button class="compose-media-button compose-media-focal-button {type === 'audio' ? 'compose-media-hidden' : ''}"
|
||||
aria-hidden={type === 'audio'}
|
||||
aria-label="Change preview"
|
||||
title="Change preview"
|
||||
on:click="onSetFocalPoint()" >
|
||||
<SvgIcon className="compose-media-button-svg" href="#fa-crosshairs" />
|
||||
</button>
|
||||
<button class="compose-media-button compose-media-delete-button"
|
||||
aria-label="Delete"
|
||||
title="Delete"
|
||||
on:click="onDeleteMedia()" >
|
||||
<SvgIcon className="compose-media-delete-button-svg" href="#fa-times" />
|
||||
<SvgIcon className="compose-media-button-svg" href="#fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="compose-media-alt">
|
||||
|
@ -18,7 +28,9 @@
|
|||
ref:textarea
|
||||
bind:value=rawText
|
||||
></textarea>
|
||||
<label for="compose-media-input-{uuid}" class="sr-only">{label}</label>
|
||||
<label for="compose-media-input-{uuid}" class="sr-only">
|
||||
Describe for the visually impaired (image, video) or auditorily impaired (audio, video)
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<style>
|
||||
|
@ -34,7 +46,6 @@
|
|||
}
|
||||
.compose-media img {
|
||||
object-fit: contain;
|
||||
object-position: center center;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
@ -60,33 +71,38 @@
|
|||
.compose-media-alt-input:focus {
|
||||
background: var(--main-bg);
|
||||
}
|
||||
.compose-media-delete {
|
||||
.compose-media-buttons {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
margin: 2px;
|
||||
}
|
||||
.compose-media-delete-button {
|
||||
.compose-media-button {
|
||||
padding: 7px 10px 5px;
|
||||
background: var(--floating-button-bg);
|
||||
border: 1px solid var(--button-border);
|
||||
}
|
||||
.compose-media-delete-button:hover {
|
||||
.compose-media-button:hover {
|
||||
background: var(--floating-button-bg-hover);
|
||||
}
|
||||
.compose-media-delete-button:active {
|
||||
.compose-media-button:active {
|
||||
background: var(--floating-button-bg-active);
|
||||
}
|
||||
:global(.compose-media-delete-button-svg) {
|
||||
:global(.compose-media-button-svg) {
|
||||
fill: var(--button-text);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.compose-media-hidden {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.audio-preview {
|
||||
background: var(--audio-bg);
|
||||
}
|
||||
|
@ -113,6 +129,9 @@
|
|||
import SvgIcon from '../SvgIcon.html'
|
||||
import { autosize } from '../../_thirdparty/autosize/autosize'
|
||||
import { ONE_TRANSPARENT_PIXEL } from '../../_static/media'
|
||||
import { get } from '../../_utils/lodash-lite'
|
||||
import { coordsToPercent } from '../../_utils/coordsToPercent'
|
||||
import { importMediaFocalPointDialog } from '../dialog/asyncDialogs'
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
|
@ -124,24 +143,27 @@
|
|||
this.teardownAutosize()
|
||||
},
|
||||
data: () => ({
|
||||
rawText: ''
|
||||
rawText: '',
|
||||
focusX: 0,
|
||||
focusY: 0
|
||||
}),
|
||||
computed: {
|
||||
filename: ({ mediaItem }) => mediaItem.file && mediaItem.file.name,
|
||||
alt: ({ filename, mediaItem }) => (
|
||||
type: ({ mediaItem }) => mediaItem.data.type,
|
||||
shortName: ({ mediaItem }) => (
|
||||
// sometimes we no longer have the file, e.g. in a delete and redraft situation,
|
||||
// so fall back to the description if it was provided
|
||||
filename || mediaItem.description || ''
|
||||
get(mediaItem, ['file', 'name']) || get(mediaItem, ['description']) || 'media'
|
||||
),
|
||||
type: ({ mediaItem }) => mediaItem.data.type,
|
||||
shortName: ({ filename }) => filename || 'media',
|
||||
previewSrc: ({ mediaItem, type }) => (
|
||||
type === 'audio' ? ONE_TRANSPARENT_PIXEL : mediaItem.data.preview_url
|
||||
),
|
||||
label: ({ shortName }) => (
|
||||
`Describe ${shortName} for the visually impaired (image, video) or auditorily impaired (audio, video)`
|
||||
),
|
||||
uuid: ({ realm, mediaItem }) => `${realm}-${mediaItem.data.id}`
|
||||
uuid: ({ realm, mediaItem }) => `${realm}-${mediaItem.data.id}`,
|
||||
objectPosition: ({ focusX, focusY }) => {
|
||||
if (!focusX && !focusY) {
|
||||
return 'center center'
|
||||
}
|
||||
return `${coordsToPercent(focusX)}% ${100 - coordsToPercent(focusY)}%`
|
||||
}
|
||||
},
|
||||
store: () => store,
|
||||
methods: {
|
||||
|
@ -150,10 +172,13 @@
|
|||
this.observe('media', media => {
|
||||
media = media || []
|
||||
let { index, rawText } = this.get()
|
||||
let text = (media[index] && media[index].description) || ''
|
||||
let text = get(media, [index, 'description'], '')
|
||||
if (rawText !== text) {
|
||||
this.set({ rawText: text })
|
||||
}
|
||||
let focusX = get(media, [index, 'focusX'], 0)
|
||||
let focusY = get(media, [index, 'focusY'], 0)
|
||||
this.set({ focusX, focusY })
|
||||
})
|
||||
},
|
||||
setupSyncToStore () {
|
||||
|
@ -161,12 +186,11 @@
|
|||
|
||||
this.observe('rawText', rawText => {
|
||||
let { realm, index, media } = this.get()
|
||||
if (media[index].description === rawText) {
|
||||
return
|
||||
if (media[index].description !== rawText) {
|
||||
media[index].description = rawText
|
||||
this.store.setComposeData(realm, { media })
|
||||
saveStore()
|
||||
}
|
||||
media[index].description = rawText
|
||||
this.store.setComposeData(realm, { media })
|
||||
saveStore()
|
||||
}, { init: false })
|
||||
},
|
||||
setupAutosize () {
|
||||
|
@ -176,11 +200,13 @@
|
|||
autosize.destroy(this.refs.textarea)
|
||||
},
|
||||
onDeleteMedia () {
|
||||
let {
|
||||
realm,
|
||||
index
|
||||
} = this.get()
|
||||
let { realm, index } = this.get()
|
||||
deleteMedia(realm, index)
|
||||
},
|
||||
async onSetFocalPoint () {
|
||||
let { realm, index } = this.get()
|
||||
let showMediaFocalPointDialog = await importMediaFocalPointDialog()
|
||||
showMediaFocalPointDialog(realm, index)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -43,3 +43,7 @@ export const importShowMuteDialog = () => import(
|
|||
export const importShowReportDialog = () => import(
|
||||
/* webpackChunkName: 'showReportDialog' */ './creators/showReportDialog'
|
||||
).then(getDefault)
|
||||
|
||||
export const importMediaFocalPointDialog = () => import(
|
||||
/* webpackChunkName: 'mediaFocalPointDialog' */ './creators/mediaFocalPointDialog'
|
||||
).then(getDefault)
|
||||
|
|
|
@ -0,0 +1,326 @@
|
|||
<ModalDialog
|
||||
{id}
|
||||
{label}
|
||||
{title}
|
||||
background="var(--main-bg)"
|
||||
className="media-focal-point-dialog"
|
||||
on:show="measure()"
|
||||
>
|
||||
<form class="media-focal-point-container"
|
||||
aria-label="Enter the focal point (X, Y) for this media"
|
||||
on:resize="measure()"
|
||||
>
|
||||
<div class="media-focal-point-image-container" ref:container>
|
||||
<img
|
||||
{intrinsicsize}
|
||||
class="media-focal-point-image"
|
||||
src={previewSrc}
|
||||
alt={shortName}
|
||||
on:load="onImageLoad()"
|
||||
/>
|
||||
<div class="media-focal-point-backdrop"></div>
|
||||
<div class="media-draggable-area"
|
||||
style={draggableAreaStyle}
|
||||
>
|
||||
<!-- 52px == 32px icon width + 10px padding -->
|
||||
<Draggable
|
||||
draggableClass="media-draggable-area-inner"
|
||||
indicatorClass="media-focal-point-indicator {imageLoaded ? '': 'hidden'}"
|
||||
indicatorWidth={52}
|
||||
indicatorHeight={52}
|
||||
x={indicatorX}
|
||||
y={indicatorY}
|
||||
on:change="onDraggableChange(event)"
|
||||
>
|
||||
<SvgIcon
|
||||
className="media-focal-point-indicator-svg"
|
||||
href="#fa-crosshairs"
|
||||
/>
|
||||
</Draggable>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-focal-point-inputs">
|
||||
<div class="media-focal-point-input-pair">
|
||||
<label for="media-focal-point-x-input-{realm}">
|
||||
X coordinate
|
||||
</label>
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
min="-1"
|
||||
max="1"
|
||||
inputmode="decimal"
|
||||
placeholder="0"
|
||||
id="media-focal-point-x-input-{realm}"
|
||||
bind:value="rawFocusX"
|
||||
/>
|
||||
</div>
|
||||
<div class="media-focal-point-input-pair">
|
||||
<label for="media-focal-point-y-input-{realm}">
|
||||
Y coordinate
|
||||
</label>
|
||||
<input type="number"
|
||||
step="0.01"
|
||||
min="-1"
|
||||
max="1"
|
||||
inputmode="decimal"
|
||||
placeholder="0"
|
||||
id="media-focal-point-y-input-{realm}"
|
||||
bind:value="rawFocusY"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ModalDialog>
|
||||
<style>
|
||||
:global(.media-focal-point-dialog) {
|
||||
max-width: calc(100%);
|
||||
}
|
||||
.media-focal-point-container {
|
||||
height: calc(100% - 44px); /* 44px X button height */
|
||||
width: calc(100vw - 40px);
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.media-focal-point-image-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
}
|
||||
.media-focal-point-image {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.media-focal-point-backdrop {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@supports (-webkit-backdrop-filter: blur(1px) saturate(1%)) or (backdrop-filter: blur(1px) saturate(1%)) {
|
||||
.media-focal-point-backdrop {
|
||||
-webkit-backdrop-filter: blur(2px) saturate(110%);
|
||||
backdrop-filter: blur(2px) saturate(110%);
|
||||
background-color: var(--focal-img-backdrop-filter);
|
||||
}
|
||||
}
|
||||
|
||||
@supports not ((-webkit-backdrop-filter: blur(1px) saturate(1%)) or (backdrop-filter: blur(1px) saturate(1%))) {
|
||||
.media-focal-point-backdrop {
|
||||
background-color: var(--focal-img-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.media-focal-point-inputs {
|
||||
display: flex;
|
||||
padding: 20px 40px;
|
||||
justify-content: space-around;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.media-focal-point-input-pair {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.media-focal-point-input-pair input {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.media-draggable-area {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
:global(.media-focal-point-indicator) {
|
||||
background: var(--focal-bg);
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:global(.media-draggable-area-inner) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.media-focal-point-indicator-svg) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 10px;
|
||||
fill: var(--focal-color);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.media-focal-point-inputs {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.media-focal-point-input-pair {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.media-focal-point-input-pair input {
|
||||
margin-left: 0;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import ModalDialog from './ModalDialog.html'
|
||||
import { show } from '../helpers/showDialog'
|
||||
import { close } from '../helpers/closeDialog'
|
||||
import { oncreate as onCreateDialog } from '../helpers/onCreateDialog'
|
||||
import { store } from '../../../_store/store'
|
||||
import { get } from '../../../_utils/lodash-lite'
|
||||
import { observe } from 'svelte-extras'
|
||||
import debounce from 'lodash-es/debounce'
|
||||
import { scheduleIdleTask } from '../../../_utils/scheduleIdleTask'
|
||||
import { coordsToPercent, percentToCoords } from '../../../_utils/coordsToPercent'
|
||||
import SvgIcon from '../../SvgIcon.html'
|
||||
import { intrinsicScale } from '../../../_thirdparty/intrinsic-scale/intrinsicScale'
|
||||
import { resize } from '../../../_utils/events'
|
||||
import Draggable from '../../Draggable.html'
|
||||
|
||||
const parseAndValidateFloat = rawText => {
|
||||
let float = parseFloat(rawText)
|
||||
if (Number.isNaN(float)) {
|
||||
float = 0
|
||||
}
|
||||
float = Math.min(1, float)
|
||||
float = Math.max(-1, float)
|
||||
float = Math.round(float * 100) / 100
|
||||
return float
|
||||
}
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
onCreateDialog.call(this)
|
||||
this.setupSyncFromStore()
|
||||
this.setupSyncToStore()
|
||||
},
|
||||
components: {
|
||||
ModalDialog,
|
||||
SvgIcon,
|
||||
Draggable
|
||||
},
|
||||
data: () => ({
|
||||
rawFocusX: '0',
|
||||
rawFocusY: '0',
|
||||
containerWidth: 0,
|
||||
containerHeight: 0,
|
||||
imageLoaded: false
|
||||
}),
|
||||
store: () => store,
|
||||
computed: {
|
||||
media: ({ $currentInstance, $composeData, realm }) => (
|
||||
get($composeData, [$currentInstance, realm, 'media'])
|
||||
),
|
||||
mediaItem: ({ media, index }) => get(media, [index]),
|
||||
focusX: ({ mediaItem }) => get(mediaItem, ['focusX'], 0),
|
||||
focusY: ({ mediaItem }) => get(mediaItem, ['focusY'], 0),
|
||||
previewSrc: ({ mediaItem }) => mediaItem.data.preview_url,
|
||||
nativeWidth: ({ mediaItem }) => get(mediaItem, ['data', 'meta', 'original', 'width'], 300),
|
||||
nativeHeight: ({ mediaItem }) => get(mediaItem, ['data', 'meta', 'original', 'height'], 200),
|
||||
shortName: ({ mediaItem }) => (
|
||||
// sometimes we no longer have the file, e.g. in a delete and redraft situation,
|
||||
// so fall back to the description if it was provided
|
||||
get(mediaItem, ['file', 'name']) || get(mediaItem, ['description']) || 'media'
|
||||
),
|
||||
intrinsicsize: ({ mediaItem }) => {
|
||||
let width = get(mediaItem, ['data', 'meta', 'original', 'width'])
|
||||
let height = get(mediaItem, ['data', 'meta', 'original', 'height'])
|
||||
if (width && height) {
|
||||
return `${width} x ${height}`
|
||||
}
|
||||
return '' // pleroma does not give us original width/height
|
||||
},
|
||||
scale: ({ nativeWidth, nativeHeight, containerWidth, containerHeight }) => (
|
||||
intrinsicScale(containerWidth, containerHeight, nativeWidth, nativeHeight)
|
||||
),
|
||||
scaleWidth: ({ scale }) => scale.width,
|
||||
scaleHeight: ({ scale }) => scale.height,
|
||||
scaleX: ({ scale }) => scale.x,
|
||||
scaleY: ({ scale }) => scale.y,
|
||||
indicatorX: ({ focusX }) => (coordsToPercent(focusX) / 100),
|
||||
indicatorY: ({ focusY }) => ((100 - coordsToPercent(focusY)) / 100),
|
||||
draggableAreaStyle: ({ scaleWidth, scaleHeight, scaleX, scaleY }) => (
|
||||
`top: ${scaleY}px; left: ${scaleX}px; width: ${scaleWidth}px; height: ${scaleHeight}px;`
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
observe,
|
||||
show,
|
||||
close,
|
||||
setupSyncFromStore () {
|
||||
this.observe('mediaItem', mediaItem => {
|
||||
let { rawFocusX, rawFocusY } = this.get()
|
||||
|
||||
const syncFromStore = (rawKey, rawFocus, key) => {
|
||||
let focus = get(mediaItem, [key], 0) || 0
|
||||
let focusAsString = focus.toString()
|
||||
if (focusAsString !== rawFocus) {
|
||||
this.set({ [rawKey]: focusAsString })
|
||||
}
|
||||
}
|
||||
|
||||
syncFromStore('rawFocusX', rawFocusX, 'focusX')
|
||||
syncFromStore('rawFocusY', rawFocusY, 'focusY')
|
||||
})
|
||||
},
|
||||
setupSyncToStore () {
|
||||
const saveStore = debounce(() => scheduleIdleTask(() => this.store.save()), 1000)
|
||||
|
||||
const observeAndSync = (rawKey, key) => {
|
||||
this.observe(rawKey, rawFocus => {
|
||||
let { realm, index, media } = this.get()
|
||||
let rawFocusDecimal = parseAndValidateFloat(rawFocus)
|
||||
if (media[index][key] !== rawFocusDecimal) {
|
||||
media[index][key] = rawFocusDecimal
|
||||
this.store.setComposeData(realm, { media })
|
||||
saveStore()
|
||||
}
|
||||
}, { init: false })
|
||||
}
|
||||
|
||||
observeAndSync('rawFocusX', 'focusX')
|
||||
observeAndSync('rawFocusY', 'focusY')
|
||||
},
|
||||
onDraggableChange ({ x, y }) {
|
||||
const saveStore = debounce(() => scheduleIdleTask(() => this.store.save()), 1000)
|
||||
|
||||
scheduleIdleTask(() => {
|
||||
let focusX = percentToCoords(x * 100)
|
||||
let focusY = percentToCoords(100 - (y * 100))
|
||||
let { realm, index, media } = this.get()
|
||||
media[index].focusX = parseAndValidateFloat(focusX)
|
||||
media[index].focusY = parseAndValidateFloat(focusY)
|
||||
this.store.setComposeData(realm, { media })
|
||||
saveStore()
|
||||
})
|
||||
},
|
||||
measure () {
|
||||
requestAnimationFrame(() => {
|
||||
if (!this.refs.container) {
|
||||
return
|
||||
}
|
||||
let rect = this.refs.container.getBoundingClientRect()
|
||||
this.set({
|
||||
containerWidth: rect.width,
|
||||
containerHeight: rect.height
|
||||
})
|
||||
})
|
||||
},
|
||||
onImageLoad () {
|
||||
this.measure()
|
||||
this.set({ imageLoaded: true })
|
||||
}
|
||||
},
|
||||
events: {
|
||||
resize
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -69,6 +69,8 @@
|
|||
}
|
||||
</style>
|
||||
<script>
|
||||
import { get } from '../../../_utils/lodash-lite'
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
type: ({ media }) => media.type,
|
||||
|
@ -77,8 +79,9 @@
|
|||
poster: ({ media }) => media.preview_url,
|
||||
static_url: ({ media }) => media.static_url,
|
||||
intrinsicsize: ({ media }) => {
|
||||
if (media.meta && media.meta.original && media.meta.original.width && media.meta.original.height) {
|
||||
let { width, height } = media.meta.original
|
||||
let width = get(media, ['meta', 'original', 'width'])
|
||||
let height = get(media, ['meta', 'original', 'height'])
|
||||
if (width && height) {
|
||||
return `${width} x ${height}`
|
||||
}
|
||||
return '' // pleroma does not give us original width/height
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import MediaFocalPointDialog from '../components/MediaFocalPointDialog.html'
|
||||
import { showDialog } from './showDialog'
|
||||
|
||||
export default function showMediaFocalPointDialog (realm, index) {
|
||||
return showDialog(MediaFocalPointDialog, {
|
||||
label: 'Change preview dialog',
|
||||
title: 'Change preview (focal point)',
|
||||
realm,
|
||||
index
|
||||
})
|
||||
}
|
21
src/routes/_thirdparty/intrinsic-scale/LICENSE
vendored
Normal file
21
src/routes/_thirdparty/intrinsic-scale/LICENSE
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) Federico Brigante <bfred-it@users.noreply.github.com> (twitter.com/bfred_it)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
21
src/routes/_thirdparty/intrinsic-scale/intrinsicScale.js
vendored
Normal file
21
src/routes/_thirdparty/intrinsic-scale/intrinsicScale.js
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
// via https://github.com/bfred-it/intrinsic-scale/blob/3d058f79902653484092ad9a2f3e1d9a3d03f09e/index.js
|
||||
|
||||
export const intrinsicScale = (parentWidth, parentHeight, childWidth, childHeight) => {
|
||||
const doRatio = childWidth / childHeight
|
||||
const cRatio = parentWidth / parentHeight
|
||||
let width = parentWidth
|
||||
let height = parentHeight
|
||||
|
||||
if (doRatio > cRatio) {
|
||||
height = width / doRatio
|
||||
} else {
|
||||
width = height * doRatio
|
||||
}
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
x: (parentWidth - width) / 2,
|
||||
y: (parentHeight - height) / 2
|
||||
}
|
||||
}
|
|
@ -13,3 +13,7 @@ export const importIndexedDBGetAllShim = () => import(
|
|||
export const importCustomElementsPolyfill = () => import(
|
||||
/* webpackChunkName: '$polyfill$-@webcomponents/custom-elements' */ '@webcomponents/custom-elements'
|
||||
)
|
||||
|
||||
export const importPointerEventsPolyfill = () => import(
|
||||
/* webpackChunkName: '$polyfill$-@wessberg/pointer-events' */ '@wessberg/pointer-events'
|
||||
)
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
export const coordsToPercent = coord => (1 + coord) / 2 * 100
|
||||
export const coordsToPercent = coord => ((1 + coord) / 2) * 100
|
||||
|
||||
export const percentToCoords = percent => ((percent / 100) * 2) - 1
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { registerResizeListener, unregisterResizeListener } from './resize'
|
||||
|
||||
export function mouseover (node, callback) {
|
||||
function onMouseEnter () {
|
||||
callback(true) // eslint-disable-line
|
||||
|
@ -71,3 +73,13 @@ export function applyFocusStylesToParent (node) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resize (node, callback) {
|
||||
registerResizeListener(callback)
|
||||
|
||||
return {
|
||||
destroy () {
|
||||
unregisterResizeListener(callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@ import {
|
|||
importCustomElementsPolyfill,
|
||||
importIndexedDBGetAllShim,
|
||||
importIntersectionObserver,
|
||||
importRequestIdleCallback
|
||||
importRequestIdleCallback,
|
||||
importPointerEventsPolyfill
|
||||
} from './asyncPolyfills'
|
||||
|
||||
export function loadPolyfills () {
|
||||
|
@ -10,6 +11,7 @@ export function loadPolyfills () {
|
|||
typeof IntersectionObserver === 'undefined' && importIntersectionObserver(),
|
||||
typeof requestIdleCallback === 'undefined' && importRequestIdleCallback(),
|
||||
!IDBObjectStore.prototype.getAll && importIndexedDBGetAllShim(),
|
||||
typeof customElements === 'undefined' && importCustomElementsPolyfill()
|
||||
typeof customElements === 'undefined' && importCustomElementsPolyfill(),
|
||||
typeof PointerEvent === 'undefined' && importPointerEventsPolyfill()
|
||||
])
|
||||
}
|
||||
|
|
18
src/routes/_utils/throttleRaf.js
Normal file
18
src/routes/_utils/throttleRaf.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
// ensure callback is only executed once per raf
|
||||
export const throttleRaf = () => {
|
||||
let rafCallback
|
||||
let rafQueued
|
||||
|
||||
return function throttledRaf (callback) {
|
||||
rafCallback = callback
|
||||
if (!rafQueued) {
|
||||
rafQueued = true
|
||||
requestAnimationFrame(() => {
|
||||
let cb = rafCallback
|
||||
rafCallback = null
|
||||
rafQueued = false
|
||||
cb()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -120,4 +120,9 @@
|
|||
--length-indicator-color: #{$main-theme-color};
|
||||
|
||||
--audio-bg: #{rgba(30, 30, 30, 0.8)};
|
||||
|
||||
--focal-img-backdrop-filter: #{rgba($main-bg-color, 0.5)};
|
||||
--focal-img-bg: #{rgba($main-bg-color, 0.3)};
|
||||
--focal-bg: #{rgba($toast-bg, 0.8)};
|
||||
--focal-color: #{$secondary-text-color};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import {
|
||||
composeInput, getNthDeleteMediaButton, getNthMedia, getNthMediaAltInput, homeNavButton, mediaButton,
|
||||
settingsNavButton, sleep,
|
||||
composeInput,
|
||||
getNthDeleteMediaButton,
|
||||
getNthMedia,
|
||||
getNthMediaAltInput,
|
||||
getNthMediaListItem,
|
||||
homeNavButton,
|
||||
mediaButton,
|
||||
settingsNavButton,
|
||||
sleep,
|
||||
uploadKittenImage
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
|
@ -13,21 +20,21 @@ test('inserts media', async t => {
|
|||
await t
|
||||
.expect(mediaButton.hasAttribute('disabled')).notOk()
|
||||
await (uploadKittenImage(1)())
|
||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
await (uploadKittenImage(2)())
|
||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
||||
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
.expect(mediaButton.hasAttribute('disabled')).notOk()
|
||||
await (uploadKittenImage(3)())
|
||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
||||
.expect(getNthMedia(3).getAttribute('alt')).eql('kitten3.jpg')
|
||||
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
.expect(getNthMediaListItem(3).getAttribute('aria-label')).eql('kitten3.jpg')
|
||||
.expect(mediaButton.hasAttribute('disabled')).notOk()
|
||||
await (uploadKittenImage(4)())
|
||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
||||
.expect(getNthMedia(3).getAttribute('alt')).eql('kitten3.jpg')
|
||||
.expect(getNthMedia(4).getAttribute('alt')).eql('kitten4.jpg')
|
||||
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
.expect(getNthMediaListItem(3).getAttribute('aria-label')).eql('kitten3.jpg')
|
||||
.expect(getNthMediaListItem(4).getAttribute('aria-label')).eql('kitten4.jpg')
|
||||
.expect(mediaButton.getAttribute('disabled')).eql('')
|
||||
.click(getNthDeleteMediaButton(4))
|
||||
.click(getNthDeleteMediaButton(3))
|
||||
|
@ -41,10 +48,10 @@ test('removes media', async t => {
|
|||
await t
|
||||
.expect(mediaButton.exists).ok()
|
||||
await (uploadKittenImage(1)())
|
||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
await (uploadKittenImage(2)())
|
||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
||||
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
.click(getNthDeleteMediaButton(2))
|
||||
.expect(getNthMedia(2).exists).notOk()
|
||||
.expect(getNthMedia(1).exists).ok()
|
||||
|
@ -79,11 +86,11 @@ test('keeps media descriptions as media is removed', async t => {
|
|||
.typeText(getNthMediaAltInput(2), 'kitten numero dos')
|
||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
||||
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
||||
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
.click(getNthDeleteMediaButton(1))
|
||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero dos')
|
||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten2.jpg')
|
||||
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
})
|
||||
|
||||
test('keeps media in local storage', async t => {
|
||||
|
@ -101,8 +108,8 @@ test('keeps media in local storage', async t => {
|
|||
.expect(composeInput.value).eql('hello hello')
|
||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
||||
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
||||
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
await sleep(1)
|
||||
await t
|
||||
.click(settingsNavButton)
|
||||
|
@ -110,12 +117,12 @@ test('keeps media in local storage', async t => {
|
|||
.expect(composeInput.value).eql('hello hello')
|
||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
||||
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
||||
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
.navigateTo('/')
|
||||
.expect(composeInput.value).eql('hello hello')
|
||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
||||
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
||||
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
})
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
import {
|
||||
composeButton, composeInput, getNthDeleteMediaButton, getNthMedia, getNthMediaAltInput, getNthStatusAndImage, getUrl,
|
||||
composeButton,
|
||||
composeInput,
|
||||
getNthDeleteMediaButton,
|
||||
getNthMedia,
|
||||
getNthMediaAltInput,
|
||||
getNthMediaListItem,
|
||||
getNthStatusAndImage,
|
||||
getUrl,
|
||||
homeNavButton,
|
||||
mediaButton, notificationsNavButton,
|
||||
mediaButton,
|
||||
notificationsNavButton,
|
||||
uploadKittenImage
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
|
@ -11,10 +19,10 @@ fixture`109-compose-media.js`
|
|||
|
||||
async function uploadTwoKittens (t) {
|
||||
await (uploadKittenImage(1)())
|
||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
await (uploadKittenImage(2)())
|
||||
await t.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
||||
await t.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
}
|
||||
|
||||
test('uploads alts for media', async t => {
|
||||
|
@ -25,10 +33,10 @@ test('uploads alts for media', async t => {
|
|||
await t.typeText(getNthMediaAltInput(2), 'kitten 2')
|
||||
.typeText(getNthMediaAltInput(1), 'kitten 1')
|
||||
.click(composeButton)
|
||||
.expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('kitten 1')
|
||||
.expect(getNthStatusAndImage(1, 0).getAttribute('title')).eql('kitten 1')
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten 2')
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('title')).eql('kitten 2')
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten 1')
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('title')).eql('kitten 1')
|
||||
.expect(getNthStatusAndImage(1, 2).getAttribute('alt')).eql('kitten 2')
|
||||
.expect(getNthStatusAndImage(1, 2).getAttribute('title')).eql('kitten 2')
|
||||
})
|
||||
|
||||
test('uploads alts when deleting and re-uploading media', async t => {
|
||||
|
@ -41,10 +49,10 @@ test('uploads alts when deleting and re-uploading media', async t => {
|
|||
.expect(getNthMedia(1).exists).notOk()
|
||||
await (uploadKittenImage(2)())
|
||||
await t.expect(getNthMediaAltInput(1).value).eql('')
|
||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten2.jpg')
|
||||
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
.typeText(getNthMediaAltInput(1), 'this will not be deleted')
|
||||
.click(composeButton)
|
||||
.expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('this will not be deleted')
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('this will not be deleted')
|
||||
})
|
||||
|
||||
test('uploads alts mixed with no-alts', async t => {
|
||||
|
@ -54,8 +62,8 @@ test('uploads alts mixed with no-alts', async t => {
|
|||
await uploadTwoKittens(t)
|
||||
await t.typeText(getNthMediaAltInput(2), 'kitten numero dos')
|
||||
.click(composeButton)
|
||||
.expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('')
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten numero dos')
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('')
|
||||
.expect(getNthStatusAndImage(1, 2).getAttribute('alt')).eql('kitten numero dos')
|
||||
})
|
||||
|
||||
test('saves alts to local storage', async t => {
|
||||
|
@ -69,13 +77,13 @@ test('saves alts to local storage', async t => {
|
|||
.expect(getUrl()).contains('/notifications')
|
||||
.click(homeNavButton)
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.expect(getNthMedia(1).getAttribute('alt')).eql('kitten1.jpg')
|
||||
.expect(getNthMedia(2).getAttribute('alt')).eql('kitten2.jpg')
|
||||
.expect(getNthMediaListItem(1).getAttribute('aria-label')).eql('kitten1.jpg')
|
||||
.expect(getNthMediaListItem(2).getAttribute('aria-label')).eql('kitten2.jpg')
|
||||
.expect(getNthMediaAltInput(1).value).eql('kitten numero uno')
|
||||
.expect(getNthMediaAltInput(2).value).eql('kitten numero dos')
|
||||
.click(composeButton)
|
||||
.expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('kitten numero uno')
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten numero dos')
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten numero uno')
|
||||
.expect(getNthStatusAndImage(1, 2).getAttribute('alt')).eql('kitten numero dos')
|
||||
})
|
||||
|
||||
test('can post a status with empty content if there is media', async t => {
|
||||
|
@ -87,5 +95,5 @@ test('can post a status with empty content if there is media', async t => {
|
|||
await t
|
||||
.typeText(getNthMediaAltInput(1), 'just an image!')
|
||||
await t.click(composeButton)
|
||||
.expect(getNthStatusAndImage(1, 0).getAttribute('alt')).eql('just an image!')
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('just an image!')
|
||||
})
|
||||
|
|
|
@ -8,12 +8,11 @@ import {
|
|||
composeModalInput,
|
||||
getNthStatusMediaImg,
|
||||
composeModalPostPrivacyButton,
|
||||
getComposeModalNthMediaImg,
|
||||
getComposeModalNthMediaAltInput,
|
||||
getNthStatusSpoiler,
|
||||
composeModalContentWarningInput,
|
||||
dialogOptionsOption,
|
||||
getNthReplyButton, getNthComposeReplyInput, getNthComposeReplyButton, getUrl, sleep
|
||||
getNthReplyButton, getNthComposeReplyInput, getNthComposeReplyButton, getUrl, sleep, getComposeModalNthMediaListItem
|
||||
} from '../utils'
|
||||
import { postAs, postEmptyStatusWithMediaAs, postWithSpoilerAndPrivacyAs } from '../serverActions'
|
||||
|
||||
|
@ -48,7 +47,7 @@ test('image with empty text delete and redraft', async t => {
|
|||
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
|
||||
.expect(composeModalInput.value).eql('')
|
||||
.expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
|
||||
.expect(getComposeModalNthMediaImg(1).getAttribute('alt')).eql('what a kitteh')
|
||||
.expect(getComposeModalNthMediaListItem(1).getAttribute('aria-label')).eql('what a kitteh')
|
||||
.expect(getComposeModalNthMediaAltInput(1).value).eql('what a kitteh')
|
||||
.typeText(composeModalInput, 'I love this kitteh', { replace: true, paste: true })
|
||||
.click(composeModalComposeButton)
|
||||
|
@ -68,7 +67,7 @@ test('image with no alt delete and redraft', async t => {
|
|||
.expect(modalDialog.hasAttribute('aria-hidden')).notOk()
|
||||
.expect(composeModalInput.value).eql('')
|
||||
.expect(composeModalPostPrivacyButton.getAttribute('aria-label')).eql('Adjust privacy (currently Public)')
|
||||
.expect(getComposeModalNthMediaImg(1).getAttribute('alt')).eql('')
|
||||
.expect(getComposeModalNthMediaListItem(1).getAttribute('aria-label')).eql('media')
|
||||
.expect(getComposeModalNthMediaAltInput(1).value).eql('')
|
||||
.typeText(composeModalInput, 'oops forgot an alt', { replace: true, paste: true })
|
||||
.typeText(getComposeModalNthMediaAltInput(1), 'lovely kitteh', { replace: true, paste: true })
|
||||
|
|
59
tests/spec/130-focal-point.js
Normal file
59
tests/spec/130-focal-point.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {
|
||||
sleep,
|
||||
composeInput,
|
||||
mediaButton,
|
||||
uploadKittenImage,
|
||||
getNthMediaAltInput,
|
||||
getNthMediaFocalPointButton,
|
||||
modalDialog,
|
||||
focalPointXInput,
|
||||
closeDialogButton,
|
||||
composeButton,
|
||||
focalPointYInput,
|
||||
getNthStatusContent,
|
||||
getNthStatusAndImage
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
|
||||
fixture`130-focal-point.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
test('Can set a focal point', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await t
|
||||
.typeText(composeInput, 'here is a focal point')
|
||||
.click(mediaButton)
|
||||
await (uploadKittenImage(1)())
|
||||
await (uploadKittenImage(2)())
|
||||
await (uploadKittenImage(3)())
|
||||
await t
|
||||
.typeText(getNthMediaAltInput(1), 'kitten 1')
|
||||
.typeText(getNthMediaAltInput(2), 'kitten 2')
|
||||
.click(getNthMediaFocalPointButton(2))
|
||||
.expect(modalDialog.hasAttribute('aria-hidden')).notOk({ timeout: 30000 })
|
||||
.typeText(focalPointXInput, '0.5')
|
||||
await sleep(1000)
|
||||
await t
|
||||
.click(closeDialogButton)
|
||||
.expect(modalDialog.exists).notOk()
|
||||
await sleep(1000)
|
||||
await t
|
||||
.click(getNthMediaFocalPointButton(3))
|
||||
.expect(modalDialog.hasAttribute('aria-hidden')).notOk({ timeout: 30000 })
|
||||
.typeText(focalPointXInput, '-0.25')
|
||||
.typeText(focalPointYInput, '1')
|
||||
await sleep(1000)
|
||||
await t
|
||||
.click(closeDialogButton)
|
||||
.expect(modalDialog.exists).notOk()
|
||||
await sleep(1000)
|
||||
await t
|
||||
.click(composeButton)
|
||||
.expect(getNthStatusContent(1).innerText).contains('here is a focal point', { timeout: 30000 })
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('alt')).eql('kitten 1')
|
||||
.expect(getNthStatusAndImage(1, 2).getAttribute('alt')).eql('kitten 2')
|
||||
.expect(getNthStatusAndImage(1, 3).getAttribute('alt')).eql('')
|
||||
.expect(getNthStatusAndImage(1, 1).getAttribute('style')).eql('object-position: 50% 50%;')
|
||||
.expect(getNthStatusAndImage(1, 2).getAttribute('style')).eql('object-position: 75% 50%;')
|
||||
.expect(getNthStatusAndImage(1, 3).getAttribute('style')).eql('object-position: 62.5% 0%;')
|
||||
})
|
|
@ -85,12 +85,15 @@ export const instanceSettingNotificationMentions = $('#instance-option-notificat
|
|||
|
||||
export const notificationBadge = $('#main-nav li:nth-child(2) .nav-link-badge')
|
||||
|
||||
export const focalPointXInput = $('.media-focal-point-inputs *:nth-child(1) input')
|
||||
export const focalPointYInput = $('.media-focal-point-inputs *:nth-child(2) input')
|
||||
|
||||
export function getComposeModalNthMediaAltInput (n) {
|
||||
return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt textarea`)
|
||||
}
|
||||
|
||||
export function getComposeModalNthMediaImg (n) {
|
||||
return $(`.modal-dialog .compose-media:nth-child(${n}) img`)
|
||||
export function getComposeModalNthMediaListItem (n) {
|
||||
return $(`.modal-dialog .compose-media:nth-child(${n})`)
|
||||
}
|
||||
|
||||
export const favoritesCountElement = $('.status-favs').addCustomDOMProperties({
|
||||
|
@ -278,6 +281,10 @@ export function getNthSearchResult (n) {
|
|||
return $(`.search-result:nth-child(${n}) a`)
|
||||
}
|
||||
|
||||
export function getNthMediaListItem (n) {
|
||||
return $(`.compose-media:nth-child(${n})`)
|
||||
}
|
||||
|
||||
export function getNthMedia (n) {
|
||||
return $(`.compose-media:nth-child(${n}) img`)
|
||||
}
|
||||
|
@ -286,6 +293,10 @@ export function getNthDeleteMediaButton (n) {
|
|||
return $(`.compose-media:nth-child(${n}) .compose-media-delete-button`)
|
||||
}
|
||||
|
||||
export function getNthMediaFocalPointButton (n) {
|
||||
return $(`.compose-media:nth-child(${n}) .compose-media-focal-button`)
|
||||
}
|
||||
|
||||
export function getAriaSetSize () {
|
||||
return getNthStatus(1 + 0).getAttribute('aria-setsize')
|
||||
}
|
||||
|
@ -331,7 +342,7 @@ export function getNthStatusHeader (n) {
|
|||
}
|
||||
|
||||
export function getNthStatusAndImage (nStatus, nImage) {
|
||||
return $(`${getNthStatusSelector(nStatus)} .status-media .show-image-button:nth-child(${nImage + 1}) img`)
|
||||
return $(`${getNthStatusSelector(nStatus)} .status-media .show-image-button:nth-child(${nImage}) img`)
|
||||
}
|
||||
|
||||
export function getFirstVisibleStatus () {
|
||||
|
|
|
@ -352,6 +352,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@webcomponents/custom-elements/-/custom-elements-1.2.4.tgz#7074543155396114617722724d6f6cb7b3800a14"
|
||||
integrity sha512-WiTlgz6/kuwajYIcgyq64rSlCtb2AvbxwwrExP3wr6rKbJ72I3hi/sb4KdGUumfC+isDn2F0btZGk4MnWpyO1Q==
|
||||
|
||||
"@wessberg/pointer-events@^1.0.9":
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@wessberg/pointer-events/-/pointer-events-1.0.9.tgz#9591f2326e3592cdfa52d630217b97ecf7c5d817"
|
||||
integrity sha512-fTOBNzakyi3wBXMmQ1mpAibU3lCbXVWSwgM9nsjtlfbJ5MH1yvteCZgZF+TDQWNtTPg8JWuQUjBF7+8cKoj92A==
|
||||
|
||||
"@xtuc/ieee754@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||
|
|
Loading…
Reference in a new issue