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:
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;
|
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;
|
||||||
|
|
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 { 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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue