forked from AkkomaGang/akkoma-fe
Compare commits
No commits in common. "c9dc8f00f9c574b082c6ae63c3f59777764837da" and "174f98b1cb0c3f506026f15f4efbde59739b8264" have entirely different histories.
c9dc8f00f9
...
174f98b1cb
6 changed files with 49 additions and 119 deletions
|
@ -13,7 +13,6 @@ const StillImage = {
|
||||||
return {
|
return {
|
||||||
stopGifs: this.$store.getters.mergedConfig.stopGifs || window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
stopGifs: this.$store.getters.mergedConfig.stopGifs || window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||||||
isAnimated: false,
|
isAnimated: false,
|
||||||
imageTypeLabel: ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -40,22 +39,27 @@ const StillImage = {
|
||||||
this.imageLoadError && this.imageLoadError()
|
this.imageLoadError && this.imageLoadError()
|
||||||
},
|
},
|
||||||
detectAnimation (image) {
|
detectAnimation (image) {
|
||||||
|
// If there are no file extensions, the mimetype isn't set, and no mediaproxy is available, we can't figure out
|
||||||
|
// the mimetype of the image.
|
||||||
|
const hasFileExtension = this.src.split('/').pop().includes('.') // TODO: Better check?
|
||||||
const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
|
const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
|
||||||
|
if (!hasFileExtension && this.mimetype === undefined && !mediaProxyAvailable) {
|
||||||
// harmless CORS errors without-- clean console with
|
|
||||||
if (!mediaProxyAvailable) {
|
|
||||||
// It's a bit aggressive to assume all images we can't find the mimetype of is animated, but necessary for
|
// It's a bit aggressive to assume all images we can't find the mimetype of is animated, but necessary for
|
||||||
// people in need of reduced motion accessibility. As such, we'll consider those images animated if the user
|
// people in need of reduced motion accessibility. As such, we'll consider those images animated if the user
|
||||||
// agent is set to prefer reduced motion. Otherwise, it'll just be used as an early exit.
|
// agent is set to prefer reduced motion. Otherwise, it'll just be used as an early exit.
|
||||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches)
|
||||||
// Only for no media-proxy for now, since we're capturing gif, webp, and apng (are there others?)
|
|
||||||
// Since the canvas and images are not pixel-perfect matching (due to scaling),
|
|
||||||
// It makes the images jiggle on hover, which is not ideal for accessibility, methinks
|
|
||||||
this.isAnimated = true
|
this.isAnimated = true
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) {
|
||||||
|
this.isAnimated = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// harmless CORS errors without-- clean console with
|
||||||
|
if (!mediaProxyAvailable) return
|
||||||
|
// Animated JPEGs?
|
||||||
|
if (!(this.src.endsWith('.webp') || this.src.endsWith('.png'))) return
|
||||||
// Browser Cache should ensure image doesn't get loaded twice if cache exists
|
// Browser Cache should ensure image doesn't get loaded twice if cache exists
|
||||||
fetch(image.src, {
|
fetch(image.src, {
|
||||||
referrerPolicy: 'same-origin'
|
referrerPolicy: 'same-origin'
|
||||||
|
@ -64,20 +68,12 @@ const StillImage = {
|
||||||
// We don't need to read the whole file so only call it once
|
// We don't need to read the whole file so only call it once
|
||||||
data.body.getReader().read()
|
data.body.getReader().read()
|
||||||
.then(reader => {
|
.then(reader => {
|
||||||
// Ordered from least to most intensive
|
if (this.src.endsWith('.webp') && this.isAnimatedWEBP(reader.value)) {
|
||||||
if (this.isGIF(reader.value)) {
|
|
||||||
this.isAnimated = true
|
this.isAnimated = true
|
||||||
this.setLabel('GIF')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.isAnimatedWEBP(reader.value)) {
|
if (this.src.endsWith('.png') && this.isAnimatedPNG(reader.value)) {
|
||||||
this.isAnimated = true
|
this.isAnimated = true
|
||||||
this.setLabel('WEBP')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (this.isAnimatedPNG(reader.value)) {
|
|
||||||
this.isAnimated = true
|
|
||||||
this.setLabel('APNG')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -85,23 +81,6 @@ const StillImage = {
|
||||||
// this.imageLoadError && this.imageLoadError()
|
// this.imageLoadError && this.imageLoadError()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setLabel (name) {
|
|
||||||
this.imageTypeLabel = name;
|
|
||||||
},
|
|
||||||
isGIF (data) {
|
|
||||||
// I am a perfectly sane individual
|
|
||||||
//
|
|
||||||
// GIF HEADER CHUNK
|
|
||||||
// === START HEADER ===
|
|
||||||
// 47 49 46 38 ("GIF8")
|
|
||||||
const gifHeader = [0x47, 0x49, 0x46];
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
if (data[i] !== gifHeader[i]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
isAnimatedWEBP (data) {
|
isAnimatedWEBP (data) {
|
||||||
/**
|
/**
|
||||||
* WEBP HEADER CHUNK
|
* WEBP HEADER CHUNK
|
||||||
|
@ -135,55 +114,16 @@ const StillImage = {
|
||||||
const idatPos = str.indexOf('IDAT')
|
const idatPos = str.indexOf('IDAT')
|
||||||
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
|
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
|
||||||
},
|
},
|
||||||
drawThumbnail() {
|
drawThumbnail () {
|
||||||
const canvas = this.$refs.canvas;
|
const canvas = this.$refs.canvas
|
||||||
if (!canvas) return;
|
if (!this.$refs.canvas) return
|
||||||
|
const image = this.$refs.src
|
||||||
const context = canvas.getContext('2d');
|
const width = image.naturalWidth
|
||||||
const image = this.$refs.src;
|
const height = image.naturalHeight
|
||||||
const parentElement = canvas.parentElement;
|
canvas.width = width
|
||||||
|
canvas.height = height
|
||||||
// Draw the quick, unscaled version first
|
canvas.getContext('2d').drawImage(image, 0, 0, width, height)
|
||||||
context.drawImage(image, 0, 0, parentElement.clientWidth, parentElement.clientHeight);
|
}
|
||||||
|
|
||||||
// Use requestAnimationFrame to schedule the scaling to the next frame
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
// Compute scaling ratio between the natural dimensions of the image and its display dimensions
|
|
||||||
const scalingRatioWidth = parentElement.clientWidth / image.naturalWidth;
|
|
||||||
const scalingRatioHeight = parentElement.clientHeight / image.naturalHeight;
|
|
||||||
|
|
||||||
// Adjust for high-DPI displays
|
|
||||||
const ratio = window.devicePixelRatio || 1;
|
|
||||||
canvas.width = image.naturalWidth * scalingRatioWidth * ratio;
|
|
||||||
canvas.height = image.naturalHeight * scalingRatioHeight * ratio;
|
|
||||||
canvas.style.width = `${parentElement.clientWidth}px`;
|
|
||||||
canvas.style.height = `${parentElement.clientHeight}px`;
|
|
||||||
context.scale(ratio, ratio);
|
|
||||||
|
|
||||||
// Maintain the aspect ratio of the image
|
|
||||||
const imgAspectRatio = image.naturalWidth / image.naturalHeight;
|
|
||||||
const canvasAspectRatio = parentElement.clientWidth / parentElement.clientHeight;
|
|
||||||
|
|
||||||
let drawWidth, drawHeight;
|
|
||||||
|
|
||||||
if (imgAspectRatio > canvasAspectRatio) {
|
|
||||||
drawWidth = parentElement.clientWidth;
|
|
||||||
drawHeight = parentElement.clientWidth / imgAspectRatio;
|
|
||||||
} else {
|
|
||||||
drawHeight = parentElement.clientHeight;
|
|
||||||
drawWidth = parentElement.clientHeight * imgAspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.clearRect(0, 0, canvas.width, canvas.height); // Clear the previous unscaled image
|
|
||||||
context.imageSmoothingEnabled = true;
|
|
||||||
context.imageSmoothingQuality = 'high';
|
|
||||||
|
|
||||||
// Draw the good one for realsies
|
|
||||||
const dx = (parentElement.clientWidth - drawWidth) / 2;
|
|
||||||
const dy = (parentElement.clientHeight - drawHeight) / 2;
|
|
||||||
context.drawImage(image, dx, dy, drawWidth, drawHeight);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
updated () {
|
updated () {
|
||||||
// On computed animated change
|
// On computed animated change
|
||||||
|
|
|
@ -1,15 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="still-image"
|
|
||||||
class="still-image"
|
class="still-image"
|
||||||
:class="{ animated: animated }"
|
:class="{ animated: animated }"
|
||||||
:style="style"
|
:style="style"
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
v-if="animated && imageTypeLabel"
|
|
||||||
class="image-type-label">
|
|
||||||
{{ imageTypeLabel }}
|
|
||||||
</div>
|
|
||||||
<canvas
|
<canvas
|
||||||
v-if="animated"
|
v-if="animated"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
|
@ -63,26 +57,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-type-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.25em;
|
|
||||||
left: 0.25em;
|
|
||||||
line-height: 1;
|
|
||||||
font-size: 0.6em;
|
|
||||||
background: rgba(127, 127, 127, 0.5);
|
|
||||||
color: #fff;
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
|
||||||
z-index: 2;
|
|
||||||
visibility: var(--_still-image-label-visibility, visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.animated {
|
&.animated {
|
||||||
|
&::before {
|
||||||
|
zoom: var(--_still_image-label-scale, 1);
|
||||||
|
content: 'gif';
|
||||||
|
position: absolute;
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 0.7em;
|
||||||
|
top: 0.5em;
|
||||||
|
left: 0.5em;
|
||||||
|
background: rgba(127, 127, 127, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: $fallback--tooltipRadius;
|
||||||
|
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||||
|
z-index: 2;
|
||||||
|
visibility: var(--_still-image-label-visibility, visible);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover canvas {
|
&:hover canvas {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover .image-type-label {
|
&:hover::before {
|
||||||
visibility: var(--_still-image-label-visibility, hidden);
|
visibility: var(--_still-image-label-visibility, hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
--_avatarShadowBox: var(--avatarStatusShadow);
|
--_avatarShadowBox: var(--avatarStatusShadow);
|
||||||
--_avatarShadowFilter: var(--avatarStatusShadowFilter);
|
--_avatarShadowFilter: var(--avatarStatusShadowFilter);
|
||||||
--_avatarShadowInset: var(--avatarStatusShadowInset);
|
--_avatarShadowInset: var(--avatarStatusShadowInset);
|
||||||
// --_still-image-label-visibility: hidden;
|
--_still-image-label-visibility: hidden;
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -35,7 +35,6 @@ const loaders = {
|
||||||
sk: () => import('./sk.json'),
|
sk: () => import('./sk.json'),
|
||||||
te: () => import('./te.json'),
|
te: () => import('./te.json'),
|
||||||
uk: () => import('./uk.json'),
|
uk: () => import('./uk.json'),
|
||||||
vi: () => import('./vi.json'),
|
|
||||||
zh: () => import('./zh.json'),
|
zh: () => import('./zh.json'),
|
||||||
zh_Hant: () => import('./zh_Hant.json')
|
zh_Hant: () => import('./zh_Hant.json')
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,18 +37,11 @@ const recentEmojis = {
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
recentEmojis: (state, getters, rootState) => state.emojis.reduce((objects, displayText) => {
|
recentEmojis: (state, getters, rootState) => state.emojis.reduce((objects, displayText) => {
|
||||||
let comparator = emoji => emoji.displayText === displayText
|
const allEmojis = rootState.instance.emoji.concat(rootState.instance.customEmoji)
|
||||||
|
let emojiObject = allEmojis.find(emoji => emoji.displayText === displayText)
|
||||||
let emojiObject = rootState.instance.emoji.find(comparator)
|
|
||||||
if (emojiObject !== undefined) {
|
if (emojiObject !== undefined) {
|
||||||
objects.push(emojiObject)
|
objects.push(emojiObject)
|
||||||
} else {
|
|
||||||
emojiObject = rootState.instance.customEmoji.find(comparator)
|
|
||||||
if (emojiObject !== undefined) {
|
|
||||||
objects.push(emojiObject)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return objects
|
return objects
|
||||||
}, []),
|
}, []),
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,7 +8,7 @@ const specialLanguageCodes = {
|
||||||
'zh': 'zh-Hans'
|
'zh': 'zh-Hans'
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalToBrowserLocale = fallbackCode => specialLanguageCodes[fallbackCode] || window.navigator.language || fallbackCode
|
const internalToBrowserLocale = code => specialLanguageCodes[code] || code
|
||||||
|
|
||||||
const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
|
const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue