├── .gitignore ├── .nvmrc ├── .swcrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config ├── env.config.js └── webpack.config.js ├── design └── ac-pattern-tool.xd ├── etc ├── compress.js ├── env.js ├── paths.js └── utils.js ├── injected ├── brimmed_cap │ ├── mCap_Crv.png │ ├── mCap_Mix.png │ ├── mCap_Nrm.png │ └── model.gltf ├── brimmed_hat │ ├── mCap_Crv.png │ ├── mCap_Mix.png │ ├── mCap_Nrm.png │ └── model.gltf ├── clothing_stand.gltf ├── coat │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── dress_acnh_long │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── dress_acnh_none │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── dress_acnh_short │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── dress_balloon │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── dress_half │ ├── mTops_Nrm.png │ └── model.gltf ├── dress_long │ ├── mTops_Nrm.png │ └── model.gltf ├── dress_none │ ├── mTops_Nrm.png │ └── model.gltf ├── dress_round │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── dressshirt_long │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── easel │ ├── mBody_Alb.png │ ├── mBody_Nrm.png │ ├── mReFabric_Mix.png │ ├── mine.png │ └── model.gltf ├── hat │ └── model.gltf ├── hoodie │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── hornhat │ └── model.gltf ├── knit_cap │ ├── mCap_Crv.png │ ├── mCap_Mix.png │ ├── mCap_Nrm.png │ └── model.gltf ├── robe │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── shirt_half │ ├── mTops_Nrm.png │ └── model.gltf ├── shirt_long │ ├── mTops_Nrm.png │ └── model.gltf ├── shirt_none │ ├── mTops_Nrm.png │ └── model.gltf ├── sweater │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── tank_pro │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── tank_simp │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf └── tee_short │ ├── mTops_Crv.png │ ├── mTops_Mix.png │ ├── mTops_Nrm.png │ ├── mTops_OP.png │ └── model.gltf ├── package-lock.json ├── package.json ├── public ├── favicon.svg ├── font.css ├── fonts │ ├── Nunito-Bold.woff │ ├── Nunito-ExtraBold.woff │ ├── Nunito-SemiBold.woff │ ├── Roboto-Black.woff │ ├── Roboto-Bold.woff │ ├── Roboto-Light.woff │ ├── Roboto-Medium.woff │ ├── Roboto-Regular.woff │ └── Roboto-Thin.woff └── index.ejs ├── scripts ├── build.js ├── clean.js └── dev.js ├── src ├── App.vue ├── assets │ ├── fonts │ │ └── .gitkeep │ ├── icons │ │ ├── .gitkeep │ │ ├── banner.svg │ │ ├── brush-large.svg │ │ ├── brush-medium.svg │ │ ├── brush-small.svg │ │ ├── brush.svg │ │ ├── bx-grid.svg │ │ ├── bx-redo.svg │ │ ├── bx-refresh.svg │ │ ├── bx-search.svg │ │ ├── bx-undo.svg │ │ ├── bxs-color-fill.svg │ │ ├── bxs-detail.svg │ │ ├── bxs-eyedropper.svg │ │ ├── bxs-inbox.svg │ │ ├── bxs-left-arrow-alt.svg │ │ ├── bxs-left-arrow.svg │ │ ├── bxs-right-arrow-alt.svg │ │ ├── bxs-right-arrow.svg │ │ ├── nav │ │ │ ├── about.svg │ │ │ ├── browse.svg │ │ │ ├── discord.svg │ │ │ ├── editor.svg │ │ │ ├── faq.svg │ │ │ ├── home.svg │ │ │ ├── nooknet.svg │ │ │ ├── twitter.svg │ │ │ └── updates.svg │ │ ├── nookphone │ │ │ ├── compass.svg │ │ │ ├── nook-gps.svg │ │ │ ├── nook-head.svg │ │ │ └── nook-service.svg │ │ ├── paint-tube.svg │ │ ├── qrcode.svg │ │ └── transparent-blob.svg │ ├── images │ │ ├── .gitkeep │ │ ├── Four_Leaves_Pattern_BW.svg │ │ ├── Leaf_Brush_Pattern_Tile.svg │ │ ├── StitchingPatternTile.svg │ │ └── char │ │ │ ├── photo_mayumi.jpg │ │ │ ├── photo_melon.png │ │ │ ├── photo_tero.jpg │ │ │ ├── photo_thulinma.svg │ │ │ └── photo_viet.jpg │ └── resources │ │ └── .gitkeep ├── components │ ├── ACNLQRGenerator.vue │ ├── Banner.vue │ ├── IconGenerator.vue │ ├── MarkdownPage.vue │ ├── MarkdownStyled.vue │ ├── PatternItems │ │ ├── Grid.vue │ │ ├── GridItem.vue │ │ ├── GridItemSelector.vue │ │ ├── MosaicPreview.vue │ │ └── SinglePreview.vue │ ├── PreviewGenerator.vue │ ├── ThreeDRender.vue │ ├── icons │ │ ├── IconColorBlob.vue │ │ ├── IconRibbonTailLeft.vue │ │ ├── IconStamp.vue │ │ └── IconTransparentBlob.vue │ ├── modals │ │ ├── CancelButton.vue │ │ └── Publish.vue │ ├── positioned │ │ ├── Banner.vue │ │ ├── NavigationButton.vue │ │ ├── NavigationMenu.vue │ │ ├── PatternContainer.vue │ │ └── UtilityBar.vue │ └── wrapper │ │ ├── FileLoader.vue │ │ └── FileLoaderCollection.vue ├── i18n │ ├── index.ts │ └── translations.csv ├── index.js ├── libs │ ├── ACNHFormat.js │ ├── ACNHKeypressGenerator.js │ ├── ACNHPBLGenerator.js │ ├── ACNLFormat.js │ ├── ACNLQRGenerator.js │ ├── DrawingTool.js │ ├── Preview3D.js │ ├── acZxing │ │ ├── AcBrowserQRCodeReader.ts │ │ ├── AcDecodeContinuouslyCallback.ts │ │ ├── AcDetector.ts │ │ ├── AcEncoder.ts │ │ ├── AcFinderPatternFinder.ts │ │ ├── AcQRCodeReader.ts │ │ ├── ImageLoadingException.ts │ │ └── index.ts │ ├── canvasHelpers.ts │ ├── component-helpers.ts │ ├── converter.ts │ ├── downloader.ts │ ├── fnv1a.js │ ├── origin.ts │ ├── reader.ts │ ├── storage.ts │ ├── typeMappings.ts │ └── xbrz.js ├── models │ ├── index.ts │ └── utilts.ts ├── pages │ ├── About │ │ ├── About.vue │ │ ├── InfoItem.vue │ │ ├── Passport.vue │ │ └── index.ts │ ├── Browse │ │ ├── Browse.vue │ │ ├── PatternEntry.vue │ │ └── index.js │ ├── Editor │ │ ├── ACNHToACNLInfo.vue │ │ ├── ACNLToACNHInfo.vue │ │ ├── ColorTools │ │ │ ├── ACNHColorPicker.vue │ │ │ ├── ACNLColorPicker.vue │ │ │ ├── ColorTools.vue │ │ │ ├── Palette.vue │ │ │ └── index.js │ │ ├── ConvertImage │ │ │ ├── ConvertImage.vue │ │ │ ├── Stages │ │ │ │ ├── Adjusting.vue │ │ │ │ ├── Cropping.vue │ │ │ │ ├── GridStencil.vue │ │ │ │ ├── Saving.vue │ │ │ │ └── Uploading.vue │ │ │ └── index.js │ │ ├── Editor.vue │ │ ├── ImportMenuItem.vue │ │ ├── KeypressInstructions.md │ │ ├── PatternSettings.vue │ │ ├── Preview.vue │ │ ├── Storage.vue │ │ ├── Toolbar.vue │ │ └── index.js │ ├── FAQ │ │ ├── FAQ.md │ │ ├── FAQ.vue │ │ └── index.ts │ ├── Home.vue │ ├── Missing.vue │ ├── Updates │ │ ├── Updates.md │ │ ├── Updates.vue │ │ └── index.ts │ └── moderator │ │ ├── Dashboard.vue │ │ ├── Index.vue │ │ └── Login.vue ├── plugins │ ├── fragment.ts │ └── vuetify.ts ├── routers │ └── index.ts ├── shims-env.d.ts ├── shims-loaders.d.ts ├── shims-vue.d.ts ├── store │ ├── index.ts │ ├── modules │ │ ├── browse │ │ │ ├── actions.ts │ │ │ ├── getters.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── mutations.ts │ │ │ └── state.ts │ │ ├── profile │ │ │ ├── actions.ts │ │ │ ├── getters.ts │ │ │ ├── index.ts │ │ │ ├── mutations.ts │ │ │ └── state.ts │ │ └── storage │ │ │ ├── actions.ts │ │ │ ├── getters.ts │ │ │ ├── index.ts │ │ │ ├── mutations.ts │ │ │ └── state.ts │ ├── storage.js │ └── types.ts ├── styles │ ├── animations.scss │ ├── colors.scss │ ├── functions.scss │ ├── icon-colors.scss │ ├── overrides.scss │ ├── positioning.scss │ ├── resets.scss │ ├── screens.scss │ └── transitions.scss └── utils │ └── if-env.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore dependencies 2 | **/node_modules 3 | 4 | # Ignore build 5 | **/build 6 | **/.env 7 | 8 | # Ignore stats 9 | /stats 10 | 11 | # Ignore OS Metadata 12 | **/.DS_Store 13 | 14 | # Ignore Workspace Metadata 15 | **/.vscode -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.13.1 2 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsc": { 3 | "parser": { 4 | "syntax": "typescript", 5 | "tsx": false, 6 | "decorators": false, 7 | "dynamicImport": true 8 | }, 9 | "transform": null, 10 | "target": "es5", 11 | "externalHelpers": true 12 | }, 13 | "module": { 14 | "type": "es6", 15 | "lazy": true, 16 | "noInterop": false 17 | } 18 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## What should I know before I get started? 4 | 5 | ### Will the backend be open sourced? 6 | 7 | ***The short answer is no***. We originally intended for open source 8 | contributions to help develop the ***offline*** tools, but anything related to 9 | the backend will be confidential. We will not provide documentation of our api 10 | or anything related. 11 | 12 | ### Design Decisions 13 | 14 | We will make design decisions for major features internally. There are many 15 | things to consider such as UI/UX and backend logistics. We'll always try our 16 | best to hear out the community, but at the end of the day we'll call the shots. 17 | Major changes to the application, especially ones that don't belong on our road 18 | map will more than likely be rejected. We will try our best to keep an updated 19 | roadmap. 20 | 21 | 22 | 23 | ## Development Guidelines 24 | 25 | ### Make the Branch Descriptive 26 | 27 | Please use industry conventions for naming branches. Group tokens are a great 28 | way to make your PR more descriptive. Here are some examples: 29 | 30 | Here are some recommend group tokens: 31 | * `bug/` - fix a bug 32 | * `feat/` - add a feature 33 | * `update/` - update anything (general use) 34 | 35 | ### Follow Conventions 36 | 37 | It's important that you follow the conventions set aside in the code. Specific 38 | organization styles are intended to keep the project maintainable and scalable. 39 | This includes things from file organization, module decoupling, to even small 40 | pieces such as import statements and parameter orders. Important concepts such 41 | as data stores, portals, and other rendering framework-related knowledge will 42 | definitely help you here. 43 | 44 | ### Performance and Optimization 45 | 46 | One of our major priorities is making sure that build size stays minimal. Any 47 | noticeable hits to performance, be it build size or cpu usage, will be more 48 | heavily reviewed. 49 | 50 | ### Fixing Bugs and Minor Optimizations 51 | 52 | We're always busy planning out new features. Obscure bugs that we're not 53 | already trying to squash or minor optimizations will have the highest 54 | chance of being approved for merge. 55 | 56 | ## How Can I Contribute 57 | 58 | ### Check for Duplicates 59 | 60 | Does a bug report or enhancement already have an issue created? Great! Stop there. 61 | 62 | ### Reporting Bugs 63 | 64 | #### How Do I Submit A Good Bug Report 65 | 66 | * Use a clear and descriptive title for the problem. The more words there are, 67 | the better. 68 | 69 | * Describe the exact steps to reproduce the problem; link attachments if 70 | necessary. 71 | 72 | * Explain the behavior you observed, and try to provide a possible explanation 73 | if you can. 74 | 75 | * Mention the version of the application or the commit id that this bug 76 | occured on. 77 | 78 | 79 | ### Suggesting Enhancements 80 | 81 | * Thoroughly describe your enhancement and how the application can benefit from 82 | it. 83 | 84 | * Provide clear use cases. If you can mock it up, even better. Try to cover 85 | edge cases like errors encountered along the way and how to deal with them. 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ACNLPatternTool 2 | 3 | An application to edit Animal Crossing pattern designs. 4 | 5 | ## Installation 6 | 7 | At the root of the project directory: 8 | ```sh 9 | npm install 10 | ``` 11 | 12 | ## Available Scripts 13 | 14 | ### `npm run dev` 15 | 16 | Runs the application in development mode. Automatically reloads with changes. 17 | Open [https://localhost:3000](https://localhost:3000) to view it in the 18 | browser. **This command does not build the submodule.** 19 | **Remember to build or even rebuild the submodule if it changes.** 20 | 21 | ### `npm run build -- ` 22 | 23 | 24 | Builds the app and outputs to a `build` directory at the repository root. Can 25 | build the the project in development mode or production mode. By default builds 26 | in the mode specified by the `.env`, but can otherwise override with a command 27 | line option. Use `--help` option to view options. 28 | 29 | 30 | ### `npm run clean` 31 | 32 | Cleans the app build directory. Recursively removes files located in the build 33 | directory. 34 | 35 | -------------------------------------------------------------------------------- /config/env.config.js: -------------------------------------------------------------------------------- 1 | // list of env strings, and if needed to specify via .env 2 | // only .env variables that the client should access here 3 | // available internally to the build 4 | const clientEnvConfig = { 5 | NODE_ENV: false, 6 | API_URL: false, 7 | IS_OFFLINE: false, 8 | } 9 | 10 | // multiple layers of correction exist for NODE_ENV 11 | // default settings must pass validation by default 12 | const defaultEnv = { 13 | DEV_HOST: "localhost", 14 | DEV_PORT: "3000", 15 | API_URL: "https://acpatterns.com/", 16 | IS_OFFLINE: false, 17 | }; 18 | 19 | // transformations to be applied to env INSIDE the build 20 | // transformed strings will not be avialable outside 21 | const transformEnv = { 22 | IS_OFFLINE: () => { 23 | const { IS_OFFLINE } = process.env; 24 | return eval(IS_OFFLINE); 25 | } 26 | }; 27 | 28 | const validateEnv = { 29 | NODE_ENV: () => { 30 | const { NODE_ENV } = process.env; 31 | return ["development", "production"].includes(NODE_ENV); 32 | }, 33 | DEV_PORT: () => { 34 | const { DEV_PORT } = process.env; 35 | try { 36 | if ( 37 | Number.isInteger(Number.parseFloat(DEV_PORT)) 38 | ) return true; 39 | else return false; 40 | } 41 | catch (error) { return false; } 42 | }, 43 | API_URL: () => { 44 | const { API_URL } = process.env; 45 | try { new URL(API_URL); return true; } 46 | catch (error) { return false; } 47 | }, 48 | IS_OFFLINE: () => { 49 | try { 50 | const IS_OFFLINE = transformEnv.IS_OFFLINE(); 51 | if ([true, false].includes(IS_OFFLINE)) return true; 52 | return true; 53 | } 54 | catch (error) { return false; } 55 | } 56 | }; 57 | 58 | module.exports = { 59 | clientEnvConfig, 60 | defaultEnv, 61 | transformEnv, 62 | validateEnv 63 | } 64 | -------------------------------------------------------------------------------- /design/ac-pattern-tool.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/design/ac-pattern-tool.xd -------------------------------------------------------------------------------- /etc/compress.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const fse = require("fs-extra"); 3 | const path = require('path'); 4 | const zlib = require("zlib"); 5 | const stream = require("stream"); 6 | const { 7 | forAllFilesIn 8 | } = require("./utils"); 9 | 10 | const compressFile = (filepath) => { 11 | const gzip = zlib.createGzip({ 12 | level: 9, 13 | }); 14 | const source = fs.createReadStream(filepath); 15 | const destination = fs.createWriteStream(`${filepath}.gz`); 16 | 17 | stream.pipeline(source, gzip, destination, (err) => { 18 | if (err) { 19 | console.log(error); 20 | process.exit(1); 21 | } 22 | }); 23 | }; 24 | 25 | const isCompressedRegEx = /.gz$/i; 26 | 27 | // creates files 28 | const create = (startPath) => { 29 | const filesToCompress = new Set(); 30 | forAllFilesIn(startPath, (filepath) => { 31 | // only add files that need compression 32 | if (!(isCompressedRegEx.test(filepath))) 33 | filesToCompress.add(filepath); 34 | }); 35 | for (let file of filesToCompress) { 36 | compressFile(file); 37 | } 38 | }; 39 | 40 | // removes files 41 | // select to remove compressed, uncompressed or all 42 | const destroy = (startPath, compressed) => { 43 | const filesToRemove = new Set(); 44 | if (compressed == null) { 45 | if (fs.existsSync(startPath)) 46 | fse.removeSync(startPath); 47 | }; 48 | forAllFilesIn(startPath, (filepath) => { 49 | const isCompressed = isCompressedRegEx.test(filepath); 50 | const doesMatch = compressed? isCompressed : !isCompressed; 51 | if (doesMatch) 52 | filesToRemove.add(filepath); 53 | }); 54 | for (let file of filesToRemove) 55 | fse.removeSync(file); 56 | }; 57 | 58 | module.exports = { 59 | create, 60 | destroy 61 | }; -------------------------------------------------------------------------------- /etc/env.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const dotenv = require('dotenv'); 3 | const signale = require('signale'); 4 | const { 5 | clientEnvConfig, 6 | defaultEnv, 7 | transformEnv, 8 | validateEnv 9 | } = require('../config/env.config'); 10 | const { pathToEnv } = require('./paths'); 11 | 12 | const clientEnvReqConfig = Object.keys(clientEnvConfig) 13 | .filter((key) => { return clientEnvConfig[key]; }); 14 | const distinctReqEnvConfig = [...new Set(clientEnvReqConfig)].sort(); 15 | const includedEnvConfig = [ 16 | ... new Set([ 17 | ...clientEnvReqConfig, 18 | ...Object.keys(defaultEnv) 19 | ]) 20 | ].sort(); 21 | 22 | const load = () => { 23 | // dot env skips ones that are already set, use it first 24 | if (fs.existsSync(pathToEnv)) 25 | dotenv.config({ path: pathToEnv }); 26 | // fill in gaps with default 27 | for (let [key, value] of Object.entries(defaultEnv)) 28 | if (!(key in process.env)) process.env[key] = value; 29 | }; 30 | 31 | const correct = () => { 32 | const { 33 | NODE_ENV, 34 | } = process.env; 35 | 36 | if (!["development", "production"].includes(NODE_ENV)) 37 | process.env.NODE_ENV = "development"; 38 | }; 39 | 40 | const check = () => { 41 | const errorMessages = []; 42 | if (!fs.existsSync(pathToEnv)) 43 | if (Object.keys(distinctReqEnvConfig).length > 0) 44 | errorMessages.push(".env file could not be found."); 45 | // checks for missing environment variables 46 | distinctReqEnvConfig.forEach((envVar) => { 47 | if (envVar in process.env === false) 48 | errorMessages.push(`Environment variable: '${envVar}' could not be found.`); 49 | }); 50 | 51 | includedEnvConfig.forEach((envVar) => { 52 | if (envVar in validateEnv) { 53 | if (!validateEnv[envVar]()) 54 | errorMessages.push(`Environment variable: '${envVar}' was not valid.`); 55 | } 56 | }); 57 | 58 | // reports all errors else continue 59 | if (errorMessages.length > 0) { 60 | errorMessages.forEach((message) => { 61 | signale.error(message); 62 | }); 63 | process.exit(1); 64 | } 65 | }; 66 | 67 | const buildClient = () => { 68 | // a, b, intersection 69 | const clientEnvVars = Object.keys(clientEnvConfig); 70 | const processEnvVars = new Set(Object.keys(process.env)); 71 | const clientAvailEnvVars = [...clientEnvVars].filter(envVar => processEnvVars.has(envVar)); 72 | 73 | // construct client env from what is available 74 | const clientEnv = clientAvailEnvVars 75 | .reduce((env, envVar) => { 76 | if (envVar in transformEnv) 77 | env[envVar] = transformEnv[envVar](); 78 | else 79 | env[envVar] = process.env[envVar]; 80 | return env; 81 | }, {}); 82 | return clientEnv; 83 | }; 84 | 85 | // UTILITIES 86 | const ifDevVal = (devVal, defaultVal) => { 87 | const { NODE_ENV } = process.env; 88 | const isDev = NODE_ENV === "development"; 89 | if (isDev) return devVal; 90 | else return defaultVal; 91 | }; 92 | 93 | const ifDevExec = (devCallback, defaultCallback) => { 94 | const { NODE_ENV } = process.env; 95 | const isDev = NODE_ENV === "development"; 96 | if (isDev) devCallback(); 97 | else if (defaultCallback) defaultCallback(); 98 | }; 99 | 100 | 101 | const ifProdVal = (prodVal, defaultVal) => { 102 | const { NODE_ENV } = process.env; 103 | const isProd = NODE_ENV === "production"; 104 | if (isProd) return prodVal; 105 | else return defaultVal; 106 | }; 107 | 108 | const ifProdExec = (prodCallback, defaultCallback) => { 109 | const { NODE_ENV } = process.env; 110 | const isProd = NODE_ENV === "production"; 111 | if (isProd) prodCallback(); 112 | else if (defaultCallback) defaultCallback(); 113 | }; 114 | 115 | 116 | const ifOfflineVal = (offlineVal, defaultVal) => { 117 | const { IS_OFFLINE } = process.env; 118 | const isOffline = IS_OFFLINE === String(true); 119 | if (isOffline) return offlineVal; 120 | else return defaultVal; 121 | } 122 | 123 | const ifOfflineExec = (offlineCallback, defaultCallback) => { 124 | const { IS_OFFLINE } = process.env; 125 | const isOffline = IS_OFFLINE === String(true); 126 | if (isOffline) offlineCallback(); 127 | else if (defaultCallback) defaultCallback(); 128 | } 129 | 130 | // process.env variables available to external (inside build process) 131 | // process.env variables available to internal (inside built process) 132 | module.exports = { 133 | load, 134 | correct, 135 | check, 136 | buildClient, 137 | ifDevVal, 138 | ifDevExec, 139 | ifProdVal, 140 | ifProdExec, 141 | ifOfflineVal, 142 | ifOfflineExec, 143 | } -------------------------------------------------------------------------------- /etc/paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const parent = path.resolve(__dirname); 4 | // path to directories 5 | const pathToRoot = path.resolve(parent, ".."); 6 | const pathToConfig = path.resolve(pathToRoot, "config"); 7 | const pathToEtc = path.resolve(pathToRoot, "etc"); 8 | const pathToPublic = path.resolve(pathToRoot, "public"); 9 | const pathToClientSrc = path.resolve(pathToRoot, "src"); 10 | const pathToBuild = path.resolve(pathToRoot, "build"); 11 | const pathToStats = path.resolve(pathToRoot, "stats"); 12 | const pathToInjected = path.resolve(pathToRoot, "injected"); 13 | const pathToNodeModules = path.resolve(pathToRoot, "node_modules"); 14 | 15 | // paths to specific files 16 | const pathToEnv = path.resolve(pathToRoot, ".env"); 17 | const pathToBabelConfig = path.resolve(pathToConfig, "babel.config.js"); 18 | const pathToEnvConfig = path.resolve(pathToConfig, "env.config.js"); 19 | const pathToWebpackConfig = path.resolve(pathToConfig, "webpack.config.js"); 20 | const pathToPublicIndex = path.resolve(pathToPublic, "index.ejs"); 21 | const pathToFavicon = path.resolve(pathToPublic, "favicon.svg"); 22 | const pathToClientSrcIndex = path.resolve(pathToClientSrc, "index.js") 23 | const pathToPathsJs = path.resolve(pathToEtc, "paths.js"); 24 | const pathToBundleStats = path.resolve(pathToStats, "bundle.html"); 25 | 26 | module.exports = { 27 | pathToRoot, 28 | pathToConfig, 29 | pathToEtc, 30 | pathToPublic, 31 | pathToClientSrc, 32 | pathToBuild, 33 | pathToStats, 34 | pathToInjected, 35 | pathToNodeModules, 36 | pathToEnv, 37 | pathToBabelConfig, 38 | pathToEnvConfig, 39 | pathToWebpackConfig, 40 | pathToPublicIndex, 41 | pathToFavicon, 42 | pathToClientSrcIndex, 43 | pathToPathsJs, 44 | pathToBundleStats 45 | }; -------------------------------------------------------------------------------- /etc/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | // depth first search 5 | const forAllFilesIn = (startPath, callback) => { 6 | if (!fs.existsSync(startPath)) return; 7 | if (!fs.lstatSync(startPath).isDirectory()) 8 | if (callback) callback(startPath); 9 | 10 | const files = fs.readdirSync(startPath); 11 | for (const file of files) { 12 | const filepath = path.resolve(startPath, file); 13 | const stat = fs.lstatSync(filepath); 14 | if (stat.isDirectory()) { 15 | forAllFilesIn(filepath, callback); 16 | continue; 17 | } 18 | // is a file 19 | if (callback) callback(filepath); 20 | } 21 | } 22 | 23 | module.exports = { 24 | forAllFilesIn, 25 | }; -------------------------------------------------------------------------------- /injected/brimmed_cap/mCap_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/brimmed_cap/mCap_Crv.png -------------------------------------------------------------------------------- /injected/brimmed_cap/mCap_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/brimmed_cap/mCap_Mix.png -------------------------------------------------------------------------------- /injected/brimmed_cap/mCap_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/brimmed_cap/mCap_Nrm.png -------------------------------------------------------------------------------- /injected/brimmed_hat/mCap_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/brimmed_hat/mCap_Crv.png -------------------------------------------------------------------------------- /injected/brimmed_hat/mCap_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/brimmed_hat/mCap_Mix.png -------------------------------------------------------------------------------- /injected/brimmed_hat/mCap_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/brimmed_hat/mCap_Nrm.png -------------------------------------------------------------------------------- /injected/coat/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/coat/mTops_Crv.png -------------------------------------------------------------------------------- /injected/coat/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/coat/mTops_Mix.png -------------------------------------------------------------------------------- /injected/coat/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/coat/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/coat/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/coat/mTops_OP.png -------------------------------------------------------------------------------- /injected/dress_acnh_long/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_long/mTops_Crv.png -------------------------------------------------------------------------------- /injected/dress_acnh_long/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_long/mTops_Mix.png -------------------------------------------------------------------------------- /injected/dress_acnh_long/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_long/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/dress_acnh_long/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_long/mTops_OP.png -------------------------------------------------------------------------------- /injected/dress_acnh_none/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_none/mTops_Crv.png -------------------------------------------------------------------------------- /injected/dress_acnh_none/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_none/mTops_Mix.png -------------------------------------------------------------------------------- /injected/dress_acnh_none/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_none/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/dress_acnh_none/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_none/mTops_OP.png -------------------------------------------------------------------------------- /injected/dress_acnh_short/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_short/mTops_Crv.png -------------------------------------------------------------------------------- /injected/dress_acnh_short/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_short/mTops_Mix.png -------------------------------------------------------------------------------- /injected/dress_acnh_short/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_short/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/dress_acnh_short/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_acnh_short/mTops_OP.png -------------------------------------------------------------------------------- /injected/dress_balloon/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_balloon/mTops_Crv.png -------------------------------------------------------------------------------- /injected/dress_balloon/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_balloon/mTops_Mix.png -------------------------------------------------------------------------------- /injected/dress_balloon/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_balloon/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/dress_balloon/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_balloon/mTops_OP.png -------------------------------------------------------------------------------- /injected/dress_half/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_half/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/dress_long/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_long/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/dress_none/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_none/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/dress_round/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_round/mTops_Mix.png -------------------------------------------------------------------------------- /injected/dress_round/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_round/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/dress_round/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dress_round/mTops_OP.png -------------------------------------------------------------------------------- /injected/dressshirt_long/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dressshirt_long/mTops_Crv.png -------------------------------------------------------------------------------- /injected/dressshirt_long/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dressshirt_long/mTops_Mix.png -------------------------------------------------------------------------------- /injected/dressshirt_long/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dressshirt_long/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/dressshirt_long/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/dressshirt_long/mTops_OP.png -------------------------------------------------------------------------------- /injected/easel/mBody_Alb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/easel/mBody_Alb.png -------------------------------------------------------------------------------- /injected/easel/mBody_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/easel/mBody_Nrm.png -------------------------------------------------------------------------------- /injected/easel/mReFabric_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/easel/mReFabric_Mix.png -------------------------------------------------------------------------------- /injected/easel/mine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/easel/mine.png -------------------------------------------------------------------------------- /injected/hoodie/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/hoodie/mTops_Crv.png -------------------------------------------------------------------------------- /injected/hoodie/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/hoodie/mTops_Mix.png -------------------------------------------------------------------------------- /injected/hoodie/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/hoodie/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/hoodie/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/hoodie/mTops_OP.png -------------------------------------------------------------------------------- /injected/knit_cap/mCap_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/knit_cap/mCap_Crv.png -------------------------------------------------------------------------------- /injected/knit_cap/mCap_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/knit_cap/mCap_Mix.png -------------------------------------------------------------------------------- /injected/knit_cap/mCap_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/knit_cap/mCap_Nrm.png -------------------------------------------------------------------------------- /injected/robe/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/robe/mTops_Crv.png -------------------------------------------------------------------------------- /injected/robe/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/robe/mTops_Mix.png -------------------------------------------------------------------------------- /injected/robe/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/robe/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/robe/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/robe/mTops_OP.png -------------------------------------------------------------------------------- /injected/shirt_half/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/shirt_half/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/shirt_long/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/shirt_long/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/shirt_none/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/shirt_none/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/sweater/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/sweater/mTops_Crv.png -------------------------------------------------------------------------------- /injected/sweater/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/sweater/mTops_Mix.png -------------------------------------------------------------------------------- /injected/sweater/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/sweater/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/sweater/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/sweater/mTops_OP.png -------------------------------------------------------------------------------- /injected/tank_pro/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tank_pro/mTops_Crv.png -------------------------------------------------------------------------------- /injected/tank_pro/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tank_pro/mTops_Mix.png -------------------------------------------------------------------------------- /injected/tank_pro/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tank_pro/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/tank_pro/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tank_pro/mTops_OP.png -------------------------------------------------------------------------------- /injected/tank_simp/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tank_simp/mTops_Crv.png -------------------------------------------------------------------------------- /injected/tank_simp/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tank_simp/mTops_Mix.png -------------------------------------------------------------------------------- /injected/tank_simp/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tank_simp/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/tank_simp/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tank_simp/mTops_OP.png -------------------------------------------------------------------------------- /injected/tee_short/mTops_Crv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tee_short/mTops_Crv.png -------------------------------------------------------------------------------- /injected/tee_short/mTops_Mix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tee_short/mTops_Mix.png -------------------------------------------------------------------------------- /injected/tee_short/mTops_Nrm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tee_short/mTops_Nrm.png -------------------------------------------------------------------------------- /injected/tee_short/mTops_OP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/injected/tee_short/mTops_OP.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ac-pattern-tool", 3 | "version": "1.4.0", 4 | "description": "Pattern tool for ACNL, ACHHD and ACNH", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "node scripts/dev.js", 8 | "clean": "node scripts/clean.js", 9 | "build": "node scripts/build.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/Thulinma/ACNLPatternTool.git" 14 | }, 15 | "author": "", 16 | "license": "WTFPL", 17 | "bugs": { 18 | "url": "https://github.com/Thulinma/ACNLPatternTool/issues" 19 | }, 20 | "homepage": "https://github.com/Thulinma/ACNLPatternTool#readme", 21 | "dependencies": { 22 | "@juggle/resize-observer": "^3.2.0", 23 | "@swc/core": "^1.2.117", 24 | "@swc/helpers": "^0.3.13", 25 | "@types/file-saver": "^2.0.5", 26 | "@types/lodash": "^4.14.178", 27 | "@types/lz-string": "^1.3.34", 28 | "@types/papaparse": "^5.3.1", 29 | "@types/three": "^0.137.0", 30 | "@types/uuid": "^8.3.4", 31 | "@vxna/optimize-three-webpack-plugin": "^5.1.3", 32 | "@zxing/library": "^0.17.1", 33 | "axios": "^0.24.0", 34 | "copy-webpack-plugin": "^11.0.0", 35 | "css-loader": "^3.4.2", 36 | "dotenv": "^10.0.0", 37 | "favicons-webpack-plugin": "^5.0.2", 38 | "file-saver": "^2.0.5", 39 | "fs-extra": "^8.1.0", 40 | "html-webpack-plugin": "^5.5.0", 41 | "jsbi": "^4.1.0", 42 | "jszip": "^3.7.1", 43 | "lodash": "^4.17.21", 44 | "lz-string": "^1.4.4", 45 | "mini-css-extract-plugin": "^2.4.5", 46 | "papaparse": "^5.3.1", 47 | "qs": "^6.10.1", 48 | "reset-css": "^5.0.1", 49 | "sass": "~1.32", 50 | "sass-loader": "^12.3.0", 51 | "signale": "^1.4.0", 52 | "stream": "^0.0.2", 53 | "swc-loader": "^0.1.15", 54 | "terser-webpack-plugin": "^5.2.5", 55 | "three": "^0.135.0", 56 | "uuid": "^8.3.2", 57 | "vue": "^2.6.14", 58 | "vue-advanced-cropper": "^1.9.0", 59 | "vue-fragment": "^1.5.2", 60 | "vue-i18n": "^8.26.7", 61 | "vue-loader": "^15.9.8", 62 | "vue-markdown-loader": "^2.5.0", 63 | "vue-router": "^3.5.3", 64 | "vue-style-loader": "^4.1.3", 65 | "vue-svg-loader": "^0.16.0", 66 | "vue-template-compiler": "^2.6.14", 67 | "vuetify": "^2.6.1", 68 | "vuetify-loader": "^1.7.3", 69 | "vuex": "^3.6.2", 70 | "webpack": "^5.64.4", 71 | "webpack-bundle-analyzer": "^4.5.0", 72 | "webpack-dev-server": "^4.6.0", 73 | "webpack-format-messages": "^3.0.1", 74 | "yargs": "^15.1.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /public/font.css: -------------------------------------------------------------------------------- 1 | /* === Nunito - 600 */ 2 | @font-face { 3 | font-family: 'Nunito'; 4 | font-style: normal; 5 | font-weight: 600; 6 | font-display: swap; 7 | 8 | src: local('Nunito'), 9 | url("./fonts//Nunito-SemiBold.woff") format("woff"); 10 | } 11 | 12 | /* === Nunito - 700 */ 13 | @font-face { 14 | font-family: 'Nunito'; 15 | font-style: normal; 16 | font-weight: 700; 17 | font-display: swap; 18 | 19 | src: local('Nunito'), 20 | url("./fonts//Nunito-Bold.woff") format("woff"); 21 | } 22 | 23 | /* === Nunito - 800 */ 24 | @font-face { 25 | font-family: 'Nunito'; 26 | font-style: normal; 27 | font-weight: 800; 28 | font-display: swap; 29 | 30 | src: local('Nunito'), 31 | url("./fonts//Nunito-ExtraBold.woff") format("woff"); 32 | } 33 | 34 | /* === Roboto - 100 */ 35 | @font-face { 36 | font-family: 'Roboto'; 37 | font-style: normal; 38 | font-weight: 100; 39 | font-display: swap; 40 | 41 | src: local('Roboto'), 42 | url("./fonts//Roboto-Thin.woff") format("woff"); 43 | } 44 | 45 | /* === Roboto - 300 */ 46 | @font-face { 47 | font-family: 'Roboto'; 48 | font-style: normal; 49 | font-weight: 300; 50 | font-display: swap; 51 | 52 | src: local('Roboto'), 53 | url("./fonts//Roboto-Light.woff") format("woff"); 54 | } 55 | 56 | /* === Roboto - regular */ 57 | @font-face { 58 | font-family: 'Roboto'; 59 | font-style: normal; 60 | font-weight: 400; 61 | font-display: swap; 62 | 63 | src: local('Roboto'), 64 | url("./fonts//Roboto-Regular.woff") format("woff"); 65 | } 66 | 67 | /* === Roboto - 500 */ 68 | @font-face { 69 | font-family: 'Roboto'; 70 | font-style: normal; 71 | font-weight: 500; 72 | font-display: swap; 73 | 74 | src: local('Roboto'), 75 | url("./fonts//Roboto-Medium.woff") format("woff"); 76 | } 77 | 78 | /* === Roboto - 700 */ 79 | @font-face { 80 | font-family: 'Roboto'; 81 | font-style: normal; 82 | font-weight: 700; 83 | font-display: swap; 84 | 85 | src: local('Roboto'), 86 | url("./fonts//Roboto-Bold.woff") format("woff"); 87 | } 88 | 89 | /* === Roboto - 900 */ 90 | @font-face { 91 | font-family: 'Roboto'; 92 | font-style: normal; 93 | font-weight: 900; 94 | font-display: swap; 95 | 96 | src: local('Roboto'), 97 | url("./fonts//Roboto-Black.woff") format("woff"); 98 | } 99 | -------------------------------------------------------------------------------- /public/fonts/Nunito-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/public/fonts/Nunito-Bold.woff -------------------------------------------------------------------------------- /public/fonts/Nunito-ExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/public/fonts/Nunito-ExtraBold.woff -------------------------------------------------------------------------------- /public/fonts/Nunito-SemiBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/public/fonts/Nunito-SemiBold.woff -------------------------------------------------------------------------------- /public/fonts/Roboto-Black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/public/fonts/Roboto-Black.woff -------------------------------------------------------------------------------- /public/fonts/Roboto-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/public/fonts/Roboto-Bold.woff -------------------------------------------------------------------------------- /public/fonts/Roboto-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/public/fonts/Roboto-Light.woff -------------------------------------------------------------------------------- /public/fonts/Roboto-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/public/fonts/Roboto-Medium.woff -------------------------------------------------------------------------------- /public/fonts/Roboto-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/public/fonts/Roboto-Regular.woff -------------------------------------------------------------------------------- /public/fonts/Roboto-Thin.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/public/fonts/Roboto-Thin.woff -------------------------------------------------------------------------------- /public/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 |
12 |
13 | 14 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | // configure cli options 2 | const yargs = require('yargs'); 3 | const argv = yargs 4 | .option("development", { 5 | alias: "d", 6 | describe: "Use development environment", 7 | type: "boolean" 8 | }) 9 | .option("production", { 10 | alias: "p", 11 | describe: "Use production environment", 12 | type: "boolean" 13 | }) 14 | .option("uncompressed", { 15 | alias: "u", 16 | describe: "Produce only uncompressed", 17 | type: "boolean" 18 | }) 19 | .option("compressed", { 20 | alias: "c", 21 | describe: "Produce only compressed", 22 | type: "boolean" 23 | }) 24 | .option("analyze", { 25 | alias: "a", 26 | describe: "Analyze bundle", 27 | type: "boolean" 28 | }) 29 | .option("offline", { 30 | alias: "o", 31 | descripe: "Offline (embedded assets)", 32 | type: "boolean", 33 | }) 34 | .conflicts("development", "production") 35 | .conflicts("uncompressed", "compressed") 36 | .parse(); 37 | 38 | // overload NODE_ENV with command line option 39 | const env = require('../etc/env'); 40 | env.load(); 41 | let buildSetting = argv.env; 42 | if (!argv.development && !argv.production) // if no argument, use default set by .env file 43 | buildSetting = null; 44 | else { 45 | if (argv.development) buildSetting = "development"; 46 | else if (argv.production) buildSetting = "production"; 47 | process.env.NODE_ENV = buildSetting 48 | } 49 | if (argv.offline) 50 | process.env.IS_OFFLINE = true; 51 | env.correct(); 52 | env.check(); 53 | 54 | const signale = require('signale'); 55 | const webpack = require('webpack'); 56 | const webpackFormatMessages = require('webpack-format-messages'); 57 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); 58 | const { 59 | webpackConfig, 60 | webpackDevConfig, 61 | webpackProdConfig, 62 | } = require('../config/webpack.config'); 63 | const { 64 | pathToBuild, 65 | pathToBundleStats, 66 | } = require('../etc/paths'); 67 | const compress = require('../etc/compress'); 68 | const { NODE_ENV } = process.env; 69 | 70 | // check process args, allow build with forced settings 71 | let selectedWebpackConfig; 72 | if (!["development", "production"].includes(buildSetting)) { 73 | selectedWebpackConfig = webpackConfig; // w/e the default is in .env 74 | buildSetting = NODE_ENV; 75 | } 76 | else 77 | if (buildSetting === "development") selectedWebpackConfig = webpackDevConfig; 78 | else selectedWebpackConfig = webpackProdConfig; 79 | 80 | if (argv.analyze) selectedWebpackConfig.plugins.push( 81 | new BundleAnalyzerPlugin({ 82 | analyzerMode: "static", 83 | reportFilename: pathToBundleStats, 84 | generateStatsFile: false, 85 | statsOptions: { source: true }, 86 | openAnalyzer: true, 87 | logLevel: "silent" 88 | }) 89 | ); 90 | 91 | let isCompiled = false; 92 | const compiler = webpack(selectedWebpackConfig); 93 | 94 | compiler.hooks.invalid.tap('invalid', function() { 95 | signale.pending('Compiling application...'); 96 | }); 97 | 98 | compiler.hooks.done.tap('done', (stats) => { 99 | const messages = webpackFormatMessages(stats); 100 | 101 | if (!messages.errors.length && !messages.warnings.length) { 102 | isCompiled = true; 103 | signale.success(`Application compiled in ${buildSetting} mode!`); 104 | if (argv.analyze) 105 | signale.success(`Bundle report generated to ${pathToBundleStats}`) 106 | } 107 | 108 | if (messages.errors.length) { 109 | signale.fatal('Application failed to compile.'); 110 | messages.errors.forEach(e => console.log(e)); 111 | return; 112 | } 113 | 114 | if (messages.warnings.length) { 115 | isCompiled = true; 116 | signale.warn('Application compiled with warnings.'); 117 | messages.warnings.forEach(w => console.log(w)); 118 | } 119 | }); 120 | 121 | ["SIGINT", "SIGTERM"].forEach((signal) => { 122 | process.on(signal, () => { 123 | console.log(""); 124 | process.exit(); 125 | }); 126 | }); 127 | 128 | (async () => { 129 | await new Promise((resolve) => { 130 | compiler.run((error, stats) => { 131 | if (error) 132 | console.log(error); 133 | else console.log(stats.toString({ 134 | chunks: true, 135 | colors: true, 136 | })); 137 | compiler.close(_error => { 138 | resolve(); 139 | }); 140 | }); 141 | }); 142 | 143 | if (!isCompiled) return; 144 | 145 | // compression & compression elimination 146 | compress.create(pathToBuild); 147 | if (argv.compressed) { 148 | compress.destroy(pathToBuild, false); // remove uncompressed 149 | signale.success('Uncompressed files removed from build.'); 150 | } 151 | if (argv.uncompressed) { 152 | compress.destroy(pathToBuild, true); // remove compressed 153 | signale.success('Compressed files removed from build.'); 154 | } 155 | })(); -------------------------------------------------------------------------------- /scripts/clean.js: -------------------------------------------------------------------------------- 1 | const fse = require('fs-extra'); 2 | const signale = require('signale'); 3 | const { 4 | pathToBuild, 5 | pathToStats 6 | } = require('../etc/paths'); 7 | 8 | ["SIGINT", "SIGTERM"].forEach((signal) => { 9 | process.on(signal, () => { 10 | console.log(""); 11 | process.exit(); 12 | }); 13 | }); 14 | 15 | fse.removeSync(pathToBuild); 16 | fse.removeSync(pathToStats); 17 | signale.success(`Build directory cleaned successfully!`); 18 | signale.success(`Stats directory cleaned successfully!`); -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | // configure cli options 2 | const yargs = require("yargs"); 3 | const argv = yargs 4 | .option("offline", { 5 | alias: "o", 6 | describe: "Offline (embedded assets)", 7 | type: "boolean", 8 | }) 9 | .parse(); 10 | 11 | // overload NODE_ENV with command line option 12 | const env = require('../etc/env'); 13 | env.load(); 14 | if (argv.offline) 15 | process.env.IS_OFFLINE = true; 16 | env.correct(); 17 | env.check(); 18 | 19 | const webpack = require('webpack'); 20 | const WebpackDevServer = require('webpack-dev-server'); 21 | const signale = require('signale'); 22 | const { pathToPublic } = require('../etc/paths'); 23 | const { webpackDevConfig } = require('../config/webpack.config'); 24 | const { 25 | DEV_HOST, 26 | DEV_PORT 27 | } = process.env; 28 | 29 | const compiler = webpack(webpackDevConfig); 30 | 31 | const webpackDevServer = new WebpackDevServer({ 32 | host: DEV_HOST, 33 | port: DEV_PORT, 34 | open: true, 35 | static: { 36 | directory: pathToPublic, // technically nonexistent, exists in memory 37 | }, 38 | client: { 39 | progress: false, 40 | reconnect: true, 41 | overlay: { 42 | errors: true, 43 | warnings: true, 44 | }, 45 | }, 46 | historyApiFallback: true, 47 | }, compiler); 48 | 49 | ["SIGINT", "SIGTERM"].forEach((signal) => { 50 | process.on(signal, () => { 51 | console.log(""); 52 | webpackDevServer.stop(); 53 | process.exit(); 54 | }); 55 | }); 56 | 57 | (async () => { 58 | await webpackDevServer.start(); 59 | signale.success(`Development server deployed in ${"development"} mode!`); 60 | })(); 61 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 35 | 36 | 44 | 45 | -------------------------------------------------------------------------------- /src/assets/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/src/assets/fonts/.gitkeep -------------------------------------------------------------------------------- /src/assets/icons/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/src/assets/icons/.gitkeep -------------------------------------------------------------------------------- /src/assets/icons/banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/brush-large.svg: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | -------------------------------------------------------------------------------- /src/assets/icons/brush-medium.svg: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | -------------------------------------------------------------------------------- /src/assets/icons/brush-small.svg: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | -------------------------------------------------------------------------------- /src/assets/icons/brush.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/bx-grid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bx-redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bx-refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bx-search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bx-undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bxs-color-fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bxs-detail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bxs-eyedropper.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bxs-inbox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bxs-left-arrow-alt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bxs-left-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bxs-right-arrow-alt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/bxs-right-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/nav/about.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 19 | 20 | 47 | 48 | 52 | 56 | 61 | 69 | 73 | 81 | 82 | 83 | 84 | 92 | 100 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 114 | 115 | -------------------------------------------------------------------------------- /src/assets/icons/nav/browse.svg: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 43 | 44 | 45 | 53 | 54 | 55 | 62 | 63 | 70 | 78 | 86 | 87 | 88 | 89 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 108 | 109 | 118 | 119 | 120 | 126 | 127 | -------------------------------------------------------------------------------- /src/assets/icons/nav/discord.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 27 | 28 | 32 | 33 | 34 | 38 | -------------------------------------------------------------------------------- /src/assets/icons/nav/faq.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | 22 | 28 | 37 | 38 | 39 | 40 | 46 | 47 | -------------------------------------------------------------------------------- /src/assets/icons/nav/home.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | 25 | 29 | 37 | 41 | -------------------------------------------------------------------------------- /src/assets/icons/nav/nooknet.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | 28 | 36 | 44 | 48 | 52 | 56 | -------------------------------------------------------------------------------- /src/assets/icons/nav/twitter.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | 28 | 32 | 36 | 44 | 48 | 52 | 53 | -------------------------------------------------------------------------------- /src/assets/icons/nav/updates.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | 25 | 26 | 35 | 43 | 50 | 51 | 52 | 53 | 59 | 65 | 66 | 67 | 74 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/assets/icons/nookphone/compass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/nookphone/nook-gps.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/nookphone/nook-head.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/nookphone/nook-service.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/paint-tube.svg: -------------------------------------------------------------------------------- 1 | 13 | 23 | -------------------------------------------------------------------------------- /src/assets/icons/qrcode.svg: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 19 | 23 | -------------------------------------------------------------------------------- /src/assets/icons/transparent-blob.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/src/assets/images/.gitkeep -------------------------------------------------------------------------------- /src/assets/images/StitchingPatternTile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/char/photo_mayumi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/src/assets/images/char/photo_mayumi.jpg -------------------------------------------------------------------------------- /src/assets/images/char/photo_melon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/src/assets/images/char/photo_melon.png -------------------------------------------------------------------------------- /src/assets/images/char/photo_tero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/src/assets/images/char/photo_tero.jpg -------------------------------------------------------------------------------- /src/assets/images/char/photo_viet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/src/assets/images/char/photo_viet.jpg -------------------------------------------------------------------------------- /src/assets/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thulinma/ACNLPatternTool/3d5d18bbc69210d9626e8c5a9f71304479290ae6/src/assets/resources/.gitkeep -------------------------------------------------------------------------------- /src/components/ACNLQRGenerator.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 39 | 40 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/Banner.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 66 | -------------------------------------------------------------------------------- /src/components/IconGenerator.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 119 | 120 | 126 | -------------------------------------------------------------------------------- /src/components/MarkdownPage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/MarkdownStyled.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/PatternItems/GridItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/components/PatternItems/GridItemSelector.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/PatternItems/MosaicPreview.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 105 | 106 | -------------------------------------------------------------------------------- /src/components/PatternItems/SinglePreview.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 49 | 50 | -------------------------------------------------------------------------------- /src/components/PreviewGenerator.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 53 | 54 | 61 | -------------------------------------------------------------------------------- /src/components/icons/IconColorBlob.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/icons/IconRibbonTailLeft.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/icons/IconTransparentBlob.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/modals/CancelButton.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 36 | 37 | -------------------------------------------------------------------------------- /src/components/positioned/Banner.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/positioned/NavigationButton.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 41 | 42 | 125 | 126 | -------------------------------------------------------------------------------- /src/components/positioned/UtilityBar.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 60 | 61 | -------------------------------------------------------------------------------- /src/components/wrapper/FileLoader.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueI18n, { 3 | LocaleMessage, 4 | LocaleMessageObject, 5 | LocaleMessages, 6 | } from 'vue-i18n'; 7 | import Papa from "papaparse"; 8 | import { last } from "lodash"; 9 | import { isProd } from '@/utils/if-env'; 10 | import translationsCsvString from "./translations.csv"; 11 | 12 | Vue.use(VueI18n); 13 | 14 | interface Translation { 15 | path: string, 16 | [key: string]: string, 17 | } 18 | 19 | const translations = Papa.parse( 20 | translationsCsvString, 21 | { 22 | dynamicTyping: false, 23 | header: true, 24 | skipEmptyLines: true, 25 | }, 26 | ).data; 27 | 28 | const messages = translations.reduce((messages, row) => { 29 | const path = row["path"]; 30 | const pathComponents = path.split("."); 31 | // locales are ISO 639-1 codes 32 | const locales = Object.keys(row) 33 | .filter(locale => locale !== "path") 34 | .map(locale => locale.toLocaleLowerCase()); 35 | // traverse messages and create properties 36 | for (const locale of locales) { 37 | const message = row[locale]; 38 | if (message.trim().length <= 0) continue; 39 | const localePathComponents = [locale, ...pathComponents]; 40 | let messagesNode: LocaleMessageObject = messages; 41 | for (const pathComponent of localePathComponents.slice(0, -1)) { 42 | if (!messagesNode.hasOwnProperty(pathComponent)) 43 | messagesNode[pathComponent] = {}; 44 | messagesNode = messagesNode[pathComponent] as LocaleMessageObject; 45 | } 46 | const lastPathComponent = last(localePathComponents) as string; 47 | (messagesNode as LocaleMessageObject)[lastPathComponent] = message; 48 | } 49 | return messages; 50 | }, {}) as LocaleMessages; 51 | 52 | const i18n = new VueI18n({ 53 | locale: navigator.language.toLowerCase(), // use default and fallback languages 54 | fallbackLocale: "en", 55 | formatFallbackMessages: true, 56 | silentTranslationWarn: isProd, 57 | messages, 58 | }); 59 | 60 | export default i18n; -------------------------------------------------------------------------------- /src/i18n/translations.csv: -------------------------------------------------------------------------------- 1 | path,en,nl,jp,fr,de 2 | nested.path.example,,,,, -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import vuetify from './plugins/vuetify'; 3 | import "@/plugins/fragment"; 4 | import App from '@/App.vue'; 5 | import i18n from '@/i18n'; // use i18n 6 | import { ifProdExec } from '@/utils/if-env'; 7 | import router from '@/routers'; // use router 8 | import store from '@/store'; // use vuex 9 | 10 | // vue global config 11 | Vue.config.productionTip = false; 12 | ifProdExec(() => { 13 | Vue.config.devtools = false; 14 | }); 15 | 16 | 17 | 18 | // mount the application 19 | new Vue({ 20 | store, 21 | router, 22 | i18n, 23 | vuetify, 24 | render: (h) => h(App), 25 | }).$mount("#app"); -------------------------------------------------------------------------------- /src/libs/ACNHKeypressGenerator.js: -------------------------------------------------------------------------------- 1 | import DrawingTool, { RenderTarget} from "@/libs/DrawingTool"; 2 | import ACNHFormat from "@/libs/ACNHFormat"; 3 | 4 | async function generateACNHKeypresses(newData){ 5 | let drawingTool = new DrawingTool(newData); 6 | 7 | const tInfo = drawingTool.typeInfo; 8 | const bytes = drawingTool.toBytes(); 9 | 10 | //Build lookup table of palette colors 11 | let palette = []; 12 | for (let i = 0; i < 16; ++i){ 13 | const slrs = ACNHFormat.colorToSliders(drawingTool.getPalette(i)); 14 | //Quantify to nearest slider position 15 | const hex = ACNHFormat.slidersToColor(slrs[0], slrs[1], slrs[2]); 16 | drawingTool.setPalette(i, hex); 17 | } 18 | drawingTool.dedupeColors(); 19 | drawingTool.sortColors(); 20 | for (let i = 0; i < 16; ++i){ 21 | palette.push(drawingTool.getPalette(i)); 22 | } 23 | 24 | let autoscript = "nohold\n"; 25 | autoscript += "left;3000\nup;3000\nl;4000;position reset\n"; //go to top left pixel, select first palette color 26 | autoscript += "x;100\n{};100\nup;100\n{};100\nright;100\n{};100\na;100\n{};100;color editor open\n";//open side menu, navigate to color editor 27 | 28 | 29 | for (let i = 0; i < 15; ++i){ 30 | if (drawingTool.countPixelsWithColor(i) > 0){ 31 | const slrs = ACNHFormat.colorToSliders(palette[i]); 32 | if (slrs[0] <= 14){ 33 | autoscript += "left;2000\n{};100\n"; 34 | for (let j = 0; j < slrs[0]; ++j){ 35 | autoscript += "right;50\n{};50\n"; 36 | } 37 | }else{ 38 | autoscript += "right;2000\n{};100\n"; 39 | for (let j = 0; j < 29-slrs[0]; ++j){ 40 | autoscript += "left;50\n{};50\n"; 41 | } 42 | } 43 | autoscript += "minus;50;hue "+(i+1)+" set\n"; 44 | 45 | autoscript += "down;100\n{};100\n";//move to next 46 | if (slrs[1] <= 7){ 47 | autoscript += "left;2000\n{};100\n"; 48 | for (let j = 0; j < slrs[1]; ++j){ 49 | autoscript += "right;50\n{};50\n"; 50 | } 51 | }else{ 52 | autoscript += "right;2000\n{};100\n"; 53 | for (let j = 0; j < 14-slrs[1]; ++j){ 54 | autoscript += "left;50\n{};50\n"; 55 | } 56 | } 57 | autoscript += "minus;50;vivid "+(i+1)+" set\n"; 58 | 59 | autoscript += "down;100\n{};100\n";//move to next 60 | if (slrs[2] <= 7){ 61 | autoscript += "left;2000\n{};100\n"; 62 | for (let j = 0; j < slrs[2]; ++j){ 63 | autoscript += "right;50\n{};50\n"; 64 | } 65 | }else{ 66 | autoscript += "right;2000\n{};100\n"; 67 | for (let j = 0; j < 14-slrs[2]; ++j){ 68 | autoscript += "left;50\n{};50\n"; 69 | } 70 | } 71 | autoscript += "minus;50;bright "+(i+1)+" set\n"; 72 | autoscript += "down;100\nr;50\n{};50;select color "+(i+1)+"\n"; 73 | curCol = i+1; 74 | } 75 | } 76 | autoscript += "a;100\n{};500\nx;100\n{};200;colors all set\n"; 77 | let curCol = 0; 78 | autoscript += "l;2000\n{};100\n"; 79 | let mt = 50; 80 | let goingRight = true; 81 | let msg = ""; 82 | for (let y = 0; y < 32; ++y){ 83 | for (let x = 0; x < 32; ++x){ 84 | let nextCol = drawingTool.getPixel(goingRight?x:31-x, y); 85 | if (nextCol != curCol){ 86 | //Switch color 87 | while (curCol < nextCol){ 88 | autoscript += "r;"+mt+"\n{};"+mt+"\n"; 89 | curCol++; 90 | } 91 | while (curCol > nextCol){ 92 | autoscript += "l;"+mt+"\n{};"+mt+"\n"; 93 | curCol--; 94 | } 95 | } 96 | let newMsg = Math.round((y*32+x)*100/(32*32))+"% complete"; 97 | if (msg != newMsg){ 98 | msg = newMsg; 99 | autoscript += "a;"+mt+"\n{};"+mt+";"+msg+"\n"; 100 | }else{ 101 | autoscript += "a;"+mt+"\n{};"+mt+"\n"; 102 | } 103 | if (goingRight){ 104 | autoscript += "right;"+mt+"\n{};"+mt+"\n"; 105 | }else{ 106 | autoscript += "left;"+mt+"\n{};"+mt+"\n"; 107 | } 108 | } 109 | if (y < 31){autoscript += "down;"+mt+"\n{};"+mt+";moved to row "+(y+1)+"\n";} 110 | goingRight = !goingRight; 111 | } 112 | 113 | return autoscript; 114 | } 115 | 116 | export default generateACNHKeypresses; 117 | 118 | -------------------------------------------------------------------------------- /src/libs/ACNHPBLGenerator.js: -------------------------------------------------------------------------------- 1 | import {default as DrawingTool, RenderTarget} from "@/libs/DrawingTool"; 2 | import ACNHFormat from '@/libs/ACNHFormat'; 3 | import {drawPreviewFromTool} from "@/libs/Preview3D"; 4 | 5 | async function generateACNHPBL(newData){ 6 | //Load pattern, prepare render canvas 7 | let drawingTool = new DrawingTool(newData); 8 | 9 | const tInfo = drawingTool.typeInfo; 10 | const bytes = drawingTool.toBytes(); 11 | const pblCanvas = document.createElement("canvas"); 12 | let width = 862; 13 | let height = 660; 14 | pblCanvas.width = width; 15 | pblCanvas.height = height; 16 | let ctx = pblCanvas.getContext('2d'); 17 | 18 | //Create pretty background pattern on temp canvas 19 | const bgCanvas = document.createElement("canvas"); 20 | bgCanvas.width=45; 21 | bgCanvas.height=45; 22 | const bgCtx = bgCanvas.getContext("2d"); 23 | bgCtx.fillStyle = "#FFFFFF"; 24 | bgCtx.fillRect(0, 0, 45, 45); 25 | bgCtx.fillStyle = "#9b003a44"; 26 | bgCtx.rotate(Math.PI / 4); 27 | bgCtx.fillRect(0, -80, 16, 160); 28 | bgCtx.fillRect(32, -80, 16, 160); 29 | bgCtx.rotate(-Math.PI / 2); 30 | bgCtx.fillRect(0, -80, 16, 160); 31 | bgCtx.fillRect(-32, -80, 16, 160); 32 | //Copy background to main canvas 33 | ctx.fillStyle = ctx.createPattern(bgCanvas, "repeat"); 34 | ctx.fillRect(0, 0, width, height); 35 | 36 | //Build lookup table of palette colors 37 | let palette = []; 38 | for (let i = 0; i < 16; ++i){ 39 | const slrs = ACNHFormat.colorToSliders(drawingTool.getPalette(i)); 40 | //Quantify to nearest slider position 41 | const hex = ACNHFormat.slidersToColor(slrs[0], slrs[1], slrs[2]); 42 | drawingTool.setPalette(i, hex); 43 | } 44 | drawingTool.dedupeColors(); 45 | drawingTool.sortColors(); 46 | for (let i = 0; i < 16; ++i){ 47 | palette.push(drawingTool.getPalette(i)); 48 | } 49 | 50 | //Draw PBL grid 51 | { 52 | const gridCanvas = document.createElement("canvas"); 53 | gridCanvas.width = 640; 54 | gridCanvas.height = 640; 55 | const texW = drawingTool.texWidth; 56 | const r = new RenderTarget(gridCanvas, {tool:drawingTool, grid:true, pbn:true}); 57 | r.calcZoom(texW, texW); 58 | drawingTool.renderToTarget(r, palette); 59 | ctx.drawImage(gridCanvas, 10, 10); 60 | } 61 | 62 | //Draw preview image 63 | try{ 64 | await drawPreviewFromTool(ctx, drawingTool, 660, 10, 192, 192); 65 | }catch(e){ 66 | console.log(e); 67 | } 68 | 69 | //Prepare background pattern for text 70 | bgCanvas.width=1; 71 | bgCanvas.height=2; 72 | bgCtx.fillStyle = "#792e58"; 73 | bgCtx.fillRect(0, 0, 1, 1); 74 | bgCtx.fillStyle = "#883e5f"; 75 | bgCtx.fillRect(0, 1, 1, 1); 76 | const txtBg = ctx.createPattern(bgCanvas, "repeat"); 77 | 78 | const drawTxtWithBg = (x, y, txt, fore) => { 79 | ctx.textBaseline = "middle"; 80 | ctx.textAlign = 'center'; 81 | ctx.font = '20px Calibri'; 82 | const txtProps = ctx.measureText(txt); 83 | var h = (txtProps.fontBoundingBoxAscent?txtProps.fontBoundingBoxAscent:txtProps.actualBoundingBoxAscent) + (txtProps.fontBoundingBoxDescent?txtProps.fontBoundingBoxDescent:txtProps.actualBoundingBoxDescent)+4; 84 | var w = txtProps.width-h/2; 85 | x += w/2; 86 | ctx.fillStyle=txtBg; 87 | ctx.strokeStyle=fore; 88 | //Calculate background 89 | ctx.beginPath(); 90 | ctx.arc(x-w/2, y, h/2, 0.5*Math.PI, 1.5*Math.PI); 91 | ctx.lineTo(x+w/2, y-h/2); 92 | ctx.arc(x+w/2, y, h/2, 1.5*Math.PI, 0.5*Math.PI); 93 | ctx.lineTo(x+-w/2, y+h/2); 94 | ctx.fill(); 95 | ctx.stroke(); 96 | ctx.fillStyle="#00000088"; 97 | ctx.strokeStyle="#00000088"; 98 | ctx.fillText(txt, x+2, y+2); 99 | ctx.fillStyle=fore; 100 | ctx.strokeStyle=fore; 101 | ctx.fillText(txt, x, y); 102 | }; 103 | 104 | const drawLetter = (x, y, c, colIndex) => { 105 | let lightness = parseInt(c.substr(1, 2), 16) * 0.299 + parseInt(c.substr(3, 2), 16) * 0.587 + parseInt(c.substr(5, 2), 16) * 0.114; 106 | if (lightness > 120){ctx.fillStyle = "#000000";}else{ctx.fillStyle = "#FFFFFF";} 107 | ctx.fillRect(x-1, y-1, 22, 22); 108 | 109 | ctx.fillStyle = c; 110 | ctx.fillRect(x, y, 20, 20); 111 | ctx.font = "20px monospace"; 112 | 113 | ctx.textAlign = "center"; 114 | ctx.textBaseline = "middle"; 115 | if (lightness > 120){ctx.fillStyle = "#000000";}else{ctx.fillStyle = "#FFFFFF";} 116 | ctx.fillText(String.fromCharCode(65+colIndex), x+10, y+11); 117 | }; 118 | 119 | //Write slider positions 120 | for (let i = 0; i < 15; ++i){ 121 | if (drawingTool.countPixelsWithColor(i) > 0){ 122 | drawLetter(660, 212+((630-212)/14*i), palette[i], i); 123 | const slrs = ACNHFormat.colorToSliders(palette[i]); 124 | drawTxtWithBg(660+35, 212+((630-212)/14*i)+10, (slrs[0]+1)+", "+(slrs[1]+1)+", "+(slrs[2]+1), "#FFFFFF"); 125 | } 126 | } 127 | 128 | return pblCanvas.toDataURL("image/png"); 129 | } 130 | 131 | export default generateACNHPBL; 132 | 133 | -------------------------------------------------------------------------------- /src/libs/acZxing/AcDecodeContinuouslyCallback.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Exception, 3 | Result, 4 | } from '@zxing/library/esm'; 5 | 6 | 7 | /** 8 | * Callback format for continuous decode scan. 9 | */ 10 | type AcDecodeContinuouslyCallback = (results: Array, error?: Exception) => any; 11 | export default AcDecodeContinuouslyCallback; -------------------------------------------------------------------------------- /src/libs/acZxing/AcDetector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BitMatrix, 3 | DecodeHintType, 4 | DetectorResult, 5 | ResultPointCallback, 6 | } from '@zxing/library/esm'; 7 | import Detector from "@zxing/library/esm/core/qrcode/detector/Detector"; 8 | import FinderPatternInfo from "@zxing/library/esm/core/qrcode/detector/FinderPatternInfo"; 9 | import AcFinderPatternFinder from "./AcFinderPatternFinder"; 10 | 11 | 12 | /** 13 | * Patched Detector for multi-QR reading. 14 | */ 15 | class MyDetector extends Detector { 16 | 17 | public detectMultiple(hints: Map): Array { 18 | // @ts-ignore 19 | this.resultPointCallback = (hints === null || hints === undefined) ? null : 20 | /*(ResultPointCallback) */hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK); 21 | const finder = new AcFinderPatternFinder( 22 | // @ts-ignore 23 | this.image, 24 | // @ts-ignore 25 | this.resultPointCallback 26 | ); 27 | const infos = finder.findMultiple(hints); 28 | return this.processMultipleFinderPatternInfo(infos); 29 | } 30 | 31 | public processMultipleFinderPatternInfo(info: Array): Array { 32 | return info.map(i => { return super.processFinderPatternInfo(i); }); 33 | } 34 | } 35 | 36 | export default MyDetector; -------------------------------------------------------------------------------- /src/libs/acZxing/AcQRCodeReader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BinaryBitmap, 3 | DecodeHintType, 4 | QRCodeReader, 5 | Result, 6 | DecoderResult, 7 | BarcodeFormat, 8 | ResultMetadataType, 9 | } from '@zxing/library/esm'; 10 | import ResultPoint from "@zxing/library/esm/core/ResultPoint"; 11 | import AcDetector from "./AcDetector"; 12 | import QRCodeDecoderMetaData from "@zxing/library/esm/core/qrcode/decoder/QRCodeDecoderMetaData"; 13 | 14 | /** 15 | * Patched QRCodeReader for multi-QR reading. 16 | */ 17 | class MyQRCodeReader extends QRCodeReader { 18 | 19 | public decodeMultiple(image: BinaryBitmap, hints?: Map): Array { 20 | let decoderResults: Array; 21 | let decoderResultsPoints: Array>; 22 | if (hints !== undefined && hints !== null && undefined !== hints.get(DecodeHintType.PURE_BARCODE)) { 23 | // @ts-ignore 24 | const bits = MyQRCodeReader.extractPureBits(image.getBlackMatrix()); 25 | decoderResults = [this.getDecoder().decodeBitMatrix(bits, hints)]; 26 | // @ts-ignore 27 | decoderResultsPoints = [(QRCodeReader.NO_POINTS)]; 28 | } 29 | else { 30 | const detectorResults = new AcDetector(image.getBlackMatrix()).detectMultiple(hints); 31 | decoderResults = detectorResults.map((detectorResult) => { 32 | return this.getDecoder().decodeBitMatrix(detectorResult.getBits(), hints); 33 | }); 34 | decoderResultsPoints = detectorResults.map((detectorResult) => { 35 | return detectorResult.getPoints(); 36 | }); 37 | } 38 | 39 | const results: Array = decoderResults.map((decoderResult, index) => { 40 | if (decoderResult.getOther() instanceof QRCodeDecoderMetaData) { 41 | (decoderResult.getOther()).applyMirroredCorrection(decoderResultsPoints[index]); 42 | } 43 | const result = new Result( 44 | decoderResult.getText(), 45 | decoderResult.getRawBytes(), 46 | undefined, decoderResultsPoints[index], 47 | BarcodeFormat.QR_CODE, undefined 48 | ); 49 | const byteSegments: Array = decoderResult.getByteSegments(); 50 | if (byteSegments !== null) { 51 | result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegments); 52 | } 53 | const ecLevel: string = decoderResult.getECLevel(); 54 | if (ecLevel !== null) { 55 | result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, ecLevel); 56 | } 57 | if (decoderResult.hasStructuredAppend()) { 58 | result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE, 59 | decoderResult.getStructuredAppendSequenceNumber()); 60 | result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_PARITY, 61 | decoderResult.getStructuredAppendParity()); 62 | } 63 | return result; 64 | }); 65 | 66 | return results; 67 | } 68 | 69 | } 70 | 71 | export default MyQRCodeReader; -------------------------------------------------------------------------------- /src/libs/acZxing/ImageLoadingException.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Exception, 3 | } from '@zxing/library/esm'; 4 | 5 | class ImageLoadingException extends Exception { } 6 | 7 | export default ImageLoadingException; -------------------------------------------------------------------------------- /src/libs/acZxing/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AcBrowserQRCodeReader } from "./AcBrowserQRCodeReader"; 2 | export { default as AcEncoder } from "./AcEncoder"; 3 | export { default as ImageLoadingException } from "./ImageLoadingException"; -------------------------------------------------------------------------------- /src/libs/component-helpers.ts: -------------------------------------------------------------------------------- 1 | 2 | type Option = { 3 | text: string, 4 | value: T 5 | }; 6 | 7 | /** 8 | * Computes the available options at each index for an array of selected values. 9 | * The corresponding option of the selected value will always be included. 10 | * @param selVals values of the selected options 11 | * @param inclOpts inclusive options (multiple use) 12 | * @param exclOpts exclusive options (single use) 13 | */ 14 | export const computeOptsList = ( 15 | selVals: T[], 16 | inclOpts: Option[], 17 | exclOpts: Option[], 18 | ): Option[][] => { 19 | // throw if exclusive and inclsive values are intersecting 20 | const inclValsSet = new Set( 21 | inclOpts 22 | .map(opt => opt.value) 23 | ); 24 | const exclValsSet = new Set( 25 | exclOpts 26 | .map(opt => opt.value) 27 | ); 28 | const exclVals = [...exclValsSet]; 29 | const intersection = new Set( 30 | [...inclValsSet] 31 | .filter(val => exclValsSet.has(val)) 32 | ); 33 | if (intersection.size > 0) 34 | throw new Error(`The following inclusive and exclusive values are being shared: ${[...intersection]}`); 35 | 36 | // throw if an exclusive value is used more than once 37 | const exclValOccs = [...exclValsSet] 38 | .map(excVal => selVals.filter(selVal => selVal === excVal).length); 39 | const reusedExclVals = new Array(exclVals.length) 40 | .fill(null) 41 | .map((_null, i) => [exclVals[i], exclValOccs[i]]) 42 | .filter(([_exclVal, occ]) => occ > 1) 43 | .map(([exclVal]) => exclVal); 44 | if (reusedExclVals.length > 0) 45 | throw new Error(`The following exclusive values were selected more than once: ${reusedExclVals}`); 46 | 47 | // available options at each index; 48 | return selVals 49 | .map((_value, i) => { 50 | 51 | // values not at the current index 52 | const nonCurrVals = [ 53 | ...selVals.slice(0, i), 54 | ...selVals.slice(i + 1, selVals.length), 55 | ]; 56 | 57 | const unusedExclOpts = exclOpts 58 | .filter(opt => !nonCurrVals.includes(opt.value)); 59 | return [...inclOpts, ...unusedExclOpts]; 60 | }); 61 | }; 62 | 63 | 64 | type On = Record; 65 | 66 | /** 67 | * Combines activator event callbacks. 68 | * Supports same-type activators. 69 | * @param ons 70 | * @returns 71 | */ 72 | export const combineOns = function ( 73 | ...ons: On[] 74 | ): On { 75 | const callbacksMap = new Map(); 76 | for (const on of ons) 77 | for (const [eventName, callback] of Object.entries(on)) 78 | if (callbacksMap.has(eventName)) 79 | (callbacksMap.get(eventName) as Function[]).push(callback); 80 | else callbacksMap.set(eventName, [callback]); 81 | const combinedOns = {} as Record; 82 | for (const [eventName, callbacks] of [...callbacksMap.entries()]) 83 | combinedOns[eventName] = function(...args: any[]) { 84 | for (const callback of callbacks) callback.call(this, ...args); 85 | }; 86 | return combinedOns; 87 | }; -------------------------------------------------------------------------------- /src/libs/downloader.ts: -------------------------------------------------------------------------------- 1 | import JSZip from "jszip"; 2 | import { saveAs } from "file-saver"; 3 | import DrawingTool from "@/libs/DrawingTool"; 4 | import generateACNLQR from "./ACNLQRGenerator"; 5 | import generateACNHPBL from "./ACNHPBLGenerator"; 6 | 7 | 8 | /** 9 | * Interroppable interface to flexibly save as single or in zip. 10 | */ 11 | export interface NamedBlob { 12 | name: string, 13 | blob: Blob, 14 | }; 15 | 16 | 17 | export const drawingToolToNamedPatternBlob = async ( 18 | drawingTool: DrawingTool, 19 | name: string = `${drawingTool.title}.${drawingTool.compatMode.toLowerCase()}`, 20 | ): Promise => { 21 | const blob = new Blob( 22 | [drawingTool.toBytes()], 23 | { type: "application/octet-stream" }, 24 | ); 25 | return { 26 | name, 27 | blob, 28 | }; 29 | }; 30 | 31 | 32 | type DataUrlGen = (drawingTool: DrawingTool) => Promise; 33 | const modeToDataUrlGen = new Map([ 34 | ["acnl", generateACNLQR], 35 | ["acnh", generateACNHPBL], 36 | ]); 37 | export const drawingToolToNamedImageBlob = async ( 38 | drawingTool: DrawingTool, 39 | name: string = `${drawingTool.title}.png`, 40 | ): Promise => { 41 | const dataUrl = await ( 42 | (modeToDataUrlGen.get(drawingTool.compatMode.toLowerCase()) as DataUrlGen) 43 | (drawingTool) 44 | ); 45 | const blob = await (await fetch(dataUrl)).blob(); 46 | return { 47 | name, 48 | blob, 49 | }; 50 | }; 51 | 52 | 53 | export const namedBlobsToNamedZipBlob = async ( 54 | namedBlobs: NamedBlob[], 55 | name: string = "patterns.zip", 56 | ): Promise => { 57 | const nameUsage = new Map(); 58 | const zip = new JSZip(); 59 | for (const { name, blob } of namedBlobs) { 60 | const [base, ...exts] = name.split("."); 61 | if (nameUsage.has(name)) { 62 | nameUsage.set(name, nameUsage.get(name) as number + 1); 63 | zip.file(`${base} (${nameUsage.get(name)}).${exts.join(".")}`, blob); 64 | } 65 | else { 66 | nameUsage.set(name, 0); // copies start at 1 67 | zip.file(name, blob); 68 | } 69 | } 70 | const blob = await zip.generateAsync({ type: "blob" }); 71 | return { 72 | name, 73 | blob, 74 | }; 75 | }; 76 | 77 | 78 | export const downloadNamedBlob = async ( 79 | { name, blob }: NamedBlob, 80 | ): Promise => { 81 | saveAs(blob, name); 82 | }; 83 | -------------------------------------------------------------------------------- /src/libs/fnv1a.js: -------------------------------------------------------------------------------- 1 | import { 2 | BigInt, 3 | asUintN, 4 | bitwiseXor, 5 | multiply 6 | } from "jsbi"; 7 | 8 | ///Calculates FNV-1a 128-bit hash on a string, ArrayBuffer or Uint8Array. 9 | function fnv1a128(v, start = 0, maxVal = 9000) { 10 | let hash = BigInt("144066263297769815596495629667062367629"); 11 | const fnvPrime = BigInt("309485009821345068724781371"); 12 | 13 | if ((typeof v) == "string"){ 14 | const j = Math.min(start+maxVal, v.length); 15 | for (let i = start; i < j; i++) { 16 | hash = bitwiseXor(hash, BigInt(v.charCodeAt(i))); 17 | hash = asUintN(128, multiply(hash, fnvPrime)); 18 | } 19 | } 20 | else if (v instanceof Uint8Array){ 21 | const j = Math.min(start+maxVal, v.byteLength); 22 | for (let i = start; i < j; i++) { 23 | hash = bitwiseXor(hash, BigInt(v[i])); 24 | hash = asUintN(128, multiply(hash, fnvPrime)); 25 | } 26 | } 27 | else if (v instanceof ArrayBuffer){ 28 | const b = new Uint8Array(v); 29 | const j = Math.min(start+maxVal, b.byteLength); 30 | for (let i = start; i < j; i++) { 31 | hash = bitwiseXor(hash, BigInt(b[i])); 32 | hash = asUintN(128, multiply(hash, fnvPrime)); 33 | } 34 | } 35 | return ("0000000000000000000000000000000" + hash.toString(16)).substr(-32); 36 | } 37 | 38 | export default fnv1a128; 39 | 40 | -------------------------------------------------------------------------------- /src/models/utilts.ts: -------------------------------------------------------------------------------- 1 | import { flow } from "lodash"; 2 | 3 | export const dataUriToByteString = (dataUri: string): string => { 4 | const BASE64_MARKER = ';base64,'; 5 | const base64Index = dataUri.indexOf(BASE64_MARKER) + BASE64_MARKER.length; 6 | const base64 = dataUri.substring(base64Index); 7 | const byteString = window.atob(base64); 8 | return byteString; 9 | }; 10 | 11 | export const byteStringToBytes = (byteString: string): Uint8Array => { 12 | const byteStringLength = byteString.length; 13 | const array = new Uint8Array(new ArrayBuffer(byteStringLength)); 14 | for(let i = 0; i < byteStringLength; i++) 15 | array[i] = byteString.charCodeAt(i); 16 | return array; 17 | }; 18 | 19 | export const bytesToObjectUrl = (bytes: Uint8Array): string => { 20 | const blob = new Blob([bytes], { type: "application/octet-stream" }); 21 | const url = URL.createObjectURL(blob); 22 | return url; 23 | }; 24 | 25 | export const dataUriToUrl = flow( 26 | dataUriToByteString, 27 | byteStringToBytes, 28 | bytesToObjectUrl, 29 | ); 30 | -------------------------------------------------------------------------------- /src/pages/About/InfoItem.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 46 | 47 | -------------------------------------------------------------------------------- /src/pages/About/index.ts: -------------------------------------------------------------------------------- 1 | export { default as default } from "./About.vue"; -------------------------------------------------------------------------------- /src/pages/Browse/PatternEntry.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/pages/Browse/index.js: -------------------------------------------------------------------------------- 1 | export { default as default } from "./Browse.vue"; -------------------------------------------------------------------------------- /src/pages/Editor/ACNHToACNLInfo.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/pages/Editor/ACNLToACNHInfo.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/Editor/ColorTools/index.js: -------------------------------------------------------------------------------- 1 | import ColorTools from "./ColorTools"; 2 | export default ColorTools; -------------------------------------------------------------------------------- /src/pages/Editor/ConvertImage/Stages/Uploading.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /src/pages/Editor/ConvertImage/index.js: -------------------------------------------------------------------------------- 1 | export { default as default } from "./ConvertImage.vue"; -------------------------------------------------------------------------------- /src/pages/Editor/ImportMenuItem.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 71 | 72 | -------------------------------------------------------------------------------- /src/pages/Editor/KeypressInstructions.md: -------------------------------------------------------------------------------- 1 | # Keypress Generator Instructions 2 | 3 | Use the copied script with the instructions found [here](https://github.com/Thulinma/SwitchControllerEmulator) to automate drawing the patterns in ACNH. -------------------------------------------------------------------------------- /src/pages/Editor/index.js: -------------------------------------------------------------------------------- 1 | import Editor from "./Editor"; 2 | export default Editor; -------------------------------------------------------------------------------- /src/pages/FAQ/FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## ZOMG this thing doesn't work!? 4 | 5 | The ACNL pattern tool is written in Vue, using modern CSS and JavaScript 6 | features. As such, it requires a decently standards-compliant browser. 7 | The latest version of Chrome / Chromium / Firefox / Android / iOS are tested 8 | and should work. No promises on anything else. Internet Explorer will probably 9 | choke on the entire page, while Edge and Safari will will run into issues 10 | with certain features. 11 | 12 | ## How do I use this thing? 13 | 14 | Click a color on the 3x5 palette and then just "draw" on the one of the pattern 15 | representations. All of the zoom levels are drawable and they will update at 16 | the same time. When you want to import to ACNL, scan the QR code by clicking the 17 | preview button. 18 | 19 | 20 | ## Why isn't my QR code being read? It works in the game! 21 | 22 | This version of the tool uses a TypeScript port of the ZXing QR library. While 23 | this library is quite decent, it has issues with multiple QR codes in a single 24 | image and has issues with blurry/zoomed QR codes. We're working on improving 25 | reading accuracy, but QR code reading is a pretty complicated thing and it 26 | will never be "perfect". 27 | 28 | Please note that the QR must be of a sufficient size and quality in order to 29 | be properly read. If any produced QR code images are downscaled, there is a 30 | high chance it will be able to be read. 31 | 32 | 33 | ## What are .ACNL and .ACNH files? 34 | 35 | It's a small binary format storage for ACNL-compatible patterns. They contain 36 | the pattern. It uses the exact binary format also used inside the QR codes and 37 | inside the game's savedata, so this is a highly accurate storage format that 38 | will never lose any details (unlike QR codes, which may become unreadable when) 39 | the image is resized or reduced in quality. 40 | 41 | ## What are .DAT files? 42 | 43 | People with a hacked 3DS can export their ACNL savegame as a "garden.dat" file. 44 | The tool can read patterns from these files and import/export them, even if the 45 | patterns normally cannot be exported to QR codes. 46 | 47 | 48 | ## Who made this? Who made what? 49 | 50 | * Thulinma: reverse engineering format, drawing system, image conversion, 3d renders, pattern database backend 51 | * DamSenViet: ui/ux design, stack selection, custom toolchain, leading component programming, animations, api integration 52 | * Myumi: QR code reading improvements, component programming 53 | * Tero: graphic design, icon design 54 | * MelonSpeedruns: data mining 55 | 56 | ## What are the planned future updates? 57 | 58 | It's hard to keep track of this on this page. For the latest information and 59 | discussion, please join our [Discord server](https://discord.gg/9rGkZNk). 60 | 61 | ## Is there an offline version? 62 | 63 | It's in the works. We've developed with this feature in mind in case we ever 64 | decide to follow up on it. 65 | 66 | ## How do I reach you guys? Can I make a suggestion? 67 | 68 | Please join our [Discord server](https://discord.gg/9rGkZNk). 69 | -------------------------------------------------------------------------------- /src/pages/FAQ/FAQ.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /src/pages/FAQ/index.ts: -------------------------------------------------------------------------------- 1 | export { default as default } from "./FAQ.vue"; -------------------------------------------------------------------------------- /src/pages/Missing.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/pages/Updates/Updates.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/pages/Updates/index.ts: -------------------------------------------------------------------------------- 1 | export { default as default } from "./Updates.vue"; -------------------------------------------------------------------------------- /src/pages/moderator/Index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/plugins/fragment.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { Plugin } from "vue-fragment"; 3 | 4 | Vue.use(Plugin); -------------------------------------------------------------------------------- /src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuetify from "vuetify/lib"; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({}); -------------------------------------------------------------------------------- /src/routers/index.ts: -------------------------------------------------------------------------------- 1 | import qs from "qs"; 2 | import Vue from "vue"; 3 | import VueRouter from 'vue-router'; 4 | 5 | Vue.use(VueRouter); 6 | 7 | const mode = "history"; 8 | 9 | const routes = [ 10 | { 11 | path: "/", 12 | component: () => import( 13 | /* webpackChunkName: "Home" */ 14 | "@/pages/Home.vue" 15 | ), 16 | }, 17 | { 18 | path: "/browse", 19 | component: () => import( 20 | /* webpackChunkName: "Browse" */ 21 | "@/pages/Browse" 22 | ), 23 | }, 24 | { 25 | path: "/editor", 26 | component: () => import( 27 | /* webpackChunkName: "Editor" */ 28 | "@/pages/Editor" 29 | ), 30 | }, 31 | { 32 | path: "/faq", 33 | component: () => import( 34 | /* webpackChunkName: "FAQ" */ 35 | "@/pages/FAQ" 36 | ), 37 | }, 38 | { 39 | path: "/about", 40 | component: () => import( 41 | /* webpackChunkName: "About" */ 42 | "@/pages/About" 43 | ), 44 | }, 45 | { 46 | path: "/updates", 47 | component: () => import( 48 | /* webpackChunkName: "Updates" */ 49 | "@/pages/Updates" 50 | ), 51 | }, 52 | { 53 | path: "/moderator", 54 | component: () => import( 55 | /* webpackChunkName: "Moderator" */ 56 | "@/pages/moderator/Index.vue" 57 | ), 58 | children: [ 59 | { 60 | path: "login", 61 | component: () => import( 62 | /* webpackChunkName: "ModeratorLogin" */ 63 | "@/pages/moderator/Login.vue" 64 | ), 65 | }, 66 | { 67 | path: "dashboard", 68 | component: () => import( 69 | /* webpackChunkName: "ModeratorDashboard" */ 70 | "@/pages/moderator/Dashboard.vue" 71 | ), 72 | }, 73 | ] 74 | }, 75 | { 76 | path: "*", 77 | component: () => import( 78 | /* webpackChunkName: "Missing" */ 79 | "@/pages/Missing.vue" 80 | ), 81 | }, 82 | ]; 83 | 84 | export default new VueRouter({ 85 | mode, 86 | routes, 87 | // replace default query parsing behavior 88 | parseQuery(query) { 89 | return qs.parse(query); 90 | }, 91 | stringifyQuery(query) { 92 | const result = qs.stringify(query); 93 | return result ? ('?' + result) : ''; 94 | } 95 | }); 96 | -------------------------------------------------------------------------------- /src/shims-env.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare global { 3 | const env: { 4 | NODE_ENV: string, 5 | IS_OFFLINE: boolean, 6 | API_URL: string, 7 | }; 8 | } 9 | 10 | export {}; -------------------------------------------------------------------------------- /src/shims-loaders.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.module.css"; 2 | 3 | declare module "*.scss" { 4 | const value: Record; 5 | export default value; 6 | } 7 | 8 | declare module "*.sass" { 9 | const value: Record; 10 | export default value; 11 | } 12 | 13 | declare module "*.md" { 14 | import Vue from "vue" 15 | export default Vue; 16 | } 17 | 18 | declare module "*.png" { 19 | const value: string; 20 | export default value; 21 | } 22 | 23 | declare module "*.jpg" { 24 | const value: string; 25 | export default value; 26 | } 27 | 28 | declare module "*.jpeg" { 29 | const value: string; 30 | export default value; 31 | } 32 | 33 | declare module "*.svg" { 34 | const value: string; 35 | export default value; 36 | } 37 | 38 | declare module "*.svg?inline" { 39 | import Vue from "vue"; 40 | export default Vue; 41 | } 42 | 43 | declare module "*.gif" { 44 | const value: string; 45 | export default value; 46 | } 47 | 48 | declare module "*.gltf" { 49 | const value: string; 50 | export default value; 51 | } 52 | 53 | declare module "*.csv" { 54 | const value: string; 55 | export default value; 56 | } -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import Vue from "vue" 3 | export default Vue 4 | } -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex from "vuex"; 3 | import { RootState } from "./types"; 4 | import browse from "./modules/browse"; 5 | import profile from "./modules/profile"; 6 | import storage from "./modules/storage"; 7 | 8 | Vue.use(Vuex); 9 | 10 | export default new Vuex.Store({ 11 | modules: { 12 | profile, 13 | browse, 14 | storage, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/store/modules/browse/actions.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { ActionTree } from "vuex"; 3 | import { RootState } from "@/store/types"; 4 | import { State } from "./state"; 5 | import { PatternEntry } from "@/libs/origin"; 6 | import { 7 | SearchOptions, 8 | correctOptions, 9 | cloneOptions, 10 | } from "./helpers"; 11 | import { 12 | Sorting, 13 | browse, 14 | } from "@/libs/origin"; 15 | 16 | const ORIGIN_PAGE_SIZE = 300; 17 | 18 | export default { 19 | /** 20 | * Fetches and returns results for a given set of search options. 21 | */ 22 | async updateResults({ state, commit }, { 23 | options, 24 | localPageSize, 25 | localPageNumber, 26 | }: { 27 | options: SearchOptions, 28 | localPageSize: number, 29 | localPageNumber: number, 30 | }): Promise { 31 | // get early reference to results of options to prevent modifications when results swap 32 | const targetOptions = cloneOptions(options); 33 | const targetKey = JSON.stringify(options); 34 | const isRandomized = ( 35 | targetOptions.titleFilter === "" && 36 | targetOptions.sorting === Sorting.Random 37 | ); 38 | const targetResults = (() => { 39 | if (!state.resultsCache.hasOwnProperty(targetKey)) 40 | state.resultsCache[targetKey] = []; 41 | return state.resultsCache[targetKey]; 42 | })(); 43 | const targetFetchedPages = (() => { 44 | if (!state.fetchedPagesCache.hasOwnProperty(targetKey)) 45 | state.fetchedPagesCache[targetKey] = []; 46 | return state.fetchedPagesCache[targetKey].slice(); // copy 47 | })(); 48 | 49 | 50 | // convert local page numbe to result indexes 51 | const startingResultIndex = localPageNumber * localPageSize; 52 | const endingResultIndex = (localPageNumber + 1) * localPageSize; 53 | 54 | // convert pages number to server page number(s) 55 | const originPageSize = ORIGIN_PAGE_SIZE; 56 | const startingOriginPageNumber = Math 57 | .floor(startingResultIndex / originPageSize); 58 | const endingOriginPageNumber = Math 59 | .ceil(endingResultIndex / originPageSize) - 1; 60 | const inclusiveRange = function* (start: number, end: number) { 61 | for (let i = start; i <= end; ++i) 62 | yield i; 63 | }; 64 | 65 | // all origin page numbers that need to be fetched 66 | const originPageNumbers = [...inclusiveRange( 67 | startingOriginPageNumber, 68 | endingOriginPageNumber, 69 | )]; 70 | const unfetchedOriginPageNumbers = originPageNumbers 71 | .filter(originPageNumber => !targetFetchedPages.includes(originPageNumber)) 72 | 73 | // console.log(startingResultIndex, endingResultIndex, unfetchedOriginPageNumbers); 74 | 75 | const updatedResults = targetResults.slice(); 76 | // only attempt pages that have not been fetched 77 | for (const originPageNumber of unfetchedOriginPageNumbers) { 78 | const { 79 | totalResultsCount: originTotalResultsCount, 80 | pageResults: originPageResults, 81 | } = await browse(correctOptions({ 82 | ...targetOptions, 83 | originPageNumber, 84 | })); 85 | 86 | // if results are random, infinite possible results, don't update length 87 | updatedResults.length = originTotalResultsCount; 88 | // merge results 89 | for (let j = 0; j < originPageResults.length; ++j) { 90 | updatedResults[(originPageNumber * originPageSize) + j] = originPageResults[j]; 91 | } 92 | // last page, we know we can skip all following origin pages 93 | if (!isRandomized && (originPageResults.length < originPageSize)) break; 94 | } 95 | 96 | commit('updateFetchedPagesCache', { 97 | targetKey, 98 | value: targetFetchedPages 99 | .concat(unfetchedOriginPageNumbers), 100 | }); 101 | commit('updateResultsCache', { 102 | targetKey, 103 | value: updatedResults, 104 | }) 105 | return updatedResults.slice(); 106 | }, 107 | /** 108 | * Fetches and returns results for a set of search options and page (or 109 | * doesn't if it's been fetched already). 110 | */ 111 | async updatePage({ dispatch }, { 112 | options, 113 | localPageSize, 114 | localPageNumber, 115 | }: { 116 | options: SearchOptions, 117 | localPageSize: number, 118 | localPageNumber: number, 119 | }): Promise { 120 | const startingResultIndex = localPageNumber * localPageSize; 121 | const endingResultIndex = (localPageNumber + 1) * localPageSize; 122 | return (await dispatch('updateResults', { 123 | options, 124 | localPageSize, 125 | localPageNumber, 126 | }) as PatternEntry[]) 127 | .slice(startingResultIndex, endingResultIndex) 128 | .filter(result => result != null); 129 | }, 130 | } as ActionTree; 131 | -------------------------------------------------------------------------------- /src/store/modules/browse/getters.ts: -------------------------------------------------------------------------------- 1 | import { GetterTree } from "vuex"; 2 | import { RootState } from "@/store/types"; 3 | import { State } from "./state"; 4 | 5 | export default { 6 | } as GetterTree; 7 | -------------------------------------------------------------------------------- /src/store/modules/browse/helpers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Sorting, 3 | StyleTag, 4 | TypeTag, 5 | browse, 6 | } from "@/libs/origin"; 7 | 8 | 9 | export interface SearchOptions { 10 | titleFilter: string, 11 | authorFilter: string, 12 | townFilter: string, 13 | styleTagsFilter: [(StyleTag | null), (StyleTag | null), (StyleTag | null)], 14 | typeTagsFilter: [(TypeTag | null), (TypeTag | null), (TypeTag | null)], 15 | sorting: Sorting, 16 | }; 17 | 18 | export interface ApiSearchOptions extends SearchOptions { 19 | originPageNumber: number, 20 | } 21 | 22 | export type Copy = { 23 | [Property in keyof ApiSearchOptions]: ApiSearchOptions[Property]; 24 | } 25 | 26 | export type BrowseSearchOptions = Parameters[0]; 27 | 28 | // resolve inconsistencies between api and store 29 | export const optionsMap: { 30 | [Property in keyof ApiSearchOptions]: keyof BrowseSearchOptions; 31 | } = { 32 | titleFilter: "q", 33 | authorFilter: "a", 34 | townFilter: "t", 35 | styleTagsFilter: "st", 36 | typeTagsFilter: "tt", 37 | originPageNumber: "start", 38 | sorting: "sorting", 39 | }; 40 | 41 | 42 | // should be applied before making api call 43 | export const remappedOptions = ( 44 | options: ApiSearchOptions 45 | ): BrowseSearchOptions => { 46 | const remapped = {} as BrowseSearchOptions; 47 | for (const [sourceKey, value] of Object.entries(options)) { 48 | // @ts-ignore 49 | const remappedKey = optionsMap[sourceKey] as (keyof BrowseSearchOptions); 50 | remapped[remappedKey] = value; 51 | }; 52 | return remapped; 53 | }; 54 | 55 | // maps properties to correct options before api call 56 | export const correctOptions = ( 57 | options: ApiSearchOptions, 58 | ): BrowseSearchOptions => { 59 | return remappedOptions(options); 60 | }; 61 | 62 | 63 | export const createOptions = (): SearchOptions => ({ 64 | titleFilter: "", 65 | authorFilter: "", 66 | townFilter: "", 67 | styleTagsFilter: [null, null, null], 68 | typeTagsFilter: [null, null, null], 69 | sorting: Sorting.Random, 70 | }); 71 | 72 | 73 | /** 74 | * Makes a clone / deep copy of the options. 75 | */ 76 | export const cloneOptions = ( 77 | options: SearchOptions, 78 | ): SearchOptions => { 79 | return JSON.parse(JSON.stringify(options)); 80 | }; 81 | -------------------------------------------------------------------------------- /src/store/modules/browse/index.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "vuex"; 2 | import { RootState } from "@/store/types"; 3 | import state, { State } from "./state"; 4 | import getters from "./getters"; 5 | import mutations from "./mutations"; 6 | import actions from "./actions"; 7 | 8 | export default { 9 | namespaced: true, 10 | state, 11 | getters, 12 | mutations, 13 | actions, 14 | } as Module; 15 | -------------------------------------------------------------------------------- /src/store/modules/browse/mutations.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { MutationTree } from "vuex"; 3 | import { State, createDefaultState } from "./state"; 4 | 5 | export default { 6 | reset(state) { 7 | Object.assign(state, createDefaultState()); 8 | }, 9 | updateResultsCache(state, { 10 | targetKey, 11 | value, 12 | }) { 13 | Vue.set(state.resultsCache, targetKey, value); 14 | }, 15 | updateFetchedPagesCache(state, { 16 | targetKey, 17 | value, 18 | }) { 19 | Vue.set(state.fetchedPagesCache, targetKey, value); 20 | }, 21 | } as MutationTree; -------------------------------------------------------------------------------- /src/store/modules/browse/state.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PatternEntry 3 | } from "@/libs/origin"; 4 | 5 | export interface State { 6 | resultsCache: Record, 7 | fetchedPagesCache: Record 8 | } 9 | 10 | export const createDefaultState = (): State => ({ 11 | resultsCache: {}, 12 | fetchedPagesCache: {}, 13 | }); 14 | 15 | export default createDefaultState(); -------------------------------------------------------------------------------- /src/store/modules/profile/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree } from "vuex"; 2 | import { RootState } from "@/store/types"; 3 | import { State } from "./state"; 4 | import { 5 | UploadEntry, 6 | modLogIn, 7 | modPending, 8 | modApprove, 9 | modDelete, 10 | } from "@/libs/origin"; 11 | 12 | export default { 13 | async logIn({ commit }, { 14 | username, 15 | password, 16 | }: { 17 | username: string, 18 | password: string, 19 | }): Promise { 20 | const { localStorage } = window; 21 | const token = await modLogIn(username, password); 22 | if (!token) { 23 | localStorage.removeItem("username"); 24 | localStorage.removeItem("password"); 25 | return; 26 | } 27 | // simulate session 28 | localStorage.setItem("username", username); 29 | localStorage.setItem("password", password); 30 | commit("setLogin", { username, password, token }); 31 | }, 32 | async logOut({ commit }): Promise { 33 | localStorage.removeItem("username"); 34 | localStorage.removeItem("password"); 35 | commit("reset"); 36 | }, 37 | // simulate session 38 | async continue({ dispatch }): Promise { 39 | const username = localStorage.getItem("username"); 40 | const password = localStorage.getItem("password"); 41 | if (username && password) { 42 | await dispatch("logIn", { username, password }); 43 | } 44 | }, 45 | async getPending({ state, commit }): Promise { 46 | const { token } = state; 47 | const pending = await modPending(token); 48 | commit("setPending", { pending }); 49 | }, 50 | async approve({ state, getters, commit, dispatch }, { 51 | hash, 52 | options, 53 | }: { 54 | hash: string, 55 | options: UploadEntry 56 | }) { 57 | const { token } = state; 58 | await modApprove(hash, options, token); 59 | 60 | const index = getters.pendingHashes.indexOf(hash); 61 | const pending = state.pending.slice(); 62 | pending.splice(index, 1); 63 | if (pending.length < 10) await dispatch('getPending'); 64 | else commit('setPending', { pending }); 65 | }, 66 | async reject({ state, getters, commit, dispatch }, hash) { 67 | const { token } = state; 68 | await modDelete(hash, token); 69 | 70 | const index = getters.pendingHashes.indexOf(hash); 71 | const pending = state.pending.slice(); 72 | pending.splice(index, 1); 73 | if (pending.length < 10) await dispatch("getPending"); 74 | else commit("setPending", { pending }); 75 | }, 76 | } as ActionTree; 77 | -------------------------------------------------------------------------------- /src/store/modules/profile/getters.ts: -------------------------------------------------------------------------------- 1 | import { GetterTree } from "vuex"; 2 | import { RootState } from "@/store/types"; 3 | import { State } from "./state"; 4 | import DrawingTool from "@/libs/DrawingTool"; 5 | 6 | export default { 7 | isLoggedIn: (state): boolean => { 8 | return Boolean(state.token); 9 | }, 10 | pendingHashes: (state): string[] => { 11 | const { pending } = state; 12 | const hashes = pending.map(i => new DrawingTool(i.bytes).pixelHash); 13 | return hashes; 14 | }, 15 | } as GetterTree; 16 | -------------------------------------------------------------------------------- /src/store/modules/profile/index.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "vuex"; 2 | import { RootState } from "@/store/types"; 3 | import state, { State } from "./state"; 4 | import getters from "./getters"; 5 | import mutations from "./mutations"; 6 | import actions from "./actions"; 7 | 8 | export default { 9 | namespaced: true, 10 | state, 11 | getters, 12 | mutations, 13 | actions, 14 | } as Module; 15 | -------------------------------------------------------------------------------- /src/store/modules/profile/mutations.ts: -------------------------------------------------------------------------------- 1 | import { MutationTree } from "vuex"; 2 | import { UploadEntry } from "@/libs/origin"; 3 | import { State, createDefaultState } from "./state"; 4 | 5 | export default { 6 | reset(state): void { 7 | Object.assign(state, createDefaultState()); 8 | }, 9 | setLogin(state, { 10 | username, 11 | password, 12 | token, 13 | }: { 14 | username: string, 15 | password: string, 16 | token: string, 17 | }): void { 18 | Object.assign(state, { 19 | username, 20 | password, 21 | token, 22 | }); 23 | }, 24 | setPending: (state, { pending }: { 25 | pending: Array, 26 | }): void => { 27 | Object.assign(state, { pending } ); 28 | } 29 | } as MutationTree; 30 | -------------------------------------------------------------------------------- /src/store/modules/profile/state.ts: -------------------------------------------------------------------------------- 1 | import { UploadEntry } from "@/libs/origin"; 2 | 3 | export interface State { 4 | token: string, 5 | username: string, 6 | password: string, 7 | pending: Array, 8 | } 9 | 10 | export const createDefaultState = (): State => ({ 11 | token: "", 12 | username: "", 13 | password: "", 14 | pending: [], 15 | }); 16 | 17 | export default createDefaultState(); -------------------------------------------------------------------------------- /src/store/modules/storage/actions.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree } from "vuex"; 2 | import { RootState } from "@/store/types"; 3 | import { State } from "./state"; 4 | 5 | export default { 6 | async add({ state, commit }, patternItems) { 7 | if (!state.initialized) 8 | commit('init'); 9 | commit('add', patternItems); 10 | }, 11 | async remove({ state, commit }, patternItems) { 12 | if (!state.initialized) 13 | commit('init'); 14 | commit('remove', patternItems); 15 | }, 16 | } as ActionTree; -------------------------------------------------------------------------------- /src/store/modules/storage/getters.ts: -------------------------------------------------------------------------------- 1 | import { GetterTree } from "vuex"; 2 | import { RootState } from "@/store/types"; 3 | import { State } from "./state"; 4 | import { PatternItem } from "@/libs/storage"; 5 | 6 | 7 | export default { 8 | patternItems(state): PatternItem[] { 9 | return Object.values(state.patternStorage); 10 | }, 11 | } as GetterTree; 12 | -------------------------------------------------------------------------------- /src/store/modules/storage/index.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "vuex"; 2 | import { RootState } from "@/store/types"; 3 | import state, { State } from "./state"; 4 | import getters from "./getters"; 5 | import mutations from "./mutations"; 6 | import actions from "./actions"; 7 | 8 | export default { 9 | namespaced: true, 10 | state, 11 | getters, 12 | mutations, 13 | actions, 14 | } as Module; 15 | -------------------------------------------------------------------------------- /src/store/modules/storage/mutations.ts: -------------------------------------------------------------------------------- 1 | import { MutationTree } from "vuex"; 2 | import { State, createDefaultState } from "./state"; 3 | import { 4 | PatternItem, 5 | saveToPatternStorage, 6 | deleteFromPatternStorage, 7 | loadFromLocalStorage, 8 | saveToLocalStorage, 9 | } from "@/libs/storage"; 10 | 11 | 12 | export default { 13 | reset(state: State): void { 14 | Object.assign(state, createDefaultState()); 15 | }, 16 | init(state): void { 17 | if (state.initialized) 18 | return; 19 | state.patternStorage = loadFromLocalStorage(); 20 | state.initialized = true; 21 | }, 22 | add(state: State, patternItems: PatternItem[]): void { 23 | for (const patternItem of patternItems) 24 | saveToPatternStorage(state.patternStorage, patternItem); 25 | saveToLocalStorage(state.patternStorage); 26 | }, 27 | remove(state: State, patternItems: PatternItem[]): void { 28 | for (const patternItem of patternItems) 29 | deleteFromPatternStorage(state.patternStorage, patternItem); 30 | saveToLocalStorage(state.patternStorage); 31 | }, 32 | } as MutationTree; 33 | -------------------------------------------------------------------------------- /src/store/modules/storage/state.ts: -------------------------------------------------------------------------------- 1 | import { PatternStorage } from "@/libs/storage"; 2 | 3 | export interface State { 4 | patternStorage: PatternStorage, 5 | initialized: boolean, 6 | } 7 | 8 | export const createDefaultState = (): State => ({ 9 | patternStorage: {}, 10 | initialized: false, 11 | }); 12 | 13 | export default createDefaultState(); -------------------------------------------------------------------------------- /src/store/storage.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Stored Patterns (JSON) 4 | * @typedef StorageItem 5 | * @property {string} name 6 | * @property {string} createdAt 7 | * @property {string} drawingTool 8 | */ -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface RootState {}; 3 | -------------------------------------------------------------------------------- /src/styles/animations.scss: -------------------------------------------------------------------------------- 1 | @mixin spin($duration: 3s, $transforms: translate(0, 0)) { 2 | @keyframes spin { 3 | from { 4 | transform: $transforms rotate(0deg) 5 | } 6 | to { 7 | transform: $transforms rotate(360deg) 8 | } 9 | } 10 | animation:spin $duration linear infinite; 11 | } -------------------------------------------------------------------------------- /src/styles/colors.scss: -------------------------------------------------------------------------------- 1 | // colors and backgrounds 2 | 3 | // whites and neutrals 4 | $white: #ffffff; 5 | $ecru-white: #f7f4e6; 6 | $soapstone: #ece3da; 7 | $orange-white: #e5e2ce; 8 | $albescent-white: #e0dbca; 9 | 10 | // grays 11 | $silver-sand: #bababa; 12 | $bon-jour: #e2e2e2; 13 | 14 | // browns 15 | $domino: #695b4d; 16 | $van-cleef: #503c35; // primary darkened 17 | $jambalaya: #624c37; 18 | $olive-haze: #8c7c64; 19 | $donkey-brown: #a39277; // canvas polkadot color 20 | $bison-hide: #b6a79a; 21 | 22 | // bluish greens 23 | $tiffany-blue: #00b6a9; // primary blush-ish green 24 | $tiffany-blue-light: #13c3b2; 25 | $turquoise: #4fe1d6; 26 | 27 | $robin-egg-blue: #00d2c2; 28 | $persian-green: #00a797; 29 | $frosted-mint: #e0fff7; 30 | 31 | // pinks 32 | $piggy-pink: #ffd8e2; 33 | $cinderella: #f7d7c9; 34 | $cannon-pink: #893e5d; 35 | $geraldine: #fd8093; 36 | 37 | $salmon: #fca4b1; 38 | $light-pink: #ffafbc; 39 | $azalea: #fbbbc2; 40 | $pink-lace: #ffcad2; 41 | $provincial-pink: #fff7f2; 42 | 43 | // greens 44 | $chrome-white: #e6f0cf; 45 | 46 | @mixin polkadots($primary, $secondary) { 47 | background-image: radial-gradient($secondary 2px, transparent 2px), radial-gradient($secondary 2px, transparent 2px); 48 | background-color: $primary; 49 | background-position: 0 0, 50px 50px; 50 | background-size: 20px 20px; 51 | background-repeat: repeat; 52 | } 53 | 54 | @mixin moving-polkadots($duration: 5s) { 55 | @keyframes moving-polkadots { 56 | from { 57 | background-position: 0px 0px, 50px 50px; 58 | } 59 | to { 60 | background-position: 50px 50px, 100px 100px; 61 | } 62 | } 63 | animation: moving-polkadots $duration linear infinite; 64 | } 65 | 66 | @mixin stripes($first, $second, $width) { 67 | background-image: repeating-linear-gradient(-45deg, $first, $first $width, $second $width, $second $width * 2); 68 | background-size: 200% 200%; 69 | } 70 | 71 | @mixin moving-stripes($duration: 20s) { 72 | @keyframes barberpole { 73 | from { 74 | background-position: 100% 100%; 75 | } 76 | to { 77 | background-position: 0% 0%; 78 | } 79 | } 80 | animation: barberpole $duration linear infinite; 81 | } 82 | 83 | 84 | :export { 85 | white: $white; 86 | ecruWhite: $ecru-white; 87 | soapstone: $soapstone; 88 | orangeWhite: $orange-white; 89 | albescent-white: $albescent-white; 90 | silverSand: $silver-sand; 91 | bonJour: $bon-jour; 92 | domino: $domino; 93 | vanCleef: $van-cleef; 94 | jambalaya: $jambalaya; 95 | oliveHaze: $olive-haze; 96 | donkeyBrown: $donkey-brown; 97 | bisonHide: $bison-hide; 98 | tiffanyBlue: $tiffany-blue; 99 | tiffanyBlueLight: $tiffany-blue-light; 100 | turqoise: $turquoise; 101 | robinEggBlue: $robin-egg-blue; 102 | persianGreen: $persian-green; 103 | frostedMint: $frosted-mint; 104 | piggyPink: $piggy-pink; 105 | cinderella: $cinderella; 106 | cannonPink: $cannon-pink; 107 | geraldine: $geraldine; 108 | salmon: $salmon; 109 | lightPink: $light-pink; 110 | azalea: $azalea; 111 | pinkLace: $pink-lace; 112 | provincialPink: $provincial-pink; 113 | chromeWhite: $chrome-white; 114 | } -------------------------------------------------------------------------------- /src/styles/functions.scss: -------------------------------------------------------------------------------- 1 | @function calc-canvas-size($scale) { 2 | @return $scale * 64px; 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/icon-colors.scss: -------------------------------------------------------------------------------- 1 | // foreground and background colors of the icons 2 | 3 | // updates 4 | $copper: #e49366; // primary background 5 | $wax-flower: #edb799; // secondary background 6 | 7 | // brows 8 | $keppel: #5bbf98; 9 | $pearl-aqua: #83d8bb; 10 | 11 | // editor 12 | $salmon: #fca4b1; 13 | $pink: #fdbfc7; 14 | 15 | // faq 16 | $cream-can: #f8cc61; 17 | $cape-honey: #fbe1a3; 18 | 19 | // discord 20 | $portage: #8b9df3; 21 | $perano: #b3bdfd; 22 | 23 | // home 24 | $pastel-red: #ff6b62; 25 | $cream-can: #f8cc61; 26 | 27 | // about us 28 | $de-york: #89c68a; 29 | 30 | // twitter 31 | $portage: #8b9df3; 32 | -------------------------------------------------------------------------------- /src/styles/overrides.scss: -------------------------------------------------------------------------------- 1 | @use "styles/colors" as colors; 2 | 3 | @mixin v-btn( 4 | $text-color: colors.$ecru-white, 5 | $background-color: colors.$olive-haze, 6 | ) { 7 | text-transform: none; 8 | font-weight: 600; 9 | letter-spacing: normal; 10 | color: $text-color !important; 11 | background-color: $background-color !important; 12 | &::v-deep > .v-btn__content { 13 | @content; 14 | } 15 | } 16 | 17 | @mixin v-text-field( 18 | $text-color: colors.$jambalaya, 19 | $background-color: colors.$cinderella, 20 | $border-color: colors.$olive-haze, 21 | ) { 22 | &::v-deep { 23 | input { 24 | color: $text-color; 25 | font-weight: 600; 26 | } 27 | .v-input__slot { 28 | background-color: $background-color; 29 | label { color: $text-color !important; } 30 | } 31 | &:not(.v-input--is-focused) { fieldset { border-color: $border-color; }} 32 | &.v-input--is-focused { 33 | fieldset { border-color: $border-color; } 34 | .v-label--active { color: $text-color !important; } 35 | } 36 | .v-messages__message { color: $text-color !important } 37 | button[aria-label="clear icon"] { color: $text-color !important; } 38 | @content; 39 | } 40 | } 41 | 42 | @mixin v-select( 43 | $text-color: colors.$jambalaya, 44 | $background-color: colors.$cinderella, 45 | $border-color: colors.$olive-haze 46 | ) { 47 | &::v-deep { 48 | .v-input__icon .v-icon { color: $text-color !important; } 49 | .v-select__selections { color: $text-color; } 50 | .v-input__slot { 51 | background-color: $background-color; 52 | label { color: $text-color !important; } 53 | } 54 | &:not(.v-input--is-focused) { fieldset { border-color: $border-color; }} 55 | &.v-input--is-focused { 56 | fieldset { border-color: $border-color; } 57 | .v-label--active { color: $text-color !important; } 58 | } 59 | @content; 60 | } 61 | } 62 | 63 | @mixin v-menu( 64 | $text-color: colors.$jambalaya, 65 | $background-color: colors.$cinderella, 66 | $hover-color: colors.$salmon, 67 | ) { 68 | box-shadow: none; 69 | .v-list { @include v-list($text-color, $background-color); } 70 | @content; 71 | } 72 | 73 | @mixin v-list( 74 | $text-color: colors.$jambalaya, 75 | $background-color: colors.$cinderella, 76 | $hover-color: rgba(0, 0, 0, 0), 77 | ) { 78 | background-color: $background-color; 79 | .v-list-item { 80 | @include v-list-item( 81 | $text-color, 82 | $background-color, 83 | $hover-color 84 | ) 85 | } 86 | @content; 87 | } 88 | 89 | @mixin v-list-item( 90 | $text-color: colors.$jambalaya, 91 | $background-color: colors.$cinderella, 92 | $hover-color: rgba(0, 0, 0, 0), 93 | ) { 94 | &:hover { background-color: $hover-color !important } 95 | .v-list-item__content { color: $text-color !important; } 96 | .v-list-item__title { color: $text-color !important; } 97 | &.v-list-item--active { color: black !important; } 98 | @content; 99 | } 100 | 101 | @mixin v-stepper-step($text-color) { 102 | &::v-deep { 103 | .v-stepper__step--active, 104 | .v-stepper__step--complete { 105 | .v-stepper__label { 106 | color: $text-color, 107 | } 108 | } 109 | } 110 | } 111 | 112 | @mixin v-pagination( 113 | $text-color, 114 | $background-color, 115 | $hover-background-color, 116 | ) { 117 | &::v-deep { 118 | .v-pagination__navigation, 119 | .v-pagination__item { 120 | box-shadow: unset !important; 121 | color: $text-color !important; 122 | background-color: $background-color !important; 123 | &:not(.v-pagination__navigation--disabled) { 124 | .v-icon { 125 | color: $text-color !important 126 | } 127 | } 128 | &:hover, 129 | &.v-pagination__item--active { 130 | background-color: $hover-background-color !important; 131 | } 132 | } 133 | } 134 | } 135 | 136 | @mixin v-tabs( 137 | $text-color, 138 | $background-color, 139 | ) { 140 | .v-tab { @include v-tab($text-color); } 141 | &::v-deep { 142 | .v-tabs-bar { 143 | background-color: $background-color !important; 144 | .v-tab { 145 | @include v-tab($text-color); 146 | } 147 | } 148 | @content; 149 | } 150 | } 151 | 152 | @mixin v-tab($text-color) { 153 | &::v-deep { 154 | color: $text-color !important; 155 | @content; 156 | } 157 | } 158 | 159 | @mixin v-tabs-items($background-color) { 160 | &::v-deep { 161 | background-color: $background-color !important; 162 | @content; 163 | } 164 | } 165 | 166 | 167 | @mixin v-toolbar() { 168 | &::v-deep { 169 | .v-toolbar__content { 170 | @content; 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /src/styles/positioning.scss: -------------------------------------------------------------------------------- 1 | // positioning and styles mixins 2 | @mixin reset-button-properties { 3 | // reset 4 | appearance: none; 5 | border: 0px; 6 | padding: 0px; 7 | outline: none; 8 | font-family: inherit; 9 | font-weight: inherit; 10 | font-size: inherit; 11 | background-color: transparent; 12 | } 13 | 14 | @mixin relative-in-place { 15 | position: relative; 16 | top: 0; 17 | left: 0; 18 | } 19 | 20 | @mixin relative-center { 21 | position: relative; 22 | top: 50%; 23 | left: 50%; 24 | transform: translate(-50%, -50%); 25 | } 26 | 27 | @mixin absolute-center { 28 | @include relative-center(); 29 | position: absolute; 30 | } 31 | 32 | @mixin flex-container--center { 33 | display: flex; 34 | flex-direction: row; 35 | justify-content: center; 36 | align-items: center; 37 | align-content: center; 38 | } 39 | 40 | @mixin invisible { 41 | opacity: 0; 42 | pointer-events: none; 43 | } 44 | 45 | @mixin visible { 46 | opacity: 1; 47 | pointer-events: all; 48 | } 49 | -------------------------------------------------------------------------------- /src/styles/resets.scss: -------------------------------------------------------------------------------- 1 | @mixin reset-input { 2 | outline: none; 3 | background-color: transparent; 4 | font-family: inherit; 5 | font-weight: inherit; 6 | font-size: inherit; 7 | color: inherit; 8 | border: 0px solid transparent; 9 | } 10 | 11 | // positioning and styles mixins 12 | @mixin reset-button { 13 | // reset 14 | border: 0px solid transparent; 15 | padding: 0px; 16 | outline: none; 17 | font-family: inherit; 18 | font-weight: inherit; 19 | font-size: inherit; 20 | color: inherit; 21 | background-color: transparent; 22 | } 23 | -------------------------------------------------------------------------------- /src/styles/screens.scss: -------------------------------------------------------------------------------- 1 | // assume that styles START from phone size scss 2 | // assume mobile portrait is defualt, don't need media query for it 3 | $break-phone-landscape: 480px; 4 | $break-tablet-portrait: 768px; 5 | $break-tablet-landscape: 992px; 6 | $break-desktop: 1200px; 7 | $break-desktop-hd: 1400px; 8 | 9 | $break-small: 480px; 10 | $break-smallX: 620px; 11 | $break-medium: 780px; 12 | $break-large: 900px; 13 | 14 | // mobile is both phone and tablet 15 | @mixin respond-to-width($breakpoint) { 16 | @media only screen and (min-width: $breakpoint) { 17 | @content; 18 | } 19 | } 20 | 21 | @mixin respond-to-height($breakpoint) { 22 | @media only screen and (min-height: $breakpoint) { 23 | @content; 24 | } 25 | } 26 | 27 | @mixin phone-landscape { 28 | @include respond-to-width($break-phone-landscape) { 29 | @content; 30 | } 31 | } 32 | 33 | @mixin tablet-portrait { 34 | @include respond-to-width($break-tablet-portrait) { 35 | @content; 36 | } 37 | } 38 | 39 | @mixin tablet-landscape { 40 | @include respond-to-width($break-tablet-landscape) { 41 | @content; 42 | } 43 | } 44 | 45 | @mixin desktop { 46 | @include respond-to-width($break-desktop) { 47 | @content; 48 | } 49 | } 50 | 51 | @mixin desktop-hd { 52 | @include respond-to-width($break-desktop-hd) { 53 | @content; 54 | } 55 | } 56 | 57 | @mixin screen-small { 58 | @include respond-to-height($break-small) { 59 | @content; 60 | } 61 | } 62 | 63 | @mixin screen-smallX { 64 | @include respond-to-height($break-smallX) { 65 | @content; 66 | } 67 | } 68 | 69 | @mixin screen-medium { 70 | @include respond-to-height($break-medium) { 71 | @content; 72 | } 73 | } 74 | 75 | @mixin screen-large { 76 | @include respond-to-height($break-large) { 77 | @content; 78 | } 79 | } 80 | 81 | // for copy/pasting 82 | @include phone-landscape { 83 | } 84 | @include tablet-portrait { 85 | } 86 | @include tablet-landscape { 87 | } 88 | @include desktop { 89 | } 90 | @include desktop-hd { 91 | } 92 | -------------------------------------------------------------------------------- /src/styles/transitions.scss: -------------------------------------------------------------------------------- 1 | $energetic: cubic-bezier(0.5, 0.1, 0.3, 1.5); 2 | -------------------------------------------------------------------------------- /src/utils/if-env.ts: -------------------------------------------------------------------------------- 1 | // SIMPLIFY HANDLING ENV CONDITIONS 2 | const { 3 | NODE_ENV, 4 | IS_OFFLINE 5 | } = env; 6 | 7 | const isDev = NODE_ENV === "development"; 8 | 9 | const ifDevVal = ( 10 | devVal: T, 11 | defaultVal: T, 12 | ): T => { 13 | if (isDev) return devVal; 14 | else return defaultVal; 15 | }; 16 | 17 | const ifDevExec = ( 18 | devCallback: T, 19 | defaultCallback: T, 20 | ): void => { 21 | if (isDev) devCallback(); 22 | else if (defaultCallback) defaultCallback(); 23 | }; 24 | 25 | 26 | const isProd = NODE_ENV === "production"; 27 | 28 | const ifProdVal = ( 29 | prodVal: T, 30 | defaultVal: T, 31 | ): T => { 32 | if (isProd) return prodVal; 33 | else return defaultVal; 34 | }; 35 | 36 | const ifProdExec = ( 37 | prodCallback: T, 38 | defaultCallback: T, 39 | ): void => { 40 | if (isProd) prodCallback(); 41 | else if (defaultCallback) defaultCallback(); 42 | }; 43 | 44 | 45 | const isOffline = IS_OFFLINE; 46 | 47 | const ifOfflineVal = ( 48 | offlineVal: T, 49 | defaultVal: T, 50 | ): T => { 51 | if (isOffline) return offlineVal; 52 | else return defaultVal; 53 | }; 54 | 55 | const ifOfflineExec = ( 56 | offlineCallback: T, 57 | defaultCallback: T, 58 | ): void => { 59 | if (isOffline) offlineCallback(); 60 | else if (defaultCallback) defaultCallback(); 61 | }; 62 | 63 | 64 | export { 65 | isDev, 66 | ifDevVal, 67 | ifDevExec, 68 | 69 | isProd, 70 | ifProdVal, 71 | ifProdExec, 72 | 73 | isOffline, 74 | ifOfflineVal, 75 | ifOfflineExec, 76 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "allowJs": true, 8 | "allowSyntheticDefaultImports": true, 9 | "outDir": "build", 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": [ 13 | "./src/*" 14 | ], 15 | "Injected/*": [ 16 | "./injected/*", 17 | ], 18 | }, 19 | "downlevelIteration": true, 20 | "types": [ 21 | "vuetify" 22 | ], 23 | "lib": [ 24 | "DOM", 25 | "ESNext", 26 | ] 27 | } 28 | } --------------------------------------------------------------------------------