diff --git a/frontend/package.json b/frontend/package.json index 00255f3..795b088 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "jsdom": "^29.1.1", "prettier": "^3.8.3", "vite": "^5.4.10", + "vite-plugin-mkcert": "^2.0.0", "vitest": "^4.1.7" } } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 563609b..af76593 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -14,12 +14,15 @@ import RelatedSubsRow from './components/RelatedSubsRow'; import SourceToggle from './components/SourceToggle'; import AdvancedSearch, { DEFAULT_ADVANCED, QUALITY_THRESHOLDS, countActiveFilters } from './components/AdvancedSearch'; import ActiveFiltersStrip from './components/ActiveFiltersStrip'; +import FeedMode from './components/FeedMode'; +import FeedExitGesture from './components/FeedExitGesture'; import { ToastProvider, useToast } from './components/Toast'; import { useFavorites } from './hooks/useFavorites'; import { useSavedSubreddits } from './hooks/useSavedSubreddits'; import { useHiddenAuthors } from './hooks/useBlockList'; import { usePreferences } from './hooks/usePreferences'; import { getInitialUrlState, useSyncUrlState } from './hooks/useUrlState'; +import { useMode } from './hooks/useMode'; import { fetchRedditUserMedia, fetchSubredditMedia, @@ -275,6 +278,8 @@ function AppShell() { useSyncUrlState(urlState); + const { mode, setMode } = useMode(); + const currentSnapshot = useMemo( () => createFeedSnapshot({ subreddit, authorView, sort, includeNsfw, mediaFilter, order, redditFilters }), [subreddit, authorView, sort, includeNsfw, mediaFilter, order, redditFilters] @@ -786,6 +791,14 @@ function AppShell() { return { videos, images, audio, all: items.length }; }, [items]); + if (mode === 'feed') { + return ( + setMode('grid')}> + + + ); + } + return (
setAdvancedOpen(true)} advancedFilterCount={countActiveFilters(advanced)} + onEnterFeed={() => setMode('feed')} /> {source === 'reddit' && ( diff --git a/frontend/src/components/FeedExitGesture.jsx b/frontend/src/components/FeedExitGesture.jsx new file mode 100644 index 0000000..466dd07 --- /dev/null +++ b/frontend/src/components/FeedExitGesture.jsx @@ -0,0 +1,71 @@ +import { useEffect } from 'react'; + +const EDGE_SWIPE_START_THRESHOLD_PX = 25; +const EDGE_SWIPE_DISTANCE_THRESHOLD_PX = 60; +const EDGE_SWIPE_VERTICAL_TOLERANCE_PX = 40; + +function FeedExitGesture({ onExit, children }) { + useEffect(() => { + let touchStart = null; + + function handleKeyDown(e) { + if (e.key === 'Escape') { + e.preventDefault(); + window.history.back(); + } + } + + function handlePopState() { + const urlMode = new URLSearchParams(window.location.search).get('mode'); + if (urlMode !== 'feed') { + onExit(); + } + } + + function handleTouchStart(e) { + const t = e.touches[0]; + if (!t) return; + if (t.clientX <= EDGE_SWIPE_START_THRESHOLD_PX) { + touchStart = { x: t.clientX, y: t.clientY }; + } else { + touchStart = null; + } + } + + function handleTouchMove(e) { + if (!touchStart) return; + const t = e.touches[0]; + if (!t) return; + const dx = t.clientX - touchStart.x; + const dy = Math.abs(t.clientY - touchStart.y); + if ( + dx > EDGE_SWIPE_DISTANCE_THRESHOLD_PX && + dy < EDGE_SWIPE_VERTICAL_TOLERANCE_PX + ) { + touchStart = null; + window.history.back(); + } + } + + function handleTouchEnd() { + touchStart = null; + } + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('popstate', handlePopState); + window.addEventListener('touchstart', handleTouchStart, { passive: true }); + window.addEventListener('touchmove', handleTouchMove, { passive: true }); + window.addEventListener('touchend', handleTouchEnd, { passive: true }); + return () => { + window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('popstate', handlePopState); + window.removeEventListener('touchstart', handleTouchStart); + window.removeEventListener('touchmove', handleTouchMove); + window.removeEventListener('touchend', handleTouchEnd); + }; + }, [onExit]); + + return <>{children}; +} + +export default FeedExitGesture; diff --git a/frontend/src/components/FeedItem.jsx b/frontend/src/components/FeedItem.jsx new file mode 100644 index 0000000..dbb8461 --- /dev/null +++ b/frontend/src/components/FeedItem.jsx @@ -0,0 +1,49 @@ +import VideoPlayer from './VideoPlayer'; +import { getModalItems } from '../utils/media'; + +function FeedItem({ item }) { + const modalItems = getModalItems(item); + const first = modalItems[0]; + if (!first) return null; + + if (first.kind === 'video') { + return ( +
+ +
+ ); + } + + if (first.kind === 'image') { + return ( +
+ +
+ ); + } + + if (first.kind === 'audio' || first.kind === 'embed') { + return null; + } + + if (first.url) { + return ( +
+ +
+ ); + } + + return null; +} + +export default FeedItem; diff --git a/frontend/src/components/FeedMode.jsx b/frontend/src/components/FeedMode.jsx new file mode 100644 index 0000000..3197202 --- /dev/null +++ b/frontend/src/components/FeedMode.jsx @@ -0,0 +1,14 @@ +import FeedItem from './FeedItem'; +import '../styles/feed.css'; + +function FeedMode({ items }) { + return ( +
+ {items.map((item) => ( + + ))} +
+ ); +} + +export default FeedMode; diff --git a/frontend/src/components/TopBar.jsx b/frontend/src/components/TopBar.jsx index 32ff88b..8ca27b2 100644 --- a/frontend/src/components/TopBar.jsx +++ b/frontend/src/components/TopBar.jsx @@ -93,7 +93,8 @@ function TopBar({ blueskySort, onBlueskySortChange, onOpenAdvanced, - advancedFilterCount = 0 + advancedFilterCount = 0, + onEnterFeed }) { const isEporner = source === 'eporner'; const isBooru = source === 'booru'; @@ -189,6 +190,14 @@ function TopBar({ )} + {onEnterFeed && ( + + )}
); diff --git a/frontend/src/components/VideoPlayer.jsx b/frontend/src/components/VideoPlayer.jsx index d708921..1c31dbb 100644 --- a/frontend/src/components/VideoPlayer.jsx +++ b/frontend/src/components/VideoPlayer.jsx @@ -33,6 +33,7 @@ const VideoPlayer = forwardRef(function VideoPlayer( className, autoPlay = true, loop = true, + controls = true, prebufferOnly = false, allowUnmutedAutoplay = false, preferredMuted = null, @@ -565,7 +566,7 @@ const VideoPlayer = forwardRef(function VideoPlayer(