feat: externalized tool configuration

This commit is contained in:
Corentin Thomasset 2021-05-28 19:44:27 +02:00
parent c3adfe30ec
commit 690bd099ef
No known key found for this signature in database
GPG key ID: DBD997E935996158
31 changed files with 387 additions and 300 deletions

View file

@ -0,0 +1,185 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-row>
<v-col cols="12" sm="4">
<v-text-field
ref="inputBaseRef"
v-model.number="inputBase"
label="Input base"
outlined
type="number"
hide-details="auto"
:rules="baseRules"
/>
</v-col>
<v-col cols="12" sm="8">
<v-text-field
ref="inputField"
v-model="inputNumber"
label="Input number"
outlined
hide-details="auto"
/>
</v-col>
</v-row>
<br>
<v-divider />
<br>
<v-row>
<v-col cols="12" sm="4">
<v-text-field
ref="outputBaseRef"
v-model.number="outputBase"
label="Output base"
outlined
type="number"
:rules="baseRules"
/>
</v-col>
<v-col cols="12" sm="8">
<v-text-field
v-model="outputNumber"
label="Output number"
outlined
readonly
append-icon="mdi-content-copy"
@click:append="copy(outputNumber)"
/>
</v-col>
</v-row>
</ToolWrapper>
</template>
<tool>
title: 'Base converter'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-swap-horizontal'
keywords: ['base', 'converter']
path: '/color-picker-converter'
</tool>
<script lang="ts">
import {Component, Ref} from 'nuxt-property-decorator'
import {CopyableMixin} from '~/mixins/copyable.mixin'
import Tool from '~/components/Tool.vue'
import type {VForm} from '~/types/VForm'
const convertBase = (value: string, fromBase: number, toBase: number) => {
const range = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/'.split('')
const fromRange = range.slice(0, fromBase)
const toRange = range.slice(0, toBase)
let decValue = value
.split('')
.reverse()
.reduce((carry: number, digit: string, index: number) => {
if (!fromRange.includes(digit)) {
throw new Error('Invalid digit `' + digit + '` for base ' + fromBase + '.')
}
return (carry += fromRange.indexOf(digit) * (Math.pow(fromBase, index)))
}, 0)
let newValue = ''
while (decValue > 0) {
newValue = toRange[decValue % toBase] + newValue
decValue = (decValue - (decValue % toBase)) / toBase
}
return newValue || '0'
}
@Component({
mixins: [CopyableMixin]
})
export default class BaseConverter extends Tool {
@Ref() readonly inputBaseRef!: VForm
@Ref() readonly outputBaseRef!: VForm
isMounted = false
inputError = ''
inputNumber = '42'
inputBase = 10
outputBase = 16
baseRules = [
(v: unknown) => !!v || 'Required',
(v: unknown) => Number.isInteger(v) || 'Base should be an integer',
(v: number) => v > 1 || 'Base should be > 1',
(v: number) => v <= 64 || 'Base should be <= 64'
]
mounted() {
this.isMounted = true
}
get outputNumber() {
if (this.isMounted && this.inputBaseRef.validate() && this.outputBaseRef.validate()) {
try {
return convertBase(this.inputNumber, this.inputBase, this.outputBase)
} catch (e) {
return e.message
}
} else {
return ''
}
}
}
// import {copyToClipboard, isInt} from "../../utils/helpers";
//
// const convertBase = (value, fromBase, toBase) => {
// const range = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/'.split('');
// const fromRange = range.slice(0, fromBase);
// const toRange = range.slice(0, toBase);
// let decValue = value.split('').reverse().reduce((carry, digit, index) => {
// if (fromRange.indexOf(digit) === -1) throw new Error('Invalid digit `' + digit + '` for base ' + fromBase + '.');
// return carry += fromRange.indexOf(digit) * (Math.pow(fromBase, index));
// }, 0);
// let newValue = '';
// while (decValue > 0) {
// newValue = toRange[decValue % toBase] + newValue;
// decValue = (decValue - (decValue % toBase)) / toBase;
// }
// return newValue || '0';
// }
// export default {
// name: "BaseConverter",
// data() {
// return {
// inputError: '',
// inputNumber: '42',
// inputBase: 10,
// outputBase: 16,
// baseRules: [
// v => isInt(v) || 'Base should be an integer',
// v => !!v || 'Required',
// v => v > 1 || 'Base should be > 1',
// v => v <= 64 || 'Base should be <= 64'
// ],
// isMounted: false
// }
// },
// mounted() {
// this.isMounted = true;
// },
// methods: {
// copy() {
// copyToClipboard(this.outputNumber);
// this.$toast.success('Copied to clipboard.')
// }
// },
// computed: {
// outputNumber() {
// if (this.isMounted && this.$refs.inputBase.validate() && this.$refs.outputBase.validate()) {
// try {
// return convertBase(this.inputNumber, this.inputBase, this.outputBase)
// } catch (e) {
// return e.message;
// }
// } else {
// return ''
// }
// }
// }
// }
</script>
<style scoped lang="less">
</style>

View file

