diff --git a/server/pkg/builtin/manifest.yml b/server/pkg/builtin/manifest.yml index 3c01a96742..5292f2b9d3 100644 --- a/server/pkg/builtin/manifest.yml +++ b/server/pkg/builtin/manifest.yml @@ -2575,4 +2575,8 @@ extensions: - key: once label: Once - key: loop - label: Loop \ No newline at end of file + label: Loop + - id: nextPageStoryBlock + name: Next Page Button + type: storyBlock + description: Down button that will skip to next page. \ No newline at end of file diff --git a/server/pkg/builtin/manifest_ja.yml b/server/pkg/builtin/manifest_ja.yml index fc61e7c111..f734207991 100644 --- a/server/pkg/builtin/manifest_ja.yml +++ b/server/pkg/builtin/manifest_ja.yml @@ -1282,4 +1282,7 @@ extensions: bgColor: title: 背景色 showLayers: - title: 見えるレイヤー \ No newline at end of file + title: 見えるレイヤー + nextPageStoryBlock: + name: 次ページボタン + description: ストーリーテリングの次ページに移動するボタンブロック \ No newline at end of file diff --git a/web/src/beta/components/Icon/Icons/nextPageStoryBlock.svg b/web/src/beta/components/Icon/Icons/nextPageStoryBlock.svg new file mode 100644 index 0000000000..b84ee3475a --- /dev/null +++ b/web/src/beta/components/Icon/Icons/nextPageStoryBlock.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/icons.ts b/web/src/beta/components/Icon/icons.ts index debb6b8320..2b9db9a317 100644 --- a/web/src/beta/components/Icon/icons.ts +++ b/web/src/beta/components/Icon/icons.ts @@ -94,6 +94,7 @@ import CameraButtonStoryBlock from "./Icons/cameraButtonStoryBlock.svg"; import ShowLayersStoryBlock from "./Icons/showLayersStoryBlock.svg"; import TimelineStoryBlock from "./Icons/timelineStoryBlock.svg"; import TimelineStoryBlockSolid from "./Icons/timelineStoryBlockSolid.svg"; +import NextPageStoryBlock from "./Icons/nextPageStoryBlock.svg"; // Widget tab import Desktop from "./Icons/desktop.svg"; @@ -194,6 +195,7 @@ export default { showLayersStoryBlock: ShowLayersStoryBlock, timelineStoryBlock: TimelineStoryBlock, timelineStoryBlockSolid: TimelineStoryBlockSolid, + nextPageStoryBlock: NextPageStoryBlock, widget: Widgets, widgets: Widgets, menu: WidgetMenu, diff --git a/web/src/beta/features/Editor/Visualizer/convert-story.ts b/web/src/beta/features/Editor/Visualizer/convert-story.ts index f108f69d0a..c176a46cf4 100644 --- a/web/src/beta/features/Editor/Visualizer/convert-story.ts +++ b/web/src/beta/features/Editor/Visualizer/convert-story.ts @@ -1,6 +1,7 @@ import { Story, StoryBlock, StoryPage } from "@reearth/beta/lib/core/StoryPanel/types"; import { valueFromGQL, valueTypeFromGQL } from "@reearth/beta/utils/value"; import { toUi } from "@reearth/services/api/propertyApi/utils"; +import { Scene } from "@reearth/services/api/sceneApi"; import { PropertyFieldFragmentFragment, PropertyFragmentFragment, @@ -8,14 +9,23 @@ import { PropertyItemFragmentFragment, PropertySchemaFieldFragmentFragment, PropertySchemaGroupFragmentFragment, - Story as GqlStory, StoryPage as GqlStoryPage, StoryBlock as GqlStoryBlock, } from "@reearth/services/gql"; import { DatasetMap, P, datasetValue } from "./convert"; -export const convertStory = (story?: GqlStory): Story | undefined => { +export const convertStory = (scene?: Scene, storyId?: string): Story | undefined => { + const story = scene?.stories.find(s => s.id === storyId); + const installedBlockNames = (scene?.plugins ?? []) + .flatMap(p => + (p.plugin?.extensions ?? []) + .filter(e => e.type === "StoryBlock") + .map(e => ({ [e.extensionId]: e.translatedName ?? e.name })) + .filter((e): e is { [key: string]: string } => !!e), + ) + .reduce((result, obj) => ({ ...result, ...obj }), {}); + if (!story) return undefined; const storyPages = (pages: GqlStoryPage[]): StoryPage[] => @@ -32,7 +42,7 @@ export const convertStory = (story?: GqlStory): Story | undefined => { id: b.id, pluginId: b.pluginId, extensionId: b.extensionId, - name: b.property?.schema?.groups.find(g => g.schemaGroupId === "default")?.translatedTitle, + name: installedBlockNames?.[b.extensionId] ?? "Story Block", propertyId: b.property?.id, property: processProperty(undefined, b.property), })); diff --git a/web/src/beta/features/Editor/Visualizer/hooks.ts b/web/src/beta/features/Editor/Visualizer/hooks.ts index 96fcb5c252..92ea6921e1 100644 --- a/web/src/beta/features/Editor/Visualizer/hooks.ts +++ b/web/src/beta/features/Editor/Visualizer/hooks.ts @@ -198,10 +198,8 @@ export default ({ }; // Story - const story = useMemo( - () => convertStory(scene?.stories.find(s => s.id === storyId)), - [storyId, scene?.stories], - ); + const story = useMemo(() => convertStory(scene, storyId), [storyId, scene]); + const handleCurrentPageChange = useCallback( (pageId?: string) => selectSelectedStoryPageId(pageId), [selectSelectedStoryPageId], diff --git a/web/src/beta/lib/core/StoryPanel/ActionPanel/index.tsx b/web/src/beta/lib/core/StoryPanel/ActionPanel/index.tsx index 842f23e014..129ef12bd6 100644 --- a/web/src/beta/lib/core/StoryPanel/ActionPanel/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/ActionPanel/index.tsx @@ -85,13 +85,14 @@ const ActionPanel: React.FC = ({ const settingsTitle = useMemo(() => t("Spacing settings"), [t]); const popoverContent = useMemo(() => { - const menuItems: { name: string; icon: Icons; onClick: () => void }[] = [ - { + const menuItems: { name: string; icon: Icons; onClick: () => void }[] = []; + if (panelSettings) { + menuItems.push({ name: settingsTitle, icon: "padding", onClick: () => setShowPadding(true), - }, - ]; + }); + } if (onRemove) { menuItems.push({ name: t("Remove"), @@ -100,7 +101,7 @@ const ActionPanel: React.FC = ({ }); } return menuItems; - }, [settingsTitle, t, setShowPadding, onRemove, handleRemove]); + }, [settingsTitle, panelSettings, t, setShowPadding, onRemove, handleRemove]); return ( {dndEnabled && ( diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Image/index.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Image/index.tsx index 944e4971a7..8c40c2a643 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Image/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Image/index.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; -import type { CommonProps as BlockProps } from "@reearth//beta/lib/core/StoryPanel/Block/types"; import BlockWrapper from "@reearth/beta/lib/core/StoryPanel/Block/builtin/common/Wrapper"; +import type { CommonProps as BlockProps } from "@reearth/beta/lib/core/StoryPanel/Block/types"; import type { ValueTypes } from "@reearth/beta/utils/value"; import { styled } from "@reearth/services/theme"; diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/NextPage/index.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/NextPage/index.tsx new file mode 100644 index 0000000000..cc8c656c82 --- /dev/null +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/NextPage/index.tsx @@ -0,0 +1,64 @@ +import { useCallback } from "react"; + +import Icon from "@reearth/beta/components/Icon"; +import BlockWrapper from "@reearth/beta/lib/core/StoryPanel/Block/builtin/common/Wrapper"; +import type { CommonProps as BlockProps } from "@reearth/beta/lib/core/StoryPanel/Block/types"; +import { styled } from "@reearth/services/theme"; + +import { usePanelContext } from "../../../context"; + +const NextPage: React.FC = ({ block, pageId, isSelected, ...props }) => { + const { pageIds, onJumpToPage } = usePanelContext(); + + const handlePageChange = useCallback(() => { + if (!pageId) return; + const pageIndex = pageIds?.findIndex(id => id === pageId); + if (pageIndex === undefined) return; + onJumpToPage?.(pageIndex + 1); + }, [pageId, pageIds, onJumpToPage]); + + return ( + + + + + + ); +}; + +export default NextPage; + +const Wrapper = styled.div` + display: flex; + width: 100%; + justify-content: center; +`; + +const StyledIcon = styled(Icon)` + transition: none; +`; + +const Button = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 4px 12px; + border: 1px solid #2c2c2c; + border-radius: 6px; + transition: none; + cursor: pointer; + + :hover { + background: #8d8d8d; + border: 1px solid #8d8d8d; + color: ${({ theme }) => theme.content.strong}; + } +`; diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/index.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/index.tsx index ca419108ba..9eda6bd64e 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/index.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; -import type { CommonProps as BlockProps } from "@reearth//beta/lib/core/StoryPanel/Block/types"; import BlockWrapper from "@reearth/beta/lib/core/StoryPanel/Block/builtin/common/Wrapper"; +import type { CommonProps as BlockProps } from "@reearth/beta/lib/core/StoryPanel/Block/types"; import TimelineEditor from "./Editor"; diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Video/index.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Video/index.tsx index 3064720af2..315984d8ce 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Video/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Video/index.tsx @@ -1,8 +1,8 @@ import { useMemo } from "react"; -import type { CommonProps as BlockProps } from "@reearth//beta/lib/core/StoryPanel/Block/types"; import BlockWrapper from "@reearth/beta/lib/core/StoryPanel/Block/builtin/common/Wrapper"; import VideoPlayer from "@reearth/beta/lib/core/StoryPanel/Block/builtin/Video/VideoPlayer"; +import type { CommonProps as BlockProps } from "@reearth/beta/lib/core/StoryPanel/Block/types"; import type { ValueTypes } from "@reearth/beta/utils/value"; const VideoBlock: React.FC = ({ block, isSelected, ...props }) => { diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/common/ActionPanel/hooks.ts b/web/src/beta/lib/core/StoryPanel/Block/builtin/common/ActionPanel/hooks.ts index b52e38d9b1..d841225075 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/common/ActionPanel/hooks.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/common/ActionPanel/hooks.ts @@ -7,6 +7,7 @@ type Props = { icon?: string; isSelected?: boolean; editMode?: boolean; + panelSettings?: any; onEditModeToggle?: (e: MouseEvent) => void; onSettingsToggle?: (e: MouseEvent) => void; }; @@ -16,6 +17,7 @@ export default ({ icon, isSelected, editMode, + panelSettings, onEditModeToggle, onSettingsToggle, }: Props) => { @@ -27,7 +29,7 @@ export default ({ }, ]; - if (onEditModeToggle) { + if (onEditModeToggle && !!panelSettings) { menuItems.push({ icon: editMode ? "exit" : "storyBlockEdit", hide: !isSelected, @@ -44,7 +46,7 @@ export default ({ } return menuItems; - }, [title, icon, isSelected, editMode, onEditModeToggle, onSettingsToggle]); + }, [title, icon, isSelected, editMode, panelSettings, onEditModeToggle, onSettingsToggle]); return { actionItems, }; diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/common/ActionPanel/index.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/common/ActionPanel/index.tsx index 2e707fbfb7..f5e7424750 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/common/ActionPanel/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/common/ActionPanel/index.tsx @@ -51,6 +51,7 @@ const BlockActionPanel: React.FC = ({ isSelected, editMode, dndEnabled, + panelSettings, onEditModeToggle, onSettingsToggle, ...actionProps @@ -60,6 +61,7 @@ const BlockActionPanel: React.FC = ({ icon, isSelected, editMode, + panelSettings, onEditModeToggle, onSettingsToggle, }); @@ -69,6 +71,7 @@ const BlockActionPanel: React.FC = ({ dndEnabled={dndEnabled} isSelected={isSelected} actionItems={actionItems} + panelSettings={panelSettings} onSettingsToggle={onSettingsToggle} {...actionProps} /> diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/common/hooks.ts b/web/src/beta/lib/core/StoryPanel/Block/builtin/common/hooks.ts index 018d033407..2da32bda6a 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/common/hooks.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/common/hooks.ts @@ -51,8 +51,9 @@ export default ({ [property], ); - const panelSettings = useMemo( - () => ({ + const panelSettings = useMemo(() => { + if (!property?.panel) return undefined; + return { padding: { ...property?.panel?.padding, value: calculatePaddingValue( @@ -61,9 +62,8 @@ export default ({ isEditable, ), }, - }), - [property?.panel, isEditable], - ); + }; + }, [property?.panel, isEditable]); const handleEditModeToggle = useCallback(() => setEditMode?.(em => !em), []); 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 3b33564a07..c4955685fc 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts @@ -10,12 +10,14 @@ import { TITLE_BUILTIN_STORY_BLOCK_ID, VIDEO_BUILTIN_STORY_BLOCK_ID, LAYER_BUILTIN_STORY_BLOCK_ID, + NEXT_PAGE_BUILTIN_STORY_BLOCK_ID, } from "../../constants"; import CameraBlock from "./Camera"; import ImageBlock from "./Image"; import LayerBlock from "./Layer"; import MdBlock from "./Markdown"; +import NextPageBlock from "./NextPage"; import TextBlock from "./Text"; import TimelineBlock from "./Timeline"; import TitleBlock from "./Title"; @@ -29,7 +31,8 @@ export type ReEarthBuiltinStoryBlocks = Record< | typeof MD_BUILTIN_STORY_BLOCK_ID | typeof CAMERA_BUILTIN_STORY_BLOCK_ID | typeof LAYER_BUILTIN_STORY_BLOCK_ID - | typeof TIMELINE_BUILTIN_STORY_BLOCK_ID, + | typeof TIMELINE_BUILTIN_STORY_BLOCK_ID + | typeof NEXT_PAGE_BUILTIN_STORY_BLOCK_ID, T >; @@ -45,6 +48,7 @@ const reearthBuiltin: BuiltinStoryBlocks = { [TIMELINE_BUILTIN_STORY_BLOCK_ID]: TimelineBlock, [CAMERA_BUILTIN_STORY_BLOCK_ID]: CameraBlock, [LAYER_BUILTIN_STORY_BLOCK_ID]: LayerBlock, + [NEXT_PAGE_BUILTIN_STORY_BLOCK_ID]: NextPageBlock, }; const builtin = merge({}, reearthBuiltin); diff --git a/web/src/beta/lib/core/StoryPanel/Block/index.tsx b/web/src/beta/lib/core/StoryPanel/Block/index.tsx index 2243d8e6f8..387c43404b 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/index.tsx @@ -9,6 +9,7 @@ import type { CommonProps, BlockProps } from "./types"; export type Props = { renderBlock?: (block: BlockProps) => ReactNode; layer?: Layer; + pageId?: string; } & CommonProps; export type Component = ComponentType; diff --git a/web/src/beta/lib/core/StoryPanel/Block/types.ts b/web/src/beta/lib/core/StoryPanel/Block/types.ts index e35d13cfb6..9b2d4523ee 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/types.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/types.ts @@ -14,6 +14,7 @@ export type BlockProps = { }; export type CommonProps = { + pageId?: string; isEditable?: boolean; isBuilt?: boolean; isSelected?: boolean; diff --git a/web/src/beta/lib/core/StoryPanel/Page/index.tsx b/web/src/beta/lib/core/StoryPanel/Page/index.tsx index 04949202ca..1ab851b30b 100644 --- a/web/src/beta/lib/core/StoryPanel/Page/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/Page/index.tsx @@ -202,6 +202,7 @@ const StoryPanel: React.FC = ({ onBlockSelect?.(b.id)} diff --git a/web/src/beta/lib/core/StoryPanel/constants.ts b/web/src/beta/lib/core/StoryPanel/constants.ts index b2bb380f20..6b355abac0 100644 --- a/web/src/beta/lib/core/StoryPanel/constants.ts +++ b/web/src/beta/lib/core/StoryPanel/constants.ts @@ -21,6 +21,7 @@ export const MD_BUILTIN_STORY_BLOCK_ID = "reearth/mdTextStoryBlock"; export const CAMERA_BUILTIN_STORY_BLOCK_ID = "reearth/cameraButtonStoryBlock"; export const LAYER_BUILTIN_STORY_BLOCK_ID = "reearth/showLayersStoryBlock"; export const TIMELINE_BUILTIN_STORY_BLOCK_ID = "reearth/timelineStoryBlock"; +export const NEXT_PAGE_BUILTIN_STORY_BLOCK_ID = "reearth/nextPageStoryBlock"; export const AVAILABLE_STORY_BLOCK_IDS = [ IMAGE_BUILTIN_STORY_BLOCK_ID, @@ -30,4 +31,5 @@ export const AVAILABLE_STORY_BLOCK_IDS = [ CAMERA_BUILTIN_STORY_BLOCK_ID, LAYER_BUILTIN_STORY_BLOCK_ID, TIMELINE_BUILTIN_STORY_BLOCK_ID, + NEXT_PAGE_BUILTIN_STORY_BLOCK_ID, ]; diff --git a/web/src/beta/lib/core/StoryPanel/context.tsx b/web/src/beta/lib/core/StoryPanel/context.tsx index 2c791e1c97..c303759997 100644 --- a/web/src/beta/lib/core/StoryPanel/context.tsx +++ b/web/src/beta/lib/core/StoryPanel/context.tsx @@ -1,8 +1,10 @@ import { createContext, FC, PropsWithChildren, useContext } from "react"; export type StoryPanelContext = { + pageIds?: string[]; layerOverride?: { extensionId: string; layerIds?: string[] }; onLayerOverride?: (id?: string, layerIds?: string[]) => void; + onJumpToPage?: (newPageIndex: number) => void; }; const PanelContext = createContext(undefined); diff --git a/web/src/beta/lib/core/StoryPanel/hooks.ts b/web/src/beta/lib/core/StoryPanel/hooks.ts index fd77efd78c..04cb5a895f 100644 --- a/web/src/beta/lib/core/StoryPanel/hooks.ts +++ b/web/src/beta/lib/core/StoryPanel/hooks.ts @@ -206,6 +206,8 @@ export default ( showPageSettings, isAutoScrolling, layerOverride, + setCurrentPageId, + setLayerOverride, handleLayerOverride, handlePageSettingsToggle, handlePageSelect, diff --git a/web/src/beta/lib/core/StoryPanel/index.tsx b/web/src/beta/lib/core/StoryPanel/index.tsx index 8e26967756..c821b2bfe0 100644 --- a/web/src/beta/lib/core/StoryPanel/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/index.tsx @@ -81,6 +81,8 @@ export const StoryPanel = memo( showPageSettings, isAutoScrolling, layerOverride, + setCurrentPageId, + setLayerOverride, handleLayerOverride, handlePageSettingsToggle, handlePageSelect, @@ -97,8 +99,27 @@ export const StoryPanel = memo( ); const panelContext = useMemo( - () => ({ layerOverride, onLayerOverride: handleLayerOverride }), - [layerOverride, handleLayerOverride], + () => ({ + layerOverride, + pageIds: selectedStory?.pages.map(p => p.id), + onLayerOverride: handleLayerOverride, + onJumpToPage: (pageIndex: number) => { + const pageId = selectedStory?.pages[pageIndex].id; + if (!pageId) return; + const element = document.getElementById(pageId); + if (!element) return; + setCurrentPageId(pageId); + setLayerOverride(undefined); + element.scrollIntoView({ behavior: "instant" } as unknown as ScrollToOptions); // TODO: when typescript is updated to 5.1, remove this cast + }, + }), + [ + layerOverride, + selectedStory?.pages, + setCurrentPageId, + setLayerOverride, + handleLayerOverride, + ], ); return ( diff --git a/web/src/classic/gql/graphql-client-api.tsx b/web/src/classic/gql/graphql-client-api.tsx index 065b389ee5..db4676b6ee 100644 --- a/web/src/classic/gql/graphql-client-api.tsx +++ b/web/src/classic/gql/graphql-client-api.tsx @@ -22,7 +22,6 @@ export type Scalars = { FileSize: { input: number; output: number; } JSON: { input: any; output: any; } Lang: { input: string; output: string; } - Map: { input: any; output: any; } TranslatedString: { input: { [lang in string]?: string } | null; output: { [lang in string]?: string } | null; } URL: { input: string; output: string; } Upload: { input: any; output: any; } diff --git a/web/src/classic/gql/graphql.schema.json b/web/src/classic/gql/graphql.schema.json index 634d7dc0ed..d8e50ccc80 100644 --- a/web/src/classic/gql/graphql.schema.json +++ b/web/src/classic/gql/graphql.schema.json @@ -6162,16 +6162,6 @@ ], "possibleTypes": null }, - { - "kind": "SCALAR", - "name": "Map", - "description": null, - "fields": null, - "inputFields": null, - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "Me", diff --git a/web/src/services/gql/__gen__/graphql.ts b/web/src/services/gql/__gen__/graphql.ts index 797ce5439e..30a24d87d8 100644 --- a/web/src/services/gql/__gen__/graphql.ts +++ b/web/src/services/gql/__gen__/graphql.ts @@ -2582,8 +2582,7 @@ export enum ValueType { String = 'STRING', Timeline = 'TIMELINE', Typography = 'TYPOGRAPHY', - Url = 'URL', - timeline = 'TIMELINE' + Url = 'URL' } export enum Visualizer {