mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-10-23 04:52:14 +00:00
Compare commits
200 Commits
v2023.4.14
...
v2023.12.2
Author | SHA1 | Date | |
---|---|---|---|
|
670f735501 | ||
|
a29ad66809 | ||
|
5ed36935c7 | ||
|
80e46c9292 | ||
|
7a70dbbe0c | ||
|
2e56641398 | ||
|
a1037cf8f1 | ||
|
0fe9a20329 | ||
|
2e396d8776 | ||
|
bd3edcb528 | ||
|
de1ee69ef9 | ||
|
8f99eb6017 | ||
|
38586caab7 | ||
|
043e4f0a08 | ||
|
ca43a25569 | ||
|
7fe47b3be4 | ||
|
16ffe6b5c9 | ||
|
478192065e | ||
|
205e360400 | ||
|
821cbea2bf | ||
|
093ff311fd | ||
|
e07e2ae5bc | ||
|
fe1de8c5c9 | ||
|
b2614990e3 | ||
|
79646375f3 | ||
|
7d94e11cee | ||
|
e87f4b1837 | ||
|
e86fd96ae3 | ||
|
58de8970f5 | ||
|
02b0d0d1a1 | ||
|
4d5a67d96d | ||
|
8174db9cf3 | ||
|
e164afb664 | ||
|
d0136962b9 | ||
|
015c673e09 | ||
|
99b1eb944d | ||
|
cc3425dc77 | ||
|
681f7bf644 | ||
|
f5eb7a8c49 | ||
|
abb8335041 | ||
|
020e9cbe41 | ||
|
02e68d3f56 | ||
|
00562ed5e8 | ||
|
4365226d01 | ||
|
57ecda1623 | ||
|
d8d7a3b9ab | ||
|
d36b18f193 | ||
|
eea9f91276 | ||
|
ebb4ec4165 | ||
|
84a4a646f6 | ||
|
a2b53c2e38 | ||
|
35563b8457 | ||
|
720201aa7b | ||
|
b2ad4f7a27 | ||
|
b408df82c1 | ||
|
88b881880c | ||
|
ee4c853b9f | ||
|
cbf58fdd28 | ||
|
a757a5155a | ||
|
025f556023 | ||
|
2d2dffb14a | ||
|
5c4d775e2d | ||
|
557b30426f | ||
|
4972159aa7 | ||
|
e371ef702e | ||
|
0eedce69a6 | ||
|
8a30b6bdb3 | ||
|
233d5565f6 | ||
|
7ab9204e96 | ||
|
c7d4562d3b | ||
|
18dd1400bd | ||
|
f035f485c0 | ||
|
e18bae1fca | ||
|
d1dff428d8 | ||
|
3a63837d3d | ||
|
81bfe57cb8 | ||
|
a9cd91ca9c | ||
|
06c35472d3 | ||
|
f3e14fc18f | ||
|
2274766a8f | ||
|
a346175d24 | ||
|
6f93cba3da | ||
|
76b2761d62 | ||
|
6ff9a01cc8 | ||
|
a2b9b157e5 | ||
|
144f86e2dc | ||
|
0f1f6590c5 | ||
|
2bcb77a9f9 | ||
|
c58d6e3423 | ||
|
f235dcd6c1 | ||
|
ba2c589f0f | ||
|
9bd4ad4dfd | ||
|
5e12991bcd | ||
|
65a9474078 | ||
|
85cc7a8447 | ||
|
4268e255de | ||
|
d1c888019b | ||
|
7924456cec | ||
|
99bc84c37e | ||
|
ea0f27cf4c | ||
|
cd5a503fc0 | ||
|
86e964a274 | ||
|
7b6232a151 | ||
|
56d74d07a8 | ||
|
e5d0ba7073 | ||
|
1a602365be | ||
|
a2494219a8 | ||
|
93f7cf0e98 | ||
|
dfa1ba8554 | ||
|
6498c9b0fa | ||
|
f40d7ecddf | ||
|
2e28c5073e | ||
|
d12dd40841 | ||
|
cf382b5a10 | ||
|
01525838e0 | ||
|
38cb61da4d | ||
|
354aed6e6f | ||
|
6b8682fd23 | ||
|
9e8349dbc4 | ||
|
f44050742d | ||
|
b0d9a3e6c7 | ||
|
30f88fc6a8 | ||
|
72c98a3c5e | ||
|
05ea545475 | ||
|
5c3bebfe62 | ||
|
8df7cd0f19 | ||
|
a9c7b89193 | ||
|
6bda2caa04 | ||
|
994a1c3401 | ||
|
05edaf423c | ||
|
8c72e692a7 | ||
|
cec9dea9e0 | ||
|
49eacea195 | ||
|
3f7d469e9f | ||
|
5f2190887d | ||
|
6cb0845336 | ||
|
12d9e5d377 | ||
|
e29b258e90 | ||
|
ea50a3fc65 | ||
|
746e5bdccc | ||
|
c7d4f112c0 | ||
|
9125dcf9c6 | ||
|
38710dce56 | ||
|
282cfc4c4b | ||
|
ba4876d0d5 | ||
|
363c2e47e6 | ||
|
9526ed8324 | ||
|
7068610438 | ||
|
847323ccba | ||
|
1b038c7826 | ||
|
ec4c533718 | ||
|
63045951e1 | ||
|
c4cec9e18f | ||
|
bcb98b359c | ||
|
732da08157 | ||
|
b9406a492d | ||
|
69f0bd079f | ||
|
4cbd7ac145 | ||
|
ebfb872fae | ||
|
a6bbeaebd8 | ||
|
f771e7a99f | ||
|
cf7b1f000a | ||
|
1e2a35b892 | ||
|
45c2474279 | ||
|
fe61f0f2f2 | ||
|
93799af83c | ||
|
962a6d6ec4 | ||
|
d2956b66fe | ||
|
105b21badc | ||
|
33c9b6643f | ||
|
4d2b037dbe | ||
|
34d8e5ce2c | ||
|
8515c24264 | ||
|
0b20f1c16a | ||
|
8c92d56318 | ||
|
7aed9c56fd | ||
|
f7fc779e63 | ||
|
b3b6b7c46b | ||
|
141c12455e | ||
|
77f2efc0b9 | ||
|
aad8d84e13 | ||
|
401f13f7e3 | ||
|
edae4c6915 | ||
|
a43c546e34 | ||
|
83a7b3bae9 | ||
|
ce3150c65d | ||
|
3f6c8f0edd | ||
|
daf2cf0285 | ||
|
b7aaea1b58 | ||
|
92bd83536f | ||
|
e88c1d5f2c | ||
|
362f2fa280 | ||
|
61ece2387f | ||
|
f080933d2a | ||
|
bb32513bd3 | ||
|
c311e3824d | ||
|
74073f5038 | ||
|
c45bce36f9 | ||
|
df989e24b3 | ||
|
6d2202597c |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
playwright-report
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
test-results
|
@@ -22,7 +22,9 @@
|
|||||||
"createGlobalState": true,
|
"createGlobalState": true,
|
||||||
"createInjectionState": true,
|
"createInjectionState": true,
|
||||||
"createReactiveFn": true,
|
"createReactiveFn": true,
|
||||||
|
"createReusableTemplate": true,
|
||||||
"createSharedComposable": true,
|
"createSharedComposable": true,
|
||||||
|
"createTemplatePromise": true,
|
||||||
"createUnrefFn": true,
|
"createUnrefFn": true,
|
||||||
"customRef": true,
|
"customRef": true,
|
||||||
"debouncedRef": true,
|
"debouncedRef": true,
|
||||||
@@ -42,9 +44,6 @@
|
|||||||
"isReactive": true,
|
"isReactive": true,
|
||||||
"isReadonly": true,
|
"isReadonly": true,
|
||||||
"isRef": true,
|
"isRef": true,
|
||||||
"logicAnd": true,
|
|
||||||
"logicNot": true,
|
|
||||||
"logicOr": true,
|
|
||||||
"makeDestructurable": true,
|
"makeDestructurable": true,
|
||||||
"markRaw": true,
|
"markRaw": true,
|
||||||
"nextTick": true,
|
"nextTick": true,
|
||||||
@@ -107,6 +106,19 @@
|
|||||||
"unrefElement": true,
|
"unrefElement": true,
|
||||||
"until": true,
|
"until": true,
|
||||||
"useActiveElement": true,
|
"useActiveElement": true,
|
||||||
|
"useAnimate": true,
|
||||||
|
"useArrayDifference": true,
|
||||||
|
"useArrayEvery": true,
|
||||||
|
"useArrayFilter": true,
|
||||||
|
"useArrayFind": true,
|
||||||
|
"useArrayFindIndex": true,
|
||||||
|
"useArrayFindLast": true,
|
||||||
|
"useArrayIncludes": true,
|
||||||
|
"useArrayJoin": true,
|
||||||
|
"useArrayMap": true,
|
||||||
|
"useArrayReduce": true,
|
||||||
|
"useArraySome": true,
|
||||||
|
"useArrayUnique": true,
|
||||||
"useAsyncQueue": true,
|
"useAsyncQueue": true,
|
||||||
"useAsyncState": true,
|
"useAsyncState": true,
|
||||||
"useAttrs": true,
|
"useAttrs": true,
|
||||||
@@ -117,8 +129,8 @@
|
|||||||
"useBroadcastChannel": true,
|
"useBroadcastChannel": true,
|
||||||
"useBrowserLocation": true,
|
"useBrowserLocation": true,
|
||||||
"useCached": true,
|
"useCached": true,
|
||||||
"useClamp": true,
|
|
||||||
"useClipboard": true,
|
"useClipboard": true,
|
||||||
|
"useCloned": true,
|
||||||
"useColorMode": true,
|
"useColorMode": true,
|
||||||
"useConfirmDialog": true,
|
"useConfirmDialog": true,
|
||||||
"useCounter": true,
|
"useCounter": true,
|
||||||
@@ -160,6 +172,7 @@
|
|||||||
"useFullscreen": true,
|
"useFullscreen": true,
|
||||||
"useGamepad": true,
|
"useGamepad": true,
|
||||||
"useGeolocation": true,
|
"useGeolocation": true,
|
||||||
|
"useI18n": true,
|
||||||
"useIdle": true,
|
"useIdle": true,
|
||||||
"useImage": true,
|
"useImage": true,
|
||||||
"useInfiniteScroll": true,
|
"useInfiniteScroll": true,
|
||||||
@@ -192,12 +205,18 @@
|
|||||||
"useOnline": true,
|
"useOnline": true,
|
||||||
"usePageLeave": true,
|
"usePageLeave": true,
|
||||||
"useParallax": true,
|
"useParallax": true,
|
||||||
|
"useParentElement": true,
|
||||||
|
"usePerformanceObserver": true,
|
||||||
"usePermission": true,
|
"usePermission": true,
|
||||||
"usePointer": true,
|
"usePointer": true,
|
||||||
|
"usePointerLock": true,
|
||||||
"usePointerSwipe": true,
|
"usePointerSwipe": true,
|
||||||
"usePreferredColorScheme": true,
|
"usePreferredColorScheme": true,
|
||||||
|
"usePreferredContrast": true,
|
||||||
"usePreferredDark": true,
|
"usePreferredDark": true,
|
||||||
"usePreferredLanguages": true,
|
"usePreferredLanguages": true,
|
||||||
|
"usePreferredReducedMotion": true,
|
||||||
|
"usePrevious": true,
|
||||||
"useRafFn": true,
|
"useRafFn": true,
|
||||||
"useRefHistory": true,
|
"useRefHistory": true,
|
||||||
"useResizeObserver": true,
|
"useResizeObserver": true,
|
||||||
@@ -211,14 +230,17 @@
|
|||||||
"useSessionStorage": true,
|
"useSessionStorage": true,
|
||||||
"useShare": true,
|
"useShare": true,
|
||||||
"useSlots": true,
|
"useSlots": true,
|
||||||
|
"useSorted": true,
|
||||||
"useSpeechRecognition": true,
|
"useSpeechRecognition": true,
|
||||||
"useSpeechSynthesis": true,
|
"useSpeechSynthesis": true,
|
||||||
"useStepper": true,
|
"useStepper": true,
|
||||||
"useStorage": true,
|
"useStorage": true,
|
||||||
"useStorageAsync": true,
|
"useStorageAsync": true,
|
||||||
"useStyleTag": true,
|
"useStyleTag": true,
|
||||||
|
"useSupported": true,
|
||||||
"useSwipe": true,
|
"useSwipe": true,
|
||||||
"useTemplateRefsList": true,
|
"useTemplateRefsList": true,
|
||||||
|
"useTextDirection": true,
|
||||||
"useTextSelection": true,
|
"useTextSelection": true,
|
||||||
"useTextareaAutosize": true,
|
"useTextareaAutosize": true,
|
||||||
"useThrottle": true,
|
"useThrottle": true,
|
||||||
@@ -230,6 +252,8 @@
|
|||||||
"useTimeoutPoll": true,
|
"useTimeoutPoll": true,
|
||||||
"useTimestamp": true,
|
"useTimestamp": true,
|
||||||
"useTitle": true,
|
"useTitle": true,
|
||||||
|
"useToNumber": true,
|
||||||
|
"useToString": true,
|
||||||
"useToggle": true,
|
"useToggle": true,
|
||||||
"useTransition": true,
|
"useTransition": true,
|
||||||
"useUrlSearchParams": true,
|
"useUrlSearchParams": true,
|
||||||
@@ -250,8 +274,10 @@
|
|||||||
"watchArray": true,
|
"watchArray": true,
|
||||||
"watchAtMost": true,
|
"watchAtMost": true,
|
||||||
"watchDebounced": true,
|
"watchDebounced": true,
|
||||||
|
"watchDeep": true,
|
||||||
"watchEffect": true,
|
"watchEffect": true,
|
||||||
"watchIgnorable": true,
|
"watchIgnorable": true,
|
||||||
|
"watchImmediate": true,
|
||||||
"watchOnce": true,
|
"watchOnce": true,
|
||||||
"watchPausable": true,
|
"watchPausable": true,
|
||||||
"watchPostEffect": true,
|
"watchPostEffect": true,
|
||||||
@@ -259,6 +285,7 @@
|
|||||||
"watchThrottled": true,
|
"watchThrottled": true,
|
||||||
"watchTriggerable": true,
|
"watchTriggerable": true,
|
||||||
"watchWithFilter": true,
|
"watchWithFilter": true,
|
||||||
"whenever": true
|
"whenever": true,
|
||||||
|
"toValue": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,39 +1,21 @@
|
|||||||
/* eslint-env node */
|
/**
|
||||||
require('@rushstack/eslint-patch/modern-module-resolution');
|
* @type {import('eslint').Linter.Config}
|
||||||
|
*/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: [
|
extends: ['@antfu', './.eslintrc-auto-import.json', '@unocss'],
|
||||||
'plugin:vue/vue3-essential',
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:vue/vue3-recommended',
|
|
||||||
'@vue/eslint-config-typescript/recommended',
|
|
||||||
'@vue/eslint-config-prettier',
|
|
||||||
'plugin:import/recommended',
|
|
||||||
'./.eslintrc-auto-import.json',
|
|
||||||
'@unocss',
|
|
||||||
],
|
|
||||||
|
|
||||||
settings: {
|
|
||||||
'import/resolver': { typescript: { project: './tsconfig.app.json' } },
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
'vue/setup-compiler-macros': true,
|
|
||||||
},
|
|
||||||
rules: {
|
rules: {
|
||||||
'vue/multi-word-component-names': ['off'],
|
'curly': ['error', 'all'],
|
||||||
'prettier/prettier': ['error'],
|
'@typescript-eslint/semi': ['error', 'always'],
|
||||||
'import/no-duplicates': ['error', { considerQueryString: true }],
|
'@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
|
||||||
'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }],
|
'vue/no-empty-component-block': ['error'],
|
||||||
'import/extensions': [
|
'no-restricted-imports': ['error', {
|
||||||
'error',
|
paths: [{
|
||||||
'ignorePackages',
|
name: '@vueuse/core',
|
||||||
{
|
importNames: ['useClipboard'],
|
||||||
js: 'never',
|
message: 'Please use local useCopy from src/composable/copy.ts instead of useClipboard.',
|
||||||
ts: 'never',
|
}],
|
||||||
tsx: 'never',
|
}],
|
||||||
},
|
|
||||||
],
|
|
||||||
'import/no-unresolved': ['error', { ignore: ['^virtual:'] }],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
4
.github/ISSUE_TEMPLATE/new-tool-request.md
vendored
4
.github/ISSUE_TEMPLATE/new-tool-request.md
vendored
@@ -6,8 +6,8 @@ labels: new tool
|
|||||||
assignees: CorentinTh
|
assignees: CorentinTh
|
||||||
---
|
---
|
||||||
|
|
||||||
**Which tool is impacted?**
|
**What tool do you want?**
|
||||||
Example: the token generator
|
Example: a token generator
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Describe the solution you'd like**
|
||||||
A clear and concise description of what you want to happen.
|
A clear and concise description of what you want to happen.
|
||||||
|
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
@@ -27,5 +27,8 @@ jobs:
|
|||||||
- name: Run unit test
|
- name: Run unit test
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: pnpm typecheck
|
||||||
|
|
||||||
- name: Build the app
|
- name: Build the app
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -37,11 +37,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -66,4 +66,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
|
16
.github/workflows/docker-nightly-release.yml
vendored
16
.github/workflows/docker-nightly-release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
should_run: ${{ steps.should_run.outputs.should_run }}
|
should_run: ${{ steps.should_run.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
- name: print latest_commit
|
- name: print latest_commit
|
||||||
run: echo ${{ github.sha }}
|
run: echo ${{ github.sha }}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
@@ -54,29 +54,29 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
47
.github/workflows/e2e-tests.yml
vendored
Normal file
47
.github/workflows/e2e-tests.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: E2E tests
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 10
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
shard: [1/3, 2/3, 3/3]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
|
||||||
|
- run: corepack enable
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Get Playwright version
|
||||||
|
id: playwright-version
|
||||||
|
run: echo "PLAYWRIGHT_VERSION=$(jq -r .dependencies.playwright package.json)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build app
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Restore Playwright browsers from cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cache/ms-playwright
|
||||||
|
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-${{ hashFiles('**/playwright.config.ts') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.PLAYWRIGHT_VERSION }}-
|
||||||
|
${{ runner.os }}-playwright-
|
||||||
|
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: pnpm exec playwright install --with-deps
|
||||||
|
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: pnpm run test:e2e --shard=${{ matrix.shard }}
|
23
.github/workflows/playwright.yml
vendored
23
.github/workflows/playwright.yml
vendored
@@ -1,23 +0,0 @@
|
|||||||
name: E2E tests
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
timeout-minutes: 60
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- run: corepack enable
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: 16
|
|
||||||
cache: 'pnpm'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: pnpm install
|
|
||||||
- name: Install Playwright Browsers
|
|
||||||
run: pnpm exec playwright install --with-deps
|
|
||||||
- name: Run Playwright tests
|
|
||||||
run: pnpm exec playwright test
|
|
14
.github/workflows/releases.yml
vendored
14
.github/workflows/releases.yml
vendored
@@ -13,29 +13,29 @@ jobs:
|
|||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
|
||||||
- run: corepack enable
|
- run: corepack enable
|
||||||
|
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -31,3 +31,5 @@ coverage
|
|||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
# Webkit with playwright creates a salt file
|
||||||
|
salt
|
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin", "dbaeumer.vscode-eslint"]
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin", "dbaeumer.vscode-eslint", "lokalise.i18n-ally"]
|
||||||
}
|
}
|
||||||
|
260
CHANGELOG.md
260
CHANGELOG.md
@@ -2,6 +2,266 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## Version 2023.12.21-5ed3693
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **i18n**: improve chinese i18n (#757) (2e56641)
|
||||||
|
- **i18n**: add tooltip and favoriteButton i18n (#756) (a1037cf)
|
||||||
|
- **i18n**: add Chinese translation base (#718) (8f99eb6)
|
||||||
|
- **new tool**: pdf signature checker (#745) (4781920)
|
||||||
|
- **new tool**: numeronym generator (#729) (e07e2ae)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- **jwt-parser**: jwt claim array support (#799) (5ed3693)
|
||||||
|
- **camera-recorder**: stop camera on navigation (#782) (80e46c9)
|
||||||
|
- **doc**: updated create new tool command in readme (#762) (7a70dbb)
|
||||||
|
- **base64-file-converter**: fix downloading of index.html content without data preambula (#750) (043e4f0)
|
||||||
|
- **docker**: rollback armv7 in docker releases (#741) (205e360)
|
||||||
|
- **eta**: corrected example (#737) (821cbea)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
- **about, i18n**: improved i18n dx with markdown (#753) (bd3edcb)
|
||||||
|
- **token, i18n**: complete fr translation (#752) (de1ee69)
|
||||||
|
- **uuid generator**: uuid version picker (#751) (38586ca)
|
||||||
|
- **case converter**: no split on lowercase, uppercase and mocking case (#748) (ca43a25)
|
||||||
|
- **ui**: replaced legacy n-upload with c-file-upload (#747) (7fe47b3)
|
||||||
|
- **token**: added password in token generator keywords (#746) (16ffe6b)
|
||||||
|
- **bcrypt**: fix input label align (#721) (093ff31)
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
- **deps**: switched from oui to oui-data for mac address lookup (#693) (0fe9a20)
|
||||||
|
- **deps**: update unocss monorepo to ^0.57.0 (#638) (2e396d8)
|
||||||
|
- **docker**: added armv7 plateform for docker releases (#722) (fe1de8c)
|
||||||
|
|
||||||
|
## Version 2023.11.02-7d94e11
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **i18n**: language selector (#710) (e86fd96)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- **dockerfile**: revert replacement of nginx image with non-privileged one (#716) (7d94e11)
|
||||||
|
- **encryption**: alert on decryption error (#711) (02b0d0d)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
- **math-evaluator**: improved description (e87f4b1)
|
||||||
|
- **math-evaluator**: improved search and UX (#713) (58de897)
|
||||||
|
|
||||||
|
## Version 2023.11.01-e164afb
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **command-palette**: clear prompt on palette close (#708) (d013696)
|
||||||
|
- **command-palette**: added about page in command palette (99b1eb9)
|
||||||
|
- **new tool**: random MAC address generator (#657) (cc3425d)
|
||||||
|
- **case-converter**: added mocking case (#705) (681f7bf)
|
||||||
|
- **date-converter**: added excel date time format (#704) (f5eb7a8)
|
||||||
|
- **i18n**: token generator (#688) (02e68d3)
|
||||||
|
- **i18n**: home page (#687) (00562ed)
|
||||||
|
- **i18n**: support for i18n in .ts files (#683) (ebb4ec4)
|
||||||
|
- **i18n**: tool card (#682) (84a4a64)
|
||||||
|
- **i18n**: about page (#680) (a2b53c2)
|
||||||
|
- **i18n**: 404 page (#679) (35563b8)
|
||||||
|
- **new tool**: text to ascii converter (#669) (b2ad4f7)
|
||||||
|
- **new tool**: ULID generator (#623) (5c4d775)
|
||||||
|
- **new tool**: add wifi qr code generator (#599) (0eedce6)
|
||||||
|
- **new tool**: iban validation and parser (#591) (3a63837)
|
||||||
|
- **new tool**: text diff and comparator (#588) (81bfe57)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- **deps**: fix issue on slugify (#593) (#673) (720201a)
|
||||||
|
- **deps**: update dependency monaco-editor to ^0.43.0 (#620) (e371ef7)
|
||||||
|
- **deps**: update dependency sql-formatter to v13 (#606) (c7d4562)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
- **ui**: better ui demo preview menu (#664) (015c673)
|
||||||
|
- **color-converter**: improved color-converter UX (#701) (abb8335)
|
||||||
|
- **docker**: improved docker config (#700) (020e9cb)
|
||||||
|
- **c-table**: added description on c-table for accessibility (b408df8)
|
||||||
|
- **ci**: reduced timeout in e2e (#666) (88b8818)
|
||||||
|
- **ui**: new c-table ui component (#665) (ee4c853)
|
||||||
|
- **ui**: removed n-page-header component in user-agent parser (#663) (cbf58fd)
|
||||||
|
- **ui**: removed n-p components in about page (#662) (a757a51)
|
||||||
|
- **ui**: switched naive tooltip components to custom ones (#661) (025f556)
|
||||||
|
- **spelling**: minor corrections to phrasing/spelling (#596) (8a30b6b)
|
||||||
|
- **i18n**: merge tools scoped locales with global ones (#612) (233d556)
|
||||||
|
- **c-key-value-list**: got rid of table for layout (#611) (7ab9204)
|
||||||
|
- **CI**: run e2e against built app and no longer vercel (#610) (18dd140)
|
||||||
|
- **bcrypt**: fix typo (#604) (e18bae1)
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
- **deps**: clean unused dependencies (#709) (e164afb)
|
||||||
|
- **deps**: update docker/setup-qemu-action action to v3 (#627) (4365226)
|
||||||
|
- **deps**: update docker/setup-buildx-action action to v3 (#626) (57ecda1)
|
||||||
|
- **deps**: update docker/login-action action to v3 (#625) (d8d7a3b)
|
||||||
|
- **deps**: update docker/build-push-action action to v5 (#624) (d36b18f)
|
||||||
|
- **deps**: update dependency node to v18.18.2 (#674) (eea9f91)
|
||||||
|
- **deps**: update dependency node to v18.18.0 (#636) (2d2dffb)
|
||||||
|
- **deps**: update actions/checkout action to v4 (#613) (4972159)
|
||||||
|
- **deps**: update dependency unplugin-icons to ^0.17.0 (#609) (f035f48)
|
||||||
|
- **deps**: update dependency @intlify/unplugin-vue-i18n to ^0.13.0 (#597) (d1dff42)
|
||||||
|
- **deps**: update dependency @antfu/eslint-config to ^0.41.0 (#585) (a9cd91c)
|
||||||
|
- **deps**: update dependency typescript to ~5.2.0 (#587) (f3e14fc)
|
||||||
|
|
||||||
|
### Doc
|
||||||
|
- **readme**: added contributors list (#622) (557b304)
|
||||||
|
- **hosting**: added cloudron in the other hosting solutions section (#589) (06c3547)
|
||||||
|
|
||||||
|
## Version 2023.08.21-6f93cba
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **copy**: support legacy copy to clipboard for older browser (#581) (6f93cba)
|
||||||
|
- **new tool**: string obfuscator (#575) (c58d6e3)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- **deps**: update dependency sql-formatter to v12 (#520) (2bcb77a)
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
- **deps**: switched to fucking typescript v5 (#501) (76b2761)
|
||||||
|
- **deps**: update dependency @antfu/eslint-config to ^0.40.0 (#552) (6ff9a01)
|
||||||
|
- **deps**: update dependency prettier to v3 (#564) (a2b9b15)
|
||||||
|
- **deps**: removed @typescript-eslint/parser (#563) (144f86e)
|
||||||
|
- **deps**: removed ts-pattern (#565) (0f1f659)
|
||||||
|
|
||||||
|
## Version 2023.08.16-9bd4ad4
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **Case Converter**: Add lowercase and uppercase (#534) (7b6232a)
|
||||||
|
- **new tool**: emoji picker (#551) (93f7cf0)
|
||||||
|
- **ui**: added c-select in the ui lib (#550) (dfa1ba8)
|
||||||
|
- **new-tool**: password strength analyzer (#502) (a9c7b89)
|
||||||
|
- **new-tool**: yaml to toml (e29b258)
|
||||||
|
- **new-tool**: json to toml (ea50a3f)
|
||||||
|
- **new-tool**: toml to yaml (746e5bd)
|
||||||
|
- **new-tool**: toml to json (c7d4f11)
|
||||||
|
- **command-palette**: random tool action (ec4c533)
|
||||||
|
- **config**: allow app to run in a subfolder via BASE_URL (#461) (6304595)
|
||||||
|
- **new-tool**: percentage calculator (#456) (b9406a4)
|
||||||
|
- **new-tool**: json to csv converter (69f0bd0)
|
||||||
|
- **new tool**: xml formatter (#457) (a6bbeae)
|
||||||
|
- **chmod-calculator**: added symbolic representation (#455) (f771e7a)
|
||||||
|
- **enhancement**: use system dark mode (#458) (cf7b1f0)
|
||||||
|
- **phone-parser**: searchable country code select (d2956b6)
|
||||||
|
- **new tool**: camera screenshot and recorder (34d8e5c)
|
||||||
|
- **base64-string-converter**: switch to encode and decode url safe base64 strings (#392) (0b20f1c)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- **deps**: update dependency uuid to v9 (#566) (5e12991)
|
||||||
|
- **deps**: update dependency mathjs to v11 (#519) (7924456)
|
||||||
|
- **deps**: update dependency @vueuse/router to v10 (#516) (ea0f27c)
|
||||||
|
- **copy**: prevent shorthand copy if source is present in useCopy (#559) (86e964a)
|
||||||
|
- **c-lib**: hide component library shortcut link in non-dev (#557) (56d74d0)
|
||||||
|
- **emoji picker**: fix copy button (#556) (e5d0ba7)
|
||||||
|
- **deps**: update dependency @vueuse/head to v1 (#515) (d12dd40)
|
||||||
|
- **deps**: update dependency country-code-lookup to ^0.1.0 (#493) (8c72e69)
|
||||||
|
- **deps**: update dependency @vueuse/head to ^0.9.0 (#492) (cec9dea)
|
||||||
|
- **i18n**: fallback for demo i18n (12d9e5d)
|
||||||
|
- **typos**: fixed more typos & uppercase JSON (#475) (9526ed8)
|
||||||
|
- **about**: typos and wording (#474) (7068610)
|
||||||
|
- **mime-types**: typos (#470) (c4cec9e)
|
||||||
|
- **sonar**: took down minor sonar warning (4cbd7ac)
|
||||||
|
- **readme**: typo (105b21b)
|
||||||
|
- **ipv4-range-expander**: calculate correct for ip addresses where the first octet is lower than 128 (#405) (8c92d56)
|
||||||
|
- **ipv4-converter**: removed readonly on input (7aed9c5)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
- **navbar**: consistent spacing in navbar buttons (#507) (30f88fc)
|
||||||
|
- **ui**: remove n-text (#506) (72c98a3)
|
||||||
|
- **ui**: replaced some n-input to c-input (#505) (05ea545)
|
||||||
|
- **json-viewer**: input monospace font (#485) (9125dcf)
|
||||||
|
- **search**: command palette design (#463) (bcb98b3)
|
||||||
|
- **c-input-text**: force usage of props with default (1e2a35b)
|
||||||
|
- **naming**: prevent auto import conflicts for git memo (45c2474)
|
||||||
|
- **imports**: removed unnecessary imports to vue (fe61f0f)
|
||||||
|
- **ui**: removed all n-space (4d2b037)
|
||||||
|
- **ui**: replaced some n-input with c-input-text (f7fc779)
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
- **deps**: update dependency vitest to ^0.34.0 (#562) (9bd4ad4)
|
||||||
|
- **deps**: update dependency node to v18.17.1 (#560) (65a9474)
|
||||||
|
- **deps**: update dependency unocss to ^0.55.0 (#561) (85cc7a8)
|
||||||
|
- **deps**: update dependency @unocss/eslint-config to ^0.55.0 (#553) (4268e25)
|
||||||
|
- **deps**: update dependency @intlify/unplugin-vue-i18n to ^0.12.0 (#526) (d1c8880)
|
||||||
|
- **deps**: update docker/login-action action to v2 (#512) (99bc84c)
|
||||||
|
- **deps**: update dependency jsdom to v22 (#499) (cd5a503)
|
||||||
|
- **deps**: update dependency @vitejs/plugin-vue-jsx to v3 (#497) (1a60236)
|
||||||
|
- **deps**: update dependency @vitejs/plugin-vue to v4 (#496) (a249421)
|
||||||
|
- **deps**: update dependency vite-plugin-pwa to ^0.16.0 (#488) (6498c9b)
|
||||||
|
- **deps**: update dependency vite to v4 (#503) (f40d7ec)
|
||||||
|
- **ci**: e2e against vercel deployement (#518) (2e28c50)
|
||||||
|
- **e2e**: execute e2e against built app (#511) (cf382b5)
|
||||||
|
- **deps**: update github/codeql-action action to v2 (#513) (0152583)
|
||||||
|
- **deps**: update node.js to v18 (#514) (38cb61d)
|
||||||
|
- **deps**: switched from vite-plugin-md to vite-plugin-vue-markdown (#510) (354aed6)
|
||||||
|
- **deps**: update dependency workbox-window to v7 (#509) (6b8682f)
|
||||||
|
- **deps**: update dependency vite-svg-loader to v4 (#508) (9e8349d)
|
||||||
|
- **deps**: update dependency typescript to ~4.9.0 (#481) (f440507)
|
||||||
|
- **deps**: update dependency vue-tsc to ^0.40.0 (#490) (b0d9a3e)
|
||||||
|
- **deps**: updated unplugin-auto-import (#504) (5c3bebf)
|
||||||
|
- **deps**: removed start-server-and-test dependency (8df7cd0)
|
||||||
|
- **deps**: update dependency c8 to v8 (#498) (6bda2ca)
|
||||||
|
- **deps**: update dependency @types/jsdom to v21 (#495) (994a1c3)
|
||||||
|
- **deps**: update node.js to v16.20.1 (#491) (05edaf4)
|
||||||
|
- **deps**: update dependency vitest to ^0.32.0 (#489) (49eacea)
|
||||||
|
- **deps**: update actions/checkout action to v3 (#494) (3f7d469)
|
||||||
|
- **deps**: update dependency unplugin-vue-components to ^0.25.0 (#484) (5f21908)
|
||||||
|
- **deps**: update dependency unplugin-auto-import to ^0.16.0 (#483) (6cb0845)
|
||||||
|
- **deps**: update dependency unocss to ^0.53.0 (#482) (38710dc)
|
||||||
|
- **deps**: update dependency @unocss/eslint-config to ^0.53.0 (#478) (282cfc4)
|
||||||
|
- **deps**: added renovate.json (#477) (363c2e4)
|
||||||
|
- **i18n**: tool scoped locales (#471) (1b038c7)
|
||||||
|
- **wysiwyg-editor**: update tiptap dependencies (732da08)
|
||||||
|
- **i18n**: setup i18n plugin config (ebfb872)
|
||||||
|
- **config**: netlify deployment support (#443) (93799af)
|
||||||
|
- **ci**: shard e2e tests (962a6d6)
|
||||||
|
- **lint**: switched to a better lint config (33c9b66)
|
||||||
|
|
||||||
|
### Refacor
|
||||||
|
- **transformers**: use monospace font for JSON and SQL text areas (#476) (ba4876d)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- **ide**: updated vscode extensions settings (#472) (847323c)
|
||||||
|
|
||||||
|
### Chors
|
||||||
|
- **deps**: updated vueuse dependency version (8515c24)
|
||||||
|
|
||||||
|
## Version 2023.05.14-77f2efc
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **list-converter**: a small converter who deals with column based data and do some stuff with it (#387) (83a7b3b)
|
||||||
|
- **new tool**: phone parser and normalizer (ce3150c)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- **phone-parser**: use default country code (a43c546)
|
||||||
|
- **home**: prevent weird blue border on card (3f6c8f0)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
- **ui**: replaced some n-input with c-input-text (77f2efc)
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
- **issues**: updated new tool request issue template (edae4c6)
|
||||||
|
|
||||||
|
### Ui-lib
|
||||||
|
- **new-component**: added text input component in the c-lib (aad8d84)
|
||||||
|
- **button**: size variants (401f13f)
|
||||||
|
|
||||||
|
## Version 2023.04.23-92bd835
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **ui-lib**: demo pages for c-lib components (92bd835)
|
||||||
|
- **new-tool**: diff of two json objects (362f2fa)
|
||||||
|
- **ipv4-range-expander**: expands a given IPv4 start and end address to a valid IPv4 subnet (#366) (df989e2)
|
||||||
|
- **date converter**: auto focus main input (6d22025)
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
- **ts**: cleaned legacy typechecking warning (e88c1d5)
|
||||||
|
- **mac-address-lookup**: added copy handler on button click (c311e38)
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
- **ui-lib**: prevent c-button to shrink (61ece23)
|
||||||
|
- **ui**: replaced naive ui cards with custom ones (f080933)
|
||||||
|
- **clean**: removed unused lodash import (bb32513)
|
||||||
|
- **clean**: removed useless br tags (74073f5)
|
||||||
|
- **ui**: getting ride of naive ui buttons (c45bce3)
|
||||||
|
|
||||||
## Version 2023.04.14-dbad773
|
## Version 2023.04.14-dbad773
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
# build stage
|
# build stage
|
||||||
FROM node:lts-alpine AS build-stage
|
FROM node:lts-alpine AS build-stage
|
||||||
|
# Set environment variables for non-interactive npm installs
|
||||||
|
ENV NPM_CONFIG_LOGLEVEL warn
|
||||||
|
ENV CI true
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
RUN npm install -g pnpm && pnpm i --frozen-lockfile
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm install -g pnpm
|
|
||||||
RUN pnpm i --frozen-lockfile
|
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
|
|
||||||
# production stage
|
# production stage
|
||||||
@@ -11,4 +14,4 @@ FROM nginx:stable-alpine AS production-stage
|
|||||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
35
README.md
35
README.md
@@ -6,7 +6,7 @@ Useful tools for developer and people working in IT. [Have a look !](https://it-
|
|||||||
|
|
||||||
Please check the [issues](https://github.com/CorentinTh/it-tools/issues) to see if some feature listed to be implemented.
|
Please check the [issues](https://github.com/CorentinTh/it-tools/issues) to see if some feature listed to be implemented.
|
||||||
|
|
||||||
You have an idea of a tool? Submit a [feature request](https://github.com/CorentinTh/it-tools/issues/new?assignees=corentinth&labels=&template=feature_request.md&title=)!
|
You have an idea of a tool? Submit a [feature request](https://github.com/CorentinTh/it-tools/issues/new/choose)!
|
||||||
|
|
||||||
## Self host
|
## Self host
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
|
|||||||
|
|
||||||
**Other solutions:**
|
**Other solutions:**
|
||||||
|
|
||||||
|
- [Cloudron](https://www.cloudron.io/store/tech.ittools.cloudron.html)
|
||||||
- [Tipi](https://www.runtipi.io/docs/apps-available)
|
- [Tipi](https://www.runtipi.io/docs/apps-available)
|
||||||
- [Unraid](https://unraid.net/community/apps?q=it-tools)
|
- [Unraid](https://unraid.net/community/apps?q=it-tools)
|
||||||
|
|
||||||
@@ -33,7 +34,25 @@ docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corent
|
|||||||
|
|
||||||
### Recommended IDE Setup
|
### Recommended IDE Setup
|
||||||
|
|
||||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
[VSCode](https://code.visualstudio.com/) with the following extensions:
|
||||||
|
|
||||||
|
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur)
|
||||||
|
- [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||||
|
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||||
|
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally)
|
||||||
|
|
||||||
|
with the following settings:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
"i18n-ally.localesPaths": ["locales", "src/tools/*/locales"],
|
||||||
|
"i18n-ally.keystyle": "nested"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Type Support for `.vue` Imports in TS
|
### Type Support for `.vue` Imports in TS
|
||||||
|
|
||||||
@@ -81,10 +100,16 @@ pnpm lint
|
|||||||
To create a new tool, there is a script that generate the boilerplate of the new tool, simply run:
|
To create a new tool, there is a script that generate the boilerplate of the new tool, simply run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm run script:create-new-tool my-tool-name
|
pnpm run script:create:tool my-tool-name
|
||||||
```
|
```
|
||||||
|
|
||||||
It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the inported tool in the proper category and develop the tool.
|
It will create a directory in `src/tools` with the correct files, and a the import in `src/tools/index.ts`. You will just need to add the imported tool in the proper category and develop the tool.
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
Big thanks to all the people who have already contributed!
|
||||||
|
|
||||||
|
[](https://github.com/corentinth/it-tools/graphs/contributors)
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
@@ -92,6 +117,8 @@ Coded with ❤️ by [Corentin Thomasset](//corentin-thomasset.fr).
|
|||||||
|
|
||||||
This project is continuously deployed using [vercel.com](https://vercel.com).
|
This project is continuously deployed using [vercel.com](https://vercel.com).
|
||||||
|
|
||||||
|
Contributor graph is generated using [contrib.rocks](https://contrib.rocks/preview?repo=corentinth/it-tools).
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=345793&theme=light" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=345793&theme=light&period=daily" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/it-tools?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-it-tools" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=345793&theme=light&period=daily" alt="IT Tools - Collection of handy online tools for devs, with great UX | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
|
|
||||||
|
6
_templates/generator/ui-component/component.demo.ejs.t
Normal file
6
_templates/generator/ui-component/component.demo.ejs.t
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
to: src/ui/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.demo.vue
|
||||||
|
---
|
||||||
|
<template>
|
||||||
|
<<%= h.changeCase.param(name) %> />
|
||||||
|
</template>
|
13
_templates/generator/ui-component/component.ejs.t
Normal file
13
_templates/generator/ui-component/component.ejs.t
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
to: src/ui/<%= h.changeCase.param(name) %>/<%= h.changeCase.param(name) %>.vue
|
||||||
|
---
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = withDefaults(defineProps<{ prop?: string }>(), { prop: '' });
|
||||||
|
const { prop } = toRefs(props);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
{{ prop }}
|
||||||
|
</div>
|
||||||
|
</template>
|
354
auto-imports.d.ts
vendored
354
auto-imports.d.ts
vendored
@@ -19,7 +19,9 @@ declare global {
|
|||||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||||
|
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||||
|
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||||
const customRef: typeof import('vue')['customRef']
|
const customRef: typeof import('vue')['customRef']
|
||||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||||
@@ -39,9 +41,6 @@ declare global {
|
|||||||
const isReactive: typeof import('vue')['isReactive']
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
const isReadonly: typeof import('vue')['isReadonly']
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
const isRef: typeof import('vue')['isRef']
|
const isRef: typeof import('vue')['isRef']
|
||||||
const logicAnd: typeof import('@vueuse/core')['logicAnd']
|
|
||||||
const logicNot: typeof import('@vueuse/core')['logicNot']
|
|
||||||
const logicOr: typeof import('@vueuse/core')['logicOr']
|
|
||||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
@@ -94,6 +93,7 @@ declare global {
|
|||||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||||
const toRef: typeof import('vue')['toRef']
|
const toRef: typeof import('vue')['toRef']
|
||||||
const toRefs: typeof import('vue')['toRefs']
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('vue')['toValue']
|
||||||
const triggerRef: typeof import('vue')['triggerRef']
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||||
@@ -104,6 +104,19 @@ declare global {
|
|||||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||||
const until: typeof import('@vueuse/core')['until']
|
const until: typeof import('@vueuse/core')['until']
|
||||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||||
|
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||||
|
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||||
|
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||||
|
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||||
|
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
||||||
|
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
||||||
|
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
|
||||||
|
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
|
||||||
|
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
||||||
|
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
||||||
|
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
||||||
|
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
||||||
|
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
|
||||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||||
const useAttrs: typeof import('vue')['useAttrs']
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
@@ -114,8 +127,8 @@ declare global {
|
|||||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||||
const useCached: typeof import('@vueuse/core')['useCached']
|
const useCached: typeof import('@vueuse/core')['useCached']
|
||||||
const useClamp: typeof import('@vueuse/core')['useClamp']
|
|
||||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||||
|
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||||
@@ -157,6 +170,7 @@ declare global {
|
|||||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||||
|
const useI18n: typeof import('vue-i18n')['useI18n']
|
||||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||||
const useImage: typeof import('@vueuse/core')['useImage']
|
const useImage: typeof import('@vueuse/core')['useImage']
|
||||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||||
@@ -189,12 +203,18 @@ declare global {
|
|||||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||||
|
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
||||||
|
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
||||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||||
|
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
||||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||||
|
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
||||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||||
|
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||||
|
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||||
@@ -208,14 +228,17 @@ declare global {
|
|||||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||||
const useShare: typeof import('@vueuse/core')['useShare']
|
const useShare: typeof import('@vueuse/core')['useShare']
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||||
|
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||||
|
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||||
@@ -227,6 +250,8 @@ declare global {
|
|||||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||||
|
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
||||||
|
const useToString: typeof import('@vueuse/core')['useToString']
|
||||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||||
@@ -247,8 +272,10 @@ declare global {
|
|||||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||||
|
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
||||||
const watchEffect: typeof import('vue')['watchEffect']
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||||
|
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
||||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
@@ -282,7 +309,9 @@ declare module 'vue' {
|
|||||||
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
||||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||||
|
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||||
|
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||||
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||||
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
||||||
@@ -302,9 +331,6 @@ declare module 'vue' {
|
|||||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||||
readonly logicAnd: UnwrapRef<typeof import('@vueuse/core')['logicAnd']>
|
|
||||||
readonly logicNot: UnwrapRef<typeof import('@vueuse/core')['logicNot']>
|
|
||||||
readonly logicOr: UnwrapRef<typeof import('@vueuse/core')['logicOr']>
|
|
||||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||||
@@ -357,6 +383,7 @@ declare module 'vue' {
|
|||||||
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
||||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||||
|
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||||
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||||
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
||||||
@@ -367,6 +394,19 @@ declare module 'vue' {
|
|||||||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||||
|
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||||
|
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||||
|
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||||
|
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||||
|
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
|
||||||
|
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
|
||||||
|
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
|
||||||
|
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
|
||||||
|
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
|
||||||
|
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
|
||||||
|
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
|
||||||
|
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
|
||||||
|
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
|
||||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||||
@@ -377,8 +417,8 @@ declare module 'vue' {
|
|||||||
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
|
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
|
||||||
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
|
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
|
||||||
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
|
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
|
||||||
readonly useClamp: UnwrapRef<typeof import('@vueuse/core')['useClamp']>
|
|
||||||
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
||||||
|
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||||
@@ -420,6 +460,7 @@ declare module 'vue' {
|
|||||||
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
|
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
|
||||||
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
|
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
|
||||||
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
||||||
|
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
|
||||||
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
|
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
|
||||||
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
|
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
|
||||||
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
|
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
|
||||||
@@ -452,12 +493,18 @@ declare module 'vue' {
|
|||||||
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||||
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||||
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||||
|
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
||||||
|
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
|
||||||
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
||||||
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
||||||
|
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
|
||||||
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
||||||
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
||||||
|
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
|
||||||
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||||
|
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||||
|
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||||
@@ -471,14 +518,17 @@ declare module 'vue' {
|
|||||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||||
|
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||||
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
||||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||||
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||||
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
||||||
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||||
|
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||||
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||||
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
||||||
|
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
|
||||||
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
||||||
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
|
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
|
||||||
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
|
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
|
||||||
@@ -490,6 +540,8 @@ declare module 'vue' {
|
|||||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||||
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
|
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
|
||||||
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
|
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
|
||||||
|
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
|
||||||
|
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
|
||||||
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
|
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
|
||||||
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
||||||
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
||||||
@@ -510,8 +562,294 @@ declare module 'vue' {
|
|||||||
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
||||||
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
||||||
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
||||||
|
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
|
||||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||||
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
||||||
|
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
|
||||||
|
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
||||||
|
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
||||||
|
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||||
|
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||||
|
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
|
||||||
|
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
|
||||||
|
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
||||||
|
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
declare module '@vue/runtime-core' {
|
||||||
|
interface ComponentCustomProperties {
|
||||||
|
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||||
|
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||||
|
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||||
|
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||||
|
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||||
|
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||||
|
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
|
||||||
|
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
|
||||||
|
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
|
||||||
|
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
|
||||||
|
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||||
|
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
|
||||||
|
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
||||||
|
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||||
|
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||||
|
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||||
|
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||||
|
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||||
|
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||||
|
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||||
|
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
||||||
|
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
|
||||||
|
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||||
|
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||||
|
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
|
||||||
|
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||||
|
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
||||||
|
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||||
|
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||||
|
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||||
|
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||||
|
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||||
|
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||||
|
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||||
|
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||||
|
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||||
|
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||||
|
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||||
|
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||||
|
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||||
|
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||||
|
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||||
|
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||||
|
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
|
||||||
|
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||||
|
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||||
|
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
|
||||||
|
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||||
|
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||||
|
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
|
||||||
|
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
|
||||||
|
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||||
|
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||||
|
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||||
|
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||||
|
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||||
|
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
|
||||||
|
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||||
|
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||||
|
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
|
||||||
|
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||||
|
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
|
||||||
|
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
|
||||||
|
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||||
|
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
|
||||||
|
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
|
||||||
|
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
|
||||||
|
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||||
|
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||||
|
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
|
||||||
|
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
|
||||||
|
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
|
||||||
|
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
||||||
|
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
|
||||||
|
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||||
|
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||||
|
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||||
|
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||||
|
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||||
|
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||||
|
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
|
||||||
|
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
|
||||||
|
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
|
||||||
|
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
|
||||||
|
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
|
||||||
|
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||||
|
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
||||||
|
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||||
|
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||||
|
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||||
|
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||||
|
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||||
|
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
||||||
|
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
|
||||||
|
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
|
||||||
|
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
|
||||||
|
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||||
|
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||||
|
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||||
|
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||||
|
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||||
|
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||||
|
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||||
|
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||||
|
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
|
||||||
|
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
|
||||||
|
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
|
||||||
|
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
|
||||||
|
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
|
||||||
|
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
|
||||||
|
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
|
||||||
|
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
|
||||||
|
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
|
||||||
|
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||||
|
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||||
|
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||||
|
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
|
||||||
|
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
|
||||||
|
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
|
||||||
|
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
|
||||||
|
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
|
||||||
|
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
|
||||||
|
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
|
||||||
|
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
||||||
|
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||||
|
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||||
|
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||||
|
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||||
|
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||||
|
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
|
||||||
|
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||||
|
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
|
||||||
|
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
|
||||||
|
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
|
||||||
|
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
|
||||||
|
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
|
||||||
|
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
|
||||||
|
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
|
||||||
|
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
|
||||||
|
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
|
||||||
|
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
|
||||||
|
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
|
||||||
|
readonly useDialog: UnwrapRef<typeof import('naive-ui')['useDialog']>
|
||||||
|
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
|
||||||
|
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
|
||||||
|
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
|
||||||
|
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
|
||||||
|
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
|
||||||
|
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
|
||||||
|
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
|
||||||
|
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
|
||||||
|
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
|
||||||
|
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
|
||||||
|
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
|
||||||
|
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
|
||||||
|
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
|
||||||
|
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
|
||||||
|
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
|
||||||
|
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
|
||||||
|
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
|
||||||
|
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
|
||||||
|
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
|
||||||
|
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
|
||||||
|
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
|
||||||
|
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
|
||||||
|
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
||||||
|
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
|
||||||
|
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
|
||||||
|
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
|
||||||
|
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
|
||||||
|
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
|
||||||
|
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
|
||||||
|
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
||||||
|
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||||
|
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||||
|
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
|
||||||
|
readonly useLoadingBar: UnwrapRef<typeof import('naive-ui')['useLoadingBar']>
|
||||||
|
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||||
|
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||||
|
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
|
||||||
|
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
|
||||||
|
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
|
||||||
|
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
|
||||||
|
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
|
||||||
|
readonly useMessage: UnwrapRef<typeof import('naive-ui')['useMessage']>
|
||||||
|
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
|
||||||
|
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
|
||||||
|
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
||||||
|
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
|
||||||
|
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
||||||
|
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
||||||
|
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
||||||
|
readonly useNotification: UnwrapRef<typeof import('naive-ui')['useNotification']>
|
||||||
|
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
||||||
|
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
|
||||||
|
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
|
||||||
|
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||||
|
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||||
|
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||||
|
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
||||||
|
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
|
||||||
|
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
||||||
|
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
||||||
|
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
|
||||||
|
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
||||||
|
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
||||||
|
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
|
||||||
|
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||||
|
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||||
|
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||||
|
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||||
|
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||||
|
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||||
|
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||||
|
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||||
|
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||||
|
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
||||||
|
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
||||||
|
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
||||||
|
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
|
||||||
|
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
|
||||||
|
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||||
|
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||||
|
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||||
|
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||||
|
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||||
|
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
||||||
|
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||||
|
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||||
|
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
||||||
|
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||||
|
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||||
|
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||||
|
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
||||||
|
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
|
||||||
|
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
||||||
|
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
|
||||||
|
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
|
||||||
|
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
|
||||||
|
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
||||||
|
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
|
||||||
|
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
|
||||||
|
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
||||||
|
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||||
|
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
|
||||||
|
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
|
||||||
|
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
|
||||||
|
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
|
||||||
|
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
|
||||||
|
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
||||||
|
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
||||||
|
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
|
||||||
|
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
|
||||||
|
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
|
||||||
|
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
|
||||||
|
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
|
||||||
|
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
|
||||||
|
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
|
||||||
|
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
|
||||||
|
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
|
||||||
|
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
|
||||||
|
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
|
||||||
|
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
|
||||||
|
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
|
||||||
|
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||||
|
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
||||||
|
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
||||||
|
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
||||||
|
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
|
||||||
|
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||||
|
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
||||||
|
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
|
||||||
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
||||||
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
||||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||||
|
172
components.d.ts
vendored
172
components.d.ts
vendored
@@ -9,18 +9,136 @@ export {}
|
|||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
'404.page': typeof import('./src/pages/404.page.vue')['default']
|
||||||
|
About: typeof import('./src/pages/About.vue')['default']
|
||||||
|
App: typeof import('./src/App.vue')['default']
|
||||||
|
'Base.layout': typeof import('./src/layouts/base.layout.vue')['default']
|
||||||
|
Base64FileConverter: typeof import('./src/tools/base64-file-converter/base64-file-converter.vue')['default']
|
||||||
|
Base64StringConverter: typeof import('./src/tools/base64-string-converter/base64-string-converter.vue')['default']
|
||||||
|
BasicAuthGenerator: typeof import('./src/tools/basic-auth-generator/basic-auth-generator.vue')['default']
|
||||||
|
Bcrypt: typeof import('./src/tools/bcrypt/bcrypt.vue')['default']
|
||||||
|
BenchmarkBuilder: typeof import('./src/tools/benchmark-builder/benchmark-builder.vue')['default']
|
||||||
|
Bip39Generator: typeof import('./src/tools/bip39-generator/bip39-generator.vue')['default']
|
||||||
|
CAlert: typeof import('./src/ui/c-alert/c-alert.vue')['default']
|
||||||
|
'CAlert.demo': typeof import('./src/ui/c-alert/c-alert.demo.vue')['default']
|
||||||
|
CameraRecorder: typeof import('./src/tools/camera-recorder/camera-recorder.vue')['default']
|
||||||
|
CaseConverter: typeof import('./src/tools/case-converter/case-converter.vue')['default']
|
||||||
|
CButton: typeof import('./src/ui/c-button/c-button.vue')['default']
|
||||||
|
'CButton.demo': typeof import('./src/ui/c-button/c-button.demo.vue')['default']
|
||||||
|
CButtonsSelect: typeof import('./src/ui/c-buttons-select/c-buttons-select.vue')['default']
|
||||||
|
'CButtonsSelect.demo': typeof import('./src/ui/c-buttons-select/c-buttons-select.demo.vue')['default']
|
||||||
|
CCard: typeof import('./src/ui/c-card/c-card.vue')['default']
|
||||||
|
'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default']
|
||||||
|
CCollapse: typeof import('./src/ui/c-collapse/c-collapse.vue')['default']
|
||||||
|
'CCollapse.demo': typeof import('./src/ui/c-collapse/c-collapse.demo.vue')['default']
|
||||||
|
CDiffEditor: typeof import('./src/ui/c-diff-editor/c-diff-editor.vue')['default']
|
||||||
|
CFileUpload: typeof import('./src/ui/c-file-upload/c-file-upload.vue')['default']
|
||||||
|
'CFileUpload.demo': typeof import('./src/ui/c-file-upload/c-file-upload.demo.vue')['default']
|
||||||
|
ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default']
|
||||||
|
Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default']
|
||||||
|
CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default']
|
||||||
|
'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default']
|
||||||
|
CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default']
|
||||||
|
CKeyValueListItem: typeof import('./src/ui/c-key-value-list/c-key-value-list-item.vue')['default']
|
||||||
|
CLabel: typeof import('./src/ui/c-label/c-label.vue')['default']
|
||||||
|
CLink: typeof import('./src/ui/c-link/c-link.vue')['default']
|
||||||
|
'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default']
|
||||||
|
CMarkdown: typeof import('./src/ui/c-markdown/c-markdown.vue')['default']
|
||||||
|
'CMarkdown.demo': typeof import('./src/ui/c-markdown/c-markdown.demo.vue')['default']
|
||||||
|
CModal: typeof import('./src/ui/c-modal/c-modal.vue')['default']
|
||||||
|
'CModal.demo': typeof import('./src/ui/c-modal/c-modal.demo.vue')['default']
|
||||||
|
CModalValue: typeof import('./src/ui/c-modal-value/c-modal-value.vue')['default']
|
||||||
|
'CModalValue.demo': typeof import('./src/ui/c-modal-value/c-modal-value.demo.vue')['default']
|
||||||
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
|
CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default']
|
||||||
|
ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default']
|
||||||
ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
|
ColoredCard: typeof import('./src/components/ColoredCard.vue')['default']
|
||||||
|
CommandPalette: typeof import('./src/modules/command-palette/command-palette.vue')['default']
|
||||||
|
CommandPaletteOption: typeof import('./src/modules/command-palette/components/command-palette-option.vue')['default']
|
||||||
|
CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default']
|
||||||
|
CSelect: typeof import('./src/ui/c-select/c-select.vue')['default']
|
||||||
|
'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default']
|
||||||
|
CTable: typeof import('./src/ui/c-table/c-table.vue')['default']
|
||||||
|
'CTable.demo': typeof import('./src/ui/c-table/c-table.demo.vue')['default']
|
||||||
|
CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default']
|
||||||
|
'CTextCopyable.demo': typeof import('./src/ui/c-text-copyable/c-text-copyable.demo.vue')['default']
|
||||||
|
CTooltip: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default']
|
||||||
|
'CTooltip.demo': typeof import('./src/ui/c-tooltip/c-tooltip.demo.vue')['default']
|
||||||
|
DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default']
|
||||||
|
'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default']
|
||||||
|
DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default']
|
||||||
|
DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default']
|
||||||
|
DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default']
|
||||||
|
DockerRunToDockerComposeConverter: typeof import('./src/tools/docker-run-to-docker-compose-converter/docker-run-to-docker-compose-converter.vue')['default']
|
||||||
|
DynamicValues: typeof import('./src/tools/benchmark-builder/dynamic-values.vue')['default']
|
||||||
|
Editor: typeof import('./src/tools/html-wysiwyg-editor/editor/editor.vue')['default']
|
||||||
|
EmojiCard: typeof import('./src/tools/emoji-picker/emoji-card.vue')['default']
|
||||||
|
EmojiGrid: typeof import('./src/tools/emoji-picker/emoji-grid.vue')['default']
|
||||||
|
EmojiPicker: typeof import('./src/tools/emoji-picker/emoji-picker.vue')['default']
|
||||||
|
Encryption: typeof import('./src/tools/encryption/encryption.vue')['default']
|
||||||
|
EtaCalculator: typeof import('./src/tools/eta-calculator/eta-calculator.vue')['default']
|
||||||
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
|
FavoriteButton: typeof import('./src/components/FavoriteButton.vue')['default']
|
||||||
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
|
FormatTransformer: typeof import('./src/components/FormatTransformer.vue')['default']
|
||||||
|
GitMemo: typeof import('./src/tools/git-memo/git-memo.vue')['default']
|
||||||
|
'GitMemo.content': typeof import('./src/tools/git-memo/git-memo.content.md')['default']
|
||||||
|
HashText: typeof import('./src/tools/hash-text/hash-text.vue')['default']
|
||||||
|
HmacGenerator: typeof import('./src/tools/hmac-generator/hmac-generator.vue')['default']
|
||||||
|
'Home.page': typeof import('./src/pages/Home.page.vue')['default']
|
||||||
|
HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default']
|
||||||
|
HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default']
|
||||||
|
HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
|
||||||
|
IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
|
||||||
|
'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
|
||||||
|
'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
|
||||||
|
'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
|
||||||
|
IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default']
|
||||||
|
IconMdiArrowRight: typeof import('~icons/mdi/arrow-right')['default']
|
||||||
|
IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
|
||||||
|
IconMdiCamera: typeof import('~icons/mdi/camera')['default']
|
||||||
|
IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
|
||||||
|
IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
|
||||||
|
IconMdiClose: typeof import('~icons/mdi/close')['default']
|
||||||
|
IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
|
||||||
|
IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
|
||||||
|
IconMdiDownload: typeof import('~icons/mdi/download')['default']
|
||||||
|
IconMdiEye: typeof import('~icons/mdi/eye')['default']
|
||||||
|
IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
|
||||||
|
IconMdiHeart: typeof import('~icons/mdi/heart')['default']
|
||||||
|
IconMdiPause: typeof import('~icons/mdi/pause')['default']
|
||||||
|
IconMdiPlay: typeof import('~icons/mdi/play')['default']
|
||||||
|
IconMdiRecord: typeof import('~icons/mdi/record')['default']
|
||||||
|
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
|
||||||
|
IconMdiSearch: typeof import('~icons/mdi/search')['default']
|
||||||
|
IconMdiTranslate: typeof import('~icons/mdi/translate')['default']
|
||||||
|
IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
|
||||||
|
IconMdiVideo: typeof import('~icons/mdi/video')['default']
|
||||||
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
|
||||||
|
IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
|
||||||
|
Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
|
||||||
|
Ipv4RangeExpander: typeof import('./src/tools/ipv4-range-expander/ipv4-range-expander.vue')['default']
|
||||||
|
Ipv4SubnetCalculator: typeof import('./src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue')['default']
|
||||||
|
Ipv6UlaGenerator: typeof import('./src/tools/ipv6-ula-generator/ipv6-ula-generator.vue')['default']
|
||||||
|
JsonDiff: typeof import('./src/tools/json-diff/json-diff.vue')['default']
|
||||||
|
JsonMinify: typeof import('./src/tools/json-minify/json-minify.vue')['default']
|
||||||
|
JsonToCsv: typeof import('./src/tools/json-to-csv/json-to-csv.vue')['default']
|
||||||
|
JsonToToml: typeof import('./src/tools/json-to-toml/json-to-toml.vue')['default']
|
||||||
|
JsonToYaml: typeof import('./src/tools/json-to-yaml-converter/json-to-yaml.vue')['default']
|
||||||
|
JsonViewer: typeof import('./src/tools/json-viewer/json-viewer.vue')['default']
|
||||||
|
JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
|
||||||
|
KeycodeInfo: typeof import('./src/tools/keycode-info/keycode-info.vue')['default']
|
||||||
|
ListConverter: typeof import('./src/tools/list-converter/list-converter.vue')['default']
|
||||||
|
LocaleSelector: typeof import('./src/modules/i18n/components/locale-selector.vue')['default']
|
||||||
|
LoremIpsumGenerator: typeof import('./src/tools/lorem-ipsum-generator/lorem-ipsum-generator.vue')['default']
|
||||||
|
MacAddressGenerator: typeof import('./src/tools/mac-address-generator/mac-address-generator.vue')['default']
|
||||||
|
MacAddressLookup: typeof import('./src/tools/mac-address-lookup/mac-address-lookup.vue')['default']
|
||||||
|
MathEvaluator: typeof import('./src/tools/math-evaluator/math-evaluator.vue')['default']
|
||||||
|
MenuBar: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar.vue')['default']
|
||||||
|
MenuBarItem: typeof import('./src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue')['default']
|
||||||
MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default']
|
MenuIconItem: typeof import('./src/components/MenuIconItem.vue')['default']
|
||||||
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
|
MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
|
||||||
|
MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
|
||||||
|
MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
|
||||||
NAlert: typeof import('naive-ui')['NAlert']
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
NAutoComplete: typeof import('naive-ui')['NAutoComplete']
|
|
||||||
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
|
||||||
NCard: typeof import('naive-ui')['NCard']
|
|
||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
NCode: typeof import('naive-ui')['NCode']
|
NCode: typeof import('naive-ui')['NCode']
|
||||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||||
@@ -37,37 +155,63 @@ declare module '@vue/runtime-core' {
|
|||||||
NH1: typeof import('naive-ui')['NH1']
|
NH1: typeof import('naive-ui')['NH1']
|
||||||
NH2: typeof import('naive-ui')['NH2']
|
NH2: typeof import('naive-ui')['NH2']
|
||||||
NH3: typeof import('naive-ui')['NH3']
|
NH3: typeof import('naive-ui')['NH3']
|
||||||
NH4: typeof import('naive-ui')['NH4']
|
|
||||||
NIcon: typeof import('naive-ui')['NIcon']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
NImage: typeof import('naive-ui')['NImage']
|
NImage: typeof import('naive-ui')['NImage']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
|
||||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||||
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
|
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
|
||||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||||
NLayout: typeof import('naive-ui')['NLayout']
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||||
NMenu: typeof import('naive-ui')['NMenu']
|
NMenu: typeof import('naive-ui')['NMenu']
|
||||||
NP: typeof import('naive-ui')['NP']
|
|
||||||
NPageHeader: typeof import('naive-ui')['NPageHeader']
|
|
||||||
NProgress: typeof import('naive-ui')['NProgress']
|
NProgress: typeof import('naive-ui')['NProgress']
|
||||||
NResult: typeof import('naive-ui')['NResult']
|
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSelect: typeof import('naive-ui')['NSelect']
|
|
||||||
NSlider: typeof import('naive-ui')['NSlider']
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
|
||||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
NTable: typeof import('naive-ui')['NTable']
|
NTable: typeof import('naive-ui')['NTable']
|
||||||
NTag: typeof import('naive-ui')['NTag']
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
NText: typeof import('naive-ui')['NText']
|
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
|
||||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
|
||||||
NUpload: typeof import('naive-ui')['NUpload']
|
NUpload: typeof import('naive-ui')['NUpload']
|
||||||
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
|
||||||
|
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
|
||||||
|
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
|
||||||
|
PdfSignatureChecker: typeof import('./src/tools/pdf-signature-checker/pdf-signature-checker.vue')['default']
|
||||||
|
PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default']
|
||||||
|
PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default']
|
||||||
|
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
|
||||||
|
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
|
||||||
|
RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
|
||||||
|
ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']
|
||||||
|
RomanNumeralConverter: typeof import('./src/tools/roman-numeral-converter/roman-numeral-converter.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SearchBar: typeof import('./src/components/SearchBar.vue')['default']
|
RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default']
|
||||||
SearchBarItem: typeof import('./src/components/SearchBarItem.vue')['default']
|
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
|
||||||
|
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
|
||||||
|
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
|
||||||
|
StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default']
|
||||||
|
SvgPlaceholderGenerator: typeof import('./src/tools/svg-placeholder-generator/svg-placeholder-generator.vue')['default']
|
||||||
|
TemperatureConverter: typeof import('./src/tools/temperature-converter/temperature-converter.vue')['default']
|
||||||
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
|
TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default']
|
||||||
|
TextDiff: typeof import('./src/tools/text-diff/text-diff.vue')['default']
|
||||||
|
TextStatistics: typeof import('./src/tools/text-statistics/text-statistics.vue')['default']
|
||||||
|
TextToBinary: typeof import('./src/tools/text-to-binary/text-to-binary.vue')['default']
|
||||||
|
TextToNatoAlphabet: typeof import('./src/tools/text-to-nato-alphabet/text-to-nato-alphabet.vue')['default']
|
||||||
|
TokenDisplay: typeof import('./src/tools/otp-code-generator-and-validator/token-display.vue')['default']
|
||||||
|
'TokenGenerator.tool': typeof import('./src/tools/token-generator/token-generator.tool.vue')['default']
|
||||||
|
TomlToJson: typeof import('./src/tools/toml-to-json/toml-to-json.vue')['default']
|
||||||
|
TomlToYaml: typeof import('./src/tools/toml-to-yaml/toml-to-yaml.vue')['default']
|
||||||
|
'Tool.layout': typeof import('./src/layouts/tool.layout.vue')['default']
|
||||||
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
|
ToolCard: typeof import('./src/components/ToolCard.vue')['default']
|
||||||
|
UlidGenerator: typeof import('./src/tools/ulid-generator/ulid-generator.vue')['default']
|
||||||
|
UrlEncoder: typeof import('./src/tools/url-encoder/url-encoder.vue')['default']
|
||||||
|
UrlParser: typeof import('./src/tools/url-parser/url-parser.vue')['default']
|
||||||
|
UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default']
|
||||||
|
UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default']
|
||||||
|
UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
|
||||||
|
WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default']
|
||||||
|
XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default']
|
||||||
|
YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
|
||||||
|
YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
index.html
12
index.html
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>IT Tools - Handy online tools for developers</title>
|
<title>IT Tools - Handy online tools for developers</title>
|
||||||
<meta itemprop="name" content="IT Tools - Handy online tools for developers" />
|
<meta itemprop="name" content="IT Tools - Handy online tools for developers" />
|
||||||
@@ -14,13 +14,13 @@
|
|||||||
itemprop="description"
|
itemprop="description"
|
||||||
content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT."
|
content="Collection of handy online tools for developers, with great UX. IT Tools is a free and open-source collection of handy online tools for developers & people working in IT."
|
||||||
/>
|
/>
|
||||||
<link rel="author" href="/humans.txt" />
|
<link rel="author" href="humans.txt" />
|
||||||
<link rel="canonical" href="https://it-tools.tech" />
|
<link rel="canonical" href="https://it-tools.tech" />
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png" />
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#18a058" />
|
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#18a058" />
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
71
locales/en.yml
Normal file
71
locales/en.yml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
home:
|
||||||
|
categories:
|
||||||
|
newestTools: Newest tools
|
||||||
|
favoriteTools: 'Your favorite tools'
|
||||||
|
allTools: 'All the tools'
|
||||||
|
subtitle: 'Handy tools for developers'
|
||||||
|
toggleMenu: 'Toggle menu'
|
||||||
|
home: Home
|
||||||
|
uiLib: 'UI Lib'
|
||||||
|
support: 'Support IT Tools development'
|
||||||
|
buyMeACoffee: 'Buy me a coffee'
|
||||||
|
follow:
|
||||||
|
title: 'You like it-tools?'
|
||||||
|
p1: 'Give us a star on'
|
||||||
|
githubRepository: 'IT-Tools GitHub repository'
|
||||||
|
p2: 'or follow us on'
|
||||||
|
twitterAccount: 'IT-Tools Twitter account'
|
||||||
|
thankYou: 'Thank you !'
|
||||||
|
nav:
|
||||||
|
github: 'GitHub repository'
|
||||||
|
githubRepository: 'IT-Tools GitHub repository'
|
||||||
|
twitter: 'Twitter account'
|
||||||
|
twitterAccount: 'IT Tools Twitter account'
|
||||||
|
about: 'About IT-Tools'
|
||||||
|
aboutLabel: 'About'
|
||||||
|
darkMode: 'Dark mode'
|
||||||
|
lightMode: 'Light mode'
|
||||||
|
mode: 'Toggle dark/light mode'
|
||||||
|
about:
|
||||||
|
content: >
|
||||||
|
# About IT-Tools
|
||||||
|
|
||||||
|
This wonderful website, made with ❤ by [Corentin Thomasset](https://github.com/CorentinTh) , aggregates useful tools for developer and people working in IT. If you find it useful, please feel free to share it to people you think may find it useful too and don't forget to bookmark it in your shortcut bar!
|
||||||
|
|
||||||
|
IT Tools is open-source (under the MIT license) and free, and will always be, but it costs me money to host and renew the domain name. If you want to support my work, and encourage me to add more tools, please consider supporting by [sponsoring me](https://www.buymeacoffee.com/cthmsst).
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
IT Tools is made in Vue.js (Vue 3) with the the Naive UI component library and is hosted and continuously deployed by Vercel. Third-party open-source libraries are used in some tools, you may find the complete list in the [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) file of the repository.
|
||||||
|
|
||||||
|
## Found a bug? A tool is missing?
|
||||||
|
|
||||||
|
If you need a tool that is currently not present here, and you think can be useful, you are welcome to submit a feature request in the [issues section](https://github.com/CorentinTh/it-tools/issues/new/choose) in the GitHub repository.
|
||||||
|
|
||||||
|
And if you found a bug, or something doesn't work as expected, please file a bug report in the [issues section](https://github.com/CorentinTh/it-tools/issues/new/choose) in the GitHub repository.
|
||||||
|
|
||||||
|
404:
|
||||||
|
notFound: '404 Not Found'
|
||||||
|
sorry: 'Sorry, this page does not seem to exist'
|
||||||
|
maybe: 'Maybe the cache is doing tricky things, try force-refreshing?'
|
||||||
|
backHome: 'Back home'
|
||||||
|
favoriteButton:
|
||||||
|
remove: 'Remove from favorites'
|
||||||
|
add: 'Add to favorites'
|
||||||
|
toolCard:
|
||||||
|
new: New
|
||||||
|
search:
|
||||||
|
label: Search
|
||||||
|
tools:
|
||||||
|
categories:
|
||||||
|
favorite-tools: 'Your favorite tools'
|
||||||
|
crypto: Crypto
|
||||||
|
converter: Converter
|
||||||
|
web: Web
|
||||||
|
images and videos: 'Images & Videos'
|
||||||
|
development: Development
|
||||||
|
network: Network
|
||||||
|
math: Math
|
||||||
|
measurement: Measurement
|
||||||
|
text: Text
|
||||||
|
data: Data
|
65
locales/fr.yml
Normal file
65
locales/fr.yml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
home:
|
||||||
|
categories:
|
||||||
|
newestTools: 'Les nouveaux outils'
|
||||||
|
favoriteTools: 'Vos outils favoris'
|
||||||
|
allTools: 'Tous les outils'
|
||||||
|
subtitle: 'Outils pour les développeurs'
|
||||||
|
toggleMenu: 'Menu'
|
||||||
|
home: Accueil
|
||||||
|
uiLib: 'UI Lib'
|
||||||
|
buyMeACoffee: 'Soutenez IT-Tools'
|
||||||
|
follow:
|
||||||
|
title: 'Vous aimez it-tools ?'
|
||||||
|
p1: 'Soutenez-nous avec une star sur'
|
||||||
|
githubRepository: "le dépôt GitHub d'IT-Tools"
|
||||||
|
p2: 'ou suivez-nous sur'
|
||||||
|
twitterAccount: "le compte Twitter d'IT-Tools"
|
||||||
|
thankYou: 'Merci !'
|
||||||
|
nav:
|
||||||
|
github: 'Dépôt GitHub'
|
||||||
|
githubRepository: "Dépôt GitHub d'IT-Tools"
|
||||||
|
twitter: 'Compte Twitter'
|
||||||
|
twitterAccount: "Compte Twitter d'IT-Tools"
|
||||||
|
about: "À propos d'IT-Tools"
|
||||||
|
aboutLabel: 'À propos'
|
||||||
|
darkMode: 'Mode sombre'
|
||||||
|
lightMode: 'Mode clair'
|
||||||
|
mode: 'Basculer le mode sombre/clair'
|
||||||
|
about:
|
||||||
|
content: >
|
||||||
|
# À propos de IT-Tools
|
||||||
|
|
||||||
|
Ce merveilleux site, fait avec ❤ par [Corentin Thomasset](https://github.com/CorentinTh), regroupe des outils utiles pour les développeurs et les personnes travaillant dans l'informatique. Si vous le trouvez utile, n'hésitez pas à le partager et n'oubliez pas de le mettre dans vos favoris !
|
||||||
|
|
||||||
|
IT Tools est open-source (sous licence MIT) et gratuit, et le restera toujours, mais cela me coûte de l'argent pour l'héberger et renouveler le nom de domaine. Si vous voulez soutenir mon travail, et m'encourager à ajouter plus d'outils, n'hésitez pas à me [soutenir](https://www.buymeacoffee.com/cthmsst).
|
||||||
|
|
||||||
|
## Technologies
|
||||||
|
|
||||||
|
IT Tools est fait en Vue.js (Vue 3) avec la bibliothèque de composants Naive UI et est hébergé et déployé en continu par Vercel. Des bibliothèques open-source tierces sont utilisées dans certains outils, vous pouvez trouver la liste complète dans le fichier [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) du dépôt.
|
||||||
|
|
||||||
|
## Vous avez trouvé un bug ? Un outil manque ?
|
||||||
|
|
||||||
|
Si vous avez besoin d'un outil qui n'est pas encore présent ici, et que vous pensez qu'il peut être utile, vous êtes invité à soumettre une demande de fonctionnalité dans la [section issue](https://github.com/CorentinTh/it-tools/issues/new/choose) du dépôt GitHub.
|
||||||
|
|
||||||
|
404:
|
||||||
|
notFound: '404 Not Found'
|
||||||
|
sorry: "Désolé, cette page n'existe pas"
|
||||||
|
maybe: 'Peut-être que le cache fait des siennes, essayez de forcer le rafraîchissement ?'
|
||||||
|
backHome: "Retour à l'accueil"
|
||||||
|
toolCard:
|
||||||
|
new: Nouveau
|
||||||
|
search:
|
||||||
|
label: Rechercher
|
||||||
|
tools:
|
||||||
|
categories:
|
||||||
|
favorite-tools: 'Vos outils favoris'
|
||||||
|
crypto: Cryptographie
|
||||||
|
converter: Convertisseur
|
||||||
|
web: Web
|
||||||
|
images and videos: 'Images & Vidéos'
|
||||||
|
development: Développement
|
||||||
|
network: Réseau
|
||||||
|
math: Math
|
||||||
|
measurement: Mesure
|
||||||
|
text: Texte
|
||||||
|
data: Données
|
71
locales/zh.yml
Normal file
71
locales/zh.yml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
home:
|
||||||
|
categories:
|
||||||
|
newestTools: '最新工具'
|
||||||
|
favoriteTools: '我的收藏'
|
||||||
|
allTools: '全部工具'
|
||||||
|
subtitle: '助力开发人员和 IT 工作者'
|
||||||
|
toggleMenu: '切换菜单'
|
||||||
|
home: '主页'
|
||||||
|
uiLib: 'UI 库'
|
||||||
|
support: '支持 IT 工具开发'
|
||||||
|
buyMeACoffee: '赞助'
|
||||||
|
follow:
|
||||||
|
title: '关注我们'
|
||||||
|
p1: '给我们 Star'
|
||||||
|
githubRepository: 'GitHub 仓库'
|
||||||
|
p2: '关注我们的'
|
||||||
|
twitterAccount: 'Twitter'
|
||||||
|
thankYou: '感谢您的支持!'
|
||||||
|
nav:
|
||||||
|
github: 'GitHub 仓库'
|
||||||
|
githubRepository: 'GitHub 仓库'
|
||||||
|
twitter: 'Twitter 账号'
|
||||||
|
twitterAccount: 'Twitter 账号'
|
||||||
|
about: '关于 IT-Tools'
|
||||||
|
aboutLabel: '关于'
|
||||||
|
darkMode: '深色模式'
|
||||||
|
lightMode: '浅色模式'
|
||||||
|
mode: '颜色模式'
|
||||||
|
about:
|
||||||
|
content: >
|
||||||
|
# 关于 IT-Tools
|
||||||
|
|
||||||
|
IT-Tools 由 [Corentin Thomasset](https://github.com/CorentinTh) 用 ❤ 开发,汇集了对开发人员和 IT 从业者有用的工具。如果对您有帮助,请将其分享给您的朋友,并且添加到收藏夹中!
|
||||||
|
|
||||||
|
IT-Tools 永久免费且开源(MIT 许可证),但需要资金用于托管和续订域名。如果您想支持我的工作,并鼓励我添加更多工具,请考虑通过 [赞助我](https://www.buymeacoffee.com/cthmsst) 进行支持。
|
||||||
|
|
||||||
|
## 技术
|
||||||
|
|
||||||
|
IT-Tools 采用 Vue.js(Vue 3)和 Naive UI 组件库开发,并由 Vercel 托管和持续部署。某些工具使用了第三方开源库,您可以在仓库的 [package.json](https://github.com/CorentinTh/it-tools/blob/main/package.json) 文件中找到完整的列表。
|
||||||
|
|
||||||
|
## 发现了 Bug?缺少工具?
|
||||||
|
|
||||||
|
如果目前这里没有您需要的工具,并且您认为它可能有用,欢迎在 GitHub 仓库的 [issues](https://github.com/CorentinTh/it-tools/issues/new/choose) 中提交新增功能的请求。
|
||||||
|
|
||||||
|
如果您发现了 Bug,或者某些功能未能按预期工作,请在 GitHub 仓库的 [issues](https://github.com/CorentinTh/it-tools/issues/new/choose) 中提交错误报告。
|
||||||
|
|
||||||
|
404:
|
||||||
|
notFound: '404 页面不存在'
|
||||||
|
sorry: '抱歉,该页面似乎不存在'
|
||||||
|
maybe: '也许缓存出现了一些问题,试试强制刷新页面?'
|
||||||
|
backHome: '返回主页'
|
||||||
|
favoriteButton:
|
||||||
|
remove: '取消收藏'
|
||||||
|
add: '加入收藏'
|
||||||
|
toolCard:
|
||||||
|
new: '新'
|
||||||
|
search:
|
||||||
|
label: '搜索'
|
||||||
|
tools:
|
||||||
|
categories:
|
||||||
|
favorite-tools: '我的收藏'
|
||||||
|
crypto: '加密'
|
||||||
|
converter: '转换器'
|
||||||
|
web: Web
|
||||||
|
images and videos: '图片和视频'
|
||||||
|
development: '开发'
|
||||||
|
network: '网络'
|
||||||
|
math: '数学'
|
||||||
|
measurement: '测量'
|
||||||
|
text: '文本'
|
||||||
|
data: '数据'
|
4
netlify.toml
Normal file
4
netlify.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[[redirects]]
|
||||||
|
from = "/*"
|
||||||
|
to = "/index.html"
|
||||||
|
status = 200
|
112
package.json
112
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "it-tools",
|
"name": "it-tools",
|
||||||
"version": "2023.4.14-dbad773",
|
"version": "2023.12.21-5ed3693",
|
||||||
"description": "Collection of handy online tools for developers, with great UX. ",
|
"description": "Collection of handy online tools for developers, with great UX. ",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"productivity",
|
"productivity",
|
||||||
@@ -21,107 +21,119 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build",
|
"build": "vue-tsc --noEmit && NODE_OPTIONS=--max_old_space_size=4096 vite build",
|
||||||
"preview": "vite preview --port 5050",
|
"preview": "vite preview --port 5050",
|
||||||
"test": "npm run test:unit",
|
"test": "npm run test:unit",
|
||||||
"test:unit": "vitest --environment jsdom",
|
"test:unit": "vitest --environment jsdom",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:dev": "BASE_URL=http://localhost:5173 NO_WEB_SERVER=true playwright test",
|
||||||
"coverage": "vitest run --coverage",
|
"coverage": "vitest run --coverage",
|
||||||
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||||
"lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
|
"lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore",
|
||||||
"script:create-new-tool": "node scripts/create-tool.mjs",
|
"script:create:tool": "node scripts/create-tool.mjs",
|
||||||
|
"script:create:ui": "hygen generator ui-component",
|
||||||
"release": "node ./scripts/release.mjs"
|
"release": "node ./scripts/release.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@it-tools/bip39": "^0.0.4",
|
"@it-tools/bip39": "^0.0.4",
|
||||||
"@it-tools/oggen": "^1.3.0",
|
"@it-tools/oggen": "^1.3.0",
|
||||||
"@sindresorhus/slugify": "^2.2.0",
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
"@tiptap/pm": "2.0.0-beta.220",
|
"@tiptap/pm": "2.1.6",
|
||||||
"@tiptap/starter-kit": "2.0.0-beta.220",
|
"@tiptap/starter-kit": "2.1.6",
|
||||||
"@tiptap/vue-3": "2.0.0-beta.220",
|
"@tiptap/vue-3": "2.0.3",
|
||||||
"@vicons/material": "^0.12.0",
|
"@vicons/material": "^0.12.0",
|
||||||
"@vicons/tabler": "^0.12.0",
|
"@vicons/tabler": "^0.12.0",
|
||||||
"@vueuse/core": "^8.9.4",
|
"@vueuse/core": "^10.3.0",
|
||||||
"@vueuse/head": "^0.7.13",
|
"@vueuse/head": "^1.0.0",
|
||||||
"@vueuse/router": "^9.13.0",
|
"@vueuse/router": "^10.0.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"change-case": "^4.1.2",
|
"change-case": "^4.1.2",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
"composerize-ts": "^0.6.2",
|
"composerize-ts": "^0.6.2",
|
||||||
|
"country-code-lookup": "^0.1.0",
|
||||||
"cron-validator": "^1.3.1",
|
"cron-validator": "^1.3.1",
|
||||||
"cronstrue": "^2.26.0",
|
"cronstrue": "^2.26.0",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"dompurify": "^3.0.6",
|
||||||
|
"emojilib": "^3.0.10",
|
||||||
"figue": "^1.2.0",
|
"figue": "^1.2.0",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
|
"iarna-toml-esm": "^3.0.5",
|
||||||
|
"ibantools": "^4.3.3",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
|
"libphonenumber-js": "^1.10.28",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mathjs": "^10.6.4",
|
"marked": "^10.0.0",
|
||||||
|
"mathjs": "^11.9.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"naive-ui": "^2.34.3",
|
"monaco-editor": "^0.43.0",
|
||||||
|
"naive-ui": "^2.35.0",
|
||||||
"netmask": "^2.0.2",
|
"netmask": "^2.0.2",
|
||||||
"node-forge": "^1.3.1",
|
"node-forge": "^1.3.1",
|
||||||
"oui": "^12.0.52",
|
"oui-data": "^1.0.10",
|
||||||
|
"pdf-signature-reader": "^1.4.2",
|
||||||
"pinia": "^2.0.34",
|
"pinia": "^2.0.34",
|
||||||
"plausible-tracker": "^0.3.8",
|
"plausible-tracker": "^0.3.8",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"randombytes": "^2.1.0",
|
"sql-formatter": "^13.0.0",
|
||||||
"sql-formatter": "^8.2.0",
|
|
||||||
"ts-pattern": "^4.2.2",
|
|
||||||
"ua-parser-js": "^1.0.35",
|
"ua-parser-js": "^1.0.35",
|
||||||
"uuid": "^8.3.2",
|
"ulid": "^2.3.0",
|
||||||
"vue": "^3.2.47",
|
"unicode-emoji-json": "^0.4.0",
|
||||||
|
"unplugin-auto-import": "^0.16.4",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"vue": "^3.3.4",
|
||||||
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-router": "^4.1.6",
|
"vue-router": "^4.1.6",
|
||||||
|
"vue-tsc": "^1.8.1",
|
||||||
|
"xml-formatter": "^3.3.2",
|
||||||
"yaml": "^2.2.1"
|
"yaml": "^2.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^0.41.0",
|
||||||
|
"@iconify-json/mdi": "^1.1.50",
|
||||||
|
"@intlify/unplugin-vue-i18n": "^0.13.0",
|
||||||
"@playwright/test": "^1.32.3",
|
"@playwright/test": "^1.32.3",
|
||||||
"@rushstack/eslint-patch": "^1.2.0",
|
"@rushstack/eslint-patch": "^1.2.0",
|
||||||
|
"@tsconfig/node18": "^18.2.0",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/jsdom": "^16.2.15",
|
"@types/dompurify": "^3.0.5",
|
||||||
|
"@types/jsdom": "^21.0.0",
|
||||||
"@types/lodash": "^4.14.192",
|
"@types/lodash": "^4.14.192",
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/netmask": "^2.0.0",
|
"@types/netmask": "^2.0.0",
|
||||||
"@types/node": "^16.18.23",
|
"@types/node": "^18.15.11",
|
||||||
"@types/node-forge": "^1.3.2",
|
"@types/node-forge": "^1.3.2",
|
||||||
"@types/prettier": "^2.7.2",
|
|
||||||
"@types/qrcode": "^1.5.0",
|
"@types/qrcode": "^1.5.0",
|
||||||
"@types/randombytes": "^2.0.0",
|
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^9.0.0",
|
||||||
"@typescript-eslint/parser": "^5.58.0",
|
"@unocss/eslint-config": "^0.57.0",
|
||||||
"@unocss/eslint-config": "^0.50.8",
|
"@vitejs/plugin-vue": "^4.3.2",
|
||||||
"@vitejs/plugin-vue": "^2.3.4",
|
"@vitejs/plugin-vue-jsx": "^3.0.2",
|
||||||
"@vitejs/plugin-vue-jsx": "^1.3.10",
|
"@vue/compiler-sfc": "^3.2.47",
|
||||||
"@vue/eslint-config-prettier": "^7.1.0",
|
"@vue/runtime-dom": "^3.3.4",
|
||||||
"@vue/eslint-config-typescript": "^10.0.0",
|
|
||||||
"@vue/test-utils": "^2.3.2",
|
"@vue/test-utils": "^2.3.2",
|
||||||
"@vue/tsconfig": "^0.1.3",
|
"@vue/tsconfig": "^0.4.0",
|
||||||
"c8": "^7.13.0",
|
|
||||||
"consola": "^3.0.2",
|
"consola": "^3.0.2",
|
||||||
"eslint": "^8.38.0",
|
"eslint": "^8.47.0",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"hygen": "^6.2.11",
|
||||||
"eslint-import-resolver-typescript": "^3.5.5",
|
"jsdom": "^22.0.0",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
|
||||||
"eslint-plugin-vue": "^8.7.1",
|
|
||||||
"jsdom": "^19.0.0",
|
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"prettier": "^2.8.7",
|
"prettier": "^3.0.0",
|
||||||
"start-server-and-test": "^1.15.4",
|
"typescript": "~5.2.0",
|
||||||
"typescript": "~4.5.5",
|
"unocss": "^0.57.0",
|
||||||
"unocss": "^0.50.8",
|
"unocss-preset-scrollbar": "^0.2.1",
|
||||||
"unplugin-auto-import": "^0.15.2",
|
"unplugin-icons": "^0.17.0",
|
||||||
"unplugin-vue-components": "^0.24.1",
|
"unplugin-vue-components": "^0.25.0",
|
||||||
"vite": "^2.9.15",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-md": "^0.12.4",
|
"vite-plugin-pwa": "^0.16.0",
|
||||||
"vite-plugin-pwa": "^0.11.13",
|
"vite-plugin-vue-markdown": "^0.23.5",
|
||||||
"vite-svg-loader": "^3.6.0",
|
"vite-svg-loader": "^4.0.0",
|
||||||
"vitest": "^0.13.1",
|
"vitest": "^0.34.0",
|
||||||
"vue-tsc": "^0.31.4",
|
"workbox-window": "^7.0.0",
|
||||||
"workbox-window": "^6.5.4",
|
|
||||||
"zx": "^7.2.1"
|
"zx": "^7.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,8 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
/**
|
const isCI = !!process.env.CI;
|
||||||
* Read environment variables from file.
|
const baseUrl = process.env.BASE_URL || 'http://localhost:5050';
|
||||||
* https://github.com/motdotla/dotenv
|
const useWebServer = process.env.NO_WEB_SERVER !== 'true';
|
||||||
*/
|
|
||||||
// require('dotenv').config();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See https://playwright.dev/docs/test-configuration.
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
@@ -15,17 +13,17 @@ export default defineConfig({
|
|||||||
/* Run tests in files in parallel */
|
/* Run tests in files in parallel */
|
||||||
fullyParallel: true,
|
fullyParallel: true,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: isCI,
|
||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: isCI ? 2 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: isCI ? 1 : undefined,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
use: {
|
use: {
|
||||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
baseURL: 'http://127.0.0.1:3000',
|
baseURL: baseUrl,
|
||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
@@ -51,32 +49,17 @@ export default defineConfig({
|
|||||||
name: 'webkit',
|
name: 'webkit',
|
||||||
use: { ...devices['Desktop Safari'] },
|
use: { ...devices['Desktop Safari'] },
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Test against mobile viewports. */
|
|
||||||
// {
|
|
||||||
// name: 'Mobile Chrome',
|
|
||||||
// use: { ...devices['Pixel 5'] },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Mobile Safari',
|
|
||||||
// use: { ...devices['iPhone 12'] },
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Test against branded browsers. */
|
|
||||||
// {
|
|
||||||
// name: 'Microsoft Edge',
|
|
||||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'Google Chrome',
|
|
||||||
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
webServer: {
|
|
||||||
command: 'npm run dev',
|
...(useWebServer
|
||||||
url: 'http://127.0.0.1:3000',
|
&& {
|
||||||
reuseExistingServer: !process.env.CI,
|
webServer: {
|
||||||
},
|
command: 'npm run preview',
|
||||||
|
url: 'http://127.0.0.1:5050',
|
||||||
|
reuseExistingServer: !isCI,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
8227
pnpm-lock.yaml
generated
8227
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
6
renovate.json
Normal file
6
renovate.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
]
|
||||||
|
}
|
@@ -29,9 +29,9 @@ createToolFile(
|
|||||||
`${toolName}.vue`,
|
`${toolName}.vue`,
|
||||||
`
|
`
|
||||||
<template>
|
<template>
|
||||||
<n-card>
|
<div>
|
||||||
Lorem ipsum
|
Lorem ipsum
|
||||||
</n-card>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
24
src/App.vue
24
src/App.vue
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { RouterView, useRoute } from 'vue-router';
|
||||||
import { useRoute, RouterView } from 'vue-router';
|
import { NGlobalStyle, NMessageProvider, NNotificationProvider, darkTheme } from 'naive-ui';
|
||||||
import { darkTheme, NGlobalStyle, NMessageProvider, NNotificationProvider } from 'naive-ui';
|
|
||||||
import { darkThemeOverrides, lightThemeOverrides } from './themes';
|
import { darkThemeOverrides, lightThemeOverrides } from './themes';
|
||||||
import { layouts } from './layouts';
|
import { layouts } from './layouts';
|
||||||
import { useStyleStore } from './stores/style.store';
|
import { useStyleStore } from './stores/style.store';
|
||||||
@@ -12,18 +11,25 @@ const styleStore = useStyleStore();
|
|||||||
|
|
||||||
const theme = computed(() => (styleStore.isDarkTheme ? darkTheme : null));
|
const theme = computed(() => (styleStore.isDarkTheme ? darkTheme : null));
|
||||||
const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrides : lightThemeOverrides));
|
const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrides : lightThemeOverrides));
|
||||||
|
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
|
syncRef(
|
||||||
|
locale,
|
||||||
|
useStorage('locale', locale),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-config-provider :theme="theme" :theme-overrides="themeOverrides">
|
<n-config-provider :theme="theme" :theme-overrides="themeOverrides">
|
||||||
<n-global-style />
|
<NGlobalStyle />
|
||||||
<n-message-provider placement="bottom">
|
<NMessageProvider placement="bottom">
|
||||||
<n-notification-provider placement="bottom-right">
|
<NNotificationProvider placement="bottom-right">
|
||||||
<component :is="layout">
|
<component :is="layout">
|
||||||
<router-view />
|
<RouterView />
|
||||||
</component>
|
</component>
|
||||||
</n-notification-provider>
|
</NNotificationProvider>
|
||||||
</n-message-provider>
|
</NMessageProvider>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@@ -1,39 +1,9 @@
|
|||||||
<template>
|
|
||||||
<div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name">
|
|
||||||
<n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })">
|
|
||||||
<n-icon :component="ChevronRight" :class="{ rotated: isCollapsed }" size="16" />
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{{ name }}
|
|
||||||
</span>
|
|
||||||
</n-text>
|
|
||||||
|
|
||||||
<n-collapse-transition :show="!isCollapsed">
|
|
||||||
<div class="menu-wrapper">
|
|
||||||
<div class="toggle-bar" @click="toggleCategoryCollapse({ name })" />
|
|
||||||
|
|
||||||
<n-menu
|
|
||||||
class="menu"
|
|
||||||
:value="(route.name as string)"
|
|
||||||
:collapsed-width="64"
|
|
||||||
:collapsed-icon-size="22"
|
|
||||||
:options="tools"
|
|
||||||
:indent="8"
|
|
||||||
:default-expand-all="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</n-collapse-transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Tool, ToolCategory } from '@/tools/tools.types';
|
|
||||||
import { ChevronRight } from '@vicons/tabler';
|
|
||||||
import { useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import { useThemeVars } from 'naive-ui';
|
import { useThemeVars } from 'naive-ui';
|
||||||
import { toRefs, computed, h } from 'vue';
|
|
||||||
import { RouterLink, useRoute } from 'vue-router';
|
import { RouterLink, useRoute } from 'vue-router';
|
||||||
import MenuIconItem from './MenuIconItem.vue';
|
import MenuIconItem from './MenuIconItem.vue';
|
||||||
|
import type { Tool, ToolCategory } from '@/tools/tools.types';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] });
|
const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] });
|
||||||
const { toolsByCategory } = toRefs(props);
|
const { toolsByCategory } = toRefs(props);
|
||||||
@@ -49,8 +19,8 @@ const collapsedCategories = useStorage<Record<string, boolean>>(
|
|||||||
{
|
{
|
||||||
deep: true,
|
deep: true,
|
||||||
serializer: {
|
serializer: {
|
||||||
read: (v) => (v ? JSON.parse(v) : null),
|
read: v => (v ? JSON.parse(v) : null),
|
||||||
write: (v) => JSON.stringify(v),
|
write: v => JSON.stringify(v),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -61,12 +31,12 @@ function toggleCategoryCollapse({ name }: { name: string }) {
|
|||||||
|
|
||||||
const menuOptions = computed(() =>
|
const menuOptions = computed(() =>
|
||||||
toolsByCategory.value.map(({ name, components }) => ({
|
toolsByCategory.value.map(({ name, components }) => ({
|
||||||
name: name,
|
name,
|
||||||
isCollapsed: collapsedCategories.value[name],
|
isCollapsed: collapsedCategories.value[name],
|
||||||
tools: components.map((tool) => ({
|
tools: components.map(tool => ({
|
||||||
label: makeLabel(tool),
|
label: makeLabel(tool),
|
||||||
icon: makeIcon(tool),
|
icon: makeIcon(tool),
|
||||||
key: tool.name,
|
key: tool.path,
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
@@ -74,27 +44,37 @@ const menuOptions = computed(() =>
|
|||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name">
|
||||||
|
<div ml-6px mt-12px flex cursor-pointer items-center op-60 @click="toggleCategoryCollapse({ name })">
|
||||||
|
<span :class="{ 'rotate-0': isCollapsed, 'rotate-90': !isCollapsed }" text-16px lh-1 op-50 transition-transform>
|
||||||
|
<icon-mdi-chevron-right />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span ml-8px text-13px>
|
||||||
|
{{ name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-collapse-transition :show="!isCollapsed">
|
||||||
|
<div class="menu-wrapper">
|
||||||
|
<div class="toggle-bar" @click="toggleCategoryCollapse({ name })" />
|
||||||
|
|
||||||
|
<n-menu
|
||||||
|
class="menu"
|
||||||
|
:value="route.path"
|
||||||
|
:collapsed-width="64"
|
||||||
|
:collapsed-icon-size="22"
|
||||||
|
:options="tools"
|
||||||
|
:indent="8"
|
||||||
|
:default-expand-all="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-collapse-transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.category-name {
|
|
||||||
font-size: 0.93em;
|
|
||||||
padding: 12px 0 0px 0;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
.n-icon {
|
|
||||||
transition: transform ease 0.5s;
|
|
||||||
transform: rotate(90deg);
|
|
||||||
margin: 0 10px 0 7px;
|
|
||||||
opacity: 0.5;
|
|
||||||
|
|
||||||
&.rotated {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-wrapper {
|
.menu-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@@ -1,8 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ icon: Component; title: string }>();
|
||||||
|
const { icon, title } = toRefs(props);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card class="colored-card">
|
<c-card class="colored-card">
|
||||||
<n-space justify="space-between" align="center">
|
<n-icon class="icon" size="40" :component="icon" />
|
||||||
<n-icon class="icon" size="40" :component="icon" />
|
|
||||||
</n-space>
|
|
||||||
<n-h3 class="title">
|
<n-h3 class="title">
|
||||||
<n-ellipsis>{{ title }}</n-ellipsis>
|
<n-ellipsis>{{ title }}</n-ellipsis>
|
||||||
</n-h3>
|
</n-h3>
|
||||||
@@ -12,16 +17,9 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</c-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { toRefs, type Component } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps<{ icon: Component; title: string }>();
|
|
||||||
const { icon, title } = toRefs(props);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.colored-card {
|
.colored-card {
|
||||||
background: rgb(37, 99, 108);
|
background: rgb(37, 99, 108);
|
||||||
|
@@ -1,25 +1,11 @@
|
|||||||
<template>
|
|
||||||
<n-tooltip trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button circle quaternary :type="buttonType" :style="{ opacity: isFavorite ? 1 : 0.2 }" @click="toggleFavorite">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="FavoriteFilled" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
{{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }}
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { FavoriteFilled } from '@vicons/material';
|
|
||||||
import { useToolStore } from '@/tools/tools.store';
|
import { useToolStore } from '@/tools/tools.store';
|
||||||
import type { Tool } from '@/tools/tools.types';
|
import type { Tool } from '@/tools/tools.types';
|
||||||
import { computed, toRefs } from 'vue';
|
|
||||||
|
const props = defineProps<{ tool: Tool }>();
|
||||||
|
|
||||||
const toolStore = useToolStore();
|
const toolStore = useToolStore();
|
||||||
|
|
||||||
const props = defineProps<{ tool: Tool }>();
|
|
||||||
const { tool } = toRefs(props);
|
const { tool } = toRefs(props);
|
||||||
|
|
||||||
const isFavorite = computed(() => toolStore.isToolFavorite({ tool }));
|
const isFavorite = computed(() => toolStore.isToolFavorite({ tool }));
|
||||||
@@ -36,3 +22,17 @@ function toggleFavorite(event: MouseEvent) {
|
|||||||
toolStore.addToolToFavorites({ tool });
|
toolStore.addToolToFavorites({ tool });
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-tooltip :tooltip="isFavorite ? $t('favoriteButton.remove') : $t('favoriteButton.add') ">
|
||||||
|
<c-button
|
||||||
|
variant="text"
|
||||||
|
circle
|
||||||
|
:type="buttonType"
|
||||||
|
:style="{ opacity: isFavorite ? 1 : 0.2 }"
|
||||||
|
@click="toggleFavorite"
|
||||||
|
>
|
||||||
|
<icon-mdi-heart />
|
||||||
|
</c-button>
|
||||||
|
</c-tooltip>
|
||||||
|
</template>
|
||||||
|
@@ -1,36 +1,17 @@
|
|||||||
<template>
|
|
||||||
<n-form-item :label="inputLabel" v-bind="validationAttrs">
|
|
||||||
<n-input
|
|
||||||
ref="inputElement"
|
|
||||||
v-model:value="input"
|
|
||||||
:placeholder="inputPlaceholder"
|
|
||||||
type="textarea"
|
|
||||||
rows="20"
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
:input-props="{ 'data-test-id': 'input' }"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="outputLabel">
|
|
||||||
<textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement" />
|
|
||||||
</n-form-item>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useValidation, type UseValidationRule } from '@/composable/validation';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import type { UseValidationRule } from '@/composable/validation';
|
||||||
|
import CInputText from '@/ui/c-input-text/c-input-text.vue';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
transformer?: (v: string) => string;
|
transformer?: (v: string) => string
|
||||||
inputValidationRules?: UseValidationRule<string>[];
|
inputValidationRules?: UseValidationRule<string>[]
|
||||||
inputLabel?: string;
|
inputLabel?: string
|
||||||
inputPlaceholder?: string;
|
inputPlaceholder?: string
|
||||||
inputDefault?: string;
|
inputDefault?: string
|
||||||
outputLabel?: string;
|
outputLabel?: string
|
||||||
outputLanguage?: string;
|
outputLanguage?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
transformer: _.identity,
|
transformer: _.identity,
|
||||||
@@ -43,15 +24,34 @@ const props = withDefaults(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } =
|
const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault }
|
||||||
toRefs(props);
|
= toRefs(props);
|
||||||
|
|
||||||
const inputElement = ref();
|
const inputElement = ref<typeof CInputText>();
|
||||||
|
|
||||||
const input = ref(inputDefault.value);
|
const input = ref(inputDefault.value);
|
||||||
const output = computed(() => transformer.value(input.value));
|
const output = computed(() => transformer.value(input.value));
|
||||||
|
|
||||||
const { attrs: validationAttrs } = useValidation({ source: input, rules: inputValidationRules.value });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<template>
|
||||||
|
<CInputText
|
||||||
|
ref="inputElement"
|
||||||
|
v-model:value="input"
|
||||||
|
:placeholder="inputPlaceholder"
|
||||||
|
:label="inputLabel"
|
||||||
|
rows="20"
|
||||||
|
autosize
|
||||||
|
raw-text
|
||||||
|
multiline
|
||||||
|
test-id="input"
|
||||||
|
:validation-rules="inputValidationRules"
|
||||||
|
monospace
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div mb-5px>
|
||||||
|
{{ outputLabel }}
|
||||||
|
</div>
|
||||||
|
<textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
@@ -1,43 +1,23 @@
|
|||||||
<template>
|
|
||||||
<n-input v-model:value="value">
|
|
||||||
<template #suffix>
|
|
||||||
<n-tooltip trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button quaternary circle @click="onCopyClicked">
|
|
||||||
<n-icon :component="ContentCopyFilled" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
{{ tooltipText }}
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
|
||||||
</n-input>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel, useClipboard } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { ContentCopyFilled } from '@vicons/material';
|
import { useCopy } from '@/composable/copy';
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps<{ value: string }>();
|
const props = defineProps<{ value: string }>();
|
||||||
const emit = defineEmits(['update:value']);
|
const emit = defineEmits(['update:value']);
|
||||||
|
|
||||||
const value = useVModel(props, 'value', emit);
|
const value = useVModel(props, 'value', emit);
|
||||||
const tooltipText = ref('Copy to clipboard');
|
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||||
|
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : 'Copy to clipboard');
|
||||||
const { copy } = useClipboard({ source: value });
|
|
||||||
|
|
||||||
function onCopyClicked() {
|
|
||||||
copy();
|
|
||||||
tooltipText.value = 'Copied !';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
tooltipText.value = 'Copy to clipboard';
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<template>
|
||||||
::v-deep(.n-input-wrapper) {
|
<c-input-text v-model:value="value">
|
||||||
padding-right: 5px;
|
<template #suffix>
|
||||||
}
|
<c-tooltip :tooltip="tooltipText">
|
||||||
</style>
|
<c-button circle variant="text" size="small" @click="copy()">
|
||||||
|
<icon-mdi-content-copy />
|
||||||
|
</c-button>
|
||||||
|
</c-tooltip>
|
||||||
|
</template>
|
||||||
|
</c-input-text>
|
||||||
|
</template>
|
||||||
|
@@ -1,14 +1,6 @@
|
|||||||
<template>
|
|
||||||
<div class="menu-icon-item">
|
|
||||||
<n-icon :component="tool.icon" />
|
|
||||||
<div v-if="tool.isNew" class="badge"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Tool } from '@/tools/tools.types';
|
|
||||||
import { useThemeVars } from 'naive-ui';
|
import { useThemeVars } from 'naive-ui';
|
||||||
import { toRefs } from 'vue';
|
import type { Tool } from '@/tools/tools.types';
|
||||||
|
|
||||||
const props = defineProps<{ tool: Tool }>();
|
const props = defineProps<{ tool: Tool }>();
|
||||||
const { tool } = toRefs(props);
|
const { tool } = toRefs(props);
|
||||||
@@ -16,6 +8,13 @@ const { tool } = toRefs(props);
|
|||||||
const theme = useThemeVars();
|
const theme = useThemeVars();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="menu-icon-item">
|
||||||
|
<n-icon :component="tool.icon" />
|
||||||
|
<div v-if="tool.isNew" class="badge" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.menu-icon-item {
|
.menu-icon-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@@ -1,3 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useStyleStore } from '@/stores/style.store';
|
||||||
|
|
||||||
|
const styleStore = useStyleStore();
|
||||||
|
const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore);
|
||||||
|
const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static'));
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-layout has-sider>
|
<n-layout has-sider>
|
||||||
<n-layout-sider
|
<n-layout-sider
|
||||||
@@ -19,15 +27,6 @@
|
|||||||
</n-layout>
|
</n-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useStyleStore } from '@/stores/style.store';
|
|
||||||
import { toRefs, computed } from 'vue';
|
|
||||||
|
|
||||||
const styleStore = useStyleStore();
|
|
||||||
const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore);
|
|
||||||
const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static'));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.overlay {
|
.overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@@ -1,71 +1,51 @@
|
|||||||
<template>
|
|
||||||
<n-tooltip trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button
|
|
||||||
size="large"
|
|
||||||
circle
|
|
||||||
quaternary
|
|
||||||
tag="a"
|
|
||||||
href="https://github.com/CorentinTh/it-tools"
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
aria-label="IT-Tools' GitHub repository"
|
|
||||||
>
|
|
||||||
<n-icon size="25" :component="BrandGithub" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
Github repository
|
|
||||||
</n-tooltip>
|
|
||||||
|
|
||||||
<n-tooltip trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button
|
|
||||||
size="large"
|
|
||||||
circle
|
|
||||||
quaternary
|
|
||||||
tag="a"
|
|
||||||
href="https://twitter.com/ittoolsdottech"
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
aria-label="IT Tools' Twitter account"
|
|
||||||
>
|
|
||||||
<n-icon size="25" :component="BrandTwitter" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
IT Tools' Twitter account
|
|
||||||
</n-tooltip>
|
|
||||||
|
|
||||||
<router-link to="/about" #="{ navigate, href }" custom>
|
|
||||||
<n-tooltip trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button tag="a" :href="href" circle quaternary size="large" aria-label="About" @click="navigate">
|
|
||||||
<n-icon size="25" :component="InfoCircle" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
About
|
|
||||||
</n-tooltip>
|
|
||||||
</router-link>
|
|
||||||
<n-tooltip trigger="hover">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button size="large" circle quaternary aria-label="Toggle dark/light mode" @click="isDarkTheme = !isDarkTheme">
|
|
||||||
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
|
|
||||||
<n-icon v-else size="25" :component="Moon" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
<span v-if="isDarkTheme">Light mode</span>
|
|
||||||
<span v-else>Dark mode</span>
|
|
||||||
</n-tooltip>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStyleStore } from '@/stores/style.store';
|
|
||||||
import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler';
|
import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler';
|
||||||
import { toRefs } from 'vue';
|
import { useStyleStore } from '@/stores/style.store';
|
||||||
|
|
||||||
const styleStore = useStyleStore();
|
const styleStore = useStyleStore();
|
||||||
const { isDarkTheme } = toRefs(styleStore);
|
const { isDarkTheme } = toRefs(styleStore);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-tooltip :tooltip="$t('home.nav.github')" position="bottom">
|
||||||
|
<c-button
|
||||||
|
circle
|
||||||
|
variant="text"
|
||||||
|
href="https://github.com/CorentinTh/it-tools"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
:aria-label="$t('home.nav.githubRepository')"
|
||||||
|
>
|
||||||
|
<n-icon size="25" :component="BrandGithub" />
|
||||||
|
</c-button>
|
||||||
|
</c-tooltip>
|
||||||
|
|
||||||
|
<c-tooltip :tooltip="$t('home.nav.twitter')" position="bottom">
|
||||||
|
<c-button
|
||||||
|
circle
|
||||||
|
variant="text"
|
||||||
|
href="https://twitter.com/ittoolsdottech"
|
||||||
|
rel="noopener"
|
||||||
|
target="_blank"
|
||||||
|
:aria-label="$t('home.nav.twitterAccount')"
|
||||||
|
>
|
||||||
|
<n-icon size="25" :component="BrandTwitter" />
|
||||||
|
</c-button>
|
||||||
|
</c-tooltip>
|
||||||
|
|
||||||
|
<c-tooltip :tooltip="$t('home.nav.about')" position="bottom">
|
||||||
|
<c-button circle variant="text" to="/about" :aria-label="$t('home.nav.aboutLabel')">
|
||||||
|
<n-icon size="25" :component="InfoCircle" />
|
||||||
|
</c-button>
|
||||||
|
</c-tooltip>
|
||||||
|
<c-tooltip :tooltip="isDarkTheme ? $t('home.nav.lightMode') : $t('home.nav.darkMode')" position="bottom">
|
||||||
|
<c-button circle variant="text" :aria-label="$t('home.nav.mode')" @click="() => styleStore.toggleDark()">
|
||||||
|
<n-icon v-if="isDarkTheme" size="25" :component="Sun" />
|
||||||
|
<n-icon v-else size="25" :component="Moon" />
|
||||||
|
</c-button>
|
||||||
|
</c-tooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.n-button {
|
.n-button {
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
|
@@ -1,110 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import { useFuzzySearch } from '@/composable/fuzzySearch';
|
|
||||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
|
||||||
import { tools } from '@/tools';
|
|
||||||
import type { Tool } from '@/tools/tools.types';
|
|
||||||
import { SearchRound } from '@vicons/material';
|
|
||||||
import { useMagicKeys, whenever } from '@vueuse/core';
|
|
||||||
import type { NInput } from 'naive-ui';
|
|
||||||
import { computed, h, ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import SearchBarItem from './SearchBarItem.vue';
|
|
||||||
|
|
||||||
const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool });
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { tracker } = useTracker();
|
|
||||||
|
|
||||||
const queryString = ref('');
|
|
||||||
const inputEl = ref<HTMLElement>();
|
|
||||||
const displayDropDown = ref(true);
|
|
||||||
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
|
|
||||||
|
|
||||||
const options = computed(() => {
|
|
||||||
if (queryString.value === '') {
|
|
||||||
return tools.map(toolToOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchResult.value.map(toolToOption);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { searchResult } = useFuzzySearch({
|
|
||||||
search: queryString,
|
|
||||||
data: tools,
|
|
||||||
options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] },
|
|
||||||
});
|
|
||||||
|
|
||||||
const keys = useMagicKeys({
|
|
||||||
passive: false,
|
|
||||||
onEventFired(e) {
|
|
||||||
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.metaKey && e.key === 'k' && e.type === 'keydown') {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
whenever(keys.ctrl_k, claimFocus);
|
|
||||||
whenever(keys.meta_k, claimFocus);
|
|
||||||
whenever(keys.escape, releaseFocus);
|
|
||||||
|
|
||||||
function renderOption({ tool }: { tool: Tool }) {
|
|
||||||
return h(SearchBarItem, { tool });
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelect(path: string) {
|
|
||||||
router.push(path);
|
|
||||||
queryString.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function claimFocus() {
|
|
||||||
displayDropDown.value = true;
|
|
||||||
|
|
||||||
inputEl.value?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function releaseFocus() {
|
|
||||||
displayDropDown.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onFocus() {
|
|
||||||
tracker.trackEvent({ eventName: 'Search-bar focused' });
|
|
||||||
displayDropDown.value = true;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="search-bar">
|
|
||||||
<n-auto-complete
|
|
||||||
v-model:value="queryString"
|
|
||||||
:options="options"
|
|
||||||
:on-select="(value) => onSelect(String(value))"
|
|
||||||
:render-label="renderOption"
|
|
||||||
:default-value="'aa'"
|
|
||||||
:get-show="() => displayDropDown"
|
|
||||||
:on-focus="onFocus"
|
|
||||||
@update:value="() => (displayDropDown = true)"
|
|
||||||
>
|
|
||||||
<template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }">
|
|
||||||
<n-input
|
|
||||||
ref="inputEl"
|
|
||||||
round
|
|
||||||
clearable
|
|
||||||
:placeholder="`Search a tool (use ${isMac ? 'Cmd' : 'Ctrl'} + K to focus)`"
|
|
||||||
:value="slotValue"
|
|
||||||
:input-props="{ autocomplete: 'disabled' }"
|
|
||||||
@input="handleInput"
|
|
||||||
@focus="handleFocus"
|
|
||||||
@blur="handleBlur"
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<n-icon :component="SearchRound" />
|
|
||||||
</template>
|
|
||||||
</n-input>
|
|
||||||
</template>
|
|
||||||
</n-auto-complete>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@@ -1,45 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import type { Tool } from '@/tools/tools.types';
|
|
||||||
import { toRefs } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps<{ tool: Tool }>();
|
|
||||||
const { tool } = toRefs(props);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="search-bar-item">
|
|
||||||
<n-icon class="icon" :component="tool.icon" />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="name">{{ tool.name }}</div>
|
|
||||||
<div class="description">{{ tool.description }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.search-bar-item {
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
font-size: 30px;
|
|
||||||
margin-right: 10px;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
opacity: 0.7;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
17
src/components/SpanCopyable.vue
Normal file
17
src/components/SpanCopyable.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useCopy } from '@/composable/copy';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ value?: string }>(), { value: '' });
|
||||||
|
const { value } = toRefs(props);
|
||||||
|
|
||||||
|
const initialText = 'Copy to clipboard';
|
||||||
|
|
||||||
|
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||||
|
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : initialText);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-tooltip :tooltip="tooltipText">
|
||||||
|
<span cursor-pointer font-mono @click="copy()">{{ value }}</span>
|
||||||
|
</c-tooltip>
|
||||||
|
</template>
|
@@ -1,6 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Copy } from '@vicons/tabler';
|
||||||
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import hljs from 'highlight.js/lib/core';
|
||||||
|
import jsonHljs from 'highlight.js/lib/languages/json';
|
||||||
|
import sqlHljs from 'highlight.js/lib/languages/sql';
|
||||||
|
import xmlHljs from 'highlight.js/lib/languages/xml';
|
||||||
|
import yamlHljs from 'highlight.js/lib/languages/yaml';
|
||||||
|
import iniHljs from 'highlight.js/lib/languages/ini';
|
||||||
|
import { useCopy } from '@/composable/copy';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
value: string
|
||||||
|
followHeightOf?: HTMLElement | null
|
||||||
|
language?: string
|
||||||
|
copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none'
|
||||||
|
copyMessage?: string
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
followHeightOf: null,
|
||||||
|
language: 'txt',
|
||||||
|
copyPlacement: 'top-right',
|
||||||
|
copyMessage: 'Copy to clipboard',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
hljs.registerLanguage('sql', sqlHljs);
|
||||||
|
hljs.registerLanguage('json', jsonHljs);
|
||||||
|
hljs.registerLanguage('html', xmlHljs);
|
||||||
|
hljs.registerLanguage('xml', xmlHljs);
|
||||||
|
hljs.registerLanguage('yaml', yamlHljs);
|
||||||
|
hljs.registerLanguage('toml', iniHljs);
|
||||||
|
|
||||||
|
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
|
||||||
|
const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) };
|
||||||
|
|
||||||
|
const { copy, isJustCopied } = useCopy({ source: value, createToast: false });
|
||||||
|
const tooltipText = computed(() => isJustCopied.value ? 'Copied!' : copyMessage.value);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="overflow-x: hidden; width: 100%">
|
<div style="overflow-x: hidden; width: 100%">
|
||||||
<n-card class="result-card">
|
<c-card relative>
|
||||||
<n-scrollbar
|
<n-scrollbar
|
||||||
x-scrollable
|
x-scrollable
|
||||||
trigger="none"
|
trigger="none"
|
||||||
@@ -10,93 +50,25 @@
|
|||||||
<n-code :code="value" :language="language" :trim="false" data-test-id="area-content" />
|
<n-code :code="value" :language="language" :trim="false" data-test-id="area-content" />
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
<n-tooltip v-if="value" trigger="hover">
|
<div absolute right-10px top-10px>
|
||||||
<template #trigger>
|
<c-tooltip v-if="value" :tooltip="tooltipText" position="left">
|
||||||
<div class="copy-button" :class="[copyPlacement]">
|
<c-button circle important:h-10 important:w-10 @click="copy()">
|
||||||
<n-button circle secondary size="large" @click="onCopyClicked">
|
<n-icon size="22" :component="Copy" />
|
||||||
<n-icon size="22" :component="Copy" />
|
</c-button>
|
||||||
</n-button>
|
</c-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</c-card>
|
||||||
<span>{{ tooltipText }}</span>
|
<div v-if="copyPlacement === 'outside'" mt-4 flex justify-center>
|
||||||
</n-tooltip>
|
<c-button @click="copy()">
|
||||||
</n-card>
|
{{ tooltipText }}
|
||||||
<n-space v-if="copyPlacement === 'outside'" justify="center" mt-4>
|
</c-button>
|
||||||
<n-button secondary @click="onCopyClicked"> {{ tooltipText }} </n-button>
|
</div>
|
||||||
</n-space>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Copy } from '@vicons/tabler';
|
|
||||||
import { useClipboard, useElementSize } from '@vueuse/core';
|
|
||||||
import hljs from 'highlight.js/lib/core';
|
|
||||||
import jsonHljs from 'highlight.js/lib/languages/json';
|
|
||||||
import sqlHljs from 'highlight.js/lib/languages/sql';
|
|
||||||
import xmlHljs from 'highlight.js/lib/languages/xml';
|
|
||||||
import yamlHljs from 'highlight.js/lib/languages/yaml';
|
|
||||||
import { ref, toRefs } from 'vue';
|
|
||||||
|
|
||||||
hljs.registerLanguage('sql', sqlHljs);
|
|
||||||
hljs.registerLanguage('json', jsonHljs);
|
|
||||||
hljs.registerLanguage('html', xmlHljs);
|
|
||||||
hljs.registerLanguage('yaml', yamlHljs);
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
value: string;
|
|
||||||
followHeightOf?: HTMLElement | null;
|
|
||||||
language?: string;
|
|
||||||
copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none';
|
|
||||||
copyMessage?: string;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
followHeightOf: null,
|
|
||||||
language: 'txt',
|
|
||||||
copyPlacement: 'top-right',
|
|
||||||
copyMessage: 'Copy to clipboard',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props);
|
|
||||||
const { height } = followHeightOf ? useElementSize(followHeightOf) : { height: ref(null) };
|
|
||||||
|
|
||||||
const { copy } = useClipboard({ source: value });
|
|
||||||
const tooltipText = ref(copyMessage.value);
|
|
||||||
|
|
||||||
function onCopyClicked() {
|
|
||||||
copy();
|
|
||||||
tooltipText.value = 'Copied !';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
tooltipText.value = copyMessage.value;
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
::v-deep(.n-scrollbar) {
|
::v-deep(.n-scrollbar) {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
margin-bottom: -10px;
|
margin-bottom: -10px;
|
||||||
}
|
}
|
||||||
.result-card {
|
|
||||||
position: relative;
|
|
||||||
.copy-button {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
&.top-right {
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bottom-right {
|
|
||||||
bottom: 10px;
|
|
||||||
right: 10px;
|
|
||||||
}
|
|
||||||
&.outside,
|
|
||||||
&.none {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,9 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useThemeVars } from 'naive-ui';
|
||||||
|
import FavoriteButton from './FavoriteButton.vue';
|
||||||
|
import { useAppTheme } from '@/ui/theme/themes';
|
||||||
|
import type { Tool } from '@/tools/tools.types';
|
||||||
|
|
||||||
|
const props = defineProps<{ tool: Tool & { category: string } }>();
|
||||||
|
const { tool } = toRefs(props);
|
||||||
|
const theme = useThemeVars();
|
||||||
|
|
||||||
|
const appTheme = useAppTheme();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-link :to="tool.path">
|
<router-link :to="tool.path">
|
||||||
<n-card class="tool-card">
|
<c-card class="tool-card">
|
||||||
<n-space justify="space-between" align="center">
|
<div flex items-center justify-between>
|
||||||
<n-icon class="icon" size="40" :component="tool.icon" />
|
<n-icon class="icon" size="40" :component="tool.icon" />
|
||||||
<n-space align="center">
|
<div flex items-center gap-8px>
|
||||||
<n-tag
|
<n-tag
|
||||||
v-if="tool.isNew"
|
v-if="tool.isNew"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -13,12 +26,12 @@
|
|||||||
:bordered="false"
|
:bordered="false"
|
||||||
:color="{ color: theme.primaryColor, textColor: theme.tagColor }"
|
:color="{ color: theme.primaryColor, textColor: theme.tagColor }"
|
||||||
>
|
>
|
||||||
New
|
{{ $t('toolCard.new') }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
|
|
||||||
<favorite-button :tool="tool" />
|
<FavoriteButton :tool="tool" />
|
||||||
</n-space>
|
</div>
|
||||||
</n-space>
|
</div>
|
||||||
<n-h3 class="title">
|
<n-h3 class="title">
|
||||||
<n-ellipsis>{{ tool.name }}</n-ellipsis>
|
<n-ellipsis>{{ tool.name }}</n-ellipsis>
|
||||||
</n-h3>
|
</n-h3>
|
||||||
@@ -26,32 +39,25 @@
|
|||||||
<div class="description">
|
<div class="description">
|
||||||
<n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px">
|
<n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px">
|
||||||
{{ tool.description }}
|
{{ tool.description }}
|
||||||
<br />
|
<br>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</c-card>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Tool } from '@/tools/tools.types';
|
|
||||||
import { useThemeVars } from 'naive-ui';
|
|
||||||
import { toRefs } from 'vue';
|
|
||||||
import FavoriteButton from './FavoriteButton.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{ tool: Tool & { category: string } }>();
|
|
||||||
const { tool } = toRefs(props);
|
|
||||||
const theme = useThemeVars();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-card {
|
.tool-card {
|
||||||
|
transition: border-color ease 0.5s;
|
||||||
|
border-width: 2px !important;
|
||||||
|
color: transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--n-color-target);
|
border-color: v-bind('appTheme.primary.colorHover');
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
22
src/composable/computed/catchedComputed.ts
Normal file
22
src/composable/computed/catchedComputed.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { type Ref, ref, watchEffect } from 'vue';
|
||||||
|
|
||||||
|
export { computedCatch };
|
||||||
|
|
||||||
|
function computedCatch<T, D>(getter: () => T, { defaultValue }: { defaultValue: D; defaultErrorMessage?: string }): [Ref<T | D>, Ref<string | undefined>];
|
||||||
|
function computedCatch<T, D>(getter: () => T, { defaultValue, defaultErrorMessage = 'Unknown error' }: { defaultValue?: D; defaultErrorMessage?: string } = {}) {
|
||||||
|
const error = ref<string | undefined>();
|
||||||
|
const value = ref<T | D | undefined>();
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
try {
|
||||||
|
error.value = undefined;
|
||||||
|
value.value = getter();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : err?.toString() ?? defaultErrorMessage;
|
||||||
|
value.value = defaultValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [value, error] as const;
|
||||||
|
}
|
@@ -11,7 +11,8 @@ function computedRefreshable<T>(getter: () => T, { throttle }: { throttle?: numb
|
|||||||
|
|
||||||
if (throttle) {
|
if (throttle) {
|
||||||
watchThrottled(getter, update, { throttle });
|
watchThrottled(getter, update, { throttle });
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
watch(getter, update);
|
watch(getter, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,15 +1,30 @@
|
|||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { useClipboard } from '@vueuse/core';
|
import { useClipboard } from '@vueuse/core';
|
||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
import type { Ref } from 'vue';
|
import type { MaybeRefOrGetter } from 'vue';
|
||||||
|
|
||||||
|
export function useCopy({ source, text = 'Copied to the clipboard', createToast = true }: { source?: MaybeRefOrGetter<string>; text?: string; createToast?: boolean } = {}) {
|
||||||
|
const { copy, copied, ...rest } = useClipboard({
|
||||||
|
source,
|
||||||
|
legacy: true,
|
||||||
|
});
|
||||||
|
|
||||||
export function useCopy({ source, text = 'Copied to the clipboard' }: { source: Ref; text?: string }) {
|
|
||||||
const { copy } = useClipboard({ source });
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async copy() {
|
...rest,
|
||||||
await copy();
|
isJustCopied: copied,
|
||||||
message.success(text);
|
async copy(content?: string, { notificationMessage }: { notificationMessage?: string } = {}) {
|
||||||
|
if (source) {
|
||||||
|
await copy();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await copy(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createToast) {
|
||||||
|
message.success(notificationMessage ?? text);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
32
src/composable/downloadBase64.test.ts
Normal file
32
src/composable/downloadBase64.test.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { getMimeTypeFromBase64 } from './downloadBase64';
|
||||||
|
|
||||||
|
describe('downloadBase64', () => {
|
||||||
|
describe('getMimeTypeFromBase64', () => {
|
||||||
|
it('when the base64 string has a data URI, it returns the mime type', () => {
|
||||||
|
expect(getMimeTypeFromBase64({ base64String: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/png' });
|
||||||
|
expect(getMimeTypeFromBase64({ base64String: 'data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/jpg' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when the base64 string has no data URI, it try to infer the mime type from the signature', () => {
|
||||||
|
// https://en.wikipedia.org/wiki/List_of_file_signatures
|
||||||
|
|
||||||
|
// PNG
|
||||||
|
expect(getMimeTypeFromBase64({ base64String: 'iVBORw0KGgoAAAANSUhEUgAAAAUA' })).to.deep.equal({ mimeType: 'image/png' });
|
||||||
|
|
||||||
|
// GIF
|
||||||
|
expect(getMimeTypeFromBase64({ base64String: 'R0lGODdh' })).to.deep.equal({ mimeType: 'image/gif' });
|
||||||
|
expect(getMimeTypeFromBase64({ base64String: 'R0lGODlh' })).to.deep.equal({ mimeType: 'image/gif' });
|
||||||
|
|
||||||
|
// JPG
|
||||||
|
expect(getMimeTypeFromBase64({ base64String: '/9j/' })).to.deep.equal({ mimeType: 'image/jpg' });
|
||||||
|
|
||||||
|
// PDF
|
||||||
|
expect(getMimeTypeFromBase64({ base64String: 'JVBERi0' })).to.deep.equal({ mimeType: 'application/pdf' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when the base64 string has no data URI and no signature, it returns an undefined mimeType', () => {
|
||||||
|
expect(getMimeTypeFromBase64({ base64String: 'JVBERi' })).to.deep.equal({ mimeType: undefined });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -1,32 +1,60 @@
|
|||||||
import { extension as getExtensionFromMime } from 'mime-types';
|
import { extension as getExtensionFromMime } from 'mime-types';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
function getFileExtensionFromBase64({
|
export { getMimeTypeFromBase64, useDownloadFileFromBase64 };
|
||||||
base64String,
|
|
||||||
|
const commonMimeTypesSignatures = {
|
||||||
|
'JVBERi0': 'application/pdf',
|
||||||
|
'R0lGODdh': 'image/gif',
|
||||||
|
'R0lGODlh': 'image/gif',
|
||||||
|
'iVBORw0KGgo': 'image/png',
|
||||||
|
'/9j/': 'image/jpg',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMimeTypeFromBase64({ base64String }: { base64String: string }) {
|
||||||
|
const [,mimeTypeFromBase64] = base64String.match(/data:(.*?);base64/i) ?? [];
|
||||||
|
|
||||||
|
if (mimeTypeFromBase64) {
|
||||||
|
return { mimeType: mimeTypeFromBase64 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const inferredMimeType = _.find(commonMimeTypesSignatures, (_mimeType, signature) => base64String.startsWith(signature));
|
||||||
|
|
||||||
|
if (inferredMimeType) {
|
||||||
|
return { mimeType: inferredMimeType };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { mimeType: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileExtensionFromMimeType({
|
||||||
|
mimeType,
|
||||||
defaultExtension = 'txt',
|
defaultExtension = 'txt',
|
||||||
}: {
|
}: {
|
||||||
base64String: string;
|
mimeType: string | undefined
|
||||||
defaultExtension?: string;
|
defaultExtension?: string
|
||||||
}) {
|
}) {
|
||||||
const hasMimeType = base64String.match(/data:(.*?);base64/i);
|
if (mimeType) {
|
||||||
|
return getExtensionFromMime(mimeType) ?? defaultExtension;
|
||||||
if (hasMimeType) {
|
|
||||||
return getExtensionFromMime(hasMimeType[1]) || defaultExtension;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultExtension;
|
return defaultExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
|
function useDownloadFileFromBase64({ source, filename }: { source: Ref<string>; filename?: string }) {
|
||||||
return {
|
return {
|
||||||
download() {
|
download() {
|
||||||
const base64String = source.value;
|
if (source.value === '') {
|
||||||
|
|
||||||
if (base64String === '') {
|
|
||||||
throw new Error('Base64 string is empty');
|
throw new Error('Base64 string is empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanFileName = filename ?? `file.${getFileExtensionFromBase64({ base64String })}`;
|
const { mimeType } = getMimeTypeFromBase64({ base64String: source.value });
|
||||||
|
const base64String = mimeType
|
||||||
|
? source.value
|
||||||
|
: `data:text/plain;base64,${source.value}`;
|
||||||
|
|
||||||
|
const cleanFileName = filename ?? `file.${getFileExtensionFromMimeType({ mimeType })}`;
|
||||||
|
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = base64String;
|
a.href = base64String;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { get, type MaybeRef } from '@vueuse/core';
|
import { type MaybeRef, get } from '@vueuse/core';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
@@ -9,14 +9,21 @@ function useFuzzySearch<Data>({
|
|||||||
data,
|
data,
|
||||||
options = {},
|
options = {},
|
||||||
}: {
|
}: {
|
||||||
search: MaybeRef<string>;
|
search: MaybeRef<string>
|
||||||
data: Data[];
|
data: Data[]
|
||||||
options?: Fuse.IFuseOptions<Data>;
|
options?: Fuse.IFuseOptions<Data> & { filterEmpty?: boolean }
|
||||||
}) {
|
}) {
|
||||||
const fuse = new Fuse(data, options);
|
const fuse = new Fuse(data, options);
|
||||||
|
const filterEmpty = options.filterEmpty ?? true;
|
||||||
|
|
||||||
const searchResult = computed(() => {
|
const searchResult = computed<Data[]>(() => {
|
||||||
return fuse.search(get(search)).map(({ item }) => item);
|
const query = get(search);
|
||||||
|
|
||||||
|
if (!filterEmpty && query === '') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fuse.search(query).map(({ item }) => item);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { searchResult };
|
return { searchResult };
|
||||||
|
@@ -26,7 +26,7 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue:
|
|||||||
|
|
||||||
return computed<T>({
|
return computed<T>({
|
||||||
get() {
|
get() {
|
||||||
return transformer.fromQuery(proxy.value) as T;
|
return transformer.fromQuery(proxy.value) as unknown as T;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
proxy.value = transformer.toQuery(value as never);
|
proxy.value = transformer.toQuery(value as never);
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { isFalsyOrHasThrown } from './validation';
|
import { isFalsyOrHasThrown } from './validation';
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ describe('useValidation', () => {
|
|||||||
expect(isFalsyOrHasThrown(() => {})).toBe(true);
|
expect(isFalsyOrHasThrown(() => {})).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
isFalsyOrHasThrown(() => {
|
isFalsyOrHasThrown(() => {
|
||||||
throw new Error();
|
throw new Error('message');
|
||||||
}),
|
}),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
@@ -1,44 +1,48 @@
|
|||||||
|
import { type MaybeRef, get } from '@vueuse/core';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { reactive, watch, type Ref } from 'vue';
|
import { type Ref, reactive, watch } from 'vue';
|
||||||
|
|
||||||
type ValidatorReturnType = unknown;
|
type ValidatorReturnType = unknown;
|
||||||
|
|
||||||
export interface UseValidationRule<T> {
|
export interface UseValidationRule<T> {
|
||||||
validator: (value: T) => ValidatorReturnType;
|
validator: (value: T) => ValidatorReturnType
|
||||||
message: string;
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
|
export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean {
|
||||||
try {
|
try {
|
||||||
const returnValue = cb();
|
const returnValue = cb();
|
||||||
|
|
||||||
if (_.isNil(returnValue)) return true;
|
if (_.isNil(returnValue)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return returnValue === false;
|
return returnValue === false;
|
||||||
} catch (_) {
|
}
|
||||||
|
catch (_) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ValidationAttrs = {
|
export interface ValidationAttrs {
|
||||||
feedback: string;
|
feedback: string
|
||||||
validationStatus: string | undefined;
|
validationStatus: string | undefined
|
||||||
};
|
}
|
||||||
|
|
||||||
export function useValidation<T>({
|
export function useValidation<T>({
|
||||||
source,
|
source,
|
||||||
rules,
|
rules,
|
||||||
watch: watchRefs = [],
|
watch: watchRefs = [],
|
||||||
}: {
|
}: {
|
||||||
source: Ref<T>;
|
source: Ref<T>
|
||||||
rules: UseValidationRule<T>[];
|
rules: MaybeRef<UseValidationRule<T>[]>
|
||||||
watch?: Ref<unknown>[];
|
watch?: Ref<unknown>[]
|
||||||
}) {
|
}) {
|
||||||
const state = reactive<{
|
const state = reactive<{
|
||||||
message: string;
|
message: string
|
||||||
status: undefined | 'error';
|
status: undefined | 'error'
|
||||||
isValid: boolean;
|
isValid: boolean
|
||||||
attrs: ValidationAttrs;
|
attrs: ValidationAttrs
|
||||||
}>({
|
}>({
|
||||||
message: '',
|
message: '',
|
||||||
status: undefined,
|
status: undefined,
|
||||||
@@ -55,7 +59,7 @@ export function useValidation<T>({
|
|||||||
state.message = '';
|
state.message = '';
|
||||||
state.status = undefined;
|
state.status = undefined;
|
||||||
|
|
||||||
for (const rule of rules) {
|
for (const rule of get(rules)) {
|
||||||
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
|
if (isFalsyOrHasThrown(() => rule.validator(source.value))) {
|
||||||
state.message = rule.message;
|
state.message = rule.message;
|
||||||
state.status = 'error';
|
state.status = 'error';
|
||||||
|
@@ -23,9 +23,9 @@ export const config = figue({
|
|||||||
env: {
|
env: {
|
||||||
doc: 'Application current env',
|
doc: 'Application current env',
|
||||||
format: 'enum',
|
format: 'enum',
|
||||||
values: ['production', 'development', 'test'],
|
values: ['production', 'development', 'preview', 'test'],
|
||||||
default: 'development',
|
default: 'development',
|
||||||
env: 'MODE',
|
env: 'VITE_VERCEL_ENV',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plausible: {
|
plausible: {
|
||||||
|
@@ -1,19 +1,19 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { NIcon, useThemeVars } from 'naive-ui';
|
import { NIcon, useThemeVars } from 'naive-ui';
|
||||||
import { computed } from 'vue';
|
|
||||||
import { RouterLink } from 'vue-router';
|
import { RouterLink } from 'vue-router';
|
||||||
import { Heart, Menu2, Home2 } from '@vicons/tabler';
|
import { Heart, Home2, Menu2 } from '@vicons/tabler';
|
||||||
import { toolsByCategory } from '@/tools';
|
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import HeroGradient from '../assets/hero-gradient.svg?component';
|
||||||
|
import MenuLayout from '../components/MenuLayout.vue';
|
||||||
|
import NavbarButtons from '../components/NavbarButtons.vue';
|
||||||
import { useStyleStore } from '@/stores/style.store';
|
import { useStyleStore } from '@/stores/style.store';
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
import type { ToolCategory } from '@/tools/tools.types';
|
import type { ToolCategory } from '@/tools/tools.types';
|
||||||
import { useToolStore } from '@/tools/tools.store';
|
import { useToolStore } from '@/tools/tools.store';
|
||||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
import { useTracker } from '@/modules/tracker/tracker.services';
|
||||||
import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue';
|
import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue';
|
||||||
import SearchBar from '../components/SearchBar.vue';
|
|
||||||
import HeroGradient from '../assets/hero-gradient.svg?component';
|
|
||||||
import MenuLayout from '../components/MenuLayout.vue';
|
|
||||||
import NavbarButtons from '../components/NavbarButtons.vue';
|
|
||||||
|
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
const styleStore = useStyleStore();
|
const styleStore = useStyleStore();
|
||||||
@@ -21,133 +21,123 @@ const version = config.app.version;
|
|||||||
const commitSha = config.app.lastCommitSha.slice(0, 7);
|
const commitSha = config.app.lastCommitSha.slice(0, 7);
|
||||||
|
|
||||||
const { tracker } = useTracker();
|
const { tracker } = useTracker();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const toolStore = useToolStore();
|
const toolStore = useToolStore();
|
||||||
|
const { favoriteTools, toolsByCategory } = storeToRefs(toolStore);
|
||||||
|
|
||||||
const tools = computed<ToolCategory[]>(() => [
|
const tools = computed<ToolCategory[]>(() => [
|
||||||
...(toolStore.favoriteTools.length > 0 ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }] : []),
|
...(favoriteTools.value.length > 0 ? [{ name: t('tools.categories.favorite-tools'), components: favoriteTools.value }] : []),
|
||||||
...toolsByCategory,
|
...toolsByCategory.value,
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<menu-layout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }">
|
<MenuLayout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }">
|
||||||
<template #sider>
|
<template #sider>
|
||||||
<router-link to="/" class="hero-wrapper">
|
<RouterLink to="/" class="hero-wrapper">
|
||||||
<hero-gradient class="gradient" />
|
<HeroGradient class="gradient" />
|
||||||
<div class="text-wrapper">
|
<div class="text-wrapper">
|
||||||
<div class="title">IT - TOOLS</div>
|
<div class="title">
|
||||||
|
IT - TOOLS
|
||||||
|
</div>
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
<div class="subtitle">Handy tools for developers</div>
|
<div class="subtitle">
|
||||||
|
{{ $t('home.subtitle') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</RouterLink>
|
||||||
|
|
||||||
<div class="sider-content">
|
<div class="sider-content">
|
||||||
<n-space v-if="styleStore.isSmallScreen" justify="center">
|
<div v-if="styleStore.isSmallScreen" flex flex-col items-center>
|
||||||
<navbar-buttons />
|
<locale-selector w="90%" />
|
||||||
</n-space>
|
|
||||||
|
|
||||||
<collapsible-tool-menu :tools-by-category="tools" />
|
<div flex justify-center>
|
||||||
|
<NavbarButtons />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CollapsibleToolMenu :tools-by-category="tools" />
|
||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div>
|
<div>
|
||||||
IT-Tools
|
IT-Tools
|
||||||
|
|
||||||
<n-button
|
<c-link target="_blank" rel="noopener" :href="`https://github.com/CorentinTh/it-tools/tree/v${version}`">
|
||||||
text
|
|
||||||
tag="a"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
type="primary"
|
|
||||||
depth="3"
|
|
||||||
:href="`https://github.com/CorentinTh/it-tools/tree/v${version}`"
|
|
||||||
>
|
|
||||||
v{{ version }}
|
v{{ version }}
|
||||||
</n-button>
|
</c-link>
|
||||||
|
|
||||||
<template v-if="commitSha && commitSha.length > 0">
|
<template v-if="commitSha && commitSha.length > 0">
|
||||||
-
|
-
|
||||||
<n-button
|
<c-link
|
||||||
text
|
|
||||||
tag="a"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
type="primary"
|
type="primary"
|
||||||
depth="3"
|
|
||||||
:href="`https://github.com/CorentinTh/it-tools/tree/${commitSha}`"
|
:href="`https://github.com/CorentinTh/it-tools/tree/${commitSha}`"
|
||||||
>
|
>
|
||||||
{{ commitSha }}
|
{{ commitSha }}
|
||||||
</n-button>
|
</c-link>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
© {{ new Date().getFullYear() }}
|
© {{ new Date().getFullYear() }}
|
||||||
<n-button text tag="a" target="_blank" rel="noopener" type="primary" href="https://github.com/CorentinTh">
|
<c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh">
|
||||||
Corentin Thomasset
|
Corentin Thomasset
|
||||||
</n-button>
|
</c-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="navigation">
|
<div flex items-center justify-center gap-2>
|
||||||
<n-button
|
<c-button
|
||||||
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
|
|
||||||
circle
|
circle
|
||||||
quaternary
|
variant="text"
|
||||||
aria-label="Toggle menu"
|
:aria-label="$t('home.toggleMenu')"
|
||||||
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
|
@click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed"
|
||||||
>
|
>
|
||||||
<n-icon size="25" :component="Menu2" />
|
<NIcon size="25" :component="Menu2" />
|
||||||
</n-button>
|
</c-button>
|
||||||
|
|
||||||
<router-link to="/" #="{ navigate, href }" custom>
|
<c-tooltip :tooltip="$t('home.home')" position="bottom">
|
||||||
<n-tooltip trigger="hover">
|
<c-button to="/" circle variant="text" :aria-label="$t('home.home')">
|
||||||
<template #trigger>
|
<NIcon size="25" :component="Home2" />
|
||||||
<n-button
|
</c-button>
|
||||||
tag="a"
|
</c-tooltip>
|
||||||
:href="href"
|
|
||||||
:size="styleStore.isSmallScreen ? 'medium' : 'large'"
|
|
||||||
circle
|
|
||||||
quaternary
|
|
||||||
aria-label="Home"
|
|
||||||
@click="navigate"
|
|
||||||
>
|
|
||||||
<n-icon size="25" :component="Home2" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
Home
|
|
||||||
</n-tooltip>
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<search-bar />
|
<c-tooltip :tooltip="$t('home.uiLib')" position="bottom">
|
||||||
|
<c-button v-if="config.app.env === 'development'" to="/c-lib" circle variant="text" :aria-label="$t('home.uiLib')">
|
||||||
|
<icon-mdi:brush-variant text-20px />
|
||||||
|
</c-button>
|
||||||
|
</c-tooltip>
|
||||||
|
|
||||||
<navbar-buttons v-if="!styleStore.isSmallScreen" />
|
<command-palette />
|
||||||
|
|
||||||
<n-tooltip trigger="hover">
|
<locale-selector v-if="!styleStore.isSmallScreen" />
|
||||||
<template #trigger>
|
|
||||||
<n-button
|
<div>
|
||||||
round
|
<NavbarButtons v-if="!styleStore.isSmallScreen" />
|
||||||
type="primary"
|
</div>
|
||||||
tag="a"
|
|
||||||
href="https://www.buymeacoffee.com/cthmsst"
|
<c-tooltip position="bottom" :tooltip="$t('home.support')">
|
||||||
rel="noopener"
|
<c-button
|
||||||
target="_blank"
|
round
|
||||||
class="support-button"
|
href="https://www.buymeacoffee.com/cthmsst"
|
||||||
:bordered="false"
|
rel="noopener"
|
||||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
target="_blank"
|
||||||
>
|
class="support-button"
|
||||||
Buy me a coffee
|
:bordered="false"
|
||||||
<n-icon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
|
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
||||||
</n-button>
|
>
|
||||||
</template>
|
{{ $t('home.buyMeACoffee') }}
|
||||||
❤ Support IT Tools development !
|
<NIcon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 />
|
||||||
</n-tooltip>
|
</c-button>
|
||||||
|
</c-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
</template>
|
</template>
|
||||||
</menu-layout>
|
</MenuLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@@ -165,8 +155,8 @@ const tools = computed<ToolCategory[]>(() => [
|
|||||||
.support-button {
|
.support-button {
|
||||||
background: rgb(37, 99, 108);
|
background: rgb(37, 99, 108);
|
||||||
background: linear-gradient(48deg, rgba(37, 99, 108, 1) 0%, rgba(59, 149, 111, 1) 60%, rgba(20, 160, 88, 1) 100%);
|
background: linear-gradient(48deg, rgba(37, 99, 108, 1) 0%, rgba(59, 149, 111, 1) 60%, rgba(20, 160, 88, 1) 100%);
|
||||||
color: #fff;
|
color: #fff !important;
|
||||||
transition: all ease 0.2s;
|
transition: padding ease 0.2s !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -225,25 +215,4 @@ const tools = computed<ToolCategory[]>(() => [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ::v-deep(.n-menu-item-content-header) {
|
|
||||||
// overflow: visible !important;
|
|
||||||
// // overflow-x: hidden !important;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.navigation {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
& > *:not(:last-child) {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar {
|
|
||||||
// width: 100%;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -2,10 +2,10 @@
|
|||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useHead } from '@vueuse/head';
|
import { useHead } from '@vueuse/head';
|
||||||
import type { HeadObject } from '@vueuse/head';
|
import type { HeadObject } from '@vueuse/head';
|
||||||
import { computed } from 'vue';
|
|
||||||
|
import BaseLayout from './base.layout.vue';
|
||||||
import FavoriteButton from '@/components/FavoriteButton.vue';
|
import FavoriteButton from '@/components/FavoriteButton.vue';
|
||||||
import type { Tool } from '@/tools/tools.types';
|
import type { Tool } from '@/tools/tools.types';
|
||||||
import BaseLayout from './base.layout.vue';
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
@@ -23,26 +23,31 @@ const head = computed<HeadObject>(() => ({
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
useHead(head);
|
useHead(head);
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const i18nKey = computed<string>(() => route.path.trim().replace('/', ''));
|
||||||
|
const toolTitle = computed<string>(() => t(`tools.${i18nKey.value}.title`, String(route.meta.name)));
|
||||||
|
const toolDescription = computed<string>(() => t(`tools.${i18nKey.value}.description`, String(route.meta.description)));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<base-layout>
|
<BaseLayout>
|
||||||
<div class="tool-layout">
|
<div class="tool-layout">
|
||||||
<div class="tool-header">
|
<div class="tool-header">
|
||||||
<n-space align="center" justify="space-between" :wrap="false">
|
<div flex flex-nowrap items-center justify-between>
|
||||||
<n-h1>
|
<n-h1>
|
||||||
{{ route.meta.name }}
|
{{ toolTitle }}
|
||||||
</n-h1>
|
</n-h1>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<favorite-button :tool="{name: route.meta.name} as Tool" />
|
<FavoriteButton :tool="{ name: route.meta.name } as Tool" />
|
||||||
</div>
|
</div>
|
||||||
</n-space>
|
</div>
|
||||||
|
|
||||||
<div class="separator" />
|
<div class="separator" />
|
||||||
|
|
||||||
<div class="description">
|
<div class="description">
|
||||||
{{ route.meta.description }}
|
{{ toolDescription }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,7 +55,7 @@ useHead(head);
|
|||||||
<div class="tool-content">
|
<div class="tool-content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</base-layout>
|
</BaseLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
@@ -1,23 +1,25 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import { createHead } from '@vueuse/head';
|
import { createHead } from '@vueuse/head';
|
||||||
// eslint-disable-next-line import/no-unresolved
|
|
||||||
import { registerSW } from 'virtual:pwa-register';
|
import { registerSW } from 'virtual:pwa-register';
|
||||||
import { plausible } from './plugins/plausible.plugin';
|
import { plausible } from './plugins/plausible.plugin';
|
||||||
|
|
||||||
import 'virtual:uno.css';
|
import 'virtual:uno.css';
|
||||||
|
|
||||||
registerSW();
|
|
||||||
|
|
||||||
import { naive } from './plugins/naive.plugin';
|
import { naive } from './plugins/naive.plugin';
|
||||||
|
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
import { i18nPlugin } from './plugins/i18n.plugin';
|
||||||
|
|
||||||
|
registerSW();
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(createHead());
|
app.use(createHead());
|
||||||
|
app.use(i18nPlugin);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
app.use(naive);
|
app.use(naive);
|
||||||
app.use(plausible);
|
app.use(plausible);
|
||||||
|
91
src/modules/command-palette/command-palette.store.ts
Normal file
91
src/modules/command-palette/command-palette.store.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import type { PaletteOption } from './command-palette.types';
|
||||||
|
import { useToolStore } from '@/tools/tools.store';
|
||||||
|
import { useFuzzySearch } from '@/composable/fuzzySearch';
|
||||||
|
import { useStyleStore } from '@/stores/style.store';
|
||||||
|
|
||||||
|
import SunIcon from '~icons/mdi/white-balance-sunny';
|
||||||
|
import GithubIcon from '~icons/mdi/github';
|
||||||
|
import BugIcon from '~icons/mdi/bug-outline';
|
||||||
|
import DiceIcon from '~icons/mdi/dice-5';
|
||||||
|
import InfoIcon from '~icons/mdi/information-outline';
|
||||||
|
|
||||||
|
export const useCommandPaletteStore = defineStore('command-palette', () => {
|
||||||
|
const toolStore = useToolStore();
|
||||||
|
const styleStore = useStyleStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const searchPrompt = ref('');
|
||||||
|
|
||||||
|
const toolsOptions = toolStore.tools.map(tool => ({
|
||||||
|
...tool,
|
||||||
|
to: tool.path,
|
||||||
|
toolCategory: tool.category,
|
||||||
|
category: 'Tools',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const searchOptions: PaletteOption[] = [
|
||||||
|
...toolsOptions,
|
||||||
|
{
|
||||||
|
name: 'Random tool',
|
||||||
|
description: 'Get a random tool from the list.',
|
||||||
|
action: () => {
|
||||||
|
const { path } = _.sample(toolStore.tools)!;
|
||||||
|
router.push(path);
|
||||||
|
},
|
||||||
|
icon: DiceIcon,
|
||||||
|
category: 'Tools',
|
||||||
|
keywords: ['random', 'tool', 'pick', 'choose', 'select'],
|
||||||
|
closeOnSelect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Toggle dark mode',
|
||||||
|
description: 'Toggle dark mode on or off.',
|
||||||
|
action: () => styleStore.toggleDark(),
|
||||||
|
icon: SunIcon,
|
||||||
|
category: 'Actions',
|
||||||
|
keywords: ['dark', 'theme', 'toggle', 'mode', 'light', 'system'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Github repository',
|
||||||
|
href: 'https://github.com/CorentinTh/it-tools',
|
||||||
|
category: 'External',
|
||||||
|
description: 'View the source code of it-tools on Github.',
|
||||||
|
keywords: ['github', 'repo', 'repository', 'source', 'code'],
|
||||||
|
icon: GithubIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Report a bug or an issue',
|
||||||
|
description: 'Report a bug or an issue to help improve it-tools.',
|
||||||
|
href: 'https://github.com/CorentinTh/it-tools/issues/new/choose',
|
||||||
|
category: 'Actions',
|
||||||
|
keywords: ['report', 'issue', 'bug', 'problem', 'error'],
|
||||||
|
icon: BugIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'About',
|
||||||
|
description: 'Learn more about IT-Tools.',
|
||||||
|
to: '/about',
|
||||||
|
category: 'Pages',
|
||||||
|
keywords: ['about', 'learn', 'more', 'info', 'information'],
|
||||||
|
icon: InfoIcon,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { searchResult } = useFuzzySearch({
|
||||||
|
search: searchPrompt,
|
||||||
|
data: searchOptions,
|
||||||
|
options: {
|
||||||
|
keys: [{ name: 'name', weight: 2 }, 'description', 'keywords', 'category'],
|
||||||
|
threshold: 0.3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredSearchResult = computed(() =>
|
||||||
|
_.chain(searchResult.value).groupBy('category').mapValues(categoryOptions => _.take(categoryOptions, 5)).value());
|
||||||
|
|
||||||
|
return {
|
||||||
|
filteredSearchResult,
|
||||||
|
searchPrompt,
|
||||||
|
};
|
||||||
|
});
|
14
src/modules/command-palette/command-palette.types.ts
Normal file
14
src/modules/command-palette/command-palette.types.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Component } from 'vue';
|
||||||
|
import type { RouteLocationRaw } from 'vue-router';
|
||||||
|
|
||||||
|
export interface PaletteOption {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
icon?: Component
|
||||||
|
action?: () => void
|
||||||
|
to?: RouteLocationRaw
|
||||||
|
category: string
|
||||||
|
keywords?: string[]
|
||||||
|
href?: string
|
||||||
|
closeOnSelect?: boolean
|
||||||
|
}
|
154
src/modules/command-palette/command-palette.vue
Normal file
154
src/modules/command-palette/command-palette.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useCommandPaletteStore } from './command-palette.store';
|
||||||
|
import type { PaletteOption } from './command-palette.types';
|
||||||
|
|
||||||
|
const isModalOpen = ref(false);
|
||||||
|
const inputRef = ref();
|
||||||
|
const router = useRouter();
|
||||||
|
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
|
||||||
|
|
||||||
|
const commandPaletteStore = useCommandPaletteStore();
|
||||||
|
const { searchPrompt, filteredSearchResult } = storeToRefs(commandPaletteStore);
|
||||||
|
|
||||||
|
const keys = useMagicKeys({
|
||||||
|
passive: false,
|
||||||
|
onEventFired(e) {
|
||||||
|
if (e.ctrlKey && e.key === 'k' && e.type === 'keydown') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.metaKey && e.key === 'k' && e.type === 'keydown') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
whenever(isModalOpen, () => inputRef.value?.focus());
|
||||||
|
|
||||||
|
whenever(keys.ctrl_k, open);
|
||||||
|
whenever(keys.meta_k, open);
|
||||||
|
whenever(keys.escape, close);
|
||||||
|
|
||||||
|
function open() {
|
||||||
|
return isModalOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
isModalOpen.value = false;
|
||||||
|
searchPrompt.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedOptionIndex = ref(0);
|
||||||
|
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
const { key } = event;
|
||||||
|
const isEnterPressed = key === 'Enter';
|
||||||
|
const isArrowUpOrDown = ['ArrowUp', 'ArrowDown'].includes(key);
|
||||||
|
const isArrowDown = key === 'ArrowDown';
|
||||||
|
|
||||||
|
if (isArrowUpOrDown) {
|
||||||
|
const increment = isArrowDown ? 1 : -1;
|
||||||
|
const maxIndex = Math.max(_.chain(filteredSearchResult.value).values().flatten().size().value() - 1, 0);
|
||||||
|
|
||||||
|
selectedOptionIndex.value = Math.min(Math.max(selectedOptionIndex.value + increment, 0), maxIndex);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEnterPressed) {
|
||||||
|
const option = _.chain(filteredSearchResult.value)
|
||||||
|
.values()
|
||||||
|
.flatten()
|
||||||
|
.nth(selectedOptionIndex.value)
|
||||||
|
.value();
|
||||||
|
|
||||||
|
activateOption(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptionIndex(option: PaletteOption) {
|
||||||
|
return _.chain(filteredSearchResult.value)
|
||||||
|
.values()
|
||||||
|
.flatten()
|
||||||
|
.findIndex(o => o === option)
|
||||||
|
.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateOption(option: PaletteOption) {
|
||||||
|
const { closeOnSelect } = option;
|
||||||
|
|
||||||
|
if (option.action) {
|
||||||
|
option.action();
|
||||||
|
|
||||||
|
if (closeOnSelect) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeAfterNavigation = closeOnSelect || _.isUndefined(closeOnSelect);
|
||||||
|
|
||||||
|
if (option.to) {
|
||||||
|
router.push(option.to);
|
||||||
|
|
||||||
|
if (closeAfterNavigation) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.href) {
|
||||||
|
window.open(option.href, '_blank');
|
||||||
|
|
||||||
|
if (closeAfterNavigation) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div flex-1>
|
||||||
|
<c-button w-full important:justify-start @click="isModalOpen = true">
|
||||||
|
<span flex items-center gap-3 op-40>
|
||||||
|
|
||||||
|
<icon-mdi-search />
|
||||||
|
{{ $t('search.label') }}
|
||||||
|
|
||||||
|
<span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline>
|
||||||
|
{{ isMac ? 'Cmd' : 'Ctrl' }} + K
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<c-modal v-model:open="isModalOpen" class="palette-modal" shadow-xl important:max-w-650px important:pa-12px @keydown="handleKeydown">
|
||||||
|
<c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable />
|
||||||
|
|
||||||
|
<div v-for="(options, category) in filteredSearchResult" :key="category">
|
||||||
|
<div ml-3 mt-3 text-sm font-bold text-primary op-60>
|
||||||
|
{{ category }}
|
||||||
|
</div>
|
||||||
|
<command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" />
|
||||||
|
</div>
|
||||||
|
</c-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.c-input-text {
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
::v-deep(.input-wrapper) {
|
||||||
|
padding: 4px;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-modal--overlay {
|
||||||
|
align-items: flex-start !important;
|
||||||
|
padding-top: 80px;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PaletteOption } from '../command-palette.types';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ option: PaletteOption; selected?: boolean }>(), {
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['activated']);
|
||||||
|
const { option } = toRefs(props);
|
||||||
|
|
||||||
|
const { selected } = toRefs(props);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
role="option"
|
||||||
|
:aria-selected="selected"
|
||||||
|
:class="{
|
||||||
|
'text-white': selected,
|
||||||
|
'bg-primary': selected,
|
||||||
|
}"
|
||||||
|
w-full flex cursor-pointer items-center overflow-hidden rounded pa-3 transition hover:bg-primary hover:text-white
|
||||||
|
@click="() => emit('activated', option)"
|
||||||
|
>
|
||||||
|
<component :is="option.icon" v-if="option.icon" mr-3 h-30px w-30px shrink-0 op-50 />
|
||||||
|
|
||||||
|
<div flex-1 overflow-hidden>
|
||||||
|
<div truncate font-bold lh-tight op-90>
|
||||||
|
{{ option.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="option.description" truncate lh-tight op-60>
|
||||||
|
{{ option.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
28
src/modules/i18n/components/locale-selector.vue
Normal file
28
src/modules/i18n/components/locale-selector.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { availableLocales, locale } = useI18n();
|
||||||
|
|
||||||
|
const localesLong: Record<string, string> = {
|
||||||
|
en: 'English',
|
||||||
|
es: 'Español',
|
||||||
|
fr: 'Français',
|
||||||
|
pt: 'Português',
|
||||||
|
ru: 'Русский',
|
||||||
|
zh: '中文',
|
||||||
|
};
|
||||||
|
|
||||||
|
const localeOptions = computed(() =>
|
||||||
|
availableLocales.map(locale => ({
|
||||||
|
label: localesLong[locale] ?? locale,
|
||||||
|
value: locale,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-select
|
||||||
|
v-model:value="locale"
|
||||||
|
:options="localeOptions"
|
||||||
|
placeholder="Select a language"
|
||||||
|
w-100px
|
||||||
|
/>
|
||||||
|
</template>
|
7
src/modules/shared/date.models.ts
Normal file
7
src/modules/shared/date.models.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
export { getUrlFriendlyDateTime };
|
||||||
|
|
||||||
|
function getUrlFriendlyDateTime({ date = new Date() }: { date?: Date } = {}) {
|
||||||
|
return format(date, 'yyyy-MM-dd-HH-mm-ss');
|
||||||
|
}
|
5
src/modules/shared/number.models.ts
Normal file
5
src/modules/shared/number.models.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
function clamp({ value, min = 0, max = 100 }: { value: number; min?: number; max?: number }) {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { clamp };
|
@@ -16,7 +16,7 @@ function useTracker() {
|
|||||||
const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible');
|
const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible');
|
||||||
|
|
||||||
if (_.isNil(plausible)) {
|
if (_.isNil(plausible)) {
|
||||||
throw new Error('Plausible must be instantiated');
|
throw new TypeError('Plausible must be instantiated');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tracker = createTrackerService({ plausible });
|
const tracker = createTrackerService({ plausible });
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Coffee } from '@vicons/tabler';
|
|
||||||
import { useHead } from '@vueuse/head';
|
import { useHead } from '@vueuse/head';
|
||||||
|
|
||||||
useHead({ title: 'Page not found - IT Tools' });
|
useHead({ title: 'Page not found - IT Tools' });
|
||||||
@@ -7,14 +6,22 @@ useHead({ title: 'Page not found - IT Tools' });
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div mt-20 flex flex-col items-center>
|
<div mt-20 flex flex-col items-center>
|
||||||
<n-icon :component="Coffee" size="100" depth="3" />
|
<span text-90px lh-1 op-50>
|
||||||
|
<icon-mdi:kettle-steam-outline />
|
||||||
|
</span>
|
||||||
|
|
||||||
<n-h1 m-0 mt-3>404 Not Found</n-h1>
|
<h1 m-0 mt-3>
|
||||||
<n-text mt-4 block depth="3">Sorry, this page does not seem to exist</n-text>
|
{{ $t('404.notFound') }}
|
||||||
<n-text mb-8 block depth="3">Maybe the cache is doing tricky things, try force-refreshing?</n-text>
|
</h1>
|
||||||
|
<div mt-4 op-60>
|
||||||
|
{{ $t('404.sorry') }}
|
||||||
|
</div>
|
||||||
|
<div mb-8 op-60>
|
||||||
|
{{ $t('404.maybe') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<router-link to="/" #="{ navigate, href }" custom>
|
<c-button to="/">
|
||||||
<n-button tag="a" :href="href" secondary @click="navigate"> Back home </n-button>
|
{{ $t('404.backHome') }}
|
||||||
</router-link>
|
</c-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -1,100 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useTracker } from '@/modules/tracker/tracker.services';
|
|
||||||
import { useHead } from '@vueuse/head';
|
import { useHead } from '@vueuse/head';
|
||||||
|
|
||||||
useHead({ title: 'About - IT Tools' });
|
useHead({ title: 'About - IT Tools' });
|
||||||
const { tracker } = useTracker();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="about-page">
|
<c-markdown :markdown="$t('about.content')" mx-auto mt-50px max-w-600px />
|
||||||
<n-h1>About</n-h1>
|
|
||||||
<n-p>
|
|
||||||
This wonderful website, made with ❤ by
|
|
||||||
<n-button text tag="a" href="https://github.com/CorentinTh" target="_blank" rel="noopener" type="primary">
|
|
||||||
Corentin Thomasset </n-button
|
|
||||||
>, aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to
|
|
||||||
share it to people you think may find it useful too and don't forget to pin it in your shortcut bar !
|
|
||||||
</n-p>
|
|
||||||
<n-p>
|
|
||||||
IT Tools is open-source (under the MIT license) and free, and will always be, but it cost me money to host and
|
|
||||||
renew the domain name, if you want to support my work, and encourage me to add more tools, please consider
|
|
||||||
supporting by
|
|
||||||
<n-button
|
|
||||||
type="primary"
|
|
||||||
tag="a"
|
|
||||||
text
|
|
||||||
href="https://www.buymeacoffee.com/cthmsst"
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
@click="() => tracker.trackEvent({ eventName: 'Support button clicked' })"
|
|
||||||
>
|
|
||||||
sponsoring me </n-button
|
|
||||||
>.
|
|
||||||
</n-p>
|
|
||||||
|
|
||||||
<n-h2>Technologies</n-h2>
|
|
||||||
<n-p>
|
|
||||||
IT Tools is made in Vue JS (vue 3) with the the naive-ui component library and is hosted and continuously deployed
|
|
||||||
by Vercel. Third party open-source libraries are used in some tools, you may find the complete list in the
|
|
||||||
<n-button
|
|
||||||
type="primary"
|
|
||||||
tag="a"
|
|
||||||
text
|
|
||||||
href="https://github.com/CorentinTh/it-tools/blob/main/package.json"
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
package.json
|
|
||||||
</n-button>
|
|
||||||
file of the repository.
|
|
||||||
</n-p>
|
|
||||||
|
|
||||||
<n-h2>Found a bug? A tool is missing?</n-h2>
|
|
||||||
<n-p>
|
|
||||||
If you need a tool that is currently not present here, and you think can be relevant, you are welcome to submit a
|
|
||||||
feature request in the
|
|
||||||
<n-button
|
|
||||||
type="primary"
|
|
||||||
tag="a"
|
|
||||||
text
|
|
||||||
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=enhancement&template=feature_request.md&title=%5BFEAT%5D%20My%20feature"
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
issues section
|
|
||||||
</n-button>
|
|
||||||
in the GitHub repository.
|
|
||||||
</n-p>
|
|
||||||
<n-p>
|
|
||||||
And if you found a bug, or something broken that doesn't work as expected, please fill a bug report in the
|
|
||||||
<n-button
|
|
||||||
type="primary"
|
|
||||||
tag="a"
|
|
||||||
text
|
|
||||||
href="https://github.com/CorentinTh/it-tools/issues/new?assignees=CorentinTh&labels=bug&template=bug_report.md&title=%5BBUG%5D%20My%20bug"
|
|
||||||
rel="noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
issues section
|
|
||||||
</n-button>
|
|
||||||
in the GitHub repository.
|
|
||||||
</n-p>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.about-page {
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 50px auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
.n-h2 {
|
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-p {
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { config } from '@/config';
|
|
||||||
import { useToolStore } from '@/tools/tools.store';
|
|
||||||
import { Heart } from '@vicons/tabler';
|
import { Heart } from '@vicons/tabler';
|
||||||
import { useHead } from '@vueuse/head';
|
import { useHead } from '@vueuse/head';
|
||||||
import ColoredCard from '../components/ColoredCard.vue';
|
import ColoredCard from '../components/ColoredCard.vue';
|
||||||
import ToolCard from '../components/ToolCard.vue';
|
import ToolCard from '../components/ToolCard.vue';
|
||||||
|
import { useToolStore } from '@/tools/tools.store';
|
||||||
|
import { config } from '@/config';
|
||||||
|
|
||||||
const toolStore = useToolStore();
|
const toolStore = useToolStore();
|
||||||
|
|
||||||
useHead({ title: 'IT Tools - Handy online tools for developers' });
|
useHead({ title: 'IT Tools - Handy online tools for developers' });
|
||||||
|
const { t } = useI18n();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -16,53 +17,52 @@ useHead({ title: 'IT Tools - Handy online tools for developers' });
|
|||||||
<div class="grid-wrapper">
|
<div class="grid-wrapper">
|
||||||
<n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
<n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<colored-card title="You like it-tools?" :icon="Heart">
|
<ColoredCard :title="$t('home.follow.title')" :icon="Heart">
|
||||||
Give us a star on
|
{{ $t('home.follow.p1') }}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/CorentinTh/it-tools"
|
href="https://github.com/CorentinTh/it-tools"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
aria-label="IT-Tools' GitHub repository"
|
:aria-label="$t('home.follow.githubRepository')"
|
||||||
>GitHub</a
|
>GitHub</a>
|
||||||
>
|
{{ $t('home.follow.p2') }}
|
||||||
or follow us on
|
|
||||||
<a
|
<a
|
||||||
href="https://twitter.com/ittoolsdottech"
|
href="https://twitter.com/ittoolsdottech"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
aria-label="IT-Tools' Twitter account"
|
:aria-label="$t('home.follow.twitterAccount')"
|
||||||
>Twitter</a
|
>Twitter</a>.
|
||||||
>! Thank you
|
{{ $t('home.follow.thankYou') }}
|
||||||
<n-icon :component="Heart" />
|
<n-icon :component="Heart" />
|
||||||
</colored-card>
|
</ColoredCard>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
|
||||||
<transition name="height">
|
<transition name="height">
|
||||||
<div v-if="toolStore.favoriteTools.length > 0">
|
<div v-if="toolStore.favoriteTools.length > 0">
|
||||||
<n-h3>Your favorite tools</n-h3>
|
<n-h3>{{ $t('home.categories.favoriteTools') }}</n-h3>
|
||||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||||
<n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name">
|
<n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name">
|
||||||
<tool-card :tool="tool" />
|
<ToolCard :tool="tool" />
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<div v-if="toolStore.newTools.length > 0">
|
<div v-if="toolStore.newTools.length > 0">
|
||||||
<n-h3>Newest tools</n-h3>
|
<n-h3>{{ t('home.categories.newestTools') }}</n-h3>
|
||||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||||
<n-gi v-for="tool in toolStore.newTools" :key="tool.name">
|
<n-gi v-for="tool in toolStore.newTools" :key="tool.name">
|
||||||
<tool-card :tool="tool" />
|
<ToolCard :tool="tool" />
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-h3>All the tools</n-h3>
|
<n-h3>{{ $t('home.categories.allTools') }}</n-h3>
|
||||||
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
|
||||||
<n-gi v-for="tool in toolStore.tools" :key="tool.name">
|
<n-gi v-for="tool in toolStore.tools" :key="tool.name">
|
||||||
<transition>
|
<transition>
|
||||||
<tool-card :tool="tool" />
|
<ToolCard :tool="tool" />
|
||||||
</transition>
|
</transition>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
|
37
src/plugins/i18n.plugin.ts
Normal file
37
src/plugins/i18n.plugin.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Plugin } from 'vue';
|
||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
import baseMessages from '@intlify/unplugin-vue-i18n/messages';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { parse as parseYaml } from 'yaml';
|
||||||
|
|
||||||
|
const i18nFiles = import.meta.glob('../tools/*/locales/**.yml', { as: 'raw' });
|
||||||
|
|
||||||
|
const messagesByTools = await Promise.all(_.map(i18nFiles, async (fileDescriptor, path) => {
|
||||||
|
const [, locale] = path.match(/\.\/tools\/.*?\/locales\/(.*)\.ya?ml$/i) ?? [];
|
||||||
|
const content = parseYaml(await fileDescriptor());
|
||||||
|
|
||||||
|
return { [locale]: content };
|
||||||
|
}));
|
||||||
|
|
||||||
|
const messages = _.merge(
|
||||||
|
baseMessages,
|
||||||
|
_.merge({}, ...messagesByTools),
|
||||||
|
);
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
locale: 'en',
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const i18nPlugin: Plugin = {
|
||||||
|
install: (app) => {
|
||||||
|
app.use(i18n);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const translate = function (localeKey: string) {
|
||||||
|
// @ts-expect-error global
|
||||||
|
const hasKey = i18n.global.te(localeKey, i18n.global.locale);
|
||||||
|
return hasKey ? i18n.global.t(localeKey) : localeKey;
|
||||||
|
};
|
@@ -1,8 +1,8 @@
|
|||||||
import { config } from '@/config';
|
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
|
|
||||||
import Plausible from 'plausible-tracker';
|
import Plausible from 'plausible-tracker';
|
||||||
import type { App } from 'vue';
|
import type { App } from 'vue';
|
||||||
|
import { config } from '@/config';
|
||||||
|
|
||||||
function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> {
|
function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> {
|
||||||
return {
|
return {
|
||||||
@@ -15,11 +15,11 @@ function createPlausibleInstance({
|
|||||||
config,
|
config,
|
||||||
}: {
|
}: {
|
||||||
config: {
|
config: {
|
||||||
isTrackerEnabled: boolean;
|
isTrackerEnabled: boolean
|
||||||
domain: string;
|
domain: string
|
||||||
apiHost: string;
|
apiHost: string
|
||||||
trackLocalhost: boolean;
|
trackLocalhost: boolean
|
||||||
};
|
}
|
||||||
}) {
|
}) {
|
||||||
if (config.isTrackerEnabled) {
|
if (config.isTrackerEnabled) {
|
||||||
return Plausible(config);
|
return Plausible(config);
|
||||||
|
@@ -4,6 +4,7 @@ import HomePage from './pages/Home.page.vue';
|
|||||||
import NotFound from './pages/404.page.vue';
|
import NotFound from './pages/404.page.vue';
|
||||||
import { tools } from './tools';
|
import { tools } from './tools';
|
||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
|
import { routes as demoRoutes } from './ui/demo/demo.routes';
|
||||||
|
|
||||||
const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
|
const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
|
||||||
path,
|
path,
|
||||||
@@ -14,7 +15,7 @@ const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({
|
|||||||
const toolsRedirectRoutes = tools
|
const toolsRedirectRoutes = tools
|
||||||
.filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0)
|
.filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0)
|
||||||
.flatMap(
|
.flatMap(
|
||||||
({ path, redirectFrom }) => redirectFrom?.map((redirectSource) => ({ path: redirectSource, redirect: path })) ?? [],
|
({ path, redirectFrom }) => redirectFrom?.map(redirectSource => ({ path: redirectSource, redirect: path })) ?? [],
|
||||||
);
|
);
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -32,6 +33,7 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
...toolsRoutes,
|
...toolsRoutes,
|
||||||
...toolsRedirectRoutes,
|
...toolsRedirectRoutes,
|
||||||
|
...(config.app.env === 'development' ? demoRoutes : []),
|
||||||
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
|
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
33
src/shims.d.ts
vendored
33
src/shims.d.ts
vendored
@@ -1,10 +1,41 @@
|
|||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
import type { ComponentOptions, ComponentOptions } from 'vue';
|
import type { ComponentOptions } from 'vue';
|
||||||
const Component: ComponentOptions;
|
const Component: ComponentOptions;
|
||||||
export default Component;
|
export default Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '*.md' {
|
declare module '*.md' {
|
||||||
|
import type { ComponentOptions } from 'vue';
|
||||||
const Component: ComponentOptions;
|
const Component: ComponentOptions;
|
||||||
export default Component;
|
export default Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'iarna-toml-esm' {
|
||||||
|
export const parse: (toml: string) => any;
|
||||||
|
export const stringify: (obj: any) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'emojilib' {
|
||||||
|
const lib: Record<string, string[]>;
|
||||||
|
export default lib;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'unicode-emoji-json' {
|
||||||
|
const emoji: Record<string, {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
group: string;
|
||||||
|
emoji_version: string;
|
||||||
|
unicode_version: string;
|
||||||
|
skin_tone_support: boolean;
|
||||||
|
skin_tone_support_unicode_version: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default emoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'pdf-signature-reader' {
|
||||||
|
const verifySignature: (pdf: ArrayBuffer) => ({signatures: SignatureInfo[]});
|
||||||
|
|
||||||
|
export default verifySignature;
|
||||||
|
}
|
@@ -1,17 +1,19 @@
|
|||||||
import { useMediaQuery, useStorage } from '@vueuse/core';
|
import { useDark, useMediaQuery, useStorage, useToggle } from '@vueuse/core';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { watch, type Ref } from 'vue';
|
import { type Ref, watch } from 'vue';
|
||||||
|
|
||||||
export const useStyleStore = defineStore('style', {
|
export const useStyleStore = defineStore('style', {
|
||||||
state: () => {
|
state: () => {
|
||||||
const isDarkTheme = useStorage('isDarkTheme', true) as Ref<boolean>;
|
const isDarkTheme = useDark();
|
||||||
|
const toggleDark = useToggle(isDarkTheme);
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 700px)');
|
const isSmallScreen = useMediaQuery('(max-width: 700px)');
|
||||||
const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>;
|
const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>;
|
||||||
|
|
||||||
watch(isSmallScreen, (v) => (isMenuCollapsed.value = v));
|
watch(isSmallScreen, v => (isMenuCollapsed.value = v));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDarkTheme,
|
isDarkTheme,
|
||||||
|
toggleDark,
|
||||||
isMenuCollapsed,
|
isMenuCollapsed,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
};
|
};
|
||||||
|
@@ -1,45 +1,10 @@
|
|||||||
<template>
|
|
||||||
<n-card title="Base64 to file">
|
|
||||||
<n-form-item
|
|
||||||
:feedback="base64InputValidation.message"
|
|
||||||
:validation-status="base64InputValidation.status"
|
|
||||||
:show-label="false"
|
|
||||||
>
|
|
||||||
<n-input v-model:value="base64Input" type="textarea" placeholder="Put your base64 file string here..." rows="5" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button :disabled="base64Input === '' || !base64InputValidation.isValid" secondary @click="downloadFile()">
|
|
||||||
Download file
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-card title="File to base64">
|
|
||||||
<n-upload v-model:file-list="fileList" :show-file-list="true" :on-before-upload="onUpload" list-type="image">
|
|
||||||
<n-upload-dragger>
|
|
||||||
<div mb-2>
|
|
||||||
<n-icon size="35" :depth="3" :component="Upload" />
|
|
||||||
</div>
|
|
||||||
<n-text style="font-size: 14px"> Click or drag a file to this area to upload </n-text>
|
|
||||||
</n-upload-dragger>
|
|
||||||
</n-upload>
|
|
||||||
|
|
||||||
<n-input :value="fileBase64" type="textarea" readonly placeholder="File in base64 will be here" />
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button secondary @click="copyFileBase64()"> Copy </n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useBase64 } from '@vueuse/core';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
import { useCopy } from '@/composable/copy';
|
import { useCopy } from '@/composable/copy';
|
||||||
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
import { useDownloadFileFromBase64 } from '@/composable/downloadBase64';
|
||||||
import { useValidation } from '@/composable/validation';
|
import { useValidation } from '@/composable/validation';
|
||||||
import { isValidBase64 } from '@/utils/base64';
|
import { isValidBase64 } from '@/utils/base64';
|
||||||
import { Upload } from '@vicons/tabler';
|
|
||||||
import { useBase64 } from '@vueuse/core';
|
|
||||||
import type { UploadFileInfo } from 'naive-ui';
|
|
||||||
import { ref, type Ref } from 'vue';
|
|
||||||
|
|
||||||
const base64Input = ref('');
|
const base64Input = ref('');
|
||||||
const { download } = useDownloadFileFromBase64({ source: base64Input });
|
const { download } = useDownloadFileFromBase64({ source: base64Input });
|
||||||
@@ -48,40 +13,66 @@ const base64InputValidation = useValidation({
|
|||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
message: 'Invalid base 64 string',
|
message: 'Invalid base 64 string',
|
||||||
validator: (value) => isValidBase64(value.trim()),
|
validator: value => isValidBase64(value.trim()),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
function downloadFile() {
|
function downloadFile() {
|
||||||
if (!base64InputValidation.isValid) return;
|
if (!base64InputValidation.isValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
download();
|
download();
|
||||||
} catch (_) {
|
}
|
||||||
|
catch (_) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileList = ref();
|
|
||||||
const fileInput = ref() as Ref<File>;
|
const fileInput = ref() as Ref<File>;
|
||||||
const { base64: fileBase64 } = useBase64(fileInput);
|
const { base64: fileBase64 } = useBase64(fileInput);
|
||||||
const { copy: copyFileBase64 } = useCopy({ source: fileBase64, text: 'Base64 string copied to the clipboard' });
|
const { copy: copyFileBase64 } = useCopy({ source: fileBase64, text: 'Base64 string copied to the clipboard' });
|
||||||
|
|
||||||
async function onUpload({ file: { file } }: { file: UploadFileInfo }) {
|
async function onUpload(file: File) {
|
||||||
if (file) {
|
if (file) {
|
||||||
fileList.value = [];
|
|
||||||
fileInput.value = file;
|
fileInput.value = file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<template>
|
||||||
.n-input,
|
<c-card title="Base64 to file">
|
||||||
.n-upload {
|
<c-input-text
|
||||||
margin-bottom: 15px;
|
v-model:value="base64Input"
|
||||||
}
|
multiline
|
||||||
|
placeholder="Put your base64 file string here..."
|
||||||
|
rows="5"
|
||||||
|
:validation="base64InputValidation"
|
||||||
|
mb-2
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<c-button :disabled="base64Input === '' || !base64InputValidation.isValid" @click="downloadFile()">
|
||||||
|
Download file
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card title="File to base64">
|
||||||
|
<c-file-upload title="Drag and drop a file here, or click to select a file" @file-upload="onUpload" />
|
||||||
|
<c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" my-2 />
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<c-button @click="copyFileBase64()">
|
||||||
|
Copy
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
::v-deep(.n-upload-trigger) {
|
::v-deep(.n-upload-trigger) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ import { defineTool } from '../tool';
|
|||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'Base64 file converter',
|
name: 'Base64 file converter',
|
||||||
path: '/base64-file-converter',
|
path: '/base64-file-converter',
|
||||||
description: "Convert string, files or images into a it's base64 representation.",
|
description: 'Convert string, files or images into a it\'s base64 representation.',
|
||||||
keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'],
|
keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'],
|
||||||
component: () => import('./base64-file-converter.vue'),
|
component: () => import('./base64-file-converter.vue'),
|
||||||
icon: FileDigit,
|
icon: FileDigit,
|
||||||
|
@@ -1,55 +1,90 @@
|
|||||||
<template>
|
|
||||||
<n-card title="String to base64">
|
|
||||||
<n-form-item label="String to encode">
|
|
||||||
<n-input v-model:value="textInput" type="textarea" placeholder="Put your string here..." rows="5" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="Base64 of string">
|
|
||||||
<n-input
|
|
||||||
:value="base64Output"
|
|
||||||
type="textarea"
|
|
||||||
readonly
|
|
||||||
placeholder="The base64 encoding of your string will be here"
|
|
||||||
rows="5"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button secondary @click="copyTextBase64()"> Copy base64 </n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-card title="Base64 to string">
|
|
||||||
<n-form-item label="Base64 string to decode" v-bind="b64Validation.attrs">
|
|
||||||
<n-input v-model:value="base64Input" type="textarea" placeholder="Your base64 string..." rows="5" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="Decoded string">
|
|
||||||
<n-input :value="textOutput" type="textarea" readonly placeholder="The decoded string will be here" rows="5" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button secondary @click="copyText()"> Copy decoded string </n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCopy } from '@/composable/copy';
|
import { useCopy } from '@/composable/copy';
|
||||||
import { useValidation } from '@/composable/validation';
|
|
||||||
import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64';
|
import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64';
|
||||||
import { withDefaultOnError } from '@/utils/defaults';
|
import { withDefaultOnError } from '@/utils/defaults';
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
const encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false);
|
||||||
|
const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false);
|
||||||
|
|
||||||
const textInput = ref('');
|
const textInput = ref('');
|
||||||
const base64Output = computed(() => textToBase64(textInput.value));
|
const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.value }));
|
||||||
const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' });
|
const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' });
|
||||||
|
|
||||||
const base64Input = ref('');
|
const base64Input = ref('');
|
||||||
const textOutput = computed(() => withDefaultOnError(() => base64ToText(base64Input.value.trim()), ''));
|
const textOutput = computed(() =>
|
||||||
|
withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''),
|
||||||
|
);
|
||||||
const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' });
|
const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' });
|
||||||
const b64Validation = useValidation({
|
const b64ValidationRules = [
|
||||||
source: base64Input,
|
{
|
||||||
rules: [{ message: 'Invalid base64 string', validator: (value) => isValidBase64(value.trim()) }],
|
message: 'Invalid base64 string',
|
||||||
});
|
validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const b64ValidationWatch = [decodeUrlSafe];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-card title="String to base64">
|
||||||
|
<n-form-item label="Encode URL safe" label-placement="left">
|
||||||
|
<n-switch v-model:value="encodeUrlSafe" />
|
||||||
|
</n-form-item>
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="textInput"
|
||||||
|
multiline
|
||||||
|
placeholder="Put your string here..."
|
||||||
|
rows="5"
|
||||||
|
label="String to encode"
|
||||||
|
raw-text
|
||||||
|
mb-5
|
||||||
|
/>
|
||||||
|
|
||||||
|
<c-input-text
|
||||||
|
label="Base64 of string"
|
||||||
|
:value="base64Output"
|
||||||
|
multiline
|
||||||
|
readonly
|
||||||
|
placeholder="The base64 encoding of your string will be here"
|
||||||
|
rows="5"
|
||||||
|
mb-5
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<c-button @click="copyTextBase64()">
|
||||||
|
Copy base64
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card title="Base64 to string">
|
||||||
|
<n-form-item label="Decode URL safe" label-placement="left">
|
||||||
|
<n-switch v-model:value="decodeUrlSafe" />
|
||||||
|
</n-form-item>
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="base64Input"
|
||||||
|
multiline
|
||||||
|
placeholder="Your base64 string..."
|
||||||
|
rows="5"
|
||||||
|
:validation-rules="b64ValidationRules"
|
||||||
|
:validation-watch="b64ValidationWatch"
|
||||||
|
label="Base64 string to decode"
|
||||||
|
mb-5
|
||||||
|
/>
|
||||||
|
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="textOutput"
|
||||||
|
label="Decoded string"
|
||||||
|
placeholder="The decoded string will be here"
|
||||||
|
multiline
|
||||||
|
rows="5"
|
||||||
|
readonly
|
||||||
|
mb-5
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<c-button @click="copyText()">
|
||||||
|
Copy decoded string
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
@@ -1,37 +1,6 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-form-item label="Username">
|
|
||||||
<n-input v-model:value="username" placeholder="Your username..." clearable />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Password">
|
|
||||||
<n-input
|
|
||||||
v-model:value="password"
|
|
||||||
placeholder="Your password..."
|
|
||||||
type="password"
|
|
||||||
show-password-on="click"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
<n-card>
|
|
||||||
<n-statistic label="Authorization header:" class="header">
|
|
||||||
<n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none">
|
|
||||||
{{ header }}
|
|
||||||
</n-scrollbar>
|
|
||||||
</n-statistic>
|
|
||||||
</n-card>
|
|
||||||
<br />
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button secondary @click="copy">Copy header</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCopy } from '@/composable/copy';
|
import { useCopy } from '@/composable/copy';
|
||||||
import { textToBase64 } from '@/utils/base64';
|
import { textToBase64 } from '@/utils/base64';
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
|
||||||
const username = ref('');
|
const username = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
@@ -40,6 +9,34 @@ const header = computed(() => `Authorization: Basic ${textToBase64(`${username.v
|
|||||||
const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' });
|
const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 />
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="Your password..."
|
||||||
|
clearable
|
||||||
|
raw-text
|
||||||
|
mb-2
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<c-card>
|
||||||
|
<n-statistic label="Authorization header:" class="header">
|
||||||
|
<n-scrollbar x-scrollable style="max-width: 550px; margin-bottom: -10px; padding-bottom: 10px" trigger="none">
|
||||||
|
{{ header }}
|
||||||
|
</n-scrollbar>
|
||||||
|
</n-statistic>
|
||||||
|
</c-card>
|
||||||
|
<div mt-5 flex justify-center>
|
||||||
|
<c-button @click="copy()">
|
||||||
|
Copy header
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
::v-deep(.n-statistic-value__content) {
|
::v-deep(.n-statistic-value__content) {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
@@ -1,63 +1,7 @@
|
|||||||
<template>
|
|
||||||
<n-card title="Hash">
|
|
||||||
<n-form label-width="120">
|
|
||||||
<n-form-item label="Your string: " label-placement="left">
|
|
||||||
<n-input
|
|
||||||
v-model:value="input"
|
|
||||||
placeholder="Your string to bcrypt..."
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Salt count: " label-placement="left">
|
|
||||||
<n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
|
|
||||||
</n-form-item>
|
|
||||||
<n-input :value="hashed" readonly style="text-align: center" />
|
|
||||||
</n-form>
|
|
||||||
<br />
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button secondary @click="copy"> Copy hash </n-button>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-card title="Compare string with hash">
|
|
||||||
<n-form label-width="120">
|
|
||||||
<n-form-item label="Your string: " label-placement="left">
|
|
||||||
<n-input
|
|
||||||
v-model:value="compareString"
|
|
||||||
placeholder="Your string to compare..."
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Your hash: " label-placement="left">
|
|
||||||
<n-input
|
|
||||||
v-model:value="compareHash"
|
|
||||||
placeholder="Your hahs to compare..."
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
|
|
||||||
<div class="compare-result" :class="{ positive: compareMatch }">
|
|
||||||
{{ compareMatch ? 'Yes' : 'No' }}
|
|
||||||
</div>
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { compareSync, hashSync } from 'bcryptjs';
|
||||||
import { hashSync, compareSync } from 'bcryptjs';
|
|
||||||
import { useCopy } from '@/composable/copy';
|
|
||||||
import { useThemeVars } from 'naive-ui';
|
import { useThemeVars } from 'naive-ui';
|
||||||
|
import { useCopy } from '@/composable/copy';
|
||||||
|
|
||||||
const themeVars = useThemeVars();
|
const themeVars = useThemeVars();
|
||||||
|
|
||||||
@@ -71,6 +15,48 @@ const compareHash = ref('');
|
|||||||
const compareMatch = computed(() => compareSync(compareString.value, compareHash.value));
|
const compareMatch = computed(() => compareSync(compareString.value, compareHash.value));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-card title="Hash">
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="input"
|
||||||
|
placeholder="Your string to bcrypt..."
|
||||||
|
raw-text
|
||||||
|
label="Your string: "
|
||||||
|
label-position="left"
|
||||||
|
label-align="right"
|
||||||
|
label-width="120px"
|
||||||
|
mb-2
|
||||||
|
/>
|
||||||
|
<n-form-item label="Salt count: " label-placement="left" label-width="120">
|
||||||
|
<n-input-number v-model:value="saltCount" placeholder="Salt rounds..." :max="10" :min="0" w-full />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<c-input-text :value="hashed" readonly text-center />
|
||||||
|
|
||||||
|
<div mt-5 flex justify-center>
|
||||||
|
<c-button @click="copy()">
|
||||||
|
Copy hash
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card title="Compare string with hash">
|
||||||
|
<n-form label-width="120">
|
||||||
|
<n-form-item label="Your string: " label-placement="left">
|
||||||
|
<c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Your hash: " label-placement="left">
|
||||||
|
<c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Do they match ? " label-placement="left" :show-feedback="false">
|
||||||
|
<div class="compare-result" :class="{ positive: compareMatch }">
|
||||||
|
{{ compareMatch ? 'Yes' : 'No' }}
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.compare-result {
|
.compare-result {
|
||||||
color: v-bind('themeVars.errorColor');
|
color: v-bind('themeVars.errorColor');
|
||||||
|
@@ -13,22 +13,22 @@ function computeAverage({ data }: { data: number[] }) {
|
|||||||
function computeVariance({ data }: { data: number[] }) {
|
function computeVariance({ data }: { data: number[] }) {
|
||||||
const mean = computeAverage({ data });
|
const mean = computeAverage({ data });
|
||||||
|
|
||||||
const squaredDiffs = data.map((value) => Math.pow(value - mean, 2));
|
const squaredDiffs = data.map(value => (value - mean) ** 2);
|
||||||
|
|
||||||
return computeAverage({ data: squaredDiffs });
|
return computeAverage({ data: squaredDiffs });
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; headerMap?: Record<string, string> }) {
|
function arrayToMarkdownTable({ data, headerMap = {} }: { data: Record<string, unknown>[]; headerMap?: Record<string, string> }) {
|
||||||
if (!Array.isArray(data) || data.length === 0) {
|
if (!Array.isArray(data) || data.length === 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = Object.keys(data[0]);
|
const headers = Object.keys(data[0]);
|
||||||
const rows = data.map((obj) => Object.values(obj));
|
const rows = data.map(obj => Object.values(obj));
|
||||||
|
|
||||||
const headerRow = `| ${headers.map((header) => headerMap[header] ?? header).join(' | ')} |`;
|
const headerRow = `| ${headers.map(header => headerMap[header] ?? header).join(' | ')} |`;
|
||||||
const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`;
|
const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`;
|
||||||
const dataRows = rows.map((row) => `| ${row.join(' | ')} |`).join('\n');
|
const dataRows = rows.map(row => `| ${row.join(' | ')} |`).join('\n');
|
||||||
|
|
||||||
return `${headerRow}\n${separatorRow}\n${dataRows}`;
|
return `${headerRow}\n${separatorRow}\n${dataRows}`;
|
||||||
}
|
}
|
||||||
|
@@ -1,92 +1,11 @@
|
|||||||
<template>
|
|
||||||
<n-scrollbar style="flex: 1" x-scrollable>
|
|
||||||
<n-space :wrap="false" style="flex: 1" justify="center" :size="0">
|
|
||||||
<div v-for="(suite, index) of suites" :key="index">
|
|
||||||
<n-card style="width: 292px; margin: 0 8px 5px">
|
|
||||||
<n-form-item label="Suite name:" :show-feedback="false" label-placement="left">
|
|
||||||
<n-input v-model:value="suite.title" placeholder="Suite name..." />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-divider></n-divider>
|
|
||||||
<n-form-item label="Suite values" :show-feedback="false">
|
|
||||||
<dynamic-values v-model:values="suite.data" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button v-if="suites.length > 1" quaternary @click="suites.splice(index, 1)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Trash" depth="3" />
|
|
||||||
</template>
|
|
||||||
Delete suite
|
|
||||||
</n-button>
|
|
||||||
<n-button quaternary @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Plus" depth="3" />
|
|
||||||
</template>
|
|
||||||
Add suite
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</n-space>
|
|
||||||
<br />
|
|
||||||
</n-scrollbar>
|
|
||||||
|
|
||||||
<div style="flex: 0 0 100%">
|
|
||||||
<div style="max-width: 600px; margin: 0 auto">
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-form-item label="Unit:" label-placement="left">
|
|
||||||
<n-input v-model:value="unit" placeholder="Unit (eg: ms)" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-button
|
|
||||||
tertiary
|
|
||||||
@click="
|
|
||||||
suites = [
|
|
||||||
{ title: 'Suite 1', data: [] },
|
|
||||||
{ title: 'Suite 2', data: [] },
|
|
||||||
]
|
|
||||||
"
|
|
||||||
>Reset suites</n-button
|
|
||||||
>
|
|
||||||
</n-space>
|
|
||||||
|
|
||||||
<n-table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ header.position }}</th>
|
|
||||||
<th>{{ header.title }}</th>
|
|
||||||
<th>{{ header.size }}</th>
|
|
||||||
<th>{{ header.mean }}</th>
|
|
||||||
<th>{{ header.variance }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="{ title, size, mean, variance, position } of results" :key="title">
|
|
||||||
<td>{{ position }}</td>
|
|
||||||
<td>{{ title }}</td>
|
|
||||||
<td>{{ size }}</td>
|
|
||||||
<td>{{ mean }}</td>
|
|
||||||
<td>{{ variance }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</n-table>
|
|
||||||
<br />
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button tertiary @click="copyAsMarkdown">Copy as markdown table</n-button>
|
|
||||||
<n-button tertiary @click="copyAsBulletList">Copy as bullet list</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Trash, Plus } from '@vicons/tabler';
|
import { Plus, Trash } from '@vicons/tabler';
|
||||||
import { useClipboard, useStorage } from '@vueuse/core';
|
import { useStorage } from '@vueuse/core';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { computed } from 'vue';
|
|
||||||
import { computeAverage, computeVariance, arrayToMarkdownTable } from './benchmark-builder.models';
|
import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models';
|
||||||
import DynamicValues from './dynamic-values.vue';
|
import DynamicValues from './dynamic-values.vue';
|
||||||
|
import { useCopy } from '@/composable/copy';
|
||||||
|
|
||||||
const suites = useStorage('benchmark-builder:suites', [
|
const suites = useStorage('benchmark-builder:suites', [
|
||||||
{ title: 'Suite 1', data: [5, 10] },
|
{ title: 'Suite 1', data: [5, 10] },
|
||||||
@@ -116,8 +35,8 @@ const results = computed(() => {
|
|||||||
const deltaWithBestMean = mean - bestMean;
|
const deltaWithBestMean = mean - bestMean;
|
||||||
const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean);
|
const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean);
|
||||||
|
|
||||||
const comparisonValues: string =
|
const comparisonValues: string
|
||||||
index !== 0 && bestMean !== mean ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
|
= (index !== 0 && bestMean !== mean) ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
position: index + 1,
|
position: index + 1,
|
||||||
@@ -129,14 +48,14 @@ const results = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const { copy } = useClipboard();
|
const { copy } = useCopy({ createToast: false });
|
||||||
|
|
||||||
const header = {
|
const header = {
|
||||||
|
position: 'Position',
|
||||||
title: 'Suite',
|
title: 'Suite',
|
||||||
size: 'Samples',
|
size: 'Samples',
|
||||||
mean: 'Mean',
|
mean: 'Mean',
|
||||||
variance: 'Variance',
|
variance: 'Variance',
|
||||||
position: 'Position',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function copyAsMarkdown() {
|
function copyAsMarkdown() {
|
||||||
@@ -159,4 +78,69 @@ function copyAsBulletList() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<template>
|
||||||
|
<n-scrollbar style="flex: 1" x-scrollable>
|
||||||
|
<div mb-5 flex flex-1 flex-nowrap justify-center gap-12px>
|
||||||
|
<div v-for="(suite, index) of suites" :key="index">
|
||||||
|
<c-card style="width: 294px">
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="suite.title"
|
||||||
|
label-position="left"
|
||||||
|
label="Suite name"
|
||||||
|
placeholder="Suite name..."
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
<n-form-item label="Suite values" :show-feedback="false">
|
||||||
|
<DynamicValues v-model:values="suite.data" />
|
||||||
|
</n-form-item>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)">
|
||||||
|
<n-icon :component="Trash" depth="3" mr-2 size="18" />
|
||||||
|
Delete suite
|
||||||
|
</c-button>
|
||||||
|
<c-button
|
||||||
|
variant="text"
|
||||||
|
@click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })"
|
||||||
|
>
|
||||||
|
<n-icon :component="Plus" depth="3" mr-2 size="18" />
|
||||||
|
Add suite
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-scrollbar>
|
||||||
|
|
||||||
|
<div style="flex: 0 0 100%">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto">
|
||||||
|
<div mx-auto max-w-sm flex justify-center gap-3>
|
||||||
|
<c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 />
|
||||||
|
|
||||||
|
<c-button
|
||||||
|
@click="
|
||||||
|
suites = [
|
||||||
|
{ title: 'Suite 1', data: [] },
|
||||||
|
{ title: 'Suite 2', data: [] },
|
||||||
|
]
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Reset suites
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<c-table :data="results" :headers="header" />
|
||||||
|
|
||||||
|
<div mt-5 flex justify-center gap-3>
|
||||||
|
<c-button @click="copyAsMarkdown()">
|
||||||
|
Copy as markdown table
|
||||||
|
</c-button>
|
||||||
|
<c-button @click="copyAsBulletList()">
|
||||||
|
Copy as bullet list
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
@@ -1,45 +1,15 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-space v-for="(value, index) of values" :key="index" :wrap="false" style="margin-bottom: 5px" :size="5">
|
|
||||||
<n-input-number
|
|
||||||
:ref="refs.set"
|
|
||||||
v-model:value="values[index]"
|
|
||||||
:show-button="false"
|
|
||||||
placeholder="Set your measure..."
|
|
||||||
autofocus
|
|
||||||
@keydown.enter="onInputEnter(index)"
|
|
||||||
/>
|
|
||||||
<n-tooltip>
|
|
||||||
<template #trigger>
|
|
||||||
<n-button circle quaternary @click="values.splice(index, 1)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Trash" depth="3" />
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
Delete value
|
|
||||||
</n-tooltip>
|
|
||||||
</n-space>
|
|
||||||
|
|
||||||
<n-button tertiary @click="addValue">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="Plus" />
|
|
||||||
</template>
|
|
||||||
Add a measure
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Trash, Plus } from '@vicons/tabler';
|
import { Plus, Trash } from '@vicons/tabler';
|
||||||
import { useTemplateRefsList, useVModel } from '@vueuse/core';
|
import { useTemplateRefsList, useVModel } from '@vueuse/core';
|
||||||
import { NInputNumber } from 'naive-ui';
|
import { NInputNumber } from 'naive-ui';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ values: (number | null)[] }>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:values']);
|
||||||
|
|
||||||
const refs = useTemplateRefsList<typeof NInputNumber>();
|
const refs = useTemplateRefsList<typeof NInputNumber>();
|
||||||
|
|
||||||
const props = defineProps<{ values: (number | null)[] }>();
|
|
||||||
const emit = defineEmits(['update:values']);
|
|
||||||
const values = useVModel(props, 'values', emit);
|
const values = useVModel(props, 'values', emit);
|
||||||
|
|
||||||
async function addValue() {
|
async function addValue() {
|
||||||
@@ -58,4 +28,27 @@ function onInputEnter(index: number) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2>
|
||||||
|
<NInputNumber
|
||||||
|
:ref="refs.set"
|
||||||
|
v-model:value="values[index]"
|
||||||
|
:show-button="false"
|
||||||
|
placeholder="Set your measure..."
|
||||||
|
autofocus
|
||||||
|
@keydown.enter="onInputEnter(index)"
|
||||||
|
/>
|
||||||
|
<c-tooltip tooltip="Delete this value">
|
||||||
|
<c-button circle variant="text" @click="values.splice(index, 1)">
|
||||||
|
<n-icon :component="Trash" depth="3" size="18" />
|
||||||
|
</c-button>
|
||||||
|
</c-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<c-button @click="addValue">
|
||||||
|
<n-icon :component="Plus" depth="3" mr-2 size="18" />
|
||||||
|
Add a measure
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
@@ -1,67 +1,4 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-card>
|
|
||||||
<n-grid cols="3" x-gap="12">
|
|
||||||
<n-gi span="1">
|
|
||||||
<n-form-item label="Language:">
|
|
||||||
<n-select
|
|
||||||
v-model:value="language"
|
|
||||||
:options="Object.keys(languages).map((label) => ({ label, value: label }))"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi span="2">
|
|
||||||
<n-form-item
|
|
||||||
label="Entropy (seed):"
|
|
||||||
:feedback="entropyValidation.message"
|
|
||||||
:validation-status="entropyValidation.status"
|
|
||||||
>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input v-model:value="entropy" placeholder="Your string..." />
|
|
||||||
<n-button @click="refreshEntropy">
|
|
||||||
<n-icon size="22">
|
|
||||||
<Refresh />
|
|
||||||
</n-icon>
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="copyEntropy">
|
|
||||||
<n-icon size="22">
|
|
||||||
<Copy />
|
|
||||||
</n-icon>
|
|
||||||
</n-button>
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item>
|
|
||||||
</n-gi>
|
|
||||||
</n-grid>
|
|
||||||
<n-form-item
|
|
||||||
label="Passphrase (mnemonic):"
|
|
||||||
:feedback="mnemonicValidation.message"
|
|
||||||
:validation-status="mnemonicValidation.status"
|
|
||||||
>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input
|
|
||||||
v-model:value="passphrase"
|
|
||||||
style="text-align: center; flex: 1"
|
|
||||||
placeholder="Your mnemonic..."
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
autocapitalize="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<n-button @click="copyPassphrase">
|
|
||||||
<n-icon size="22" :component="Copy" />
|
|
||||||
</n-button>
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCopy } from '@/composable/copy';
|
|
||||||
import { useValidation } from '@/composable/validation';
|
|
||||||
import { isNotThrowing } from '@/utils/boolean';
|
|
||||||
import { withDefaultOnError } from '@/utils/defaults';
|
|
||||||
import {
|
import {
|
||||||
chineseSimplifiedWordList,
|
chineseSimplifiedWordList,
|
||||||
chineseTraditionalWordList,
|
chineseTraditionalWordList,
|
||||||
@@ -78,19 +15,23 @@ import {
|
|||||||
spanishWordList,
|
spanishWordList,
|
||||||
} from '@it-tools/bip39';
|
} from '@it-tools/bip39';
|
||||||
import { Copy, Refresh } from '@vicons/tabler';
|
import { Copy, Refresh } from '@vicons/tabler';
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
import { useCopy } from '@/composable/copy';
|
||||||
|
import { useValidation } from '@/composable/validation';
|
||||||
|
import { isNotThrowing } from '@/utils/boolean';
|
||||||
|
import { withDefaultOnError } from '@/utils/defaults';
|
||||||
|
|
||||||
const languages = {
|
const languages = {
|
||||||
English: englishWordList,
|
'English': englishWordList,
|
||||||
'Chinese simplified': chineseSimplifiedWordList,
|
'Chinese simplified': chineseSimplifiedWordList,
|
||||||
'Chinese traditional': chineseTraditionalWordList,
|
'Chinese traditional': chineseTraditionalWordList,
|
||||||
Czech: czechWordList,
|
'Czech': czechWordList,
|
||||||
French: frenchWordList,
|
'French': frenchWordList,
|
||||||
Italian: italianWordList,
|
'Italian': italianWordList,
|
||||||
Japanese: japaneseWordList,
|
'Japanese': japaneseWordList,
|
||||||
Korean: koreanWordList,
|
'Korean': koreanWordList,
|
||||||
Portuguese: portugueseWordList,
|
'Portuguese': portugueseWordList,
|
||||||
Spanish: spanishWordList,
|
'Spanish': spanishWordList,
|
||||||
};
|
};
|
||||||
|
|
||||||
const entropy = ref(generateEntropy());
|
const entropy = ref(generateEntropy());
|
||||||
@@ -111,11 +52,11 @@ const entropyValidation = useValidation({
|
|||||||
source: entropy,
|
source: entropy,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
validator: (value) => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
|
validator: value => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
|
||||||
message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4',
|
message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
validator: (value) => /^[a-fA-F0-9]*$/.test(value),
|
validator: value => /^[a-fA-F0-9]*$/.test(value),
|
||||||
message: 'Entropy should be an hexadecimal string',
|
message: 'Entropy should be an hexadecimal string',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -125,7 +66,7 @@ const mnemonicValidation = useValidation({
|
|||||||
source: passphrase,
|
source: passphrase,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
validator: (value) => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
|
validator: value => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
|
||||||
message: 'Invalid mnemonic',
|
message: 'Invalid mnemonic',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -138,3 +79,53 @@ function refreshEntropy() {
|
|||||||
const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' });
|
const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' });
|
||||||
const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' });
|
const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-grid cols="3" x-gap="12">
|
||||||
|
<n-gi span="1">
|
||||||
|
<c-select
|
||||||
|
v-model:value="language"
|
||||||
|
searchable
|
||||||
|
label="Language:"
|
||||||
|
:options="Object.keys(languages)"
|
||||||
|
/>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi span="2">
|
||||||
|
<n-form-item
|
||||||
|
label="Entropy (seed):"
|
||||||
|
:feedback="entropyValidation.message"
|
||||||
|
:validation-status="entropyValidation.status"
|
||||||
|
>
|
||||||
|
<n-input-group>
|
||||||
|
<c-input-text v-model:value="entropy" placeholder="Your string..." />
|
||||||
|
|
||||||
|
<c-button @click="refreshEntropy()">
|
||||||
|
<n-icon size="22">
|
||||||
|
<Refresh />
|
||||||
|
</n-icon>
|
||||||
|
</c-button>
|
||||||
|
<c-button @click="copyEntropy()">
|
||||||
|
<n-icon size="22">
|
||||||
|
<Copy />
|
||||||
|
</n-icon>
|
||||||
|
</c-button>
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
<n-form-item
|
||||||
|
label="Passphrase (mnemonic):"
|
||||||
|
:feedback="mnemonicValidation.message"
|
||||||
|
:validation-status="mnemonicValidation.status"
|
||||||
|
>
|
||||||
|
<n-input-group>
|
||||||
|
<c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text />
|
||||||
|
|
||||||
|
<c-button @click="copyPassphrase()">
|
||||||
|
<n-icon size="22" :component="Copy" />
|
||||||
|
</c-button>
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
217
src/tools/camera-recorder/camera-recorder.vue
Normal file
217
src/tools/camera-recorder/camera-recorder.vue
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { useMediaRecorder } from './useMediaRecorder';
|
||||||
|
|
||||||
|
interface Media { type: 'image' | 'video'; value: string; createdAt: Date }
|
||||||
|
|
||||||
|
const {
|
||||||
|
videoInputs: cameras,
|
||||||
|
audioInputs: microphones,
|
||||||
|
permissionGranted,
|
||||||
|
isSupported,
|
||||||
|
ensurePermissions,
|
||||||
|
} = useDevicesList({
|
||||||
|
requestPermissions: true,
|
||||||
|
constraints: { video: true, audio: true },
|
||||||
|
onUpdated() {
|
||||||
|
refreshCurrentDevices();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const video = ref<HTMLVideoElement>();
|
||||||
|
const medias = ref<Media[]>([]);
|
||||||
|
const currentCamera = ref(cameras.value[0]?.deviceId);
|
||||||
|
const currentMicrophone = ref(microphones.value[0]?.deviceId);
|
||||||
|
const permissionCannotBePrompted = ref(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
stream,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
enabled: isMediaStreamAvailable,
|
||||||
|
} = useUserMedia({
|
||||||
|
constraints: computed(() => ({
|
||||||
|
video: { deviceId: currentCamera.value },
|
||||||
|
...(currentMicrophone.value ? { audio: { deviceId: currentMicrophone.value } } : {}),
|
||||||
|
})),
|
||||||
|
autoSwitch: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
isRecordingSupported,
|
||||||
|
onRecordAvailable,
|
||||||
|
startRecording,
|
||||||
|
stopRecording,
|
||||||
|
pauseRecording,
|
||||||
|
recordingState,
|
||||||
|
resumeRecording,
|
||||||
|
} = useMediaRecorder({
|
||||||
|
stream,
|
||||||
|
});
|
||||||
|
|
||||||
|
onRecordAvailable((value) => {
|
||||||
|
medias.value.unshift({ type: 'video', value, createdAt: new Date() });
|
||||||
|
});
|
||||||
|
|
||||||
|
function refreshCurrentDevices() {
|
||||||
|
if (_.isNil(currentCamera) || !cameras.value.find(i => i.deviceId === currentCamera.value)) {
|
||||||
|
currentCamera.value = cameras.value[0]?.deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.isNil(microphones) || !microphones.value.find(i => i.deviceId === currentMicrophone.value)) {
|
||||||
|
currentMicrophone.value = microphones.value[0]?.deviceId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeScreenshot() {
|
||||||
|
if (!video.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = video.value.videoWidth;
|
||||||
|
canvas.height = video.value.videoHeight;
|
||||||
|
canvas.getContext('2d')?.drawImage(video.value, 0, 0);
|
||||||
|
const image = canvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
medias.value.unshift({ type: 'image', value: image, createdAt: new Date() });
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (video.value && stream.value) {
|
||||||
|
video.value.srcObject = stream.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => stop());
|
||||||
|
|
||||||
|
async function requestPermissions() {
|
||||||
|
try {
|
||||||
|
await ensurePermissions();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
permissionCannotBePrompted.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadMedia({ type, value, createdAt }: Media) {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = value;
|
||||||
|
link.download = `${type}-${createdAt.getTime()}.${type === 'image' ? 'png' : 'webm'}`;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<c-card v-if="!isSupported">
|
||||||
|
Your browser does not support recording video from camera
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card v-else-if="!permissionGranted" text-center>
|
||||||
|
You need to grant permission to use your camera and microphone
|
||||||
|
|
||||||
|
<c-alert v-if="permissionCannotBePrompted" mt-4 text-left>
|
||||||
|
Your browser has blocked permission request or does not support it. You need to grant permission manually in
|
||||||
|
your browser settings (usually the lock icon in the address bar).
|
||||||
|
</c-alert>
|
||||||
|
|
||||||
|
<div v-else mt-4 flex justify-center>
|
||||||
|
<c-button @click="requestPermissions">
|
||||||
|
Grant permission
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<c-card v-else>
|
||||||
|
<div flex flex-col gap-2>
|
||||||
|
<c-select
|
||||||
|
v-model:value="currentCamera"
|
||||||
|
label-position="left"
|
||||||
|
label-width="60px"
|
||||||
|
label="Video:"
|
||||||
|
:options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))"
|
||||||
|
placeholder="Select camera"
|
||||||
|
/>
|
||||||
|
<c-select
|
||||||
|
v-if="currentMicrophone && microphones.length > 0"
|
||||||
|
v-model:value="currentMicrophone"
|
||||||
|
label="Audio:"
|
||||||
|
label-position="left"
|
||||||
|
label-width="60px"
|
||||||
|
:options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))"
|
||||||
|
placeholder="Select microphone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isMediaStreamAvailable" mt-3 flex justify-center>
|
||||||
|
<c-button type="primary" @click="start">
|
||||||
|
Start webcam
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div my-2>
|
||||||
|
<video ref="video" autoplay controls playsinline max-h-full w-full />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div flex items-center justify-between gap-2>
|
||||||
|
<c-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot">
|
||||||
|
<span mr-2> <icon-mdi-camera /></span>
|
||||||
|
Take screenshot
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<div v-if="isRecordingSupported" flex justify-center gap-2>
|
||||||
|
<c-button v-if="recordingState === 'stopped'" @click="startRecording">
|
||||||
|
<span mr-2> <icon-mdi-video /></span>
|
||||||
|
Start recording
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<c-button v-if="recordingState === 'recording'" @click="pauseRecording">
|
||||||
|
<span mr-2> <icon-mdi-pause /></span>
|
||||||
|
Pause
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<c-button v-if="recordingState === 'paused'" @click="resumeRecording">
|
||||||
|
<span mr-2> <icon-mdi-play /></span>
|
||||||
|
Resume
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<c-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording">
|
||||||
|
<span mr-2> <icon-mdi-record /></span>
|
||||||
|
Stop
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
<div v-else italic op-60>
|
||||||
|
Video recording is not supported in your browser
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
|
||||||
|
<div grid grid-cols-2 mt-5 gap-2>
|
||||||
|
<c-card v-for="({ type, value, createdAt }, index) in medias" :key="index">
|
||||||
|
<img v-if="type === 'image'" :src="value" max-h-full w-full alt="screenshot">
|
||||||
|
|
||||||
|
<video v-else :src="value" controls max-h-full w-full />
|
||||||
|
|
||||||
|
<div flex items-center justify-between>
|
||||||
|
<div font-bold>
|
||||||
|
{{ type === 'image' ? 'Screenshot' : 'Video' }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div flex gap-2>
|
||||||
|
<c-button @click="downloadMedia({ type, value, createdAt })">
|
||||||
|
<icon-mdi-download />
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<c-button @click="medias = medias.filter((_ignored, i) => i !== index)">
|
||||||
|
<icon-mdi-delete-outline />
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
12
src/tools/camera-recorder/index.ts
Normal file
12
src/tools/camera-recorder/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Camera } from '@vicons/tabler';
|
||||||
|
import { defineTool } from '../tool';
|
||||||
|
|
||||||
|
export const tool = defineTool({
|
||||||
|
name: 'Camera recorder',
|
||||||
|
path: '/camera-recorder',
|
||||||
|
description: 'Take a picture or record a video from your webcam or camera.',
|
||||||
|
keywords: ['camera', 'recoder'],
|
||||||
|
component: () => import('./camera-recorder.vue'),
|
||||||
|
icon: Camera,
|
||||||
|
createdAt: new Date('2023-05-15'),
|
||||||
|
});
|
114
src/tools/camera-recorder/useMediaRecorder.ts
Normal file
114
src/tools/camera-recorder/useMediaRecorder.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { type Ref, computed, ref } from 'vue';
|
||||||
|
|
||||||
|
export { useMediaRecorder };
|
||||||
|
|
||||||
|
function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): {
|
||||||
|
isRecordingSupported: Ref<boolean>
|
||||||
|
recordingState: Ref<'stopped' | 'recording' | 'paused'>
|
||||||
|
startRecording: () => void
|
||||||
|
stopRecording: () => void
|
||||||
|
pauseRecording: () => void
|
||||||
|
resumeRecording: () => void
|
||||||
|
onRecordAvailable: (cb: (url: string) => void) => void
|
||||||
|
} {
|
||||||
|
const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm'));
|
||||||
|
const mediaRecorder = ref<MediaRecorder | null>(null);
|
||||||
|
const recordedChunks = ref<Blob[]>([]);
|
||||||
|
const recordAvailable = createEventHook();
|
||||||
|
const recordingState = ref<'stopped' | 'recording' | 'paused'>('stopped');
|
||||||
|
|
||||||
|
const createVideo = () => {
|
||||||
|
const blob = new Blob(recordedChunks.value, { type: 'video/webm' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
recordedChunks.value = [];
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startRecording = () => {
|
||||||
|
if (!isRecordingSupported.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!stream.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recordingState.value !== 'stopped') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' });
|
||||||
|
|
||||||
|
mediaRecorder.value.ondataavailable = (e) => {
|
||||||
|
if (e.data.size > 0) {
|
||||||
|
recordedChunks.value.push(e.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.value.onstop = () => {
|
||||||
|
recordAvailable.trigger(createVideo());
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mediaRecorder.value.state !== 'inactive') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorder.value.start();
|
||||||
|
recordingState.value = 'recording';
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRecording = () => {
|
||||||
|
if (!isRecordingSupported.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mediaRecorder.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recordingState.value === 'stopped') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorder.value.stop();
|
||||||
|
recordingState.value = 'stopped';
|
||||||
|
};
|
||||||
|
|
||||||
|
const pauseRecording = () => {
|
||||||
|
if (!isRecordingSupported.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mediaRecorder.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (recordingState.value !== 'recording') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorder.value.pause();
|
||||||
|
recordingState.value = 'paused';
|
||||||
|
};
|
||||||
|
|
||||||
|
const resumeRecording = () => {
|
||||||
|
if (!isRecordingSupported.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mediaRecorder.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recordingState.value !== 'paused') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaRecorder.value.resume();
|
||||||
|
recordingState.value = 'recording';
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isRecordingSupported,
|
||||||
|
startRecording,
|
||||||
|
stopRecording,
|
||||||
|
pauseRecording,
|
||||||
|
resumeRecording,
|
||||||
|
recordingState,
|
||||||
|
|
||||||
|
onRecordAvailable: recordAvailable.on,
|
||||||
|
};
|
||||||
|
}
|
@@ -1,51 +1,4 @@
|
|||||||
<template>
|
|
||||||
<n-card>
|
|
||||||
<n-form label-width="120" label-placement="left" :show-feedback="false">
|
|
||||||
<n-form-item label="Your string:">
|
|
||||||
<n-input v-model:value="input" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-divider />
|
|
||||||
|
|
||||||
<n-form-item label="Camelcase:">
|
|
||||||
<input-copyable :value="camelCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Capitalcase:">
|
|
||||||
<input-copyable :value="capitalCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Constantcase:">
|
|
||||||
<input-copyable :value="constantCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Dotcase:">
|
|
||||||
<input-copyable :value="dotCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Headercase:">
|
|
||||||
<input-copyable :value="headerCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Nocase:">
|
|
||||||
<input-copyable :value="noCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Paramcase:">
|
|
||||||
<input-copyable :value="paramCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Pascalcase:">
|
|
||||||
<input-copyable :value="pascalCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Pathcase:">
|
|
||||||
<input-copyable :value="pathCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Sentencecase:">
|
|
||||||
<input-copyable :value="sentenceCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Snakecase:">
|
|
||||||
<input-copyable :value="snakeCase(input, baseConfig)" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
|
||||||
import {
|
import {
|
||||||
camelCase,
|
camelCase,
|
||||||
capitalCase,
|
capitalCase,
|
||||||
@@ -66,10 +19,95 @@ const baseConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const input = ref('lorem ipsum dolor sit amet');
|
const input = ref('lorem ipsum dolor sit amet');
|
||||||
|
|
||||||
|
const formats = computed(() => [
|
||||||
|
{
|
||||||
|
label: 'Lowercase:',
|
||||||
|
value: input.value.toLocaleLowerCase(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Uppercase:',
|
||||||
|
value: input.value.toLocaleUpperCase(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Camelcase:',
|
||||||
|
value: camelCase(input.value, baseConfig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Capitalcase:',
|
||||||
|
value: capitalCase(input.value, baseConfig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Constantcase:',
|
||||||
|
value: constantCase(input.value, baseConfig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Dotcase:',
|
||||||
|
value: dotCase(input.value, baseConfig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Headercase:',
|
||||||
|
value: headerCase(input.value, baseConfig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Nocase:',
|
||||||
|
value: noCase(input.value, baseConfig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Paramcase:',
|
||||||
|
value: paramCase(input.value, baseConfig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pascalcase:',
|
||||||
|
value: pascalCase(input.value, baseConfig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Pathcase:',
|
||||||
|
value: pathCase(input.value, baseConfig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Sentencecase:',
|
||||||
|
value: sentenceCase(input.value, baseConfig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Snakecase:',
|
||||||
|
value: snakeCase(input.value, baseConfig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Mockingcase:',
|
||||||
|
value: input.value
|
||||||
|
.split('')
|
||||||
|
.map((char, index) => (index % 2 === 0 ? char.toUpperCase() : char.toLowerCase()))
|
||||||
|
.join(''),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const inputLabelAlignmentConfig = {
|
||||||
|
labelPosition: 'left',
|
||||||
|
labelWidth: '120px',
|
||||||
|
labelAlign: 'right',
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<template>
|
||||||
.n-form-item {
|
<c-card>
|
||||||
margin: 5px 0;
|
<c-input-text
|
||||||
}
|
v-model:value="input"
|
||||||
</style>
|
label="Your string:"
|
||||||
|
placeholder="Your string..."
|
||||||
|
raw-text
|
||||||
|
v-bind="inputLabelAlignmentConfig"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div my-16px divider />
|
||||||
|
|
||||||
|
<InputCopyable
|
||||||
|
v-for="format in formats"
|
||||||
|
:key="format.label"
|
||||||
|
:value="format.value"
|
||||||
|
:label="format.label"
|
||||||
|
v-bind="inputLabelAlignmentConfig"
|
||||||
|
mb-1
|
||||||
|
/>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { expect, describe, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
|
import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service';
|
||||||
|
|
||||||
describe('chmod-calculator', () => {
|
describe('chmod-calculator', () => {
|
||||||
describe('computeChmodOctalRepresentation', () => {
|
describe('computeChmodOctalRepresentation', () => {
|
||||||
@@ -64,5 +64,67 @@ describe('chmod-calculator', () => {
|
|||||||
}),
|
}),
|
||||||
).to.eql('222');
|
).to.eql('222');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('get the symbolic representation from permissions', () => {
|
||||||
|
expect(
|
||||||
|
computeChmodSymbolicRepresentation({
|
||||||
|
permissions: {
|
||||||
|
owner: { read: true, write: true, execute: true },
|
||||||
|
group: { read: true, write: true, execute: true },
|
||||||
|
public: { read: true, write: true, execute: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).to.eql('rwxrwxrwx');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
computeChmodSymbolicRepresentation({
|
||||||
|
permissions: {
|
||||||
|
owner: { read: false, write: false, execute: false },
|
||||||
|
group: { read: false, write: false, execute: false },
|
||||||
|
public: { read: false, write: false, execute: false },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).to.eql('---------');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
computeChmodSymbolicRepresentation({
|
||||||
|
permissions: {
|
||||||
|
owner: { read: false, write: true, execute: false },
|
||||||
|
group: { read: false, write: true, execute: true },
|
||||||
|
public: { read: true, write: false, execute: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).to.eql('-w--wxr-x');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
computeChmodSymbolicRepresentation({
|
||||||
|
permissions: {
|
||||||
|
owner: { read: true, write: false, execute: false },
|
||||||
|
group: { read: false, write: true, execute: false },
|
||||||
|
public: { read: false, write: false, execute: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).to.eql('r---w---x');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
computeChmodSymbolicRepresentation({
|
||||||
|
permissions: {
|
||||||
|
owner: { read: false, write: false, execute: true },
|
||||||
|
group: { read: false, write: true, execute: false },
|
||||||
|
public: { read: true, write: false, execute: false },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).to.eql('--x-w-r--');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
computeChmodSymbolicRepresentation({
|
||||||
|
permissions: {
|
||||||
|
owner: { read: false, write: true, execute: false },
|
||||||
|
group: { read: false, write: true, execute: false },
|
||||||
|
public: { read: false, write: true, execute: false },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).to.eql('-w--w--w-');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import type { GroupPermissions, Permissions } from './chmod-calculator.types';
|
import type { GroupPermissions, Permissions } from './chmod-calculator.types';
|
||||||
|
|
||||||
export { computeChmodOctalRepresentation };
|
export { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation };
|
||||||
|
|
||||||
function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string {
|
function computeChmodOctalRepresentation({ permissions }: { permissions: Permissions }): string {
|
||||||
const permissionValue = { read: 4, write: 2, execute: 1 };
|
const permissionValue = { read: 4, write: 2, execute: 1 };
|
||||||
@@ -15,3 +15,16 @@ function computeChmodOctalRepresentation({ permissions }: { permissions: Permiss
|
|||||||
getGroupPermissionValue(permissions.public),
|
getGroupPermissionValue(permissions.public),
|
||||||
].join('');
|
].join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function computeChmodSymbolicRepresentation({ permissions }: { permissions: Permissions }): string {
|
||||||
|
const permissionValue = { read: 'r', write: 'w', execute: 'x' };
|
||||||
|
|
||||||
|
const getGroupPermissionValue = (permission: GroupPermissions) =>
|
||||||
|
_.reduce(permission, (acc, isPermSet, key) => acc + (isPermSet ? _.get(permissionValue, key, '') : '-'), '');
|
||||||
|
|
||||||
|
return [
|
||||||
|
getGroupPermissionValue(permissions.owner),
|
||||||
|
getGroupPermissionValue(permissions.group),
|
||||||
|
getGroupPermissionValue(permissions.public),
|
||||||
|
].join('');
|
||||||
|
}
|
||||||
|
@@ -1,38 +1,8 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="text-center" scope="col"></th>
|
|
||||||
<th class="text-center" scope="col">Owner (u)</th>
|
|
||||||
<th class="text-center" scope="col">Group (g)</th>
|
|
||||||
<th class="text-center" scope="col">Public (o)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="{ scope, title } of scopes" :key="scope">
|
|
||||||
<td class="line-header">{{ title }}</td>
|
|
||||||
<td v-for="group of groups" :key="group" class="text-center">
|
|
||||||
<!-- <n-switch v-model:value="permissions[group][scope]" /> -->
|
|
||||||
<n-checkbox v-model:checked="permissions[group][scope]" size="large" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</n-table>
|
|
||||||
|
|
||||||
<div class="octal-result">
|
|
||||||
{{ octal }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input-copyable :value="`chmod ${octal} path`" readonly />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useThemeVars } from 'naive-ui';
|
import { useThemeVars } from 'naive-ui';
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { computeChmodOctalRepresentation } from './chmod-calculator.service';
|
|
||||||
import InputCopyable from '../../components/InputCopyable.vue';
|
import InputCopyable from '../../components/InputCopyable.vue';
|
||||||
|
import { computeChmodOctalRepresentation, computeChmodSymbolicRepresentation } from './chmod-calculator.service';
|
||||||
|
|
||||||
import type { Group, Scope } from './chmod-calculator.types';
|
import type { Group, Scope } from './chmod-calculator.types';
|
||||||
|
|
||||||
@@ -52,8 +22,50 @@ const permissions = ref({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
|
const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value }));
|
||||||
|
const symbolic = computed(() => computeChmodSymbolicRepresentation({ permissions: permissions.value }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-center" scope="col" />
|
||||||
|
<th class="text-center" scope="col">
|
||||||
|
Owner (u)
|
||||||
|
</th>
|
||||||
|
<th class="text-center" scope="col">
|
||||||
|
Group (g)
|
||||||
|
</th>
|
||||||
|
<th class="text-center" scope="col">
|
||||||
|
Public (o)
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="{ scope, title } of scopes" :key="scope">
|
||||||
|
<td class="line-header">
|
||||||
|
{{ title }}
|
||||||
|
</td>
|
||||||
|
<td v-for="group of groups" :key="group" class="text-center">
|
||||||
|
<!-- <n-switch v-model:value="permissions[group][scope]" /> -->
|
||||||
|
<n-checkbox v-model:checked="permissions[group][scope]" size="large" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</n-table>
|
||||||
|
|
||||||
|
<div class="octal-result">
|
||||||
|
{{ octal }}
|
||||||
|
</div>
|
||||||
|
<div class="octal-result">
|
||||||
|
{{ symbolic }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InputCopyable :value="`chmod ${octal} path`" readonly />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.octal-result {
|
.octal-result {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@@ -1,21 +1,6 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-card>
|
|
||||||
<div class="duration">{{ formatMs(counter) }}</div>
|
|
||||||
</n-card>
|
|
||||||
<br />
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-button v-if="!isRunning" secondary type="primary" @click="resume">Start</n-button>
|
|
||||||
<n-button v-else secondary type="warning" @click="pause">Stop</n-button>
|
|
||||||
|
|
||||||
<n-button secondary @click="counter = 0">Reset</n-button>
|
|
||||||
</n-space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRafFn } from '@vueuse/core';
|
import { useRafFn } from '@vueuse/core';
|
||||||
import { ref } from 'vue';
|
|
||||||
import { formatMs } from './chronometer.service';
|
import { formatMs } from './chronometer.service';
|
||||||
|
|
||||||
const isRunning = ref(false);
|
const isRunning = ref(false);
|
||||||
@@ -43,6 +28,28 @@ function pause() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<c-card>
|
||||||
|
<div class="duration">
|
||||||
|
{{ formatMs(counter) }}
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
<div mt-5 flex justify-center gap-3>
|
||||||
|
<c-button v-if="!isRunning" type="primary" @click="resume">
|
||||||
|
Start
|
||||||
|
</c-button>
|
||||||
|
<c-button v-else type="warning" @click="pause">
|
||||||
|
Stop
|
||||||
|
</c-button>
|
||||||
|
|
||||||
|
<c-button @click="counter = 0">
|
||||||
|
Reset
|
||||||
|
</c-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.duration {
|
.duration {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
23
src/tools/color-converter/color-converter.e2e.spec.ts
Normal file
23
src/tools/color-converter/color-converter.e2e.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Tool - Color converter', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/color-converter');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Has title', async ({ page }) => {
|
||||||
|
await expect(page).toHaveTitle('Color converter - IT Tools');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Color is converted from its name to other formats', async ({ page }) => {
|
||||||
|
await page.getByTestId('input-name').fill('olive');
|
||||||
|
|
||||||
|
expect(await page.getByTestId('input-name').inputValue()).toEqual('olive');
|
||||||
|
expect(await page.getByTestId('input-hex').inputValue()).toEqual('#808000');
|
||||||
|
expect(await page.getByTestId('input-rgb').inputValue()).toEqual('rgb(128, 128, 0)');
|
||||||
|
expect(await page.getByTestId('input-hsl').inputValue()).toEqual('hsl(60, 100%, 25%)');
|
||||||
|
expect(await page.getByTestId('input-hwb').inputValue()).toEqual('hwb(60 0% 50%)');
|
||||||
|
expect(await page.getByTestId('input-cmyk').inputValue()).toEqual('device-cmyk(0% 0% 100% 50%)');
|
||||||
|
expect(await page.getByTestId('input-lch').inputValue()).toEqual('lch(52.15% 56.81 99.57)');
|
||||||
|
});
|
||||||
|
});
|
13
src/tools/color-converter/color-converter.models.test.ts
Normal file
13
src/tools/color-converter/color-converter.models.test.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { removeAlphaChannelWhenOpaque } from './color-converter.models';
|
||||||
|
|
||||||
|
describe('color-converter models', () => {
|
||||||
|
describe('removeAlphaChannelWhenOpaque', () => {
|
||||||
|
it('remove alpha channel of an hex color when it is opaque (alpha = 1)', () => {
|
||||||
|
expect(removeAlphaChannelWhenOpaque('#000000ff')).toBe('#000000');
|
||||||
|
expect(removeAlphaChannelWhenOpaque('#ffffffFF')).toBe('#ffffff');
|
||||||
|
expect(removeAlphaChannelWhenOpaque('#000000FE')).toBe('#000000FE');
|
||||||
|
expect(removeAlphaChannelWhenOpaque('#00000000')).toBe('#00000000');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
52
src/tools/color-converter/color-converter.models.ts
Normal file
52
src/tools/color-converter/color-converter.models.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { type Colord, colord } from 'colord';
|
||||||
|
import { withDefaultOnError } from '@/utils/defaults';
|
||||||
|
import { useValidation } from '@/composable/validation';
|
||||||
|
|
||||||
|
export { removeAlphaChannelWhenOpaque, buildColorFormat };
|
||||||
|
|
||||||
|
function removeAlphaChannelWhenOpaque(hexColor: string) {
|
||||||
|
return hexColor.replace(/^(#(?:[0-9a-f]{3}){1,2})ff$/i, '$1');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildColorFormat({
|
||||||
|
label,
|
||||||
|
parse = value => colord(value),
|
||||||
|
format,
|
||||||
|
placeholder,
|
||||||
|
invalidMessage = `Invalid ${label.toLowerCase()} format.`,
|
||||||
|
type = 'text',
|
||||||
|
}: {
|
||||||
|
label: string
|
||||||
|
parse?: (value: string) => Colord
|
||||||
|
format: (value: Colord) => string
|
||||||
|
placeholder?: string
|
||||||
|
invalidMessage?: string
|
||||||
|
type?: 'text' | 'color-picker'
|
||||||
|
}) {
|
||||||
|
const value = ref('');
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
parse: (v: string) => withDefaultOnError(() => parse(v), undefined),
|
||||||
|
format,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
validation: useValidation({
|
||||||
|
source: value,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
message: invalidMessage,
|
||||||
|
validator: v => withDefaultOnError(() => {
|
||||||
|
if (v === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parse(v).isValid();
|
||||||
|
}, false),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
@@ -1,69 +1,103 @@
|
|||||||
<template>
|
|
||||||
<n-card>
|
|
||||||
<n-form label-width="100" label-placement="left">
|
|
||||||
<n-form-item label="color picker:">
|
|
||||||
<n-color-picker
|
|
||||||
v-model:value="hex"
|
|
||||||
placement="bottom-end"
|
|
||||||
@update:value="(v: string) => onInputUpdated(v, 'hex')"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="color name:">
|
|
||||||
<input-copyable v-model:value="name" :on-input="(v: string) => onInputUpdated(v, 'name')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="hex:">
|
|
||||||
<input-copyable v-model:value="hex" :on-input="(v: string) => onInputUpdated(v, 'hex')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="rgb:">
|
|
||||||
<input-copyable v-model:value="rgb" :on-input="(v: string) => onInputUpdated(v, 'rgb')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="hsl:">
|
|
||||||
<input-copyable v-model:value="hsl" :on-input="(v: string) => onInputUpdated(v, 'hsl')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="hwb:">
|
|
||||||
<input-copyable v-model:value="hwb" :on-input="(v: string) => onInputUpdated(v, 'hwb')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="lch:">
|
|
||||||
<input-copyable v-model:value="lch" :on-input="(v: string) => onInputUpdated(v, 'lch')" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="cmyk:">
|
|
||||||
<input-copyable v-model:value="cmyk" :on-input="(v: string) => onInputUpdated(v, 'cmyk')" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import type { Colord } from 'colord';
|
||||||
import { colord, extend } from 'colord';
|
import { colord, extend } from 'colord';
|
||||||
|
import _ from 'lodash';
|
||||||
import cmykPlugin from 'colord/plugins/cmyk';
|
import cmykPlugin from 'colord/plugins/cmyk';
|
||||||
import hwbPlugin from 'colord/plugins/hwb';
|
import hwbPlugin from 'colord/plugins/hwb';
|
||||||
import namesPlugin from 'colord/plugins/names';
|
import namesPlugin from 'colord/plugins/names';
|
||||||
import lchPlugin from 'colord/plugins/lch';
|
import lchPlugin from 'colord/plugins/lch';
|
||||||
import InputCopyable from '../../components/InputCopyable.vue';
|
import { buildColorFormat } from './color-converter.models';
|
||||||
|
|
||||||
extend([cmykPlugin, hwbPlugin, namesPlugin, lchPlugin]);
|
extend([cmykPlugin, hwbPlugin, namesPlugin, lchPlugin]);
|
||||||
|
|
||||||
const name = ref('');
|
const formats = {
|
||||||
const hex = ref('#1ea54cff');
|
picker: buildColorFormat({
|
||||||
const rgb = ref('');
|
label: 'color picker',
|
||||||
const hsl = ref('');
|
format: (v: Colord) => v.toHex(),
|
||||||
const hwb = ref('');
|
type: 'color-picker',
|
||||||
const cmyk = ref('');
|
}),
|
||||||
const lch = ref('');
|
hex: buildColorFormat({
|
||||||
|
label: 'hex',
|
||||||
|
format: (v: Colord) => v.toHex(),
|
||||||
|
placeholder: 'e.g. #ff0000',
|
||||||
|
}),
|
||||||
|
rgb: buildColorFormat({
|
||||||
|
label: 'rgb',
|
||||||
|
format: (v: Colord) => v.toRgbString(),
|
||||||
|
placeholder: 'e.g. rgb(255, 0, 0)',
|
||||||
|
}),
|
||||||
|
hsl: buildColorFormat({
|
||||||
|
label: 'hsl',
|
||||||
|
format: (v: Colord) => v.toHslString(),
|
||||||
|
placeholder: 'e.g. hsl(0, 100%, 50%)',
|
||||||
|
}),
|
||||||
|
hwb: buildColorFormat({
|
||||||
|
label: 'hwb',
|
||||||
|
format: (v: Colord) => v.toHwbString(),
|
||||||
|
placeholder: 'e.g. hwb(0, 0%, 0%)',
|
||||||
|
}),
|
||||||
|
lch: buildColorFormat({
|
||||||
|
label: 'lch',
|
||||||
|
format: (v: Colord) => v.toLchString(),
|
||||||
|
placeholder: 'e.g. lch(53.24, 104.55, 40.85)',
|
||||||
|
}),
|
||||||
|
cmyk: buildColorFormat({
|
||||||
|
label: 'cmyk',
|
||||||
|
format: (v: Colord) => v.toCmykString(),
|
||||||
|
placeholder: 'e.g. cmyk(0, 100%, 100%, 0)',
|
||||||
|
}),
|
||||||
|
name: buildColorFormat({
|
||||||
|
label: 'name',
|
||||||
|
format: (v: Colord) => v.toName({ closest: true }) ?? 'Unknown',
|
||||||
|
placeholder: 'e.g. red',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
function onInputUpdated(value: string, omit: string) {
|
updateColorValue(colord('#1ea54c'));
|
||||||
const color = colord(value);
|
|
||||||
|
|
||||||
if (omit !== 'name') name.value = color.toName({ closest: true }) ?? '';
|
function updateColorValue(value: Colord | undefined, omitLabel?: string) {
|
||||||
if (omit !== 'hex') hex.value = color.toHex();
|
if (value === undefined) {
|
||||||
if (omit !== 'rgb') rgb.value = color.toRgbString();
|
return;
|
||||||
if (omit !== 'hsl') hsl.value = color.toHslString();
|
}
|
||||||
if (omit !== 'hwb') hwb.value = color.toHwbString();
|
|
||||||
if (omit !== 'cmyk') cmyk.value = color.toCmykString();
|
if (!value.isValid()) {
|
||||||
if (omit !== 'lch') lch.value = color.toLchString();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_.forEach(formats, ({ value: valueRef, format }, key) => {
|
||||||
|
if (key !== omitLabel) {
|
||||||
|
valueRef.value = format(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputUpdated(hex.value, 'hex');
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-card>
|
||||||
|
<template v-for="({ label, parse, placeholder, validation, type }, key) in formats" :key="key">
|
||||||
|
<input-copyable
|
||||||
|
v-if="type === 'text'"
|
||||||
|
v-model:value="formats[key].value.value"
|
||||||
|
:test-id="`input-${key}`"
|
||||||
|
:label="`${label}:`"
|
||||||
|
label-position="left"
|
||||||
|
label-width="100px"
|
||||||
|
label-align="right"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:validation="validation"
|
||||||
|
raw-text
|
||||||
|
clearable
|
||||||
|
mt-2
|
||||||
|
@update:value="(v:string) => updateColorValue(parse(v), key)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<n-form-item v-else-if="type === 'color-picker'" :label="`${label}:`" label-width="100" label-placement="left" :show-feedback="false">
|
||||||
|
<n-color-picker
|
||||||
|
v-model:value="formats[key].value.value"
|
||||||
|
placement="bottom-end"
|
||||||
|
@update:value="(v:string) => updateColorValue(parse(v), key)"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</template>
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
@@ -1,92 +1,6 @@
|
|||||||
<template>
|
|
||||||
<n-card>
|
|
||||||
<n-form-item
|
|
||||||
class="cron"
|
|
||||||
:show-label="false"
|
|
||||||
:feedback="cronValidation.message"
|
|
||||||
:validation-status="cronValidation.status"
|
|
||||||
>
|
|
||||||
<n-input v-model:value="cron" size="large" placeholder="* * * * *" />
|
|
||||||
</n-form-item>
|
|
||||||
<div class="cron-string">
|
|
||||||
{{ cronString }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-divider />
|
|
||||||
|
|
||||||
<n-space justify="center">
|
|
||||||
<n-form :show-feedback="false" label-width="170" label-placement="left">
|
|
||||||
<n-form-item label="Verbose">
|
|
||||||
<n-switch v-model:value="cronstrueConfig.verbose" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Use 24 hour time format">
|
|
||||||
<n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item label="Days start at 0">
|
|
||||||
<n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
<n-card>
|
|
||||||
<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
|
|
||||||
>
|
|
||||||
|
|
||||||
<n-space v-if="styleStore.isSmallScreen" vertical>
|
|
||||||
<n-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" embedded :bordered="false">
|
|
||||||
<div>
|
|
||||||
Symbol: <strong>{{ symbol }}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Meaning: <strong>{{ meaning }}</strong>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Example:
|
|
||||||
<strong
|
|
||||||
><code>{{ example }}</code></strong
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Equivalent: <strong>{{ equivalent }}</strong>
|
|
||||||
</div>
|
|
||||||
</n-card>
|
|
||||||
</n-space>
|
|
||||||
<n-table v-else size="small">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="text-left" scope="col">Symbol</th>
|
|
||||||
<th class="text-left" scope="col">Meaning</th>
|
|
||||||
<th class="text-left" scope="col">Example</th>
|
|
||||||
<th class="text-left" scope="col">Equivalent</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol">
|
|
||||||
<td>{{ symbol }}</td>
|
|
||||||
<td>{{ meaning }}</td>
|
|
||||||
<td>
|
|
||||||
<code>{{ example }}</code>
|
|
||||||
</td>
|
|
||||||
<td>{{ equivalent }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</n-table>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import cronstrue from 'cronstrue';
|
import cronstrue from 'cronstrue';
|
||||||
import { isValidCron } from 'cron-validator';
|
import { isValidCron } from 'cron-validator';
|
||||||
import { computed, reactive, ref } from 'vue';
|
|
||||||
import { useValidation } from '@/composable/validation';
|
|
||||||
import { useStyleStore } from '@/stores/style.store';
|
import { useStyleStore } from '@/stores/style.store';
|
||||||
|
|
||||||
function isCronValid(v: string) {
|
function isCronValid(v: string) {
|
||||||
@@ -185,30 +99,85 @@ const cronString = computed(() => {
|
|||||||
return ' ';
|
return ' ';
|
||||||
});
|
});
|
||||||
|
|
||||||
const cronValidation = useValidation({
|
const cronValidationRules = [
|
||||||
source: cron,
|
{
|
||||||
rules: [
|
validator: (value: string) => isCronValid(value),
|
||||||
{
|
message: 'This cron is invalid',
|
||||||
validator: (value) => isCronValid(value),
|
},
|
||||||
message: 'This cron is invalid',
|
];
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<c-card>
|
||||||
|
<div mx-auto max-w-sm>
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="cron"
|
||||||
|
size="large"
|
||||||
|
placeholder="* * * * *"
|
||||||
|
:validation-rules="cronValidationRules"
|
||||||
|
mb-3
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cron-string">
|
||||||
|
{{ cronString }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<div flex justify-center>
|
||||||
|
<n-form :show-feedback="false" label-width="170" label-placement="left">
|
||||||
|
<n-form-item label="Verbose">
|
||||||
|
<n-switch v-model:value="cronstrueConfig.verbose" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Use 24 hour time format">
|
||||||
|
<n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="Days start at 0">
|
||||||
|
<n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
<c-card>
|
||||||
|
<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 v-if="styleStore.isSmallScreen">
|
||||||
|
<c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none>
|
||||||
|
<div>
|
||||||
|
Symbol: <strong>{{ symbol }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Meaning: <strong>{{ meaning }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Example:
|
||||||
|
<strong><code>{{ example }}</code></strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Equivalent: <strong>{{ equivalent }}</strong>
|
||||||
|
</div>
|
||||||
|
</c-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<c-table v-else :data="helpers" />
|
||||||
|
</c-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.cron {
|
::v-deep(input) {
|
||||||
|
font-size: 30px;
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
max-width: 400px;
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
.n-input {
|
|
||||||
font-size: 30px;
|
|
||||||
font-family: monospace;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cron-string {
|
.cron-string {
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
test.describe('Date time converter - json to yaml', () => {
|
test.describe('Date time converter - json to yaml', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@@ -29,5 +29,6 @@ test.describe('Date time converter - json to yaml', () => {
|
|||||||
expect((await page.getByTestId('Timestamp').inputValue()).trim()).toEqual('1681333824000');
|
expect((await page.getByTestId('Timestamp').inputValue()).trim()).toEqual('1681333824000');
|
||||||
expect((await page.getByTestId('UTC format').inputValue()).trim()).toEqual('Wed, 12 Apr 2023 21:10:24 GMT');
|
expect((await page.getByTestId('UTC format').inputValue()).trim()).toEqual('Wed, 12 Apr 2023 21:10:24 GMT');
|
||||||
expect((await page.getByTestId('Mongo ObjectID').inputValue()).trim()).toEqual('64371e400000000000000000');
|
expect((await page.getByTestId('Mongo ObjectID').inputValue()).trim()).toEqual('64371e400000000000000000');
|
||||||
|
expect((await page.getByTestId('Excel date/time').inputValue()).trim()).toEqual('45028.88222222222');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,13 +1,16 @@
|
|||||||
import { describe, test, expect } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
import {
|
import {
|
||||||
|
dateToExcelFormat,
|
||||||
|
excelFormatToDate,
|
||||||
|
isExcelFormat,
|
||||||
isISO8601DateTimeString,
|
isISO8601DateTimeString,
|
||||||
isISO9075DateString,
|
isISO9075DateString,
|
||||||
|
isMongoObjectId,
|
||||||
isRFC3339DateString,
|
isRFC3339DateString,
|
||||||
isRFC7231DateString,
|
isRFC7231DateString,
|
||||||
isUnixTimestamp,
|
|
||||||
isTimestamp,
|
isTimestamp,
|
||||||
isUTCDateString,
|
isUTCDateString,
|
||||||
isMongoObjectId,
|
isUnixTimestamp,
|
||||||
} from './date-time-converter.models';
|
} from './date-time-converter.models';
|
||||||
|
|
||||||
describe('date-time-converter models', () => {
|
describe('date-time-converter models', () => {
|
||||||
@@ -139,4 +142,39 @@ describe('date-time-converter models', () => {
|
|||||||
expect(isMongoObjectId('')).toBe(false);
|
expect(isMongoObjectId('')).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isExcelFormat', () => {
|
||||||
|
test('an Excel format string is a floating number that can be negative', () => {
|
||||||
|
expect(isExcelFormat('0')).toBe(true);
|
||||||
|
expect(isExcelFormat('1')).toBe(true);
|
||||||
|
expect(isExcelFormat('1.1')).toBe(true);
|
||||||
|
expect(isExcelFormat('-1.1')).toBe(true);
|
||||||
|
expect(isExcelFormat('-1')).toBe(true);
|
||||||
|
|
||||||
|
expect(isExcelFormat('')).toBe(false);
|
||||||
|
expect(isExcelFormat('foo')).toBe(false);
|
||||||
|
expect(isExcelFormat('1.1.1')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dateToExcelFormat', () => {
|
||||||
|
test('a date in Excel format is the number of days since 01/01/1900', () => {
|
||||||
|
expect(dateToExcelFormat(new Date('2016-05-20T00:00:00.000Z'))).toBe('42510');
|
||||||
|
expect(dateToExcelFormat(new Date('2016-05-20T12:00:00.000Z'))).toBe('42510.5');
|
||||||
|
expect(dateToExcelFormat(new Date('2023-10-31T09:26:06.421Z'))).toBe('45230.39312987268');
|
||||||
|
expect(dateToExcelFormat(new Date('1970-01-01T00:00:00.000Z'))).toBe('25569');
|
||||||
|
expect(dateToExcelFormat(new Date('1800-01-01T00:00:00.000Z'))).toBe('-36522');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('excelFormatToDate', () => {
|
||||||
|
test('a date in Excel format is the number of days since 01/01/1900', () => {
|
||||||
|
expect(excelFormatToDate('0')).toEqual(new Date('1899-12-30T00:00:00.000Z'));
|
||||||
|
expect(excelFormatToDate('1')).toEqual(new Date('1899-12-31T00:00:00.000Z'));
|
||||||
|
expect(excelFormatToDate('2')).toEqual(new Date('1900-01-01T00:00:00.000Z'));
|
||||||
|
expect(excelFormatToDate('4242.4242')).toEqual(new Date('1911-08-12T10:10:50.880Z'));
|
||||||
|
expect(excelFormatToDate('42738.22626859954')).toEqual(new Date('2017-01-03T05:25:49.607Z'));
|
||||||
|
expect(excelFormatToDate('-1000')).toEqual(new Date('1897-04-04T00:00:00.000Z'));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -9,18 +9,23 @@ export {
|
|||||||
isTimestamp,
|
isTimestamp,
|
||||||
isUTCDateString,
|
isUTCDateString,
|
||||||
isMongoObjectId,
|
isMongoObjectId,
|
||||||
|
dateToExcelFormat,
|
||||||
|
excelFormatToDate,
|
||||||
|
isExcelFormat,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ISO8601_REGEX =
|
const ISO8601_REGEX
|
||||||
/^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
|
= /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
|
||||||
const ISO9075_REGEX =
|
const ISO9075_REGEX
|
||||||
/^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/;
|
= /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/;
|
||||||
|
|
||||||
const RFC3339_REGEX =
|
const RFC3339_REGEX
|
||||||
/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/;
|
= /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/;
|
||||||
|
|
||||||
const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/;
|
const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/;
|
||||||
|
|
||||||
|
const EXCEL_FORMAT_REGEX = /^-?\d+(\.\d+)?$/;
|
||||||
|
|
||||||
function createRegexMatcher(regex: RegExp) {
|
function createRegexMatcher(regex: RegExp) {
|
||||||
return (date?: string) => !_.isNil(date) && regex.test(date);
|
return (date?: string) => !_.isNil(date) && regex.test(date);
|
||||||
}
|
}
|
||||||
@@ -33,6 +38,8 @@ const isUnixTimestamp = createRegexMatcher(/^[0-9]{1,10}$/);
|
|||||||
const isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/);
|
const isTimestamp = createRegexMatcher(/^[0-9]{1,13}$/);
|
||||||
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
|
const isMongoObjectId = createRegexMatcher(/^[0-9a-fA-F]{24}$/);
|
||||||
|
|
||||||
|
const isExcelFormat = createRegexMatcher(EXCEL_FORMAT_REGEX);
|
||||||
|
|
||||||
function isUTCDateString(date?: string) {
|
function isUTCDateString(date?: string) {
|
||||||
if (_.isNil(date)) {
|
if (_.isNil(date)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -40,7 +47,16 @@ function isUTCDateString(date?: string) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return new Date(date).toUTCString() === date;
|
return new Date(date).toUTCString() === date;
|
||||||
} catch (_ignored) {
|
}
|
||||||
|
catch (_ignored) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dateToExcelFormat(date: Date) {
|
||||||
|
return String(((date.getTime()) / (1000 * 60 * 60 * 24)) + 25569);
|
||||||
|
}
|
||||||
|
|
||||||
|
function excelFormatToDate(excelFormat: string | number) {
|
||||||
|
return new Date((Number(excelFormat) - 25569) * 86400 * 1000);
|
||||||
|
}
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
export type ToDateMapper = (value: string) => Date;
|
export type ToDateMapper = (value: string) => Date;
|
||||||
|
|
||||||
export type DateFormat = {
|
export interface DateFormat {
|
||||||
name: string;
|
name: string
|
||||||
fromDate: (date: Date) => string;
|
fromDate: (date: Date) => string
|
||||||
toDate: (value: string) => Date;
|
toDate: (value: string) => Date
|
||||||
formatMatcher: (dateString: string) => boolean;
|
formatMatcher: (dateString: string) => boolean
|
||||||
};
|
}
|
||||||
|
@@ -1,37 +1,3 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-form-item :show-label="false" v-bind="validation.attrs">
|
|
||||||
<n-input-group>
|
|
||||||
<n-input
|
|
||||||
v-model:value="inputDate"
|
|
||||||
:on-input="onDateInputChanged"
|
|
||||||
placeholder="Put you date string here..."
|
|
||||||
clearable
|
|
||||||
:input-props="{ 'data-test-id': 'date-time-converter-input' }"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<n-select
|
|
||||||
v-model:value="formatIndex"
|
|
||||||
style="flex: 0 0 170px"
|
|
||||||
:options="formats.map(({ name }, i) => ({ label: name, value: i }))"
|
|
||||||
data-test-id="date-time-converter-format-select"
|
|
||||||
/>
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item>
|
|
||||||
<n-divider style="margin-top: 0" />
|
|
||||||
<div v-for="{ name, fromDate } in formats" :key="name" mt-1>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label style="flex: 0 0 170px"> {{ name }}: </n-input-group-label>
|
|
||||||
<input-copyable
|
|
||||||
:value="formatDateUsingFormatter(fromDate, normalizedDate)"
|
|
||||||
placeholder="Invalid date..."
|
|
||||||
:input-props="{ 'data-test-id': name }"
|
|
||||||
/>
|
|
||||||
</n-input-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
formatISO,
|
formatISO,
|
||||||
@@ -41,41 +7,36 @@ import {
|
|||||||
fromUnixTime,
|
fromUnixTime,
|
||||||
getTime,
|
getTime,
|
||||||
getUnixTime,
|
getUnixTime,
|
||||||
parseISO,
|
|
||||||
parseJSON,
|
|
||||||
isDate,
|
isDate,
|
||||||
isValid,
|
isValid,
|
||||||
|
parseISO,
|
||||||
|
parseJSON,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { withDefaultOnError } from '@/utils/defaults';
|
|
||||||
import { useValidation } from '@/composable/validation';
|
|
||||||
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
|
import type { DateFormat, ToDateMapper } from './date-time-converter.types';
|
||||||
import {
|
import {
|
||||||
|
dateToExcelFormat,
|
||||||
|
excelFormatToDate,
|
||||||
|
isExcelFormat,
|
||||||
isISO8601DateTimeString,
|
isISO8601DateTimeString,
|
||||||
isISO9075DateString,
|
isISO9075DateString,
|
||||||
|
isMongoObjectId,
|
||||||
isRFC3339DateString,
|
isRFC3339DateString,
|
||||||
isRFC7231DateString,
|
isRFC7231DateString,
|
||||||
isTimestamp,
|
isTimestamp,
|
||||||
isUTCDateString,
|
isUTCDateString,
|
||||||
isUnixTimestamp,
|
isUnixTimestamp,
|
||||||
isMongoObjectId,
|
|
||||||
} from './date-time-converter.models';
|
} from './date-time-converter.models';
|
||||||
|
import { withDefaultOnError } from '@/utils/defaults';
|
||||||
|
import { useValidation } from '@/composable/validation';
|
||||||
|
|
||||||
const inputDate = ref('');
|
const inputDate = ref('');
|
||||||
|
|
||||||
const toDate: ToDateMapper = (date) => new Date(date);
|
const toDate: ToDateMapper = date => new Date(date);
|
||||||
|
|
||||||
function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) {
|
|
||||||
if (!date || !validation.isValid) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return withDefaultOnError(() => formatter(date), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
const formats: DateFormat[] = [
|
const formats: DateFormat[] = [
|
||||||
{
|
{
|
||||||
name: 'JS locale date string',
|
name: 'JS locale date string',
|
||||||
fromDate: (date) => date.toString(),
|
fromDate: date => date.toString(),
|
||||||
toDate,
|
toDate,
|
||||||
formatMatcher: () => false,
|
formatMatcher: () => false,
|
||||||
},
|
},
|
||||||
@@ -83,49 +44,55 @@ const formats: DateFormat[] = [
|
|||||||
name: 'ISO 8601',
|
name: 'ISO 8601',
|
||||||
fromDate: formatISO,
|
fromDate: formatISO,
|
||||||
toDate: parseISO,
|
toDate: parseISO,
|
||||||
formatMatcher: (date) => isISO8601DateTimeString(date),
|
formatMatcher: date => isISO8601DateTimeString(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ISO 9075',
|
name: 'ISO 9075',
|
||||||
fromDate: formatISO9075,
|
fromDate: formatISO9075,
|
||||||
toDate: parseISO,
|
toDate: parseISO,
|
||||||
formatMatcher: (date) => isISO9075DateString(date),
|
formatMatcher: date => isISO9075DateString(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'RFC 3339',
|
name: 'RFC 3339',
|
||||||
fromDate: formatRFC3339,
|
fromDate: formatRFC3339,
|
||||||
toDate,
|
toDate,
|
||||||
formatMatcher: (date) => isRFC3339DateString(date),
|
formatMatcher: date => isRFC3339DateString(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'RFC 7231',
|
name: 'RFC 7231',
|
||||||
fromDate: formatRFC7231,
|
fromDate: formatRFC7231,
|
||||||
toDate,
|
toDate,
|
||||||
formatMatcher: (date) => isRFC7231DateString(date),
|
formatMatcher: date => isRFC7231DateString(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Unix timestamp',
|
name: 'Unix timestamp',
|
||||||
fromDate: (date) => String(getUnixTime(date)),
|
fromDate: date => String(getUnixTime(date)),
|
||||||
toDate: (sec) => fromUnixTime(+sec),
|
toDate: sec => fromUnixTime(+sec),
|
||||||
formatMatcher: (date) => isUnixTimestamp(date),
|
formatMatcher: date => isUnixTimestamp(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Timestamp',
|
name: 'Timestamp',
|
||||||
fromDate: (date) => String(getTime(date)),
|
fromDate: date => String(getTime(date)),
|
||||||
toDate: (ms) => parseJSON(+ms),
|
toDate: ms => parseJSON(+ms),
|
||||||
formatMatcher: (date) => isTimestamp(date),
|
formatMatcher: date => isTimestamp(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'UTC format',
|
name: 'UTC format',
|
||||||
fromDate: (date) => date.toUTCString(),
|
fromDate: date => date.toUTCString(),
|
||||||
toDate,
|
toDate,
|
||||||
formatMatcher: (date) => isUTCDateString(date),
|
formatMatcher: date => isUTCDateString(date),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Mongo ObjectID',
|
name: 'Mongo ObjectID',
|
||||||
fromDate: (date) => Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000',
|
fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`,
|
||||||
toDate: (objectId) => new Date(parseInt(objectId.substring(0, 8), 16) * 1000),
|
toDate: objectId => new Date(Number.parseInt(objectId.substring(0, 8), 16) * 1000),
|
||||||
formatMatcher: (date) => isMongoObjectId(date),
|
formatMatcher: date => isMongoObjectId(date),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Excel date/time',
|
||||||
|
fromDate: date => dateToExcelFormat(date),
|
||||||
|
toDate: excelFormatToDate,
|
||||||
|
formatMatcher: isExcelFormat,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -141,7 +108,8 @@ const normalizedDate = computed(() => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return toDate(inputDate.value);
|
return toDate(inputDate.value);
|
||||||
} catch (_ignored) {
|
}
|
||||||
|
catch (_ignored) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -159,9 +127,11 @@ const validation = useValidation({
|
|||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
message: 'This date is invalid for this format',
|
message: 'This date is invalid for this format',
|
||||||
validator: (value) =>
|
validator: value =>
|
||||||
withDefaultOnError(() => {
|
withDefaultOnError(() => {
|
||||||
if (value === '') return true;
|
if (value === '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const maybeDate = formats[formatIndex.value].toDate(value);
|
const maybeDate = formats[formatIndex.value].toDate(value);
|
||||||
return isDate(maybeDate) && isValid(maybeDate);
|
return isDate(maybeDate) && isValid(maybeDate);
|
||||||
@@ -169,4 +139,51 @@ const validation = useValidation({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) {
|
||||||
|
if (!date || !validation.isValid) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return withDefaultOnError(() => formatter(date), '');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div flex gap-2>
|
||||||
|
<c-input-text
|
||||||
|
v-model:value="inputDate"
|
||||||
|
autofocus
|
||||||
|
placeholder="Put your date string here..."
|
||||||
|
clearable
|
||||||
|
test-id="date-time-converter-input"
|
||||||
|
:validation="validation"
|
||||||
|
@update:value="onDateInputChanged"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<c-select
|
||||||
|
v-model:value="formatIndex"
|
||||||
|
style="flex: 0 0 170px"
|
||||||
|
:options="formats.map(({ name }, i) => ({ label: name, value: i }))"
|
||||||
|
data-test-id="date-time-converter-format-select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider />
|
||||||
|
|
||||||
|
<input-copyable
|
||||||
|
v-for="{ name, fromDate } in formats"
|
||||||
|
:key="name"
|
||||||
|
:label="name"
|
||||||
|
label-width="150px"
|
||||||
|
label-position="left"
|
||||||
|
label-align="right"
|
||||||
|
:value="formatDateUsingFormatter(fromDate, normalizedDate)"
|
||||||
|
placeholder="Invalid date..."
|
||||||
|
:test-id="name"
|
||||||
|
readonly
|
||||||
|
mt-2
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user