@ -0,0 +1,58 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-textarea
v-model="clearText"
outlined
label="Clear text"
/>
<v-textarea
v-model="base64Text"
outlined
readonly
label="Base64 text"
/>
<div class="text-center">
<v-btn depressed @click="copy(clearText)">
Copy clear
</v-btn>
<v-btn depressed @click="copy(base64Text)">
Copy hash
</v-btn>
</div>
</ToolWrapper>
</template>
<tool>
title: 'Base64 string converter'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-text-box-outline'
keywords: ['base64', 'base', '64', 'converter']
path: '/base64-string-converter'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import {CopyableMixin} from '~/mixins/copyable.mixin'
import Tool from '~/components/Tool.vue'
import {base64ToString, stringToBase64} from '~/utils/convert'
@Component({
mixins: [CopyableMixin]
})
export default class Base64StringConverter extends Tool {
clearText = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.'
get base64Text() {
return stringToBase64(this.clearText)
}
set base64Text(value: string) {
this.clearText = base64ToString(value)
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,188 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-row no-gutters align="center" align-content="center" justify="center">
<v-col cols="12" sm="6" align="center">
<v-color-picker
v-model="rgbPicker"
flat
canvas-height="300"
hide-inputs
mode="rgba"
@input="(v) => updateColors(v, 'picker')"
/>
</v-col>
<v-col cols="12" sm="6" align="center">
<v-text-field
ref="hex"
outlined
label="hex"
:value="hex"
:rules="rules.hex"
append-icon="mdi-content-copy"
@input="(v) => updateColors(v, 'hex')"
@click:append="copy(hex)"
/>
<v-text-field
ref="rgb"
outlined
label="rgb"
:value="rgb"
:rules="rules.rgb"
append-icon="mdi-content-copy"
@input="(v) => updateColors(v, 'rgb')"
@click:append="copy(rgb)"
/>
<v-text-field
ref="hsl"
outlined
label="hsl"
:value="hsl"
:rules="rules.hsl"
append-icon="mdi-content-copy"
@input="(v) => updateColors(v, 'hsl')"
@click:append="copy(hsl)"
/>
<v-combobox
ref="keyword"
:value="keyword"
outlined
label="css keyword"
:items="colorsName"
:rules="rules.keyword"
no-data-text="This is not an authorized color name."
append-icon="mdi-content-copy"
@change="(v) => updateColors(v, 'keyword')"
@click:append="copy(keyword)"
/>
</v-col>
</v-row>
</ToolWrapper>
</template>
<tool>
title: 'Color picker/converter'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-palette'
keywords: ['rgb', 'hsl', 'hex', 'keyword', 'css', 'picker']
path: '/color-picker-converter'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import colors from 'color-name'
import convert from 'color-convert'
import {CopyableMixin} from '~/mixins/copyable.mixin'
import Tool from '~/components/Tool.vue'
import type {VForm} from '~/types/VForm'
const required = (v: unknown) => !!v || 'A value is required'
@Component({
mixins: [CopyableMixin]
})
export default class ColorPickerConverter extends Tool {
rgbPicker = {
r: 76,
g: 175,
b: 80
}
colorsName = Object.keys(colors).sort()
valid = true
rules = {
hex: [
required,
(v: string) => /^#(?:[0-9a-fA-F]{6})$/.test(v) || 'Format should be like #112233'
],
rgb: [
required,
(v: string) => /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.test(v) || 'Format should be like rgb(255, 0, 0)'
],
hsl: [
required,
(v: string) => /^hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)$/.test(v) || 'Format should be like hsl(360, 100%, 50%)'
],
keywords: [
required,
(v: string) => this.colorsName.includes(v) || 'Value should be from the list'
]
}
hex = '#4CAF50'
rgb = 'rgb(76, 175, 80)'
hsl = 'hsl(122, 39%, 49%)'
keyword = 'mediumseagreen'
setHSL(r: number, g: number, b: number) {
const [h, s, l] = convert.rgb.hsl(r, g, b)
this.hsl = `hsl(${Math.floor(h)}, ${Math.floor(s)}%, ${Math.floor(l)}%)`
}
setRGB(r: number, g: number, b: number) {
this.rgb = `rgb(${r}, ${g}, ${b})`
}
setHEX(r: number, g: number, b: number) {
const result = convert.rgb.hex(r, g, b)
this.hex = `#${result}`
}
setKeyword(r: number, g: number, b: number) {
this.keyword = convert.rgb.keyword(r, g, b)
}
updateColors(value: string, fromType: string) {
if (fromType === 'picker' || (this.$refs[fromType] as VForm)?.validate()) {
if (fromType === 'rgb') {
const [r, g, b] = value.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/)?.slice(1).map(v => parseInt(v)) ?? [0, 0, 0]
this.rgbPicker = {r, g, b}
this.setHEX(r, g, b)
this.setHSL(r, g, b)
this.setKeyword(r, g, b)
} else if (fromType === 'hex') {
const [r, g, b] = convert.hex.rgb(value.replace(/#/g, ''))
this.rgbPicker = {r, g, b}
this.setRGB(r, g, b)
this.setHSL(r, g, b)
this.setKeyword(r, g, b)
} else if (fromType === 'hsl') {
const [h, s, l] = value.match(/^hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)$/)?.slice(1).map(v => parseInt(v)) ?? [0, 0, 0]
// @ts-ignore
const [r, g, b] = convert.hsl.rgb(h, s, l)
this.rgbPicker = {r, g, b}
this.setRGB(r, g, b)
this.setHEX(r, g, b)
this.setKeyword(r, g, b)
} else if (fromType === 'keyword') {
try {
// @ts-ignore
const [r, g, b] = convert.keyword.rgb(value)
this.rgbPicker = {r, g, b}
this.setRGB(r, g, b)
this.setHEX(r, g, b)
this.setHSL(r, g, b)
} catch (ignored) {
// ignored
}
} else if (fromType === 'picker') {
const {r, g, b} = value as unknown as { r: number, g: number, b: number }
this.setRGB(r, g, b)
this.setHEX(r, g, b)
this.setHSL(r, g, b)
this.setKeyword(r, g, b)
}
}
}
mounted() {
this.updateColors(this.hex, 'hex')
}
}
</script>
<style scoped lang="less">
::v-deep .v-input__icon {
height: 18px !important;
}
</style>

View file

@ -0,0 +1,133 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-row>
<v-col md="3" sm="12" class="pt-0 pb-0">
<div class="text-center">
<v-switch v-model="useCurrentDate" label="Use current date" />
</div>
</v-col>
<v-col md="3" sm="12" class="pt-0 pb-0">
<v-select
v-model="inputFormatterTitle"
:items="formats.filter(f => !f.title.toLowerCase().includes('locale')).map(v => v.title)"
outlined
label="Your date format"
placeholder="Input format"
:disabled="useCurrentDate"
@input="userInputChanged()"
/>
</v-col>
<v-col md="6" sm="12" class="pt-0 pb-0">
<v-text-field
v-model="inputString"
outlined
label="Your date string"
:error="invalidInput"
:disabled="useCurrentDate"
@input="userInputChanged()"
/>
</v-col>
</v-row>
<br>
<v-divider />
<br>
<br>
<v-text-field
v-for="format of formats"
:key="format.title"
dense
readonly
outlined
:label="format.title"
:value="format.getDate(displayedDate)"
append-icon="mdi-content-copy"
@click:append="copy(format.getDate(displayedDate))"
/>
</ToolWrapper>
</template>
<tool>
title: 'Date/Time converter'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-calendar-range'
keywords: ['date', 'time', 'converter', 'iso']
path: '/date-converter'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import {CopyableMixin} from '@/mixins/copyable.mixin'
import Tool from '@/components/Tool.vue'
@Component({
mixins: [CopyableMixin]
})
export default class DateConverter extends Tool {
inputString = ''
inputFormatterTitle: string | null = null
useCurrentDate = true
displayedDate = new Date()
invalidInput = false
formats = [
{
title: 'Locale datetime',
getDate: (displayedDate: Date) => displayedDate.toLocaleString(),
dateFromFormat: (dateString: string) => dateString
},
{
title: 'ISO 8601',
getDate: (displayedDate: Date) => displayedDate.toISOString(),
dateFromFormat: (dateString: string) => new Date(dateString)
},
{
title: 'UTC format',
getDate: (displayedDate: Date) => displayedDate.toUTCString(),
dateFromFormat: (dateString: string) => new Date(dateString)
},
{
title: 'UNIX Timestamp (ms)',
getDate: (displayedDate: Date) => displayedDate.getTime(),
dateFromFormat: (dateString: string) => new Date(parseInt(dateString))
},
{
title: 'Complete',
getDate: (displayedDate: Date) => displayedDate.toString(),
dateFromFormat: (dateString: string) => new Date(dateString)
}
]
refreshCurrentDate() {
if (this.useCurrentDate) {
this.displayedDate = new Date()
}
}
userInputChanged() {
try {
this.invalidInput = false
const newDate = this.formats.find(f => f.title === this.inputFormatterTitle)?.dateFromFormat(this.inputString)
if (newDate && !isNaN((newDate as Date).getTime())) {
this.useCurrentDate = false
this.displayedDate = newDate as Date
} else if (this.inputString.length > 0) {
this.invalidInput = true
}
} catch (ignored) {
//
}
}
created() {
setInterval(this.refreshCurrentDate.bind(this), 1000)
this.inputFormatterTitle = this.formats[1].title
}
}
</script>
<style scoped lang="less">
</style>

View file

@ -0,0 +1,108 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-select
v-model="language"
outlined
label="Language"
:items="languageList"
@change="languageChanged"
/>
<v-text-field
ref="entropy"
v-model="entropy"
outlined
label="Entropy"
append-icon="mdi-content-copy"
:rules="rules.entropy"
@click:append="copy(entropy)"
/>
<v-text-field
ref="passphrase"
v-model="passphrase"
outlined
label="Passphrase"
append-icon="mdi-content-copy"
:rules="rules.passphrase"
@click:append="copy(passphrase)"
/>
<div class="text-center">
<v-btn @click="refresh">
refresh
</v-btn>
</div>
</ToolWrapper>
</template>
<tool>
title: 'BIP39 passphrase generator'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-message-text-lock-outline'
keywords: ['BIP39', 'passphrase', 'generator']
path: '/bip39-generator'
</tool>
<script lang="ts">
import * as bip39 from 'bip39'
import {shuffle} from '@/utils/string'
import {Component, Ref} from 'nuxt-property-decorator'
import {CopyableMixin} from '@/mixins/copyable.mixin'
import Tool from '@/components/Tool.vue'
import type {VForm} from '~/types/VForm'
const getRandomBuffer = () => Buffer.from(shuffle('0123456789abcdef'.repeat(16)).substring(0, 32), 'hex')
@Component({
mixins: [CopyableMixin]
})
export default class Bip39Generator extends Tool {
@Ref() readonly entropyRef!: VForm
@Ref() readonly passphraseRef!: VForm
buffer = getRandomBuffer()
language: string = 'english'
languageList = Object
.keys(bip39.wordlists)
.filter(k => !k.match(/[A-Z]{2}/))
.map(k => ({
text: k.split('_').map(k => k.charAt(0).toUpperCase() + k.slice(1)).join(' '),
value: k
}))
rules = {
passphrase: [(v: string | undefined) => (!!v && bip39.validateMnemonic(v)) || 'Invalid mnemonic.'],
entropy: [(v: string | undefined) => (!!v && !!v.match(/[0-9a-fA-F]{32}/)) || 'Invalid entropy.']
}
get entropy() {
return this.buffer.toString('hex')
}
set entropy(value) {
if (this.entropyRef.validate()) {
this.buffer = Buffer.from(value, 'hex')
}
}
get passphrase() {
return bip39.entropyToMnemonic(this.buffer)
}
set passphrase(value) {
if (this.passphraseRef.validate()) {
this.buffer = Buffer.from(bip39.mnemonicToEntropy(value), 'hex')
}
}
refresh() {
this.buffer = getRandomBuffer()
}
languageChanged() {
bip39.setDefaultWordlist(this.language)
this.passphrase = bip39.entropyToMnemonic(this.buffer)
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,104 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-row justify="center" align="center">
<v-col cols="12" lg="8" md="12">
<v-textarea
v-model="key"
outlined
label="Encryption key"
rows="1"
@input="encrypt"
/>
</v-col>
<v-col cols="12" lg="4" md="12">
<v-select
v-model="algorithm"
:items="Object.keys(algorithms)"
label="Algorithm"
outlined
@change="encrypt"
/>
</v-col>
</v-row>
<v-textarea
v-model="decrypted"
outlined
label="Clear text"
@input="encrypt"
/>
<v-textarea
v-model="encrypted"
outlined
label="Cyphered text"
@input="decrypt"
/>
<div class="text-center">
<v-btn depressed @click="copy(encrypted)">
Copy result
</v-btn>
</div>
</ToolWrapper>
</template>
<tool>
title: 'Cypher / uncypher text'
description: 'Cypher and uncyfer text.'
icon: 'mdi-lock-open'
keywords: ['cypher', 'uncypher', 'text', 'AES', 'TripleDES', 'Rabbit', 'RabbitLegacy', 'RC4']
path: '/cypher-uncyfer-text'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import {CopyableMixin} from '@/mixins/copyable.mixin'
import Tool from '@/components/Tool.vue'
import CryptoJS from 'crypto-js'
const algos = {
AES: CryptoJS.AES,
TripleDES: CryptoJS.TripleDES,
Rabbit: CryptoJS.Rabbit,
RabbitLegacy: CryptoJS.RabbitLegacy,
RC4: CryptoJS.RC4
}
@Component({
mixins: [CopyableMixin]
})
export default class CypherUncyferText extends Tool {
algorithm: keyof typeof algos = 'AES'
algorithms: typeof algos = algos
key = 'sup3r s3cr3t k3y'
decrypted = 'Lorem ipsum dolor sit amet.'
encrypted = ''
mounted() {
this.encrypt()
}
encrypt() {
try {
this.encrypted = this.algorithms[this.algorithm]
.encrypt(this.decrypted.trim(), this.key)
.toString()
} catch (ignored) {
// ignored
}
}
decrypt() {
try {
this.decrypted = this.algorithms[this.algorithm]
.decrypt(this.encrypted.trim(), this.key)
.toString(CryptoJS.enc.Utf8)
} catch (ignored) {
// ignored
}
}
}
</script>
<style lang="less">
</style>

View file

@ -0,0 +1,76 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-textarea
v-model="inputText"
outlined
label="Text to hash"
/>
<v-select
v-model="algorithm"
:items="Object.keys(algorithms)"
label="Algorithm"
outlined
/>
<v-textarea
v-model="hashed"
outlined
readonly
label="Hashed text"
/>
<div class="text-center">
<v-btn depressed @click="copy(hashed)">
Copy hash
</v-btn>
</div>
</ToolWrapper>
</template>
<tool>
title: 'Hash text'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-script-text-play'
keywords: ['hash', 'text', 'MD5', 'SHA1', 'SHA256', 'SHA224', 'SHA512', 'SHA384', 'SHA3', 'RIPEMD160']
path: '/hash-text'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import CryptoJS from 'crypto-js'
import {CopyableMixin} from '~/mixins/copyable.mixin'
import Tool from '~/components/Tool.vue'
const algos = {
MD5: CryptoJS.MD5,
SHA1: CryptoJS.SHA1,
SHA256: CryptoJS.SHA256,
SHA224: CryptoJS.SHA224,
SHA512: CryptoJS.SHA512,
SHA384: CryptoJS.SHA384,
SHA3: CryptoJS.SHA3,
RIPEMD160: CryptoJS.RIPEMD160
}
@Component({
mixins: [CopyableMixin]
})
export default class HashText extends Tool {
inputText = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.'
algorithm: keyof typeof algos = 'SHA256'
algorithms: typeof algos = algos
get hashed() {
if (this.algorithms[this.algorithm]) {
return this.algorithms[this.algorithm](this.inputText).toString()
} else {
this.$toast.error('Invalid algorithm.')
return ''
}
}
}
</script>
<style>
</style>

View file

@ -0,0 +1,90 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-row no-gutters>
<v-col lg="6" md="12">
<v-switch v-model="withLowercase" label="Lowercase (abc...)" />
<v-switch v-model="withUppercase" label="Uppercase (ABC...)" />
</v-col>
<v-col lg="6" md="12">
<v-switch v-model="withNumbers" label="Numbers (123...)" />
<v-switch v-model="withSpecials" label="Specials (#]-...)" />
</v-col>
</v-row>
<v-slider v-model="length" :label="`Length (${length})`" min="1" max="512" />
<v-textarea v-model="token" outlined />
<div class="text-center">
<v-btn depressed class="mr-4" @click="refreshToken()">
Refresh
</v-btn>
<v-btn depressed @click="copy(token)">
Copy token
</v-btn>
</div>
</ToolWrapper>
</template>
<tool>
title: 'Token generator'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-key-chain-variant'
keywords: ['token', 'random', 'string', 'alphanumeric', 'symbols']
path: '/token-generator'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import Tool from '~/components/Tool.vue'
import {CopyableMixin} from '~/mixins/copyable.mixin'
import {shuffle} from '~/utils/string'
const lowercase = 'abcdefghijklmopqrstuvwxyz'
const uppercase = 'ABCDEFGHIJKLMOPQRSTUVWXYZ'
const numbers = '0123456789'
const specials = '.,;:!?./-"\'#{([-|\\@)]=}*+'
@Component({
mixins: [CopyableMixin]
})
export default class TokenGenerator extends Tool {
withNumbers = true;
withLowercase = true;
withUppercase = true;
withSpecials = false;
length = 64;
refreshBool = true;
refreshToken() {
this.refreshBool = !this.refreshBool
}
get token() {
if (this.refreshBool) {
(() => {
})()
} // To force recomputation
let result = ''
if (this.withLowercase) {
result += lowercase
}
if (this.withUppercase) {
result += uppercase
}
if (this.withNumbers) {
result += numbers
}
if (this.withSpecials) {
result += specials
}
return shuffle(result.repeat(this.length)).substring(0, this.length)
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,90 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-text-field
v-model.number="quantity"
outlined
type="number"
label="Quantity"
dense
class="quantity"
:rules="rules.quantity"
/>
<v-textarea
v-model="token"
outlined
class="centered-input"
:rows="quantity <= 10 ? quantity : 10"
readonly
/>
<div class="text-center">
<v-btn depressed class="mr-4" @click="computeToken">
Refresh
</v-btn>
<v-btn depressed @click="copy(token, `UUID${quantity > 1 ? 's' : ''} copied !`)">
Copy UUID{{ quantity > 1 ? 's' : '' }}
</v-btn>
</div>
</ToolWrapper>
</template>
<tool>
title: 'UUIDs generator'
description: 'A universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems. '
icon: 'mdi-fingerprint'
keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity']
path: '/uuid-generator'
</tool>
<script lang="ts">
import {Component, Ref, Watch} from 'nuxt-property-decorator'
import {CopyableMixin} from '@/mixins/copyable.mixin'
import { VTextField } from 'vuetify/lib'
import Tool from '~/components/Tool.vue'
const generateUuid = () => '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, c => ((c as unknown as number) ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> (c as unknown as number) / 4).toString(16))
@Component({
mixins: [CopyableMixin]
})
export default class UuidGenerator extends Tool {
@Ref() readonly quantityEl! : typeof VTextField
token = ''
quantity = 1
rules = {
quantity: [
(v: any) => !!v || 'Quantity is required',
(v: any) => (v > 0 && v <= 50) || 'Quantity should be > 0 and <= 50',
(v: any) => Number.isInteger(v) || 'Quantity should be an integer'
]
}
mounted() {
this.computeToken()
}
@Watch('quantity')
computeToken() {
this.token = Array.from({length: this.quantity}, generateUuid).join('\n')
}
}
</script>
<style scoped lang="less">
.quantity {
width: 100px;
margin: auto;
text-align: center;
::v-deep input {
text-align: center;
}
}
::v-deep .centered-input textarea {
text-align: center;
margin-top: 13px !important;
font-family: Consolas, monospace;
}
</style>

62
tools/memos/git-memo.vue Normal file
View file

@ -0,0 +1,62 @@
<tool>
title: 'Git memo'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-git'
keywords: ['git', 'memo', 'cheat', 'sheet']
path: '/git-memo'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import Memo from '~/components/Memo.vue'
@Component
export default class GitMemo extends Memo {
}
</script>
<i18n lang="yaml">
en:
memo:
- section: Titre de la section 1
items:
- text: Tip n°1
- text: Tip n°2 (avec sous titre
subtitle: Un super sous titre
- text: Tip n°3 (avec du code !)
code: rm -rf / --no-preserve-root
- text: Tip n°4 (avec du code et un sous titre !)
code: rm -rf / --no-preserve-root
subtitle: Un super sous titre, yeah
- section: Titre de la section 2
items:
- text: Tip n°1
- text: Tip n°2
- text: Tip n°3 (avec du code !)
code: rm -rf / --no-preserve-root
- text: Tip n°4
code: "multiline\ncode sample"
- section: Titre de la section 3
items:
- text: Tip n°1
- text: Tip n°2 (avec sous titre
subtitle: Un super sous titre
- text: Tip n°3 (avec du code !)
code: rm -rf / --no-preserve-root
- text: Tip n°4 (avec du code et un sous titre !)
code: rm -rf / --no-preserve-root
subtitle: Un super sous titre, yeah
- section: Titre de la section 4
items:
- text: Tip n°1
- text: Tip n°2 (avec sous titre
subtitle: Un super sous titre
- text: Tip n°3 (avec du code !)
code: rm -rf / --no-preserve-root
- text: Tip n°4 (avec du code et un sous titre !)
code: rm -rf / --no-preserve-root
subtitle: Un super sous titre, yeah
</i18n>

View file

@ -0,0 +1,232 @@
<template>
<ToolWrapper :config="$toolConfig">
<div class="result">
{{ cronString }}
</div>
<v-text-field
ref="cron"
v-model="cron"
class="cron-wrapper"
outlined
label="Cron"
append-icon="fa-copy"
:rules="[isCronValid]"
hide-details="auto"
@click:append="copy(cron)"
/>
<v-checkbox
v-model="cronstrueConfig.verbose"
hide-details
label="Verbose"
/>
<v-checkbox
v-model="cronstrueConfig.use24HourTimeFormat"
hide-details
label="Use 24 hour time format"
/>
<v-checkbox
v-model="cronstrueConfig.dayOfWeekStartIndexZero"
hide-details
label="Day of the week start a index 0"
/>
<div class="text-center">
<pre>
[optional] seconds (0 - 59)
| minute (0 - 59)
| | hour (0 - 23)
| | | day of month (1 - 31)
| | | | month (1 - 12) OR jan,feb,mar,apr ...
| | | | | day of week (0 - 6, sunday=0) OR sun,mon ...
| | | | | |
* * * * * * command</pre>
</div>
<br>
<v-simple-table dense>
<template #default>
<thead>
<tr>
<th class="text-left">
Symbol
</th>
<th class="text-left">
Meaning
</th>
<th class="text-left">
Example
</th>
<th class="text-left">
Equivalent
</th>
</tr>
</thead>
<tbody>
<tr>
<td>*</td>
<td>Any value</td>
<td>
<pre>* * * *</pre>
</td>
<td>Every minute</td>
</tr>
<tr>
<td>-</td>
<td>Range of values</td>
<td>
<pre>1-10 * * *</pre>
</td>
<td>Minutes 1 through 10</td>
</tr>
<tr>
<td>,</td>
<td>List of values</td>
<td>
<pre>1,10 * * *</pre>
</td>
<td>At minutes 1 and 10</td>
</tr>
<tr>
<td>/</td>
<td>Step values</td>
<td>
<pre>*/10 * * *</pre>
</td>
<td>Every 10 minutes</td>
</tr>
<tr>
<td>@yearly</td>
<td>Once every year at midnight of 1 January</td>
<td>
<pre>@yearly</pre>
</td>
<td>0 0 1 1 *</td>
</tr>
<tr>
<td>@annually</td>
<td>Same as @yearly</td>
<td>
<pre>@annually</pre>
</td>
<td>0 0 1 1 *</td>
</tr>
<tr>
<td>@monthly</td>
<td>Once a month at midnight on the first day</td>
<td>
<pre>@monthly</pre>
</td>
<td>0 0 1 * *</td>
</tr>
<tr>
<td>@weekly</td>
<td>Once a week at midnight on Sunday morning</td>
<td>
<pre>@weekly</pre>
</td>
<td>0 0 * * 0</td>
</tr>
<tr>
<td>@daily</td>
<td>Once a day at midnight</td>
<td>
<pre>@daily</pre>
</td>
<td>0 0 * * *</td>
</tr>
<tr>
<td>@midnight</td>
<td>Same as @daily</td>
<td>
<pre>@midnight</pre>
</td>
<td>0 0 * * *</td>
</tr>
<tr>
<td>@hourly</td>
<td>Once an hour at the beginning of the hour</td>
<td>
<pre>@hourly</pre>
</td>
<td>0 * * * *</td>
</tr>
<tr>
<td>@reboot</td>
<td>Run at startup</td>
<td />
<td />
</tr>
</tbody>
</template>
</v-simple-table>
</ToolWrapper>
</template>
<tool>
title: 'Crontab generator'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-calendar-clock'
keywords: ['year', 'month', 'week', 'day', 'minute', 'second']
path: '/crontab-generator'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import cronstrue from 'cronstrue'
import {isValidCron} from 'cron-validator'
import {CopyableMixin} from '~/mixins/copyable.mixin'
import Tool from '~/components/Tool.vue'
@Component({
mixins: [CopyableMixin]
})
export default class CrontabGenerator extends Tool {
cron = '* * * * *'
cronstrueConfig = {
verbose: true,
dayOfWeekStartIndexZero: true,
use24HourTimeFormat: true,
throwExceptionOnParseError: true
}
isCronValid(v: string) {
return isValidCron(v, {allowBlankDay: true, alias: true, seconds: true})
}
get cronString() {
if (this.isCronValid(this.cron)) {
return cronstrue.toString(this.cron, this.cronstrueConfig)
} else {
return ' '
}
}
}
</script>
<style scoped lang="less">
::v-deep {
.cron-wrapper input {
text-align: center;
font-size: 22px;
font-family: Consolas, monospace;
}
.v-data-table {
background-color: transparent;
}
}
.result {
text-align: center;
font-size: 18px;
margin-bottom: 22px;
}
.text-center {
pre {
display: inline-block;
text-align: left;
}
}
</style>

View file

@ -0,0 +1,59 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-text-field
v-model="port"
outlined
class="centered-input"
readonly
/>
<div class="text-center">
<v-btn depressed class="mr-4" @click="refreshPort">
Refresh
</v-btn>
<v-btn depressed @click="copy(port.toString())">
Copy port
</v-btn>
</div>
</ToolWrapper>
</template>
<tool>
title: 'Random port generator'
description: 'Random port generator without the range of "known" ports (0-1023).'
icon: 'mdi-lan-pending'
keywords: ['system', 'port', 'lan']
path: '/random-port-generator'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import {CopyableMixin} from '@/mixins/copyable.mixin'
import Tool from '~/components/Tool.vue'
import {randIntFromInterval} from '~/utils/random'
const generatePort = () => randIntFromInterval(1024, 65535)
@Component({
mixins: [CopyableMixin]
})
export default class RandomPortGenerator extends Tool {
port!: number
created() {
this.refreshPort()
}
refreshPort() {
this.port = generatePort()
}
}
</script>
<style scoped lang="less">
::v-deep .centered-input input {
text-align: center;
font-family: Consolas, monospace;
}
</style>

View file

@ -0,0 +1,103 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-slider v-model="paragraphs" min="1" max="20" label="Paragraphs" thumb-label />
<v-range-slider
v-model="sentencePerParagraph"
min="1"
max="50"
label="Sentences per paragraph"
thumb-label
/>
<v-range-slider
v-model="wordPerSentence"
min="1"
max="50"
label="Words per sentence"
thumb-label
hide-details
/>
<v-checkbox v-model="startWithLoremIpsum" label="Start with 'Lorem ipsum ...'" hide-details />
<v-checkbox v-model="asHTML" label="As HTML" />
<v-textarea
v-model="loremIpsum"
outlined
readonly
hide-details="auto"
rows="15"
class="text-justify"
/>
<div class="text-center mt-4">
<v-btn depressed @click="copy(loremIpsum)">
Copy
</v-btn>
</div>
</ToolWrapper>
</template>
<tool>
title: 'Lorem ipsum generator'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-comment-text'
keywords: ['Lorem', 'ipsum', 'dolor', 'sit', 'amet']
path: '/lorem-ipsum-generator'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import {CopyableMixin} from '~/mixins/copyable.mixin'
import Tool from '~/components/Tool.vue'
import {randFromArray, randIntFromInterval} from '~/utils/random'
const vocabulary = ['a', 'ac', 'accumsan', 'ad', 'adipiscing', 'aenean', 'aliquam', 'aliquet', 'amet', 'ante', 'aptent', 'arcu', 'at', 'auctor', 'bibendum', 'blandit', 'class', 'commodo', 'condimentum', 'congue', 'consectetur', 'consequat', 'conubia', 'convallis', 'cras', 'cubilia', 'cum', 'curabitur', 'curae', 'dapibus', 'diam', 'dictum', 'dictumst', 'dignissim', 'dolor', 'donec', 'dui', 'duis', 'egestas', 'eget', 'eleifend', 'elementum', 'elit', 'enim', 'erat', 'eros', 'est', 'et', 'etiam', 'eu', 'euismod', 'facilisi', 'faucibus', 'felis', 'fermentum', 'feugiat', 'fringilla', 'fusce', 'gravida', 'habitant', 'habitasse', 'hac', 'hendrerit', 'himenaeos', 'iaculis', 'id', 'imperdiet', 'in', 'inceptos', 'integer', 'interdum', 'ipsum', 'justo', 'lacinia', 'lacus', 'laoreet', 'lectus', 'leo', 'ligula', 'litora', 'lobortis', 'lorem', 'luctus', 'maecenas', 'magna', 'magnis', 'malesuada', 'massa', 'mattis', 'mauris', 'metus', 'mi', 'molestie', 'mollis', 'montes', 'morbi', 'mus', 'nam', 'nascetur', 'natoque', 'nec', 'neque', 'netus', 'nisi', 'nisl', 'non', 'nostra', 'nulla', 'nullam', 'nunc', 'odio', 'orci', 'ornare', 'parturient', 'pellentesque', 'penatibus', 'per', 'pharetra', 'phasellus', 'placerat', 'platea', 'porta', 'porttitor', 'posuere', 'potenti', 'praesent', 'pretium', 'primis', 'proin', 'pulvinar', 'purus', 'quam', 'quis', 'quisque', 'rhoncus', 'ridiculus', 'risus', 'rutrum', 'sagittis', 'sapien', 'scelerisque', 'sed', 'sem', 'semper', 'senectus', 'sit', 'sociis', 'sociosqu', 'sodales', 'sollicitudin', 'suscipit', 'suspendisse', 'taciti', 'tellus', 'tempor', 'tempus', 'tincidunt', 'torquent', 'tortor', 'turpis', 'ullamcorper', 'ultrices', 'ultricies', 'urna', 'varius', 'vehicula', 'vel', 'velit', 'venenatis', 'vestibulum', 'vitae', 'vivamus', 'viverra', 'volutpat', 'vulputate']
const firstSentence = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
const generateSentence = (length: number) => {
let sentence = Array.from({length}).map(() => randFromArray(vocabulary)).join(' ')
sentence = sentence.charAt(0).toUpperCase() + sentence.slice(1) + '.'
return sentence
}
@Component({
mixins: [CopyableMixin]
})
export default class LoremIpsumGenerator extends Tool {
paragraphs = 1
sentencePerParagraph = [3, 8]
wordPerSentence = [8, 15]
startWithLoremIpsum = true
asHTML = false
get loremIpsum() {
const lorem = Array
.from({length: this.paragraphs})
.map(() => {
const length = randIntFromInterval(this.sentencePerParagraph[0], this.sentencePerParagraph[1])
return Array.from({length}).map(() => {
const wordCount = randIntFromInterval(this.wordPerSentence[0], this.wordPerSentence[1])
return generateSentence(wordCount)
})
})
if (this.startWithLoremIpsum) {
lorem[0][0] = firstSentence
}
let result
if (this.asHTML) {
result = `<p>${lorem.map(s => s.join(' ')).join('</p>\n\n<p>')}</p>`
} else {
result = lorem.map(s => s.join(' ')).join('\n\n')
}
return result
}
}
</script>
<style scoped lang="less">
::v-deep {
.v-label {
// To align labels
min-width: 200px !important;
}
}
</style>

93
tools/text/text-stats.vue Normal file
View file

@ -0,0 +1,93 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-textarea
v-model="text"
outlined
label="Input text"
hide-details
auto-grow
/>
<div>{{ $toolListFlat }}</div>
<table>
<tbody>
<tr>
<td><strong>Character count:</strong></td>
<td>{{ textLength }}</td>
</tr>
<tr>
<td><strong>Word count:</strong></td>
<td>{{ textWordCount }}</td>
</tr>
<tr>
<td><strong>Line count:</strong></td>
<td>{{ textLineCount }}</td>
</tr>
<tr>
<td><strong>Byte size:</strong></td>
<td>{{ textSize }}</td>
</tr>
</tbody>
</table>
</ToolWrapper>
</template>
<tool>
title: 'Text stats'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: mdi-text
keywords: [ length, character, count ]
path: '/text-stats'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import {CopyableMixin} from '~/mixins/copyable.mixin'
import Tool from '~/components/Tool.vue'
import {formatBytes} from '~/utils/convert'
@Component({
mixins: [CopyableMixin]
})
export default class TextStats extends Tool {
text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.'
created() {
}
get textLength() {
return this.text.length
}
get textWordCount() {
return this.text.split(/\s+/).length
}
get textLineCount() {
return this.text.split(/\r\n|\r|\n/).length
}
get textSize() {
return formatBytes(Uint8Array.from(this.text as unknown as ArrayLike<number>).buffer.byteLength, 3)
}
}
</script>
<style scoped lang="less">
table {
width: 100%;
tr {
td {
width: 50%;
padding: 5px;
&:first-child {
text-align: right;
}
}
}
}
</style>

View file

@ -0,0 +1,103 @@
<template>
<ToolWrapper :config="$toolConfig" no-card="true">
<FileUploader v-model="file" />
<div v-if="base64 || loading" class="mt-10">
<v-card>
<v-card-title>Result</v-card-title>
<v-card-text>
<v-textarea
v-model="base64"
label="File in base 64"
outlined
readonly
hide-details
:loading="loading"
/>
<div class="text-center mt-4">
<v-btn depressed @click="copy(base64)">
Copy base64
</v-btn>
</div>
</v-card-text>
</v-card>
</div>
</ToolWrapper>
</template>
<tool>
title: 'File to base64'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-file-link-outline'
keywords: ['file', 'base64']
path: '/file-to-base64'
</tool>
<script lang="ts">
import {Component, Watch} from 'nuxt-property-decorator'
import {CopyableMixin} from '@/mixins/copyable.mixin'
import Tool from '@/components/Tool.vue'
import FileUploader from '~/components/FileUploader.vue'
@Component({
mixins: [CopyableMixin],
components: {FileUploader}
})
export default class FileToBase64 extends Tool {
file: Blob | null = null
loading = false
base64 = ''
handleBase64(base64: string) {
this.base64 = base64
this.loading = false
}
@Watch('file')
onFile() {
if (this.file) {
this.loading = true
this.base64 = ''
const reader = new FileReader()
reader.onload = () => this.handleBase64(reader.result as string)
reader.onerror = () => this.handleBase64('[An error as occurred]')
reader.readAsDataURL(this.file)
}
}
}
// export default {
// name: 'FileToBase64',
// components: {FileUploader},
// data() {
// return {
// file: undefined,
// loading: false,
// base64: '',
// copyBase64() {
// copyToClipboard(this.base64)
// this.$toast.success('Copied to clipboard.')
// }
// }
// },
// watch: {
// file() {
// this.loading = true
// this.base64 = ''
// const reader = new FileReader()
// reader.onload = () => this.handleBase64(reader.result)
// reader.onerror = () => this.handleBase64('[An error as occurred]')
// reader.readAsDataURL(this.file)
// }
// },
// methods: {
// handleBase64(base64) {
// this.base64 = base64
// this.loading = false
// }
// }
// }
</script>
<style scoped>
</style>

View file

@ -0,0 +1,136 @@
<template>
<ToolWrapper :config="$toolConfig">
<v-row justify="center" align="center">
<v-col cols="12" lg="6" sm="12">
<v-text-field
v-model="value"
outlined
label="Data"
:rules="rules.value"
/>
<v-slider v-model="size" min="100" max="1920" label="Size (preview will not change): " thumb-label />
<v-select
v-model="level"
outlined
:items="levels"
label="Error resistance"
/>
<v-row>
<v-col cols="12" md="6" sm="12">
<ColorInput v-model="fgColor" label="Foreground color" />
</v-col>
<v-col cols="12" md="6" sm="12">
<ColorInput v-model="bgColor" label="Background color" />
</v-col>
</v-row>
</v-col>
<v-col cols="12" lg="6" sm="12" class="text-center mt-5 ">
<qrcode-vue
:value="value"
:size="size"
:level="level"
:background="bgColor"
:foreground="fgColor"
render-as="svg"
class-name="qrcode-wrapper"
/>
</v-col>
</v-row>
<div class="text-center mb-sm-2 mt-4">
<v-btn class="mr-1" color="primary" @click="download('png')">
download as png
</v-btn>
<v-btn class="ml-1" color="primary" @click="download('svg')">
download as svg
</v-btn>
</div>
</ToolWrapper>
</template>
<tool>
title: 'QR-code generator'
description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus distinctio dolor dolorum eaque eligendi, facilis impedit laboriosam odit placeat.'
icon: 'mdi-qrcode'
keywords: ['editor']
path: '/qrcode-generator'
</tool>
<script lang="ts">
import {Component} from 'nuxt-property-decorator'
import QrcodeVue from 'qrcode.vue'
import colors from 'color-name'
import {CopyableMixin} from '~/mixins/copyable.mixin'
import Tool from '~/components/Tool.vue'
import {downloadBase64File} from '~/utils/file'
import {stringToBase64} from '~/utils/convert'
import ColorInput from '~/components/ColorInput.vue'
@Component({
components: {QrcodeVue, ColorInput},
mixins: [CopyableMixin]
})
export default class QrcodeGenerator extends Tool {
value = 'https://it-tools.tech'
size = 300
level = 'M'
bgColor = 'transparent'
fgColor = '#ffffff'
levels = [
{text: 'Low', value: 'L'},
{text: 'Medium', value: 'M'},
{text: 'Quartile', value: 'Q'},
{text: 'High', value: 'H'}
]
rules = {
value: [
(v: string) => v.length > 0 || 'Value is needed'
],
color: [
(v: string) => {
v = v.trim()
const isFFFFFF = /^#[0-9a-fA-F]{6}$/.test(v)
const isFFF = /^#[0-9a-fA-F]{3}$/.test(v)
const isRGB = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.test(v)
const isHSL = /^hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)$/.test(v)
const isKeyword = v in colors
const isTransparent = v === 'transparent'
return isFFFFFF || isFFF || isKeyword || isTransparent || isRGB || isHSL || 'Incorrect color.'
}
]
}
download(type: string) {
const svgEl = this.$el.querySelector('.qrcode-wrapper svg')!
const svgString = new XMLSerializer().serializeToString(svgEl)
const svgUrl = `data:image/svg+xml;base64,${stringToBase64(svgString)}`
if (type === 'png') {
const canvas = document.createElement('canvas')
canvas.width = this.size
canvas.height = this.size
const ctx = canvas.getContext('2d')!
const image = new Image()
image.onload = function () {
ctx.drawImage(image, 0, 0)
const result = canvas.toDataURL()
downloadBase64File(result, 'qr-code')
}
image.src = svgUrl
} else {
downloadBase64File(svgUrl, 'qr-code')
}
}
}
</script>
<style scoped lang="less">
::v-deep .qrcode-wrapper {
& > * {
width: 300px !important;
height: 300px !important;
}
}
</style>