From 8e8203a2807eb6a7067803f3ecfc08e3864c97d9 Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Sun, 15 Oct 2023 16:53:30 +0300 Subject: [PATCH 01/38] add timeline in installable block --- .../components/Icon/Icons/timelineStoryBlock.svg | 15 +++++++++++++++ web/src/beta/components/Icon/icons.ts | 2 ++ web/src/services/api/storytellingApi/blocks.ts | 3 ++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 web/src/beta/components/Icon/Icons/timelineStoryBlock.svg diff --git a/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg b/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg new file mode 100644 index 0000000000..621bbdfddc --- /dev/null +++ b/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/web/src/beta/components/Icon/icons.ts b/web/src/beta/components/Icon/icons.ts index 6ea99a2739..866330a2ba 100644 --- a/web/src/beta/components/Icon/icons.ts +++ b/web/src/beta/components/Icon/icons.ts @@ -81,6 +81,7 @@ import VideoStoryBlock from "./Icons/videoStoryBlock.svg"; import ImageStoryBlock from "./Icons/imageStoryBlock.svg"; import MdTextStoryBlock from "./Icons/mdTextStoryBlock.svg"; import CameraButtonStoryBlock from "./Icons/cameraButtonStoryBlock.svg"; +import TimelineStoryBlock from "./Icons/timelineStoryBlock.svg"; // Widget tab import Desktop from "./Icons/desktop.svg"; @@ -168,6 +169,7 @@ export default { imageStoryBlock: ImageStoryBlock, mdTextStoryBlock: MdTextStoryBlock, cameraButtonStoryBlock: CameraButtonStoryBlock, + timelineStoryBlock: TimelineStoryBlock, widget: Widgets, widgets: Widgets, menu: WidgetMenu, diff --git a/web/src/services/api/storytellingApi/blocks.ts b/web/src/services/api/storytellingApi/blocks.ts index e82c47a316..d0d66ae0f9 100644 --- a/web/src/services/api/storytellingApi/blocks.ts +++ b/web/src/services/api/storytellingApi/blocks.ts @@ -33,12 +33,14 @@ 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 TIMELINE_BUILTIN_STORY_BLOCK_ID = "reearth/timelineStoryBlock"; 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, + TIMELINE_BUILTIN_STORY_BLOCK_ID, ]; export type StoryBlockQueryProps = SceneQueryProps; @@ -176,7 +178,6 @@ export default () => { const getInstallableStoryBlocks = (rawScene?: GetSceneQuery) => { const scene = rawScene?.node?.__typename === "Scene" ? rawScene.node : undefined; - return scene?.plugins .map(p => { const plugin = p.plugin; From 5ae74276dba2212e46ea9fd748e7d6bb3bb78200 Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Tue, 17 Oct 2023 17:43:58 +0300 Subject: [PATCH 02/38] improve timeline ui --- web/src/beta/components/Icon/Icons/play.svg | 3 + .../Icon/Icons/timeline-play-left.svg | 3 + .../Icon/Icons/timeline-play-right.svg | 3 + .../Icon/Icons/timelineStoryBlock.svg | 16 +- web/src/beta/components/Icon/icons.ts | 6 + .../Block/builtin/Timeline/index.tsx | 146 ++++++++++++++++++ .../core/StoryPanel/Block/builtin/index.ts | 6 +- 7 files changed, 168 insertions(+), 15 deletions(-) create mode 100644 web/src/beta/components/Icon/Icons/play.svg create mode 100644 web/src/beta/components/Icon/Icons/timeline-play-left.svg create mode 100644 web/src/beta/components/Icon/Icons/timeline-play-right.svg create mode 100644 web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/index.tsx diff --git a/web/src/beta/components/Icon/Icons/play.svg b/web/src/beta/components/Icon/Icons/play.svg new file mode 100644 index 0000000000..38cd5e13fa --- /dev/null +++ b/web/src/beta/components/Icon/Icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/Icons/timeline-play-left.svg b/web/src/beta/components/Icon/Icons/timeline-play-left.svg new file mode 100644 index 0000000000..8e40d78935 --- /dev/null +++ b/web/src/beta/components/Icon/Icons/timeline-play-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/Icons/timeline-play-right.svg b/web/src/beta/components/Icon/Icons/timeline-play-right.svg new file mode 100644 index 0000000000..93c1ab0e76 --- /dev/null +++ b/web/src/beta/components/Icon/Icons/timeline-play-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg b/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg index 621bbdfddc..4648358fb5 100644 --- a/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg +++ b/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg @@ -1,15 +1,3 @@ - - - - - - - - - - - - - - + + diff --git a/web/src/beta/components/Icon/icons.ts b/web/src/beta/components/Icon/icons.ts index 866330a2ba..a0f21fe089 100644 --- a/web/src/beta/components/Icon/icons.ts +++ b/web/src/beta/components/Icon/icons.ts @@ -52,6 +52,9 @@ import Timeline from "./Icons/timeline.svg"; import PlayRight from "./Icons/play-right.svg"; import PlayLeft from "./Icons/play-left.svg"; import Ellipse from "./Icons/ellipse.svg"; +import TimelinePlayRight from "./Icons/timeline-play-right.svg"; +import TimelinePlayLeft from "./Icons/timeline-play-left.svg"; +import Play from "./Icons/play.svg"; // Dashboard import Dashboard from "./Icons/dashboard.svg"; @@ -134,6 +137,9 @@ export default { ellipse: Ellipse, playRight: PlayRight, playLeft: PlayLeft, + timelinePlayLeft: TimelinePlayLeft, + timelinePlayRight: TimelinePlayRight, + play: Play, storyPage: StoryPage, square: Square, swiper: Swiper, 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 new file mode 100644 index 0000000000..2643a60cf8 --- /dev/null +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/index.tsx @@ -0,0 +1,146 @@ +import { useCallback, useMemo, useState } from "react"; + +import type { CommonProps as BlockProps } from "@reearth//beta/lib/core/StoryPanel/Block/types"; +import Icon from "@reearth/beta/components/Icon"; +import * as Popover from "@reearth/beta/components/Popover"; +import Text from "@reearth/beta/components/Text"; +import BlockWrapper from "@reearth/beta/lib/core/StoryPanel/Block/builtin/common/Wrapper"; +import { getFieldValue } from "@reearth/beta/lib/core/StoryPanel/utils"; +import type { ValueTypes } from "@reearth/beta/utils/value"; +import { styled } from "@reearth/services/theme"; + +const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => { + const src = useMemo( + () => getFieldValue(block?.property?.items ?? [], "src") as ValueTypes["string"], + [block?.property?.items], + ); + console.log(src); + + const [open, setOpen] = useState(false); + const playSpeedOptions = ["1 min/sec", "0.1 hr/sec", "0.5 hr/sec", "1 hr/sec"]; + const [selected, setSelected] = useState("1 min/sec"); + + const handlePopOver = useCallback(() => setOpen(!open), [open]); + + const handleClick = useCallback( + (value: string) => { + setOpen(false); + if (value !== selected) setSelected(value); + }, + [selected], + ); + + return ( + + + + + + + + + + + + + + + + + + + + {playSpeedOptions?.map((playSpeed, key) => ( + + ))} + + +
mmm
+ + + + ); +}; + +export default TimelineBlock; + +const Wrapper = styled.div` + color: ${({ theme }) => theme.content.weaker}; + border-radius: 8px; + border: 1px solid ${({ theme }) => theme.bg[3]}; + width: 100%; +`; + +const TimelineControl = styled.div` + display: flex; + align-items: center; + padding-bottom: 22px; + gap: 30px; +`; + +const StyledIcon = styled.div` + color: ${({ theme }) => theme.content.strong}; + cursor: pointer; + background: ${({ theme }) => theme.bg[4]}; + padding: 4px 6px 2px; + border-radius: 6px 0 8px 0; +`; +const PlayControl = styled.div` + display: flex; + gap: 10px; +`; + +const InputWrapper = styled.div` + position: relative; + cursor: "pointer"; +`; + +const ArrowIcon = styled(Icon)<{ open: boolean }>` + position: absolute; + right: 10px; + top: 50%; + transform: ${({ open }) => (open ? "translateY(-50%) scaleY(-1)" : "translateY(-50%)")}; + color: ${({ theme }) => theme.content.weaker}; +`; + +const Select = styled.div` + padding: 7px 8px; + font-size: 14px; + line-height: 1; + padding-right: 28px; + color: ${({ theme }) => theme.content.weaker}; +`; + +const PickerWrapper = styled(Popover.Content)` + min-width: 100px; + border: 1px solid ${({ theme }) => theme.outline.weak}; + outline: none; + border-radius: 4px; + background: ${({ theme }) => theme.bg[3]}; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); + display: flex; + gap: 4px; + flex-direction: column; + justify-content: space-between; + z-index: 1; +`; + +const Option = styled(Text)` + padding: 4px 12px; + &:hover { + background: ${({ theme }) => theme.bg[2]}; + } +`; 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 78959a9174..677a80ff8a 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts @@ -4,6 +4,7 @@ import { IMAGE_BUILTIN_STORY_BLOCK_ID, MD_BUILTIN_STORY_BLOCK_ID, TEXT_BUILTIN_STORY_BLOCK_ID, + TIMELINE_BUILTIN_STORY_BLOCK_ID, TITLE_BUILTIN_STORY_BLOCK_ID, VIDEO_BUILTIN_STORY_BLOCK_ID, } from "@reearth/services/api/storytellingApi/blocks"; @@ -13,6 +14,7 @@ import { Component } from ".."; import ImageBlock from "./Image"; import MdBlock from "./Markdown"; import TextBlock from "./Text"; +import TimelineBlock from "./Timeline"; import TitleBlock from "./Title"; import VideoBlock from "./Video"; @@ -21,7 +23,8 @@ export type ReEarthBuiltinStoryBlocks = Record< | typeof IMAGE_BUILTIN_STORY_BLOCK_ID | typeof TEXT_BUILTIN_STORY_BLOCK_ID | typeof VIDEO_BUILTIN_STORY_BLOCK_ID - | typeof MD_BUILTIN_STORY_BLOCK_ID, + | typeof MD_BUILTIN_STORY_BLOCK_ID + | typeof TIMELINE_BUILTIN_STORY_BLOCK_ID, T >; @@ -34,6 +37,7 @@ const reearthBuiltin: BuiltinStoryBlocks = { [TEXT_BUILTIN_STORY_BLOCK_ID]: TextBlock, [VIDEO_BUILTIN_STORY_BLOCK_ID]: VideoBlock, [MD_BUILTIN_STORY_BLOCK_ID]: MdBlock, + [TIMELINE_BUILTIN_STORY_BLOCK_ID]: TimelineBlock, }; const builtin = merge({}, reearthBuiltin); From 458ad3e43415ce3b02e3a752558c3bb22c625b75 Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Tue, 17 Oct 2023 17:44:49 +0300 Subject: [PATCH 03/38] improve timeline ui --- .../StoryPanel/Block/builtin/Timeline/index.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 2643a60cf8..257ec3bf01 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 @@ -7,6 +7,7 @@ import Text from "@reearth/beta/components/Text"; import BlockWrapper from "@reearth/beta/lib/core/StoryPanel/Block/builtin/common/Wrapper"; import { getFieldValue } from "@reearth/beta/lib/core/StoryPanel/utils"; import type { ValueTypes } from "@reearth/beta/utils/value"; +import { useT } from "@reearth/services/i18n"; import { styled } from "@reearth/services/theme"; const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => { @@ -15,7 +16,7 @@ const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => [block?.property?.items], ); console.log(src); - + const t = useT(); const [open, setOpen] = useState(false); const playSpeedOptions = ["1 min/sec", "0.1 hr/sec", "0.5 hr/sec", "1 hr/sec"]; const [selected, setSelected] = useState("1 min/sec"); @@ -50,21 +51,21 @@ const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => - + {playSpeedOptions?.map((playSpeed, key) => ( - + {t(`${playSpeed}`)} + ))} @@ -138,7 +139,7 @@ const PickerWrapper = styled(Popover.Content)` z-index: 1; `; -const Option = styled(Text)` +const SpeedOption = styled(Text)` padding: 4px 12px; &:hover { background: ${({ theme }) => theme.bg[2]}; From 427f6685663d7e90f1bb515b08cd9d5a9b7d87fa Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Wed, 18 Oct 2023 13:18:34 +0300 Subject: [PATCH 04/38] add slider ui --- web/src/beta/components/Icon/Icons/slider.svg | 3 + .../Icon/Icons/timelineStoryBlock.svg | 2 +- web/src/beta/components/Icon/icons.ts | 2 + .../Block/builtin/Timeline/Timeline.tsx | 174 ++++++++++++++++++ .../Block/builtin/Timeline/index.tsx | 126 +------------ 5 files changed, 184 insertions(+), 123 deletions(-) create mode 100644 web/src/beta/components/Icon/Icons/slider.svg create mode 100644 web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx diff --git a/web/src/beta/components/Icon/Icons/slider.svg b/web/src/beta/components/Icon/Icons/slider.svg new file mode 100644 index 0000000000..a216ee08b3 --- /dev/null +++ b/web/src/beta/components/Icon/Icons/slider.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg b/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg index 4648358fb5..c9052b6fcc 100644 --- a/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg +++ b/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg @@ -1,3 +1,3 @@ - + diff --git a/web/src/beta/components/Icon/icons.ts b/web/src/beta/components/Icon/icons.ts index a0f21fe089..656e636843 100644 --- a/web/src/beta/components/Icon/icons.ts +++ b/web/src/beta/components/Icon/icons.ts @@ -55,6 +55,7 @@ import Ellipse from "./Icons/ellipse.svg"; import TimelinePlayRight from "./Icons/timeline-play-right.svg"; import TimelinePlayLeft from "./Icons/timeline-play-left.svg"; import Play from "./Icons/play.svg"; +import Slider from "./Icons/slider.svg"; // Dashboard import Dashboard from "./Icons/dashboard.svg"; @@ -140,6 +141,7 @@ export default { timelinePlayLeft: TimelinePlayLeft, timelinePlayRight: TimelinePlayRight, play: Play, + slider: Slider, storyPage: StoryPage, square: Square, swiper: Swiper, diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx new file mode 100644 index 0000000000..eca0133bbe --- /dev/null +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx @@ -0,0 +1,174 @@ +import { useCallback, useState } from "react"; + +import Icon from "@reearth/beta/components/Icon"; +import * as Popover from "@reearth/beta/components/Popover"; +import Text from "@reearth/beta/components/Text"; +import { useT } from "@reearth/services/i18n"; +import { styled } from "@reearth/services/theme"; + +const Timeline: React.FC = () => { + const t = useT(); + const [open, setOpen] = useState(false); + const playSpeedOptions = ["1 min/sec", "0.1 hr/sec", "0.5 hr/sec", "1 hr/sec"]; + const [selected, setSelected] = useState("1 min/sec"); + + const handlePopOver = useCallback(() => setOpen(!open), [open]); + + const handleClick = useCallback( + (value: string) => { + setOpen(false); + if (value !== selected) setSelected(value); + }, + [selected], + ); + + return ( + + + + + + + + + + + + + + + + + + + {playSpeedOptions?.map((playSpeed, key) => ( + { + setSelected(playSpeed); + handleClick(playSpeed); + }}> + {t(`${playSpeed}`)} + + ))} + + + 2023/10/18 00:00:00 + + + + {[...Array(11)].map((_, index) => ( + + ))} + + + + + + + ); +}; + +export default Timeline; + +const Wrapper = styled.div` + color: ${({ theme }) => theme.content.weaker}; + border-radius: 8px; + border: 1px solid ${({ theme }) => theme.bg[3]}; + width: 100%; +`; + +const TimelineControl = styled.div` + display: flex; + align-items: center; + padding-bottom: 6px; + gap: 23px; +`; + +const StyledIcon = styled.div` + color: ${({ theme }) => theme.content.strong}; + cursor: pointer; + background: ${({ theme }) => theme.bg[4]}; + padding: 4px 6px 2px; + border-radius: 6px 0 8px 0; +`; +const PlayControl = styled.div` + display: flex; + gap: 10px; +`; + +const InputWrapper = styled.div` + position: relative; + cursor: pointer; +`; + +const ArrowIcon = styled(Icon)<{ open: boolean }>` + position: absolute; + right: 10px; + top: 60%; + transform: ${({ open }) => (open ? "translateY(-50%) scaleY(-1)" : "translateY(-50%)")}; + color: ${({ theme }) => theme.content.weaker}; +`; + +const Select = styled.div` + font-size: 15px; + line-height: 1; + padding-right: 28px; + width: 100%; + color: ${({ theme }) => theme.content.weaker}; +`; + +const PickerWrapper = styled(Popover.Content)` + min-width: 100px; + border: 1px solid ${({ theme }) => theme.outline.weak}; + outline: none; + border-radius: 4px; + background: ${({ theme }) => theme.bg[3]}; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); + display: flex; + gap: 4px; + flex-direction: column; + justify-content: space-between; + z-index: 1; +`; + +const SpeedOption = styled(Text)` + padding: 4px 12px; + &:hover { + background: ${({ theme }) => theme.bg[2]}; + } +`; + +const CurrentTime = styled.div` + color: ${({ theme }) => theme.content.weaker}; + position: relative; +`; + +const TimelineSlider = styled.div` + background: #e0e0e0; + height: 38px; + width: 100%; + border-radius: 0px 0 8px 8px; + position: relative; +`; + +const ScaleList = styled.div` + width: 100%; + display: flex; + height: 38px; + align-items: flex-end; + justify-content: space-between; +`; + +const IconWrapper = styled.div` + position: absolute; + top: 4px; + left: 16px; +`; + +const Scale = styled.div` + height: 6px; + border-left: 1px solid ${({ theme }) => theme.content.weak}; + margin: 0 auto; +`; 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 257ec3bf01..f136e152d6 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,14 +1,11 @@ -import { useCallback, useMemo, useState } from "react"; +import { useMemo } from "react"; import type { CommonProps as BlockProps } from "@reearth//beta/lib/core/StoryPanel/Block/types"; -import Icon from "@reearth/beta/components/Icon"; -import * as Popover from "@reearth/beta/components/Popover"; -import Text from "@reearth/beta/components/Text"; import BlockWrapper from "@reearth/beta/lib/core/StoryPanel/Block/builtin/common/Wrapper"; import { getFieldValue } from "@reearth/beta/lib/core/StoryPanel/utils"; import type { ValueTypes } from "@reearth/beta/utils/value"; -import { useT } from "@reearth/services/i18n"; -import { styled } from "@reearth/services/theme"; + +import Timeline from "./Timeline"; const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => { const src = useMemo( @@ -16,20 +13,6 @@ const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => [block?.property?.items], ); console.log(src); - const t = useT(); - const [open, setOpen] = useState(false); - const playSpeedOptions = ["1 min/sec", "0.1 hr/sec", "0.5 hr/sec", "1 hr/sec"]; - const [selected, setSelected] = useState("1 min/sec"); - - const handlePopOver = useCallback(() => setOpen(!open), [open]); - - const handleClick = useCallback( - (value: string) => { - setOpen(false); - if (value !== selected) setSelected(value); - }, - [selected], - ); return ( = ({ block, isSelected, ...props }) => propertyId={block?.property?.id} propertyItems={block?.property?.items} {...props}> - - - - - - - - - - - - - - - - - - - {playSpeedOptions?.map((playSpeed, key) => ( - { - setSelected(playSpeed); - handleClick(playSpeed); - }}> - {t(`${playSpeed}`)} - - ))} - - -
mmm
-
-
+
); }; export default TimelineBlock; - -const Wrapper = styled.div` - color: ${({ theme }) => theme.content.weaker}; - border-radius: 8px; - border: 1px solid ${({ theme }) => theme.bg[3]}; - width: 100%; -`; - -const TimelineControl = styled.div` - display: flex; - align-items: center; - padding-bottom: 22px; - gap: 30px; -`; - -const StyledIcon = styled.div` - color: ${({ theme }) => theme.content.strong}; - cursor: pointer; - background: ${({ theme }) => theme.bg[4]}; - padding: 4px 6px 2px; - border-radius: 6px 0 8px 0; -`; -const PlayControl = styled.div` - display: flex; - gap: 10px; -`; - -const InputWrapper = styled.div` - position: relative; - cursor: "pointer"; -`; - -const ArrowIcon = styled(Icon)<{ open: boolean }>` - position: absolute; - right: 10px; - top: 50%; - transform: ${({ open }) => (open ? "translateY(-50%) scaleY(-1)" : "translateY(-50%)")}; - color: ${({ theme }) => theme.content.weaker}; -`; - -const Select = styled.div` - padding: 7px 8px; - font-size: 14px; - line-height: 1; - padding-right: 28px; - color: ${({ theme }) => theme.content.weaker}; -`; - -const PickerWrapper = styled(Popover.Content)` - min-width: 100px; - border: 1px solid ${({ theme }) => theme.outline.weak}; - outline: none; - border-radius: 4px; - background: ${({ theme }) => theme.bg[3]}; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); - display: flex; - gap: 4px; - flex-direction: column; - justify-content: space-between; - z-index: 1; -`; - -const SpeedOption = styled(Text)` - padding: 4px 12px; - &:hover { - background: ${({ theme }) => theme.bg[2]}; - } -`; From 5d04c7dffb2146cb965f648902e7d17c8ebccc28 Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Thu, 19 Oct 2023 14:04:21 +0300 Subject: [PATCH 05/38] implement intial time for time block --- .../beta/lib/core/Map/useTimelineManager.ts | 2 +- .../Block/builtin/Timeline/Timeline.tsx | 47 +++++++++++++++---- .../StoryPanel/Block/builtin/Timeline/hook.ts | 43 +++++++++++++++++ .../beta/lib/core/StoryPanel/Block/types.ts | 6 +++ .../core/StoryPanel/hooks/useTimelineBlock.ts | 42 +++++++++++++++++ web/src/beta/lib/core/StoryPanel/utils.ts | 43 +++++++++++++++++ 6 files changed, 174 insertions(+), 9 deletions(-) create mode 100644 web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts create mode 100644 web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts diff --git a/web/src/beta/lib/core/Map/useTimelineManager.ts b/web/src/beta/lib/core/Map/useTimelineManager.ts index e84ac4ecef..f82c4c69f7 100644 --- a/web/src/beta/lib/core/Map/useTimelineManager.ts +++ b/web/src/beta/lib/core/Map/useTimelineManager.ts @@ -25,7 +25,7 @@ export type Timeline = { stop: Date; }; -type EngineClock = { +export type EngineClock = { current: Date | undefined; start: Date | undefined; stop: Date | undefined; diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx index eca0133bbe..ab33b4bdfa 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx @@ -3,6 +3,8 @@ import { useCallback, useState } from "react"; import Icon from "@reearth/beta/components/Icon"; import * as Popover from "@reearth/beta/components/Popover"; import Text from "@reearth/beta/components/Text"; +import useHooks from "@reearth/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook"; +import useTimelineBlock from "@reearth/beta/lib/core/StoryPanel/hooks/useTimelineBlock"; import { useT } from "@reearth/services/i18n"; import { styled } from "@reearth/services/theme"; @@ -11,6 +13,8 @@ const Timeline: React.FC = () => { const [open, setOpen] = useState(false); const playSpeedOptions = ["1 min/sec", "0.1 hr/sec", "0.5 hr/sec", "1 hr/sec"]; const [selected, setSelected] = useState("1 min/sec"); + const { currentTime, range } = useTimelineBlock(); + const { formattedCurrentTime, timeRange } = useHooks({ currentTime, range }); const handlePopOver = useCallback(() => setOpen(!open), [open]); @@ -54,12 +58,29 @@ const Timeline: React.FC = () => { ))} - 2023/10/18 00:00:00 + {currentTime && formattedCurrentTime} - {[...Array(11)].map((_, index) => ( - + {[...Array(11)].map((_, idx) => ( + + {idx === 0 ? ( + <> + {timeRange?.startTime.date} + {timeRange?.startTime.time} + + ) : idx === 5 ? ( + <> + {timeRange?.midTime.date} + {timeRange?.midTime.time} + + ) : idx === 10 ? ( + <> + {timeRange?.endTime.date} + {timeRange?.endTime.time} + + ) : null} + ))} @@ -83,7 +104,7 @@ const TimelineControl = styled.div` display: flex; align-items: center; padding-bottom: 6px; - gap: 23px; + gap: 24px; `; const StyledIcon = styled.div` @@ -130,7 +151,7 @@ const PickerWrapper = styled(Popover.Content)` gap: 4px; flex-direction: column; justify-content: space-between; - z-index: 1; + z-idx: 1; `; const SpeedOption = styled(Text)` @@ -143,6 +164,7 @@ const SpeedOption = styled(Text)` const CurrentTime = styled.div` color: ${({ theme }) => theme.content.weaker}; position: relative; + font-size: 14px; `; const TimelineSlider = styled.div` @@ -154,11 +176,11 @@ const TimelineSlider = styled.div` `; const ScaleList = styled.div` - width: 100%; + width: 99%; display: flex; height: 38px; align-items: flex-end; - justify-content: space-between; + padding-left: 17px; `; const IconWrapper = styled.div` @@ -168,7 +190,16 @@ const IconWrapper = styled.div` `; const Scale = styled.div` - height: 6px; + height: 5px; border-left: 1px solid ${({ theme }) => theme.content.weak}; margin: 0 auto; + flex: 1; + text-align: center; +`; + +const ScaleLabel = styled.div` + font-size: 10px; + position: relative; + bottom: 28px; + right: 15px; `; diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts new file mode 100644 index 0000000000..3a8f74e45a --- /dev/null +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts @@ -0,0 +1,43 @@ +import { useMemo } from "react"; + +import { formatDateForSliderTimeline, formatDateForTimeline } from "../../../utils"; +import { Range } from "../../types"; + +type TimelineProps = { + currentTime: number; + range?: Range; +}; + +export default ({ currentTime, range }: TimelineProps) => { + const formattedCurrentTime = useMemo(() => { + const textDate = formatDateForTimeline(currentTime, { detail: true }); + return textDate; + }, [currentTime]); + + const formatRangeDateAndTime = (data: string) => { + const lastIdx = data.lastIndexOf(" "); + const date = data.slice(0, lastIdx); + const time = data.slice(lastIdx + 1); + return { + date, + time, + }; + }; + + const timeRange = useMemo(() => { + if (range) { + return { + startTime: formatRangeDateAndTime( + formatDateForSliderTimeline(range.start, { detail: true }), + ), + midTime: formatRangeDateAndTime(formatDateForSliderTimeline(range.mid)), + endTime: formatRangeDateAndTime(formatDateForSliderTimeline(range.end)), + }; + } + }, [range]); + + return { + formattedCurrentTime, + timeRange, + }; +}; diff --git a/web/src/beta/lib/core/StoryPanel/Block/types.ts b/web/src/beta/lib/core/StoryPanel/Block/types.ts index fa5fc821ff..d390f7466c 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/types.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/types.ts @@ -32,3 +32,9 @@ export type CommonProps = { ) => Promise; onFlyTo?: FlyTo; }; + +export type Range = { + start: number; + mid: number; + end: number; +}; diff --git a/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts b/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts new file mode 100644 index 0000000000..943f666527 --- /dev/null +++ b/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts @@ -0,0 +1,42 @@ +import { useState } from "react"; + +import { useVisualizer } from "@reearth/beta/lib/core/Visualizer"; + +const getNewDate = (d?: Date) => d ?? new Date(); + +const calculateEndTime = (date: Date) => { + date.setHours(23, 59, 59, 999); + return date.getTime(); +}; + +const calculateMidTime = (startTime: number, stopTime: number) => { + return (startTime + stopTime) / 2; +}; + +const timeRange = (startTime?: number, stopTime?: number) => { + // To avoid out of range error in Cesium, we need to turn back a hour. + const now = Date.now() - 3600000; + return { + start: startTime || now, + end: stopTime || calculateEndTime(new Date()), + mid: calculateMidTime(startTime || now, stopTime || calculateEndTime(new Date())), + }; +}; +export default () => { + const visualizerContext = useVisualizer(); + const [currentTime] = useState(() => + getNewDate(visualizerContext?.current?.timeline?.current?.timeline?.current)?.getTime(), + ); + + const [range] = useState(() => + timeRange( + visualizerContext?.current?.timeline?.current?.timeline?.start?.getTime(), + visualizerContext?.current?.timeline?.current?.timeline?.stop?.getTime(), + ), + ); + + return { + currentTime, + range, + }; +}; diff --git a/web/src/beta/lib/core/StoryPanel/utils.ts b/web/src/beta/lib/core/StoryPanel/utils.ts index 3353ee7d40..c40606fe4a 100644 --- a/web/src/beta/lib/core/StoryPanel/utils.ts +++ b/web/src/beta/lib/core/StoryPanel/utils.ts @@ -43,3 +43,46 @@ export const calculatePaddingValue = ( ) : defaultValue; }; + +const MONTH_LABEL_LIST = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +]; + +export const formatDateForTimeline = (time: number, options: { detail?: boolean } = {}) => { + const d = new Date(time); + + const year = d.getFullYear(); + const month = MONTH_LABEL_LIST[d.getMonth()]; + const date = `${d.getDate()}`.padStart(2, "0"); + const hour = `${d.getHours()}`.padStart(2, "0"); + if (!options.detail) { + return `${year} ${month} ${date} ${hour}:00:00.00`; + } + const minutes = `${d.getMinutes()}`.padStart(2, "0"); + const seconds = `${d.getSeconds()}`.padStart(2, "0"); + return `${year} ${month} ${date} ${hour}:${minutes}:${seconds}.00`; +}; + +export const formatDateForSliderTimeline = (time: number, options: { detail?: boolean } = {}) => { + const d = new Date(time); + + const month = MONTH_LABEL_LIST[d.getMonth()]; + const date = `${d.getDate()}`.padStart(2, "0"); + const hour = `${d.getHours()}`.padStart(2, "0"); + if (!options.detail) { + return `${month} ${date} ${hour}:59`; + } + const minutes = `${d.getMinutes()}`.padStart(2, "0"); + return ` ${month} ${date} ${hour}:${minutes}`; +}; From 58ccd92f224347ac9fa6e00a13f918636a84a214 Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Fri, 20 Oct 2023 01:44:53 +0300 Subject: [PATCH 06/38] implement commit methods --- .../Block/builtin/Timeline/Timeline.tsx | 34 ++- .../StoryPanel/Block/builtin/Timeline/hook.ts | 59 ++++- .../Block/builtin/Timeline/index.tsx | 18 +- .../core/StoryPanel/hooks/useTimelineBlock.ts | 220 +++++++++++++++++- web/src/beta/lib/core/StoryPanel/utils.ts | 5 + 5 files changed, 308 insertions(+), 28 deletions(-) diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx index ab33b4bdfa..2b1c279d17 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx @@ -8,16 +8,35 @@ import useTimelineBlock from "@reearth/beta/lib/core/StoryPanel/hooks/useTimelin import { useT } from "@reearth/services/i18n"; import { styled } from "@reearth/services/theme"; -const Timeline: React.FC = () => { +type TimelineProps = { + blockId?: string; +}; +const Timeline = ({ blockId }: TimelineProps) => { const t = useT(); const [open, setOpen] = useState(false); const playSpeedOptions = ["1 min/sec", "0.1 hr/sec", "0.5 hr/sec", "1 hr/sec"]; const [selected, setSelected] = useState("1 min/sec"); - const { currentTime, range } = useTimelineBlock(); - const { formattedCurrentTime, timeRange } = useHooks({ currentTime, range }); + const { currentTime, range, speed, onClick, onDrag, onPlay, onPlayReversed, onSpeedChange } = + useTimelineBlock(); + const { + formattedCurrentTime, + timeRange, + toggleIsPlaying, + toggleIsPlayingReversed, + handleOnSpeedChange, + } = useHooks({ + currentTime, + range, + onClick, + onDrag, + onPlay, + onPlayReversed, + onSpeedChange, + }); const handlePopOver = useCallback(() => setOpen(!open), [open]); + console.log(speed, blockId); const handleClick = useCallback( (value: string) => { setOpen(false); @@ -33,9 +52,9 @@ const Timeline: React.FC = () => { - + - + @@ -50,6 +69,7 @@ const Timeline: React.FC = () => { size="footnote" key={key} onClick={() => { + handleOnSpeedChange; setSelected(playSpeed); handleClick(playSpeed); }}> @@ -104,7 +124,7 @@ const TimelineControl = styled.div` display: flex; align-items: center; padding-bottom: 6px; - gap: 24px; + gap: 28px; `; const StyledIcon = styled.div` @@ -164,7 +184,7 @@ const SpeedOption = styled(Text)` const CurrentTime = styled.div` color: ${({ theme }) => theme.content.weaker}; position: relative; - font-size: 14px; + font-size: 13px; `; const TimelineSlider = styled.div` diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts index 3a8f74e45a..03fc4d7672 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts @@ -1,14 +1,62 @@ -import { useMemo } from "react"; +import { ChangeEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { formatDateForSliderTimeline, formatDateForTimeline } from "../../../utils"; -import { Range } from "../../types"; +import { Range } from "@reearth/beta/lib/core/StoryPanel/Block/types"; +import { + formatDateForSliderTimeline, + formatDateForTimeline, +} from "@reearth/beta/lib/core/StoryPanel/utils"; type TimelineProps = { currentTime: number; range?: Range; + onClick?: (t: number) => void; + onDrag?: (t: number) => void; + onPlay?: (isPlaying: boolean) => void; + onPlayReversed?: (isPlaying: boolean) => void; + onSpeedChange?: (speed: number) => void; }; -export default ({ currentTime, range }: TimelineProps) => { +export default ({ + currentTime, + range, + onClick, + onDrag, + onPlay, + onPlayReversed, + onSpeedChange, +}: TimelineProps) => { + const [isPlaying, setIsPlaying] = useState(false); + const [isPlayingReversed, setIsPlayingReversed] = useState(false); + const syncCurrentTimeRef = useRef(currentTime); + console.log(onClick, onDrag); + const handleOnSpeedChange: ChangeEventHandler = useCallback( + e => { + onSpeedChange?.(parseInt(e.currentTarget.value, 10)); + }, + [onSpeedChange], + ); + const toggleIsPlaying = useCallback(() => { + if (isPlayingReversed) { + setIsPlayingReversed(false); + onPlayReversed?.(false); + } + setIsPlaying(p => !p); + onPlay?.(!isPlaying); + }, [isPlayingReversed, onPlay, isPlaying, onPlayReversed]); + + const toggleIsPlayingReversed = useCallback(() => { + if (isPlaying) { + setIsPlaying(false); + onPlay?.(false); + } + setIsPlayingReversed(p => !p); + onPlayReversed?.(!isPlayingReversed); + }, [isPlaying, isPlayingReversed, onPlay, onPlayReversed]); + + useEffect(() => { + syncCurrentTimeRef.current = currentTime; + }, [currentTime]); + const formattedCurrentTime = useMemo(() => { const textDate = formatDateForTimeline(currentTime, { detail: true }); return textDate; @@ -39,5 +87,8 @@ export default ({ currentTime, range }: TimelineProps) => { return { formattedCurrentTime, timeRange, + toggleIsPlaying, + toggleIsPlayingReversed, + handleOnSpeedChange, }; }; 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 f136e152d6..4821770665 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,27 +1,19 @@ -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 { getFieldValue } from "@reearth/beta/lib/core/StoryPanel/utils"; -import type { ValueTypes } from "@reearth/beta/utils/value"; import Timeline from "./Timeline"; const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => { - const src = useMemo( - () => getFieldValue(block?.property?.items ?? [], "src") as ValueTypes["string"], - [block?.property?.items], - ); - console.log(src); - return ( - + ); }; diff --git a/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts b/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts index 943f666527..cf8fc90190 100644 --- a/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts +++ b/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts @@ -1,9 +1,12 @@ -import { useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useVisualizer } from "@reearth/beta/lib/core/Visualizer"; -const getNewDate = (d?: Date) => d ?? new Date(); +import { TickEventCallback, TimelineCommitter } from "../../Map/useTimelineManager"; +import { formatDateToSting } from "../utils"; +type TimeHandler = (t: number) => void; +const getNewDate = (d?: Date) => d ?? new Date(); const calculateEndTime = (date: Date) => { date.setHours(23, 59, 59, 999); return date.getTime(); @@ -22,21 +25,230 @@ const timeRange = (startTime?: number, stopTime?: number) => { mid: calculateMidTime(startTime || now, stopTime || calculateEndTime(new Date())), }; }; + export default () => { const visualizerContext = useVisualizer(); - const [currentTime] = useState(() => + const [currentTime, setCurrentTime] = useState(() => getNewDate(visualizerContext?.current?.timeline?.current?.timeline?.current)?.getTime(), ); - const [range] = useState(() => + const [range, setRange] = useState(() => timeRange( visualizerContext?.current?.timeline?.current?.timeline?.start?.getTime(), visualizerContext?.current?.timeline?.current?.timeline?.stop?.getTime(), ), ); + const onPause = useCallback( + (committer?: TimelineCommitter) => { + return visualizerContext.current?.timeline?.current?.commit({ + cmd: "PAUSE", + committer: { source: "storyTimelineBlock", id: committer?.id }, + }); + }, + [visualizerContext], + ); + + const onPlay = useCallback( + (committer?: TimelineCommitter) => { + return visualizerContext.current?.timeline?.current?.commit({ + cmd: "PLAY", + committer: { source: "storyTimelineBlock", id: committer?.id }, + }); + }, + [visualizerContext], + ); + + const onTimeChange = useCallback( + (time: Date, committer?: TimelineCommitter) => { + return visualizerContext.current?.timeline?.current?.commit({ + cmd: "SET_TIME", + payload: { + start: formatDateToSting(range?.start), + current: formatDateToSting(currentTime), + stop: formatDateToSting(range.end), + }, + committer: { source: "storyTimelineBlock", id: committer?.id }, + }); + }, + [currentTime, range.end, range?.start, visualizerContext], + ); + + const onSpeedChange = useCallback( + (speed: number, committer?: TimelineCommitter) => { + return visualizerContext.current?.timeline?.current?.commit({ + cmd: "SET_OPTIONS", + payload: { + multiplier: speed, + stepType: "rate", + }, + committer: { source: "storyTimelineBlock", id: committer?.id }, + }); + }, + [visualizerContext], + ); + + const onTick = useCallback( + (cb: TickEventCallback) => visualizerContext?.current?.timeline?.current?.onTick(cb), + [visualizerContext], + ); + const removeTickEventListener = useCallback( + (cb: TickEventCallback) => visualizerContext?.current?.timeline?.current?.offTick(cb), + [visualizerContext], + ); + + const timeSpeed = visualizerContext?.current?.timeline?.current?.options.multiplier || 1; + const startTime = visualizerContext?.current?.timeline?.current?.timeline?.start?.getTime(); + const stopTime = visualizerContext?.current?.timeline?.current?.timeline?.stop?.getTime(); + const [speed, setSpeed] = useState(timeSpeed); + + const handleOnPlay = useCallback( + (playing: boolean) => { + // Stop cesium animation + if (playing) { + onPlay?.(); + console.log("called"); + } else { + onPause?.(); + } + onSpeedChange?.(Math.abs(speed)); + }, + [onPause, onPlay, onSpeedChange, speed], + ); + + const handleOnPlayReversed = useCallback( + (playing: boolean) => { + // Stop cesium animation + if (playing) { + onPlay?.(); + } else { + onPause?.(); + } + onSpeedChange?.(Math.abs(speed) * -1); + }, + [onPause, onPlay, onSpeedChange, speed], + ); + + const handleTimeEvent: TimeHandler = useCallback( + currentTime => { + const t = new Date(currentTime); + onTimeChange?.(t); + setCurrentTime(currentTime); + }, + [onTimeChange], + ); + + const handleOnSpeedChange = useCallback( + (speed: number) => { + setSpeed(speed); + + const absSpeed = Math.abs(speed); + onSpeedChange?.( + (visualizerContext?.current?.timeline?.current?.options.multiplier ?? 1) > 0 + ? absSpeed + : absSpeed * -1, + ); + }, + [onSpeedChange, visualizerContext], + ); + + const isClockInitialized = useRef(false); + + useEffect(() => { + if (!isClockInitialized.current) { + isClockInitialized.current = true; + queueMicrotask(() => { + onSpeedChange?.(1); + }); + } + }, [onSpeedChange]); + + const handleRange = useCallback((start: number | undefined, stop: number | undefined) => { + setRange(prev => { + const next = timeRange(start, stop); + if (prev.start !== next.start || prev.end !== next.end) { + return next; + } + return prev; + }); + }, []); + + const overriddenStart = + visualizerContext?.current?.timeline?.current?.computedTimeline?.start?.getTime(); + const overriddenStop = + visualizerContext?.current?.timeline?.current?.computedTimeline?.stop.getTime(); + + // Sync cesium clock. + useEffect(() => { + handleRange(overriddenStart ?? startTime, overriddenStop ?? stopTime); + setSpeed(Math.abs(timeSpeed)); + }, [overriddenStart, overriddenStop, handleRange, startTime, stopTime, timeSpeed]); + + const lastTime = useRef(); + const switchCurrentTimeToStart = useCallback( + (t: number, isRangeChanged: boolean) => { + const cur = isRangeChanged + ? t + : t > range.end + ? range.start + : t < range.start + ? range.end + : t; + + if (lastTime.current !== cur) { + lastTime.current = cur; + onTimeChange?.(new Date(cur)); + } + return cur; + }, + [range, onTimeChange], + ); + + useEffect(() => { + const h: TickEventCallback = (d, c) => { + const isDifferentRange = range.start !== c.start.getTime() || range.end !== c.stop.getTime(); + if (isDifferentRange) { + handleRange(c.start.getTime(), c.stop.getTime()); + } + setCurrentTime(switchCurrentTimeToStart(d.getTime(), isDifferentRange)); + }; + onTick?.(h); + return () => { + removeTickEventListener?.(h); + }; + }, [ + switchCurrentTimeToStart, + handleRange, + range.start, + range.end, + onTick, + removeTickEventListener, + ]); + + const onTimeChangeRef = useRef(); + + useEffect(() => { + onTimeChangeRef.current = onTimeChange; + }, [onTimeChange]); + + const overriddenCurrentTime = + visualizerContext?.current?.timeline?.current?.computedTimeline?.current?.getTime(); + useEffect(() => { + if (overriddenCurrentTime) { + const t = Math.max(Math.min(range.end, overriddenCurrentTime), range.start); + setCurrentTime(t); + // onTimeChangeRef.current?.(new Date(t)); + } + }, [overriddenCurrentTime, range]); + return { currentTime, range, + speed, + onClick: handleTimeEvent, + onDrag: handleTimeEvent, + onPlay: handleOnPlay, + onPlayReversed: handleOnPlayReversed, + onSpeedChange: handleOnSpeedChange, }; }; diff --git a/web/src/beta/lib/core/StoryPanel/utils.ts b/web/src/beta/lib/core/StoryPanel/utils.ts index c40606fe4a..838b69ff4a 100644 --- a/web/src/beta/lib/core/StoryPanel/utils.ts +++ b/web/src/beta/lib/core/StoryPanel/utils.ts @@ -86,3 +86,8 @@ export const formatDateForSliderTimeline = (time: number, options: { detail?: bo const minutes = `${d.getMinutes()}`.padStart(2, "0"); return ` ${month} ${date} ${hour}:${minutes}`; }; + +export const formatDateToSting = (d: number) => { + const date = new Date(d); + return date.toISOString(); +}; From 5a84334cec68298ea268fcf16cc0f1a713a410fc Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Fri, 20 Oct 2023 16:59:48 +0300 Subject: [PATCH 07/38] improve play controll --- .../Block/builtin/Timeline/Timeline.tsx | 41 +++++++++-- .../StoryPanel/Block/builtin/Timeline/hook.ts | 67 +++++++++++++----- .../Block/builtin/Timeline/index.tsx | 4 +- .../core/StoryPanel/hooks/useTimelineBlock.ts | 70 +++++++++---------- 4 files changed, 122 insertions(+), 60 deletions(-) diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx index 2b1c279d17..0d1ef97fa2 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx @@ -10,33 +10,40 @@ import { styled } from "@reearth/services/theme"; type TimelineProps = { blockId?: string; + isSelected?: boolean; }; -const Timeline = ({ blockId }: TimelineProps) => { +const Timeline = ({ blockId, isSelected }: TimelineProps) => { const t = useT(); const [open, setOpen] = useState(false); const playSpeedOptions = ["1 min/sec", "0.1 hr/sec", "0.5 hr/sec", "1 hr/sec"]; const [selected, setSelected] = useState("1 min/sec"); - const { currentTime, range, speed, onClick, onDrag, onPlay, onPlayReversed, onSpeedChange } = + const { currentTime, range, onClick, onDrag, onPlay, onPlayReversed, onSpeedChange, onPause } = useTimelineBlock(); const { formattedCurrentTime, timeRange, + isPlaying, + isPlayingReversed, + isPause, toggleIsPlaying, toggleIsPlayingReversed, + toggleIsPause, handleOnSpeedChange, } = useHooks({ currentTime, range, + isSelected, + blockId, onClick, onDrag, onPlay, onPlayReversed, onSpeedChange, + onPause, }); const handlePopOver = useCallback(() => setOpen(!open), [open]); - console.log(speed, blockId); const handleClick = useCallback( (value: string) => { setOpen(false); @@ -52,9 +59,25 @@ const Timeline = ({ blockId }: TimelineProps) => { - - - + + + + { + if (isPlaying || isPlayingReversed || isPause) { + toggleIsPause(); + } + }}> + + + + + @@ -139,6 +162,12 @@ const PlayControl = styled.div` gap: 10px; `; +const PlayButton = styled.div<{ isPlaying?: boolean; isClicked?: boolean }>` + color: ${({ isPlaying, theme }) => (isPlaying ? theme.select.main : "")}; + cursor: ${({ isClicked }) => (isClicked ? "pointer" : "not-allowed")}; + pointer-events: ${({ isClicked }) => (isClicked ? "auto" : "")}; +`; + const InputWrapper = styled.div` position: relative; cursor: pointer; diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts index 03fc4d7672..1f254eb1a7 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts @@ -1,5 +1,6 @@ import { ChangeEventHandler, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { TimelineCommitter } from "@reearth/beta/lib/core/Map/useTimelineManager"; import { Range } from "@reearth/beta/lib/core/StoryPanel/Block/types"; import { formatDateForSliderTimeline, @@ -9,49 +10,79 @@ import { type TimelineProps = { currentTime: number; range?: Range; + isSelected?: boolean; + blockId?: string; onClick?: (t: number) => void; onDrag?: (t: number) => void; - onPlay?: (isPlaying: boolean) => void; - onPlayReversed?: (isPlaying: boolean) => void; - onSpeedChange?: (speed: number) => void; + onPlay?: (isPlaying: boolean, committer: TimelineCommitter) => void; + onPlayReversed?: (isPlaying: boolean, committer: TimelineCommitter) => void; + onSpeedChange?: (speed: number, committer: TimelineCommitter) => void; + onPause: (isPause: boolean, committer: TimelineCommitter) => void; }; export default ({ currentTime, range, - onClick, - onDrag, + isSelected, + blockId, onPlay, onPlayReversed, onSpeedChange, + onPause, }: TimelineProps) => { const [isPlaying, setIsPlaying] = useState(false); const [isPlayingReversed, setIsPlayingReversed] = useState(false); + const [isPause, setIsPause] = useState(false); + const syncCurrentTimeRef = useRef(currentTime); - console.log(onClick, onDrag); + const [committer, setCommiter] = useState({ source: "storyTimelineBlock" }); + + useEffect(() => { + if (isSelected) + setCommiter(prev => { + return { source: prev.source, id: blockId }; + }); + }, [blockId, isSelected]); + const handleOnSpeedChange: ChangeEventHandler = useCallback( e => { - onSpeedChange?.(parseInt(e.currentTarget.value, 10)); + onSpeedChange?.(parseInt(e.currentTarget.value, 10), committer); }, - [onSpeedChange], + [committer, onSpeedChange], ); + const toggleIsPlaying = useCallback(() => { - if (isPlayingReversed) { + if (isPlayingReversed || isPause) { setIsPlayingReversed(false); - onPlayReversed?.(false); + setIsPause(false); + onPlayReversed?.(false, committer); + onPause?.(false, committer); } setIsPlaying(p => !p); - onPlay?.(!isPlaying); - }, [isPlayingReversed, onPlay, isPlaying, onPlayReversed]); + onPlay?.(!isPlaying, committer); + }, [isPlayingReversed, isPause, onPlay, isPlaying, committer, onPlayReversed, onPause]); const toggleIsPlayingReversed = useCallback(() => { - if (isPlaying) { + if (isPlaying || isPause) { setIsPlaying(false); - onPlay?.(false); + setIsPause(false); + onPlay?.(false, committer); + onPause?.(false, committer); } setIsPlayingReversed(p => !p); - onPlayReversed?.(!isPlayingReversed); - }, [isPlaying, isPlayingReversed, onPlay, onPlayReversed]); + onPlayReversed?.(!isPlayingReversed, committer); + }, [committer, isPause, isPlaying, isPlayingReversed, onPause, onPlay, onPlayReversed]); + + const toggleIsPause = useCallback(() => { + if (isPlayingReversed || isPlaying) { + setIsPlayingReversed(false); + setIsPlaying(false); + onPlayReversed?.(false, committer); + onPlay?.(false, committer); + } + setIsPause(p => !p); + onPause?.(!isPause, committer); + }, [isPlayingReversed, isPlaying, onPause, isPause, committer, onPlayReversed, onPlay]); useEffect(() => { syncCurrentTimeRef.current = currentTime; @@ -87,8 +118,12 @@ export default ({ return { formattedCurrentTime, timeRange, + isPlaying, + isPlayingReversed, + isPause, toggleIsPlaying, toggleIsPlayingReversed, + toggleIsPause, handleOnSpeedChange, }; }; 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 4821770665..5ccf32b0d4 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 @@ -8,12 +8,12 @@ const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => - + ); }; diff --git a/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts b/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts index cf8fc90190..2de6a2c4b9 100644 --- a/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts +++ b/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts @@ -65,13 +65,13 @@ export default () => { cmd: "SET_TIME", payload: { start: formatDateToSting(range?.start), - current: formatDateToSting(currentTime), + current: time, stop: formatDateToSting(range.end), }, committer: { source: "storyTimelineBlock", id: committer?.id }, }); }, - [currentTime, range.end, range?.start, visualizerContext], + [range.end, range?.start, visualizerContext], ); const onSpeedChange = useCallback( @@ -102,33 +102,51 @@ export default () => { const stopTime = visualizerContext?.current?.timeline?.current?.timeline?.stop?.getTime(); const [speed, setSpeed] = useState(timeSpeed); + const lastTime = useRef(); + const switchCurrentTimeToStart = useCallback( + (t: number, isRangeChanged: boolean) => { + const cur = isRangeChanged + ? t + : t > range?.end + ? range?.start + : t < range?.start + ? range?.end + : t; + + if (lastTime.current !== cur) { + lastTime.current = cur; + onTimeChange?.(new Date(cur)); + } + return cur; + }, + [range, onTimeChange], + ); + const handleOnPlay = useCallback( - (playing: boolean) => { + (playing: boolean, committer: TimelineCommitter) => { // Stop cesium animation - if (playing) { - onPlay?.(); - console.log("called"); - } else { - onPause?.(); - } + playing ? onPlay?.(committer) : onPause?.(committer); onSpeedChange?.(Math.abs(speed)); }, [onPause, onPlay, onSpeedChange, speed], ); const handleOnPlayReversed = useCallback( - (playing: boolean) => { + (playing: boolean, committer: TimelineCommitter) => { // Stop cesium animation - if (playing) { - onPlay?.(); - } else { - onPause?.(); - } + playing ? onPlay?.(committer) : onPause?.(committer); onSpeedChange?.(Math.abs(speed) * -1); }, [onPause, onPlay, onSpeedChange, speed], ); + const handleOnPause = useCallback( + (pause: boolean, committer: TimelineCommitter) => { + pause && onPause?.(committer); + }, + [onPause], + ); + const handleTimeEvent: TimeHandler = useCallback( currentTime => { const t = new Date(currentTime); @@ -184,26 +202,6 @@ export default () => { setSpeed(Math.abs(timeSpeed)); }, [overriddenStart, overriddenStop, handleRange, startTime, stopTime, timeSpeed]); - const lastTime = useRef(); - const switchCurrentTimeToStart = useCallback( - (t: number, isRangeChanged: boolean) => { - const cur = isRangeChanged - ? t - : t > range.end - ? range.start - : t < range.start - ? range.end - : t; - - if (lastTime.current !== cur) { - lastTime.current = cur; - onTimeChange?.(new Date(cur)); - } - return cur; - }, - [range, onTimeChange], - ); - useEffect(() => { const h: TickEventCallback = (d, c) => { const isDifferentRange = range.start !== c.start.getTime() || range.end !== c.stop.getTime(); @@ -237,7 +235,6 @@ export default () => { if (overriddenCurrentTime) { const t = Math.max(Math.min(range.end, overriddenCurrentTime), range.start); setCurrentTime(t); - // onTimeChangeRef.current?.(new Date(t)); } }, [overriddenCurrentTime, range]); @@ -250,5 +247,6 @@ export default () => { onPlay: handleOnPlay, onPlayReversed: handleOnPlayReversed, onSpeedChange: handleOnSpeedChange, + onPause: handleOnPause, }; }; From fc33dd816680597406e0a466fb1a83a3b06836c3 Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Mon, 23 Oct 2023 09:13:33 +0300 Subject: [PATCH 08/38] implement time settings --- .../Timeline/{Timeline.tsx => Editor.tsx} | 53 +++++++++++-------- .../Block/builtin/Timeline/index.tsx | 17 ++++-- .../core/StoryPanel/hooks/useTimelineBlock.ts | 21 +++++--- web/src/beta/lib/core/StoryPanel/utils.ts | 12 +++++ .../components/atoms/Timeline/index.tsx | 6 +-- 5 files changed, 73 insertions(+), 36 deletions(-) rename web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/{Timeline.tsx => Editor.tsx} (85%) diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx similarity index 85% rename from web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx rename to web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx index 0d1ef97fa2..c1c1d0df2c 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Timeline.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from "react"; import Icon from "@reearth/beta/components/Icon"; import * as Popover from "@reearth/beta/components/Popover"; -import Text from "@reearth/beta/components/Text"; +// import { Timeline } from "@reearth/beta/lib/core/Map/useTimelineManager"; import useHooks from "@reearth/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook"; import useTimelineBlock from "@reearth/beta/lib/core/StoryPanel/hooks/useTimelineBlock"; import { useT } from "@reearth/services/i18n"; @@ -11,14 +11,16 @@ import { styled } from "@reearth/services/theme"; type TimelineProps = { blockId?: string; isSelected?: boolean; + timeValues?: any; }; -const Timeline = ({ blockId, isSelected }: TimelineProps) => { + +const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { const t = useT(); const [open, setOpen] = useState(false); - const playSpeedOptions = ["1 min/sec", "0.1 hr/sec", "0.5 hr/sec", "1 hr/sec"]; - const [selected, setSelected] = useState("1 min/sec"); + const playSpeedOptions = [1, 0.1, 0.5, 1]; + const [selected, setSelected] = useState(1); const { currentTime, range, onClick, onDrag, onPlay, onPlayReversed, onSpeedChange, onPause } = - useTimelineBlock(); + useTimelineBlock(timeValues); const { formattedCurrentTime, timeRange, @@ -28,7 +30,6 @@ const Timeline = ({ blockId, isSelected }: TimelineProps) => { toggleIsPlaying, toggleIsPlayingReversed, toggleIsPause, - handleOnSpeedChange, } = useHooks({ currentTime, range, @@ -45,7 +46,7 @@ const Timeline = ({ blockId, isSelected }: TimelineProps) => { const handlePopOver = useCallback(() => setOpen(!open), [open]); const handleClick = useCallback( - (value: string) => { + (value: number) => { setOpen(false); if (value !== selected) setSelected(value); }, @@ -54,6 +55,7 @@ const Timeline = ({ blockId, isSelected }: TimelineProps) => { return ( + {/* {inEditor && } */} @@ -82,22 +84,21 @@ const Timeline = ({ blockId, isSelected }: TimelineProps) => { - + {playSpeedOptions?.map((playSpeed, key) => ( - { - handleOnSpeedChange; setSelected(playSpeed); handleClick(playSpeed); }}> - {t(`${playSpeed}`)} - + {key === 0 ? `${playSpeed} min/sec` : `${playSpeed} hr/sec`} + ))} @@ -134,7 +135,7 @@ const Timeline = ({ blockId, isSelected }: TimelineProps) => { ); }; -export default Timeline; +export default TimelineEditor; const Wrapper = styled.div` color: ${({ theme }) => theme.content.weaker}; @@ -147,7 +148,7 @@ const TimelineControl = styled.div` display: flex; align-items: center; padding-bottom: 6px; - gap: 28px; + gap: 20px; `; const StyledIcon = styled.div` @@ -182,9 +183,9 @@ const ArrowIcon = styled(Icon)<{ open: boolean }>` `; const Select = styled.div` - font-size: 15px; + font-size: 14px; line-height: 1; - padding-right: 28px; + padding-right: 24px; width: 100%; color: ${({ theme }) => theme.content.weaker}; `; @@ -197,17 +198,21 @@ const PickerWrapper = styled(Popover.Content)` background: ${({ theme }) => theme.bg[3]}; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); display: flex; - gap: 4px; flex-direction: column; justify-content: space-between; - z-idx: 1; + z-index: 2; `; -const SpeedOption = styled(Text)` - padding: 4px 12px; +const InputOptions = styled.option` + background: ${({ theme }) => theme.bg[1]}; + border: none; + cursor: pointer; + padding: 8px 12px; + font-size: 12px; &:hover { background: ${({ theme }) => theme.bg[2]}; } + color: ${({ theme }) => theme.content.main}; `; const CurrentTime = styled.div` @@ -252,3 +257,9 @@ const ScaleLabel = styled.div` bottom: 28px; right: 15px; `; + +// const Overlay = styled.div` +// position: absolute; +// width: 100%; +// height: 100%; +// `; 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 5ccf32b0d4..1084eba7cb 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,19 +1,28 @@ +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 Timeline from "./Timeline"; +import TimelineEditor from "./Editor"; const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => { + const timeValues = useMemo(() => { + return { + current: block?.property?.default?.currentTime.value, + start: block?.property?.default?.startTime?.value, + stop: block?.property?.default?.startTime?.value, + }; + }, [block?.property?.default]); + return ( - + ); }; diff --git a/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts b/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts index 2de6a2c4b9..08e4fac7d5 100644 --- a/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts +++ b/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useVisualizer } from "@reearth/beta/lib/core/Visualizer"; import { TickEventCallback, TimelineCommitter } from "../../Map/useTimelineManager"; -import { formatDateToSting } from "../utils"; +import { formatDateToSting, formatDateToUnix } from "../utils"; type TimeHandler = (t: number) => void; const getNewDate = (d?: Date) => d ?? new Date(); @@ -26,12 +26,11 @@ const timeRange = (startTime?: number, stopTime?: number) => { }; }; -export default () => { +export default (timeValues: any) => { const visualizerContext = useVisualizer(); const [currentTime, setCurrentTime] = useState(() => getNewDate(visualizerContext?.current?.timeline?.current?.timeline?.current)?.getTime(), ); - const [range, setRange] = useState(() => timeRange( visualizerContext?.current?.timeline?.current?.timeline?.start?.getTime(), @@ -97,10 +96,10 @@ export default () => { [visualizerContext], ); - const timeSpeed = visualizerContext?.current?.timeline?.current?.options.multiplier || 1; + const clockSpeed = visualizerContext?.current?.timeline?.current?.options.multiplier || 1; const startTime = visualizerContext?.current?.timeline?.current?.timeline?.start?.getTime(); const stopTime = visualizerContext?.current?.timeline?.current?.timeline?.stop?.getTime(); - const [speed, setSpeed] = useState(timeSpeed); + const [speed, setSpeed] = useState(clockSpeed); const lastTime = useRef(); const switchCurrentTimeToStart = useCallback( @@ -117,9 +116,13 @@ export default () => { lastTime.current = cur; onTimeChange?.(new Date(cur)); } + if (timeValues.current) + setRange(() => + timeRange(formatDateToUnix(timeValues?.start), formatDateToUnix(timeValues.stop)), + ); return cur; }, - [range, onTimeChange], + [range?.end, range?.start, timeValues, onTimeChange], ); const handleOnPlay = useCallback( @@ -199,8 +202,8 @@ export default () => { // Sync cesium clock. useEffect(() => { handleRange(overriddenStart ?? startTime, overriddenStop ?? stopTime); - setSpeed(Math.abs(timeSpeed)); - }, [overriddenStart, overriddenStop, handleRange, startTime, stopTime, timeSpeed]); + setSpeed(Math.abs(clockSpeed)); + }, [overriddenStart, overriddenStop, handleRange, startTime, stopTime, clockSpeed]); useEffect(() => { const h: TickEventCallback = (d, c) => { @@ -221,6 +224,8 @@ export default () => { range.end, onTick, removeTickEventListener, + timeValues?.start, + timeValues.stop, ]); const onTimeChangeRef = useRef(); diff --git a/web/src/beta/lib/core/StoryPanel/utils.ts b/web/src/beta/lib/core/StoryPanel/utils.ts index 838b69ff4a..9ba49dcc9a 100644 --- a/web/src/beta/lib/core/StoryPanel/utils.ts +++ b/web/src/beta/lib/core/StoryPanel/utils.ts @@ -91,3 +91,15 @@ export const formatDateToSting = (d: number) => { const date = new Date(d); return date.toISOString(); }; + +export const formatDateToUnix = (t: string) => { + const today = new Date(); + const year = today.getFullYear(); + const month = today.getMonth() + 1; + const day = today.getDate(); + + const [hours, minutes, seconds] = t.split(":").map(Number); + const date = new Date(year, month - 1, day, hours, minutes, seconds); + const timestamp = date.getTime(); + return timestamp; +}; diff --git a/web/src/classic/components/atoms/Timeline/index.tsx b/web/src/classic/components/atoms/Timeline/index.tsx index f7c33298bc..b6e0ffce55 100644 --- a/web/src/classic/components/atoms/Timeline/index.tsx +++ b/web/src/classic/components/atoms/Timeline/index.tsx @@ -111,9 +111,9 @@ const Timeline: React.FC = memo(function TimelinePresenter({ From b694aeec02db7f25ee7417a5931b801ae5263530 Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Tue, 24 Oct 2023 00:13:41 +0300 Subject: [PATCH 09/38] refactor date fields settings --- .../components/fields/DateTimeField/index.tsx | 1 + .../fields/common/TextInput/index.tsx | 2 +- .../Block/builtin/Timeline/Editor.tsx | 13 +-- .../Block/builtin/Timeline/index.tsx | 12 +- .../StoryPanel/hooks/useFieldComponent.tsx | 10 +- .../core/StoryPanel/hooks/useTimelineBlock.ts | 107 +++++++----------- web/src/beta/lib/core/StoryPanel/utils.ts | 12 -- 7 files changed, 65 insertions(+), 92 deletions(-) diff --git a/web/src/beta/components/fields/DateTimeField/index.tsx b/web/src/beta/components/fields/DateTimeField/index.tsx index 1b36f9be49..01d8977316 100644 --- a/web/src/beta/components/fields/DateTimeField/index.tsx +++ b/web/src/beta/components/fields/DateTimeField/index.tsx @@ -52,4 +52,5 @@ const Wrapper = styled.div` display: flex; align-items: stretch; gap: 4px; + padding-bottom: 10px; `; diff --git a/web/src/beta/components/fields/common/TextInput/index.tsx b/web/src/beta/components/fields/common/TextInput/index.tsx index 2f0168eaa4..553a243693 100644 --- a/web/src/beta/components/fields/common/TextInput/index.tsx +++ b/web/src/beta/components/fields/common/TextInput/index.tsx @@ -89,7 +89,7 @@ const StyledInput = styled.input` border-radius: 4px; padding: 4px 8px; transition: all 0.3s; - + flex: 1; :focus { border-color: ${({ theme }) => theme.outline.main}; } diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx index c1c1d0df2c..0d178f5c7c 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from "react"; import Icon from "@reearth/beta/components/Icon"; import * as Popover from "@reearth/beta/components/Popover"; -// import { Timeline } from "@reearth/beta/lib/core/Map/useTimelineManager"; +import { Timeline } from "@reearth/beta/lib/core/Map/useTimelineManager"; import useHooks from "@reearth/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook"; import useTimelineBlock from "@reearth/beta/lib/core/StoryPanel/hooks/useTimelineBlock"; import { useT } from "@reearth/services/i18n"; @@ -11,7 +11,7 @@ import { styled } from "@reearth/services/theme"; type TimelineProps = { blockId?: string; isSelected?: boolean; - timeValues?: any; + timeValues?: Timeline; }; const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { @@ -19,8 +19,10 @@ const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { const [open, setOpen] = useState(false); const playSpeedOptions = [1, 0.1, 0.5, 1]; const [selected, setSelected] = useState(1); + const { currentTime, range, onClick, onDrag, onPlay, onPlayReversed, onSpeedChange, onPause } = useTimelineBlock(timeValues); + const { formattedCurrentTime, timeRange, @@ -55,7 +57,6 @@ const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { return ( - {/* {inEditor && } */} @@ -257,9 +258,3 @@ const ScaleLabel = styled.div` bottom: 28px; right: 15px; `; - -// const Overlay = styled.div` -// position: absolute; -// width: 100%; -// height: 100%; -// `; 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 1084eba7cb..276808ea4a 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,16 +1,17 @@ import { useMemo } from "react"; import type { CommonProps as BlockProps } from "@reearth//beta/lib/core/StoryPanel/Block/types"; +import { Timeline } from "@reearth/beta/lib/core/Map/useTimelineManager"; import BlockWrapper from "@reearth/beta/lib/core/StoryPanel/Block/builtin/common/Wrapper"; import TimelineEditor from "./Editor"; const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => { - const timeValues = useMemo(() => { + const timeValues: Timeline = useMemo(() => { return { current: block?.property?.default?.currentTime.value, start: block?.property?.default?.startTime?.value, - stop: block?.property?.default?.startTime?.value, + stop: block?.property?.default?.endTime?.value, }; }, [block?.property?.default]); @@ -22,7 +23,12 @@ const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => propertyId={block?.propertyId} property={block?.property} {...props}> - + ); }; diff --git a/web/src/beta/lib/core/StoryPanel/hooks/useFieldComponent.tsx b/web/src/beta/lib/core/StoryPanel/hooks/useFieldComponent.tsx index ccc1513515..3380588a39 100644 --- a/web/src/beta/lib/core/StoryPanel/hooks/useFieldComponent.tsx +++ b/web/src/beta/lib/core/StoryPanel/hooks/useFieldComponent.tsx @@ -1,5 +1,6 @@ import CameraField from "@reearth/beta/components/fields/CameraField"; import ColorField from "@reearth/beta/components/fields/ColorField"; +import DateTimeField from "@reearth/beta/components/fields/DateTimeField"; import LocationField from "@reearth/beta/components/fields/LocationField"; import NumberField from "@reearth/beta/components/fields/NumberField"; import SelectField from "@reearth/beta/components/fields/SelectField"; @@ -91,7 +92,14 @@ export const FieldComponent = ({ onChange={handlePropertyValueUpdate(groupId, propertyId, fieldId, field?.type)} /> ) : field?.type === "string" ? ( - field?.ui === "color" ? ( + field?.ui === "datetime" ? ( + + ) : field?.ui === "color" ? ( void; + const getNewDate = (d?: Date) => d ?? new Date(); + const calculateEndTime = (date: Date) => { date.setHours(23, 59, 59, 999); return date.getTime(); @@ -26,17 +28,27 @@ const timeRange = (startTime?: number, stopTime?: number) => { }; }; -export default (timeValues: any) => { +export default (timeValues?: Timeline) => { const visualizerContext = useVisualizer(); - const [currentTime, setCurrentTime] = useState(() => - getNewDate(visualizerContext?.current?.timeline?.current?.timeline?.current)?.getTime(), - ); - const [range, setRange] = useState(() => - timeRange( - visualizerContext?.current?.timeline?.current?.timeline?.start?.getTime(), - visualizerContext?.current?.timeline?.current?.timeline?.stop?.getTime(), - ), - ); + + const initialCurrentTime = timeValues?.current + ? getNewDate(new Date(timeValues?.current)).getTime() + : getNewDate(visualizerContext?.current?.timeline?.current?.timeline?.current).getTime(); + + const initialRange = timeValues?.start + ? timeRange( + getNewDate(new Date(timeValues?.start)).getTime(), + getNewDate(new Date(timeValues?.stop)).getTime(), + ) + : timeRange( + visualizerContext?.current?.timeline?.current?.timeline?.start?.getTime(), + visualizerContext?.current?.timeline?.current?.timeline?.stop?.getTime(), + ); + + const [currentTime, setCurrentTime] = useState(initialCurrentTime); + const [range, setRange] = useState(initialRange); + const clockSpeed = visualizerContext?.current?.timeline?.current?.options.multiplier || 1; + const [speed, setSpeed] = useState(clockSpeed); const onPause = useCallback( (committer?: TimelineCommitter) => { @@ -96,12 +108,6 @@ export default (timeValues: any) => { [visualizerContext], ); - const clockSpeed = visualizerContext?.current?.timeline?.current?.options.multiplier || 1; - const startTime = visualizerContext?.current?.timeline?.current?.timeline?.start?.getTime(); - const stopTime = visualizerContext?.current?.timeline?.current?.timeline?.stop?.getTime(); - const [speed, setSpeed] = useState(clockSpeed); - - const lastTime = useRef(); const switchCurrentTimeToStart = useCallback( (t: number, isRangeChanged: boolean) => { const cur = isRangeChanged @@ -112,17 +118,9 @@ export default (timeValues: any) => { ? range?.end : t; - if (lastTime.current !== cur) { - lastTime.current = cur; - onTimeChange?.(new Date(cur)); - } - if (timeValues.current) - setRange(() => - timeRange(formatDateToUnix(timeValues?.start), formatDateToUnix(timeValues.stop)), - ); return cur; }, - [range?.end, range?.start, timeValues, onTimeChange], + [range?.end, range?.start], ); const handleOnPlay = useCallback( @@ -173,17 +171,6 @@ export default (timeValues: any) => { [onSpeedChange, visualizerContext], ); - const isClockInitialized = useRef(false); - - useEffect(() => { - if (!isClockInitialized.current) { - isClockInitialized.current = true; - queueMicrotask(() => { - onSpeedChange?.(1); - }); - } - }, [onSpeedChange]); - const handleRange = useCallback((start: number | undefined, stop: number | undefined) => { setRange(prev => { const next = timeRange(start, stop); @@ -194,23 +181,26 @@ export default (timeValues: any) => { }); }, []); - const overriddenStart = - visualizerContext?.current?.timeline?.current?.computedTimeline?.start?.getTime(); - const overriddenStop = - visualizerContext?.current?.timeline?.current?.computedTimeline?.stop.getTime(); - - // Sync cesium clock. + // update clock. useEffect(() => { - handleRange(overriddenStart ?? startTime, overriddenStop ?? stopTime); + if (timeValues?.current || timeValues?.start || timeValues?.stop) { + const startTime = getNewDate(new Date(timeValues?.start)).getTime(); + const endTime = getNewDate(new Date(timeValues?.stop)).getTime(); + setCurrentTime(prev => { + const next = getNewDate(new Date(timeValues?.current)).getTime(); + if (prev !== next) { + onTimeChange?.(new Date(next)); + } + return prev; + }); + return handleRange(startTime, endTime); + } setSpeed(Math.abs(clockSpeed)); - }, [overriddenStart, overriddenStop, handleRange, startTime, stopTime, clockSpeed]); + }, [handleRange, clockSpeed, timeValues?.start, timeValues?.stop, timeValues, onTimeChange]); useEffect(() => { const h: TickEventCallback = (d, c) => { const isDifferentRange = range.start !== c.start.getTime() || range.end !== c.stop.getTime(); - if (isDifferentRange) { - handleRange(c.start.getTime(), c.stop.getTime()); - } setCurrentTime(switchCurrentTimeToStart(d.getTime(), isDifferentRange)); }; onTick?.(h); @@ -225,24 +215,9 @@ export default (timeValues: any) => { onTick, removeTickEventListener, timeValues?.start, - timeValues.stop, + timeValues?.stop, ]); - const onTimeChangeRef = useRef(); - - useEffect(() => { - onTimeChangeRef.current = onTimeChange; - }, [onTimeChange]); - - const overriddenCurrentTime = - visualizerContext?.current?.timeline?.current?.computedTimeline?.current?.getTime(); - useEffect(() => { - if (overriddenCurrentTime) { - const t = Math.max(Math.min(range.end, overriddenCurrentTime), range.start); - setCurrentTime(t); - } - }, [overriddenCurrentTime, range]); - return { currentTime, range, diff --git a/web/src/beta/lib/core/StoryPanel/utils.ts b/web/src/beta/lib/core/StoryPanel/utils.ts index 9ba49dcc9a..838b69ff4a 100644 --- a/web/src/beta/lib/core/StoryPanel/utils.ts +++ b/web/src/beta/lib/core/StoryPanel/utils.ts @@ -91,15 +91,3 @@ export const formatDateToSting = (d: number) => { const date = new Date(d); return date.toISOString(); }; - -export const formatDateToUnix = (t: string) => { - const today = new Date(); - const year = today.getFullYear(); - const month = today.getMonth() + 1; - const day = today.getDate(); - - const [hours, minutes, seconds] = t.split(":").map(Number); - const date = new Date(year, month - 1, day, hours, minutes, seconds); - const timestamp = date.getTime(); - return timestamp; -}; From cc9b46d3f21262907dab0aa3fd3436884db85484 Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Tue, 24 Oct 2023 00:34:53 +0300 Subject: [PATCH 10/38] resolve type --- .../components/Icon/Icons/{play.svg => pause.svg} | 0 .../components/Icon/Icons/timelineStoryBlock.svg | 2 +- web/src/beta/components/Icon/icons.ts | 4 ++-- .../StoryPanel/Block/builtin/Timeline/Editor.tsx | 14 +++++++------- .../core/StoryPanel/Block/builtin/Timeline/hook.ts | 1 + .../StoryPanel/Block/builtin/Timeline/index.tsx | 7 +------ 6 files changed, 12 insertions(+), 16 deletions(-) rename web/src/beta/components/Icon/Icons/{play.svg => pause.svg} (100%) diff --git a/web/src/beta/components/Icon/Icons/play.svg b/web/src/beta/components/Icon/Icons/pause.svg similarity index 100% rename from web/src/beta/components/Icon/Icons/play.svg rename to web/src/beta/components/Icon/Icons/pause.svg diff --git a/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg b/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg index c9052b6fcc..e3d0fc806f 100644 --- a/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg +++ b/web/src/beta/components/Icon/Icons/timelineStoryBlock.svg @@ -1,3 +1,3 @@ - + diff --git a/web/src/beta/components/Icon/icons.ts b/web/src/beta/components/Icon/icons.ts index 9bd28fb6c1..efe15ea338 100644 --- a/web/src/beta/components/Icon/icons.ts +++ b/web/src/beta/components/Icon/icons.ts @@ -57,7 +57,7 @@ import PlayLeft from "./Icons/play-left.svg"; import Ellipse from "./Icons/ellipse.svg"; import TimelinePlayRight from "./Icons/timeline-play-right.svg"; import TimelinePlayLeft from "./Icons/timeline-play-left.svg"; -import Play from "./Icons/play.svg"; +import Pause from "./Icons/pause.svg"; import Slider from "./Icons/slider.svg"; // Dashboard @@ -146,7 +146,7 @@ export default { playLeft: PlayLeft, timelinePlayLeft: TimelinePlayLeft, timelinePlayRight: TimelinePlayRight, - play: Play, + pause: Pause, slider: Slider, storyPage: StoryPage, square: Square, diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx index 0d178f5c7c..8348c4afac 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx @@ -76,7 +76,7 @@ const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { toggleIsPause(); } }}> - + @@ -111,18 +111,18 @@ const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { {idx === 0 ? ( <> - {timeRange?.startTime.date} - {timeRange?.startTime.time} + {timeRange?.startTime?.date} + {timeRange?.startTime?.time} ) : idx === 5 ? ( <> - {timeRange?.midTime.date} - {timeRange?.midTime.time} + {timeRange?.midTime?.date} + {timeRange?.midTime?.time} ) : idx === 10 ? ( <> - {timeRange?.endTime.date} - {timeRange?.endTime.time} + {timeRange?.endTime?.date} + {timeRange?.endTime?.time} ) : null} diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts index 1f254eb1a7..db9b7cb3b3 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts @@ -113,6 +113,7 @@ export default ({ endTime: formatRangeDateAndTime(formatDateForSliderTimeline(range.end)), }; } + return {}; }, [range]); return { 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 276808ea4a..a4ec04f0c9 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 @@ -23,12 +23,7 @@ const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => propertyId={block?.propertyId} property={block?.property} {...props}> - + ); }; From f3533f280b68d986c14a8d9dbea7b5ad6e471f3b Mon Sep 17 00:00:00 2001 From: Beatrice Mkumbo Date: Tue, 24 Oct 2023 00:40:16 +0300 Subject: [PATCH 11/38] Update index.tsx --- web/src/classic/components/atoms/Timeline/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/classic/components/atoms/Timeline/index.tsx b/web/src/classic/components/atoms/Timeline/index.tsx index b6e0ffce55..f7c33298bc 100644 --- a/web/src/classic/components/atoms/Timeline/index.tsx +++ b/web/src/classic/components/atoms/Timeline/index.tsx @@ -111,9 +111,9 @@ const Timeline: React.FC = memo(function TimelinePresenter({ From a0a245ed85e0edf7b8d42f41a549b9364f4b946c Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Wed, 25 Oct 2023 08:52:17 +0300 Subject: [PATCH 12/38] fix bug in speed --- .../Block/builtin/Timeline/Editor.tsx | 61 +++++++++++++------ .../StoryPanel/Block/builtin/Timeline/hook.ts | 13 ++++ .../Block/builtin/Timeline/index.tsx | 7 ++- .../core/StoryPanel/hooks/useTimelineBlock.ts | 45 +++++++------- web/src/beta/lib/core/StoryPanel/utils.ts | 34 +++++++++++ 5 files changed, 117 insertions(+), 43 deletions(-) diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx index 8348c4afac..46975f9b0f 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/Editor.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from "react"; +import { useCallback, useContext, useState } from "react"; import Icon from "@reearth/beta/components/Icon"; import * as Popover from "@reearth/beta/components/Popover"; @@ -8,20 +8,32 @@ import useTimelineBlock from "@reearth/beta/lib/core/StoryPanel/hooks/useTimelin import { useT } from "@reearth/services/i18n"; import { styled } from "@reearth/services/theme"; +import { BlockContext } from "../common/Wrapper"; + type TimelineProps = { blockId?: string; isSelected?: boolean; timeValues?: Timeline; + inEditor?: boolean; }; -const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { +const TimelineEditor = ({ blockId, isSelected, timeValues, inEditor }: TimelineProps) => { const t = useT(); const [open, setOpen] = useState(false); - const playSpeedOptions = [1, 0.1, 0.5, 1]; - const [selected, setSelected] = useState(1); - const { currentTime, range, onClick, onDrag, onPlay, onPlayReversed, onSpeedChange, onPause } = - useTimelineBlock(timeValues); + const [selected, setSelected] = useState("1min/sec"); + + const { + currentTime, + range, + playSpeedOptions, + onClick, + onDrag, + onPlay, + onPlayReversed, + onSpeedChange, + onPause, + } = useTimelineBlock(timeValues); const { formattedCurrentTime, @@ -29,6 +41,7 @@ const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { isPlaying, isPlayingReversed, isPause, + currentPosition, toggleIsPlaying, toggleIsPlayingReversed, toggleIsPause, @@ -45,18 +58,23 @@ const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { onPause, }); + const context = useContext(BlockContext); const handlePopOver = useCallback(() => setOpen(!open), [open]); + console.log(currentPosition); + const handleClick = useCallback( - (value: number) => { + (value: string, second: number) => { setOpen(false); if (value !== selected) setSelected(value); + onSpeedChange(second); }, - [selected], + [onSpeedChange, selected], ); return ( + {!context?.editMode && inEditor && } @@ -85,7 +103,7 @@ const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { - + @@ -93,12 +111,11 @@ const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { {playSpeedOptions?.map((playSpeed, key) => ( { - setSelected(playSpeed); - handleClick(playSpeed); + handleClick(playSpeed.timeString, playSpeed.seconds); }}> - {key === 0 ? `${playSpeed} min/sec` : `${playSpeed} hr/sec`} + {playSpeed.timeString} ))} @@ -128,7 +145,10 @@ const TimelineEditor = ({ blockId, isSelected, timeValues }: TimelineProps) => { ))} - + @@ -149,7 +169,7 @@ const TimelineControl = styled.div` display: flex; align-items: center; padding-bottom: 6px; - gap: 20px; + gap: 22px; `; const StyledIcon = styled.div` @@ -173,6 +193,7 @@ const PlayButton = styled.div<{ isPlaying?: boolean; isClicked?: boolean }>` const InputWrapper = styled.div` position: relative; cursor: pointer; + width: 90px; `; const ArrowIcon = styled(Icon)<{ open: boolean }>` @@ -186,8 +207,7 @@ const ArrowIcon = styled(Icon)<{ open: boolean }>` const Select = styled.div` font-size: 14px; line-height: 1; - padding-right: 24px; - width: 100%; + padding-right: 12px; color: ${({ theme }) => theme.content.weaker}; `; @@ -241,7 +261,6 @@ const ScaleList = styled.div` const IconWrapper = styled.div` position: absolute; top: 4px; - left: 16px; `; const Scale = styled.div` @@ -258,3 +277,9 @@ const ScaleLabel = styled.div` bottom: 28px; right: 15px; `; + +const Overlay = styled.div` + position: absolute; + width: 100%; + height: 100%; +`; diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts index db9b7cb3b3..02f88d4327 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Timeline/hook.ts @@ -116,12 +116,25 @@ export default ({ return {}; }, [range]); + const currentPosition = useMemo(() => { + if (range) { + const startTime = range.start; + const endTime = range.end; + const rangeDuration = endTime - startTime; + const currentDuration = currentTime - startTime; + const position = currentDuration / rangeDuration + 16; // Change to pixel value if needed + return position; + } + return 16; // Return a default value if range is not available + }, [range, currentTime]); + return { formattedCurrentTime, timeRange, isPlaying, isPlayingReversed, isPause, + currentPosition, toggleIsPlaying, toggleIsPlayingReversed, toggleIsPause, 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 a4ec04f0c9..995abc2e30 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 @@ -23,7 +23,12 @@ const TimelineBlock: React.FC = ({ block, isSelected, ...props }) => propertyId={block?.propertyId} property={block?.property} {...props}> - + ); }; diff --git a/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts b/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts index c722a22776..c5521a4ee2 100644 --- a/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts +++ b/web/src/beta/lib/core/StoryPanel/hooks/useTimelineBlock.ts @@ -1,9 +1,9 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useVisualizer } from "@reearth/beta/lib/core/Visualizer"; import { TickEventCallback, Timeline, TimelineCommitter } from "../../Map/useTimelineManager"; -import { formatDateToSting } from "../utils"; +import { convertOptionToSeconds, formatDateToSting } from "../utils"; type TimeHandler = (t: number) => void; @@ -47,8 +47,12 @@ export default (timeValues?: Timeline) => { const [currentTime, setCurrentTime] = useState(initialCurrentTime); const [range, setRange] = useState(initialRange); - const clockSpeed = visualizerContext?.current?.timeline?.current?.options.multiplier || 1; - const [speed, setSpeed] = useState(clockSpeed); + const playSpeedOptions = useMemo(() => { + const speedOpt = ["1min/sec", "0.1hr/sec", "0.5hr/sec", "1hr/sec"]; + return convertOptionToSeconds(speedOpt); + }, []); + + const [speed, setSpeed] = useState(playSpeedOptions[0].seconds); const onPause = useCallback( (committer?: TimelineCommitter) => { @@ -72,6 +76,7 @@ export default (timeValues?: Timeline) => { const onTimeChange = useCallback( (time: Date, committer?: TimelineCommitter) => { + console.log(time); return visualizerContext.current?.timeline?.current?.commit({ cmd: "SET_TIME", payload: { @@ -123,9 +128,17 @@ export default (timeValues?: Timeline) => { [range?.end, range?.start], ); + const handleOnSpeedChange = useCallback( + (speed: number) => { + const absSpeed = Math.abs(speed); + onSpeedChange?.(absSpeed); + setSpeed(speed); + }, + [onSpeedChange], + ); + const handleOnPlay = useCallback( (playing: boolean, committer: TimelineCommitter) => { - // Stop cesium animation playing ? onPlay?.(committer) : onPause?.(committer); onSpeedChange?.(Math.abs(speed)); }, @@ -134,7 +147,6 @@ export default (timeValues?: Timeline) => { const handleOnPlayReversed = useCallback( (playing: boolean, committer: TimelineCommitter) => { - // Stop cesium animation playing ? onPlay?.(committer) : onPause?.(committer); onSpeedChange?.(Math.abs(speed) * -1); }, @@ -157,20 +169,6 @@ export default (timeValues?: Timeline) => { [onTimeChange], ); - const handleOnSpeedChange = useCallback( - (speed: number) => { - setSpeed(speed); - - const absSpeed = Math.abs(speed); - onSpeedChange?.( - (visualizerContext?.current?.timeline?.current?.options.multiplier ?? 1) > 0 - ? absSpeed - : absSpeed * -1, - ); - }, - [onSpeedChange, visualizerContext], - ); - const handleRange = useCallback((start: number | undefined, stop: number | undefined) => { setRange(prev => { const next = timeRange(start, stop); @@ -181,7 +179,7 @@ export default (timeValues?: Timeline) => { }); }, []); - // update clock. + // update block time setting. useEffect(() => { if (timeValues?.current || timeValues?.start || timeValues?.stop) { const startTime = getNewDate(new Date(timeValues?.start)).getTime(); @@ -195,8 +193,7 @@ export default (timeValues?: Timeline) => { }); return handleRange(startTime, endTime); } - setSpeed(Math.abs(clockSpeed)); - }, [handleRange, clockSpeed, timeValues?.start, timeValues?.stop, timeValues, onTimeChange]); + }, [handleRange, timeValues?.start, timeValues?.stop, timeValues, onTimeChange]); useEffect(() => { const h: TickEventCallback = (d, c) => { @@ -221,7 +218,7 @@ export default (timeValues?: Timeline) => { return { currentTime, range, - speed, + playSpeedOptions, onClick: handleTimeEvent, onDrag: handleTimeEvent, onPlay: handleOnPlay, diff --git a/web/src/beta/lib/core/StoryPanel/utils.ts b/web/src/beta/lib/core/StoryPanel/utils.ts index 838b69ff4a..39040d3216 100644 --- a/web/src/beta/lib/core/StoryPanel/utils.ts +++ b/web/src/beta/lib/core/StoryPanel/utils.ts @@ -91,3 +91,37 @@ export const formatDateToSting = (d: number) => { const date = new Date(d); return date.toISOString(); }; + +const timeStringToSeconds = (timeString: string) => { + const parts = timeString.split("/"); + const valueUnit = parts[0].trim(); + const value = parseFloat(valueUnit); + const unit = valueUnit.substr(value.toString().length).trim().toLowerCase(); + + switch (unit) { + case "sec": + case "secs": + return value; + case "min": + case "mins": + return value * 60; + case "hr": + case "hrs": + return value * 3600; + default: + return NaN; + } +}; + +export const convertOptionToSeconds = (data: string[]) => { + const objectsArray = []; + + for (const timeString of data) { + const seconds = timeStringToSeconds(timeString); + if (!isNaN(seconds)) { + objectsArray.push({ timeString, seconds }); + } + } + + return objectsArray; +}; From 5638db91fc6c7c1751c90c52cdf2ac8bbb491e95 Mon Sep 17 00:00:00 2001 From: mkumbobeaty Date: Wed, 25 Oct 2023 13:46:26 +0300 Subject: [PATCH 13/38] improve datetime UI --- web/package.json | 1 + web/src/beta/components/Icon/Icons/Clock.svg | 3 + web/src/beta/components/Icon/icons.ts | 2 + .../fields/DateTimeField/EditPanel/index.tsx | 136 ++++++++++++++++++ .../components/fields/DateTimeField/index.tsx | 93 ++++++++---- .../components/fields/SelectField/index.tsx | 4 +- .../fields/common/TextInput/index.tsx | 1 - web/yarn.lock | 12 ++ 8 files changed, 226 insertions(+), 26 deletions(-) create mode 100644 web/src/beta/components/Icon/Icons/Clock.svg create mode 100644 web/src/beta/components/fields/DateTimeField/EditPanel/index.tsx diff --git a/web/package.json b/web/package.json index ddcdd51308..2dd69d2b6c 100644 --- a/web/package.json +++ b/web/package.json @@ -152,6 +152,7 @@ "lodash-es": "4.17.21", "lru-cache": "8.0.4", "mini-svg-data-uri": "1.4.4", + "moment-timezone": "^0.5.43", "parse-domain": "7.0.1", "quickjs-emscripten": "0.23.0", "quickjs-emscripten-sync": "1.5.2", diff --git a/web/src/beta/components/Icon/Icons/Clock.svg b/web/src/beta/components/Icon/Icons/Clock.svg new file mode 100644 index 0000000000..998df51713 --- /dev/null +++ b/web/src/beta/components/Icon/Icons/Clock.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/icons.ts b/web/src/beta/components/Icon/icons.ts index b7cb401fc0..0e0c7e61dd 100644 --- a/web/src/beta/components/Icon/icons.ts +++ b/web/src/beta/components/Icon/icons.ts @@ -43,6 +43,7 @@ import ZoomToLayer from "./Icons/zoomToLayer.svg"; import LayerStyleIcon from "./Icons/layerStyle.svg"; import AddLayerStyleButtonIcon from "./Icons/addLayerStyleButton.svg"; import LayerInspector from "./Icons/layerInspector.svg"; +import Clock from "./Icons/Clock.svg"; // MSIC import CheckCircle from "./Icons/checkCircle.svg"; @@ -123,6 +124,7 @@ export default { text: InfoText, html: InfoHTML, video: InfoVideo, + clock: Clock, location: InfoLocation, photooverlay: PrimPhotoOverlay, arrowUpDown: ArrowUpDown, diff --git a/web/src/beta/components/fields/DateTimeField/EditPanel/index.tsx b/web/src/beta/components/fields/DateTimeField/EditPanel/index.tsx new file mode 100644 index 0000000000..70ec2463de --- /dev/null +++ b/web/src/beta/components/fields/DateTimeField/EditPanel/index.tsx @@ -0,0 +1,136 @@ +import moment from "moment-timezone"; +import { useCallback, useState } from "react"; + +import Button from "@reearth/beta/components/Button"; +import PanelCommon from "@reearth/beta/components/fields/CameraField/PanelCommon"; +import { useT } from "@reearth/services/i18n"; +import { styled } from "@reearth/services/theme"; + +import TextInput from "../../common/TextInput"; +import SelectField from "../../SelectField"; + +type Props = { + onChange?: (value?: string | undefined) => void; + onClose: () => void; +}; + +const EditPanel: React.FC = ({ onChange, onClose }) => { + const t = useT(); + const [time, setTime] = useState(""); + const [date, setDate] = useState(""); + const [timezones] = useState(moment.tz.names()); + const [selectedTimezone, setSelectedTimezone] = useState("0: 00"); + + const handleTimeChange = useCallback( + (newValue: string | undefined) => { + if (newValue === undefined) return; + + setTime(newValue); + onChange?.(date + " " + newValue); + }, + [date, onChange], + ); + + const handleDateChange = useCallback( + (newValue: string | undefined) => { + if (newValue === undefined) return; + + setDate(newValue); + onChange?.(newValue + " " + time); + }, + [time, onChange], + ); + + const offsetFromUTC = useCallback((timezone: string) => { + const offset = moment.tz(timezone).utcOffset() / 60; + const offsetString = offset >= 0 ? `+${offset}` : `${offset}`; + const tzName = moment.tz(timezone).zoneAbbr(); + + return `${offsetString}:00 - ${tzName}`; + }, []); + + return ( + + + + + + + + + + + + + + ({ + key: timezone, + label: `${offsetFromUTC(timezone)}`, + }))} + onChange={setSelectedTimezone} + /> + + + + + + {}} /> + + + ); +}; + +const TextWrapper = styled.div` + margin-left: 8px; + width: 88%; + .customTextInput { + width: 100%; + } +`; + +const FieldGroup = styled.div` + padding-bottom: 8px; +`; + +const Label = styled.div` + font-size: 14px; + padding: 10px 0; +`; + +const Divider = styled.div` + border-top: 1px solid ${({ theme }) => theme.outline.weak}; +`; + +const ButtonWrapper = styled.div` + display: flex; + gap: 8px; + padding: 8px; +`; + +const StyledButton = styled(Button)` + flex: 1; +`; + +const SelectWrapper = styled.div` + margin-left: 8px; + width: 95%; + .timezone { + height: 120px; + overflow-y: auto; + width: 100%; + } +`; +export default EditPanel; diff --git a/web/src/beta/components/fields/DateTimeField/index.tsx b/web/src/beta/components/fields/DateTimeField/index.tsx index 1b36f9be49..a4c25aa9e9 100644 --- a/web/src/beta/components/fields/DateTimeField/index.tsx +++ b/web/src/beta/components/fields/DateTimeField/index.tsx @@ -1,9 +1,15 @@ import { useCallback, useState } from "react"; +import Button from "@reearth/beta/components/Button"; +import Icon from "@reearth/beta/components/Icon"; +import * as Popover from "@reearth/beta/components/Popover"; +import Text from "@reearth/beta/components/Text"; +import { useT } from "@reearth/services/i18n"; import { styled } from "@reearth/services/theme"; import Property from ".."; -import TextInput from "../common/TextInput"; + +import EditPanel from "./EditPanel"; export type Props = { name?: string; @@ -13,34 +19,37 @@ export type Props = { }; const DateTimeField: React.FC = ({ name, description, value, onChange }) => { - const [time, setTime] = useState(value?.split(" ")[1] ?? "HH:MM:SS"); - const [date, setDate] = useState(value?.split(" ")[0] ?? "YYYY-MM-DD"); - - const handleTimeChange = useCallback( - (newValue: string | undefined) => { - if (newValue === undefined) return; + const [open, setOpen] = useState(false); + const t = useT(); - setTime(newValue); - onChange?.(date + " " + newValue); - }, - [date, onChange], - ); - - const handleDateChange = useCallback( - (newValue: string | undefined) => { - if (newValue === undefined) return; - - setDate(newValue); - onChange?.(newValue + " " + time); - }, - [time, onChange], - ); + const handlePopOver = useCallback(() => setOpen(!open), [open]); return ( - - + + + + + + {value} + + + + handlePopOver()} + /> + + + + {open && } + + ); @@ -53,3 +62,39 @@ const Wrapper = styled.div` align-items: stretch; gap: 4px; `; + +const InputWrapper = styled.div<{ disabled?: boolean }>` + display: flex; + width: 100%; + gap: 10px; + height: 28px; +`; + +const Input = styled.div<{ dataTimeSet?: boolean }>` + display: flex; + align-items: center; + justify-content: space-between; + gap: 4px; + flex: 1; + padding: 0 8px; + border-radius: 4px; + border: 1px solid ${({ theme }) => theme.outline.weak}; + color: ${({ theme }) => theme.content.strong}; + background: ${({ theme }) => theme.bg[1]}; + box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.25) inset; + + color: ${({ theme, dataTimeSet }) => (dataTimeSet ? theme.content.strong : theme.content.weak)}; +`; + +const TriggerButton = styled(Button)` + margin: 0; +`; + +const DeleteIcon = styled(Icon)<{ disabled?: boolean }>` + ${({ disabled, theme }) => + disabled + ? `color: ${theme.content.weaker};` + : `:hover { + cursor: pointer; + }`} +`; diff --git a/web/src/beta/components/fields/SelectField/index.tsx b/web/src/beta/components/fields/SelectField/index.tsx index 5f182b353a..aa055d4914 100644 --- a/web/src/beta/components/fields/SelectField/index.tsx +++ b/web/src/beta/components/fields/SelectField/index.tsx @@ -22,6 +22,7 @@ export type Props = { // Property field name?: string; description?: string; + className?: string; }; const SelectField: React.FC = ({ @@ -31,6 +32,7 @@ const SelectField: React.FC = ({ disabled = false, name, description, + className, }) => { const t = useT(); @@ -61,7 +63,7 @@ const SelectField: React.FC = ({ - + {options?.map(({ label: value, key }) => (