1
0
Fork 0
mirror of https://github.com/cheeaun/phanpy.git synced 2025-03-11 00:18:51 +01:00

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; display: inline-block;
margin: 4px; margin: 4px;
align-self: center; align-self: center;
&.clickable {
cursor: pointer;
}
} }
.tag .icon { .tag .icon {
vertical-align: middle; 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 { getBlurHashAverageColor } from 'fast-blurhash';
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks'; import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -6,9 +6,9 @@ import { useHotkeys } from 'react-hotkeys-hook';
import Icon from './icon'; import Icon from './icon';
import Link from './link'; import Link from './link';
import Media from './media'; import Media from './media';
import MediaAltModal from './media-alt-modal';
import MenuLink from './menu-link'; import MenuLink from './menu-link';
import Modal from './modal'; import Modal from './modal';
import TranslationBlock from './translation-block';
function MediaModal({ function MediaModal({
mediaAttachments, 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; export default MediaModal;

View file

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

View file

@ -11,6 +11,7 @@ import AccountSheet from './account-sheet';
import Compose from './compose'; import Compose from './compose';
import Drafts from './drafts'; import Drafts from './drafts';
import GenericAccounts from './generic-accounts'; import GenericAccounts from './generic-accounts';
import MediaAltModal from './media-alt-modal';
import MediaModal from './media-modal'; import MediaModal from './media-modal';
import Modal from './modal'; import Modal from './modal';
import ShortcutsSettings from './shortcuts-settings'; import ShortcutsSettings from './shortcuts-settings';
@ -178,6 +179,23 @@ export default function Modals() {
/> />
</Modal> </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; -webkit-line-clamp: 2;
line-clamp: 2; line-clamp: 2;
line-height: 1.2; line-height: 1.2;
cursor: pointer;
} }
} }
@ -833,7 +834,7 @@
.status .media:is(:hover, :focus) { .status .media:is(:hover, :focus) {
border-color: var(--outline-hover-color); border-color: var(--outline-hover-color);
} }
.status .media:active { .status .media:active:not(:has(button:active)) {
filter: brightness(0.8); filter: brightness(0.8);
transform: scale(0.99); transform: scale(0.99);
} }
@ -845,6 +846,51 @@
} }
.status .media { .status .media {
cursor: pointer; 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), .status .media img:is(:hover, :focus),
a:focus-visible .status .media img { a:focus-visible .status .media img {
@ -874,9 +920,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
color: var(--video-fg-color); color: var(--media-fg-color);
background-color: var(--video-bg-color); background-color: var(--media-bg-color);
box-shadow: inset 0 0 0 2px var(--video-outline-color); box-shadow: inset 0 0 0 2px var(--media-outline-color);
display: flex; display: flex;
place-content: center; place-content: center;
place-items: center; place-items: center;
@ -893,9 +939,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
right: 8px; right: 8px;
color: var(--video-fg-color); color: var(--media-fg-color);
background-color: var(--video-bg-color); background-color: var(--media-bg-color);
border: var(--hairline-width) solid var(--video-outline-color); border: var(--hairline-width) solid var(--media-outline-color);
border-radius: 4px; border-radius: 4px;
padding: 0 4px; padding: 0 4px;
} }
@ -910,9 +956,9 @@ body:has(#modal-container .carousel) .status .media img:hover {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
right: 8px; right: 8px;
color: var(--bg-faded-color); color: var(--media-fg-color);
background-color: var(--text-insignificant-color); background-color: var(--media-bg-color);
backdrop-filter: blur(6px) saturate(3) invert(0.2); border: var(--hairline-width) solid var(--media-outline-color);
border-radius: 4px; border-radius: 4px;
padding: 0 4px; padding: 0 4px;
} }

View file

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

View file

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