this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

It's time for global media alt modal

+208 -98
+4
src/app.css
··· 1423 1423 display: inline-block; 1424 1424 margin: 4px; 1425 1425 align-self: center; 1426 + 1427 + &.clickable { 1428 + cursor: pointer; 1429 + } 1426 1430 } 1427 1431 .tag .icon { 1428 1432 vertical-align: middle;
+53
src/components/media-alt-modal.jsx
··· 1 + import { Menu, MenuItem } from '@szhsin/react-menu'; 2 + import { useState } from 'preact/hooks'; 3 + 4 + import Icon from './icon'; 5 + import TranslationBlock from './translation-block'; 6 + 7 + export default function MediaAltModal({ alt, onClose }) { 8 + const [forceTranslate, setForceTranslate] = useState(false); 9 + return ( 10 + <div class="sheet"> 11 + {!!onClose && ( 12 + <button type="button" class="sheet-close outer" onClick={onClose}> 13 + <Icon icon="x" /> 14 + </button> 15 + )} 16 + <header class="header-grid"> 17 + <h2>Media description</h2> 18 + <div class="header-side"> 19 + <Menu 20 + align="end" 21 + menuButton={ 22 + <button type="button" class="plain4"> 23 + <Icon icon="more" alt="More" size="xl" /> 24 + </button> 25 + } 26 + > 27 + <MenuItem 28 + disabled={forceTranslate} 29 + onClick={() => { 30 + setForceTranslate(true); 31 + }} 32 + > 33 + <Icon icon="translate" /> 34 + <span>Translate</span> 35 + </MenuItem> 36 + </Menu> 37 + </div> 38 + </header> 39 + <main> 40 + <p 41 + style={{ 42 + whiteSpace: 'pre-wrap', 43 + }} 44 + > 45 + {alt} 46 + </p> 47 + {forceTranslate && ( 48 + <TranslationBlock forceTranslate={forceTranslate} text={alt} /> 49 + )} 50 + </main> 51 + </div> 52 + ); 53 + }
+2 -50
src/components/media-modal.jsx
··· 1 - import { Menu, MenuItem } from '@szhsin/react-menu'; 1 + import { Menu } from '@szhsin/react-menu'; 2 2 import { getBlurHashAverageColor } from 'fast-blurhash'; 3 3 import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks'; 4 4 import { useHotkeys } from 'react-hotkeys-hook'; ··· 6 6 import Icon from './icon'; 7 7 import Link from './link'; 8 8 import Media from './media'; 9 + import MediaAltModal from './media-alt-modal'; 9 10 import MenuLink from './menu-link'; 10 11 import Modal from './modal'; 11 - import TranslationBlock from './translation-block'; 12 12 13 13 function MediaModal({ 14 14 mediaAttachments, ··· 284 284 /> 285 285 </Modal> 286 286 )} 287 - </div> 288 - ); 289 - } 290 - 291 - function MediaAltModal({ alt, onClose }) { 292 - const [forceTranslate, setForceTranslate] = useState(false); 293 - return ( 294 - <div class="sheet"> 295 - {!!onClose && ( 296 - <button type="button" class="sheet-close outer" onClick={onClose}> 297 - <Icon icon="x" /> 298 - </button> 299 - )} 300 - <header class="header-grid"> 301 - <h2>Media description</h2> 302 - <div class="header-side"> 303 - <Menu 304 - align="end" 305 - menuButton={ 306 - <button type="button" class="plain4"> 307 - <Icon icon="more" alt="More" size="xl" /> 308 - </button> 309 - } 310 - > 311 - <MenuItem 312 - disabled={forceTranslate} 313 - onClick={() => { 314 - setForceTranslate(true); 315 - }} 316 - > 317 - <Icon icon="translate" /> 318 - <span>Translate</span> 319 - </MenuItem> 320 - </Menu> 321 - </div> 322 - </header> 323 - <main> 324 - <p 325 - style={{ 326 - whiteSpace: 'pre-wrap', 327 - }} 328 - > 329 - {alt} 330 - </p> 331 - {forceTranslate && ( 332 - <TranslationBlock forceTranslate={forceTranslate} text={alt} /> 333 - )} 334 - </main> 335 287 </div> 336 288 ); 337 289 }
+70 -35
src/components/media.jsx
··· 9 9 } from 'preact/hooks'; 10 10 import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom'; 11 11 12 + import states from '../utils/states'; 13 + 12 14 import Icon from './icon'; 13 15 import Link from './link'; 14 16 import { formatDuration } from './status'; ··· 24 26 video = Video clip 25 27 audio = Audio track 26 28 */ 29 + 30 + const dataAltLabel = 'ALT'; 31 + const AltBadge = (props) => { 32 + const { alt, ...rest } = props; 33 + if (!alt || !alt.trim()) return null; 34 + return ( 35 + <button 36 + type="button" 37 + class="tag collapsed clickable" 38 + {...rest} 39 + onClick={(e) => { 40 + e.stopPropagation(); 41 + e.preventDefault(); 42 + states.showMediaAlt = alt; 43 + }} 44 + title="Media description" 45 + > 46 + {dataAltLabel} 47 + </button> 48 + ); 49 + }; 27 50 28 51 function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) { 29 52 const { ··· 157 180 class={`media media-image`} 158 181 onClick={onClick} 159 182 data-orientation={orientation} 183 + data-has-alt={!!description || undefined} 160 184 style={ 161 185 showOriginal 162 186 ? { ··· 193 217 /> 194 218 </QuickPinchZoom> 195 219 ) : ( 196 - <img 197 - src={mediaURL} 198 - alt={description} 199 - width={width} 200 - height={height} 201 - data-orientation={orientation} 202 - loading="lazy" 203 - style={{ 204 - backgroundColor: 205 - rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`, 206 - backgroundPosition: focalBackgroundPosition || 'center', 207 - // Duration based on width or height in pixels 208 - // 100px per second (rough estimate) 209 - // Clamp between 5s and 120s 210 - '--anim-duration': `${Math.min( 211 - Math.max(Math.max(width, height) / 100, 5), 212 - 120, 213 - )}s`, 214 - }} 215 - onLoad={(e) => { 216 - e.target.closest('.media-image').style.backgroundImage = ''; 217 - e.target.dataset.loaded = true; 218 - }} 219 - onError={(e) => { 220 - const { src } = e.target; 221 - if (src === mediaURL) { 222 - e.target.src = remoteMediaURL; 223 - } 224 - }} 225 - /> 220 + <> 221 + <img 222 + src={mediaURL} 223 + alt={description} 224 + width={width} 225 + height={height} 226 + data-orientation={orientation} 227 + loading="lazy" 228 + style={{ 229 + backgroundColor: 230 + rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`, 231 + backgroundPosition: focalBackgroundPosition || 'center', 232 + // Duration based on width or height in pixels 233 + // 100px per second (rough estimate) 234 + // Clamp between 5s and 120s 235 + '--anim-duration': `${Math.min( 236 + Math.max(Math.max(width, height) / 100, 5), 237 + 120, 238 + )}s`, 239 + }} 240 + onLoad={(e) => { 241 + e.target.closest('.media-image').style.backgroundImage = ''; 242 + e.target.dataset.loaded = true; 243 + }} 244 + onError={(e) => { 245 + const { src } = e.target; 246 + if (src === mediaURL) { 247 + e.target.src = remoteMediaURL; 248 + } 249 + }} 250 + /> 251 + <AltBadge alt={description} /> 252 + </> 226 253 )} 227 254 </Parent> 228 255 ); ··· 264 291 data-orientation={orientation} 265 292 data-formatted-duration={formattedDuration} 266 293 data-label={isGIF && !showOriginal && !autoGIFAnimate ? 'GIF' : ''} 294 + data-has-alt={!!description || undefined} 267 295 // style={{ 268 296 // backgroundColor: 269 297 // rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`, ··· 339 367 </div> 340 368 </> 341 369 )} 370 + {!showOriginal && !showInlineDesc && <AltBadge alt={description} />} 342 371 </Parent> 343 372 {showInlineDesc && ( 344 373 <figcaption 345 - onClick={() => { 346 - location.hash = to; 374 + onClick={(e) => { 375 + e.preventDefault(); 376 + e.stopPropagation(); 377 + states.showMediaAlt = description; 347 378 }} 348 379 > 349 380 {description} ··· 357 388 <Parent 358 389 class="media media-audio" 359 390 data-formatted-duration={formattedDuration} 391 + data-has-alt={!!description || undefined} 360 392 onClick={onClick} 361 393 style={!showOriginal && mediaStyles} 362 394 > ··· 373 405 /> 374 406 ) : null} 375 407 {!showOriginal && ( 376 - <div class="media-play"> 377 - <Icon icon="play" size="xl" /> 378 - </div> 408 + <> 409 + <div class="media-play"> 410 + <Icon icon="play" size="xl" /> 411 + </div> 412 + <AltBadge alt={description} /> 413 + </> 379 414 )} 380 415 </Parent> 381 416 );
+18
src/components/modals.jsx
··· 11 11 import Compose from './compose'; 12 12 import Drafts from './drafts'; 13 13 import GenericAccounts from './generic-accounts'; 14 + import MediaAltModal from './media-alt-modal'; 14 15 import MediaModal from './media-modal'; 15 16 import Modal from './modal'; 16 17 import ShortcutsSettings from './shortcuts-settings'; ··· 175 176 > 176 177 <GenericAccounts 177 178 onClose={() => (states.showGenericAccounts = false)} 179 + /> 180 + </Modal> 181 + )} 182 + {!!snapStates.showMediaAlt && ( 183 + <Modal 184 + class="light" 185 + onClick={(e) => { 186 + if (e.target === e.currentTarget) { 187 + states.showMediaAlt = false; 188 + } 189 + }} 190 + > 191 + <MediaAltModal 192 + alt={snapStates.showMediaAlt} 193 + onClose={() => { 194 + states.showMediaAlt = false; 195 + }} 178 196 /> 179 197 </Modal> 180 198 )}
+56 -10
src/components/status.css
··· 723 723 -webkit-line-clamp: 2; 724 724 line-clamp: 2; 725 725 line-height: 1.2; 726 + cursor: pointer; 726 727 } 727 728 } 728 729 ··· 833 834 .status .media:is(:hover, :focus) { 834 835 border-color: var(--outline-hover-color); 835 836 } 836 - .status .media:active { 837 + .status .media:active:not(:has(button:active)) { 837 838 filter: brightness(0.8); 838 839 transform: scale(0.99); 839 840 } ··· 845 846 } 846 847 .status .media { 847 848 cursor: pointer; 849 + 850 + &[data-has-alt] { 851 + position: relative; 852 + 853 + .tag { 854 + position: absolute; 855 + bottom: 8px; 856 + left: 8px; 857 + font-size: 12px; 858 + font-weight: bold; 859 + border: var(--hairline-width) solid var(--media-outline-color); 860 + mix-blend-mode: luminosity; 861 + 862 + &:before { 863 + content: ''; 864 + position: absolute; 865 + inset: -12px; 866 + } 867 + 868 + &:is(:hover, :focus):not(:active) { 869 + transition: transform 0.15s ease-out; 870 + transform: scale(1.15); 871 + } 872 + } 873 + 874 + &:before { 875 + font-size: 12px; 876 + font-weight: bold; 877 + pointer-events: none; 878 + content: attr(data-alt-label); 879 + position: absolute; 880 + bottom: 8px; 881 + left: 8px; 882 + color: var(--media-fg-color); 883 + background-color: var(--media-bg-color); 884 + border: var(--hairline-width) solid var(--media-outline-color); 885 + border-radius: 4px; 886 + padding: 0 4px; 887 + transition: opacity 0.2s ease-in-out; 888 + } 889 + 890 + &:hover:before { 891 + opacity: 0.2; 892 + } 893 + } 848 894 } 849 895 .status .media img:is(:hover, :focus), 850 896 a:focus-visible .status .media img { ··· 874 920 left: 50%; 875 921 top: 50%; 876 922 transform: translate(-50%, -50%); 877 - color: var(--video-fg-color); 878 - background-color: var(--video-bg-color); 879 - box-shadow: inset 0 0 0 2px var(--video-outline-color); 923 + color: var(--media-fg-color); 924 + background-color: var(--media-bg-color); 925 + box-shadow: inset 0 0 0 2px var(--media-outline-color); 880 926 display: flex; 881 927 place-content: center; 882 928 place-items: center; ··· 893 939 position: absolute; 894 940 bottom: 8px; 895 941 right: 8px; 896 - color: var(--video-fg-color); 897 - background-color: var(--video-bg-color); 898 - border: var(--hairline-width) solid var(--video-outline-color); 942 + color: var(--media-fg-color); 943 + background-color: var(--media-bg-color); 944 + border: var(--hairline-width) solid var(--media-outline-color); 899 945 border-radius: 4px; 900 946 padding: 0 4px; 901 947 } ··· 910 956 position: absolute; 911 957 bottom: 8px; 912 958 right: 8px; 913 - color: var(--bg-faded-color); 914 - background-color: var(--text-insignificant-color); 915 - backdrop-filter: blur(6px) saturate(3) invert(0.2); 959 + color: var(--media-fg-color); 960 + background-color: var(--media-bg-color); 961 + border: var(--hairline-width) solid var(--media-outline-color); 916 962 border-radius: 4px; 917 963 padding: 0 4px; 918 964 }
+3 -3
src/index.css
··· 64 64 --close-button-hover-color: rgba(0, 0, 0, 1); 65 65 66 66 /* Video colors won't change based on color scheme */ 67 - --video-fg-color: #f0f2f5; 68 - --video-bg-color: #242526; 69 - --video-outline-color: color-mix(in lch, var(--video-fg-color), transparent); 67 + --media-fg-color: #f0f2f5; 68 + --media-bg-color: #242526; 69 + --media-outline-color: color-mix(in lch, var(--media-fg-color), transparent); 70 70 71 71 --timing-function: cubic-bezier(0.3, 0.5, 0, 1); 72 72 }
+2
src/utils/states.js
··· 40 40 showShortcutsSettings: false, 41 41 showKeyboardShortcutsHelp: false, 42 42 showGenericAccounts: false, 43 + showMediaAlt: false, 43 44 // Shortcuts 44 45 shortcuts: store.account.get('shortcuts') ?? [], 45 46 // Settings ··· 141 142 states.showShortcutsSettings = false; 142 143 states.showKeyboardShortcutsHelp = false; 143 144 states.showGenericAccounts = false; 145 + states.showMediaAlt = false; 144 146 } 145 147 146 148 export function statusKey(id, instance) {