From b40bbb32c2b4c12615b4d6d9679fcc0299fa2b7a Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sun, 29 Oct 2023 21:41:03 +0800 Subject: [PATCH] Alrighty, this is media-view layout --- src/app.css | 72 +++++++++++++++ src/cloak-mode.css | 1 + src/components/icon.jsx | 1 + src/components/media-post.css | 87 ++++++++++++++++++ src/components/media-post.jsx | 126 ++++++++++++++++++++++++++ src/components/media.jsx | 24 +++-- src/components/shortcuts-settings.jsx | 11 ++- src/components/status.css | 16 ++-- src/components/timeline.jsx | 108 ++++++++++++++++------ src/pages/account-statuses.jsx | 1 + src/pages/hashtag.jsx | 46 ++++++++-- 11 files changed, 440 insertions(+), 53 deletions(-) create mode 100644 src/components/media-post.css create mode 100644 src/components/media-post.jsx diff --git a/src/app.css b/src/app.css index 2b819a3..338286f 100644 --- a/src/app.css +++ b/src/app.css @@ -209,6 +209,60 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) { .timeline { margin: 0 auto; padding: 0; + + &.timeline-media { + --grid-gap: 8px; + display: grid; + grid-template-columns: 1fr 1fr; + grid-auto-rows: fit-content; + gap: var(--grid-gap); + padding: var(--grid-gap); + + &:not(#columns &) { + background-color: var(--bg-faded-color); + } + + @media (min-width: 40em) { + &:not(#columns &) { + --grid-gap: 16px; + grid-template-columns: 1fr 1fr 1fr; + + @media (min-width: 40em) { + width: 95vw; + max-width: calc(320px * 3.3); + transform: translateX(calc(-50% + var(--main-width) / 2)); + } + } + + #columns & { + padding-inline: 0; + } + } + + li { + padding: 0 !important; + margin: 0 !important; + border: 0 !important; + overflow: visible !important; + background-color: transparent !important; + box-shadow: none !important; + } + + @supports (grid-template-rows: masonry) { + grid-template-rows: masonry; + masonry-auto-flow: pack; + + .media-post a { + aspect-ratio: revert !important; + + video, + img, + audio { + min-height: 88px; /* for extreme dimensions */ + } + } + } + } } .timeline.grow { /* min-height: 100vh; @@ -1678,6 +1732,24 @@ body > .szh-menu-container { opacity: 1; } +.szh-menu + .szh-menu__item--type-checkbox:not(.szh-menu__item--disabled):not( + .szh-menu__item--hover + ) { + .icon { + opacity: 0.15; + } + + &.szh-menu__item--checked { + color: var(--link-color); + + .icon { + opacity: 1; + color: inherit; + } + } +} + .szh-menu .menu-wrap { display: flex; flex-wrap: wrap; diff --git a/src/cloak-mode.css b/src/cloak-mode.css index d703b7a..f96f499 100644 --- a/src/cloak-mode.css +++ b/src/cloak-mode.css @@ -25,6 +25,7 @@ body.cloak, } .status :is(img, video, audio), + .media-post .media, .avatar, .emoji, .header-banner { diff --git a/src/components/icon.jsx b/src/components/icon.jsx index 42e33f2..cd2b535 100644 --- a/src/components/icon.jsx +++ b/src/components/icon.jsx @@ -102,6 +102,7 @@ export const ICONS = { keyboard: () => import('@iconify-icons/mingcute/keyboard-line'), cloud: () => import('@iconify-icons/mingcute/cloud-line'), month: () => import('@iconify-icons/mingcute/calendar-month-line'), + media: () => import('@iconify-icons/mingcute/photo-album-line'), }; function Icon({ diff --git a/src/components/media-post.css b/src/components/media-post.css new file mode 100644 index 0000000..b7d3285 --- /dev/null +++ b/src/components/media-post.css @@ -0,0 +1,87 @@ +.media-post { + --item-radius: 16px; + position: relative; + animation: appear-smooth 1s ease-out; + + &:is(.filtered, .has-spoiler) :is(img, video) { + filter: blur(32px); + image-rendering: crisp-edges; + image-rendering: pixelated; + animation: none !important; + } + + &.filtered[data-filtered-text]:before, + &.has-spoiler[data-spoiler-text]:before { + pointer-events: none; + content: attr(data-spoiler-text); + position: absolute; + top: 0; + left: 0; + z-index: 1; + background-color: var(--bg-blur-color); + margin: 8px; + padding: 4px 6px; + border-radius: calc(var(--item-radius) / 2); + font-size: 90%; + border: var(--hairline-width) dashed var(--bg-color); + + > * { + pointer-events: none; + } + } + + .media { + border-radius: var(--item-radius); + overflow: hidden; + position: relative; + display: block; + aspect-ratio: 1 !important; + + &:before { + position: absolute; + inset: 0; + content: ''; + border: 1px solid var(--outline-color); + border-radius: inherit; + } + + &:not(.media-audio) { + background-color: var(--average-color, var(--media-bg-color)); + } + + @media (hover: hover) { + &:hover { + --drop-shadow: var(--drop-shadow-color); + box-shadow: 0 8px 16px -4px var(--drop-shadow), + 0 4px 8px var(--drop-shadow); + + @media (prefers-color-scheme: dark) { + --drop-shadow: var(--link-color); + } + } + } + + &:active:not(:has(button:active)) { + box-shadow: none; + filter: brightness(0.8); + transform: scale(0.99); + } + + video, + img, + audio { + border-radius: 16px; + /* object-fit: scale-down; */ + object-fit: cover; + width: 100%; + height: 100%; + vertical-align: top; + } + + &:is(:hover, :focus) img { + /* Less delay here to make it feel more responsive */ + animation: position-object 5s ease-in-out 0.3s 5; + animation-duration: var(--anim-duration, 5s); + } + } +} diff --git a/src/components/media-post.jsx b/src/components/media-post.jsx new file mode 100644 index 0000000..c208896 --- /dev/null +++ b/src/components/media-post.jsx @@ -0,0 +1,126 @@ +import './media-post.css'; + +import { memo } from 'preact/compat'; +import { useSnapshot } from 'valtio'; + +import states, { statusKey } from '../utils/states'; + +import Media from './media'; + +function MediaPost({ + class: className, + statusID, + status, + instance, + parent, + allowFilters, + onMediaClick, +}) { + let sKey = statusKey(statusID, instance); + const snapStates = useSnapshot(states); + if (!status) { + status = snapStates.statuses[sKey] || snapStates.statuses[statusID]; + sKey = statusKey(status?.id, instance); + } + if (!status) { + return null; + } + + const { + account: { + acct, + avatar, + avatarStatic, + id: accountId, + url: accountURL, + displayName, + username, + emojis: accountEmojis, + bot, + group, + }, + id, + repliesCount, + reblogged, + reblogsCount, + favourited, + favouritesCount, + bookmarked, + poll, + muted, + sensitive, + spoilerText, + visibility, // public, unlisted, private, direct + language, + editedAt, + filtered, + card, + createdAt, + inReplyToId, + inReplyToAccountId, + content, + mentions, + mediaAttachments, + reblog, + uri, + url, + emojis, + // Non-API props + _deleted, + _pinned, + _filtered, + } = status; + + if (!mediaAttachments?.length) { + return null; + } + + const debugHover = (e) => { + if (e.shiftKey) { + console.log({ + ...status, + }); + } + }; + + console.debug('RENDER Media post', id, status?.account.displayName); + + // const readingExpandSpoilers = useMemo(() => { + // const prefs = store.account.get('preferences') || {}; + // return !!prefs['reading:expand:spoilers']; + // }, []); + const hasSpoiler = spoilerText || sensitive; + + const Parent = parent || 'div'; + + return mediaAttachments.map((media, i) => { + const mediaKey = `${sKey}-${media.id}`; + return ( + + onMediaClick(e, i, media, status) : undefined + } + /> + + ); + }); +} + +export default memo(MediaPost); diff --git a/src/components/media.jsx b/src/components/media.jsx index 590caa4..f039045 100644 --- a/src/components/media.jsx +++ b/src/components/media.jsx @@ -62,6 +62,7 @@ export const isMediaCaptionLong = mem((caption) => ); function Media({ + class: className = '', media, to, lang, @@ -170,6 +171,9 @@ function Media({ const maxAspectHeight = window.innerHeight * (orientation === 'portrait' ? 0.45 : 0.33); const maxHeight = orientation === 'portrait' ? 0 : 160; + const averageColorStyle = { + '--average-color': rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`, + }; const mediaStyles = width && height ? { @@ -180,8 +184,11 @@ function Media({ (width / height) * Math.max(maxHeight, maxAspectHeight) }px`, aspectRatio: `${width} / ${height}`, + ...averageColorStyle, } - : {}; + : { + ...averageColorStyle, + }; const longDesc = isMediaCaptionLong(description); const showInlineDesc = @@ -233,7 +240,7 @@ function Media({
hashtag, subtitle: ({ instance }) => instance || api().instance, - path: ({ hashtag, instance }) => - `${instance ? `/${instance}` : ''}/t/${hashtag.split(/\s+/).join('+')}`, + path: ({ hashtag, instance, media }) => + `${instance ? `/${instance}` : ''}/t/${hashtag.split(/\s+/).join('+')}${ + media ? '?media=1' : '' + }`, icon: 'hashtag', }, }; diff --git a/src/components/status.css b/src/components/status.css index 77f55d9..36b3fdc 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -846,7 +846,7 @@ object-fit: cover; vertical-align: middle; } -.status .media { +.media { cursor: pointer; &[data-has-alt] { @@ -885,7 +885,7 @@ body:has(#modal-container .carousel) .status .media img:hover { position: relative; background-clip: padding-box; } -.status :is(.media-video, .media-audio) .media-play { +:is(.media-video, .media-audio) .media-play { pointer-events: none; width: 44px; height: 44px; @@ -902,10 +902,10 @@ body:has(#modal-container .carousel) .status .media img:hover { border-radius: 70px; transition: transform 0.2s ease-in-out; } -.status :is(.media-video, .media-audio):hover:not(:active) .media-play { +:is(.media-video, .media-audio):hover:not(:active) .media-play { transform: translate(-50%, -50%) scale(1.1); } -.status :is(.media-video, .media-audio)[data-formatted-duration]:after { +:is(.media-video, .media-audio)[data-formatted-duration]:after { font-size: 12px; pointer-events: none; content: attr(data-formatted-duration); @@ -918,10 +918,10 @@ body:has(#modal-container .carousel) .status .media img:hover { border-radius: 4px; padding: 0 4px; } -.status .media-audio[data-formatted-duration]:after { +.media-audio[data-formatted-duration]:after { content: '♬ ' attr(data-formatted-duration); } -.status .media-gif[data-label]:not(:hover):after { +.media-gif[data-label]:not(:hover):after { font-size: 12px; font-weight: bold; pointer-events: none; @@ -953,12 +953,12 @@ body:has(#modal-container .carousel) .status .media img:hover { .status .media-audio audio { width: 100%; } */ -.status .media-audio { +.media-audio { width: 100%; height: 100%; background-image: radial-gradient( circle at center center, - var(--bg-color), + transparent, var(--bg-faded-color) ), repeating-radial-gradient( diff --git a/src/components/timeline.jsx b/src/components/timeline.jsx index c893ec8..ecbe693 100644 --- a/src/components/timeline.jsx +++ b/src/components/timeline.jsx @@ -13,6 +13,8 @@ import useScroll from '../utils/useScroll'; import Icon from './icon'; import Link from './link'; +import Media from './media'; +import MediaPost from './media-post'; import NavMenu from './nav-menu'; import Status from './status'; @@ -39,6 +41,7 @@ function Timeline({ timelineStart, allowFilters, refresh, + view, }) { const snapStates = useSnapshot(states); const [items, setItems] = useState([]); @@ -50,6 +53,7 @@ function Timeline({ console.debug('RENDER Timeline', id, refresh); + const allowGrouping = view !== 'media'; const loadItems = useDebouncedCallback( (firstLoad) => { setShowNew(false); @@ -59,10 +63,12 @@ function Timeline({ try { let { done, value } = await fetchItems(firstLoad); if (Array.isArray(value)) { - if (boostsCarousel) { - value = groupBoosts(value); + if (allowGrouping) { + if (boostsCarousel) { + value = groupBoosts(value); + } + value = groupContext(value); } - value = groupContext(value); console.log(value); if (firstLoad) { setItems(value); @@ -210,6 +216,14 @@ function Timeline({ } }, [nearReachEnd, showMore]); + const prevView = useRef(view); + useEffect(() => { + if (prevView.current !== view) { + prevView.current = view; + setItems([]); + } + }, [view]); + const loadOrCheckUpdates = useCallback( async ({ disableIdleCheck = false } = {}) => { console.log('✨ Load or check updates', { @@ -346,7 +360,7 @@ function Timeline({ )} {!!items.length ? ( <> -
    +
      {items.map((status) => ( ))} - {showMore && uiState === 'loading' && ( - <> -
    • - -
    • -
    • - -
    • - - )} + {showMore && + uiState === 'loading' && + (view === 'media' ? null : ( + <> +
    • + +
    • +
    • + +
    • + + ))}
    {uiState === 'default' && (showMore ? ( @@ -399,11 +416,19 @@ function Timeline({ ) : uiState === 'loading' ? (
      - {Array.from({ length: 5 }).map((_, i) => ( -
    • - -
    • - ))} + {Array.from({ length: 5 }).map((_, i) => + view === 'media' ? ( +
      + ) : ( +
    • + +
    • + ), + )}
    ) : ( uiState !== 'error' &&

    {emptyText}

    @@ -426,7 +451,7 @@ function Timeline({ ); } -function TimelineItem({ status, instance, useItemID, allowFilters }) { +function TimelineItem({ status, instance, useItemID, allowFilters, view }) { const { id: statusID, reblog, items, type, _pinned } = status; const actualStatusID = reblog?.id || statusID; const url = instance @@ -531,8 +556,33 @@ function TimelineItem({ status, instance, useItemID, allowFilters }) { ); }); } + + const itemKey = `timeline-${statusID + _pinned}`; + + if (view === 'media') { + return useItemID ? ( + + ) : ( + + ); + } + return ( -
  • +
  • {useItemID ? ( { - saveStatus(item, instance); + saveStatus(item, instance, { + skipThreading: media, // If media view, no need to form threads + }); }); maxID.current = value[value.length - 1].id; @@ -136,6 +143,8 @@ function Hashtags({ columnMode, ...props }) { fetchItems={fetchHashtags} checkForUpdates={checkForUpdates} useItemID + view={media ? 'media' : undefined} + refresh={media} headerEnd={ )} + Filters + { + if (media) { + searchParams.delete('media'); + } else { + searchParams.set('media', '1'); + } + setSearchParams(searchParams); + }} + > + {' '} + Media only + + {({ ref }) => (
    @@ -267,8 +293,8 @@ function Hashtags({ columnMode, ...props }) { // : `/t/${hashtags.join('+')}`, // ); location.hash = instance - ? `/${instance}/t/${hashtags.join('+')}` - : `/t/${hashtags.join('+')}`; + ? `/${instance}/t/${hashtags.join('+')}${linkParams}` + : `/t/${hashtags.join('+')}${linkParams}`; }} > @@ -287,6 +313,7 @@ function Hashtags({ columnMode, ...props }) { type: 'hashtag', hashtag: hashtags.join(' '), instance, + media: media ? 'on' : undefined, }; // Check if already exists const exists = states.shortcuts.some( @@ -300,7 +327,8 @@ function Hashtags({ columnMode, ...props }) { .split(/[\s+]+/) .sort() .join(' ') && - (s.instance ? s.instance === shortcut.instance : true), + (s.instance ? s.instance === shortcut.instance : true) && + (s.media ? !!s.media === !!shortcut.media : true), ); if (exists) { alert('This shortcut already exists'); @@ -324,7 +352,9 @@ function Hashtags({ columnMode, ...props }) { if (newInstance) { newInstance = newInstance.toLowerCase().trim(); // navigate(`/${newInstance}/t/${hashtags.join('+')}`); - location.hash = `/${newInstance}/t/${hashtags.join('+')}`; + location.hash = `/${newInstance}/t/${hashtags.join( + '+', + )}${linkParams}`; } }} >