diff --git a/bin/scaffold b/bin/scaffold new file mode 100755 index 0000000..6e9a69a --- /dev/null +++ b/bin/scaffold @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +const SimpleScaffold = require('simple-scaffold').default +const path = require('path') + +const args = process.argv.slice(2) + +function pascalCase(str) { + str = String(str) + return str[0].toUpperCase() + str.slice(1).replace(/_([a-z])/gi, (_, letter) => letter.toUpperCase()) +} + +const [type, name] = args +const outputMap = { + component: path.join('src', 'components') +} + +new SimpleScaffold({ + name, + templates: [path.join(process.cwd(), 'templates', type, '**', '*')], + output: path.join(process.cwd(), outputMap[type] || ''), + locals: { + clsName: pascalCase(name) + }, + createSubfolder: false, +}).run() diff --git a/package.json b/package.json index bc94551..2ecf11d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "babel-loader": "^8.0.5", "chrome-url-loader": "^1.1.1", "copy-webpack-plugin": "^5.0.2", + "simple-scaffold": "^0.5.0", "webpack": "^4.29.6", "webpack-cli": "^3.3.0" }, diff --git a/src/background.js b/src/background.js index b0bf4df..04470d1 100644 --- a/src/background.js +++ b/src/background.js @@ -1,13 +1,19 @@ import 'chrome' import '@babel/polyfill' -import { createImageElement, getImageSize } from './lib/resizer' import { assert, Message } from './lib/utils' -chrome.contextMenus.create({ - contexts: ['image'], - title: 'Resize to 50%', - onclick: async (info, tab) => { - assert(info.mediaType === 'image', 'expected image') - chrome.tabs.sendMessage(tab.id, new Message('select_image', { src: info.srcUrl })) +function item(title, ratio) { + return { + contexts: ['image'], + title, + onclick: async (info, tab) => { + assert(info.mediaType === 'image', 'expected image') + chrome.tabs.sendMessage(tab.id, new Message('select_image', { src: info.srcUrl, ratio })) + } } -}) +} + +chrome.contextMenus.create(item('Resize to 25%', 0.25)) +chrome.contextMenus.create(item('Resize to 50%', 0.5)) +chrome.contextMenus.create(item('Resize to 75%', 0.75)) +chrome.contextMenus.create(item('Custom resize...')) diff --git a/src/components/image_canvas.jsx b/src/components/image_canvas.jsx new file mode 100644 index 0000000..47f2e3c --- /dev/null +++ b/src/components/image_canvas.jsx @@ -0,0 +1,54 @@ +import * as React from 'react' +import propTypes from 'prop-types' +import styled from 'styled-components' +import { Size } from '../lib/resizer' + +const Container = styled.div`` + +export default class ImageCanvas extends React.Component { + constructor(props) { + super(props) + // this.state = { + // // + // } + } + + componentDidMount() { + this.redraw() + } + + componentWillUpdate(props) { + this.redraw({ newSize: props.size }) + } + + canvasCtx() { + return this.canvasRef && this.canvasRef.getContext('2d') + } + + redraw({ newSize } = {}) { + const { image } = this.props + let { size } = this.props + size = newSize || size + const ctx = this.canvasCtx() + this.canvasRef.width = size.width + this.canvasRef.height = size.height + ctx.clearRect(0, 0, size.width, size.height) + ctx.drawImage(image, 0, 0, size.width, size.height) + } + + render() { + return ( + + this.canvasRef = ref}> + + ) + } +} + +ImageCanvas.propTypes = { + image: propTypes.instanceOf(Image).isRequired, + size: propTypes.oneOfType([ + propTypes.instanceOf(Size), + propTypes.number, + ]) +} diff --git a/src/components/upload_resizer.jsx b/src/components/upload_resizer.jsx index 4152606..354f774 100644 --- a/src/components/upload_resizer.jsx +++ b/src/components/upload_resizer.jsx @@ -3,6 +3,7 @@ import propTypes from 'prop-types' import styled from 'styled-components' import { spacing, MAX_Z_INDEX } from '../lib/cssvars' import { Size, createImageElement, getImageSize } from '../lib/resizer' +import ImageCanvas from './image_canvas.jsx' const Overlay = styled.div` position: fixed; @@ -12,6 +13,8 @@ const Overlay = styled.div` bottom: 0; background: rgba(0, 0 , 0, 0.4); z-index: ${MAX_Z_INDEX}; + font-family: Helvetica, Arial, sans-serif; + font-size: 14px; &, & * { box-sizing: border-box; @@ -32,61 +35,115 @@ const Container = styled.div` const boxPadding = spacing * 2; const boxMargin = spacing * 2; const Box = styled.div` - min-width: 500px; max-width: calc(100vw - ${boxMargin * 2}px); + max-height: calc(100vh - ${boxMargin * 2}px); + min-width: 500px; margin: ${boxMargin}px; + padding: ${boxMargin / 2}px; background: white; border-radius: 3px; - padding: ${boxPadding}px; + overflow: auto; ` +const TopBar = styled.div`` +const BottomBar = styled.div`` + const BoxContent = styled.div` - max-height: calc(100vh - ${boxMargin * 2}px - ${boxPadding * 2}px); - overflow-y: auto; + max-width: calc(100vw - ${boxMargin * 2}px); + max-height: calc(100vh - 200px); + overflow: auto; ` -const ImageContainer = styled.div` - max-width: 100%; - overflow: auto; +const ImageContainer = styled.div`` + +const Metadata = styled.div` + display: flex; + margin-top: ${spacing * 2}px; +` + +const Chip = styled.span` + display: inline-block; + background: #eee; + border-radius: 1em; + padding: 6px 9px; + font-size: 0.95em; + &:not(:last-child) { + margin-right: ${spacing}px; + } ` export default class UploadResizer extends React.Component { constructor(props) { super(props) this.state = { - size: new Size(), + imageSize: new Size(), + customSize: new Size(), + image: null, } } async componentWillMount() { - const img = await createImageElement(this.props.image) - this.setState({ size: getImageSize(img) }) + const image = await createImageElement(this.props.image) + const imageSize = getImageSize(image); + const customSize = imageSize.resize(this.props.ratio) + this.setState({ image, imageSize, customSize }) } render() { - const { size } = this.state + const { imageSize, customSize, image } = this.state + const { onClose } = this.props return ( - - + onClose && onClose(e)}> + e.stopPropagation()}> - - Image Preview: - - - + + Final Size Preview + - - Original Size: {size.width}x{size.height} - + + + {image ? : 'Loading...'} + + + + + Original Size: {imageSize.width}x{imageSize.height} + + + New Size: {customSize.width}x{customSize.height} + this.changeRatio(e.target.value)} + defaultValue={this.props.ratio}> + {[0.25, 0.5, 0.75].map((ratio) => { + return ( + + {ratio * 100}% of original + + ) + })} + Original Size + + + ) } + + changeRatio(ratio) { + const { imageSize } = this.state + const customSize = imageSize.resize(Number(ratio)) + this.setState({ customSize }) + } } UploadResizer.propTypes = { image: propTypes.string.isRequired, + ratio: propTypes.oneOfType([ + propTypes.instanceOf(Size), + propTypes.number, + ]), + onClose: propTypes.func, } diff --git a/src/frontend.jsx b/src/frontend.jsx index 1251d4c..ed3f785 100644 --- a/src/frontend.jsx +++ b/src/frontend.jsx @@ -6,12 +6,18 @@ import UploadResizer from './components/upload_resizer' const __EL_ID = '__upload_resizer_box' -function bootstrap(imgSrc) { +function bootstrap(imgSrc, ratio) { removeExistingBootstrap() const el = document.createElement('div') el.id = __EL_ID document.body.appendChild(el) - ReactDOM.render(, el) + const mainComponent = ( + removeExistingBootstrap()} /> + ) + ReactDOM.render(mainComponent, el) } function removeExistingBootstrap() { @@ -24,7 +30,7 @@ function removeExistingBootstrap() { chrome.runtime.onMessage.addListener((message, sender, respond) => { console.log('got msg', message, sender) if (message && message.action === 'select_image') { - bootstrap(message.payload.src) + bootstrap(message.payload.src, message.payload.ratio) } return true }) diff --git a/src/lib/resizer.js b/src/lib/resizer.js index 1474594..663ed53 100644 --- a/src/lib/resizer.js +++ b/src/lib/resizer.js @@ -32,4 +32,17 @@ export class Size { this.height = height } } + + resize(ratio) { + if (ratio === undefined) { + return new Size(this.width, this.height) + } + if (typeof ratio === 'number') { + return new Size(this.width * ratio, this.height * ratio) + } + if (ratio instanceof Size) { + return ratio + } + throw new Error('bad ratio value') + } } diff --git a/templates/component/{{name}}.jsx b/templates/component/{{name}}.jsx new file mode 100644 index 0000000..b44b44d --- /dev/null +++ b/templates/component/{{name}}.jsx @@ -0,0 +1,26 @@ +import * as React from 'react' +import propTypes from 'prop-types' +import styled from 'styled-components' + +const Container = styled.div`` + +export default class {{clsName}} extends React.Component { + constructor(props) { + super(props) + // this.state = { + // // + // } + } + + render() { + return ( + + {{name}} component + + ) + } +} + +{{clsName}}.propTypes = { + // +}; diff --git a/yarn.lock b/yarn.lock index 9b59d82..194be9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -950,6 +950,18 @@ arr-union@^3.1.0: resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= +array-back@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-2.0.0.tgz#6877471d51ecc9c9bfa6136fb6c7d5fe69748022" + integrity sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw== + dependencies: + typical "^2.6.1" + +array-back@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + array-union@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" @@ -1350,7 +1362,27 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -commander@^2.19.0: +command-line-args@^5.0.2: + version "5.1.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.1.1.tgz#88e793e5bb3ceb30754a86863f0401ac92fd369a" + integrity sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg== + dependencies: + array-back "^3.0.1" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-usage@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-5.0.5.tgz#5f25933ffe6dedd983c635d38a21d7e623fda357" + integrity sha512-d8NrGylA5oCXSbGoKz05FkehDAzSmIm4K03S5VDh4d5lZAtTWfc3D1RuETtuQCn8129nYfJfDdF7P/lwcz1BlA== + dependencies: + array-back "^2.0.0" + chalk "^2.4.1" + table-layout "^0.4.3" + typical "^2.6.1" + +commander@^2.19.0, commander@~2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== @@ -1575,7 +1607,7 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= -deep-extend@^0.6.0: +deep-extend@^0.6.0, deep-extend@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== @@ -1877,6 +1909,13 @@ find-cache-dir@^2.0.0: make-dir "^2.0.0" pkg-dir "^3.0.0" +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -2050,6 +2089,17 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== +handlebars@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.1.tgz#6e4e41c18ebe7719ae4d38e5aca3d32fa3dd23d3" + integrity sha512-3Zhi6C0euYZL5sM0Zcy7lInLXKQ+YLcF/olbN010mzGQ4XVm50JeyBnMqofHh696GrciGruC7kCcApPDJvVgwA== + dependencies: + neo-async "^2.6.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2495,6 +2545,16 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + +lodash.padend@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e" + integrity sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4= + lodash@^3.5.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" @@ -2641,6 +2701,11 @@ minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + minipass@^2.2.1, minipass@^2.3.4: version "2.3.5" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" @@ -2745,7 +2810,7 @@ needle@^2.2.1: iconv-lite "^0.4.4" sax "^1.2.4" -neo-async@^2.5.0: +neo-async@^2.5.0, neo-async@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835" integrity sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA== @@ -2910,6 +2975,14 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + os-browserify@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" @@ -3265,6 +3338,11 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +reduce-flatten@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327" + integrity sha1-JYx479FT3fk8tWEjf2EYTzaW4yc= + regenerate-unicode-properties@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.2.tgz#7b38faa296252376d363558cfbda90c9ce709662" @@ -3527,6 +3605,16 @@ signal-exit@^3.0.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= +simple-scaffold@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/simple-scaffold/-/simple-scaffold-0.5.0.tgz#4f9f2af7275c8041923b6bdd5385f6f6cabec12c" + integrity sha512-J5w6mt8C5OlJY4fFrKf04cHPGsvjMZGUqTnOR+3XZVoS2Iha5+nMGmfxa4W37JSO6IF//lR7JtW+NhIKcHfqnQ== + dependencies: + command-line-args "^5.0.2" + command-line-usage "^5.0.5" + glob "^7.1.3" + handlebars "^4.1.0" + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" @@ -3744,6 +3832,17 @@ supports-color@^5.3.0, supports-color@^5.5.0: dependencies: has-flag "^3.0.0" +table-layout@^0.4.3: + version "0.4.4" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.4.tgz#bc5398b2a05e58b67b05dd9238354b89ef27be0f" + integrity sha512-uNaR3SRMJwfdp9OUr36eyEi6LLsbcTqTO/hfTsNviKsNeyMBPICJCC7QXRF3+07bAP6FRwA8rczJPBqXDc0CkQ== + dependencies: + array-back "^2.0.0" + deep-extend "~0.6.0" + lodash.padend "^4.6.1" + typical "^2.6.1" + wordwrapjs "^3.0.0" + tapable@^1.0.0, tapable@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e" @@ -3855,6 +3954,24 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= +typical@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d" + integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0= + +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +uglify-js@^3.1.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.5.4.tgz#4a64d57f590e20a898ba057f838dcdfb67a939b9" + integrity sha512-GpKo28q/7Bm5BcX9vOu4S46FwisbPbAmkkqPnGIpKvKTM96I85N6XHQV+k4I6FA2wxgLhcsSyHoNhzucwCflvA== + dependencies: + commander "~2.20.0" + source-map "~0.6.1" + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -4075,6 +4192,19 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +wordwrapjs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-3.0.0.tgz#c94c372894cadc6feb1a66bff64e1d9af92c5d1e" + integrity sha512-mO8XtqyPvykVCsrwj5MlOVWvSnCdT+C+QVbm6blradR7JExAhbkZ7hZ9A+9NUtwzSqrlUo9a67ws0EiILrvRpw== + dependencies: + reduce-flatten "^1.0.1" + typical "^2.6.1" + worker-farm@^1.5.2: version "1.6.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0"