From 4e38836b2591756f937ebab4f11d2e28aa7dc154 Mon Sep 17 00:00:00 2001 From: KaWaite <34051327+KaWaite@users.noreply.github.com> Date: Sat, 4 Nov 2023 09:58:17 +0900 Subject: [PATCH] chore(web): beta story project publishing (#784) --- .../beta/features/Editor/Visualizer/hooks.ts | 29 ++-- .../beta/features/Editor/Visualizer/index.tsx | 23 ++- web/src/beta/features/Editor/hooks.ts | 14 +- web/src/beta/features/Editor/index.tsx | 11 +- .../Editor/tabs/map/RightPanel/index.tsx | 5 +- .../features/Editor/tabs/publish/Nav/hooks.ts | 65 ++++++-- .../Editor/tabs/publish/Nav/index.tsx | 13 +- .../Editor/tabs/widgets/Nav/index.tsx | 7 +- .../Editor/tabs/widgets/RightPanel/hooks.ts | 17 +-- web/src/beta/features/Editor/useLayers.ts | 11 +- .../features/Editor/useSecondaryNavbar.tsx | 12 +- .../beta/features/Editor/useStorytelling.ts | 75 +++------- .../PublishedVisualizer/convert-story.ts | 31 ++++ .../features/PublishedVisualizer/convert.ts | 29 ++++ .../{ => googleAnalytics}/ga.ts | 0 .../{ => googleAnalytics}/ga4.ts | 0 .../{ => googleAnalytics}/useGA.ts | 0 .../features/PublishedVisualizer/hooks.ts | 123 ++++++++------- .../features/PublishedVisualizer/index.tsx | 20 ++- .../features/PublishedVisualizer/types.ts | 26 +++- web/src/beta/lib/core/Map/types/index.ts | 58 ++++++- .../Block/builtin/Camera/Editor.tsx | 5 +- .../StoryPanel/Block/builtin/Text/index.tsx | 1 + .../core/StoryPanel/Block/builtin/index.ts | 5 +- .../beta/lib/core/StoryPanel/Page/hooks.ts | 9 +- .../beta/lib/core/StoryPanel/Page/index.tsx | 6 +- .../lib/core/StoryPanel/PanelContent/hooks.ts | 28 ++-- .../core/StoryPanel/PanelContent/index.tsx | 15 +- web/src/beta/lib/core/StoryPanel/constants.ts | 27 ++++ web/src/beta/lib/core/StoryPanel/hooks.ts | 87 ++++++++--- web/src/beta/lib/core/StoryPanel/index.tsx | 141 +++++++++--------- web/src/beta/lib/core/StoryPanel/types.ts | 1 + web/src/beta/lib/core/Visualizer/index.tsx | 2 +- .../Authentication/RootPage/hooks.ts | 2 +- .../organisms/Common/AssetContainer/hooks.ts | 2 +- .../components/organisms/Dashboard/hooks.ts | 6 +- .../organisms/EarthEditor/CanvasArea/hooks.ts | 8 +- .../EarthEditor/DataSourcePane/hooks.ts | 4 +- .../EarthEditor/DatasetInfoPane/hooks.ts | 2 +- .../organisms/EarthEditor/ExportPane/hooks.ts | 2 +- .../organisms/EarthEditor/Header/hooks.ts | 2 +- .../organisms/EarthEditor/LeftMenu/hooks.ts | 2 +- .../EarthEditor/OutlinePane/hooks.tsx | 8 +- .../EarthEditor/PrimitiveHeader/hooks.ts | 2 +- .../EarthEditor/PropertyPane/hooks-queries.ts | 2 +- .../EarthEditor/PropertyPane/hooks.ts | 6 +- .../organisms/EarthEditor/RightMenu/hooks.ts | 2 +- .../EarthEditor/TagPane/commonHooks.ts | 2 +- .../EarthEditor/core/CanvasArea/hooks.ts | 18 +-- .../organisms/GlobalModal/index.tsx | 6 +- .../organisms/Notification/hooks.ts | 2 +- .../organisms/Published/core/hooks.ts | 2 +- .../organisms/Settings/Account/hooks.ts | 2 +- .../Settings/Project/Dataset/hooks.ts | 2 +- .../Settings/Project/Plugin/hooks.ts | 2 +- .../Settings/Project/Public/hooks.ts | 4 +- .../organisms/Settings/Project/hooks.ts | 2 +- .../organisms/Settings/ProjectList/hooks.ts | 2 +- .../organisms/Settings/SettingPage/hooks.ts | 2 +- .../Settings/Workspace/Asset/hooks.ts | 2 +- .../organisms/Settings/Workspace/hooks.ts | 2 +- .../components/pages/Authentication/hooks.ts | 2 +- .../components/pages/EarthEditor/hooks.ts | 2 +- .../components/pages/Preview/index.tsx | 2 +- .../state/jotai.ts => classic/state/index.ts} | 2 +- .../services/api/storytellingApi/blocks.ts | 17 +-- web/src/services/api/storytellingApi/utils.ts | 2 + web/src/services/state/index.ts | 99 +++++++++++- 68 files changed, 730 insertions(+), 392 deletions(-) create mode 100644 web/src/beta/features/PublishedVisualizer/convert-story.ts create mode 100644 web/src/beta/features/PublishedVisualizer/convert.ts rename web/src/beta/features/PublishedVisualizer/{ => googleAnalytics}/ga.ts (100%) rename web/src/beta/features/PublishedVisualizer/{ => googleAnalytics}/ga4.ts (100%) rename web/src/beta/features/PublishedVisualizer/{ => googleAnalytics}/useGA.ts (100%) create mode 100644 web/src/beta/lib/core/StoryPanel/constants.ts rename web/src/{services/state/jotai.ts => classic/state/index.ts} (97%) diff --git a/web/src/beta/features/Editor/Visualizer/hooks.ts b/web/src/beta/features/Editor/Visualizer/hooks.ts index 813e72784e..f6546a1e7a 100644 --- a/web/src/beta/features/Editor/Visualizer/hooks.ts +++ b/web/src/beta/features/Editor/Visualizer/hooks.ts @@ -1,4 +1,3 @@ -import { useReactiveVar } from "@apollo/client"; import { useMemo, useEffect, useCallback } from "react"; import type { Alignment, Location } from "@reearth/beta/lib/core/Crust"; @@ -20,10 +19,11 @@ import { useIsCapturing, useSelectedBlock, useWidgetAlignEditorActivated, - selectedWidgetAreaVar, - isVisualizerReadyVar, + useSelectedWidgetArea, + useIsVisualizerReady, useZoomedLayerId, - selectedLayerVar, + useSelectedLayer, + useSelectedStoryPageId, } from "@reearth/services/state"; import { convertWidgets, processLayers } from "./convert"; @@ -58,15 +58,16 @@ export default ({ const [sceneMode, setSceneMode] = useSceneMode(); const [isCapturing, onIsCapturingChange] = useIsCapturing(); const [selectedBlock, selectBlock] = useSelectedBlock(); + const [_, selectSelectedStoryPageId] = useSelectedStoryPageId(); const [widgetAlignEditorActivated] = useWidgetAlignEditorActivated(); const [zoomedLayerId, zoomToLayer] = useZoomedLayerId(); - const selectedLayer = useReactiveVar(selectedLayerVar); + const [selectedLayer, setSelectedLayer] = useSelectedLayer(); - const selectedWidgetArea = useReactiveVar(selectedWidgetAreaVar); - const isVisualizerReady = useReactiveVar(isVisualizerReadyVar); + const [selectedWidgetArea, setSelectedWidgetArea] = useSelectedWidgetArea(); + const [isVisualizerReady, setIsVisualizerReady] = useIsVisualizerReady(); - const handleMount = useCallback(() => isVisualizerReadyVar(true), []); + const handleMount = useCallback(() => setIsVisualizerReady(true), [setIsVisualizerReady]); const onBlockMove = useCallback( async (_id: string, _fromIndex: number, _toIndex: number) => { @@ -125,11 +126,11 @@ export default ({ ) => { if (id === selectedLayer?.layerId && featureId === selectedLayer?.featureId) return; - selectedLayerVar( + setSelectedLayer( id ? { layerId: id, featureId, layer: await layer?.(), layerSelectionReason } : undefined, ); }, - [selectedLayer], + [selectedLayer, setSelectedLayer], ); const onBlockChange = useCallback( @@ -205,6 +206,11 @@ export default ({ [storyId, scene?.stories], ); + const handleCurrentPageChange = useCallback( + (pageId?: string) => selectSelectedStoryPageId(pageId), + [selectSelectedStoryPageId], + ); + const handleStoryBlockCreate = useCallback( async (pageId?: string, extensionId?: string, pluginId?: string, index?: number) => { if (!extensionId || !pluginId || !storyId || !pageId) return; @@ -272,8 +278,9 @@ export default ({ engineMeta, useExperimentalSandbox, isVisualizerReady, - selectWidgetArea: selectedWidgetAreaVar, + selectWidgetArea: setSelectedWidgetArea, zoomedLayerId, + handleCurrentPageChange, handleStoryBlockCreate, handleStoryBlockDelete, handlePropertyValueUpdate, diff --git a/web/src/beta/features/Editor/Visualizer/index.tsx b/web/src/beta/features/Editor/Visualizer/index.tsx index cea024707e..2a8a4564b3 100644 --- a/web/src/beta/features/Editor/Visualizer/index.tsx +++ b/web/src/beta/features/Editor/Visualizer/index.tsx @@ -2,7 +2,10 @@ import { MutableRefObject, useCallback } from "react"; import ContentPicker from "@reearth/beta/components/ContentPicker"; import type { MapRef } from "@reearth/beta/lib/core/Map/ref"; -import StoryPanel, { type InstallableStoryBlock } from "@reearth/beta/lib/core/StoryPanel"; +import StoryPanel, { + StoryPanelRef, + type InstallableStoryBlock, +} from "@reearth/beta/lib/core/StoryPanel"; import CoreVisualizer, { type Props as VisualizerProps } from "@reearth/beta/lib/core/Visualizer"; import type { Camera } from "@reearth/beta/utils/value"; import type { Story, Page } from "@reearth/services/api/storytellingApi/utils"; @@ -18,12 +21,11 @@ export type Props = { inEditor?: boolean; currentCamera?: Camera; // storytelling + storyPanelRef?: MutableRefObject; showStoryPanel?: boolean; selectedStory?: Story; currentPage?: Page; - isAutoScrolling?: MutableRefObject; installableBlocks?: InstallableStoryBlock[]; - onCurrentPageChange: (id: string, disableScrollIntoView?: boolean) => void; onStoryBlockMove: (id: string, targetId: number, blockId: string) => void; onCameraChange: (camera: Camera) => void; }; @@ -34,12 +36,11 @@ const Visualizer: React.FC = ({ isBuilt, inEditor, currentCamera, + storyPanelRef, showStoryPanel, selectedStory, currentPage, - isAutoScrolling, installableBlocks, - onCurrentPageChange, onStoryBlockMove, onCameraChange, }) => { @@ -60,6 +61,7 @@ const Visualizer: React.FC = ({ useExperimentalSandbox, isVisualizerReady: _isVisualizerReady, zoomedLayerId, + handleCurrentPageChange, handleStoryBlockCreate, handleStoryBlockDelete, handlePropertyValueUpdate, @@ -129,25 +131,18 @@ const Visualizer: React.FC = ({ renderInfoboxInsertionPopup={renderInfoboxInsertionPopUp}> {showStoryPanel && ( )} - {/* */} ); }; diff --git a/web/src/beta/features/Editor/hooks.ts b/web/src/beta/features/Editor/hooks.ts index b02d06e07a..64d0d2a6ed 100644 --- a/web/src/beta/features/Editor/hooks.ts +++ b/web/src/beta/features/Editor/hooks.ts @@ -1,4 +1,3 @@ -import { useReactiveVar } from "@apollo/client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { devices } from "@reearth/beta/features/Editor/tabs/widgets/Nav/Devices"; @@ -7,8 +6,8 @@ import type { FlyTo } from "@reearth/beta/lib/core/types"; import type { Camera } from "@reearth/beta/utils/value"; import { useWidgetAlignEditorActivated, - isVisualizerReadyVar, - currentCameraVar, + useIsVisualizerReady, + useCurrentCamera, } from "@reearth/services/state"; import type { Tab } from "../Navbar"; @@ -19,8 +18,8 @@ import type { Device } from "./tabs/widgets/Nav"; export default ({ tab }: { sceneId: string; tab: Tab }) => { const visualizerRef = useRef(null); - const isVisualizerReady = useReactiveVar(isVisualizerReadyVar); - const currentCamera = useReactiveVar(currentCameraVar); + const [isVisualizerReady] = useIsVisualizerReady(); + const [currentCamera, setCurrentCamera] = useCurrentCamera(); const [selectedSceneSetting, setSceneSetting] = useState(false); const [selectedDevice, setDevice] = useState("desktop"); @@ -85,7 +84,10 @@ export default ({ tab }: { sceneId: string; tab: Tab }) => { [isVisualizerReady], ); - const handleCameraUpdate = useCallback((camera: Camera) => currentCameraVar(camera), []); + const handleCameraUpdate = useCallback( + (camera: Camera) => setCurrentCamera(camera), + [setCurrentCamera], + ); return { visualizerRef, diff --git a/web/src/beta/features/Editor/index.tsx b/web/src/beta/features/Editor/index.tsx index 6172028e26..d3314301e3 100644 --- a/web/src/beta/features/Editor/index.tsx +++ b/web/src/beta/features/Editor/index.tsx @@ -46,8 +46,8 @@ const Editor: React.FC = ({ sceneId, projectId, workspaceId, tab }) => { const { selectedStory, + storyPanelRef, currentPage, - isAutoScrolling, installableStoryBlocks, handleCurrentPageChange, handlePageDuplicate, @@ -58,7 +58,6 @@ const Editor: React.FC = ({ sceneId, projectId, workspaceId, tab }) => { handlePageUpdate, } = useStorytelling({ sceneId, - onFlyTo: handleFlyTo, }); const { @@ -152,7 +151,8 @@ const Editor: React.FC = ({ sceneId, projectId, workspaceId, tab }) => { const { secondaryNavbar } = useSecondaryNavbar({ tab, - projectId, + sceneId, + id: selectedProjectType === "story" ? selectedStory?.id : projectId, selectedDevice, selectedProjectType, showWidgetEditor, @@ -190,14 +190,13 @@ const Editor: React.FC = ({ sceneId, projectId, workspaceId, tab }) => { diff --git a/web/src/beta/features/Editor/tabs/map/RightPanel/index.tsx b/web/src/beta/features/Editor/tabs/map/RightPanel/index.tsx index 4822af3478..b1a4b3bf4b 100644 --- a/web/src/beta/features/Editor/tabs/map/RightPanel/index.tsx +++ b/web/src/beta/features/Editor/tabs/map/RightPanel/index.tsx @@ -1,4 +1,3 @@ -import { useReactiveVar } from "@apollo/client"; import { useMemo } from "react"; import SceneSettings from "@reearth/beta/features/Editor/Settings"; @@ -12,7 +11,7 @@ import { NLSLayer } from "@reearth/services/api/layersApi/utils"; import { LayerStyle } from "@reearth/services/api/layerStyleApi/utils"; import { convert } from "@reearth/services/api/propertyApi/utils"; import { useT } from "@reearth/services/i18n"; -import { selectedLayerVar } from "@reearth/services/state"; +import { useSelectedLayer } from "@reearth/services/state"; import LayerInspector from "./LayerInspector"; import LayerStyleEditor from "./LayerStyleValueEditor"; @@ -48,7 +47,7 @@ const MapRightPanel: React.FC = ({ const scenePropertyId = useMemo(() => scene?.property?.id, [scene?.property?.id]); const sceneSettings = useMemo(() => convert(scene?.property), [scene?.property]); - const selectedLayerId = useReactiveVar(selectedLayerVar); + const [selectedLayerId] = useSelectedLayer(); return ( { - const handleNavigationToSettings = useSettingsNavigation({ projectId }); +export type ProjectType = "default" | "story"; + +type SelectedProject = { + id: string; + alias?: string; + publishmentStatus?: string; +}; + +export default ({ + id, + sceneId, + selectedProjectType, +}: { + id?: string; + sceneId?: string; + selectedProjectType?: ProjectType; +}) => { + const handleNavigationToSettings = useSettingsNavigation({ projectId: id }); + const [project, setProject] = useState(); + + // Regular Project const { + publishProjectLoading, useProjectQuery, useProjectAliasCheckLazyQuery, usePublishProject, - publishProjectLoading, } = useProjectFetcher(); - const { project } = useProjectQuery(projectId); + const { project: defaultProject } = useProjectQuery(id); + + // Storytelling Project + const { useStoriesQuery, usePublishStory } = useStorytellingFetcher(); + + const { stories } = useStoriesQuery({ sceneId }); + + // General + useEffect(() => { + setProject(() => { + const proj = + selectedProjectType === "story" ? stories?.find(s => s.id === id) : defaultProject; + return proj + ? { + id: proj.id, + alias: proj.alias, + publishmentStatus: proj.publishmentStatus, + } + : undefined; + }); + }, [id, defaultProject, stories, selectedProjectType]); const [publishing, setPublishing] = useState("unpublishing"); const [dropdownOpen, setDropdown] = useState(false); @@ -51,21 +90,23 @@ export default ({ projectId }: { projectId?: string }) => { ); }, [validatingAlias, checkProjectAliasData, project]); - const publishStatus: PublishStatus = useMemo(() => { - const status = + const publishStatus: PublishStatus = useMemo( + () => project?.publishmentStatus === "PUBLIC" ? "published" : project?.publishmentStatus === "LIMITED" ? "limited" - : "unpublished"; - return status; - }, [project?.publishmentStatus]); + : "unpublished", + [project?.publishmentStatus], + ); const handleProjectPublish = useCallback( async (alias: string | undefined, publishStatus: PublishStatus) => { - await usePublishProject(publishStatus, projectId, alias); + selectedProjectType === "story" + ? await usePublishStory(publishStatus, project?.id, alias) + : await usePublishProject(publishStatus, project?.id, alias); }, - [projectId, usePublishProject], + [project?.id, selectedProjectType, usePublishStory, usePublishProject], ); const handleOpenProjectSettings = useCallback(() => { diff --git a/web/src/beta/features/Editor/tabs/publish/Nav/index.tsx b/web/src/beta/features/Editor/tabs/publish/Nav/index.tsx index 6eb0f8219c..92be0283dc 100644 --- a/web/src/beta/features/Editor/tabs/publish/Nav/index.tsx +++ b/web/src/beta/features/Editor/tabs/publish/Nav/index.tsx @@ -10,21 +10,22 @@ import { config } from "@reearth/services/config"; import { useT } from "@reearth/services/i18n"; import { styled } from "@reearth/services/theme"; -import useHooks from "./hooks"; +import useHooks, { type ProjectType } from "./hooks"; import PublishModal from "./PublishModal"; import { PublishStatus } from "./PublishModal/hooks"; -export { SECONDARY_NAVBAR_HEIGHT } from "@reearth/beta/features/Editor/SecondaryNav"; +export type { ProjectType } from "./hooks"; -export type ProjectType = "default" | "story"; +export { SECONDARY_NAVBAR_HEIGHT } from "@reearth/beta/features/Editor/SecondaryNav"; type Props = { - projectId?: string; + id?: string; + sceneId?: string; selectedProjectType?: ProjectType; onProjectTypeChange: (type: ProjectType) => void; }; -const Nav: React.FC = ({ projectId, selectedProjectType, onProjectTypeChange }) => { +const Nav: React.FC = ({ id, sceneId, selectedProjectType, onProjectTypeChange }) => { const t = useT(); const { @@ -43,7 +44,7 @@ const Nav: React.FC = ({ projectId, selectedProjectType, onProjectTypeCha handleProjectAliasCheck, handleOpenProjectSettings, handleNavigationToSettings, - } = useHooks({ projectId }); + } = useHooks({ id, sceneId, selectedProjectType }); const text = useMemo( () => diff --git a/web/src/beta/features/Editor/tabs/widgets/Nav/index.tsx b/web/src/beta/features/Editor/tabs/widgets/Nav/index.tsx index 1773f79321..c0a95b31a6 100644 --- a/web/src/beta/features/Editor/tabs/widgets/Nav/index.tsx +++ b/web/src/beta/features/Editor/tabs/widgets/Nav/index.tsx @@ -4,7 +4,7 @@ import Text from "@reearth/beta/components/Text"; import Toggle from "@reearth/beta/components/Toggle"; import SecondaryNav from "@reearth/beta/features/Editor/SecondaryNav"; import { useT } from "@reearth/services/i18n"; -import { selectedWidgetAreaVar } from "@reearth/services/state"; +import { useSelectedWidgetArea } from "@reearth/services/state"; import { styled } from "@reearth/services/theme"; import Devices, { type Device } from "./Devices"; @@ -27,12 +27,13 @@ const Nav: React.FC = ({ onDeviceChange, }) => { const t = useT(); + const [, setSelectedWidgetArea] = useSelectedWidgetArea(); useEffect(() => { if (!showWidgetEditor) { - selectedWidgetAreaVar(undefined); + setSelectedWidgetArea(undefined); } - }, [showWidgetEditor]); + }, [showWidgetEditor, setSelectedWidgetArea]); return ( diff --git a/web/src/beta/features/Editor/tabs/widgets/RightPanel/hooks.ts b/web/src/beta/features/Editor/tabs/widgets/RightPanel/hooks.ts index 1c2fb4b8d6..a13185ae9b 100644 --- a/web/src/beta/features/Editor/tabs/widgets/RightPanel/hooks.ts +++ b/web/src/beta/features/Editor/tabs/widgets/RightPanel/hooks.ts @@ -1,10 +1,9 @@ -import { useReactiveVar } from "@apollo/client"; import { useCallback, useMemo } from "react"; import { useWidgetsFetcher } from "@reearth/services/api"; import { - selectedWidgetVar, - selectedWidgetAreaVar, + useSelectedWidget, + useSelectedWidgetArea, type WidgetAreaState, } from "@reearth/services/state"; @@ -19,8 +18,8 @@ export default ({ sceneId }: { sceneId?: string }) => { const { installableWidgets } = useInstallableWidgetsQuery({ sceneId }); const { installedWidgets } = useInstalledWidgetsQuery({ sceneId }); - const selectedWidget = useReactiveVar(selectedWidgetVar); - const selectedWidgetArea = useReactiveVar(selectedWidgetAreaVar); + const [selectedWidget, setSelectedWidget] = useSelectedWidget(); + const [selectedWidgetArea, setSelectedWidgetArea] = useSelectedWidgetArea(); const propertyItems = useMemo( () => installedWidgets?.find(w => w.id === selectedWidget?.id)?.property.items, @@ -32,9 +31,9 @@ export default ({ sceneId }: { sceneId?: string }) => { if (!w) return; if (w.id === selectedWidget?.id) { - selectedWidgetVar(undefined); + setSelectedWidget(undefined); } else { - selectedWidgetVar({ + setSelectedWidget({ id: w.id, pluginId: w.pluginId, extensionId: w.extensionId, @@ -62,10 +61,10 @@ export default ({ sceneId }: { sceneId?: string }) => { if (!sceneId || !widgetAreaState) return; const results = await useUpdateWidgetAlignSystem(widgetAreaState, sceneId); if (results.status === "success") { - selectedWidgetAreaVar(widgetAreaState); + setSelectedWidgetArea(widgetAreaState); } }, - [sceneId, useUpdateWidgetAlignSystem], + [sceneId, useUpdateWidgetAlignSystem, setSelectedWidgetArea], ); return { diff --git a/web/src/beta/features/Editor/useLayers.ts b/web/src/beta/features/Editor/useLayers.ts index cd87f60f97..8c4531be04 100644 --- a/web/src/beta/features/Editor/useLayers.ts +++ b/web/src/beta/features/Editor/useLayers.ts @@ -1,11 +1,10 @@ -import { useReactiveVar } from "@apollo/client"; import { MutableRefObject, useCallback, useMemo } from "react"; import { MapRef } from "@reearth/beta/lib/core/Crust/types"; import { LayerSimple } from "@reearth/beta/lib/core/Map"; import { useLayersFetcher } from "@reearth/services/api"; import { useT } from "@reearth/services/i18n"; -import { selectedLayerVar } from "@reearth/services/state"; +import { useSelectedLayer } from "@reearth/services/state"; type LayerProps = { sceneId: string; @@ -43,7 +42,7 @@ export default function ({ sceneId, isVisualizerReady, visualizerRef }: LayerPro useLayersFetcher(); const { nlsLayers = [] } = useGetLayersQuery({ sceneId }); - const selectedLayerId = useReactiveVar(selectedLayerVar); + const [selectedLayerId, setSelectedLayerId] = useSelectedLayer(); const selectedLayer = useMemo( () => nlsLayers.find(l => l.id === selectedLayerId?.layerId) || undefined, @@ -55,15 +54,15 @@ export default function ({ sceneId, isVisualizerReady, visualizerRef }: LayerPro if (!isVisualizerReady) return; if (layerId && layerId !== selectedLayerId?.layerId) { - selectedLayerVar({ layerId }); + setSelectedLayerId({ layerId }); } else { - selectedLayerVar(undefined); + setSelectedLayerId(undefined); } // lib/core doesn't support selecting a layer without auto-selecting a feature, so // Either way, we want to deselect from core as we are either deselecting, or changing to a new layer visualizerRef?.current?.layers.select(undefined); }, - [selectedLayerId?.layerId, isVisualizerReady, visualizerRef], + [selectedLayerId?.layerId, isVisualizerReady, visualizerRef, setSelectedLayerId], ); const handleLayerDelete = useCallback( diff --git a/web/src/beta/features/Editor/useSecondaryNavbar.tsx b/web/src/beta/features/Editor/useSecondaryNavbar.tsx index 91527782cb..d00f6e0c65 100644 --- a/web/src/beta/features/Editor/useSecondaryNavbar.tsx +++ b/web/src/beta/features/Editor/useSecondaryNavbar.tsx @@ -6,7 +6,8 @@ import { Tab } from "@reearth/beta/features/Navbar"; type Props = { tab: Tab; - projectId?: string; + sceneId?: string; + id?: string; showWidgetEditor?: boolean; selectedDevice: Device; selectedProjectType?: ProjectType; @@ -17,7 +18,8 @@ type Props = { export default ({ tab, - projectId, + sceneId, + id, showWidgetEditor, selectedDevice, selectedProjectType, @@ -39,7 +41,8 @@ export default ({ case "publish": return ( @@ -51,7 +54,8 @@ export default ({ } }, [ tab, - projectId, + sceneId, + id, selectedDevice, selectedProjectType, showWidgetEditor, diff --git a/web/src/beta/features/Editor/useStorytelling.ts b/web/src/beta/features/Editor/useStorytelling.ts index 7d26ba2a2d..711d387642 100644 --- a/web/src/beta/features/Editor/useStorytelling.ts +++ b/web/src/beta/features/Editor/useStorytelling.ts @@ -1,14 +1,13 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useRef } from "react"; -import type { FlyTo } from "@reearth/beta/lib/core/types"; -import type { Camera } from "@reearth/beta/utils/value"; +import { StoryPanelRef } from "@reearth/beta/lib/core/StoryPanel"; import useStorytellingAPI from "@reearth/services/api/storytellingApi"; import type { Page } from "@reearth/services/api/storytellingApi/utils"; import { useT } from "@reearth/services/i18n"; +import { useSelectedStoryPageId } from "@reearth/services/state"; type Props = { sceneId: string; - onFlyTo: FlyTo; }; const getPage = (id?: string, pages?: Page[]) => { @@ -16,9 +15,12 @@ const getPage = (id?: string, pages?: Page[]) => { return pages.find(p => p.id === id); }; -export default function ({ sceneId, onFlyTo }: Props) { +export default function ({ sceneId }: Props) { const t = useT(); + const storyPanelRef = useRef(null); + const [selectedStoryPageId] = useSelectedStoryPageId(); + const { useStoriesQuery, useCreateStoryPage, @@ -32,58 +34,23 @@ export default function ({ sceneId, onFlyTo }: Props) { const { stories } = useStoriesQuery({ sceneId }); const { installableStoryBlocks } = useInstallableStoryBlocksQuery({ sceneId }); - const [currentPage, setCurrentPage] = useState(undefined); - const isAutoScrolling = useRef(false); - const [selectedPageId, setSelectedPageId] = useState(undefined); - const selectedStory = useMemo(() => { - return stories?.length ? stories[0] : undefined; - }, [stories]); + const selectedStory = useMemo(() => (stories?.length ? stories[0] : undefined), [stories]); - useEffect(() => { - if (selectedPageId) { - const newPage = getPage(selectedPageId, selectedStory?.pages); - if (newPage) { - setCurrentPage(newPage); - } - } else { - setCurrentPage(selectedStory?.pages?.[0]); - } - }, [currentPage, selectedPageId, selectedStory?.pages]); + const currentPage = useMemo( + () => selectedStory?.pages?.find(p => p.id === selectedStoryPageId), + [selectedStory?.pages, selectedStoryPageId], + ); const handleCurrentPageChange = useCallback( - (pageId: string, disableScrollIntoView?: boolean) => { + (pageId: string) => { + if (selectedStoryPageId && selectedStoryPageId === pageId) return; const newPage = getPage(pageId, selectedStory?.pages); - if (!newPage) return; - - setSelectedPageId(pageId); - - if (!disableScrollIntoView) { - const element = document.getElementById(newPage.id); - isAutoScrolling.current = true; - element?.scrollIntoView({ behavior: "smooth" }); - } - const cameraFieldGroup = newPage.property.items?.find( - i => i.schemaGroup === "cameraAnimation", - ); - const schemaFields = cameraFieldGroup?.schemaFields; - - let destination = schemaFields?.find(sf => sf.id === "cameraPosition") - ?.defaultValue as Camera; - let duration = schemaFields?.find(sf => sf.id === "cameraDuration")?.defaultValue as number; - - if (cameraFieldGroup && "fields" in cameraFieldGroup) { - destination = (cameraFieldGroup.fields.find(f => f.id === "cameraPosition")?.value ?? - destination) as Camera; - if (!destination) return; - - duration = (cameraFieldGroup.fields.find(f => f.id === "cameraDuration")?.value ?? - duration) as number; - - onFlyTo({ ...destination }, { duration }); + if (newPage) { + storyPanelRef?.current?.handleCurrentPageChange(pageId); } }, - [selectedStory?.pages, onFlyTo], + [selectedStoryPageId, selectedStory?.pages, storyPanelRef], ); const handlePageDuplicate = useCallback(async (pageId: string) => { @@ -102,11 +69,11 @@ export default function ({ sceneId, onFlyTo }: Props) { storyId: selectedStory.id, pageId, }); - if (pageId === currentPage?.id) { - setSelectedPageId(pages[deletedPageIndex + 1]?.id ?? pages[deletedPageIndex - 1]?.id); + if (pageId === selectedStoryPageId) { + handleCurrentPageChange(pages[deletedPageIndex + 1].id ?? pages[deletedPageIndex - 1].id); } }, - [selectedStory, useDeleteStoryPage, sceneId, currentPage], + [selectedStory, sceneId, selectedStoryPageId, handleCurrentPageChange, useDeleteStoryPage], ); const handlePageAdd = useCallback( @@ -164,9 +131,9 @@ export default function ({ sceneId, onFlyTo }: Props) { ); return { + storyPanelRef, selectedStory, currentPage, - isAutoScrolling, installableStoryBlocks, handleCurrentPageChange, handlePageDuplicate, diff --git a/web/src/beta/features/PublishedVisualizer/convert-story.ts b/web/src/beta/features/PublishedVisualizer/convert-story.ts new file mode 100644 index 0000000000..13a26abd5b --- /dev/null +++ b/web/src/beta/features/PublishedVisualizer/convert-story.ts @@ -0,0 +1,31 @@ +import { mapValues } from "lodash-es"; + +export const processStoryProperty = (p: any): any => { + if (typeof p !== "object") return p; + return mapValues(p, g => + Array.isArray(g) ? g.map(h => processPropertyGroup(h)) : processPropertyGroup(g), + ); +}; + +function processPropertyGroup(g: any): any { + if (typeof g !== "object") return g; + return mapValues(g, v => { + // For compability + if (Array.isArray(v)) { + return v.map(vv => + typeof v === "object" && v && "lat" in v && "lng" in v && "altitude" in v + ? { value: { ...vv, height: vv.altitude } } + : { value: vv }, + ); + } + if (typeof v === "object" && v && "lat" in v && "lng" in v && "altitude" in v) { + return { + value: { + ...v, + height: v.altitude, + }, + }; + } + return { value: v }; + }); +} diff --git a/web/src/beta/features/PublishedVisualizer/convert.ts b/web/src/beta/features/PublishedVisualizer/convert.ts new file mode 100644 index 0000000000..726403e388 --- /dev/null +++ b/web/src/beta/features/PublishedVisualizer/convert.ts @@ -0,0 +1,29 @@ +import { mapValues } from "lodash-es"; + +export const processProperty = (p: any): any => { + if (typeof p !== "object") return p; + return mapValues(p, g => + Array.isArray(g) ? g.map(h => processPropertyGroup(h)) : processPropertyGroup(g), + ); +}; + +const processPropertyGroup = (g: any): any => { + if (typeof g !== "object") return g; + return mapValues(g, v => { + // For compability + if (Array.isArray(v)) { + return v.map(vv => + typeof v === "object" && v && "lat" in v && "lng" in v && "altitude" in v + ? { ...vv, height: vv.altitude } + : vv, + ); + } + if (typeof v === "object" && v && "lat" in v && "lng" in v && "altitude" in v) { + return { + ...v, + height: v.altitude, + }; + } + return v; + }); +}; diff --git a/web/src/beta/features/PublishedVisualizer/ga.ts b/web/src/beta/features/PublishedVisualizer/googleAnalytics/ga.ts similarity index 100% rename from web/src/beta/features/PublishedVisualizer/ga.ts rename to web/src/beta/features/PublishedVisualizer/googleAnalytics/ga.ts diff --git a/web/src/beta/features/PublishedVisualizer/ga4.ts b/web/src/beta/features/PublishedVisualizer/googleAnalytics/ga4.ts similarity index 100% rename from web/src/beta/features/PublishedVisualizer/ga4.ts rename to web/src/beta/features/PublishedVisualizer/googleAnalytics/ga4.ts diff --git a/web/src/beta/features/PublishedVisualizer/useGA.ts b/web/src/beta/features/PublishedVisualizer/googleAnalytics/useGA.ts similarity index 100% rename from web/src/beta/features/PublishedVisualizer/useGA.ts rename to web/src/beta/features/PublishedVisualizer/googleAnalytics/useGA.ts diff --git a/web/src/beta/features/PublishedVisualizer/hooks.ts b/web/src/beta/features/PublishedVisualizer/hooks.ts index e4a4e024d0..6ac9d57130 100644 --- a/web/src/beta/features/PublishedVisualizer/hooks.ts +++ b/web/src/beta/features/PublishedVisualizer/hooks.ts @@ -1,5 +1,4 @@ -import { mapValues } from "lodash-es"; -import { useState, useMemo, useEffect, useRef } from "react"; +import { useState, useMemo, useEffect, useCallback } from "react"; import { InternalWidget, @@ -8,11 +7,15 @@ import { BuiltinWidgets, isBuiltinWidget, } from "@reearth/beta/lib/core/Crust"; -import { MapRef } from "@reearth/beta/lib/core/Crust/types"; +import { Story } from "@reearth/beta/lib/core/StoryPanel"; import { config } from "@reearth/services/config"; +import { useSelectedStoryPageId } from "@reearth/services/state"; import { processLayers } from "../Editor/Visualizer/convert"; +import { processProperty } from "./convert"; +import { processStoryProperty } from "./convert-story"; +import { useGA } from "./googleAnalytics/useGA"; import type { PublishedData, WidgetZone, @@ -20,13 +23,11 @@ import type { WidgetArea, WidgetAreaPadding, } from "./types"; -import { useGA } from "./useGA"; export default (alias?: string) => { const [data, setData] = useState(); const [ready, setReady] = useState(false); const [error, setError] = useState(false); - const visualizerRef = useRef(null); const sceneProperty = processProperty(data?.property); const pluginProperty = useMemo( @@ -38,21 +39,6 @@ export default (alias?: string) => { [data?.plugins], ); - const layers = useMemo( - () => - processLayers( - data?.nlsLayers?.map(l => ({ - id: l.id, - title: l.title, - config: l.config, - layerType: l.layerType, - visible: !!l.isVisible, - })) ?? [], - data?.layerStyles, - ), - [data?.nlsLayers, data?.layerStyles], - ); - const widgets = useMemo< | { floatingWidgets: InternalWidget[]; @@ -159,6 +145,65 @@ export default (alias?: string) => { [alias], ); + const story = useMemo(() => { + const s = data?.story; + const processedStory: Story | undefined = !s + ? undefined + : { + id: s.id, + title: s.title, + position: s.position, + pages: s.pages.map(p => { + return { + id: p.id, + swipeable: p.swipeable, + layerIds: p.layers, + property: processStoryProperty(p.property), + blocks: p.blocks.map(b => { + return { + id: b.id, + pluginId: b.pluginId, + extensionId: b.extensionId, + property: processStoryProperty(b.property), + }; + }), + }; + }), + }; + return processedStory; + }, [data?.story]); + + const [currentPageId, setCurrentPageId] = useSelectedStoryPageId(); + + const currentPage = useMemo( + () => story?.pages.find(p => p.id === currentPageId), + [currentPageId, story?.pages], + ); + + const handleCurrentPageChange = useCallback( + (pageId?: string) => setCurrentPageId(pageId), + [setCurrentPageId], + ); + + const layers = useMemo(() => { + const processedLayers = processLayers( + data?.nlsLayers?.map(l => ({ + id: l.id, + title: l.title, + config: l.config, + layerType: l.layerType, + visible: !!l.isVisible, + })) ?? [], + data?.layerStyles, + ); + if (!story) return processedLayers; + + return processedLayers?.map(layer => ({ + ...layer, + visible: currentPage?.layerIds?.includes(layer.id), + })); + }, [data?.nlsLayers, data?.layerStyles, currentPage?.layerIds, story]); + useEffect(() => { const url = dataUrl(actualAlias); (async () => { @@ -206,51 +251,21 @@ export default (alias?: string) => { useGA(sceneProperty); return { - visualizerRef, - alias: actualAlias, sceneProperty, pluginProperty, layers, widgets, + story, ready, error, engineMeta, + handleCurrentPageChange, }; }; -function processProperty(p: any): any { - if (typeof p !== "object") return p; - return mapValues(p, g => - Array.isArray(g) ? g.map(h => processPropertyGroup(h)) : processPropertyGroup(g), - ); -} - -function processPropertyGroup(g: any): any { - if (typeof g !== "object") return g; - return mapValues(g, v => { - // For compability - if (Array.isArray(v)) { - return v.map(vv => - typeof v === "object" && v && "lat" in v && "lng" in v && "altitude" in v - ? { value: { ...vv, height: vv.altitude } } - : { value: vv }, - ); - } - if (typeof v === "object" && v && "lat" in v && "lng" in v && "altitude" in v) { - return { - value: { - ...v, - height: v.altitude, - }, - }; - } - return { value: v }; - }); -} - -function dataUrl(alias?: string): string { +const dataUrl = (alias?: string): string => { if (alias && window.REEARTH_CONFIG?.api) { return `${window.REEARTH_CONFIG.api}/published_data/${alias}`; } return "data.json"; -} +}; diff --git a/web/src/beta/features/PublishedVisualizer/index.tsx b/web/src/beta/features/PublishedVisualizer/index.tsx index 149ed26c4f..7b08622fe0 100644 --- a/web/src/beta/features/PublishedVisualizer/index.tsx +++ b/web/src/beta/features/PublishedVisualizer/index.tsx @@ -1,4 +1,5 @@ import NotFound from "@reearth/beta/components/NotFound"; +import StoryPanel from "@reearth/beta/lib/core/StoryPanel"; import Visualizer from "@reearth/beta/lib/core/Visualizer"; import { config } from "@reearth/services/config"; import { useT } from "@reearth/services/i18n"; @@ -11,8 +12,17 @@ export type Props = { export default function Published({ alias }: Props) { const t = useT(); - const { sceneProperty, pluginProperty, layers, widgets, ready, error, engineMeta } = - useHooks(alias); + const { + sceneProperty, + pluginProperty, + layers, + widgets, + story, + ready, + error, + engineMeta, + handleCurrentPageChange, + } = useHooks(alias); return error ? ( + meta={engineMeta}> + {story && } + ); } diff --git a/web/src/beta/features/PublishedVisualizer/types.ts b/web/src/beta/features/PublishedVisualizer/types.ts index 71e1f21848..844e616af7 100644 --- a/web/src/beta/features/PublishedVisualizer/types.ts +++ b/web/src/beta/features/PublishedVisualizer/types.ts @@ -1,5 +1,4 @@ import type { DataType, SceneProperty } from "@reearth/beta/lib/core/Map/types"; -import { Story } from "@reearth/beta/lib/core/StoryPanel"; import { LayerStyle } from "@reearth/services/api/layerStyleApi/utils"; export type PublishedData = { @@ -15,6 +14,31 @@ export type PublishedData = { story?: Story; }; +export type Story = { + id: string; + title?: string; + position: "left" | "right"; + pages: StoryPage[]; +}; + +export type StoryPage = { + id: string; + swipeable?: boolean; + swipeableLayers?: string[]; + layers?: string[]; + property?: any; + blocks: StoryBlock[]; +}; + +export type StoryBlock = { + id: string; + name?: string | null; + pluginId: string; + extensionId: string; + propertyId?: string; + property?: any; +}; + export type Plugin = { id: string; property: any; diff --git a/web/src/beta/lib/core/Map/types/index.ts b/web/src/beta/lib/core/Map/types/index.ts index a72227a14a..a20c19b180 100644 --- a/web/src/beta/lib/core/Map/types/index.ts +++ b/web/src/beta/lib/core/Map/types/index.ts @@ -241,7 +241,10 @@ export type TerrainProperty = { terrainNormal?: boolean; }; -export type SceneProperty = { +export type SceneProperty = OldSceneProperty; +// export type SceneProperty = OldSceneProperty | NewSceneProperty; + +export type OldSceneProperty = { default?: { camera?: Camera; allowEnterGround?: boolean; @@ -338,6 +341,59 @@ export type SceneProperty = { }; }; +export type NewSceneProperty = { + main: { + sceneMode?: SceneMode; // default: scene3d + ion?: string; + vr?: boolean; + }; + tiles: { + id: string; + tile_type?: string; + tile_url?: string; + tile_zoomLevel?: number; + tile_opacity?: number; + }[]; + terrain: { + terrain?: boolean; + terrainType?: "cesium" | "arcgis" | "cesiumion"; // default: cesium + terrainCesiumIonAsset?: string; + terrainCesiumIonAccessToken?: string; + terrainCesiumIonUrl?: string; + terrainExaggeration?: number; // default: 1 + terrainExaggerationRelativeHeight?: number; // default: 0 + depthTestAgainstTerrain?: boolean; + }; + globeLighting: { + globeLightingBool?: boolean; + }; + globeShadow: { + globeShadowBool?: boolean; + }; + globeAtmosphere: { + globeAtmosphereBool?: boolean; + globeAtmosphereIntensity?: number; + }; + skyBox: { + skyBoxBool?: boolean; + }; + sun: { + sunBool?: boolean; + }; + moon: { + moonBool?: boolean; + }; + skyAtmosphere: { + skyAtmosphereBool?: boolean; + skyAtmosphereIntensity?: number; + }; + camera: { + camera?: Camera; + allowEnterGround?: boolean; + fov?: number; + }; +}; + export type EngineComponent = ForwardRefExoticComponent< PropsWithoutRef & RefAttributes >; diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Camera/Editor.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Camera/Editor.tsx index a6ed437217..a5545dbca7 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Camera/Editor.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Camera/Editor.tsx @@ -1,4 +1,3 @@ -import { useReactiveVar } from "@apollo/client"; import { debounce } from "lodash-es"; import { useCallback, useContext, useMemo, useState } from "react"; @@ -10,7 +9,7 @@ import TextField from "@reearth/beta/components/fields/TextField"; import { Camera } from "@reearth/beta/lib/core/engines"; import { useVisualizer } from "@reearth/beta/lib/core/Visualizer/context"; import { useT } from "@reearth/services/i18n"; -import { currentCameraVar } from "@reearth/services/state"; +import { useCurrentCamera } from "@reearth/services/state"; import { styled } from "@reearth/services/theme"; import type { Field } from "../../../types"; @@ -51,7 +50,7 @@ const CameraBlockEditor: React.FC = ({ const [selected, setSelected] = useState(items[0]?.id); const visualizer = useVisualizer(); - const currentCamera = useReactiveVar(currentCameraVar); + const [currentCamera] = useCurrentCamera(); const handleFlyTo = useMemo(() => visualizer.current?.engine.flyTo, [visualizer]); diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Text/index.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Text/index.tsx index bef03721f4..290cd3aa27 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Text/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Text/index.tsx @@ -14,6 +14,7 @@ export type Props = BlockProps; // from the common editor panel, but manage it by itself directly. const TextBlock: React.FC = ({ block, isSelected, ...props }) => { + console.log("BB", isSelected, props); const text = useMemo( () => block?.property?.default?.text?.value as ValueTypes["string"], [block?.property?.default?.text?.value], diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts b/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts index 8316026201..0f4bb0c38a 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts @@ -1,5 +1,6 @@ import { merge } from "lodash-es"; +import { Component } from ".."; import { CAMERA_BUILTIN_STORY_BLOCK_ID, IMAGE_BUILTIN_STORY_BLOCK_ID, @@ -7,9 +8,7 @@ import { TEXT_BUILTIN_STORY_BLOCK_ID, TITLE_BUILTIN_STORY_BLOCK_ID, VIDEO_BUILTIN_STORY_BLOCK_ID, -} from "@reearth/services/api/storytellingApi/blocks"; - -import { Component } from ".."; +} from "../../constants"; import CameraBlock from "./Camera"; import ImageBlock from "./Image"; diff --git a/web/src/beta/lib/core/StoryPanel/Page/hooks.ts b/web/src/beta/lib/core/StoryPanel/Page/hooks.ts index 2901e6df77..4e7f9be55c 100644 --- a/web/src/beta/lib/core/StoryPanel/Page/hooks.ts +++ b/web/src/beta/lib/core/StoryPanel/Page/hooks.ts @@ -1,14 +1,11 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { Spacing } from "../../mantle"; +import { DEFAULT_STORY_PAGE_GAP, DEFAULT_STORY_PAGE_PADDING } from "../constants"; import { StoryPage } from "../types"; import { calculatePaddingValue } from "../utils"; export type { StoryPage } from "../types"; -export const DEFAULT_PAGE_GAP = 2; -export const DEFAULT_PAGE_PADDING: Spacing = { top: 4, bottom: 4, left: 4, right: 4 }; - export default ({ page, isEditable, @@ -48,12 +45,12 @@ export default ({ padding: { ...property?.panel?.padding, value: calculatePaddingValue( - DEFAULT_PAGE_PADDING, + DEFAULT_STORY_PAGE_PADDING, property?.panel?.padding?.value, isEditable, ), }, - gap: property?.panel?.gap ?? DEFAULT_PAGE_GAP, + gap: property?.panel?.gap ?? DEFAULT_STORY_PAGE_GAP, }), [property?.panel, isEditable], ); diff --git a/web/src/beta/lib/core/StoryPanel/Page/index.tsx b/web/src/beta/lib/core/StoryPanel/Page/index.tsx index e6812900ea..3c95542c9b 100644 --- a/web/src/beta/lib/core/StoryPanel/Page/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/Page/index.tsx @@ -36,7 +36,7 @@ type Props = { vt?: ValueType, v?: ValueTypes[ValueType], ) => Promise; - onStoryBlockMove: (id: string, targetId: number, blockId: string) => void; + onStoryBlockMove?: (id: string, targetId: number, blockId: string) => void; }; const StoryPanel: React.FC = ({ @@ -87,7 +87,7 @@ const StoryPanel: React.FC = ({ onClickAway={onPageSelect} onSettingsToggle={onPageSettingsToggle}> - {(isEditable || title?.title?.value) && ( + {(isEditable ?? title?.title?.value) && ( = ({ items.splice(index, 0, item); return items; }); - await onStoryBlockMove(page?.id || "", index, item.id); + await onStoryBlockMove?.(page?.id || "", index, item.id); }} renderItem={(b, idx) => { return ( diff --git a/web/src/beta/lib/core/StoryPanel/PanelContent/hooks.ts b/web/src/beta/lib/core/StoryPanel/PanelContent/hooks.ts index 31f6c2519d..f5adf87afe 100644 --- a/web/src/beta/lib/core/StoryPanel/PanelContent/hooks.ts +++ b/web/src/beta/lib/core/StoryPanel/PanelContent/hooks.ts @@ -1,21 +1,21 @@ import { MutableRefObject, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; +import { STORY_PANEL_CONTENT_ELEMENT_ID } from "../constants"; import type { StoryPage } from "../hooks"; export type { StoryPage } from "../hooks"; - -export const PAGES_ELEMENT_ID = "story-page-content"; +export { STORY_PANEL_CONTENT_ELEMENT_ID } from "../constants"; export default ({ pages, - selectedPageId, + currentPageId, isAutoScrolling, onBlockCreate, onBlockDelete, onCurrentPageChange, }: { pages?: StoryPage[]; - selectedPageId?: string; + currentPageId?: string; isAutoScrolling?: MutableRefObject; onBlockCreate?: ( pageId?: string | undefined, @@ -24,7 +24,7 @@ export default ({ index?: number | undefined, ) => Promise; onBlockDelete?: (pageId?: string | undefined, blockId?: string | undefined) => Promise; - onCurrentPageChange?: (pageId: string) => void; + onCurrentPageChange?: (pageId: string, disableScrollIntoView?: boolean) => void; }) => { const scrollRef = useRef(undefined); const scrollTimeoutRef = useRef(); @@ -48,13 +48,13 @@ export default ({ ); useLayoutEffect(() => { - const pageWrapperElement = document.getElementById(PAGES_ELEMENT_ID); + const pageWrapperElement = document.getElementById(STORY_PANEL_CONTENT_ELEMENT_ID); if (pageWrapperElement) setPageGap(pageWrapperElement.clientHeight - 40); // 40px is the height of the page title block }, [setPageGap]); useEffect(() => { const resizeCallback = () => { - const pageWrapperElement = document.getElementById(PAGES_ELEMENT_ID); + const pageWrapperElement = document.getElementById(STORY_PANEL_CONTENT_ELEMENT_ID); if (pageWrapperElement) setPageGap(pageWrapperElement.clientHeight - 40); // 40px is the height of the page title block }; window.addEventListener("resize", resizeCallback); @@ -63,13 +63,13 @@ export default ({ useEffect(() => { const ids = pages?.map(p => p.id) as string[]; - const panelContentElement = document.getElementById(PAGES_ELEMENT_ID); + const panelContentElement = document.getElementById(STORY_PANEL_CONTENT_ELEMENT_ID); const observer = new IntersectionObserver( entries => { - // to avoid conflicts with page selection in editor + // to avoid conflicts with page selection in core's parent if (isAutoScrolling?.current) { - const wrapperElement = document.getElementById(PAGES_ELEMENT_ID); + const wrapperElement = document.getElementById(STORY_PANEL_CONTENT_ELEMENT_ID); wrapperElement?.addEventListener("scroll", () => { clearTimeout(scrollTimeoutRef.current); @@ -83,13 +83,13 @@ export default ({ entries.forEach(entry => { const id = entry.target.getAttribute("id") ?? ""; - if (selectedPageId === id) return; + if (!id ?? currentPageId === id) return; const diff = (scrollRef.current as number) - (panelContentElement?.scrollTop as number); const isScrollingUp = diff > 0; if (entry.isIntersecting) { - onCurrentPageChange?.(id); + onCurrentPageChange?.(id, true); scrollRef.current = panelContentElement?.scrollTop; return; } @@ -97,7 +97,7 @@ export default ({ const prevEntry = ids[currentIndex - 1]; if (isScrollingUp) { const id = prevEntry; - onCurrentPageChange?.(id); + onCurrentPageChange?.(id, true); } }); }, @@ -120,7 +120,7 @@ export default ({ } }); }; - }, [pages, selectedPageId, isAutoScrolling, onCurrentPageChange]); + }, [pages, currentPageId, isAutoScrolling, onCurrentPageChange]); return { pageGap, diff --git a/web/src/beta/lib/core/StoryPanel/PanelContent/index.tsx b/web/src/beta/lib/core/StoryPanel/PanelContent/index.tsx index a7490cdc19..5b6bb52384 100644 --- a/web/src/beta/lib/core/StoryPanel/PanelContent/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/PanelContent/index.tsx @@ -6,10 +6,11 @@ import { styled } from "@reearth/services/theme"; import StoryPage from "../Page"; -import useHooks, { PAGES_ELEMENT_ID, type StoryPage as StoryPageType } from "./hooks"; +import useHooks, { STORY_PANEL_CONTENT_ELEMENT_ID, type StoryPage as StoryPageType } from "./hooks"; export type Props = { pages?: StoryPageType[]; + currentPageId?: string; selectedPageId?: string; installableStoryBlocks?: InstallableStoryBlock[]; selectedStoryBlockId?: string; @@ -35,12 +36,13 @@ export type Props = { vt?: ValueType, v?: ValueTypes[ValueType], ) => Promise; - onCurrentPageChange?: (pageId: string) => void; - onStoryBlockMove: (id: string, targetId: number, blockId: string) => void; + onCurrentPageChange?: (pageId: string, disableScrollIntoView?: boolean) => void; + onStoryBlockMove?: (id: string, targetId: number, blockId: string) => void; }; const StoryContent: React.FC = ({ pages, + currentPageId, selectedPageId, installableStoryBlocks, selectedStoryBlockId, @@ -59,7 +61,7 @@ const StoryContent: React.FC = ({ }) => { const { pageGap, handleBlockCreate, handleBlockDelete } = useHooks({ pages, - selectedPageId, + currentPageId, isAutoScrolling, onBlockCreate, onBlockDelete, @@ -67,7 +69,10 @@ const StoryContent: React.FC = ({ }); return ( - + {pages?.map(p => ( void; -}) => { + handleCurrentPageChange: (pageId: string, disableScrollIntoView?: boolean) => void; +}; + +export default ( + { + selectedStory, + isEditable, + onCurrentPageChange, + }: { + selectedStory?: Story; + isEditable?: boolean; + onCurrentPageChange?: (id: string, disableScrollIntoView?: boolean) => void; + }, + ref: Ref, +) => { + const isAutoScrolling = useRef(false); + + const visualizer = useVisualizer(); + const [showPageSettings, setShowPageSettings] = useState(false); + const [currentPageId, setCurrentPageId] = useState(); const [selectedPageId, setSelectedPageId] = useState(); const [selectedBlockId, setSelectedBlockId] = useState(); @@ -47,11 +62,31 @@ export default ({ ); const handleCurrentPageChange = useCallback( - (pageId: string) => { - if (currentPageId === pageId) return; - onCurrentPageChange(pageId, true); // true disables scrollIntoView + (pageId: string, disableScrollIntoView?: boolean) => { + if (pageId === currentPageId) return; + + const newPage = getPage(pageId, selectedStory?.pages); + if (!newPage) return; + + onCurrentPageChange?.(pageId); + setCurrentPageId(pageId); + + if (!disableScrollIntoView) { + const element = document.getElementById(newPage.id); + isAutoScrolling.current = true; + element?.scrollIntoView({ behavior: "smooth" }); + } + + const cameraAnimation = newPage.property?.cameraAnimation; + + const destination = cameraAnimation?.cameraPosition?.value; + if (!destination) return; + + const duration = cameraAnimation?.cameraDuration?.value ?? DEFAULT_STORY_PAGE_DURATION; + + visualizer.current?.engine.flyTo({ ...destination }, { duration }); }, - [currentPageId, onCurrentPageChange], + [selectedStory?.pages, currentPageId, visualizer, onCurrentPageChange], ); const pageInfo = useMemo(() => { @@ -62,18 +97,34 @@ export default ({ return { currentPage: currentIndex + 1, maxPage: pages.length, - onPageChange: (pageIndex: number) => onCurrentPageChange(pages[pageIndex - 1]?.id), + onPageChange: (pageIndex: number) => handleCurrentPageChange(pages[pageIndex - 1]?.id), }; - }, [selectedStory, currentPageId, onCurrentPageChange]); + }, [selectedStory, currentPageId, handleCurrentPageChange]); + + useImperativeHandle( + ref, + () => ({ + currentPageId, + handleCurrentPageChange, + }), + [currentPageId, handleCurrentPageChange], + ); return { pageInfo, + currentPageId, selectedPageId, selectedBlockId, showPageSettings, + isAutoScrolling, handlePageSettingsToggle, handlePageSelect, handleBlockSelect, handleCurrentPageChange, }; }; + +const getPage = (id?: string, pages?: StoryPage[]) => { + if (!id || !pages || !pages.length) return; + return pages.find(p => p.id === id); +}; diff --git a/web/src/beta/lib/core/StoryPanel/index.tsx b/web/src/beta/lib/core/StoryPanel/index.tsx index 3ab0c8ee86..b779db8294 100644 --- a/web/src/beta/lib/core/StoryPanel/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/index.tsx @@ -1,15 +1,14 @@ -import { MutableRefObject } from "react"; +import { forwardRef, memo, Ref } from "react"; import { ValueType, ValueTypes } from "@reearth/beta/utils/value"; import { styled } from "@reearth/services/theme"; -import useHooks, { type Story } from "./hooks"; +import { STORY_PANEL_WIDTH } from "./constants"; +import useHooks, { type StoryPanelRef, type Story } from "./hooks"; import PageIndicator from "./PageIndicator"; import StoryContent from "./PanelContent"; -export const storyPanelWidth = 442; - -export { type Story, type StoryPage } from "./hooks"; +export type { Story, StoryPage, StoryPanelRef } from "./hooks"; export type InstallableStoryBlock = { name: string; @@ -23,9 +22,7 @@ export type InstallableStoryBlock = { export type StoryPanelProps = { selectedStory?: Story; - currentPageId?: string; isEditable?: boolean; - isAutoScrolling?: MutableRefObject; installableBlocks?: InstallableStoryBlock[]; onBlockCreate?: ( pageId?: string | undefined, @@ -42,73 +39,83 @@ export type StoryPanelProps = { vt?: ValueType, v?: ValueTypes[ValueType], ) => Promise; - onCurrentPageChange: (id: string, disableScrollIntoView?: boolean) => void; - onStoryBlockMove: (id: string, targetId: number, blockId: string) => void; + onCurrentPageChange?: (id: string, disableScrollIntoView?: boolean) => void; + onStoryBlockMove?: (id: string, targetId: number, blockId: string) => void; }; -export const StoryPanel: React.FC = ({ - selectedStory, - currentPageId, - isEditable, - isAutoScrolling, - installableBlocks, - onBlockCreate, - onBlockDelete, - onPropertyUpdate, - onCurrentPageChange, - onStoryBlockMove, -}) => { - const { - pageInfo, - selectedPageId, - selectedBlockId, - showPageSettings, - handlePageSettingsToggle, - handlePageSelect, - handleBlockSelect, - handleCurrentPageChange, - } = useHooks({ - selectedStory, - currentPageId, - isEditable, - onCurrentPageChange, - }); +export const StoryPanel = memo( + forwardRef( + ( + { + selectedStory, + isEditable, + installableBlocks, + onBlockCreate, + onBlockDelete, + onPropertyUpdate, + onCurrentPageChange, + onStoryBlockMove, + }, + ref: Ref, + ) => { + const { + pageInfo, + currentPageId, + selectedPageId, + selectedBlockId, + showPageSettings, + isAutoScrolling, + handlePageSettingsToggle, + handlePageSelect, + handleBlockSelect, + handleCurrentPageChange, + } = useHooks( + { + selectedStory, + isEditable, + onCurrentPageChange, + }, + ref, + ); - return ( - - {!!pageInfo && ( - - )} - - - ); -}; + return ( + + {!!pageInfo && ( + + )} + + + ); + }, + ), +); export default StoryPanel; const PanelWrapper = styled.div` - flex: 0 0 ${storyPanelWidth}px; + flex: 0 0 ${STORY_PANEL_WIDTH}px; background: #f1f1f1; color: ${({ theme }) => theme.content.weak}; `; diff --git a/web/src/beta/lib/core/StoryPanel/types.ts b/web/src/beta/lib/core/StoryPanel/types.ts index 9fed84a4a1..4f27526332 100644 --- a/web/src/beta/lib/core/StoryPanel/types.ts +++ b/web/src/beta/lib/core/StoryPanel/types.ts @@ -11,6 +11,7 @@ export type StoryPage = { id: string; title?: string; swipeable?: boolean; + layerIds?: string[]; propertyId?: string; property?: any; blocks: StoryBlock[]; diff --git a/web/src/beta/lib/core/Visualizer/index.tsx b/web/src/beta/lib/core/Visualizer/index.tsx index ed350f9d48..920d68ddc8 100644 --- a/web/src/beta/lib/core/Visualizer/index.tsx +++ b/web/src/beta/lib/core/Visualizer/index.tsx @@ -149,7 +149,7 @@ const Visualizer = memo( pluginProperty, zoomedLayerId, useExperimentalSandbox, - storyPanelPosition, + storyPanelPosition = "left", children: storyPanel, onLayerDrop, onLayerSelect, diff --git a/web/src/classic/components/organisms/Authentication/RootPage/hooks.ts b/web/src/classic/components/organisms/Authentication/RootPage/hooks.ts index d41e9966ac..efd2494541 100644 --- a/web/src/classic/components/organisms/Authentication/RootPage/hooks.ts +++ b/web/src/classic/components/organisms/Authentication/RootPage/hooks.ts @@ -3,9 +3,9 @@ import { useCallback, useEffect, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { useGetTeamsQuery } from "@reearth/classic/gql"; +import { useWorkspace, useNotification, useUserId } from "@reearth/classic/state"; import { useAuth, useCleanUrl } from "@reearth/services/auth"; import { useT } from "@reearth/services/i18n"; -import { useWorkspace, useNotification, useUserId } from "@reearth/services/state"; export type Mode = "layer" | "widget"; diff --git a/web/src/classic/components/organisms/Common/AssetContainer/hooks.ts b/web/src/classic/components/organisms/Common/AssetContainer/hooks.ts index 90977ec7e9..eb29f3c8ca 100644 --- a/web/src/classic/components/organisms/Common/AssetContainer/hooks.ts +++ b/web/src/classic/components/organisms/Common/AssetContainer/hooks.ts @@ -9,8 +9,8 @@ import { Maybe, AssetSortType as GQLSortType, } from "@reearth/classic/gql"; +import { useNotification } from "@reearth/classic/state"; import { useT } from "@reearth/services/i18n"; -import { useNotification } from "@reearth/services/state"; export type AssetNodes = NonNullable[]; diff --git a/web/src/classic/components/organisms/Dashboard/hooks.ts b/web/src/classic/components/organisms/Dashboard/hooks.ts index 0d9df39357..70efdd3aed 100644 --- a/web/src/classic/components/organisms/Dashboard/hooks.ts +++ b/web/src/classic/components/organisms/Dashboard/hooks.ts @@ -14,14 +14,14 @@ import { Visualizer, GetProjectsQuery, } from "@reearth/classic/gql"; -import useStorytellingAPI from "@reearth/services/api/storytellingApi"; -import { useT } from "@reearth/services/i18n"; import { useWorkspace, useProject, useUnselectProject, useNotification, -} from "@reearth/services/state"; +} from "@reearth/classic/state"; +import useStorytellingAPI from "@reearth/services/api/storytellingApi"; +import { useT } from "@reearth/services/i18n"; import { ProjectType } from "@reearth/types"; export type ProjectNodes = NonNullable[]; diff --git a/web/src/classic/components/organisms/EarthEditor/CanvasArea/hooks.ts b/web/src/classic/components/organisms/EarthEditor/CanvasArea/hooks.ts index 0b6053228c..bd47a4fc5a 100644 --- a/web/src/classic/components/organisms/EarthEditor/CanvasArea/hooks.ts +++ b/web/src/classic/components/organisms/EarthEditor/CanvasArea/hooks.ts @@ -21,9 +21,6 @@ import { WidgetAreaAlign, ValueType, } from "@reearth/classic/gql"; -import { valueTypeToGQL, ValueTypes, valueToGQL, LatLng } from "@reearth/classic/util/value"; -import { config } from "@reearth/services/config"; -import { useLang } from "@reearth/services/i18n"; import { useSceneId, useSceneMode, @@ -35,7 +32,10 @@ import { useZoomedLayerId, useClock, useSelectedWidgetArea, -} from "@reearth/services/state"; +} from "@reearth/classic/state"; +import { valueTypeToGQL, ValueTypes, valueToGQL, LatLng } from "@reearth/classic/util/value"; +import { config } from "@reearth/services/config"; +import { useLang } from "@reearth/services/i18n"; import { convertWidgets, diff --git a/web/src/classic/components/organisms/EarthEditor/DataSourcePane/hooks.ts b/web/src/classic/components/organisms/EarthEditor/DataSourcePane/hooks.ts index 7f79878e48..adec57fb8d 100644 --- a/web/src/classic/components/organisms/EarthEditor/DataSourcePane/hooks.ts +++ b/web/src/classic/components/organisms/EarthEditor/DataSourcePane/hooks.ts @@ -12,7 +12,6 @@ import { useRemoveDatasetMutation, useGetDatasetSchemasWithCountQuery, } from "@reearth/classic/gql"; -import { useT, useLang } from "@reearth/services/i18n"; import { useSceneId, useNotification, @@ -20,7 +19,8 @@ import { useProject, NotificationType, useCurrentTheme, -} from "@reearth/services/state"; +} from "@reearth/classic/state"; +import { useT, useLang } from "@reearth/services/i18n"; export default () => { const t = useT(); diff --git a/web/src/classic/components/organisms/EarthEditor/DatasetInfoPane/hooks.ts b/web/src/classic/components/organisms/EarthEditor/DatasetInfoPane/hooks.ts index 9f67bb5dc8..e76e6c1fac 100644 --- a/web/src/classic/components/organisms/EarthEditor/DatasetInfoPane/hooks.ts +++ b/web/src/classic/components/organisms/EarthEditor/DatasetInfoPane/hooks.ts @@ -5,8 +5,8 @@ import { useGetDatasetsForDatasetInfoPaneQuery, useGetScenePluginsForDatasetInfoPaneQuery, } from "@reearth/classic/gql"; +import { useNotification, useProject, useRootLayerId, useSelected } from "@reearth/classic/state"; import { useT } from "@reearth/services/i18n"; -import { useNotification, useProject, useRootLayerId, useSelected } from "@reearth/services/state"; import { processDatasets, processDatasetHeaders, processPrimitives } from "./convert"; diff --git a/web/src/classic/components/organisms/EarthEditor/ExportPane/hooks.ts b/web/src/classic/components/organisms/EarthEditor/ExportPane/hooks.ts index 26536f1f52..fdad30b608 100644 --- a/web/src/classic/components/organisms/EarthEditor/ExportPane/hooks.ts +++ b/web/src/classic/components/organisms/EarthEditor/ExportPane/hooks.ts @@ -1,8 +1,8 @@ import { useCallback } from "react"; import { Format } from "@reearth/classic/components/molecules/EarthEditor/ExportPane"; +import { useRootLayerId, useSelected } from "@reearth/classic/state"; import { useAuth } from "@reearth/services/auth"; -import { useRootLayerId, useSelected } from "@reearth/services/state"; const ext: { [key in Format]: string } = { kml: "kml", diff --git a/web/src/classic/components/organisms/EarthEditor/Header/hooks.ts b/web/src/classic/components/organisms/EarthEditor/Header/hooks.ts index fd8ce59244..403aefd8ed 100644 --- a/web/src/classic/components/organisms/EarthEditor/Header/hooks.ts +++ b/web/src/classic/components/organisms/EarthEditor/Header/hooks.ts @@ -12,9 +12,9 @@ import { useCheckProjectAliasLazyQuery, useCreateTeamMutation, } from "@reearth/classic/gql"; +import { useSceneId, useWorkspace, useProject, useNotification } from "@reearth/classic/state"; import { useAuth } from "@reearth/services/auth"; import { useT } from "@reearth/services/i18n"; -import { useSceneId, useWorkspace, useProject, useNotification } from "@reearth/services/state"; export default () => { const url = window.REEARTH_CONFIG?.published?.split("{}"); diff --git a/web/src/classic/components/organisms/EarthEditor/LeftMenu/hooks.ts b/web/src/classic/components/organisms/EarthEditor/LeftMenu/hooks.ts index 45ec805cf2..aefb7fffcb 100644 --- a/web/src/classic/components/organisms/EarthEditor/LeftMenu/hooks.ts +++ b/web/src/classic/components/organisms/EarthEditor/LeftMenu/hooks.ts @@ -1,4 +1,4 @@ -import { useIsCapturing } from "@reearth/services/state"; +import { useIsCapturing } from "@reearth/classic/state"; export default () => { const [isCapturing] = useIsCapturing(); diff --git a/web/src/classic/components/organisms/EarthEditor/OutlinePane/hooks.tsx b/web/src/classic/components/organisms/EarthEditor/OutlinePane/hooks.tsx index 10196e8f03..c0a0d3823f 100644 --- a/web/src/classic/components/organisms/EarthEditor/OutlinePane/hooks.tsx +++ b/web/src/classic/components/organisms/EarthEditor/OutlinePane/hooks.tsx @@ -27,9 +27,6 @@ import { PluginExtensionType, GetLayersFromLayerIdQuery, } from "@reearth/classic/gql"; -import deepFind from "@reearth/classic/util/deepFind"; -import deepGet from "@reearth/classic/util/deepGet"; -import { useLang, useT } from "@reearth/services/i18n"; import { useSceneId, useSelected, @@ -38,7 +35,10 @@ import { useWidgetAlignEditorActivated, useZoomedLayerId, useSelectedWidgetArea, -} from "@reearth/services/state"; +} from "@reearth/classic/state"; +import deepFind from "@reearth/classic/util/deepFind"; +import deepGet from "@reearth/classic/util/deepGet"; +import { useLang, useT } from "@reearth/services/i18n"; const convertFormat = (format: Format) => { if (format === "kml") return LayerEncodingFormat.Kml; diff --git a/web/src/classic/components/organisms/EarthEditor/PrimitiveHeader/hooks.ts b/web/src/classic/components/organisms/EarthEditor/PrimitiveHeader/hooks.ts index c8a63db802..06b6fa4c06 100644 --- a/web/src/classic/components/organisms/EarthEditor/PrimitiveHeader/hooks.ts +++ b/web/src/classic/components/organisms/EarthEditor/PrimitiveHeader/hooks.ts @@ -1,8 +1,8 @@ import { useMemo } from "react"; import { useGetPrimitivesQuery, useAddLayerItemFromPrimitiveMutation } from "@reearth/classic/gql"; +import { useSceneId, useSelected } from "@reearth/classic/state"; import { useLang } from "@reearth/services/i18n"; -import { useSceneId, useSelected } from "@reearth/services/state"; // ポリゴンやポリラインは現在編集できないため、それらを新規レイヤーとして追加しても何も表示されない const hiddenExtensions = ["reearth/polyline", "reearth/polygon", "reearth/rect"]; diff --git a/web/src/classic/components/organisms/EarthEditor/PropertyPane/hooks-queries.ts b/web/src/classic/components/organisms/EarthEditor/PropertyPane/hooks-queries.ts index 95c3a6aa4e..56055edf26 100644 --- a/web/src/classic/components/organisms/EarthEditor/PropertyPane/hooks-queries.ts +++ b/web/src/classic/components/organisms/EarthEditor/PropertyPane/hooks-queries.ts @@ -6,7 +6,7 @@ import { useGetLinkableDatasetsQuery, useGetLayersFromLayerIdQuery, } from "@reearth/classic/gql"; -import { Selected } from "@reearth/services/state"; +import { Selected } from "@reearth/classic/state"; import { convert, Pane, convertLinkableDatasets, convertLayers } from "./convert"; diff --git a/web/src/classic/components/organisms/EarthEditor/PropertyPane/hooks.ts b/web/src/classic/components/organisms/EarthEditor/PropertyPane/hooks.ts index 1f96d32714..0a4990a04e 100644 --- a/web/src/classic/components/organisms/EarthEditor/PropertyPane/hooks.ts +++ b/web/src/classic/components/organisms/EarthEditor/PropertyPane/hooks.ts @@ -23,8 +23,6 @@ import { WidgetSectionType, WidgetZoneType, } from "@reearth/classic/gql"; -import { valueTypeToGQL, Camera, toGQLSimpleValue, valueToGQL } from "@reearth/classic/util/value"; -import { useLang } from "@reearth/services/i18n"; import { useSelected, useRootLayerId, @@ -36,7 +34,9 @@ import { useSelectedBlock, useWidgetAlignEditorActivated, useSelectedWidgetArea, -} from "@reearth/services/state"; +} from "@reearth/classic/state"; +import { valueTypeToGQL, Camera, toGQLSimpleValue, valueToGQL } from "@reearth/classic/util/value"; +import { useLang } from "@reearth/services/i18n"; import useQueries, { Mode as RawMode } from "./hooks-queries"; diff --git a/web/src/classic/components/organisms/EarthEditor/RightMenu/hooks.ts b/web/src/classic/components/organisms/EarthEditor/RightMenu/hooks.ts index 3a9427bec3..073dbf4f3c 100644 --- a/web/src/classic/components/organisms/EarthEditor/RightMenu/hooks.ts +++ b/web/src/classic/components/organisms/EarthEditor/RightMenu/hooks.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from "react"; -import { useSelected, useSelectedBlock, useIsCapturing } from "@reearth/services/state"; +import { useSelected, useSelectedBlock, useIsCapturing } from "@reearth/classic/state"; export type Tab = | "layer" diff --git a/web/src/classic/components/organisms/EarthEditor/TagPane/commonHooks.ts b/web/src/classic/components/organisms/EarthEditor/TagPane/commonHooks.ts index a0efc61438..3da4e64c45 100644 --- a/web/src/classic/components/organisms/EarthEditor/TagPane/commonHooks.ts +++ b/web/src/classic/components/organisms/EarthEditor/TagPane/commonHooks.ts @@ -12,9 +12,9 @@ import { useRemoveTagMutation, useUpdateTagMutation, } from "@reearth/classic/gql"; +import { useNotification, useSceneId, useSelected } from "@reearth/classic/state"; import { useAuth } from "@reearth/services/auth"; import { useT } from "@reearth/services/i18n"; -import { useNotification, useSceneId, useSelected } from "@reearth/services/state"; export default () => { const { isAuthenticated } = useAuth(); diff --git a/web/src/classic/components/organisms/EarthEditor/core/CanvasArea/hooks.ts b/web/src/classic/components/organisms/EarthEditor/core/CanvasArea/hooks.ts index 666c86fe51..e0e7703bf9 100644 --- a/web/src/classic/components/organisms/EarthEditor/core/CanvasArea/hooks.ts +++ b/web/src/classic/components/organisms/EarthEditor/core/CanvasArea/hooks.ts @@ -24,14 +24,6 @@ import { type WidgetAreaAlign, ValueType, } from "@reearth/classic/gql"; -import { - valueTypeToGQL, - type ValueTypes, - valueToGQL, - type LatLng, -} from "@reearth/classic/util/value"; -import { config } from "@reearth/services/config"; -import { useLang } from "@reearth/services/i18n"; import { useSceneId, useSceneMode, @@ -42,7 +34,15 @@ import { useWidgetAlignEditorActivated, useZoomedLayerId, useSelectedWidgetArea, -} from "@reearth/services/state"; +} from "@reearth/classic/state"; +import { + valueTypeToGQL, + type ValueTypes, + valueToGQL, + type LatLng, +} from "@reearth/classic/util/value"; +import { config } from "@reearth/services/config"; +import { useLang } from "@reearth/services/i18n"; import { convertWidgets, diff --git a/web/src/classic/components/organisms/GlobalModal/index.tsx b/web/src/classic/components/organisms/GlobalModal/index.tsx index 850939bf07..3be7d0925a 100644 --- a/web/src/classic/components/organisms/GlobalModal/index.tsx +++ b/web/src/classic/components/organisms/GlobalModal/index.tsx @@ -1,12 +1,12 @@ import { useCallback, useEffect, useState } from "react"; -import { useAuth } from "@reearth/services/auth"; -import { useLang as useCurrentLang } from "@reearth/services/i18n"; import { NotificationType, useCurrentTheme as useCurrentTheme, useNotification, -} from "@reearth/services/state"; +} from "@reearth/classic/state"; +import { useAuth } from "@reearth/services/auth"; +import { useLang as useCurrentLang } from "@reearth/services/i18n"; const GlobalModal: React.FC = () => { const extensions = window.REEARTH_CONFIG?.extensions?.globalModal; diff --git a/web/src/classic/components/organisms/Notification/hooks.ts b/web/src/classic/components/organisms/Notification/hooks.ts index 2062649b51..7ba909f54c 100644 --- a/web/src/classic/components/organisms/Notification/hooks.ts +++ b/web/src/classic/components/organisms/Notification/hooks.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback, useMemo } from "react"; +import { useError, useNotification, Notification } from "@reearth/classic/state"; import { useT, useLang } from "@reearth/services/i18n"; -import { useError, useNotification, Notification } from "@reearth/services/state"; export type PolicyItems = | "layer" diff --git a/web/src/classic/components/organisms/Published/core/hooks.ts b/web/src/classic/components/organisms/Published/core/hooks.ts index 0e147b47f7..9f54132cce 100644 --- a/web/src/classic/components/organisms/Published/core/hooks.ts +++ b/web/src/classic/components/organisms/Published/core/hooks.ts @@ -17,8 +17,8 @@ import { } from "@reearth/classic/core/mantle"; import type { ComputedLayer } from "@reearth/classic/core/mantle/types"; import type { LayerSelectionReason } from "@reearth/classic/core/Map/Layers/hooks"; +import { useSelected } from "@reearth/classic/state"; import { config } from "@reearth/services/config"; -import { useSelected } from "@reearth/services/state"; import type { PublishedData, diff --git a/web/src/classic/components/organisms/Settings/Account/hooks.ts b/web/src/classic/components/organisms/Settings/Account/hooks.ts index bbb59487b9..bac5e5e440 100644 --- a/web/src/classic/components/organisms/Settings/Account/hooks.ts +++ b/web/src/classic/components/organisms/Settings/Account/hooks.ts @@ -2,8 +2,8 @@ import { useApolloClient } from "@apollo/client"; import { useCallback } from "react"; import { useUpdateMeMutation, useGetProfileQuery, Theme as GQLTheme } from "@reearth/classic/gql"; +import { useWorkspace, useProject, useNotification } from "@reearth/classic/state"; import { useT } from "@reearth/services/i18n"; -import { useWorkspace, useProject, useNotification } from "@reearth/services/state"; const enumTypeMapper: Partial> = { [GQLTheme.Default]: "default", diff --git a/web/src/classic/components/organisms/Settings/Project/Dataset/hooks.ts b/web/src/classic/components/organisms/Settings/Project/Dataset/hooks.ts index cb85765a5d..3435d92063 100644 --- a/web/src/classic/components/organisms/Settings/Project/Dataset/hooks.ts +++ b/web/src/classic/components/organisms/Settings/Project/Dataset/hooks.ts @@ -8,9 +8,9 @@ import { useRemoveDatasetMutation, useDatasetsListQuery, } from "@reearth/classic/gql"; +import { useWorkspace, useProject, useNotification } from "@reearth/classic/state"; import { useAuth } from "@reearth/services/auth"; import { useT } from "@reearth/services/i18n"; -import { useWorkspace, useProject, useNotification } from "@reearth/services/state"; type Nodes = NonNullable; diff --git a/web/src/classic/components/organisms/Settings/Project/Plugin/hooks.ts b/web/src/classic/components/organisms/Settings/Project/Plugin/hooks.ts index eec055f681..9ef10429a6 100644 --- a/web/src/classic/components/organisms/Settings/Project/Plugin/hooks.ts +++ b/web/src/classic/components/organisms/Settings/Project/Plugin/hooks.ts @@ -9,9 +9,9 @@ import { useUploadPluginMutation, useUpgradePluginMutation, } from "@reearth/classic/gql/graphql-client-api"; +import { useProject, useNotification, useCurrentTheme } from "@reearth/classic/state"; import { useAuth } from "@reearth/services/auth"; import { useLang, useT } from "@reearth/services/i18n"; -import { useProject, useNotification, useCurrentTheme } from "@reearth/services/state"; export type Plugin = { fullId: string; diff --git a/web/src/classic/components/organisms/Settings/Project/Public/hooks.ts b/web/src/classic/components/organisms/Settings/Project/Public/hooks.ts index 9b98ff475f..34f4a2e22b 100644 --- a/web/src/classic/components/organisms/Settings/Project/Public/hooks.ts +++ b/web/src/classic/components/organisms/Settings/Project/Public/hooks.ts @@ -9,14 +9,14 @@ import { usePublishProjectMutation, useUpdateProjectMutation, } from "@reearth/classic/gql"; -import { useLang as useCurrentLang } from "@reearth/services/i18n"; import { useWorkspace, useProject, useNotification, NotificationType, useCurrentTheme as useCurrentTheme, -} from "@reearth/services/state"; +} from "@reearth/classic/state"; +import { useLang as useCurrentLang } from "@reearth/services/i18n"; type Params = { projectId: string; diff --git a/web/src/classic/components/organisms/Settings/Project/hooks.ts b/web/src/classic/components/organisms/Settings/Project/hooks.ts index 9368ee59ab..a9df3e7a0d 100644 --- a/web/src/classic/components/organisms/Settings/Project/hooks.ts +++ b/web/src/classic/components/organisms/Settings/Project/hooks.ts @@ -6,8 +6,8 @@ import { useArchiveProjectMutation, useDeleteProjectMutation, } from "@reearth/classic/gql"; +import { useWorkspace, useNotification } from "@reearth/classic/state"; import { useT } from "@reearth/services/i18n"; -import { useWorkspace, useNotification } from "@reearth/services/state"; type Params = { projectId: string; diff --git a/web/src/classic/components/organisms/Settings/ProjectList/hooks.ts b/web/src/classic/components/organisms/Settings/ProjectList/hooks.ts index 3d616033c1..5c1f7f58af 100644 --- a/web/src/classic/components/organisms/Settings/ProjectList/hooks.ts +++ b/web/src/classic/components/organisms/Settings/ProjectList/hooks.ts @@ -12,9 +12,9 @@ import { Visualizer, GetProjectsQuery, } from "@reearth/classic/gql"; +import { useWorkspace, useProject, useNotification } from "@reearth/classic/state"; import { useMeFetcher } from "@reearth/services/api"; import { useT } from "@reearth/services/i18n"; -import { useWorkspace, useProject, useNotification } from "@reearth/services/state"; import { ProjectType } from "@reearth/types"; const toPublishmentStatus = (s: PublishmentStatus) => diff --git a/web/src/classic/components/organisms/Settings/SettingPage/hooks.ts b/web/src/classic/components/organisms/Settings/SettingPage/hooks.ts index 4949deffe9..bf3f92cd7d 100644 --- a/web/src/classic/components/organisms/Settings/SettingPage/hooks.ts +++ b/web/src/classic/components/organisms/Settings/SettingPage/hooks.ts @@ -9,7 +9,7 @@ import { useGetProjectWithSceneIdQuery, useCreateTeamMutation, } from "@reearth/classic/gql"; -import { useWorkspace, useProject } from "@reearth/services/state"; +import { useWorkspace, useProject } from "@reearth/classic/state"; type Params = { workspaceId?: string; diff --git a/web/src/classic/components/organisms/Settings/Workspace/Asset/hooks.ts b/web/src/classic/components/organisms/Settings/Workspace/Asset/hooks.ts index 7c9ed09201..c5c98376e3 100644 --- a/web/src/classic/components/organisms/Settings/Workspace/Asset/hooks.ts +++ b/web/src/classic/components/organisms/Settings/Workspace/Asset/hooks.ts @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { useNavigate } from "react-router-dom"; import assetHooks from "@reearth/classic/components/organisms/Common/AssetContainer/hooks"; -import { useWorkspace, useProject } from "@reearth/services/state"; +import { useWorkspace, useProject } from "@reearth/classic/state"; export type Params = { workspaceId: string; diff --git a/web/src/classic/components/organisms/Settings/Workspace/hooks.ts b/web/src/classic/components/organisms/Settings/Workspace/hooks.ts index 3b408c6bdb..fca077385a 100644 --- a/web/src/classic/components/organisms/Settings/Workspace/hooks.ts +++ b/web/src/classic/components/organisms/Settings/Workspace/hooks.ts @@ -14,8 +14,8 @@ import { useRemoveMemberFromTeamMutation, } from "@reearth/classic/gql"; import { Team } from "@reearth/classic/gql/graphql-client-api"; +import { useWorkspace, useProject, useNotification } from "@reearth/classic/state"; import { useT } from "@reearth/services/i18n"; -import { useWorkspace, useProject, useNotification } from "@reearth/services/state"; type Params = { workspaceId: string; diff --git a/web/src/classic/components/pages/Authentication/hooks.ts b/web/src/classic/components/pages/Authentication/hooks.ts index aedd341ecf..5d44c67ef6 100644 --- a/web/src/classic/components/pages/Authentication/hooks.ts +++ b/web/src/classic/components/pages/Authentication/hooks.ts @@ -3,9 +3,9 @@ import { useCallback, useEffect, useMemo } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { useGetTeamsQuery } from "@reearth/classic/gql"; +import { useWorkspace, useNotification, useUserId } from "@reearth/classic/state"; import { useAuth, useCleanUrl } from "@reearth/services/auth"; import { useT } from "@reearth/services/i18n"; -import { useWorkspace, useNotification, useUserId } from "@reearth/services/state"; // TODO: move hooks to molecules (page components should be thin) export default () => { diff --git a/web/src/classic/components/pages/EarthEditor/hooks.ts b/web/src/classic/components/pages/EarthEditor/hooks.ts index 246da03d6a..f1e16009c4 100644 --- a/web/src/classic/components/pages/EarthEditor/hooks.ts +++ b/web/src/classic/components/pages/EarthEditor/hooks.ts @@ -1,8 +1,8 @@ import { useEffect } from "react"; import { useGetSceneQuery } from "@reearth/classic/gql"; +import { useSceneId, useRootLayerId, useZoomedLayerId } from "@reearth/classic/state"; import { useAuth } from "@reearth/services/auth"; -import { useSceneId, useRootLayerId, useZoomedLayerId } from "@reearth/services/state"; export type Mode = "layer" | "widget"; diff --git a/web/src/classic/components/pages/Preview/index.tsx b/web/src/classic/components/pages/Preview/index.tsx index eb94f924c8..24c9ed95a9 100644 --- a/web/src/classic/components/pages/Preview/index.tsx +++ b/web/src/classic/components/pages/Preview/index.tsx @@ -3,10 +3,10 @@ import { useParams } from "react-router-dom"; import CanvasArea from "@reearth/classic/components/organisms/EarthEditor/CanvasArea"; import CoreCanvasArea from "@reearth/classic/components/organisms/EarthEditor/core/CanvasArea"; +import { useSceneId } from "@reearth/classic/state"; import { useCore } from "@reearth/classic/util/use-core"; import { Provider as DndProvider } from "@reearth/classic/util/use-dnd"; import { AuthenticatedPage } from "@reearth/services/auth"; -import { useSceneId } from "@reearth/services/state"; import { PublishedAppProvider as ThemeProvider } from "@reearth/services/theme"; export type Props = { diff --git a/web/src/services/state/jotai.ts b/web/src/classic/state/index.ts similarity index 97% rename from web/src/services/state/jotai.ts rename to web/src/classic/state/index.ts index 12170fe7b1..7831c8d4e2 100644 --- a/web/src/services/state/jotai.ts +++ b/web/src/classic/state/index.ts @@ -6,7 +6,7 @@ import { LayerSelectionReason } from "@reearth/classic/core/Map"; import { Camera } from "@reearth/classic/util/value"; import { ProjectType } from "@reearth/types"; -export { default as useSetError, useError } from "./gqlErrorHandling"; +export { default as useSetError, useError } from "../../services/state/gqlErrorHandling"; const sceneId = atom(undefined); export const useSceneId = () => useAtom(sceneId); diff --git a/web/src/services/api/storytellingApi/blocks.ts b/web/src/services/api/storytellingApi/blocks.ts index 0145c52a7c..fafb1b9d30 100644 --- a/web/src/services/api/storytellingApi/blocks.ts +++ b/web/src/services/api/storytellingApi/blocks.ts @@ -1,6 +1,7 @@ import { useMutation, useQuery } from "@apollo/client"; import { useCallback, useMemo } from "react"; +import { AVAILABLE_STORY_BLOCK_IDS } from "@reearth/beta/lib/core/StoryPanel/constants"; import { MutationReturn } from "@reearth/services/api/types"; import { CreateStoryBlockInput, @@ -27,22 +28,6 @@ import { useNotification } from "@reearth/services/state"; import { Item, convert } from "../propertyApi/utils"; import { SceneQueryProps } from "../sceneApi"; -export const TITLE_BUILTIN_STORY_BLOCK_ID = "reearth/titleStoryBlock"; // pseudo storyblock - -export const IMAGE_BUILTIN_STORY_BLOCK_ID = "reearth/imageStoryBlock"; -export const TEXT_BUILTIN_STORY_BLOCK_ID = "reearth/textStoryBlock"; -export const VIDEO_BUILTIN_STORY_BLOCK_ID = "reearth/videoStoryBlock"; -export const MD_BUILTIN_STORY_BLOCK_ID = "reearth/mdTextStoryBlock"; -export const CAMERA_BUILTIN_STORY_BLOCK_ID = "reearth/cameraButtonStoryBlock"; - -export const AVAILABLE_STORY_BLOCK_IDS = [ - IMAGE_BUILTIN_STORY_BLOCK_ID, - TEXT_BUILTIN_STORY_BLOCK_ID, - VIDEO_BUILTIN_STORY_BLOCK_ID, - MD_BUILTIN_STORY_BLOCK_ID, - CAMERA_BUILTIN_STORY_BLOCK_ID, -]; - export type StoryBlockQueryProps = SceneQueryProps; export type InstallableStoryBlock = { diff --git a/web/src/services/api/storytellingApi/utils.ts b/web/src/services/api/storytellingApi/utils.ts index 5e5682138b..24e03d8d2d 100644 --- a/web/src/services/api/storytellingApi/utils.ts +++ b/web/src/services/api/storytellingApi/utils.ts @@ -33,6 +33,7 @@ export type Story = { isBasicAuthActive?: boolean; basicAuthUsername?: string; basicAuthPassword?: string; + publishmentStatus?: string; panelPosition?: "left" | "right"; alias?: string; pages?: Page[]; @@ -44,6 +45,7 @@ export const getStories = (rawScene?: GetSceneQuery) => { return scene?.stories.map(s => { return { ...s, + publishmentStatus: s.publishmentStatus, panelPosition: s.panelPosition === "RIGHT" ? "right" : "left", pages: s.pages.map(p => { return { diff --git a/web/src/services/state/index.ts b/web/src/services/state/index.ts index ee44c49c51..619f420c78 100644 --- a/web/src/services/state/index.ts +++ b/web/src/services/state/index.ts @@ -1,9 +1,25 @@ -import { makeVar } from "@apollo/client"; +import { atom, useAtom } from "jotai"; -import type { WidgetAreaType } from "@reearth/beta/lib/core/Crust"; import type { ComputedLayer, LayerSelectionReason } from "@reearth/beta/lib/core/Map"; import type { Camera } from "@reearth/beta/utils/value"; +export { default as useSetError, useError } from "./gqlErrorHandling"; + +export type WidgetAreaState = { + zone: "inner" | "outer"; + section: "left" | "center" | "right"; + area: "top" | "middle" | "bottom"; + align?: WidgetAlignment; + padding?: WidgetAreaPadding; + gap?: number | null; + centered?: boolean; + background?: string; +}; + +export type WidgetAlignment = "start" | "centered" | "end"; + +export type WidgetAreaPadding = { top: number; bottom: number; left: number; right: number }; + export type SelectedWidget = { id: string; pluginId: string; @@ -18,13 +34,80 @@ export type SelectedLayer = { layerSelectionReason?: LayerSelectionReason; }; +export type SelectedStoryPageId = string; + +export type NotificationType = "error" | "warning" | "info" | "success"; + +export type Notification = { + type: NotificationType; + heading?: string; + text: string; + duration?: number | "persistent"; +}; + +export type Policy = { + id: string; + name: string; + projectCount?: number | null; + memberCount?: number | null; + publishedProjectCount?: number | null; + layerCount?: number | null; + assetStorageSize?: number | null; + datasetSchemaCount?: number | null; + datasetCount?: number | null; +}; + +export type Workspace = { + id: string; + name: string; + members?: Array; + assets?: any; + projects?: any; + personal?: boolean; + policyId?: string | null; + policy?: Policy | null; +}; + // Visualizer -export const isVisualizerReadyVar = makeVar(false); -export const currentCameraVar = makeVar(undefined); +const isVisualizerReady = atom(false); +export const useIsVisualizerReady = () => useAtom(isVisualizerReady); +const currentCamera = atom(undefined); +export const useCurrentCamera = () => useAtom(currentCamera); + +const widgetAlignEditor = atom(undefined); +export const useWidgetAlignEditorActivated = () => useAtom(widgetAlignEditor); // Selected -export const selectedWidgetVar = makeVar(undefined); -export const selectedWidgetAreaVar = makeVar(undefined); -export const selectedLayerVar = makeVar(undefined); +const selectedWidget = atom(undefined); +export const useSelectedWidget = () => useAtom(selectedWidget); +const selectedWidgetArea = atom(undefined); +export const useSelectedWidgetArea = () => useAtom(selectedWidgetArea); +const selectedLayer = atom(undefined); +export const useSelectedLayer = () => useAtom(selectedLayer); +const selectedStoryPageId = atom(undefined); +export const useSelectedStoryPageId = () => useAtom(selectedStoryPageId); + +// Misc +const notification = atom(undefined); +export const useNotification = () => useAtom(notification); + +// Not sure we need belowNot sure we need belowNot sure we need belowNot sure we need below +// Not sure we need belowNot sure we need belowNot sure we need belowNot sure we need below +const isCapturing = atom(false); +export const useIsCapturing = () => useAtom(isCapturing); + +export type SceneMode = "3d" | "2d" | "columbus"; +const sceneMode = atom("3d"); +export const useSceneMode = () => useAtom(sceneMode); + +const selectedBlock = atom(undefined); +export const useSelectedBlock = () => useAtom(selectedBlock); + +const zoomedLayerId = atom(undefined); +export const useZoomedLayerId = () => useAtom(zoomedLayerId); + +const currentTheme = atom<"light" | "dark">("dark"); +export const useCurrentTheme = () => useAtom(currentTheme); -export * from "./jotai"; +const workspace = atom(undefined); +export const useWorkspace = () => useAtom(workspace);