mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-02 14:16:39 +01:00
It's time for global media alt modal
This commit is contained in:
parent
fd1b45900d
commit
13cf7b3f92
8 changed files with 208 additions and 98 deletions
|
@ -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;
|
||||
|
|
53
src/components/media-alt-modal.jsx
Normal file
53
src/components/media-alt-modal.jsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue