It's time for global media alt modal

This commit is contained in:
Lim Chee Aun 2023-09-28 15:48:32 +08:00
parent fd1b45900d
commit 13cf7b3f92
8 changed files with 208 additions and 98 deletions

View file

@ -1423,6 +1423,10 @@ body:has(.media-modal-container + .status-deck) .media-post-link {
display: inline-block;
margin: 4px;
align-self: center;
&.clickable {
cursor: pointer;
}
}
.tag .icon {
vertical-align: middle;

View file

@ -0,0 +1,53 @@
import { Menu, MenuItem } from '@szhsin/react-menu';
import { useState } from 'preact/hooks';
import Icon from './icon';
import TranslationBlock from './translation-block';
export default function MediaAltModal({ alt, onClose }) {
const [forceTranslate, setForceTranslate] = useState(false);
return (
<div class="sheet">
{!!onClose && (
<button type="button" class="sheet-close outer" onClick={onClose}>
<Icon icon="x" />
</button>
)}
<header class="header-grid">
<h2>Media description</h2>
<div class="header-side">
<Menu
align="end"
menuButton={
<button type="button" class="plain4">
<Icon icon="more" alt="More" size="xl" />
</button>
}
>
<MenuItem
disabled={forceTranslate}
onClick={() => {
setForceTranslate(true);
}}
>
<Icon icon="translate" />
<span>Translate</span>
</MenuItem>
</Menu>
</div>
</header>
<main>
<p
style={{
whiteSpace: 'pre-wrap',
}}
>
{alt}
</p>
{forceTranslate && (
<TranslationBlock forceTranslate={forceTranslate} text={alt} />
)}
</main>
</div>
);
}

View file

@ -1,4 +1,4 @@
import { Menu, MenuItem } from '@szhsin/react-menu';
import { Menu } from '@szhsin/react-menu';
import { getBlurHashAverageColor } from 'fast-blurhash';
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
@ -6,9 +6,9 @@ import { useHotkeys } from 'react-hotkeys-hook';
import Icon from './icon';
import Link from './link';
import Media from './media';
import MediaAltModal from './media-alt-modal';
import MenuLink from './menu-link';
import Modal from './modal';
import TranslationBlock from './translation-block';
function MediaModal({
mediaAttachments,
@ -288,52 +288,4 @@ function MediaModal({
);
}
function MediaAltModal({ alt, onClose }) {
const [forceTranslate, setForceTranslate] = useState(false);
return (
<div class="sheet">
{!!onClose && (
<button type="button" class="sheet-close outer" onClick={onClose}>
<Icon icon="x" />
</button>
)}
<header class="header-grid">
<h2>Media description</h2>
<div class="header-side">
<Menu
align="end"
menuButton={
<button type="button" class="plain4">
<Icon icon="more" alt="More" size="xl" />
</button>
}
>
<MenuItem
disabled={forceTranslate}
onClick={() => {
setForceTranslate(true);
}}
>
<Icon icon="translate" />
<span>Translate</span>
</MenuItem>
</Menu>
</div>
</header>
<main>
<p
style={{
whiteSpace: 'pre-wrap',
}}
>
{alt}
</p>
{forceTranslate && (
<TranslationBlock forceTranslate={forceTranslate} text={alt} />
)}
</main>
</div>
);
}
export default MediaModal;

View file

@ -9,6 +9,8 @@ import {
} from 'preact/hooks';
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom';
import states from '../utils/states';
import Icon from './icon';
import Link from './link';
import { formatDuration } from './status';
@ -25,6 +27,27 @@ video = Video clip
audio = Audio track
*/
const dataAltLabel = 'ALT';
const AltBadge = (props) => {
const { alt, ...rest } = props;
if (!alt || !alt.trim()) return null;
return (
<button
type="button"
class="tag collapsed clickable"
{...rest}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
states.showMediaAlt = alt;
}}
title="Media description"
>
{dataAltLabel}
</button>
);
};
function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
const {
blurhash,
@ -157,6 +180,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
class={`media media-image`}
onClick={onClick}
data-orientation={orientation}
data-has-alt={!!description || undefined}
style={
showOriginal
? {
@ -193,36 +217,39 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
/>
</QuickPinchZoom>
) : (
<img
src={mediaURL}
alt={description}
width={width}
height={height}
data-orientation={orientation}
loading="lazy"
style={{
backgroundColor:
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
backgroundPosition: focalBackgroundPosition || 'center',
// Duration based on width or height in pixels
// 100px per second (rough estimate)
// Clamp between 5s and 120s
'--anim-duration': `${Math.min(
Math.max(Math.max(width, height) / 100, 5),
120,
)}s`,
}}
onLoad={(e) => {
e.target.closest('.media-image').style.backgroundImage = '';
e.target.dataset.loaded = true;
}}
onError={(e) => {
const { src } = e.target;
if (src === mediaURL) {
e.target.src = remoteMediaURL;
}
}}
/>
<>
<img
src={mediaURL}
alt={description}
width={width}
height={height}
data-orientation={orientation}
loading="lazy"
style={{
backgroundColor:
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
backgroundPosition: focalBackgroundPosition || 'center',
// Duration based on width or height in pixels
// 100px per second (rough estimate)
// Clamp between 5s and 120s
'--anim-duration': `${Math.min(
Math.max(Math.max(width, height) / 100, 5),
120,
)}s`,
}}
onLoad={(e) => {
e.target.closest('.media-image').style.backgroundImage = '';
e.target.dataset.loaded = true;
}}
onError={(e) => {
const { src } = e.target;
if (src === mediaURL) {
e.target.src = remoteMediaURL;
}
}}
/>
<AltBadge alt={description} />
</>
)}
</Parent>
);
@ -264,6 +291,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
data-orientation={orientation}
data-formatted-duration={formattedDuration}
data-label={isGIF && !showOriginal && !autoGIFAnimate ? 'GIF' : ''}
data-has-alt={!!description || undefined}
// style={{
// backgroundColor:
// rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
@ -339,11 +367,14 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
</div>
</>
)}
{!showOriginal && !showInlineDesc && <AltBadge alt={description} />}
</Parent>
{showInlineDesc && (
<figcaption
onClick={() => {
location.hash = to;
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
states.showMediaAlt = description;
}}
>
{description}
@ -357,6 +388,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
<Parent
class="media media-audio"
data-formatted-duration={formattedDuration}
data-has-alt={!!description || undefined}
onClick={onClick}
style={!showOriginal && mediaStyles}
>
@ -373,9 +405,12 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
/>
) : null}
{!showOriginal && (
<div class="media-play">
<Icon icon="play" size="xl" />
</div>
<>
<div class="media-play">
<Icon icon="play" size="xl" />
</div>
<AltBadge alt={description} />
</>
)}
</Parent>
);

