├── .babelrc ├── .debug ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── .prettierrc ├── CSXS └── manifest.xml ├── LICENSE.md ├── README.md ├── ZXPSignCmd.exe ├── app_src ├── components │ ├── footer │ │ ├── footer.jsx │ │ └── footer.scss │ ├── hiddenFileInput │ │ └── hiddenFileInput.jsx │ ├── main │ │ ├── main.jsx │ │ └── main.scss │ ├── modal │ │ ├── editFolder.jsx │ │ ├── editFolder.scss │ │ ├── editStyle.jsx │ │ ├── editStyle.scss │ │ ├── export.jsx │ │ ├── help.jsx │ │ ├── modal.jsx │ │ ├── modal.scss │ │ ├── settings.jsx │ │ ├── shortCut.jsx │ │ └── update.jsx │ ├── previewBlock │ │ ├── previewBlock.jsx │ │ └── previewBlock.scss │ ├── stylesBlock │ │ ├── stylesBlock.jsx │ │ └── stylesBlock.scss │ └── textBlock │ │ ├── textBlock.jsx │ │ └── textBlock.scss ├── config.js ├── context.jsx ├── host.js ├── hotkeys.jsx ├── index.html ├── index.jsx ├── index.scss ├── lib │ ├── CSInterface.js │ ├── jam │ │ ├── jamActions-min.jsxinc │ │ ├── jamActions.jsxinc │ │ ├── jamBooks-min.jsxinc │ │ ├── jamBooks.jsxinc │ │ ├── jamColors-min.jsxinc │ │ ├── jamColors.jsxinc │ │ ├── jamEngine-min.jsxinc │ │ ├── jamEngine.jsxinc │ │ ├── jamHelpers-min.jsxinc │ │ ├── jamHelpers.jsxinc │ │ ├── jamJSON-min.jsxinc │ │ ├── jamJSON.jsxinc │ │ ├── jamLayers-min.jsxinc │ │ ├── jamLayers.jsxinc │ │ ├── jamShapes-min.jsxinc │ │ ├── jamShapes.jsxinc │ │ ├── jamStyles-min.jsxinc │ │ ├── jamStyles.jsxinc │ │ ├── jamText-min.jsxinc │ │ ├── jamText.jsxinc │ │ ├── jamUtils-min.jsxinc │ │ └── jamUtils.jsxinc │ ├── themeManager.js │ └── topcoat │ │ ├── css │ │ ├── topcoat-desktop-dark.css │ │ ├── topcoat-desktop-dark.min.css │ │ ├── topcoat-desktop-light.css │ │ ├── topcoat-desktop-light.min.css │ │ ├── topcoat-mobile-dark.css │ │ ├── topcoat-mobile-dark.min.css │ │ ├── topcoat-mobile-light.css │ │ └── topcoat-mobile-light.min.css │ │ ├── font │ │ ├── LICENSE.txt │ │ ├── SourceCodePro-Black.otf │ │ ├── SourceCodePro-Bold.otf │ │ ├── SourceCodePro-ExtraLight.otf │ │ ├── SourceCodePro-Light.otf │ │ ├── SourceCodePro-Regular.otf │ │ ├── SourceCodePro-Semibold.otf │ │ ├── SourceSansPro-Black.otf │ │ ├── SourceSansPro-BlackIt.otf │ │ ├── SourceSansPro-Bold.otf │ │ ├── SourceSansPro-BoldIt.otf │ │ ├── SourceSansPro-ExtraLight.otf │ │ ├── SourceSansPro-ExtraLightIt.otf │ │ ├── SourceSansPro-It.otf │ │ ├── SourceSansPro-Light.otf │ │ ├── SourceSansPro-LightIt.otf │ │ ├── SourceSansPro-Regular.otf │ │ ├── SourceSansPro-Semibold.otf │ │ └── SourceSansPro-SemiboldIt.otf │ │ └── img │ │ ├── avatar.png │ │ ├── bg_dark.png │ │ ├── breadcrumb.png │ │ ├── checkbox_checked.png │ │ ├── checkbox_checked_dark.png │ │ ├── checkbox_unchecked.png │ │ ├── checkbox_unchecked_dark.png │ │ ├── checkmark_bw.svg │ │ ├── dark-combo-box-bg.png │ │ ├── dark-combo-box-bg2x.png │ │ ├── dark-grips.png │ │ ├── dark-sprites2x.png │ │ ├── dialog-zone-bg.png │ │ ├── drop-down-triangle-dark.png │ │ ├── drop-down-triangle.png │ │ ├── hamburger_bw.svg │ │ ├── hamburger_dark.svg │ │ ├── hamburger_light.svg │ │ ├── light-combo-box-bg.png │ │ ├── light-combo-box-bg2x.png │ │ ├── light-grips.png │ │ ├── light-sprites2x.png │ │ ├── pop-up-triangle-dark.png │ │ ├── pop-up-triangle.png │ │ ├── search-bg.png │ │ ├── search-bg2x.png │ │ ├── search.svg │ │ ├── search_bw.svg │ │ ├── search_dark.svg │ │ ├── search_light.svg │ │ ├── spinner.png │ │ └── spinner2x.png └── utils.js ├── icons ├── iconDarkNormal.png ├── iconDarkNormal@2X.png ├── iconDarkRollover.png ├── iconDarkRollover@2X.png ├── iconDisabled.png ├── iconDisabled@2X.png ├── iconNormal.png ├── iconNormal@2X.png ├── iconRollover.png └── iconRollover@2X.png ├── icons_src ├── icons.psd └── icons@2X.psd ├── install_mac.sh ├── install_win.cmd ├── locale ├── de_DE │ └── messages.properties ├── es_SP │ └── messages.properties ├── fr_FR │ └── messages.properties ├── messages.properties ├── pt_BR │ └── messages.properties ├── ru_RU │ └── messages.properties ├── tr_TR │ └── messages.properties ├── uk_UA │ └── messages.properties └── vi_VN │ └── messages.properties ├── pack.zip.ps1 ├── pack.zxp.cmd ├── package-lock.json ├── package.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | ["@babel/preset-env", { 5 | "targets": {"chrome": "18"} 6 | }] 7 | ], 8 | "plugins": [ 9 | "@babel/plugin-proposal-nullish-coalescing-operator", 10 | "@babel/plugin-proposal-optional-chaining", 11 | "@babel/plugin-proposal-class-properties", 12 | "@babel/plugin-syntax-dynamic-import", 13 | "@babel/plugin-transform-runtime", 14 | "lodash" 15 | ], 16 | "env": { 17 | "development": {}, 18 | "production": { 19 | "presets": [ 20 | ["minify", {"builtIns": false}] 21 | ] 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /.debug: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | app -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 2018 5 | }, 6 | "env": { 7 | "es6": true, 8 | "browser": true, 9 | "node": true 10 | }, 11 | "rules": { 12 | "no-prototype-builtins": 0, 13 | "require-atomic-updates": 0, 14 | "react/display-name": 0, 15 | "react-hooks/rules-of-hooks": "error" 16 | }, 17 | "extends": ["eslint:recommended", "plugin:react/recommended"], 18 | "plugins": ["react", "react-hooks"] 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | app 4 | storage 5 | TypeR-*.zip 6 | TypeR-*.zxp 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps = true 2 | EOF -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "printWidth": 8e24, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "semi": true, 7 | "singleQuote": false, 8 | "trailingComma": "es5", 9 | "bracketSpacing": true, 10 | "bracketSameLine": false, 11 | "arrowParens": "always", 12 | "endOfLine": "lf" 13 | } 14 | -------------------------------------------------------------------------------- /CSXS/manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ./app/index.html 23 | ./app/host.jsx 24 | 25 | --allow-file-access 26 | --allow-file-access-from-files 27 | 28 | 29 | 30 | true 31 | 32 | 33 | Panel 34 | TypeR 35 | 36 | 37 | 900 38 | 600 39 | 40 | 41 | 300 42 | 250 43 | 44 | 45 | 9999 46 | 800 47 | 48 | 49 | 50 | ./icons/iconNormal.png 51 | ./icons/iconRollover.png 52 | ./icons/iconDisabled.png 53 | ./icons/iconDarkNormal.png 54 | ./icons/iconDarkRollover.png 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Swirt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeR 2 | 3 | TypeR is a better version of TyperTools, a Photoshop extension designed for typesetters working with manga and comics script. It simplifies routine tasks of typeset such as placing text on an image, aligning text, and performing style management. This version includes several bug fixes and new features to improve your workflow. 4 | 5 | ## Key Features 6 | 7 | - **Bug Fixes**: Multiple bugs from the original TyperTools have been fixed. 8 | - **Text Smoothing Issue Resolved**: Fixed the bug that changed text smoothing from "Strong" to "Smooth" when increasing text size. 9 | - **Stable Auto-Centering**: Text shape no longer changes when using auto-centering. 10 | - **Customizable Shortcuts**: You can now modify keyboard shortcuts. (+ added some new keyboard shortcuts) 11 | - **Automatic Page Detection**: Automatically detects pages when importing. 12 | - **Automatic Page Switching**: Automatically switches pages for seamless workflow. 13 | - **Resize TypeR**: Decreased size limit of the TypeR window so it can be much smaller. 14 | - **Line Spacing Sync**: When increasing/decreasing text size with TypeR, line spacing adjusts accordingly. 15 | - **Adaptive Size**: If no fixed text size is defined, the size of the selected layer will be used. 16 | - **Line Break on Insert**: A line break is now automatically added when inserting text on the current layer. 17 | - **Duplicate Style Folders**: You can now duplicate a style folder easily. 18 | - **Export a Single Folder**: No need to export all parameters and font styles—just export/import one folder as needed. 19 | - **Change Language**: Select the interface language directly from the settings. 20 | 21 | 22 | 23 | ## Requirements 24 | 25 | - Windows 8/macOS 10.9 or newer. 26 | - Adobe Photoshop CC 2015 or newer. 27 | (There may be problems with some portable or lightweight builds) 28 | 29 | ## Installation Guide 30 | # If you download from the source code : 31 | ### Prerequisites 32 | 33 | - Ensure you have Node.js installed on your system. You can download it from [Node.js official website](https://nodejs.org/). 34 | 35 | ### Steps 36 | 37 | 1. Clone the repository and navigate to the root directory in your terminal. 38 | 39 | ```sh 40 | git clone https://github.com/ScanR/TypeR.git 41 | cd TypeR 42 | ``` 43 | 44 | 2. Install the necessary dependencies. 45 | 46 | ```sh 47 | npm install 48 | ``` 49 | 50 | 3. Build the project using npm. 51 | 52 | 53 | ```sh 54 | npm run build 55 | ``` 56 | 57 | 4. Execute the installation script for your operating system. 58 | 59 | For macOS: 60 | ```sh 61 | chmod +x install_mac.sh 62 | ./install_mac.sh 63 | ``` 64 | 65 | For Windows: 66 | ```sh 67 | install_win.cmd 68 | ``` 69 | 70 | # If you download from the release : 71 | 1. Extract the archive and execute the installation script for your operating system. 72 | 73 | For macOS: 74 | ```sh 75 | chmod +x install_mac.sh 76 | ./install_mac.sh 77 | ``` 78 | 79 | For Windows: 80 | ```sh 81 | install_win.cmd 82 | ``` 83 | ## Usage 84 | 85 | After installation, you can access TypeR within Adobe Photoshop Extensions tab. 86 | 87 | ## Contributing 88 | 89 | If you encounter any issues or have suggestions for improvements, feel free to open an issue or submit a pull request. 90 | -------------------------------------------------------------------------------- /ZXPSignCmd.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeanR-ScanR/TypeR/7327564b077da3cf068bca06690851f86d579517/ZXPSignCmd.exe -------------------------------------------------------------------------------- /app_src/components/footer/footer.jsx: -------------------------------------------------------------------------------- 1 | import "./footer.scss"; 2 | 3 | import React from "react"; 4 | import { locale } from "../../utils"; 5 | import { useContext } from "../../context"; 6 | import HiddenFileInput from "../hiddenFileInput/hiddenFileInput"; 7 | 8 | const AppFooter = React.memo(function AppFooter() { 9 | const context = useContext(); 10 | const openSettings = () => { 11 | context.dispatch({ 12 | type: "setModal", 13 | modal: "settings", 14 | }); 15 | }; 16 | const openHelp = () => { 17 | context.dispatch({ 18 | type: "setModal", 19 | modal: "help", 20 | }); 21 | }; 22 | const fileInputRef = React.useRef(); 23 | 24 | const openRepository = () => { 25 | if (context.state.images.length) { 26 | context.dispatch({ type: "setImages", images: [] }); 27 | return; 28 | } 29 | fileInputRef.current?.click(); 30 | }; 31 | 32 | return ( 33 | 34 | 35 | {locale.footerHelp} 36 | 37 | 38 | {locale.footerSettings} 39 | 40 | 41 | {context.state.images.length 42 | ? locale.footerDesyncRepo 43 | : locale.footerOpenRepo} 44 | 45 | 46 | 47 | ); 48 | }); 49 | 50 | export default AppFooter; 51 | -------------------------------------------------------------------------------- /app_src/components/footer/footer.scss: -------------------------------------------------------------------------------- 1 | .footer-block { 2 | box-sizing: border-box; 3 | padding: 2px 10px 6px; 4 | min-height: 20px; 5 | font-size: 12px; 6 | display: flex; 7 | .link { 8 | opacity: 0.7; 9 | &:hover { 10 | opacity: 1; 11 | } 12 | } 13 | .link-left { 14 | margin-left: auto !important; 15 | margin-right: 3%; 16 | } 17 | .link + .link { 18 | margin-left: 8px; 19 | } 20 | } 21 | 22 | @media (max-height: 450px) { 23 | .footer-block { 24 | display: none; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app_src/components/hiddenFileInput/hiddenFileInput.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useContext } from "../../context"; 3 | 4 | const allowed = ".psd,.png,.jpg,.jpeg"; 5 | 6 | export default React.forwardRef(function HiddenFileInput(_, ref) { 7 | const context = useContext(); 8 | 9 | const onChange = (e) => { 10 | const files = Array.from(e.target.files || []).filter((f) => 11 | allowed 12 | .split(",") 13 | .includes("." + f.name.split(".").pop().toLowerCase()) 14 | ); 15 | if (!files.length) return; 16 | 17 | const images = files 18 | .map((f) => { 19 | const baseName = f.name.replace(/\.[^.]+$/, ""); 20 | return { name: f.name, baseName, path: f.path || f.name }; 21 | }) 22 | .sort((a, b) => a.baseName.localeCompare(b.baseName)); 23 | 24 | context.dispatch({ type: "setImages", images }); 25 | e.target.value = ""; 26 | }; 27 | 28 | return ( 29 | 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /app_src/components/main/main.jsx: -------------------------------------------------------------------------------- 1 | import "./main.scss"; 2 | 3 | import React from "react"; 4 | import { readStorage, writeToStorage, resizeTextArea } from "../../utils"; 5 | import Modal from "../modal/modal"; 6 | import TextBlock from "../textBlock/textBlock"; 7 | import PreviewBlock from "../previewBlock/previewBlock"; 8 | import StylesBlock from "../stylesBlock/stylesBlock"; 9 | import AppFooter from "../footer/footer"; 10 | 11 | const topHeight = 130; 12 | const minMiddleHeight = 100; 13 | const minBottomHeight = 70; 14 | 15 | const ResizeableCont = React.memo(function ResizeableCont() { 16 | const appBlock = React.useRef(); 17 | const bottomBlock = React.useRef(); 18 | 19 | let dragging = false; 20 | let resizeStartY = 0; 21 | let resizeStartH = 0; 22 | let bottomHeight = 0; 23 | let appHeight = 0; 24 | 25 | const startBottomResize = (e) => { 26 | resizeStartH = bottomBlock.current.offsetHeight; 27 | resizeStartY = e.pageY; 28 | dragging = true; 29 | }; 30 | 31 | const stopBottomResize = () => { 32 | if (dragging) { 33 | writeToStorage({ bottomHeight }); 34 | dragging = false; 35 | } 36 | }; 37 | 38 | const moveBottomResize = (e) => { 39 | if (dragging) { 40 | e.preventDefault(); 41 | const dy = e.pageY - resizeStartY; 42 | const newHeight = resizeStartH - dy; 43 | setBottomSize(newHeight); 44 | } 45 | }; 46 | 47 | const setBottomSize = (height) => { 48 | const maxBottomHeight = appHeight - (appHeight > 450 ? topHeight : 0) - minMiddleHeight; 49 | bottomHeight = height || readStorage("bottomHeight") || minBottomHeight; 50 | if (height < minBottomHeight) bottomHeight = minBottomHeight; 51 | if (height > maxBottomHeight) bottomHeight = maxBottomHeight; 52 | bottomBlock.current.style.height = bottomHeight + "px"; 53 | resizeTextArea(); 54 | }; 55 | 56 | const setAppSize = () => { 57 | appHeight = document.documentElement.clientHeight; 58 | appBlock.current.style.height = appHeight + "px"; 59 | setBottomSize(); 60 | }; 61 | 62 | React.useEffect(() => { 63 | window.addEventListener("resize", setAppSize); 64 | setAppSize(); 65 | }, []); 66 | 67 | return ( 68 |
69 | 70 |
71 | 72 |
73 |
74 |
75 | 76 |
77 |
78 |
79 |
80 |
81 | 82 |
83 |
84 | 85 |
86 |
87 | ); 88 | }); 89 | 90 | export default ResizeableCont; 91 | -------------------------------------------------------------------------------- /app_src/components/main/main.scss: -------------------------------------------------------------------------------- 1 | .app-body { 2 | flex-direction: column; 3 | position: relative; 4 | overflow: hidden; 5 | display: flex; 6 | } 7 | 8 | .top-block { 9 | flex: 0 0 auto; 10 | } 11 | .top-divider { 12 | flex: 0 0 auto; 13 | height: 4px; 14 | } 15 | .middle-block { 16 | flex-grow: 2; 17 | } 18 | .middle-divider { 19 | cursor: ns-resize; 20 | padding: 3px 0; 21 | flex: 0 0 auto; 22 | & > DIV { 23 | height: 2px; 24 | } 25 | } 26 | .bottom-block { 27 | flex: 0 0 auto; 28 | } 29 | .footer-block { 30 | flex: 0 0 auto; 31 | } -------------------------------------------------------------------------------- /app_src/components/modal/editFolder.jsx: -------------------------------------------------------------------------------- 1 | import './editFolder.scss'; 2 | 3 | import React from 'react'; 4 | import PropTypes from 'prop-types'; 5 | import {FiX} from "react-icons/fi"; 6 | import {MdDelete, MdCancel, MdSave} from "react-icons/md"; 7 | 8 | import {locale, nativeAlert, nativeConfirm} from '../../utils'; 9 | import {useContext} from '../../context'; 10 | 11 | 12 | const EditFolderModal = React.memo(function EditFolderModal() { 13 | const context = useContext(); 14 | const currentData = context.state.modalData; 15 | const folderStyleIds = currentData.id ? context.state.styles.filter(s => (s.folder === currentData.id)).map(s => s.id) : []; 16 | const [name, setName] = React.useState(currentData.name || ''); 17 | const [styleIds, setStyleIds] = React.useState(folderStyleIds); 18 | const [edited, setEdited] = React.useState(false); 19 | const nameInputRef = React.useRef(); 20 | 21 | const close = () => { 22 | context.dispatch({type: 'setModal'}); 23 | }; 24 | 25 | const changeFolderName = e => { 26 | setName(e.target.value); 27 | setEdited(true); 28 | }; 29 | 30 | const changeFolderStyles = (id, add) => { 31 | let folderStyles = styleIds.concat([]); 32 | if (add) { 33 | folderStyles.push(id); 34 | } else { 35 | folderStyles = folderStyles.filter(sid => (sid !== id)); 36 | } 37 | setStyleIds(folderStyles); 38 | setEdited(true); 39 | }; 40 | 41 | const saveFolder = e => { 42 | e.preventDefault(); 43 | if (!name) { 44 | nativeAlert(locale.errorFolderCreation, locale.errorTitle, true); 45 | return false; 46 | } 47 | const data = {name, styleIds}; 48 | if (currentData.create) { 49 | data.id = Math.random().toString(36).substr(2, 8); 50 | } else { 51 | data.id = currentData.id; 52 | } 53 | context.dispatch({type: 'saveFolder', data}); 54 | close(); 55 | }; 56 | 57 | const deleteFolder = e => { 58 | e.preventDefault(); 59 | if (!currentData.id) return; 60 | const permanent = e.shiftKey; 61 | const confirmText = permanent ? locale.confirmDeleteFolderPermanent : locale.confirmDeleteFolder; 62 | nativeConfirm(confirmText, locale.confirmTitle, ok => { 63 | if (!ok) return; 64 | if (permanent) { 65 | context.state.styles.filter(s => s.folder === currentData.id).forEach(s => { 66 | context.dispatch({type: 'deleteStyle', id: s.id}); 67 | }); 68 | } 69 | context.dispatch({type: 'deleteFolder', id: currentData.id}); 70 | close(); 71 | }); 72 | }; 73 | 74 | React.useEffect(() => { 75 | if (nameInputRef.current) nameInputRef.current.focus(); 76 | }, []); 77 | 78 | const unsortedStyles = context.state.styles.filter(s => !s.folder); 79 | 80 | return ( 81 | 82 |
83 |
84 | {currentData.create ? locale.createFolderTitle : locale.editFolderTitle} 85 |
86 | 89 |
90 |
91 |
92 |
93 |
94 |
95 | {locale.editFolderNameLabel} 96 |
97 |
98 | 105 |
106 |
107 |
108 |
109 | {locale.editFolderStyles} 110 |
111 |
112 |
113 | {context.state.styles.length ? ( 114 | 115 | {(unsortedStyles.length > 0) && ( 116 | 122 | )} 123 | {context.state.folders.map(folder => ( 124 | (s.folder === folder.id))} 128 | toggleStyle={changeFolderStyles} 129 | selected={styleIds} 130 | /> 131 | ))} 132 | 133 | ) : ( 134 |
135 | {locale.editFolderNoStyles} 136 |
137 | )} 138 |
139 |
140 |
141 |
142 |
143 | 146 | {currentData.create ? ( 147 | 150 | ) : ( 151 | 154 | )} 155 |
156 |
157 |
158 |
159 | ); 160 | }); 161 | 162 | 163 | const FolderStylesList = React.memo(function FolderStylesList(props) { 164 | return ( 165 | 166 | {props.styles.map(style => ( 167 | 181 | ))} 182 | 183 | ); 184 | }); 185 | FolderStylesList.propTypes = { 186 | name: PropTypes.string.isRequired, 187 | styles: PropTypes.array.isRequired, 188 | toggleStyle: PropTypes.func.isRequired, 189 | selected: PropTypes.array.isRequired 190 | }; 191 | 192 | export default EditFolderModal; -------------------------------------------------------------------------------- /app_src/components/modal/editFolder.scss: -------------------------------------------------------------------------------- 1 | .folder-styles-list { 2 | background: rgba(#000, .2); 3 | max-height: 345px; 4 | overflow-y: auto; 5 | padding: 4px; 6 | } 7 | .folder-style-item { 8 | display: flex; 9 | align-items: center; 10 | justify-content: space-between; 11 | padding: 4px; 12 | opacity: .7; 13 | & + & { 14 | margin-top: 4px; 15 | } 16 | &:hover { 17 | outline: 1px solid rgba(#fff, .5); 18 | } 19 | .light-theme &:hover { 20 | outline: 1px solid rgba(#000, .5); 21 | } 22 | &.m-selected { 23 | font-weight: bold; 24 | opacity: 1; 25 | } 26 | } 27 | .folder-style-cbx { 28 | margin-right: 6px; 29 | flex: 0 0 auto; 30 | } 31 | .folder-style-title { 32 | flex-grow: 2; 33 | SPAN { 34 | font-weight: normal; 35 | margin-left: 4px; 36 | font-size: 12px; 37 | opacity: .5; 38 | } 39 | } 40 | .folder-styles-list-empty { 41 | text-align: center; 42 | padding: 4px 10px; 43 | font-size: 14px; 44 | opacity: .5; 45 | } 46 | 47 | .folder-edit-actions { 48 | justify-content: space-between; 49 | display: flex; 50 | } 51 | .folder-edit-save { 52 | width: 50%; 53 | } -------------------------------------------------------------------------------- /app_src/components/modal/editStyle.scss: -------------------------------------------------------------------------------- 1 | .style-edit-details { 2 | border: 1px solid rgba(#000, .12); 3 | background: rgba(#000, .06); 4 | border-radius: 3px; 5 | padding: 5px 8px; 6 | margin-top: 6px; 7 | font-size: 12px; 8 | } 9 | .style-edit-props { 10 | max-width: 345px; 11 | margin-top: 8px; 12 | INPUT, SELECT { 13 | display: inline-block; 14 | max-width: 100%; 15 | min-width: 0; 16 | width: 100%; 17 | } 18 | SELECT { 19 | padding-left: 6px; 20 | } 21 | INPUT[type=number] { 22 | padding-right: 0; 23 | } 24 | .style-edit-font-style { 25 | width: calc(50% - 37px); 26 | margin-left: 8px; 27 | flex: 0 0 auto; 28 | } 29 | .topcoat-icon-button--large:focus { 30 | box-shadow: inset 0 1px #737373; 31 | border: 1px solid #333434; 32 | } 33 | .topcoat-icon-button--large--quiet { 34 | border: 1px solid rgba(#000, .1); 35 | &:hover, &:active { 36 | border: 1px solid #333434; 37 | } 38 | &:focus { 39 | box-shadow: none; 40 | } 41 | } 42 | } 43 | .style-edit-props-row { 44 | justify-content: space-between; 45 | align-items: center; 46 | display: flex; 47 | & + & { 48 | margin-top: 10px; 49 | } 50 | &.m-autoleading { 51 | position: relative; 52 | &:before { 53 | border-right: 1px solid rgba(#000, .3); 54 | border-left: 1px solid rgba(#000, .3); 55 | position: absolute; 56 | content: ''; 57 | height: 10px; 58 | width: 1px; 59 | right: 68px; 60 | top: -10px; 61 | } 62 | .style-edit-props-label { 63 | text-align: right; 64 | font-size: 12px; 65 | line-height: 1; 66 | flex-grow: 2; 67 | opacity: .4; 68 | SPAN { 69 | display: inline-block; 70 | margin-top: -2px; 71 | max-width: 100px; 72 | } 73 | } 74 | INPUT[type=number] { 75 | width: calc(50% - 37px); 76 | margin-left: 8px; 77 | flex: 0 0 auto; 78 | } 79 | } 80 | } 81 | .style-edit-props-col { 82 | position: relative; 83 | align-items: center; 84 | display: flex; 85 | width: 50%; 86 | &.m-justify { 87 | justify-content: space-between; 88 | } 89 | & + & { 90 | margin-left: 15px; 91 | } 92 | } 93 | .style-edit-props-icon { 94 | flex: 0 0 auto; 95 | height: 25px; 96 | width: 30px; 97 | opacity: .4; 98 | } 99 | .style-edit-props-unit { 100 | position: absolute; 101 | opacity: .4; 102 | right: 6px; 103 | z-index: 2; 104 | *:hover + &, 105 | *:focus + & { 106 | display: none; 107 | } 108 | } 109 | 110 | .style-edit-color { 111 | position: relative; 112 | width: 100%; 113 | .sketch-picker { 114 | position: absolute; 115 | bottom: 26px; 116 | z-index: 21; 117 | left: 0; 118 | } 119 | &.m-right { 120 | .sketch-picker { 121 | left: auto; 122 | right: 0; 123 | } 124 | } 125 | } 126 | .style-edit-color-sample { 127 | box-shadow: 0 0 4px 1px rgba(#000, .4); 128 | position: relative; 129 | background: #fff; 130 | text-align: center; 131 | line-height: 21px; 132 | font-size: 14px; 133 | cursor: pointer; 134 | color: #000; 135 | padding: 2px; 136 | DIV { 137 | position: absolute; 138 | padding: 0 4px; 139 | z-index: 1; 140 | bottom: 2px; 141 | right: 2px; 142 | left: 2px; 143 | top: 2px; 144 | } 145 | SPAN { 146 | text-shadow: 0 0 6px #fff; 147 | position: relative; 148 | z-index: 2; 149 | } 150 | &.m-opacity { 151 | DIV { 152 | opacity: .9; 153 | } 154 | } 155 | } 156 | .style-edit-stroke { 157 | display: flex; 158 | align-items: center; 159 | INPUT { 160 | max-width: 70px; 161 | margin-right: 8px; 162 | } 163 | } 164 | .color-picker-overlay { 165 | position: fixed; 166 | height: 100%; 167 | width: 100%; 168 | z-index: 20; 169 | left: 0; 170 | top: 0; 171 | } 172 | 173 | .style-edit-actions { 174 | justify-content: space-between; 175 | display: flex; 176 | } 177 | .style-edit-save { 178 | width: 50%; 179 | } -------------------------------------------------------------------------------- /app_src/components/modal/export.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FiX } from "react-icons/fi"; 3 | import { MdSave } from "react-icons/md"; 4 | 5 | import config from "../../config"; 6 | import { locale } from "../../utils"; 7 | import { useContext } from "../../context"; 8 | 9 | const ExportModal = React.memo(function ExportModal() { 10 | const context = useContext(); 11 | const [selected, setSelected] = React.useState([]); 12 | const [withSettings, setWithSettings] = React.useState(true); 13 | 14 | const close = () => { 15 | context.dispatch({ type: "setModal" }); 16 | }; 17 | 18 | const toggleFolder = (id, checked) => { 19 | let arr = selected.slice(); 20 | if (checked) arr.push(id); 21 | else arr = arr.filter((fid) => fid !== id); 22 | setSelected(arr); 23 | }; 24 | 25 | const exportData = (e) => { 26 | e.preventDefault(); 27 | if (!selected.length && !withSettings) return; 28 | const pathSelect = window.cep.fs.showSaveDialogEx( 29 | false, 30 | false, 31 | ["json"], 32 | config.exportFileName + ".json" 33 | ); 34 | if (!pathSelect?.data) return false; 35 | const folders = context.state.folders.filter((f) => selected.includes(f.id)); 36 | const styles = context.state.styles.filter((s) => selected.includes(s.folder)); 37 | const data = { 38 | folders, 39 | styles, 40 | version: config.appVersion, 41 | exported: new Date(), 42 | }; 43 | if (withSettings) { 44 | data.ignoreLinePrefixes = context.state.ignoreLinePrefixes; 45 | data.defaultStyleId = context.state.defaultStyleId; 46 | data.language = context.state.language; 47 | data.autoClosePSD = context.state.autoClosePSD; 48 | data.textItemKind = context.state.setTextItemKind; 49 | } 50 | window.cep.fs.writeFile(pathSelect.data, JSON.stringify(data)); 51 | close(); 52 | }; 53 | 54 | return ( 55 | 56 |
57 |
{locale.settingsExport}
58 | 61 |
62 |
63 |
64 |
65 | {context.state.folders.map((folder) => ( 66 | 75 | ))} 76 | 85 |
86 |
87 | 90 |
91 |
92 |
93 |
94 | ); 95 | }); 96 | 97 | export default ExportModal; 98 | -------------------------------------------------------------------------------- /app_src/components/modal/help.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {FiX} from "react-icons/fi"; 3 | 4 | import config from '../../config'; 5 | import {locale, openUrl} from '../../utils'; 6 | import {useContext} from '../../context'; 7 | 8 | 9 | const HelpModal = React.memo(function HelpModal() { 10 | const context = useContext(); 11 | const close = () => { 12 | context.dispatch({type: 'setModal'}); 13 | }; 14 | return ( 15 | 16 |
17 |
18 | {locale.helpTitle} 19 |
20 | 23 |
24 |
25 |
30 |
31 |
32 | openUrl(config.appUrl)}>{config.appTitle} ({locale.helpVersion}: {config.appVersion}){', '} 33 | {locale.helpAuthor} openUrl(config.authorUrl)}>{config.authorName} 34 |
35 |
36 | ); 37 | }); 38 | 39 | export default HelpModal; -------------------------------------------------------------------------------- /app_src/components/modal/modal.jsx: -------------------------------------------------------------------------------- 1 | import './modal.scss'; 2 | 3 | import React from 'react'; 4 | import {useContext} from '../../context'; 5 | import HelpModal from './help'; 6 | import SettingsModal from './settings'; 7 | import EditStyleModal from './editStyle'; 8 | import EditFolderModal from './editFolder'; 9 | import ExportModal from './export'; 10 | import UpdateModal from './update'; 11 | 12 | 13 | const Modal = React.memo(function Modal() { 14 | const context = useContext(); 15 | 16 | let modalContent = null; 17 | let modalType = context.state.modalType; 18 | if (modalType === 'help') modalContent = ; 19 | else if (modalType === 'settings') modalContent = ; 20 | else if (modalType === 'editStyle') modalContent = ; 21 | else if (modalType === 'editFolder') modalContent = ; 22 | else if (modalType === 'export') modalContent = ; 23 | else if (modalType === 'update') modalContent = ; 24 | 25 | React.useEffect(() => { 26 | if (!context.state.notFirstTime) { 27 | context.dispatch({type: 'removeFirstTime'}); 28 | } 29 | }, []); 30 | 31 | return modalContent ? ( 32 |
33 |
34 |
35 | {modalContent} 36 |
37 |
38 | ) : null; 39 | }); 40 | 41 | export default Modal; -------------------------------------------------------------------------------- /app_src/components/modal/modal.scss: -------------------------------------------------------------------------------- 1 | .app-modal { 2 | z-index: 10; 3 | position: absolute; 4 | height: 100%; 5 | width: 100%; 6 | left: 0; 7 | top: 0; 8 | } 9 | .app-modal-hatch { 10 | z-index: 1; 11 | opacity: .8; 12 | position: absolute; 13 | height: 100%; 14 | width: 100%; 15 | left: 0; 16 | top: 0; 17 | } 18 | .app-modal-inner { 19 | z-index: 2; 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: space-between; 23 | box-shadow: 0 0 6px 2px rgba(#000, .4); 24 | position: absolute; 25 | border-radius: 6px; 26 | bottom: 20px; 27 | right: 8px; 28 | left: 8px; 29 | top: 8px; 30 | } 31 | 32 | .app-modal-header { 33 | justify-content: space-between; 34 | align-items: center; 35 | display: flex; 36 | padding: 10px; 37 | flex: 0 0 auto; 38 | } 39 | .app-modal-title { 40 | font-weight: bold; 41 | font-size: 16px; 42 | } 43 | 44 | .app-modal-body { 45 | overflow: auto; 46 | flex-grow: 2; 47 | } 48 | .app-modal-body-inner { 49 | font-size: 14px; 50 | padding: 10px; 51 | } 52 | 53 | .app-modal-footer { 54 | padding: 6px 10px; 55 | font-size: 12px; 56 | flex: 0 0 auto; 57 | } 58 | 59 | .export-folder-item, 60 | .export-settings-item { 61 | display: flex; 62 | align-items: center; 63 | padding: 4px; 64 | } 65 | .export-folder-item + .export-folder-item { 66 | margin-top: 4px; 67 | } 68 | .export-folder-title, 69 | .export-settings-title { 70 | margin-left: 6px; 71 | } -------------------------------------------------------------------------------- /app_src/components/modal/shortCut.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { locale } from "../../utils"; 3 | 4 | const Shortcut = (props) => { 5 | const changeShortCut = (e) => { 6 | e.preventDefault(); 7 | let shortCut = ""; 8 | if (e.metaKey) { 9 | shortCut += "WIN"; 10 | } 11 | if (e.ctrlKey) { 12 | shortCut += `${shortCut ? " + " : ""}CTRL`; 13 | } 14 | if (e.altKey) { 15 | shortCut += `${shortCut ? " + " : ""}ALT`; 16 | } 17 | if (e.shiftKey) { 18 | shortCut += `${shortCut ? " + " : ""}SHIFT`; 19 | } 20 | if (e.key && !["Meta", "Control", "Alt", "Shift"].includes(e.key)) { 21 | if (e.key === "+") { 22 | shortCut += `${shortCut ? " + " : ""}PLUS`; 23 | } else if (e.key === "-") { 24 | shortCut += `${shortCut ? " + " : ""}MINUS`; 25 | } else if (e.key === "=") { 26 | shortCut += `${shortCut ? " + " : ""}EQUAL`; 27 | } else if (e.key === "/") { 28 | shortCut += `${shortCut ? " + " : ""}DIVIDE`; 29 | } else if (e.key === "*") { 30 | shortCut += `${shortCut ? " + " : ""}MULTIPLY`; 31 | } else { 32 | shortCut += `${shortCut ? " + " : ""}${e.key.toUpperCase()}`; 33 | } 34 | } 35 | e.target.value = shortCut; 36 | }; 37 | 38 | return ( 39 | 40 |
{locale[`shortcut_${props.index}`]}
41 |
42 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default Shortcut; 49 | -------------------------------------------------------------------------------- /app_src/components/modal/update.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FiX } from 'react-icons/fi'; 3 | 4 | import { locale, openUrl } from '../../utils'; 5 | import { useContext } from '../../context'; 6 | 7 | const UpdateModal = React.memo(function UpdateModal() { 8 | const context = useContext(); 9 | const { version, body } = context.state.modalData; 10 | const close = () => { 11 | context.dispatch({ type: 'setModal' }); 12 | }; 13 | const download = () => { 14 | openUrl('https://github.com/ScanR/TypeR/releases/latest'); 15 | close(); 16 | }; 17 | return ( 18 | 19 |
20 |
{locale.updateTitle}
21 | 24 |
25 |
26 |
27 |

{locale.updateText.replace('{version}', version)}

28 | {body && ( 29 | 30 |
31 | 32 | )} 33 |
34 |
35 |
36 | 37 |
38 | 39 | ); 40 | }); 41 | 42 | export default UpdateModal; 43 | -------------------------------------------------------------------------------- /app_src/components/previewBlock/previewBlock.jsx: -------------------------------------------------------------------------------- 1 | import "./previewBlock.scss"; 2 | 3 | import _ from "lodash"; 4 | import React from "react"; 5 | import { FiArrowRightCircle, FiPlusCircle, FiMinusCircle, FiArrowUp, FiArrowDown } from "react-icons/fi"; 6 | import { AiOutlineBorderInner } from "react-icons/ai"; 7 | import { MdCenterFocusWeak } from "react-icons/md"; 8 | 9 | import { locale, setActiveLayerText, createTextLayerInSelection, alignTextLayerToSelection, changeActiveLayerTextSize, getStyleObject, scrollToLine } from "../../utils"; 10 | import { useContext } from "../../context"; 11 | 12 | const PreviewBlock = React.memo(function PreviewBlock() { 13 | const context = useContext(); 14 | const style = context.state.currentStyle || {}; 15 | const line = context.state.currentLine || { text: "" }; 16 | const textStyle = style.textProps?.layerText.textStyleRange[0].textStyle || {}; 17 | const styleObject = getStyleObject(textStyle); 18 | 19 | const createLayer = () => { 20 | let lineStyle = context.state.currentStyle; 21 | if (lineStyle && context.state.textScale) { 22 | lineStyle = _.cloneDeep(lineStyle); 23 | const txtStyle = lineStyle.textProps?.layerText.textStyleRange?.[0]?.textStyle || {}; 24 | if (typeof txtStyle.size === "number") { 25 | txtStyle.size *= context.state.textScale / 100; 26 | } 27 | if (typeof txtStyle.leading === "number" && txtStyle.leading) { 28 | txtStyle.leading *= context.state.textScale / 100; 29 | } 30 | } 31 | const pointText = context.state.pastePointText; 32 | createTextLayerInSelection(line.text, lineStyle, pointText, (ok) => { 33 | if (ok) context.dispatch({ type: "nextLine", add: true }); 34 | }); 35 | }; 36 | 37 | const insertStyledText = () => { 38 | let lineStyle = context.state.currentStyle; 39 | if (lineStyle && context.state.textScale) { 40 | lineStyle = _.cloneDeep(lineStyle); 41 | const txtStyle = lineStyle.textProps?.layerText.textStyleRange?.[0]?.textStyle || {}; 42 | if (typeof txtStyle.size === "number") { 43 | txtStyle.size *= context.state.textScale / 100; 44 | } 45 | if (typeof txtStyle.leading === "number" && txtStyle.leading) { 46 | txtStyle.leading *= context.state.textScale / 100; 47 | } 48 | } 49 | setActiveLayerText(line.text, lineStyle, (ok) => { 50 | if (ok) context.dispatch({ type: "nextLine", add: true }); 51 | }); 52 | }; 53 | 54 | const currentLineClick = () => { 55 | if (line.rawIndex === void 0) return; 56 | scrollToLine(line.rawIndex); 57 | }; 58 | 59 | const setTextScale = (scale) => { 60 | context.dispatch({ type: "setTextScale", scale }); 61 | }; 62 | const focusScale = () => { 63 | if (!context.state.textScale) setTextScale(100); 64 | }; 65 | const blurScale = () => { 66 | if (context.state.textScale === 100) setTextScale(null); 67 | }; 68 | 69 | return ( 70 | 71 |
72 | 75 | 78 |
79 | 82 | 85 |
86 |
87 |
88 |
89 | 92 | 95 |
96 |
97 |
98 |
99 | {locale.previewLine}: {line.index || "—"}, {locale.previewStyle}: {style.name || "—"}, {locale.previewTextScale}: 100 |
101 | setTextScale(e.target.value)} onFocus={focusScale} onBlur={blurScale} className="topcoat-text-input" /> 102 | % 103 |
104 |
105 |
106 | 107 |
108 |
109 |
${line.text || ""}` }}>
110 |
111 |
112 |
113 | ); 114 | }); 115 | 116 | export default PreviewBlock; 117 | -------------------------------------------------------------------------------- /app_src/components/previewBlock/previewBlock.scss: -------------------------------------------------------------------------------- 1 | .preview-block { 2 | box-sizing: border-box; 3 | padding: 10px 10px 0; 4 | } 5 | .preview-top { 6 | margin-bottom: 10px; 7 | justify-content: space-between; 8 | align-items: center; 9 | display: flex; 10 | .preview-top_big-btn { 11 | margin-right: 10px; 12 | flex-grow: 2; 13 | width: 50%; 14 | } 15 | .preview-top_change-size-cont { 16 | display: flex; 17 | BUTTON { 18 | border-radius: 0; 19 | } 20 | BUTTON + BUTTON { 21 | margin-left: -1px; 22 | } 23 | BUTTON:first-child { 24 | border-radius: 4px 0 0 4px; 25 | } 26 | BUTTON:last-child { 27 | border-radius: 0 4px 4px 0; 28 | } 29 | } 30 | } 31 | 32 | .preview-bottom { 33 | justify-content: space-between; 34 | display: flex; 35 | } 36 | // .preview-nav { 37 | // flex: 0 0 auto; 38 | // width: 40px; 39 | // & > BUTTON { 40 | // height: 34px; 41 | // width: 30px; 42 | // } 43 | // & > BUTTON + BUTTON { 44 | // margin-top: 4px; 45 | // } 46 | // } 47 | .preview-current { 48 | -webkit-font-smoothing: antialiased; 49 | border: 1px solid rgba(#000, 0.3); 50 | box-sizing: border-box; 51 | border-radius: 6px; 52 | height: 72px; 53 | flex-grow: 2; 54 | display: flex; 55 | flex-direction: column; 56 | justify-content: space-between; 57 | } 58 | .preview-line-info { 59 | border-bottom: 1px solid rgba(#fff, 0.12); 60 | background: rgba(#fff, 0.06); 61 | padding: 2px 4px 2px 8px; 62 | flex: 0 0 auto; 63 | display: flex; 64 | align-items: center; 65 | justify-content: space-between; 66 | .light-theme & { 67 | border-bottom: 1px solid rgba(#000, 0.12); 68 | background: rgba(#000, 0.06); 69 | } 70 | } 71 | .preview-line-info-text { 72 | transition: opacity 0.2s linear; 73 | line-height: 16px; 74 | font-size: 11px; 75 | flex-grow: 2; 76 | opacity: 0.5; 77 | & > B { 78 | font-size: 12px; 79 | } 80 | .preview-current:hover & { 81 | opacity: 0.8; 82 | } 83 | } 84 | .preview-line-style-name { 85 | text-overflow: ellipsis; 86 | vertical-align: bottom; 87 | display: inline-block; 88 | white-space: nowrap; 89 | overflow: hidden; 90 | max-width: 82px; 91 | } 92 | .preview-line-info-color { 93 | box-shadow: 0 0 4px 1px rgba(#000, 0.4); 94 | border: 1px solid rgba(#fff, 0.8); 95 | display: inline-block; 96 | position: relative; 97 | border-radius: 50%; 98 | margin-left: 8px; 99 | height: 10px; 100 | width: 10px; 101 | top: 1px; 102 | } 103 | .preview-line-scale { 104 | justify-content: space-between; 105 | vertical-align: bottom; 106 | display: inline-flex; 107 | align-items: center; 108 | margin-left: 3px; 109 | INPUT { 110 | appearance: textfield; 111 | text-align: center; 112 | line-height: 12px; 113 | font-size: 12px; 114 | padding: 0 2px; 115 | margin: 0 3px; 116 | width: 26px; 117 | &::-webkit-inner-spin-button, 118 | &::-webkit-outer-spin-button { 119 | -webkit-appearance: none; 120 | } 121 | } 122 | } 123 | .preview-line-info-actions { 124 | flex: 0 0 auto; 125 | height: 16px; 126 | & > SVG { 127 | cursor: pointer; 128 | opacity: 0.7; 129 | &:hover { 130 | color: #fff; 131 | opacity: 1; 132 | } 133 | } 134 | } 135 | .preview-line-text { 136 | word-break: break-word; 137 | letter-spacing: 1px; 138 | padding: 5px 8px; 139 | font-size: 10px !important; 140 | overflow: auto; 141 | flex-grow: 2; 142 | } 143 | 144 | @media (max-height: 450px) { 145 | .preview-block { 146 | display: none; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /app_src/components/stylesBlock/stylesBlock.jsx: -------------------------------------------------------------------------------- 1 | import "./stylesBlock.scss"; 2 | 3 | import React from "react"; 4 | import PropTypes from "prop-types"; 5 | import { ReactSortable } from "react-sortablejs"; 6 | import { FiArrowRightCircle, FiPlus, FiFolderPlus, FiChevronDown, FiChevronUp, FiCopy } from "react-icons/fi"; 7 | import { MdEdit, MdLock } from "react-icons/md"; 8 | import { CiExport } from "react-icons/ci"; 9 | 10 | import config from "../../config"; 11 | import { locale, getActiveLayerText, setActiveLayerText, rgbToHex, getStyleObject } from "../../utils"; 12 | import { useContext } from "../../context"; 13 | 14 | const StylesBlock = React.memo(function StylesBlock() { 15 | const context = useContext(); 16 | const unsortedStyles = context.state.styles.filter((s) => !s.folder); 17 | return ( 18 | 19 |
20 | {context.state.folders.length || context.state.styles.length ? ( 21 | 22 | {unsortedStyles.length > 0 && } 23 | context.dispatch({ type: "setFolders", data })}> 24 | {context.state.folders.map((folder) => ( 25 | 26 | ))} 27 | 28 | 29 | ) : ( 30 |
31 | {locale.addStylesHint} 32 |
33 | )} 34 |
35 |
36 | 39 | 42 |
43 |
44 | ); 45 | }); 46 | 47 | const FolderItem = React.memo(function FolderItem(props) { 48 | const context = useContext(); 49 | const openFolder = (e) => { 50 | e.stopPropagation(); 51 | context.dispatch({ type: "setModal", modal: "editFolder", data: props.data }); 52 | }; 53 | const sortFolderStyles = (folderStyles) => { 54 | let styles = props.data.id ? context.state.styles.filter((s) => s.folder !== props.data.id) : context.state.styles.filter((s) => !!s.folder); 55 | styles = styles.concat(folderStyles); 56 | context.dispatch({ type: "setStyles", data: styles }); 57 | }; 58 | const styles = props.data.id ? context.state.styles.filter((s) => s.folder === props.data.id) : context.state.styles.filter((s) => !s.folder); 59 | 60 | const exportFolder = (e) => { 61 | e.stopPropagation(); 62 | const pathSelect = window.cep.fs.showSaveDialogEx(false, false, ["json"], props.data.name + ".json"); 63 | if (!pathSelect?.data) return false; 64 | const exportedFolder = {}; 65 | exportedFolder.name = props.data.name; 66 | const exportedStyles = []; 67 | exportedStyles.push( 68 | ...styles.map((style) => { 69 | return { 70 | name: style.name, 71 | textProps: style.textProps, 72 | prefixes: style.prefixes, 73 | prefixColor: style.prefixColor, 74 | stroke: style.stroke, 75 | }; 76 | }) 77 | ); 78 | exportedFolder.exportedStyles = exportedStyles; 79 | 80 | window.cep.fs.writeFile(pathSelect.data, JSON.stringify(exportedFolder)); 81 | }; 82 | const duplicateFolder = (e) => { 83 | e.stopPropagation(); 84 | context.dispatch({ type: "duplicateFolder", data: props.data }); 85 | }; 86 | 87 | const isOpen = props.data.id ? context.state.openFolders.includes(props.data.id) : context.state.openFolders.includes("unsorted"); 88 | const hasActive = context.state.currentStyleId ? !!styles.find((s) => s.id === context.state.currentStyleId) : false; 89 | return ( 90 |
91 |
context.dispatch({ type: "toggleFolder", id: props.data.id })}> 92 |
{isOpen ? : }
93 |
94 | {hasActive ? {props.data.name} : {props.data.name}} 95 | ({styles.length}) 96 |
97 |
98 | {props.data.id ? ( 99 | <> 100 | 103 | 106 | 109 | 110 | ) : ( 111 | 112 | )} 113 |
114 |
115 | {isOpen && ( 116 |
117 | {styles.length ? ( 118 | 119 | {styles.map((style) => ( 120 | context.dispatch({ type: "setCurrentStyleId", id: style.id })} openStyle={() => context.dispatch({ type: "setModal", modal: "editStyle", data: style })} style={style} /> 121 | ))} 122 | 123 | ) : ( 124 |
125 | {locale.noStylesInfolder} 126 |
127 | )} 128 |
129 | )} 130 |
131 | ); 132 | }); 133 | FolderItem.propTypes = { 134 | data: PropTypes.object.isRequired, 135 | }; 136 | 137 | const StyleItem = React.memo(function StyleItem(props) { 138 | const textStyle = props.style.textProps.layerText.textStyleRange[0]?.textStyle || {}; 139 | const styleObject = getStyleObject(textStyle); 140 | const context = useContext(); 141 | const openStyle = (e) => { 142 | e.stopPropagation(); 143 | props.openStyle(); 144 | }; 145 | const insertStyle = (e) => { 146 | e.stopPropagation(); 147 | if (e.ctrlKey) { 148 | getActiveLayerText((data) => { 149 | textStyle.size = data.textProps.layerText.textStyleRange[0].textStyle.size; 150 | setActiveLayerText("", props.style); 151 | }); 152 | } else { 153 | setActiveLayerText("", props.style); 154 | } 155 | }; 156 | const duplicateStyle = (e) => { 157 | e.stopPropagation(); 158 | context.dispatch({ type: "duplicateStyle", data: props.style }); 159 | }; 160 | return ( 161 |
162 |
163 |
164 | {!!props.style.prefixes.length && ( 165 |
166 |
167 |
168 | )} 169 |
170 |
${props.style.name}` }}>
171 |
172 | 175 | 178 | 181 |
182 |
183 | ); 184 | }); 185 | StyleItem.propTypes = { 186 | selectStyle: PropTypes.func.isRequired, 187 | openStyle: PropTypes.func.isRequired, 188 | style: PropTypes.object.isRequired, 189 | active: PropTypes.bool, 190 | }; 191 | 192 | export default StylesBlock; 193 | -------------------------------------------------------------------------------- /app_src/components/stylesBlock/stylesBlock.scss: -------------------------------------------------------------------------------- 1 | .styles-block { 2 | position: relative; 3 | justify-content: space-between; 4 | flex-direction: column; 5 | display: flex; 6 | } 7 | .folders-list { 8 | position: relative; 9 | overflow-y: auto; 10 | padding: 10px; 11 | flex-grow: 2; 12 | } 13 | .styles-empty { 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | position: absolute; 18 | height: 100%; 19 | width: 100%; 20 | left: 0; 21 | top: 0; 22 | & > SPAN { 23 | text-align: center; 24 | font-size: 16px; 25 | margin: 40px; 26 | opacity: 0.6; 27 | } 28 | } 29 | .style-add { 30 | display: flex; 31 | align-items: center; 32 | justify-content: space-between; 33 | flex: 0 0 auto; 34 | padding: 10px; 35 | BUTTON { 36 | width: 50%; 37 | } 38 | BUTTON + BUTTON { 39 | margin-left: 8px; 40 | } 41 | } 42 | 43 | .folder-item { 44 | background: rgba(#000, 0.15); 45 | margin-bottom: 8px; 46 | } 47 | .folder-header { 48 | display: flex; 49 | align-items: center; 50 | justify-content: space-between; 51 | padding: 2px 4px; 52 | opacity: 0.6; 53 | &:hover { 54 | outline: 1px solid rgba(#fff, 0.5); 55 | opacity: 0.8; 56 | } 57 | .light-theme &:hover { 58 | outline: 1px solid rgba(#000, 0.5); 59 | } 60 | .folder-item.m-open & { 61 | opacity: 1; 62 | } 63 | } 64 | .folder-marker { 65 | flex: 0 0 auto; 66 | height: 18px; 67 | } 68 | .folder-title { 69 | max-width: calc(100% - 50px); 70 | text-overflow: ellipsis; 71 | white-space: nowrap; 72 | overflow: hidden; 73 | font-size: 14px; 74 | flex-grow: 2; 75 | SPAN { 76 | opacity: 0.7; 77 | } 78 | } 79 | .folder-styles-count { 80 | margin-left: 4px; 81 | font-size: 12px; 82 | opacity: 0.3; 83 | } 84 | .folder-actions { 85 | flex: 0 0 auto; 86 | flex-direction: row-reverse; 87 | width: 36px; 88 | display: flex; 89 | BUTTON { 90 | height: 18px; 91 | width: 18px; 92 | padding: 0; 93 | } 94 | } 95 | .folder-locked { 96 | height: 18px; 97 | opacity: 0.3; 98 | } 99 | .folder-styles { 100 | padding: 6px 4px 2px; 101 | } 102 | .folder-styles-empty { 103 | padding: 2px 10px 6px; 104 | text-align: center; 105 | font-size: 14px; 106 | opacity: 0.4; 107 | } 108 | 109 | .styles-list { 110 | justify-content: space-between; 111 | flex-wrap: wrap; 112 | display: flex; 113 | } 114 | .style-item { 115 | justify-content: space-between; 116 | align-items: center; 117 | display: flex; 118 | box-sizing: border-box; 119 | padding: 4px 4px 4px 6px; 120 | margin-bottom: 4px; 121 | opacity: 0.8; 122 | width: 100%; 123 | cursor: pointer; 124 | &:hover { 125 | outline: 1px solid rgba(#fff, 0.5); 126 | opacity: 1; 127 | } 128 | .light-theme &:hover { 129 | outline: 1px solid rgba(#000, 0.5); 130 | } 131 | &.m-current { 132 | outline: 1px solid #134f7f !important; 133 | background: #288edf; 134 | color: #fff; 135 | opacity: 1; 136 | } 137 | } 138 | @media screen and (min-width: 423px) { 139 | .style-item { 140 | width: calc(50% - 2px); 141 | } 142 | } 143 | .style-marker { 144 | flex: 0 0 auto; 145 | margin-right: 6px; 146 | } 147 | .style-color { 148 | box-shadow: 0 0 4px 1px rgba(#000, 0.4); 149 | border: 1px solid rgba(#fff, 0.8); 150 | border-radius: 50%; 151 | height: 8px; 152 | width: 8px; 153 | } 154 | .style-prefix-color { 155 | box-shadow: 0 0 4px 1px rgba(#000, 0.4); 156 | background: #fff; 157 | margin-top: 3px; 158 | padding: 1px; 159 | DIV { 160 | opacity: 0.9; 161 | height: 5px; 162 | } 163 | } 164 | .style-name { 165 | -webkit-font-smoothing: antialiased; 166 | max-width: calc(100% - 70px); 167 | text-overflow: ellipsis; 168 | letter-spacing: 1px; 169 | white-space: nowrap; 170 | overflow: hidden; 171 | font-size: 14px; 172 | flex-grow: 2; 173 | } 174 | .style-actions { 175 | flex: 0 0 auto; 176 | display: flex; 177 | BUTTON { 178 | height: 20px; 179 | width: 20px; 180 | padding: 0; 181 | } 182 | BUTTON + BUTTON { 183 | margin-left: 3px; 184 | } 185 | } 186 | 187 | @media (max-height: 450px) { 188 | .style-btn-list { 189 | display: none; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /app_src/components/textBlock/textBlock.jsx: -------------------------------------------------------------------------------- 1 | import "./textBlock.scss"; 2 | 3 | import React from "react"; 4 | import { FiArrowRightCircle, FiTarget } from "react-icons/fi"; 5 | 6 | import config from "../../config"; 7 | import { locale, setActiveLayerText, resizeTextArea, scrollToLine, openFile } from "../../utils"; 8 | import { useContext } from "../../context"; 9 | 10 | const TextBlock = React.memo(function TextBlock() { 11 | const context = useContext(); 12 | const [focused, setFocused] = React.useState(false); 13 | const lastOpenedPath = React.useRef(null); 14 | React.useEffect(resizeTextArea); 15 | React.useEffect(() => { 16 | scrollToLine(context.state.currentLineIndex, 1000); 17 | }, []); 18 | 19 | React.useEffect(() => { 20 | let pageIndex = 0; 21 | let currentPage = 0; 22 | for (const line of context.state.lines) { 23 | if (line.ignore) { 24 | const page = line.rawText.match(/Page ([0-9]+)/); 25 | if (page && context.state.images[page[1] - 1]) { 26 | const img = context.state.images[page[1] - 1]; 27 | currentPage = context.state.images.indexOf(img); 28 | } 29 | } 30 | if (line.rawIndex === context.state.currentLineIndex) { 31 | pageIndex = currentPage; 32 | break; 33 | } 34 | } 35 | const image = context.state.images[pageIndex]; 36 | if (image && image.path !== lastOpenedPath.current) { 37 | openFile(image.path, context.state.autoClosePSD); 38 | lastOpenedPath.current = image.path; 39 | } 40 | }, [context.state.currentLineIndex, context.state.autoClosePSD, context.state.images]); 41 | 42 | let currentPage = 0; 43 | 44 | const classNameLine = (line) => { 45 | let style = "text-line"; 46 | if (line.ignore) { 47 | style += " m-empty"; 48 | } 49 | if (context.state.currentLineIndex === line.rawIndex) { 50 | style += " m-current"; 51 | } 52 | if (line.rawText.match("Page [0-9]+")) { 53 | style += " m-page"; 54 | } 55 | return style; 56 | }; 57 | 58 | const getTextLineNum = (line) => { 59 | if (line.ignore) { 60 | const page = line.rawText.match("Page ([0-9]+)"); 61 | if (page && context.state.images[page[1] - 1]) { 62 | const currentImage = context.state.images[page[1] - 1]; 63 | currentPage = context.state.images.indexOf(currentImage); 64 | return currentImage.name; 65 | } 66 | return " "; 67 | } 68 | return line.index; 69 | }; 70 | 71 | return ( 72 | 73 |
74 | {context.state.lines.map((line) => ( 75 |
76 |
{getTextLineNum(line)}
77 |
78 | {line.ignore ? " " : context.dispatch({ type: "setCurrentLineIndex", index: line.rawIndex })} />} 79 |
80 |
81 | {line.ignorePrefix ? ( 82 | 83 | {line.ignorePrefix} 84 | {line.rawText.replace(line.ignorePrefix, "")} 85 | 86 | ) : line.stylePrefix ? ( 87 | 88 | 89 | {line.stylePrefix} 90 | 91 | {line.rawText.replace(line.stylePrefix, "")} 92 | 93 | ) : ( 94 | {line.rawText || " "} 95 | )} 96 |
97 |
98 | {line.ignore ? " " : ( 99 | { 102 | setActiveLayerText(line.text); 103 | context.dispatch({ type: "nextLine", add: true }); 104 | }} 105 | /> 106 | )} 107 |
108 |
109 | ))} 110 |
111 |