actionExport.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import React from "react";
  2. import { trackEvent } from "../analytics";
  3. import { load, questionCircle, save, saveAs } from "../components/icons";
  4. import { ProjectName } from "../components/ProjectName";
  5. import { ToolButton } from "../components/ToolButton";
  6. import "../components/ToolIcon.scss";
  7. import { Tooltip } from "../components/Tooltip";
  8. import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
  9. import { loadFromJSON, saveAsJSON } from "../data";
  10. import { t } from "../i18n";
  11. import { useIsMobile } from "../is-mobile";
  12. import { KEYS } from "../keys";
  13. import { register } from "./register";
  14. import { supported } from "browser-fs-access";
  15. export const actionChangeProjectName = register({
  16. name: "changeProjectName",
  17. perform: (_elements, appState, value) => {
  18. trackEvent("change", "title");
  19. return { appState: { ...appState, name: value }, commitToHistory: false };
  20. },
  21. PanelComponent: ({ appState, updateData, appProps }) => (
  22. <ProjectName
  23. label={t("labels.fileTitle")}
  24. value={appState.name || "Unnamed"}
  25. onChange={(name: string) => updateData(name)}
  26. isNameEditable={
  27. typeof appProps.name === "undefined" && !appState.viewModeEnabled
  28. }
  29. />
  30. ),
  31. });
  32. export const actionChangeExportBackground = register({
  33. name: "changeExportBackground",
  34. perform: (_elements, appState, value) => {
  35. return {
  36. appState: { ...appState, exportBackground: value },
  37. commitToHistory: false,
  38. };
  39. },
  40. PanelComponent: ({ appState, updateData }) => (
  41. <label>
  42. <input
  43. type="checkbox"
  44. checked={appState.exportBackground}
  45. onChange={(event) => updateData(event.target.checked)}
  46. />{" "}
  47. {t("labels.withBackground")}
  48. </label>
  49. ),
  50. });
  51. export const actionChangeExportEmbedScene = register({
  52. name: "changeExportEmbedScene",
  53. perform: (_elements, appState, value) => {
  54. return {
  55. appState: { ...appState, exportEmbedScene: value },
  56. commitToHistory: false,
  57. };
  58. },
  59. PanelComponent: ({ appState, updateData }) => (
  60. <label style={{ display: "flex" }}>
  61. <input
  62. type="checkbox"
  63. checked={appState.exportEmbedScene}
  64. onChange={(event) => updateData(event.target.checked)}
  65. />{" "}
  66. {t("labels.exportEmbedScene")}
  67. <Tooltip
  68. label={t("labels.exportEmbedScene_details")}
  69. position="above"
  70. long={true}
  71. >
  72. <div className="TooltipIcon">{questionCircle}</div>
  73. </Tooltip>
  74. </label>
  75. ),
  76. });
  77. export const actionChangeShouldAddWatermark = register({
  78. name: "changeShouldAddWatermark",
  79. perform: (_elements, appState, value) => {
  80. return {
  81. appState: { ...appState, shouldAddWatermark: value },
  82. commitToHistory: false,
  83. };
  84. },
  85. PanelComponent: ({ appState, updateData }) => (
  86. <label>
  87. <input
  88. type="checkbox"
  89. checked={appState.shouldAddWatermark}
  90. onChange={(event) => updateData(event.target.checked)}
  91. />{" "}
  92. {t("labels.addWatermark")}
  93. </label>
  94. ),
  95. });
  96. export const actionSaveScene = register({
  97. name: "saveScene",
  98. perform: async (elements, appState, value) => {
  99. const fileHandleExists = !!appState.fileHandle;
  100. try {
  101. const { fileHandle } = await saveAsJSON(elements, appState);
  102. return {
  103. commitToHistory: false,
  104. appState: {
  105. ...appState,
  106. fileHandle,
  107. toastMessage: fileHandleExists
  108. ? fileHandle.name
  109. ? t("toast.fileSavedToFilename").replace(
  110. "{filename}",
  111. `"${fileHandle.name}"`,
  112. )
  113. : t("toast.fileSaved")
  114. : null,
  115. },
  116. };
  117. } catch (error) {
  118. if (error?.name !== "AbortError") {
  119. console.error(error);
  120. }
  121. return { commitToHistory: false };
  122. }
  123. },
  124. keyTest: (event) =>
  125. event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
  126. PanelComponent: ({ updateData }) => (
  127. <ToolButton
  128. type="button"
  129. icon={save}
  130. title={t("buttons.save")}
  131. aria-label={t("buttons.save")}
  132. showAriaLabel={useIsMobile()}
  133. onClick={() => updateData(null)}
  134. />
  135. ),
  136. });
  137. export const actionSaveAsScene = register({
  138. name: "saveAsScene",
  139. perform: async (elements, appState, value) => {
  140. try {
  141. const { fileHandle } = await saveAsJSON(elements, {
  142. ...appState,
  143. fileHandle: null,
  144. });
  145. return { commitToHistory: false, appState: { ...appState, fileHandle } };
  146. } catch (error) {
  147. if (error?.name !== "AbortError") {
  148. console.error(error);
  149. }
  150. return { commitToHistory: false };
  151. }
  152. },
  153. keyTest: (event) =>
  154. event.key === KEYS.S && event.shiftKey && event[KEYS.CTRL_OR_CMD],
  155. PanelComponent: ({ updateData }) => (
  156. <ToolButton
  157. type="button"
  158. icon={saveAs}
  159. title={t("buttons.saveAs")}
  160. aria-label={t("buttons.saveAs")}
  161. showAriaLabel={useIsMobile()}
  162. hidden={!supported}
  163. onClick={() => updateData(null)}
  164. />
  165. ),
  166. });
  167. export const actionLoadScene = register({
  168. name: "loadScene",
  169. perform: async (elements, appState) => {
  170. try {
  171. const {
  172. elements: loadedElements,
  173. appState: loadedAppState,
  174. } = await loadFromJSON(appState);
  175. return {
  176. elements: loadedElements,
  177. appState: loadedAppState,
  178. commitToHistory: true,
  179. };
  180. } catch (error) {
  181. if (error?.name === "AbortError") {
  182. return false;
  183. }
  184. return {
  185. elements,
  186. appState: { ...appState, errorMessage: error.message },
  187. commitToHistory: false,
  188. };
  189. }
  190. },
  191. keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
  192. PanelComponent: ({ updateData, appState }) => (
  193. <ToolButton
  194. type="button"
  195. icon={load}
  196. title={t("buttons.load")}
  197. aria-label={t("buttons.load")}
  198. showAriaLabel={useIsMobile()}
  199. onClick={updateData}
  200. />
  201. ),
  202. });
  203. export const actionExportWithDarkMode = register({
  204. name: "exportWithDarkMode",
  205. perform: (_elements, appState, value) => {
  206. return {
  207. appState: { ...appState, exportWithDarkMode: value },
  208. commitToHistory: false,
  209. };
  210. },
  211. PanelComponent: ({ appState, updateData }) => (
  212. <div
  213. style={{
  214. display: "flex",
  215. justifyContent: "flex-end",
  216. marginTop: "-45px",
  217. marginBottom: "10px",
  218. }}
  219. >
  220. <DarkModeToggle
  221. value={appState.exportWithDarkMode ? "dark" : "light"}
  222. onChange={(theme: Appearence) => {
  223. updateData(theme === "dark");
  224. }}
  225. title={t("labels.toggleExportColorScheme")}
  226. />
  227. </div>
  228. ),
  229. });