├── .gitignore ├── images ├── banner.png ├── logo.svg └── m3.svg ├── static ├── cover.jpeg └── favicon.png ├── .eslintrc.js ├── package.json ├── README.md ├── LICENSE.md ├── index.html ├── styles └── main.css └── scripts └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | .DS_Store -------------------------------------------------------------------------------- /images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limhenry/gde-badge-generator/HEAD/images/banner.png -------------------------------------------------------------------------------- /static/cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limhenry/gde-badge-generator/HEAD/static/cover.jpeg -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/limhenry/gde-badge-generator/HEAD/static/favicon.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es2021': true, 5 | 'node': true, 6 | }, 7 | 'extends': ['eslint:recommended', 'google'], 8 | 'overrides': [ 9 | ], 10 | 'parserOptions': { 11 | 'ecmaVersion': 'latest', 12 | 'sourceType': 'module', 13 | }, 14 | 'rules': { 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gde-badge-generator", 3 | "version": "2.0.0", 4 | "description": "", 5 | "devDependencies": { 6 | "eslint": "^8.57.1", 7 | "eslint-config-google": "^0.14.0", 8 | "vite": "^7.2.4" 9 | }, 10 | "source": "src/index.html", 11 | "browserslist": "> 0.5%, last 2 versions, not dead", 12 | "type": "module", 13 | "scripts": { 14 | "dev": "vite", 15 | "build": "vite build" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/limhenry/gde-badge-generator.git" 20 | }, 21 | "author": "limhenry", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/limhenry/gde-badge-generator/issues" 25 | }, 26 | "homepage": "https://github.com/limhenry/gde-badge-generator#readme" 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GDE Profile Badge Generator 2 | 3 |  4 | 5 | > Generate your GDE Profile Badge directly in your browser. No extra software required! 6 | 7 | 8 | ## Features 9 | 10 | :white_check_mark: No need to install Photoshop! 11 | 12 | :white_check_mark: Generate the profile badge in full resolution 13 | 14 | :white_check_mark: Customizable category name (HTML's ``) 15 | 16 | :white_check_mark: Crop image to square, circle, or [Material 3 shape](https://gde-badge.limhenry.xyz/?material=true) 17 | 18 | :white_check_mark: Gridline for easier image composition 19 | 20 | :white_check_mark: Open image by drag and drop 21 | 22 | :blowfish: Open image from Clipboard (Async Clipboard API) 23 | 24 | 25 | ## License 26 | 27 | This project is published under the [MIT license](/LICENSE.md). 28 | 29 | This project is not endorsed and/or supported by Google, the corporation. 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2025 Henry Lim 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /images/m3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GDE Profile Badge Generator 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | GDE Profile Badge Generator 24 | 25 | 26 | 27 | 28 | Drop your image here 29 | 30 | 31 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | Download 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /styles/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #1e88e5; 3 | } 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | html, body { 10 | margin: 0; 11 | font-family: 'Google Sans', sans-serif; 12 | height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | header { 18 | display: grid; 19 | align-items: center; 20 | height: 64px; 21 | min-height: 64px; 22 | font-size: 1.25em; 23 | padding: 0 1.5em; 24 | font-weight: 600; 25 | background-color: #e3f2fd; 26 | grid-auto-flow: column; 27 | justify-content: flex-start; 28 | grid-gap: 0.5em; 29 | 30 | img { 31 | width: 42px; 32 | height: 20px; 33 | } 34 | } 35 | 36 | main { 37 | display: grid; 38 | grid-template-columns: 400px 1fr; 39 | flex: 1; 40 | overflow: hidden; 41 | position: relative; 42 | 43 | .drop { 44 | position: absolute; 45 | background-color: rgba(0, 0, 0, 0.5); 46 | width: 100%; 47 | top: 0; 48 | bottom: 0; 49 | z-index: 1; 50 | padding: 1em; 51 | opacity: 0; 52 | pointer-events: none; 53 | 54 | &[active] { 55 | opacity: 1; 56 | } 57 | 58 | :is(.drop-border) { 59 | border: 0.2em dashed #fff; 60 | height: 100%; 61 | border-radius: 1em; 62 | display: grid; 63 | place-items: center; 64 | color: #fff; 65 | font-size: 1.5em; 66 | } 67 | } 68 | 69 | aside { 70 | display: flex; 71 | flex-direction: column; 72 | overflow: auto; 73 | padding: 1.5em; 74 | } 75 | 76 | .form { 77 | display: grid; 78 | grid-gap: 1.5em; 79 | margin-bottom: auto; 80 | background-color: #fff; 81 | 82 | .input { 83 | display: grid; 84 | grid-gap: 0.4em; 85 | justify-items: flex-start; 86 | 87 | & > label { 88 | font-size: 0.9em; 89 | } 90 | 91 | button { 92 | height: 40px; 93 | padding: 0 1.5em; 94 | border-radius: 40px; 95 | border: none; 96 | outline: none; 97 | cursor: pointer; 98 | font-weight: 500; 99 | font-size: 0.9em; 100 | background-color: var(--primary-color); 101 | color: #fff; 102 | font-family: 'Google Sans', sans-serif; 103 | } 104 | 105 | input[type="text"] { 106 | height: 40px; 107 | border-radius: 1em; 108 | width: 100%; 109 | border: 1px solid #9e9e9e; 110 | outline: none; 111 | padding: 0 1em; 112 | } 113 | 114 | .file { 115 | input[type="file"] { 116 | display: none; 117 | } 118 | } 119 | 120 | .range { 121 | display: grid; 122 | grid-template-columns: 1fr 60px; 123 | grid-gap: 0.25em; 124 | width: 100%; 125 | 126 | input { 127 | width: 100%; 128 | accent-color: var(--primary-color); 129 | } 130 | 131 | div { 132 | text-align: right; 133 | font-size: 0.9em; 134 | letter-spacing: -0.025em; 135 | } 136 | } 137 | 138 | .radio { 139 | display: grid; 140 | grid-auto-flow: column; 141 | overflow: hidden; 142 | border: 1px solid #9e9e9e; 143 | height: 40px; 144 | border-radius: 40px; 145 | 146 | input { 147 | display: none; 148 | 149 | &:disabled, 150 | &:disabled + label { 151 | display: none; 152 | } 153 | } 154 | 155 | input:checked + label { 156 | background-color: #bbdefb; 157 | } 158 | 159 | label { 160 | display: grid; 161 | align-items: center; 162 | padding: 0 1.25em; 163 | cursor: pointer; 164 | text-align: center; 165 | border-left: 1px solid #9e9e9e; 166 | font-size: 0.9em; 167 | 168 | &:first-of-type { 169 | border-left: none; 170 | } 171 | } 172 | } 173 | } 174 | } 175 | 176 | .preview { 177 | background-color: #eeeeee; 178 | border: 1px solid #9e9e9e; 179 | border-radius: 8px; 180 | place-items: center; 181 | margin: 1.5em 1.5em 1.5em 0; 182 | padding: 3em; 183 | overflow: hidden; 184 | 185 | .canvas { 186 | width: 100%; 187 | height: 100%; 188 | overflow: hidden; 189 | position: relative; 190 | display: flex; 191 | 192 | canvas { 193 | display: block; 194 | margin: auto; 195 | max-width: 100%; 196 | max-height: 100%; 197 | } 198 | 199 | &[data-shape="circle"] canvas { 200 | border-radius: 50%; 201 | } 202 | } 203 | } 204 | 205 | .fab { 206 | display: grid; 207 | grid-auto-flow: column; 208 | align-items: center; 209 | grid-gap: 0.5em; 210 | position: fixed; 211 | right: 3em; 212 | bottom: 3em; 213 | height: 56px; 214 | border-radius: 56px; 215 | padding: 0 2em 0 1.7em; 216 | background-color: var(--primary-color); 217 | color: #fff; 218 | border: none; 219 | outline: none; 220 | font-size: 1em; 221 | font-weight: 500; 222 | font-family: 'Google Sans', sans-serif; 223 | cursor: pointer; 224 | } 225 | 226 | footer { 227 | margin-top: 3em; 228 | font-size: 0.8em; 229 | text-align: center; 230 | opacity: 0.8; 231 | 232 | a { 233 | color: #0d47a1; 234 | text-decoration: underline; 235 | } 236 | } 237 | } 238 | 239 | @media screen and (max-width: 720px) { 240 | header { 241 | letter-spacing: -0.025em; 242 | padding: 0 1em; 243 | grid-gap: 0.35em; 244 | } 245 | 246 | main { 247 | grid-template-columns: 1fr; 248 | grid-template-rows: 1fr 1fr; 249 | grid-template-areas: 'preview' 'aside'; 250 | 251 | aside { 252 | grid-area: aside; 253 | padding: 1em 1em 6em; 254 | } 255 | 256 | .preview { 257 | grid-area: preview; 258 | margin: 1em; 259 | padding: 1em; 260 | } 261 | 262 | .fab { 263 | right: 1rem; 264 | bottom: 1rem; 265 | } 266 | } 267 | } -------------------------------------------------------------------------------- /scripts/app.js: -------------------------------------------------------------------------------- 1 | const settings = new Proxy({ 2 | banner: '', 3 | image: { 4 | src: '', 5 | fileName: '', 6 | }, 7 | category: 'Category Name', 8 | shape: 'original', 9 | grid: 'none', 10 | x: 0, 11 | y: 0, 12 | z: 1, 13 | isExport: false, 14 | }, { 15 | get: (target, property) => target[property], 16 | set: (target, property, value) => { 17 | target[property] = value; 18 | draw(); 19 | return true; 20 | }, 21 | }); 22 | 23 | const loadCategories = () => { 24 | const categories = [ 25 | 'AI', 26 | 'Android', 27 | 'Angular', 28 | 'Dart', 29 | 'Earth Engine', 30 | 'Firebase', 31 | 'Flutter', 32 | 'Google Cloud', 33 | 'Go', 34 | 'Identity', 35 | 'Kaggle', 36 | 'Google Maps Platform', 37 | 'Payments', 38 | 'Web Technologies', 39 | 'Google Workspace', 40 | ]; 41 | const fragment = document.createDocumentFragment(); 42 | categories.forEach((e) => { 43 | const opt = document.createElement('option'); 44 | opt.value = e; 45 | fragment.appendChild(opt); 46 | }); 47 | document.querySelector('datalist#categories').appendChild(fragment); 48 | }; 49 | 50 | const loadFile = (name, file) => { 51 | const reader = new FileReader(); 52 | reader.onload = (event) => loadImage(event.target.result, file.name, name); 53 | reader.readAsDataURL(file); 54 | }; 55 | 56 | const loadImage = (src, fileName, name) => { 57 | const img = new Image(); 58 | img.onload = () => settings[name] = {img, fileName}; 59 | img.src = src instanceof Blob ? URL.createObjectURL(src) : src; 60 | }; 61 | 62 | const fileListener = (name) => { 63 | const ele = document.querySelector(`.form input#${name}`); 64 | const btn = document.querySelector(`.form input#${name} + button`); 65 | ele.addEventListener('change', (e) => { 66 | if (e && e.target.files && e.target.files[0]) { 67 | const file = e.target.files[0]; 68 | loadFile(name, file); 69 | } 70 | }); 71 | btn.addEventListener('click', () => ele.click()); 72 | }; 73 | 74 | const textListener = (name) => { 75 | const ele = document.querySelector(`.form input#${name}`); 76 | ele.addEventListener('input', (e) => { 77 | const value = e.target.value; 78 | settings[name] = value; 79 | }); 80 | }; 81 | 82 | const radioListener = (name) => { 83 | document.querySelectorAll(`input[name="${name}"]`).forEach((ele) => { 84 | ele.addEventListener('change', (e) => { 85 | settings[name] = e.target.value; 86 | }); 87 | }); 88 | }; 89 | 90 | const rangeListener = (name, dp) => { 91 | const eleName = `.form input#image-${name}`; 92 | const ele = document.querySelector(eleName); 93 | ele.addEventListener('input', (e) => { 94 | const value = e.target.value; 95 | const text = parseFloat(value).toFixed(dp); 96 | document.querySelector(`${eleName} + div span`).textContent = text; 97 | settings[name] = value; 98 | }); 99 | }; 100 | 101 | const updateRange = (name, value, dp) => { 102 | const eleName = `.form input#image-${name}`; 103 | document.querySelector(eleName).value = value; 104 | const text = parseFloat(value).toFixed(dp); 105 | document.querySelector(`${eleName} + div span`).textContent = text; 106 | settings[name] = value; 107 | }; 108 | 109 | const resetButtonListener = () => { 110 | const ele = document.querySelector(`.form button#reset`); 111 | ele.addEventListener('click', () => { 112 | updateRange('x', 0, 1); 113 | updateRange('y', 0, 1); 114 | updateRange('z', 1, 2); 115 | }); 116 | }; 117 | 118 | const downloadButtonListener = () => { 119 | const ele = document.querySelector(`button#download`); 120 | ele.addEventListener('click', () => { 121 | settings.isExport = true; 122 | setTimeout(() => { 123 | const a = document.createElement('a'); 124 | const canvas = document.querySelector('canvas'); 125 | const url = canvas.toDataURL('image/png;base64'); 126 | const fileName = settings.image.fileName.replace(/\.[^/.]+$/, ''); 127 | a.download = `${fileName || Date.now()}-gde-badge.png`; 128 | a.href = url; 129 | a.click(); 130 | settings.isExport = false; 131 | }, 100); 132 | }); 133 | }; 134 | 135 | const dropListener = () => { 136 | const body = document.querySelector('body'); 137 | const drop = document.querySelector('.drop'); 138 | body.addEventListener('dragenter', (e) => { 139 | e.preventDefault(); 140 | e.stopPropagation(); 141 | drop.setAttribute('active', ''); 142 | }, false); 143 | 144 | body.addEventListener('dragleave', (e) => { 145 | e.preventDefault(); 146 | e.stopPropagation(); 147 | drop.removeAttribute('active'); 148 | }, false); 149 | 150 | body.addEventListener('dragover', (e) => { 151 | e.preventDefault(); 152 | e.stopPropagation(); 153 | drop.setAttribute('active', ''); 154 | }, false); 155 | 156 | body.addEventListener('drop', (e) => { 157 | e.preventDefault(); 158 | e.stopPropagation(); 159 | drop.removeAttribute('active'); 160 | if (e && e.dataTransfer.files && e.dataTransfer.files[0]) { 161 | const file = e.dataTransfer.files[0]; 162 | loadFile('image', file); 163 | } 164 | }, false); 165 | }; 166 | 167 | const pasteListener = () => { 168 | document.addEventListener('paste', async (e) => { 169 | e.preventDefault(); 170 | const clipboardItems = await navigator.clipboard.read(); 171 | for (const clipboardItem of clipboardItems) { 172 | for (const type of clipboardItem.types) { 173 | const blob = await clipboardItem.getType(type); 174 | if (type.startsWith('image/')) loadImage(blob, '', 'image'); 175 | } 176 | } 177 | }); 178 | }; 179 | 180 | const drawGrid = (canvas, ctx) => { 181 | if (settings.isExport) return; 182 | 183 | ctx.beginPath(); 184 | ctx.moveTo(0, canvas.height / 3); 185 | ctx.lineTo(canvas.width, canvas.height / 3); 186 | ctx.moveTo(0, canvas.height / 3 * 2); 187 | ctx.lineTo(canvas.width, canvas.height / 3 * 2); 188 | ctx.moveTo(canvas.width / 3, 0); 189 | ctx.lineTo(canvas.width / 3, canvas.height); 190 | ctx.moveTo(canvas.width / 3 * 2, 0); 191 | ctx.lineTo(canvas.width / 3 * 2, canvas.height); 192 | ctx.lineWidth = 5; 193 | ctx.strokeStyle = 'rgba(0, 0, 0, .25)'; 194 | ctx.stroke(); 195 | }; 196 | 197 | const drawCheckPattern = (canvas, ctx) => { 198 | if (settings.isExport) return; 199 | 200 | const size = canvas.width / 40; 201 | ctx.fillStyle = '#bdbdbd'; 202 | 203 | for (let i = 0; i < 40; ++i ) { 204 | for (let j = 0, col = 40 / 2; j < col; ++j) { 205 | ctx.rect(2 * j * size + (i % 2 ? 0 : size), i * size, size, size); 206 | } 207 | } 208 | 209 | ctx.fill(); 210 | }; 211 | 212 | const draw = () => { 213 | const canvas = document.querySelector('canvas'); 214 | const ctx = canvas.getContext('2d'); 215 | 216 | const {image: imageObj, x, y, z, shape, grid, category, banner} = settings; 217 | const image = imageObj.img; 218 | 219 | if (image) { 220 | switch (shape) { 221 | case 'original': { 222 | canvas.width = image.width; 223 | canvas.height = image.height; 224 | ctx.save(); 225 | ctx.translate( 226 | ((canvas.width - (image.width * z)) / 2) * x / 100, 227 | ((canvas.width - (image.width * z)) / 2) * y / 100, 228 | ); 229 | ctx.transform( 230 | z, 0, 0, z, 231 | -(z-1) * canvas.width / 2, 232 | -(z-1) * canvas.height / 2, 233 | ); 234 | drawCheckPattern(canvas, ctx); 235 | ctx.drawImage(image, 0, 0); 236 | ctx.restore(); 237 | break; 238 | } 239 | default: { 240 | const size = Math.min(image.width, image.height); 241 | canvas.width = size; 242 | canvas.height = size; 243 | const hRatio = canvas.width / image.width; 244 | const vRatio = canvas.height / image.height; 245 | const ratio = Math.max( hRatio, vRatio ); 246 | const canvasX = ( canvas.width - image.width * ratio ) / 2; 247 | const canvasY = ( canvas.height - image.height * ratio ) / 2; 248 | ctx.save(); 249 | ctx.translate( 250 | ((canvas.width - (image.width * z)) / 2) * x / 100, 251 | ((canvas.height - (image.height * z)) / 2) * y / 100, 252 | ); 253 | ctx.transform( 254 | z, 0, 0, z, 255 | -(z-1) * canvas.width / 2, 256 | -(z-1) * canvas.height / 2, 257 | ); 258 | drawCheckPattern(canvas, ctx); 259 | ctx.drawImage( 260 | image, 0, 0, image.width, image.height, 261 | canvasX, canvasY, image.width * ratio, image.height * ratio, 262 | ); 263 | ctx.restore(); 264 | break; 265 | } 266 | } 267 | } else { 268 | // Set transparent canvas 269 | ctx.canvas.width = 1920; 270 | ctx.canvas.height = 1920; 271 | ctx.clearRect(0, 0, canvas.width, canvas.height); 272 | drawCheckPattern(canvas, ctx); 273 | } 274 | 275 | // Draw "Banner" 276 | const height = banner.height / banner.width * canvas.width; 277 | const bannerY = canvas.height - height; 278 | ctx.drawImage( 279 | banner, 0, 0, banner.width, banner.height, 280 | 0, bannerY, canvas.width, height, 281 | ); 282 | 283 | // Draw "Category Name" 284 | const textSize = canvas.width / 17.2; 285 | const textY = bannerY + height * 0.908; 286 | ctx.fillStyle = '#757575'; 287 | ctx.textAlign = 'center'; 288 | ctx.textBaseline = 'middle'; 289 | ctx.font = `${textSize}px Google Sans, sans-serif`; 290 | ctx.fillText(category, canvas.width / 2, textY); 291 | 292 | // Draw grid 293 | if (grid === 'grid') drawGrid(canvas, ctx); 294 | 295 | switch (shape) { 296 | // Mask image into circle 297 | case 'circle': { 298 | ctx.globalCompositeOperation = 'destination-in'; 299 | ctx.beginPath(); 300 | ctx.arc( 301 | canvas.width / 2, canvas.height / 2, 302 | canvas.height / 2, 0, Math.PI * 2, 303 | ); 304 | ctx.closePath(); 305 | ctx.fill(); 306 | document.querySelector('.canvas').dataset.shape = 'circle'; 307 | break; 308 | } 309 | case 'material': { 310 | ctx.globalCompositeOperation = 'destination-in'; 311 | ctx.drawImage(settings.material, 0, 0, canvas.width, canvas.height); 312 | document.querySelector('.canvas').dataset.shape = 'circle'; 313 | break; 314 | } 315 | default: { 316 | delete document.querySelector('.canvas').dataset.shape; 317 | break; 318 | } 319 | } 320 | }; 321 | 322 | const loadBanner = async () => { 323 | settings.banner = new Image(); 324 | settings.banner.src = (await import('../images/banner.png')).default; 325 | settings.banner.onload = async () => { 326 | await document.fonts.ready; 327 | draw(); 328 | }; 329 | }; 330 | 331 | const loadMaterial = async () => { 332 | document.querySelector('input#shape-material').disabled = false; 333 | settings.material = new Image(); 334 | settings.material.src = (await import('../images/m3.svg')).default; 335 | settings.material.onload = async () => { 336 | draw(); 337 | }; 338 | }; 339 | 340 | const checkMaterialFlag = () => { 341 | const params = new URLSearchParams(location.search); 342 | const material = params.get('material'); 343 | if (material === 'true') loadMaterial(); 344 | }; 345 | 346 | loadCategories(); 347 | rangeListener('x', 1); 348 | rangeListener('y', 1); 349 | rangeListener('z', 2); 350 | radioListener('shape'); 351 | radioListener('grid'); 352 | textListener('category'); 353 | fileListener('image'); 354 | resetButtonListener(); 355 | downloadButtonListener(); 356 | dropListener(); 357 | pasteListener(); 358 | loadBanner(); 359 | checkMaterialFlag(); 360 | --------------------------------------------------------------------------------