diff --git a/src-ui/app/config_page/setting_section/SettingSection.jsx b/src-ui/app/config_page/setting_section/SettingSection.jsx index cfa4c9a2..c39e6221 100644 --- a/src-ui/app/config_page/setting_section/SettingSection.jsx +++ b/src-ui/app/config_page/setting_section/SettingSection.jsx @@ -1,9 +1,17 @@ +import { useRef, useLayoutEffect, useEffect } from "react"; + import styles from "./SettingSection.module.scss"; import { SettingBox } from "./setting_box/SettingBox"; +import { store } from "@store"; export const SettingSection = () => { + const scrollContainerRef = useRef(null); + useLayoutEffect(() => { + store.setting_box_scroll_container = scrollContainerRef; + }, []); + return ( -
+
diff --git a/src-ui/app/config_page/setting_section/setting_box/supporters/Supporters.jsx b/src-ui/app/config_page/setting_section/setting_box/supporters/Supporters.jsx index d8e72d34..1d64b9c5 100644 --- a/src-ui/app/config_page/setting_section/setting_box/supporters/Supporters.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/supporters/Supporters.jsx @@ -1,7 +1,12 @@ +import { useState, useEffect } from "react"; import styles from "./Supporters.module.scss"; import clsx from "clsx"; import { useTranslation } from "react-i18next"; +import { + useSettingBoxScrollPosition, +} from "@logics_configs" + const supporter_images = import.meta.glob("@images/supporters/supporters_images/*.{png,jpg,jpeg,svg}", { eager: true }); const chato_expression_images = import.meta.glob("@images/supporters/chato_expressions/*.{png,jpg,jpeg,svg}", { eager: true }); import fanbox_img from "@images/supporters/c_fanbox_1620x580.png"; @@ -9,16 +14,20 @@ import vrct_supporters_title from "@images/supporters/vrct_supporters_title.png" import fanbox_button from "@images/supporters/fanbox_button.png"; import kofi_preparing from "@images/supporters/kofi_preparing.png"; +import ExternalLink from "@images/external_link.svg?react"; + const mogu_count = 8; const mochi_count = 3; const fuwa_count = 4; const basic_count = 5; const former_count = 2; +const and_you_count = 1; const default_icon_numbers = ["05", "06", "07", "11"]; -const supporters_filenames = Array.from({ length: 22 }, (_, index) => `supporter_${String(index + 1).padStart(2, "0")}`); +const supporters_filenames = Array.from({ length: 23 }, (_, index) => `supporter_${String(index + 1).padStart(2, "0")}`); const chato_expressions_filenames = Array.from({ length: 7 }, (_, index) => `chato_expression_${String(index + 1).padStart(2, "0")}`); +const SHUFFLE_INTERVAL_TIME = 20000; const shuffleArray = (array) => { return array .map((value) => ({ value, sort: Math.random() })) @@ -26,37 +35,6 @@ const shuffleArray = (array) => { .map(({ value }) => value); }; -const getCategoryImages = (start, count) => { - const category_images = supporters_filenames.slice(start, start + count); - return shuffleArray(category_images); -}; - -const mogu_images = getCategoryImages(0, mogu_count); -const mochi_images = getCategoryImages(mogu_count, mochi_count); -const fuwa_images = getCategoryImages(mogu_count + mochi_count, fuwa_count); -const basic_images = getCategoryImages(mogu_count + mochi_count + fuwa_count, basic_count); -const former_images = getCategoryImages(mogu_count + mochi_count + fuwa_count + basic_count, former_count); - -const getRandomImage = (images) => { - const random_index = Math.floor(Math.random() * images.length); - return images[random_index]; -}; - -const getSupportersImageByFileName = (file_name) => { - const image_path = Object.keys(supporter_images).find((path) => path.endsWith(file_name + ".png")); - return image_path ? supporter_images[image_path]?.default : null; -}; - -const getChatoImageByFileName = (file_name) => { - const image_path = Object.keys(chato_expression_images).find((path) => path.endsWith(file_name + ".png")); - return image_path ? chato_expression_images[image_path]?.default : null; -}; - -const getRandomDelay = (min, max) => { - const random_value = Math.random() * (max - min) + min; - return `${random_value.toFixed(1)}s`; -}; - export const Supporters = () => { return (
@@ -72,10 +50,9 @@ const SupportUsContainer = () => {
- - {/* for adjust size to their parent component's width. */} - - + + +

日本語 / Mainly Japanese

@@ -87,20 +64,121 @@ const SupportUsContainer = () => { ); }; +const getRandomImage = (images) => { + const random_index = Math.floor(Math.random() * images.length); + return images[random_index]; +}; + export const SupportersContainer = () => { - const renderImages = (image_list, class_name) => { - return image_list.map((file_name) => { + + return ( +
+ +

{`VRCT3.0のアップデートに向けて、めちゃ大変な開発を支えてくれた "Early Supporters" です。\nThey are the 'Early Supporters' who supported us through the incredibly challenging development toward the VRCT3.0 update.`}

+ + + +

{`みなさんのおかげで、みしゃ社長は布団で寝ることを許され(in開発室) しいなは喜び庭駆け回っています!!!ふわもちもぐもぐです!ありがとうございます。これからもまだまだ進化するVRCTをどうかよろしくお願いします!\nThanks to everyone, Misha has been granted the privilege of sleeping in a proper bed (in the development room), and Shiina is so happy, running around the yard! Fuwa-mochi-mogu-mogu! Thank you so much! We hope you'll continue to support the ever-evolving VRCT!`}

+
+ ); +}; + +const ProgressBar = () => { + const [is_active, setIsActive] = useState(false); + + useEffect(() => { + setIsActive(true); + const interval = setInterval(() => { + setIsActive(false); + setTimeout(() => setIsActive(true), 50); + }, SHUFFLE_INTERVAL_TIME); + + return () => clearInterval(interval); + }, []); + + return ( +
+ ); +}; + +const SupportsWrapper = () => { + const { saveScrollPosition, restoreScrollPosition } = useSettingBoxScrollPosition(); + const [imagesState, setImagesState] = useState({ + mogu_images: [], + mochi_images: [], + fuwa_images: [], + basic_images: [], + former_images: [], + and_you_images: [], + chato_images: [], + }); + + const shuffleImages = () => { + saveScrollPosition(); + const getCategoryImages = (start, count) => { + const category_images = supporters_filenames.slice(start, start + count); + return shuffleArray(category_images); + }; + + const randomChatoImages = shuffleArray( + Array.from({ length: mogu_count + mochi_count + fuwa_count + basic_count + former_count }, () => + getRandomImage(chato_expressions_filenames) + ) + ); + + setImagesState({ + mogu_images: getCategoryImages(0, mogu_count), + mochi_images: getCategoryImages(mogu_count, mochi_count), + fuwa_images: getCategoryImages(mogu_count + mochi_count, fuwa_count), + basic_images: getCategoryImages(mogu_count + mochi_count + fuwa_count, basic_count), + former_images: getCategoryImages(mogu_count + mochi_count + fuwa_count + basic_count, former_count), + and_you_images: getCategoryImages(mogu_count + mochi_count + fuwa_count + basic_count + former_count, and_you_count), + chato_images: randomChatoImages, + }); + setTimeout(() => restoreScrollPosition(), 0); + }; + + useEffect(() => { + shuffleImages(); + const interval = setInterval(() => { + shuffleImages(); + }, SHUFFLE_INTERVAL_TIME); + + return () => clearInterval(interval); + }, []); + + + const getSupportersImageByFileName = (file_name) => { + const image_path = Object.keys(supporter_images).find((path) => path.endsWith(file_name + ".png")); + return image_path ? supporter_images[image_path]?.default : null; + }; + + const getChatoImageByFileName = (file_name) => { + const image_path = Object.keys(chato_expression_images).find((path) => path.endsWith(file_name + ".png")); + return image_path ? chato_expression_images[image_path]?.default : null; + }; + + const getRandomDelay = (min, max) => { + const random_value = Math.random() * (max - min) + min; + return `${random_value.toFixed(1)}s`; + }; + + + const renderImages = (image_list, chato_list, options = {}) => { + return image_list.map((file_name, index) => { const img_src = getSupportersImageByFileName(file_name); - const is_default_icon = default_icon_numbers.some((default_num) => file_name.endsWith(default_num)); - const chato_expression_src = is_default_icon - ? getChatoImageByFileName(getRandomImage(chato_expressions_filenames)) - : null; + const is_default_icon = default_icon_numbers.some((element) => file_name.endsWith(element)); + const chato_expression_src = is_default_icon ? getChatoImageByFileName(chato_list[index]) : null; const random_delay = getRandomDelay(0.1, 6); return img_src ? (
@@ -110,22 +188,37 @@ export const SupportersContainer = () => { src={chato_expression_src} /> )} + {options.is_and_you_icon ? : null}
) : null; }); }; return ( -
- -

{`VRCT3.0のアップデートに向けて、むちゃ大変な開発を支えてくれた "Early Supporters" です。\nThey are the 'Early Supporters' who supported us through the incredibly challenging development toward the VRCT3.0 update.`}

-
- {renderImages(mogu_images, `${styles.mogu_image}`)} - {renderImages(mochi_images)} - {renderImages(fuwa_images)} - {renderImages(basic_images)} - {renderImages(former_images)} -
+
+ {renderImages(imagesState.mogu_images, imagesState.chato_images, { class_name: styles.mogu_image })} + {renderImages(imagesState.mochi_images, imagesState.chato_images)} + {renderImages(imagesState.fuwa_images, imagesState.chato_images)} + {renderImages(imagesState.basic_images, imagesState.chato_images)} + {renderImages(imagesState.former_images, imagesState.chato_images)} + + {renderImages(imagesState.and_you_images, imagesState.chato_images, { is_and_you_icon: true, class_name: styles.and_you_image })} +
); }; + +const AndYouIcon = () => { + return ( + <> +
+
+
+
+

+ FANBOX + +

+ + ); +}; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/supporters/Supporters.module.scss b/src-ui/app/config_page/setting_section/setting_box/supporters/Supporters.module.scss index bd556503..4288b653 100644 --- a/src-ui/app/config_page/setting_section/setting_box/supporters/Supporters.module.scss +++ b/src-ui/app/config_page/setting_section/setting_box/supporters/Supporters.module.scss @@ -128,4 +128,78 @@ width: 2.8rem; transform: translate(-50%, -50%) rotate(10deg); opacity: 0.8; +} + +.and_you_container { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + transition: all 0.3s ease; +} + +.and_you_1, .and_you_2 { + width: 2.2rem; + height: 0.2rem; + border-radius: 50%; + background-color: var(--dark_400_color); +} + +.and_you_2 { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) rotate(90deg); +} + +.supporter_image_wrapper { + &:hover .and_you_container { + top: 40%; + transform: translate(-50%, -50%) rotate(180deg); + } + &:hover .and_you_fanbox_link_text { + top: 70%; + opacity: 1; + } +} + +.supporter_image_wrapper.and_you_image { + cursor: pointer; + &:active { + opacity: 0.6; + } +} + +.and_you_fanbox_link_text { + font-size: 1.2rem; + color: var(--dark_400_color); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + transition: all 0.3s ease; + opacity: 0; +} + +.external_link_svg { + color: var(--dark_200_color); + width: 1.2rem; + margin-left: 0.6rem; + padding-bottom: 0.2rem; +} + +.vrct_supporters_desc_end { + font-size: 1.4rem; + margin-top: 2rem; + color: var(--dark_300_color); +} + +.progress_bar { + height: 0.2rem; + width: 0%; + &.progress_bar_active { + transition: width 20000ms linear; + background-color: var(--primary_400_color); + width: 100%; + } } \ No newline at end of file diff --git a/src-ui/assets/supporters/supporters_images/supporter_23.png b/src-ui/assets/supporters/supporters_images/supporter_23.png new file mode 100644 index 00000000..207be07e Binary files /dev/null and b/src-ui/assets/supporters/supporters_images/supporter_23.png differ diff --git a/src-ui/logics/configs/index.js b/src-ui/logics/configs/index.js index 646d24c1..b8426255 100644 --- a/src-ui/logics/configs/index.js +++ b/src-ui/logics/configs/index.js @@ -54,4 +54,5 @@ export { useSendTextToOverlay } from "./vr/useSendTextToOverlay"; export { useOscIpAddress } from "./advanced_settings/useOscIpAddress"; export { useOscPort } from "./advanced_settings/useOscPort"; +export { useSettingBoxScrollPosition } from "./useSettingBoxScrollPosition"; export { useSoftwareVersion } from "./useSoftwareVersion"; \ No newline at end of file diff --git a/src-ui/logics/configs/useSettingBoxScrollPosition.js b/src-ui/logics/configs/useSettingBoxScrollPosition.js new file mode 100644 index 00000000..c0a19b98 --- /dev/null +++ b/src-ui/logics/configs/useSettingBoxScrollPosition.js @@ -0,0 +1,28 @@ +import { store, useStore_SettingBoxScrollPosition } from "@store"; + +export const useSettingBoxScrollPosition = () => { + const { currentSettingBoxScrollPosition, updateSettingBoxScrollPosition, pendingSettingBoxScrollPosition } = useStore_SettingBoxScrollPosition(); + + const saveScrollPosition = () => { + if (store.setting_box_scroll_container.current) { + updateSettingBoxScrollPosition(store.setting_box_scroll_container.current.scrollTop); + } + }; + const restoreScrollPosition = () => { + + if (store.setting_box_scroll_container.current) { + updateSettingBoxScrollPosition((pre) => { + store.setting_box_scroll_container.current.scrollTop = pre.data; + return pre.data; + }) + } + + }; + + return { + saveScrollPosition, + restoreScrollPosition, + currentSettingBoxScrollPosition, + updateSettingBoxScrollPosition, + }; +}; \ No newline at end of file diff --git a/src-ui/store.js b/src-ui/store.js index a7950a2a..19d81081 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -16,6 +16,7 @@ import { export const store = { backend_subprocess: null, config_page: null, + setting_box_scroll_container: null, log_box_ref: null, is_applied_init_message_box_height: false, }; @@ -160,6 +161,7 @@ export const { atomInstance: Atom_IsOpenedLanguageSelector, useHook: useStore_Is // Common export const { atomInstance: Atom_SoftwareVersion, useHook: useStore_SoftwareVersion } = createAtomWithHook("-", "SoftwareVersion"); export const { atomInstance: Atom_SelectedConfigTabId, useHook: useStore_SelectedConfigTabId } = createAtomWithHook("device", "SelectedConfigTabId"); +export const { atomInstance: Atom_SettingBoxScrollPosition, useHook: useStore_SettingBoxScrollPosition } = createAtomWithHook(0, "SettingBoxScrollPosition"); // Designs export const { atomInstance: Atom_IsOpenedDropdownMenu, useHook: useStore_IsOpenedDropdownMenu } = createAtomWithHook("", "IsOpenedDropdownMenu");