diff --git a/src/app.css b/src/app.css index afd4d5c9..9f92d714 100644 --- a/src/app.css +++ b/src/app.css @@ -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; diff --git a/src/components/media-alt-modal.jsx b/src/components/media-alt-modal.jsx new file mode 100644 index 00000000..95745764 --- /dev/null +++ b/src/components/media-alt-modal.jsx @@ -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 ( +
+ {!!onClose && ( + + )} +
+

Media description

+
+ + + + } + > + { + setForceTranslate(true); + }} + > + + Translate + + +
+
+
+

+ {alt} +

+ {forceTranslate && ( + + )} +
+
+ ); +} diff --git a/src/components/media-modal.jsx b/src/components/media-modal.jsx index 4290035e..7ec4769c 100644 --- a/src/components/media-modal.jsx +++ b/src/components/media-modal.jsx @@ -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 ( -
- {!!onClose && ( - - )} -
-

Media description

-
- - - - } - > - { - setForceTranslate(true); - }} - > - - Translate - - -
-
-
-

- {alt} -

- {forceTranslate && ( - - )} -
-
- ); -} - export default MediaModal; diff --git a/src/components/media.jsx b/src/components/media.jsx index c661757b..bab23396 100644 --- a/src/components/media.jsx +++ b/src/components/media.jsx @@ -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 ( + + ); +}; + 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 = () => {} }) { /> ) : ( - {description} { - 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; - } - }} - /> + <> + {description} { + 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; + } + }} + /> + + )} ); @@ -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 = () => {} }) { )} + {!showOriginal && !showInlineDesc && } {showInlineDesc && (
{ - location.hash = to; + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + states.showMediaAlt = description; }} > {description} @@ -357,6 +388,7 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) { @@ -373,9 +405,12 @@ function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) { /> ) : null} {!showOriginal && ( -
- -
+ <> +
+ +
+ + )}
); diff --git a/src/components/modals.jsx b/src/components/modals.jsx index bab9952c..70b56eb9 100644 --- a/src/components/modals.jsx +++ b/src/components/modals.jsx @@ -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() { /> )} + {!!snapStates.showMediaAlt && ( + { + if (e.target === e.currentTarget) { + states.showMediaAlt = false; + } + }} + > + { + states.showMediaAlt = false; + }} + /> + + )} ); } diff --git a/src/components/status.css b/src/components/status.css index 911d9b0c..aa00d5fd 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -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; } diff --git a/src/index.css b/src/index.css index 4ff57833..dd6d8767 100644 --- a/src/index.css +++ b/src/index.css @@ -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); } diff --git a/src/utils/states.js b/src/utils/states.js index 55a6e618..042b3d5f 100644 --- a/src/utils/states.js +++ b/src/utils/states.js @@ -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) {