-
Notifications
You must be signed in to change notification settings - Fork 1
Fullscreen Mode as own pages #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { ScrollRefProvider } from "@/hooks/use-scroll-ref"; | ||
| import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons"; | ||
| import { Link, Stack, usePathname } from "expo-router"; | ||
| import { ComponentType, ReactNode } from "react"; | ||
|
|
||
| export function withFullscreenLayout( | ||
| LayoutComp: ComponentType<{ children: ReactNode }>, | ||
| isFullscreen: boolean, | ||
| ) { | ||
| function RootLayout() { | ||
| const pathname = usePathname(); | ||
| const navigationName = isFullscreen | ||
| ? pathname.replace("fullscreen", "map") | ||
| : pathname.replace("map", "fullscreen"); | ||
|
|
||
| const stack = ( | ||
| <Stack | ||
| screenOptions={{ | ||
| headerRight: () => ( | ||
| // @ts-ignore Can't be dynamically inferred | ||
| <Link asChild href={navigationName}> | ||
| <MaterialCommunityIcons | ||
| name={isFullscreen ? "fullscreen-exit" : "fullscreen"} | ||
| size={28} | ||
| color="inherit" | ||
| /> | ||
| </Link> | ||
| ), | ||
| }} | ||
| /> | ||
| ); | ||
|
|
||
| return ( | ||
| <ScrollRefProvider> | ||
| <LayoutComp>{stack}</LayoutComp> | ||
| </ScrollRefProvider> | ||
| ); | ||
| } | ||
|
|
||
| return RootLayout; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import useMoveTo from "@/hooks/use-move-to"; | ||
| import useWikiQuery from "@/hooks/use-wiki-query"; | ||
| import { useLocalSearchParams } from "expo-router"; | ||
| import { useEffect } from "react"; | ||
| import PageRootView from "./_page-root-view"; | ||
| import { usePushCityVisit } from "@/hooks/visited-cities"; | ||
|
|
||
| export function generatePage(isFullscreen: boolean) { | ||
| return function Page() { | ||
| let { pageId } = useLocalSearchParams(); | ||
| pageId = typeof pageId === "string" ? pageId : pageId[0]; | ||
| const pageQuery = useWikiQuery(pageId); | ||
| const moveTo = useMoveTo(); | ||
|
|
||
| useEffect(() => { | ||
| if (pageQuery.data) { | ||
| moveTo( | ||
| // @ts-ignore NEEDS FIXING WHEN GEO REVISED | ||
| parseFloat(pageQuery.data.properties.geo["2"]), | ||
| // @ts-ignore NEEDS FIXING WHEN GEO REVISED | ||
| parseFloat(pageQuery.data.properties.geo["1"]), | ||
| // @ts-ignore NEEDS FIXING WHEN GEO REVISED | ||
| parseFloat(pageQuery.data.properties.geo?.zoom ?? "13"), | ||
| ); | ||
| } | ||
| }, [pageQuery.data, moveTo]); | ||
|
|
||
| usePushCityVisit({ id: pageId, title: pageQuery.data?.properties.title }); | ||
|
|
||
| return ( | ||
| <PageRootView | ||
| pageQuery={pageQuery} | ||
| id={pageId} | ||
| isFullscreen={isFullscreen} | ||
| /> | ||
| ); | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,10 +1,9 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useIsFullscreen } from "@/hooks/use-is-fullscreen"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getCityAtom } from "@/utils/bookmarks"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { MapMarker, MarkerType, useMapStore } from "@/utils/store"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NodeType, RootNode } from "@bcye/structured-wikivoyage-types"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { UseQueryResult } from "@tanstack/react-query"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Link, Route, Stack } from "expo-router"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Link, Route, Stack, usePathname } from "expo-router"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useAtomValue } from "jotai/react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { filter, map, split, splitEvery } from "ramda"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useEffect } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -14,23 +13,28 @@ import { Card, SkeletonView, Text, View } from "react-native-ui-lib"; | |||||||||||||||||||||||||||||||||||||||||||||||||||
| function PageContent({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pageQuery, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isFullscreen, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pageQuery: UseQueryResult<RootNode, Error>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isFullscreen: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bookmarks = useAtomValue(getCityAtom(id)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const registerMarker = useMapStore((s) => s.registerMarker); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const deregisterMarker = useMapStore((s) => s.deregisterMarker); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| function registerBookmarks() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isFullscreen) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const markers: MapMarker[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [bId, bookmark] of Object.entries(bookmarks)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [lat, long] = map(parseFloat, split(",", bId)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const marker: MapMarker = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: bId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // somehow broken else | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| link: `/main/explore/page/${id}/section/${bookmark.section}` as Route, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // if this is fullscreen, the markers are not gonna be available anywhere so it is safe to hardcode this to map view. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| link: `/main/explore/page/${id}/map/section/${bookmark.section}` as Route, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clarify the misleading comment. The comment on lines 36-37 states "if this is fullscreen, the markers are not gonna be available anywhere," but the early return at line 28 ensures this code never executes when Apply this diff to clarify the comment: if (isFullscreen) return;
const markers: MapMarker[] = [];
for (const [bId, bookmark] of Object.entries(bookmarks)) {
const [lat, long] = map(parseFloat, split(",", bId));
const marker: MapMarker = {
id: bId,
- // somehow broken else
- // if this is fullscreen, the markers are not gonna be available anywhere so it is safe to hardcode this to map view.
+ // Markers are only registered in map view (non-fullscreen).
+ // Link hardcoded to map route since that's where bookmarks are displayed.
link: `/main/explore/page/${id}/map/section/${bookmark.section}` as Route,
lat,
long,
type: MarkerType.Bookmark,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| lat, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| long, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: MarkerType.Bookmark, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -45,7 +49,7 @@ function PageContent({ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| [bookmarks, id, registerMarker, deregisterMarker], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| [bookmarks, id, registerMarker, deregisterMarker, isFullscreen], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return map( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -71,12 +75,12 @@ function PageContent({ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| export default function PageRootView({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pageQuery, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isFullscreen, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pageQuery: UseQueryResult<RootNode, Error>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isFullscreen: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isFullscreen = useIsFullscreen(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <View padding-8 flex> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Stack.Screen | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -94,11 +98,19 @@ export default function PageRootView({ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : pageQuery.data && id ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| !isFullscreen ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <BottomSheetScrollView> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <PageContent id={id} pageQuery={pageQuery} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <PageContent | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| id={id} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pageQuery={pageQuery} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isFullscreen={isFullscreen} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </BottomSheetScrollView> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ScrollView> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <PageContent id={id} pageQuery={pageQuery} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <PageContent | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| id={id} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pageQuery={pageQuery} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| isFullscreen={isFullscreen} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ScrollView> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : null | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -118,12 +130,18 @@ export default function PageRootView({ | |||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param pageId - The identifier for the page, used to construct the dynamic navigation route. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| function Infocard({ title, pageId }: { title: string; pageId: string }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pathname = usePathname(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Link | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| asChild | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| href={{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pathname: "/main/explore/page/[pageId]/section/[title]", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| params: { pageId, title }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ugly debounce, pathname gets updated before navigation | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // if screen stalls we end up navigating to section/X/section/X -> 404 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-ignore cannot be inferred | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pathname: pathname.includes("section") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? pathname | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : pathname + "/section/[title]", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| params: { title }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+133
to
145
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strengthen the pathname logic and address type suppression. The current substring check Additionally, the Consider these improvements:
- // ugly debounce, pathname gets updated before navigation
- // if screen stalls we end up navigating to section/X/section/X -> 404
- // @ts-ignore cannot be inferred
- pathname: pathname.includes("section")
- ? pathname
- : pathname + "/section/[title]",
+ pathname: pathname.split("/").includes("section")
+ ? pathname
+ : (pathname + "/section/[title]") as Route,
params: { title },
📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Card flex padding-12 height={48}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import WikiContent from "@/components/render-node"; | ||
| import { useScrollRef } from "@/hooks/use-scroll-ref"; | ||
| import useWikiQuery from "@/hooks/use-wiki-query"; | ||
| import { citiesAtom, getCityAtom } from "@/utils/bookmarks"; | ||
| import { NodeType, SectionNode } from "@bcye/structured-wikivoyage-types"; | ||
| import { BottomSheetScrollView } from "@gorhom/bottom-sheet"; | ||
| import { Stack, useLocalSearchParams } from "expo-router"; | ||
| import { useAtom } from "jotai/react"; | ||
| import { append, assoc, dissoc } from "ramda"; | ||
| import { useCallback } from "react"; | ||
| import { ScrollView } from "react-native"; | ||
| import { SkeletonView, View } from "react-native-ui-lib"; | ||
| import { toast } from "sonner-native"; | ||
|
|
||
| export function generateSection(isFullscreen: boolean) { | ||
| /** | ||
| * Renders a specific section of a Wikipedia page in Markdown format. | ||
| * | ||
| * The component retrieves the page title and ID from local search parameters, fetches the page's wikitext | ||
| * via a TRPC query, and then extracts and converts the designated section—identified by the title—to Markdown. | ||
| * It slices off the section header from the converted Markdown and displays a skeleton view while the content loads. | ||
| */ | ||
| return function Section() { | ||
| const { title, pageId } = useLocalSearchParams(); | ||
| const wikiQuery = useWikiQuery(pageId as string); | ||
| const section = wikiQuery.data?.children.find( | ||
| (c) => c.type === NodeType.Section && c.properties.title === title, | ||
| ) as SectionNode | undefined; | ||
| const ref = useScrollRef(); | ||
| const [bookmarks, setBookmarks] = useAtom(getCityAtom(pageId as string)); | ||
| const [cities, setCities] = useAtom(citiesAtom); | ||
| const pageTitle = wikiQuery.data?.properties.title; | ||
|
|
||
| const isBookmarked = useCallback( | ||
| function isBookmarked(id: string) { | ||
| return !!bookmarks[id]; | ||
| }, | ||
| [bookmarks], | ||
| ); | ||
|
|
||
| const toggleBookmarked = useCallback( | ||
| function toggleBookmarked(id: string) { | ||
| if (id == ",") { | ||
| toast.error( | ||
| "This listing doesn't have a location and can't be bookmarked. Add one on en.wikivoyage.org", | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| if (isBookmarked(id)) { | ||
| setBookmarks(dissoc(id, bookmarks)); | ||
| } else { | ||
| if (!cities.find((c) => c.qid === pageId)) { | ||
| setCities( | ||
| append({ qid: pageId as string, name: pageTitle! }, cities), | ||
| ); | ||
| } | ||
| // no valid lat-lng | ||
| setBookmarks( | ||
| assoc( | ||
| id, | ||
| { section: title, properties: section!.properties }, | ||
| bookmarks, | ||
| ), | ||
| ); | ||
| } | ||
| }, | ||
| [ | ||
| bookmarks, | ||
| setBookmarks, | ||
| section, | ||
| isBookmarked, | ||
| pageTitle, | ||
| cities, | ||
| setCities, | ||
| pageId, | ||
| title, | ||
| ], | ||
| ); | ||
|
|
||
| if (!section) return null; | ||
|
|
||
| return ( | ||
| <View padding-8 flex> | ||
| <Stack.Screen options={{ title: section.properties.title }} /> | ||
| <SkeletonView | ||
| template={SkeletonView.templates.TEXT_CONTENT} | ||
| showContent={wikiQuery.isSuccess} | ||
| renderContent={() => | ||
| !isFullscreen ? ( | ||
| <BottomSheetScrollView ref={ref}> | ||
| <WikiContent | ||
| node={section} | ||
| root={true} | ||
| isBookmarked={isBookmarked} | ||
| toggleBookmarked={toggleBookmarked} | ||
| /> | ||
| </BottomSheetScrollView> | ||
| ) : ( | ||
| <ScrollView> | ||
| <WikiContent | ||
| node={section} | ||
| root={true} | ||
| isBookmarked={isBookmarked} | ||
| toggleBookmarked={toggleBookmarked} | ||
| /> | ||
| </ScrollView> | ||
| ) | ||
| } | ||
| /> | ||
| </View> | ||
| ); | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import { Fragment } from "react"; | ||
| import { withFullscreenLayout } from "../_layout-base"; | ||
|
|
||
| export default withFullscreenLayout(Fragment, true); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| import { generatePage } from "../_main-base"; | ||
| export default generatePage(true); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { generateSection } from "../../_section-base"; | ||
|
|
||
| export default generateSection(true); |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard moveTo: skip on fullscreen and validate geo to avoid NaN camera updates.
If geo is missing/invalid, parseFloat yields NaN; calling moveTo with NaN can misbehave. Also no need to move camera in fullscreen.
🤖 Prompt for AI Agents