View file

@ -11,6 +11,7 @@ import AccountSheet from './account-sheet';
import Compose from './compose';
import Drafts from './drafts';
import GenericAccounts from './generic-accounts';
import MediaAltModal from './media-alt-modal';
import MediaModal from './media-modal';
import Modal from './modal';
import ShortcutsSettings from './shortcuts-settings';
@ -178,6 +179,23 @@ export default function Modals() {
/>
</Modal>
)}
{!!snapStates.showMediaAlt && (
<Modal
class="light"
onClick={(e) => {
if (e.target === e.currentTarget) {
states.showMediaAlt = false;
}
}}
>
<MediaAltModal
alt={snapStates.showMediaAlt}
onClose={() => {
states.showMediaAlt = false;
}}
/>
</Modal>
)}
</>
);
}

View file

@ -723,6 +723,7 @@
-webkit-line-clamp: 2;
line-clamp: 2;
line-height: 1.2;
cursor: pointer;
}
}
@ -833,7 +834,7 @@
.status .media:is(:hover, :focus) {
border-color: var(--outline-hover-color);
}
.status .media:active {
.status .media:active:not(:has(button:active)) {
filter: brightness(0.8);
transform: scale(0.99);
}
@ -845,6 +846,51 @@
}
.status .media {
cursor: pointer;
&[data-has-alt] {
position: relative;
.tag {
position: absolute;
bottom: 8px;
left: 8px;
font-size: 12px;
font-weight: bold;
border: var(--hairline-width) solid var(--media-outline-color);
mix-blend-mode: luminosity;
&:before {
content: '';
position: absolute;
inset: -12px;
}
&:is(:hover, :focus):not(:active) {
transition: transform 0.15s ease-out;
transform: scale(1.15);
}
}
&:before {
font-size: 12px;
font-weight: bold;
pointer-events: none;
content: attr(data-alt-label);
position: absolute;
bottom: 8px;
left: 8px;
color: var(--media-fg-color);
background-color: var(--media-bg-color);
border: var(--hairline-width) solid var(--media-outline-color);
border-radius: 4px;
padding: 0 4px;
transition: opacity 0.2s ease-in-out;
}
&:hover:before {
opacity: 0.2;
}
}
}
.status .media img:is(:hover, :focus),
a:focus-visible .status .media img {
@ -874,9 +920,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: var(--video-fg-color);
background-color: var(--video-bg-color);
box-shadow: inset 0 0 0 2px var(--video-outline-color);
color: var(--media-fg-color);
background-color: var(--media-bg-color);
box-shadow: inset 0 0 0 2px var(--media-outline-color);
display: flex;
place-content: center;
place-items: center;
@ -893,9 +939,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
position: absolute;
bottom: 8px;
right: 8px;
color: var(--video-fg-color);
background-color: var(--video-bg-color);
border: var(--hairline-width) solid var(--video-outline-color);
color: var(--media-fg-color);
background-color: var(--media-bg-color);
border: var(--hairline-width) solid var(--media-outline-color);
border-radius: 4px;
padding: 0 4px;
}
@ -910,9 +956,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
position: absolute;
bottom: 8px;
right: 8px;
color: var(--bg-faded-color);
background-color: var(--text-insignificant-color);
backdrop-filter: blur(6px) saturate(3) invert(0.2);
color: var(--media-fg-color);
background-color: var(--media-bg-color);
border: var(--hairline-width) solid var(--media-outline-color);
border-radius: 4px;
padding: 0 4px;
}

View file

@ -64,9 +64,9 @@
--close-button-hover-color: rgba(0, 0, 0, 1);
/* Video colors won't change based on color scheme */
--video-fg-color: #f0f2f5;
--video-bg-color: #242526;
--video-outline-color: color-mix(in lch, var(--video-fg-color), transparent);
--media-fg-color: #f0f2f5;
--media-bg-color: #242526;
--media-outline-color: color-mix(in lch, var(--media-fg-color), transparent);
--timing-function: cubic-bezier(0.3, 0.5, 0, 1);
}

View file

@ -40,6 +40,7 @@ const states = proxy({
showShortcutsSettings: false,
showKeyboardShortcutsHelp: false,
showGenericAccounts: false,
showMediaAlt: false,
// Shortcuts
shortcuts: store.account.get('shortcuts') ?? [],
// Settings
@ -141,6 +142,7 @@ export function hideAllModals() {
states.showShortcutsSettings = false;
states.showKeyboardShortcutsHelp = false;
states.showGenericAccounts = false;
states.showMediaAlt = false;
}
export function statusKey(id, instance) {