mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
Merge branch 'main' into pr/Srivarshan-T/216
This commit is contained in:
commit
adbc389c5d
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- name: Run tests
|
||||
@ -31,7 +31,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install Playwright Browsers
|
||||
@ -96,7 +96,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- name: Build project
|
||||
|
||||
528
.idea/workspace.xml
generated
528
.idea/workspace.xml
generated
@ -4,12 +4,10 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="chore: translate userTypes">
|
||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="fix(i18n): Correct "hireMe" translation in navbar This commit corrects the translation of "hireMe" in the navigation bar across all supported languages. The order of the elements was also fixed to be consistent.">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/index.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/random-number-generator/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/random-number-generator/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/number/random-port-generator/meta.ts" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/public/locales/es/translation.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/es/translation.json" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/public/locales/fr/translation.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/fr/translation.json" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@ -43,7 +41,7 @@
|
||||
<option name="PUSH_AUTO_UPDATE" value="true" />
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="fork/AshAnand34/tool/random-generators" />
|
||||
<entry key="$PROJECT_DIR$" value="28f4c64d3044df927dc088435164e803e14f8794" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
@ -55,15 +53,14 @@
|
||||
"assignee": "iib0011"
|
||||
},
|
||||
{
|
||||
"searchQuery": "filter",
|
||||
"state": "OPEN"
|
||||
},
|
||||
{
|
||||
"searchQuery": "filter",
|
||||
"state": "OPEN"
|
||||
}
|
||||
],
|
||||
"lastFilter": {
|
||||
"searchQuery": "filter",
|
||||
"state": "OPEN"
|
||||
}
|
||||
}</component>
|
||||
@ -230,13 +227,6 @@
|
||||
},
|
||||
"lastSeen": 1752158748013
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6eqzP7",
|
||||
"number": 190
|
||||
},
|
||||
"lastSeen": 1752404173008
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6et6vx",
|
||||
@ -260,10 +250,101 @@
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6fo_ig",
|
||||
"number": 209
|
||||
"id": "PR_kwDOMJIfts6rjINx",
|
||||
"number": 259
|
||||
},
|
||||
"lastSeen": 1753201966322
|
||||
"lastSeen": 1759434090574
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6qcP13",
|
||||
"number": 256
|
||||
},
|
||||
"lastSeen": 1759434257615
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6ow8QZ",
|
||||
"number": 252
|
||||
},
|
||||
"lastSeen": 1759434340504
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6myVeZ",
|
||||
"number": 247
|
||||
},
|
||||
"lastSeen": 1759434588110
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6i5ZAq",
|
||||
"number": 239
|
||||
},
|
||||
"lastSeen": 1759434599664
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6iiuGd",
|
||||
"number": 237
|
||||
},
|
||||
"lastSeen": 1759434652702
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6gwm8n",
|
||||
"number": 230
|
||||
},
|
||||
"lastSeen": 1759434669914
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6f5JeZ",
|
||||
"number": 220
|
||||
},
|
||||
"lastSeen": 1759434706785
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6ftgWI",
|
||||
"number": 217
|
||||
},
|
||||
"lastSeen": 1759434804548
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6XsHfL",
|
||||
"number": 128
|
||||
},
|
||||
"lastSeen": 1759434870000
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6ec-tz",
|
||||
"number": 180
|
||||
},
|
||||
"lastSeen": 1759434882113
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6fsi5n",
|
||||
"number": 216
|
||||
},
|
||||
"lastSeen": 1759434902813
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6ZkP3F",
|
||||
"number": 142
|
||||
},
|
||||
"lastSeen": 1759434918778
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6qcbuA",
|
||||
"number": 257
|
||||
},
|
||||
"lastSeen": 1759438234107
|
||||
}
|
||||
]
|
||||
}</component>
|
||||
@ -298,65 +379,66 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ASKED_ADD_EXTERNAL_FILES": "true",
|
||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||
"Docker.Dockerfile build.executor": "Run",
|
||||
"Docker.Dockerfile.executor": "Run",
|
||||
"Node.js.add-i18n-to-meta.js.executor": "Run",
|
||||
"Node.js.locize-upload.js.executor": "Run",
|
||||
"Node.js.update-i18n-from-meta.js.executor": "Run",
|
||||
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
|
||||
"Playwright.JoinText Component.executor": "Run",
|
||||
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"Vitest.compute function (1).executor": "Run",
|
||||
"Vitest.compute function.executor": "Run",
|
||||
"Vitest.generatePassword.executor": "Run",
|
||||
"Vitest.mergeText.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
|
||||
"Vitest.parsePageRanges.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
|
||||
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
|
||||
"Vitest.replaceText function.executor": "Run",
|
||||
"Vitest.timeBetweenDates.executor": "Run",
|
||||
"git-widget-placeholder": "#218 on fork/AshAnand34/tool/random-generators",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"npm.build.executor": "Run",
|
||||
"npm.dev.executor": "Run",
|
||||
"npm.i18n:extract.executor": "Run",
|
||||
"npm.i18n:pull.executor": "Run",
|
||||
"npm.i18n:push.executor": "Run",
|
||||
"npm.i18n:sync.executor": "Run",
|
||||
"npm.lint.executor": "Run",
|
||||
"npm.prebuild.executor": "Run",
|
||||
"npm.script:create:tool.executor": "Run",
|
||||
"npm.test.executor": "Run",
|
||||
"npm.test:e2e.executor": "Run",
|
||||
"npm.test:e2e:run.executor": "Run",
|
||||
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
|
||||
"project.structure.last.edited": "Problems",
|
||||
"project.structure.proportion": "0.0",
|
||||
"project.structure.side.proportion": "0.2",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
|
||||
"ts.rename.search.for.js.occurrences": "false",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"ASKED_ADD_EXTERNAL_FILES": "true",
|
||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||
"Docker.Dockerfile build.executor": "Run",
|
||||
"Docker.Dockerfile.executor": "Run",
|
||||
"Node.js.add-i18n-to-meta.js.executor": "Run",
|
||||
"Node.js.locize-upload.js.executor": "Run",
|
||||
"Node.js.update-i18n-from-meta.js.executor": "Run",
|
||||
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
|
||||
"Playwright.JoinText Component.executor": "Run",
|
||||
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"Vitest.compute function (1).executor": "Run",
|
||||
"Vitest.compute function.executor": "Run",
|
||||
"Vitest.generatePassword.executor": "Run",
|
||||
"Vitest.mergeText.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
|
||||
"Vitest.parsePageRanges.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
|
||||
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
|
||||
"Vitest.replaceText function.executor": "Run",
|
||||
"Vitest.timeBetweenDates.executor": "Run",
|
||||
"git-widget-placeholder": "main",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"npm.build.executor": "Run",
|
||||
"npm.dev.executor": "Run",
|
||||
"npm.i18n:extract.executor": "Run",
|
||||
"npm.i18n:pull.executor": "Run",
|
||||
"npm.i18n:push.executor": "Run",
|
||||
"npm.i18n:sync.executor": "Run",
|
||||
"npm.lint.executor": "Run",
|
||||
"npm.prebuild.executor": "Run",
|
||||
"npm.script:create:tool.executor": "Run",
|
||||
"npm.test.executor": "Run",
|
||||
"npm.test:e2e.executor": "Run",
|
||||
"npm.test:e2e:run.executor": "Run",
|
||||
"npm.typecheck.executor": "Run",
|
||||
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
|
||||
"project.structure.last.edited": "Problems",
|
||||
"project.structure.proportion": "0.0",
|
||||
"project.structure.side.proportion": "0.2",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
|
||||
"ts.rename.search.for.js.occurrences": "false",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="ReactDesignerToolWindowState">
|
||||
<option name="myId2Visible">
|
||||
<map>
|
||||
@ -383,19 +465,6 @@
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager" selected="npm.dev">
|
||||
<configuration name="generatePassword" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
|
||||
<node-interpreter value="project" />
|
||||
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
|
||||
<working-dir value="$PROJECT_DIR$" />
|
||||
<vitest-options value="--run" />
|
||||
<envs />
|
||||
<scope-kind value="SUITE" />
|
||||
<test-file value="$PROJECT_DIR$/src/pages/tools/string/password-generator/password-generator.service.test.ts" />
|
||||
<test-names>
|
||||
<test-name value="generatePassword" />
|
||||
</test-names>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration default="true" type="docker-deploy" factoryName="dockerfile" temporary="true">
|
||||
<deployment type="dockerfile">
|
||||
<settings />
|
||||
@ -415,26 +484,6 @@
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="i18n:extract" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="i18n:extract" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="i18n:pull" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="i18n:pull" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="i18n:sync" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
@ -447,20 +496,50 @@
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="test" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="test" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="test:e2e" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="test:e2e" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<configuration name="typecheck" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="typecheck" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
<list>
|
||||
<item itemvalue="npm.i18n:extract" />
|
||||
<item itemvalue="npm.i18n:pull" />
|
||||
<item itemvalue="npm.test" />
|
||||
<item itemvalue="npm.test:e2e" />
|
||||
<item itemvalue="npm.typecheck" />
|
||||
<item itemvalue="npm.i18n:sync" />
|
||||
<item itemvalue="npm.dev" />
|
||||
<item itemvalue="Vitest.generatePassword" />
|
||||
</list>
|
||||
<recent_temporary>
|
||||
<list>
|
||||
<item itemvalue="npm.dev" />
|
||||
<item itemvalue="npm.i18n:sync" />
|
||||
<item itemvalue="Vitest.generatePassword" />
|
||||
<item itemvalue="npm.i18n:pull" />
|
||||
<item itemvalue="npm.i18n:extract" />
|
||||
<item itemvalue="npm.test:e2e" />
|
||||
<item itemvalue="npm.test" />
|
||||
<item itemvalue="npm.typecheck" />
|
||||
</list>
|
||||
</recent_temporary>
|
||||
</component>
|
||||
@ -580,78 +659,8 @@
|
||||
<workItem from="1753201599796" duration="4449000" />
|
||||
<workItem from="1753206561770" duration="119000" />
|
||||
<workItem from="1753206717510" duration="3599000" />
|
||||
</task>
|
||||
<task id="LOCAL-00201" summary="chore: rename from Omni Tools to OmniTools">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751630993003</created>
|
||||
<option name="number" value="00201" />
|
||||
<option name="presentableId" value="LOCAL-00201" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751630993003</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00202" summary="fix: tools by category page title">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751846877842</created>
|
||||
<option name="number" value="00202" />
|
||||
<option name="presentableId" value="LOCAL-00202" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751846877842</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00203" summary="chore: use scrollY">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751848478091</created>
|
||||
<option name="number" value="00203" />
|
||||
<option name="presentableId" value="LOCAL-00203" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751848478091</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00204" summary="chore: remove flip x and y">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751849423899</created>
|
||||
<option name="number" value="00204" />
|
||||
<option name="presentableId" value="LOCAL-00204" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751849423899</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00205" summary="fix: tsc">
|
||||
<option name="closed" value="true" />
|
||||
<created>1751850152784</created>
|
||||
<option name="number" value="00205" />
|
||||
<option name="presentableId" value="LOCAL-00205" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1751850152784</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00206" summary="chore: new logo and font">
|
||||
<option name="closed" value="true" />
|
||||
<created>1752022118195</created>
|
||||
<option name="number" value="00206" />
|
||||
<option name="presentableId" value="LOCAL-00206" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1752022118199</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00207" summary="chore: white logo">
|
||||
<option name="closed" value="true" />
|
||||
<created>1752022731608</created>
|
||||
<option name="number" value="00207" />
|
||||
<option name="presentableId" value="LOCAL-00207" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1752022731608</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00208" summary="chore: png icon">
|
||||
<option name="closed" value="true" />
|
||||
<created>1752023182341</created>
|
||||
<option name="number" value="00208" />
|
||||
<option name="presentableId" value="LOCAL-00208" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1752023182341</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00209" summary="fix: remove xml viewer">
|
||||
<option name="closed" value="true" />
|
||||
<created>1752023796004</created>
|
||||
<option name="number" value="00209" />
|
||||
<option name="presentableId" value="LOCAL-00209" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1752023796004</updated>
|
||||
<workItem from="1759497758761" duration="1012000" />
|
||||
<workItem from="1759502144651" duration="1106000" />
|
||||
</task>
|
||||
<task id="LOCAL-00210" summary="feat: convert to jpg">
|
||||
<option name="closed" value="true" />
|
||||
@ -973,7 +982,79 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1753210033390</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="250" />
|
||||
<task id="LOCAL-00250" summary="feat: remove temperature conversion from generic-calc This commit removes the temperature conversion tool from the generic-calc tool. This is because the tool was causing issues. The following files were modified: - src/pages/tools/number/generic-calc/data/index.ts - src/pages/tools/number/generic-calc/data/temperature.ts - package.json - .idea/workspace.xml">
|
||||
<option name="closed" value="true" />
|
||||
<created>1759439927012</created>
|
||||
<option name="number" value="00250" />
|
||||
<option name="presentableId" value="LOCAL-00250" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1759439927012</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00251" summary="feat: Remove onnxruntime-web dependency (main) This commit removes the onnxruntime-web package from package.json. ```">
|
||||
<option name="closed" value="true" />
|
||||
<created>1759441368519</created>
|
||||
<option name="number" value="00251" />
|
||||
<option name="presentableId" value="LOCAL-00251" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1759441368519</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00252" summary="fix: onnxruntime-web version">
|
||||
<option name="closed" value="true" />
|
||||
<created>1759441908841</created>
|
||||
<option name="number" value="00252" />
|
||||
<option name="presentableId" value="LOCAL-00252" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1759441908841</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00253" summary="feat: Upgrade Node.js versions in CI (main) This commit updates the Node.js versions used in the CI workflows to version 20. This ensures that the CI environment uses a more up-to-date and supported version of Node.js.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1759442128298</created>
|
||||
<option name="number" value="00253" />
|
||||
<option name="presentableId" value="LOCAL-00253" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1759442128299</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00254" summary="feat: Upgrade onnxruntime-web and onnxruntime-common (main) Upgrades onnxruntime-web and onnxruntime-common to versions 1.23.0. This includes updates to dependencies and related packages.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1759442775014</created>
|
||||
<option name="number" value="00254" />
|
||||
<option name="presentableId" value="LOCAL-00254" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1759442775014</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00255" summary="chore: Replace "Buy me a coffee" with "Hire me" (main) This commit replaces the "Buy me a coffee" button with a "Hire me" button in the navbar. It also updates the corresponding translation in the `en/translation.json` file. The icon has been changed to a job search icon, and the link points to a Google Drive document.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1759502472647</created>
|
||||
<option name="number" value="00255" />
|
||||
<option name="presentableId" value="LOCAL-00255" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1759502472647</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00256" summary="feat: Update workspace and translation files (main) This commit includes the following changes: - Updated the workspace configuration in .idea/workspace.xml to reflect the recent npm script execution. - Modified the English translation file (translation.json) by reordering the "navbar" keys.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1759502707923</created>
|
||||
<option name="number" value="00256" />
|
||||
<option name="presentableId" value="LOCAL-00256" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1759502707923</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00257" summary="feat: Update workspace and translation files (main) This commit includes the following changes: - Updated the workspace configuration in .idea/workspace.xml to reflect the recent npm script execution. - Modified the English translation file (translation.json) by reordering the "navbar" keys. - Updated translation files in other languages with "hireMe".">
|
||||
<option name="closed" value="true" />
|
||||
<created>1759503018626</created>
|
||||
<option name="number" value="00257" />
|
||||
<option name="presentableId" value="LOCAL-00257" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1759503018627</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00258" summary="fix(i18n): Correct "hireMe" translation in navbar This commit corrects the translation of "hireMe" in the navigation bar across all supported languages. The order of the elements was also fixed to be consistent.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1759503051936</created>
|
||||
<option name="number" value="00258" />
|
||||
<option name="presentableId" value="LOCAL-00258" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1759503051936</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="259" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@ -996,6 +1077,11 @@
|
||||
<entry key="Branch">
|
||||
<value>
|
||||
<list>
|
||||
<RecentGroup>
|
||||
<option name="FILTER_VALUES">
|
||||
<option value="origin/main" />
|
||||
</option>
|
||||
</RecentGroup>
|
||||
<RecentGroup>
|
||||
<option name="FILTER_VALUES">
|
||||
<option value="origin/examples" />
|
||||
@ -1010,7 +1096,19 @@
|
||||
<map>
|
||||
<entry key="MAIN">
|
||||
<value>
|
||||
<State />
|
||||
<State>
|
||||
<option name="FILTERS">
|
||||
<map>
|
||||
<entry key="branch">
|
||||
<value>
|
||||
<list>
|
||||
<option value="origin/main" />
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</State>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
@ -1020,32 +1118,32 @@
|
||||
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
|
||||
<option name="CHECK_NEW_TODO" value="false" />
|
||||
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
||||
<MESSAGE value="chore: i18n in meta" />
|
||||
<MESSAGE value="chore: add i18n to meta script" />
|
||||
<MESSAGE value="chore: bundle translations at build time" />
|
||||
<MESSAGE value="fix: tsc" />
|
||||
<MESSAGE value="chore: remove unnecessary" />
|
||||
<MESSAGE value="chore: saveMissing" />
|
||||
<MESSAGE value="fix: translation related behaviors" />
|
||||
<MESSAGE value="feat: password generator to test translation" />
|
||||
<MESSAGE value="docs: translation docs" />
|
||||
<MESSAGE value="fix: translations" />
|
||||
<MESSAGE value="chore: delete unused i18n json files" />
|
||||
<MESSAGE value="fix: create-tool.mjs to use i18n object" />
|
||||
<MESSAGE value="fix: show Use this tool only if medium breakpoint" />
|
||||
<MESSAGE value="chore: sync locales" />
|
||||
<MESSAGE value="fix: i18n" />
|
||||
<MESSAGE value="chore: remove prebuild" />
|
||||
<MESSAGE value="fix: broken translations" />
|
||||
<MESSAGE value="fix: i18n tsc" />
|
||||
<MESSAGE value="chore: i18n pull dutch" />
|
||||
<MESSAGE value="chore: sync locize" />
|
||||
<MESSAGE value="feat: language browser detection" />
|
||||
<MESSAGE value="fix: misc" />
|
||||
<MESSAGE value="chore: show only necessary tags on a category" />
|
||||
<MESSAGE value="chore: CATEGORIES_USER_TYPES_MAPPINGS" />
|
||||
<MESSAGE value="chore: translate userTypes" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="chore: translate userTypes" />
|
||||
<MESSAGE value="feat: Upgrade Node.js versions in CI (main) This commit updates the Node.js versions used in the CI workflows to version 20. This ensures that the CI environment uses a more up-to-date and supported version of Node.js." />
|
||||
<MESSAGE value="feat" />
|
||||
<MESSAGE value="feat: Upgrade onnxruntime-web and onnxruntime-common (main) Up" />
|
||||
<MESSAGE value="feat: Upgrade onnxruntime-web and onnxruntime-common (main) Upgrades onnxruntime-web and onnxruntime-common to versions 1" />
|
||||
<MESSAGE value="feat: Upgrade onnxruntime-web and onnxruntime-common (main) Upgrades onnxruntime-web and onnxruntime-common to versions 1.23.0. This includes updates to dependencies and related packages. " />
|
||||
<MESSAGE value="feat: Upgrade onnxruntime-web and onnxruntime-common (main) Upgrades onnxruntime-web and onnxruntime-common to versions 1.23.0. This includes updates to dependencies and related packages." />
|
||||
<MESSAGE value="``` feat: Replace "Buy me a coffee" with "Hire me" (main) " />
|
||||
<MESSAGE value="``` feat: Replace "Buy me a coffee" with "Hire me" (main) This commit replaces the "Buy me a coffee" button with a "Hire" />
|
||||
<MESSAGE value="``` feat: Replace "Buy me a coffee" with "Hire me" (main) This commit replaces the "Buy me a coffee" button with a "Hire me" button in the navbar. It also updates the corresponding translation in the `en/translation.json` file. The icon has been changed to a" />
|
||||
<MESSAGE value="``` feat: Replace "Buy me a coffee" with "Hire me" (main) This commit replaces the "Buy me a coffee" button with a "Hire me" button in the navbar. It also updates the corresponding translation in the `en/translation.json` file. The icon has been changed to a job search icon, and the link points to a Google Drive document. ```" />
|
||||
<MESSAGE value="chore: Replace "Buy me a coffee" with "Hire me" (main) This commit replaces the "Buy me a coffee" button with a "Hire me" button in the navbar. It also updates the corresponding translation in the `en/translation.json` file. The icon has been changed to a job search icon, and the link points to a Google Drive document." />
|
||||
<MESSAGE value="``` feat: Update workspace and translation files (main) This commit includes the following changes:" />
|
||||
<MESSAGE value="``` feat: Update workspace and translation files (main) This commit includes the following changes: - Updated the workspace configuration in .idea/workspace.xml to reflect" />
|
||||
<MESSAGE value="``` feat: Update workspace and translation files (main) This commit includes the following changes: - Updated the workspace configuration in .idea/workspace.xml to reflect the recent npm script execution. - Modified the English translation file (translation.json) by reordering the "navbar" keys. ``` " />
|
||||
<MESSAGE value="feat: Update workspace and translation files (main) This commit includes the following changes: - Updated the workspace configuration in .idea/workspace.xml to reflect the recent npm script execution. - Modified the English translation file (translation.json) by reordering the "navbar" keys." />
|
||||
<MESSAGE value="``` feat: Update workspace and translation files (main) This commit includes the following" />
|
||||
<MESSAGE value="``` feat: Update workspace and translation files (main) This commit includes the following changes: - Updated the workspace configuration in .idea/workspace.xml to " />
|
||||
<MESSAGE value="``` feat: Update workspace and translation files (main) This commit includes the following changes: - Updated the workspace configuration in .idea/workspace.xml to reflect the recent npm script execution. - Modified the English translation file (translation.json) by reordering the "navbar" keys. -" />
|
||||
<MESSAGE value="``` feat: Update workspace and translation files (main) This commit includes the following changes: - Updated the workspace configuration in .idea/workspace.xml to reflect the recent npm script execution. - Modified the English translation file (translation.json) by reordering the "navbar" keys. - Updated translation files in other languages with "hireMe". ``` " />
|
||||
<MESSAGE value="feat: Update workspace and translation files (main) This commit includes the following changes: - Updated the workspace configuration in .idea/workspace.xml to reflect the recent npm script execution. - Modified the English translation file (translation.json) by reordering the "navbar" keys. - Updated translation files in other languages with "hireMe"." />
|
||||
<MESSAGE value="```" />
|
||||
<MESSAGE value="``` fix(i18n): Correct "hireMe" translation in navbar" />
|
||||
<MESSAGE value="``` fix(i18n): Correct "hireMe" translation in navbar This commit corrects the translation of "hireMe" in the navigation bar" />
|
||||
<MESSAGE value="``` fix(i18n): Correct "hireMe" translation in navbar This commit corrects the translation of "hireMe" in the navigation bar across all supported languages. The order of the elements was also fixed to be consistent. ``` " />
|
||||
<MESSAGE value="fix(i18n): Correct "hireMe" translation in navbar This commit corrects the translation of "hireMe" in the navigation bar across all supported languages. The order of the elements was also fixed to be consistent." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="fix(i18n): Correct "hireMe" translation in navbar This commit corrects the translation of "hireMe" in the navigation bar across all supported languages. The order of the elements was also fixed to be consistent." />
|
||||
</component>
|
||||
<component name="VgoProject">
|
||||
<integration-enabled>false</integration-enabled>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<a href="https://trendshift.io/repositories/13055" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13055" alt="iib0011%2Fomni-tools | Trendshift" style="width: 200px;" width="200"/></a>
|
||||
<br /><br />
|
||||
<a href="https://github.com/iib0011/omni-tools/releases">
|
||||
<img src="https://img.shields.io/badge/version-0.5.0-blue?style=for-the-badge" />
|
||||
<img src="https://img.shields.io/badge/version-0.6.0-blue?style=for-the-badge" />
|
||||
</a>
|
||||
<a href="https://hub.docker.com/r/iib0011/omni-tools">
|
||||
<img src="https://img.shields.io/docker/pulls/iib0011/omni-tools?style=for-the-badge&logo=docker" />
|
||||
|
||||
6660
package-lock.json
generated
6660
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -59,6 +59,7 @@
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jimp": "^0.22.12",
|
||||
"js-quantities": "^1.8.0",
|
||||
"jspdf": "^3.0.3",
|
||||
"jszip": "^3.10.1",
|
||||
"lint-staged": "^15.4.3",
|
||||
"locize": "^4.0.14",
|
||||
@ -68,6 +69,7 @@
|
||||
"nerdamer-prime": "^1.2.4",
|
||||
"notistack": "^3.0.1",
|
||||
"omggif": "^1.0.10",
|
||||
"onnxruntime-web": "1.21.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^5.2.133",
|
||||
"playwright": "^1.45.0",
|
||||
@ -81,6 +83,7 @@
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"styled-components": "^6.1.19",
|
||||
"tesseract.js": "^6.0.0",
|
||||
"type-fest": "^4.35.0",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
@ -99,6 +102,7 @@
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-helmet": "^6.1.11",
|
||||
"@types/trusted-types": "^1.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
@ -114,6 +118,7 @@
|
||||
"husky": "^9.0.11",
|
||||
"i18next-locize-backend": "^7.0.4",
|
||||
"locize-cli": "^10.1.1",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "3.1.1",
|
||||
"start-server-and-test": "^2.0.4",
|
||||
|
||||
@ -63,6 +63,11 @@
|
||||
"shortDescription": "Konvertieren Sie PDF in PNG-Bilder",
|
||||
"title": "PDF zu PNG"
|
||||
},
|
||||
"convertToPdf": {
|
||||
"title": "Bilder in PDF konvertieren",
|
||||
"description": "Verschiedene Bildformate (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) in PDF konvertieren, mit Optionen zum Skalieren des Bildes und zur Wahl der Seitenorientierung.",
|
||||
"shortDescription": "Bilder in PDF mit Skalierungs- und Orientierungskontrolle konvertieren"
|
||||
},
|
||||
"protectPdf": {
|
||||
"description": "Fügen Sie Ihren PDF-Dateien sicher in Ihrem Browser einen Passwortschutz hinzu",
|
||||
"shortDescription": "PDF-Dateien sicher mit einem Passwort schützen",
|
||||
|
||||
@ -121,6 +121,7 @@
|
||||
},
|
||||
"navbar": {
|
||||
"buyMeACoffee": "Kauf mir einen Kaffee",
|
||||
"hireMe": "Stellen Sie mich ein",
|
||||
"home": "Heim",
|
||||
"tools": "Werkzeuge"
|
||||
},
|
||||
|
||||
@ -63,6 +63,11 @@
|
||||
"shortDescription": "Convert PDF into PNG images",
|
||||
"title": "PDF to PNG"
|
||||
},
|
||||
"convertToPdf": {
|
||||
"title": "Images to PDF",
|
||||
"description": "Convert various image formats (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) to PDF, with options to scale the image and choose page orientation.",
|
||||
"shortDescription": "Convert images to PDF with scale and orientation control"
|
||||
},
|
||||
"protectPdf": {
|
||||
"description": "Add password protection to your PDF files securely in your browser",
|
||||
"shortDescription": "Password protect PDF files securely",
|
||||
|
||||
@ -307,5 +307,21 @@
|
||||
"shortDescription": "Quickly URL-escape a string.",
|
||||
"title": "String URL encoder"
|
||||
}
|
||||
},
|
||||
"unicode": {
|
||||
"title": "Unicode Encoder / Decoder",
|
||||
"inputTitle": "Input",
|
||||
"resultTitle": "Processed Output",
|
||||
"optionsTitle": "Mode",
|
||||
"caseOptionsTitle": "Case Options",
|
||||
"encode": "Encode",
|
||||
"decode": "Decode",
|
||||
"uppercase": "Uppercase Hex",
|
||||
"description": "Convert text to Unicode escape sequences or decode them back to readable text.",
|
||||
"shortDescription": "Encode or decode text using Unicode escape sequences.",
|
||||
"toolInfo": {
|
||||
"title": "Unicode Encoder / Decoder",
|
||||
"description": "This tool lets you convert plain text into Unicode escape sequences (e.g., \\uXXXX) and decode Unicode escape sequences back into standard text. You can also choose whether the hexadecimal output is formatted in uppercase or lowercase when encoding."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,5 +113,11 @@
|
||||
"zeroPaddingDescription": "Make all time components always be two digits wide.",
|
||||
"zeroPrintDescription": "Display the dropped parts as zero values \"00\".",
|
||||
"zeroPrintTruncatedParts": "Zero-print Truncated Parts"
|
||||
},
|
||||
"convertTimeToDecimal": {
|
||||
"title": "Convert time to decimal",
|
||||
"description": "Convert a formatted time duration (HH:MM:SS) into a decimal hour value.",
|
||||
"shortDescription": "Convert human time to decimal time",
|
||||
"longDescription": "Convert a formatted time string (HH:MM:SS or HH:MM) into its decimal-hour equivalent. Hours can be any positive number, while minutes and seconds accept values from 0-59 and can be single or double digits (e.g., '12:5' or '12:05'). This function interprets hours, minutes, and seconds, then calculates the total duration as a single decimal value. It is useful for productivity tracking, payroll calculations, time-based billing, data analysis, or any workflow that requires converting human-readable time into a numerical format that can be easily summed, compared, or processed. For example, '03:26:00' becomes 3.43 hours, and '12:5' becomes 12.08 hours."
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,6 +133,7 @@
|
||||
},
|
||||
"navbar": {
|
||||
"buyMeACoffee": "Buy me a coffee",
|
||||
"hireMe": "Hire me",
|
||||
"home": "Home",
|
||||
"tools": "Tools"
|
||||
},
|
||||
|
||||
@ -63,6 +63,11 @@
|
||||
"shortDescription": "Convertir PDF a imágenes PNG",
|
||||
"title": "PDF a PNG"
|
||||
},
|
||||
"convertToPdf": {
|
||||
"title": "Imágenes a PDF",
|
||||
"description": "Convertir varios formatos de imagen (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) a PDF, con opciones para escalar la imagen y elegir la orientación de la página.",
|
||||
"shortDescription": "Convertir imágenes a PDF con control de escala y orientación"
|
||||
},
|
||||
"protectPdf": {
|
||||
"description": "Agregue protección con contraseña a sus archivos PDF de forma segura en su navegador",
|
||||
"shortDescription": "Proteger con contraseña los archivos PDF de forma segura",
|
||||
|
||||
@ -122,7 +122,8 @@
|
||||
"navbar": {
|
||||
"buyMeACoffee": "Invítame a un café",
|
||||
"home": "Hogar",
|
||||
"tools": "Herramientas"
|
||||
"tools": "Herramientas",
|
||||
"hireMe": "Contrátame"
|
||||
},
|
||||
"number": {
|
||||
"generate": {
|
||||
|
||||
@ -63,6 +63,11 @@
|
||||
"shortDescription": "Convertir des PDF en images PNG",
|
||||
"title": "PDF en PNG"
|
||||
},
|
||||
"convertToPdf": {
|
||||
"title": "Images vers PDF",
|
||||
"description": "Convertir divers formats d'image (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) en PDF, avec des options pour redimensionner l'image et choisir l'orientation de la page.",
|
||||
"shortDescription": "Convertir des images en PDF avec contrôle de taille et d'orientation"
|
||||
},
|
||||
"protectPdf": {
|
||||
"description": "Ajoutez une protection par mot de passe à vos fichiers PDF en toute sécurité dans votre navigateur",
|
||||
"shortDescription": "Protégez les fichiers PDF en toute sécurité avec un mot de passe",
|
||||
|
||||
@ -121,6 +121,7 @@
|
||||
},
|
||||
"navbar": {
|
||||
"buyMeACoffee": "Offre-moi un café",
|
||||
"hireMe": "Embauchez-moi",
|
||||
"home": "Maison",
|
||||
"tools": "Outils"
|
||||
},
|
||||
|
||||
@ -82,6 +82,11 @@
|
||||
"shortDescription": "PDF को PNG छवियों में परिवर्तित करें",
|
||||
"title": "पीडीएफ से पीएनजी"
|
||||
},
|
||||
"convertToPdf": {
|
||||
"title": "PDF में छवि बदलें",
|
||||
"description": "विभिन्न छवि प्रारूपों (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) को PDF में परिवर्तित करें और छवि का आकार और पृष्ठ की अभिविन्यास समायोजित करें।",
|
||||
"shortDescription": "छवियों को PDF में परिवर्तित करें, आकार और अभिविन्यास समायोजित करने योग्य"
|
||||
},
|
||||
"protectPdf": {
|
||||
"allowCopying": "कॉपी करने की अनुमति दें",
|
||||
"allowModification": "संशोधन की अनुमति दें",
|
||||
|
||||
@ -121,6 +121,7 @@
|
||||
},
|
||||
"navbar": {
|
||||
"buyMeACoffee": "मुझे कॉफी खरीदें",
|
||||
"hireMe": "मुझे नौकरी दें",
|
||||
"home": "होम",
|
||||
"tools": "टूल्स"
|
||||
},
|
||||
|
||||
@ -63,6 +63,11 @@
|
||||
"shortDescription": "PDFをPNG画像に変換する",
|
||||
"title": "PDFからPNGへ"
|
||||
},
|
||||
"convertToPdf": {
|
||||
"description": "様々な画像形式(PNG、GIF、JPG、TIF、PSD、SVG、WEBP、HEIC、RAW)をPDFに変換します。画像サイズとページの向きを調整できます。",
|
||||
"shortDescription": "画像を PDF に変換し、画像のサイズやページの向きを調整できます。",
|
||||
"title": "画像を PDF に変換"
|
||||
},
|
||||
"protectPdf": {
|
||||
"description": "ブラウザで安全にPDFファイルにパスワード保護を追加します",
|
||||
"shortDescription": "PDFファイルをパスワードで安全に保護する",
|
||||
|
||||
@ -121,6 +121,7 @@
|
||||
},
|
||||
"navbar": {
|
||||
"buyMeACoffee": "コーヒーを買ってください",
|
||||
"hireMe": "私を雇ってください",
|
||||
"home": "家",
|
||||
"tools": "ツール"
|
||||
},
|
||||
|
||||
@ -63,6 +63,11 @@
|
||||
"shortDescription": "PDF naar PNG-afbeeldingen converteren",
|
||||
"title": "PDF naar PNG"
|
||||
},
|
||||
"convertToPdf": {
|
||||
"title": "Afbeeldingen naar PDF",
|
||||
"description": "Verschillende afbeeldingsformaten (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) naar PDF converteren, met opties om de afbeelding te schalen en de paginaoriëntatie te kiezen.",
|
||||
"shortDescription": "Afbeeldingen naar PDF converteren met schaal- en oriëntatiecontrole"
|
||||
},
|
||||
"protectPdf": {
|
||||
"description": "Voeg wachtwoordbeveiliging toe aan uw PDF-bestanden veilig in uw browser",
|
||||
"shortDescription": "PDF-bestanden veilig met een wachtwoord beveiligen",
|
||||
|
||||
@ -121,6 +121,7 @@
|
||||
},
|
||||
"navbar": {
|
||||
"buyMeACoffee": "Koop me een koffie",
|
||||
"hireMe": "Neem mij aan",
|
||||
"home": "Thuis",
|
||||
"tools": "Hulpmiddelen"
|
||||
},
|
||||
|
||||
@ -63,6 +63,11 @@
|
||||
"shortDescription": "Converter PDF em imagens PNG",
|
||||
"title": "PDF para PNG"
|
||||
},
|
||||
"convertToPdf": {
|
||||
"title": "Imagens para PDF",
|
||||
"description": "Converter vários formatos de imagem (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) em PDF, com opções para redimensionar a imagem e escolher a orientação da página.",
|
||||
"shortDescription": "Converter imagens em PDF com controle de escala e orientação"
|
||||
},
|
||||
"protectPdf": {
|
||||
"description": "Adicione proteção por senha aos seus arquivos PDF com segurança no seu navegador",
|
||||
"shortDescription": "Proteja arquivos PDF com senha com segurança",
|
||||
|
||||
@ -121,6 +121,7 @@
|
||||
},
|
||||
"navbar": {
|
||||
"buyMeACoffee": "Compre-me um café",
|
||||
"hireMe": "Me contrate",
|
||||
"home": "Lar",
|
||||
"tools": "Ferramentas"
|
||||
},
|
||||
|
||||
@ -63,6 +63,11 @@
|
||||
"shortDescription": "Конвертировать PDF в изображения PNG",
|
||||
"title": "PDF в PNG"
|
||||
},
|
||||
"convertToPdf": {
|
||||
"title": "Изображения в PDF",
|
||||
"description": "Преобразовать различные форматы изображений (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) в PDF с возможностью масштабирования изображения и выбора ориентации страницы.",
|
||||
"shortDescription": "Преобразовать изображения в PDF с управлением масштабом и ориентацией"
|
||||
},
|
||||
"protectPdf": {
|
||||
"description": "Добавьте надежную защиту паролем для ваших PDF-файлов в браузере.",
|
||||
"shortDescription": "Надежная защита паролем PDF-файлов",
|
||||
|
||||
@ -35,12 +35,12 @@
|
||||
"title": "Инструменты для работы с изображениями"
|
||||
},
|
||||
"json": {
|
||||
"description": "Инструменты для работы со структурами данных JSON — упрощайте и минимизируйте объекты JSON, выравнивайте массивы JSON, преобразуйте значения JSON в строки, анализируйте данные и многое другое.",
|
||||
"title": "Инструменты JSON"
|
||||
"description": "Инструменты для работы со структурой данных JSON — упрощайте и минимизируйте объекты JSON, выравнивайте массивы JSON, преобразуйте значения JSON в строки, анализируйте данные и многое другое.",
|
||||
"title": "JSON-инструменты"
|
||||
},
|
||||
"list": {
|
||||
"description": "Инструменты для работы со списками — сортировка, обратный порядок, рандомизация списков, поиск уникальных и повторяющихся элементов списка, изменение разделителей элементов списка и многое другое.",
|
||||
"title": "Список инструментов"
|
||||
"title": "Инструменты для работы со списками"
|
||||
},
|
||||
"number": {
|
||||
"description": "Инструменты для работы с числами — генерация числовых последовательностей, преобразование чисел в слова и слов в числа, сортировка, округление, разложение чисел на множители и многое другое.",
|
||||
@ -52,18 +52,18 @@
|
||||
},
|
||||
"png": {
|
||||
"description": "Инструменты для работы с изображениями PNG — конвертируйте PNG в JPG, создавайте прозрачные PNG, изменяйте цвета PNG, обрезайте, поворачивайте, изменяйте размер PNG и многое другое.",
|
||||
"title": "Инструменты PNG"
|
||||
"title": "PNG-инструменты"
|
||||
},
|
||||
"seeAll": "Смотреть все {{title}}",
|
||||
"seeAll": "Посмотреть {{title}}",
|
||||
"string": {
|
||||
"description": "Инструменты для работы с текстом — преобразование текста в изображения, поиск и замена текста, разделение текста на фрагменты, объединение текстовых строк, повтор текста и многое другое.",
|
||||
"title": "Текстовые инструменты"
|
||||
},
|
||||
"time": {
|
||||
"description": "Инструменты для работы со временем и датой — расчет разницы во времени, преобразование часовых поясов, форматирование дат, создание последовательностей дат и многое другое.",
|
||||
"title": "Инструменты времени"
|
||||
"title": "Инструменты для работы со временем"
|
||||
},
|
||||
"try": "Пытаться {{title}}",
|
||||
"try": "Запустить {{title}}",
|
||||
"video": {
|
||||
"description": "Инструменты для работы с видео — извлечение кадров из видео, создание GIF-файлов из видео, конвертация видео в различные форматы и многое другое.",
|
||||
"title": "Видео инструменты"
|
||||
@ -81,20 +81,20 @@
|
||||
}
|
||||
},
|
||||
"hero": {
|
||||
"brand": "ОмниИнструменты",
|
||||
"brand": "OmniTools",
|
||||
"description": "Повысьте свою производительность с OmniTools — лучшим набором инструментов для быстрого решения задач! Получите доступ к тысячам удобных утилит для редактирования изображений, текста, списков и данных — и всё это прямо в браузере.",
|
||||
"examples": {
|
||||
"calculateNumberSum": "Вычислить сумму чисел",
|
||||
"changeGifSpeed": "Изменить скорость GIF-анимации",
|
||||
"compressPng": "Сжать PNG",
|
||||
"createTransparentImage": "Создать прозрачное изображение",
|
||||
"prettifyJson": "Упрощение JSON",
|
||||
"prettifyJson": "Отформатировать JSON",
|
||||
"sortList": "Сортировать список",
|
||||
"splitPdf": "Разделить PDF",
|
||||
"splitText": "Разделить текст",
|
||||
"trimVideo": "Обрезать видео"
|
||||
},
|
||||
"searchPlaceholder": "Искать все инструменты",
|
||||
"searchPlaceholder": "Найти инструмент…",
|
||||
"title": "Выполняйте задачи быстро с помощью"
|
||||
},
|
||||
"inputFooter": {
|
||||
@ -110,7 +110,7 @@
|
||||
},
|
||||
"reverse": {
|
||||
"description": "Это очень простое браузерное приложение, которое выводит все элементы списка в обратном порядке. Элементы ввода можно разделять любым символом, и вы также можете изменить разделитель для элементов перевёрнутого списка.",
|
||||
"name": "Обеспечить регресс",
|
||||
"name": "Вывести в обратном порядке",
|
||||
"shortDescription": "Быстро перевернуть список"
|
||||
},
|
||||
"sort": {
|
||||
@ -121,6 +121,7 @@
|
||||
},
|
||||
"navbar": {
|
||||
"buyMeACoffee": "Купи мне кофе",
|
||||
"hireMe": "Нанять меня",
|
||||
"home": "Дом",
|
||||
"tools": "Инструменты"
|
||||
},
|
||||
|
||||
@ -63,6 +63,11 @@
|
||||
"shortDescription": "将 PDF 转换为 PNG 图像",
|
||||
"title": "PDF 转 PNG"
|
||||
},
|
||||
"convertToPdf": {
|
||||
"title": "将图像转换为 PDF",
|
||||
"description": "将各种图像格式(PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW)转换为 PDF,并可调整图像大小和页面方向。",
|
||||
"shortDescription": "将图像转换为 PDF,可调整大小和方向"
|
||||
},
|
||||
"protectPdf": {
|
||||
"description": "在浏览器中安全地为 PDF 文件添加密码保护",
|
||||
"shortDescription": "使用密码安全地保护 PDF 文件",
|
||||
|
||||
@ -121,6 +121,7 @@
|
||||
},
|
||||
"navbar": {
|
||||
"buyMeACoffee": "请我喝杯咖啡",
|
||||
"hireMe": "雇用我",
|
||||
"home": "家",
|
||||
"tools": "工具"
|
||||
},
|
||||
|
||||
@ -127,7 +127,10 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
></iframe>,
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.open('https://buymeacoffee.com/iib0011', '_blank');
|
||||
window.open(
|
||||
'https://drive.google.com/file/d/1-r9-rDYnDJic9dnDywKTAsueehIAVp5F/view?usp=sharing',
|
||||
'_blank'
|
||||
);
|
||||
}}
|
||||
sx={{ borderRadius: '100px' }}
|
||||
variant={'contained'}
|
||||
@ -135,11 +138,11 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
<Icon
|
||||
style={{ cursor: 'pointer' }}
|
||||
fontSize={25}
|
||||
icon={'mdi:heart-outline'}
|
||||
icon={'hugeicons:job-search'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t('navbar.buyMeACoffee')}
|
||||
{t('navbar.hireMe')}
|
||||
</Button>
|
||||
];
|
||||
const drawerList = (
|
||||
|
||||
@ -41,7 +41,7 @@ export async function changeAudioSpeed(
|
||||
const outputName = `output.${outputFormat}`;
|
||||
await ffmpeg.writeFile(fileName, await fetchFile(input));
|
||||
const audioFilter = computeAudioFilter(newSpeed);
|
||||
let args = ['-i', fileName, '-filter:a', audioFilter];
|
||||
const args = ['-i', fileName, '-filter:a', audioFilter];
|
||||
if (outputFormat === 'mp3') {
|
||||
args.push('-b:a', '192k', '-f', 'mp3', outputName);
|
||||
} else if (outputFormat === 'aac') {
|
||||
@ -64,7 +64,7 @@ export async function changeAudioSpeed(
|
||||
let mimeType = 'audio/mp3';
|
||||
if (outputFormat === 'aac') mimeType = 'audio/aac';
|
||||
if (outputFormat === 'wav') mimeType = 'audio/wav';
|
||||
const blob = new Blob([data], { type: mimeType });
|
||||
const blob = new Blob([data as any], { type: mimeType });
|
||||
const newFile = new File(
|
||||
[blob],
|
||||
fileName.replace(/\.[^/.]+$/, `-${newSpeed}x.${outputFormat}`),
|
||||
|
||||
@ -57,7 +57,7 @@ export async function extractAudioFromVideo(
|
||||
|
||||
return new File(
|
||||
[
|
||||
new Blob([extractedAudio], {
|
||||
new Blob([extractedAudio as any], {
|
||||
type: `audio/${configuredOutputAudioFormat}`
|
||||
})
|
||||
],
|
||||
|
||||
@ -105,7 +105,7 @@ export async function mergeAudioFiles(
|
||||
|
||||
return new File(
|
||||
[
|
||||
new Blob([mergedAudio], {
|
||||
new Blob([mergedAudio as any], {
|
||||
type: mimeType
|
||||
})
|
||||
],
|
||||
|
||||
@ -98,7 +98,7 @@ export async function trimAudio(
|
||||
|
||||
return new File(
|
||||
[
|
||||
new Blob([trimmedAudio], {
|
||||
new Blob([trimmedAudio as any], {
|
||||
type: mimeType
|
||||
})
|
||||
],
|
||||
|
||||
@ -40,7 +40,7 @@ const initialValues: InitialValuesType = {
|
||||
// WiFi
|
||||
wifiSsid: '',
|
||||
wifiPassword: '',
|
||||
wifiEncryption: 'WPA/WPA2',
|
||||
wifiEncryption: 'WPA',
|
||||
|
||||
// vCard
|
||||
vCardName: '',
|
||||
@ -353,7 +353,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
|
||||
label="Encryption Type"
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value="WPA/WPA2">WPA/WPA2</MenuItem>
|
||||
<MenuItem value="WPA">WPA</MenuItem>
|
||||
<MenuItem value="WEP">WEP</MenuItem>
|
||||
<MenuItem value="None">None</MenuItem>
|
||||
</TextField>
|
||||
|
||||
@ -7,7 +7,7 @@ export type QRCodeType =
|
||||
| 'WiFi'
|
||||
| 'vCard';
|
||||
|
||||
export type WifiEncryptionType = 'WPA/WPA2' | 'WEP' | 'None';
|
||||
export type WifiEncryptionType = 'WPA' | 'WEP' | 'None';
|
||||
|
||||
export interface InitialValuesType {
|
||||
qrCodeType: QRCodeType;
|
||||
|
||||
@ -148,7 +148,7 @@ export const processImage = async (
|
||||
const data = await ffmpeg.readFile('output.gif');
|
||||
|
||||
// Create a new File object
|
||||
return new File([data], file.name, { type: 'image/gif' });
|
||||
return new File([data as any], file.name, { type: 'image/gif' });
|
||||
} catch (error) {
|
||||
console.error('Error processing GIF with FFmpeg:', error);
|
||||
// Fall back to canvas method if FFmpeg processing fails
|
||||
|
||||
@ -66,7 +66,7 @@ export const processImage = async (
|
||||
|
||||
// Read the output file
|
||||
const data = await ffmpeg.readFile('output.' + file.name.split('.').pop());
|
||||
return new File([data], file.name, { type: file.type });
|
||||
return new File([data as any], file.name, { type: file.type });
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error);
|
||||
return null;
|
||||
|
||||
@ -3,6 +3,7 @@ import voltageDropInWire from './voltageDropInWire';
|
||||
import sphereArea from './sphereArea';
|
||||
import sphereVolume from './sphereVolume';
|
||||
import slackline from './slackline';
|
||||
|
||||
export default [
|
||||
ohmslaw,
|
||||
voltageDropInWire,
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import ConvertToPdf from './index';
|
||||
import { vi } from 'vitest';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
describe('ConvertToPdf', () => {
|
||||
it('renders with default state values (full, portrait hidden, no scale shown)', () => {
|
||||
render(<ConvertToPdf title="Test PDF" />);
|
||||
|
||||
expect(screen.getByLabelText(/Full Size \(Same as Image\)/i)).toBeChecked();
|
||||
|
||||
expect(screen.queryByLabelText(/A4 Page/i)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/Portrait/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Scale image:/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches to A4 page type and shows orientation and scale', () => {
|
||||
render(<ConvertToPdf title="Test PDF" />);
|
||||
|
||||
const a4Option = screen.getByLabelText(/A4 Page/i);
|
||||
fireEvent.click(a4Option);
|
||||
expect(a4Option).toBeChecked();
|
||||
|
||||
expect(screen.getByLabelText(/Portrait/i)).toBeChecked();
|
||||
expect(screen.getByText(/Scale image:\s*100%/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates scale when slider moves (after switching to A4)', () => {
|
||||
render(<ConvertToPdf title="Test PDF" />);
|
||||
|
||||
fireEvent.click(screen.getByLabelText(/A4 Page/i));
|
||||
|
||||
const slider = screen.getByRole('slider');
|
||||
fireEvent.change(slider, { target: { value: 80 } });
|
||||
|
||||
expect(screen.getByText(/Scale image:\s*80%/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
159
src/pages/tools/pdf/convert-to-pdf/index.tsx
Normal file
159
src/pages/tools/pdf/convert-to-pdf/index.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import {
|
||||
Box,
|
||||
Slider,
|
||||
Typography,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
Stack
|
||||
} from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolImageInput from 'components/input/ToolImageInput';
|
||||
import ToolFileResult from 'components/result/ToolFileResult';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { FormValues, Orientation, PageType, initialValues } from './types';
|
||||
import { buildPdf } from './service';
|
||||
|
||||
const initialFormValues: FormValues = initialValues;
|
||||
|
||||
export default function ConvertToPdf({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [imageSize, setImageSize] = useState<{
|
||||
widthMm: number;
|
||||
heightMm: number;
|
||||
widthPx: number;
|
||||
heightPx: number;
|
||||
} | null>(null);
|
||||
|
||||
const compute = async (values: FormValues) => {
|
||||
if (!input) return;
|
||||
const { pdfFile, imageSize } = await buildPdf({
|
||||
file: input,
|
||||
pageType: values.pageType,
|
||||
orientation: values.orientation,
|
||||
scale: values.scale
|
||||
});
|
||||
setResult(pdfFile);
|
||||
setImageSize(imageSize);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolContent<FormValues, File | null>
|
||||
title={title}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
initialValues={initialFormValues}
|
||||
compute={compute}
|
||||
inputComponent={
|
||||
<Box>
|
||||
<ToolImageInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={[
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/tiff',
|
||||
'image/gif',
|
||||
'image/heic',
|
||||
'image/heif',
|
||||
'image/x-adobe-dng',
|
||||
'image/x-canon-cr2',
|
||||
'image/x-nikon-nef',
|
||||
'image/x-sony-arw',
|
||||
'image/vnd.adobe.photoshop'
|
||||
]}
|
||||
title="Input Image"
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
getGroups={({ values, updateField }) => {
|
||||
return [
|
||||
{
|
||||
title: '',
|
||||
component: (
|
||||
<Stack spacing={4}>
|
||||
<Box>
|
||||
<Typography variant="h6">PDF Type</Typography>
|
||||
<RadioGroup
|
||||
row
|
||||
value={values.pageType}
|
||||
onChange={(e) =>
|
||||
updateField('pageType', e.target.value as PageType)
|
||||
}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="full"
|
||||
control={<Radio />}
|
||||
label="Full Size (Same as Image)"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="a4"
|
||||
control={<Radio />}
|
||||
label="A4 Page"
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
{values.pageType === 'full' && imageSize && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Image size: {imageSize.widthMm.toFixed(1)} ×{' '}
|
||||
{imageSize.heightMm.toFixed(1)} mm ({imageSize.widthPx} ×{' '}
|
||||
{imageSize.heightPx} px)
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{values.pageType === 'a4' && (
|
||||
<Box>
|
||||
<Typography variant="h6">Orientation</Typography>
|
||||
<RadioGroup
|
||||
row
|
||||
value={values.orientation}
|
||||
onChange={(e) =>
|
||||
updateField(
|
||||
'orientation',
|
||||
e.target.value as Orientation
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="portrait"
|
||||
control={<Radio />}
|
||||
label="Portrait (Vertical)"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="landscape"
|
||||
control={<Radio />}
|
||||
label="Landscape (Horizontal)"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{values.pageType === 'a4' && (
|
||||
<Box>
|
||||
<Typography variant="h6">Scale</Typography>
|
||||
<Typography>Scale image: {values.scale}%</Typography>
|
||||
<Slider
|
||||
value={values.scale}
|
||||
onChange={(_, v) => updateField('scale', v as number)}
|
||||
min={10}
|
||||
max={100}
|
||||
step={1}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
] as const;
|
||||
}}
|
||||
resultComponent={
|
||||
<ToolFileResult title="Output PDF" value={result} extension="pdf" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
32
src/pages/tools/pdf/convert-to-pdf/meta.ts
Normal file
32
src/pages/tools/pdf/convert-to-pdf/meta.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('pdf', {
|
||||
i18n: {
|
||||
name: 'pdf:convertToPdf.title',
|
||||
description: 'pdf:convertToPdf.description',
|
||||
shortDescription: 'pdf:convertToPdf.shortDescription'
|
||||
},
|
||||
|
||||
path: 'convert-to-pdf',
|
||||
icon: 'ph:file-pdf-thin',
|
||||
|
||||
keywords: [
|
||||
'convert',
|
||||
'pdf',
|
||||
'image',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif',
|
||||
'tiff',
|
||||
'webp',
|
||||
'heic',
|
||||
'raw',
|
||||
'psd',
|
||||
'svg',
|
||||
'quality',
|
||||
'compression'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
80
src/pages/tools/pdf/convert-to-pdf/service.ts
Normal file
80
src/pages/tools/pdf/convert-to-pdf/service.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import jsPDF from 'jspdf';
|
||||
import { Orientation, PageType, ImageSize } from './types';
|
||||
|
||||
export interface ComputeOptions {
|
||||
file: File;
|
||||
pageType: PageType;
|
||||
orientation: Orientation;
|
||||
scale: number; // 10..100 (only applied for A4)
|
||||
}
|
||||
|
||||
export interface ComputeResult {
|
||||
pdfFile: File;
|
||||
imageSize: ImageSize;
|
||||
}
|
||||
|
||||
export async function buildPdf({
|
||||
file,
|
||||
pageType,
|
||||
orientation,
|
||||
scale
|
||||
}: ComputeOptions): Promise<ComputeResult> {
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(file);
|
||||
|
||||
try {
|
||||
await img.decode();
|
||||
|
||||
const pxToMm = (px: number) => px * 0.264583;
|
||||
const imgWidthMm = pxToMm(img.width);
|
||||
const imgHeightMm = pxToMm(img.height);
|
||||
|
||||
const pdf =
|
||||
pageType === 'full'
|
||||
? new jsPDF({
|
||||
orientation: imgWidthMm > imgHeightMm ? 'landscape' : 'portrait',
|
||||
unit: 'mm',
|
||||
format: [imgWidthMm, imgHeightMm]
|
||||
})
|
||||
: new jsPDF({
|
||||
orientation,
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
});
|
||||
|
||||
pdf.setDisplayMode('fullwidth');
|
||||
|
||||
const pageWidth = pdf.internal.pageSize.getWidth();
|
||||
const pageHeight = pdf.internal.pageSize.getHeight();
|
||||
|
||||
const widthRatio = pageWidth / img.width;
|
||||
const heightRatio = pageHeight / img.height;
|
||||
const fitScale = Math.min(widthRatio, heightRatio);
|
||||
|
||||
const finalWidth =
|
||||
pageType === 'full' ? pageWidth : img.width * fitScale * (scale / 100);
|
||||
|
||||
const finalHeight =
|
||||
pageType === 'full' ? pageHeight : img.height * fitScale * (scale / 100);
|
||||
|
||||
const x = pageType === 'full' ? 0 : (pageWidth - finalWidth) / 2;
|
||||
const y = pageType === 'full' ? 0 : (pageHeight - finalHeight) / 2;
|
||||
|
||||
pdf.addImage(img, 'JPEG', x, y, finalWidth, finalHeight);
|
||||
|
||||
const blob = pdf.output('blob');
|
||||
const fileName = file.name.replace(/\.[^/.]+$/, '') + '.pdf';
|
||||
|
||||
return {
|
||||
pdfFile: new File([blob], fileName, { type: 'application/pdf' }),
|
||||
imageSize: {
|
||||
widthMm: imgWidthMm,
|
||||
heightMm: imgHeightMm,
|
||||
widthPx: img.width,
|
||||
heightPx: img.height
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
URL.revokeObjectURL(img.src);
|
||||
}
|
||||
}
|
||||
21
src/pages/tools/pdf/convert-to-pdf/types.ts
Normal file
21
src/pages/tools/pdf/convert-to-pdf/types.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export type Orientation = 'portrait' | 'landscape';
|
||||
export type PageType = 'a4' | 'full';
|
||||
|
||||
export interface ImageSize {
|
||||
widthMm: number;
|
||||
heightMm: number;
|
||||
widthPx: number;
|
||||
heightPx: number;
|
||||
}
|
||||
|
||||
export interface FormValues {
|
||||
pageType: PageType;
|
||||
orientation: Orientation;
|
||||
scale: number;
|
||||
}
|
||||
|
||||
export const initialValues: FormValues = {
|
||||
pageType: 'full',
|
||||
orientation: 'portrait',
|
||||
scale: 100
|
||||
};
|
||||
@ -7,6 +7,7 @@ import { tool as compressPdfTool } from './compress-pdf/meta';
|
||||
import { tool as protectPdfTool } from './protect-pdf/meta';
|
||||
import { meta as pdfToEpub } from './pdf-to-epub/meta';
|
||||
import { tool as pdfEditor } from './editor/meta';
|
||||
import { tool as convertToPdf } from './convert-to-pdf/meta';
|
||||
|
||||
export const pdfTools: DefinedTool[] = [
|
||||
pdfEditor,
|
||||
@ -16,5 +17,6 @@ export const pdfTools: DefinedTool[] = [
|
||||
protectPdfTool,
|
||||
mergePdf,
|
||||
pdfToEpub,
|
||||
pdfPdfToPng
|
||||
pdfPdfToPng,
|
||||
convertToPdf
|
||||
];
|
||||
|
||||
@ -70,7 +70,9 @@ export async function splitPdf(
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
const newFileName = pdfFile.name.replace('.pdf', '-extracted.pdf');
|
||||
return new File([newPdfBytes], newFileName, { type: 'application/pdf' });
|
||||
return new File([newPdfBytes as any], newFileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,7 +91,7 @@ export async function mergePdf(pdfFiles: File[]): Promise<File> {
|
||||
|
||||
const mergedPdfBytes = await mergedPdf.save();
|
||||
const mergedFileName = 'merged.pdf';
|
||||
return new File([mergedPdfBytes], mergedFileName, {
|
||||
return new File([mergedPdfBytes as any], mergedFileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ export async function convertPdfToPngImages(pdfFile: File): Promise<{
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
await page.render({ canvas, canvasContext: context, viewport }).promise;
|
||||
|
||||
const blob = await new Promise<Blob>((resolve) =>
|
||||
canvas.toBlob((b) => b && resolve(b), 'image/png')
|
||||
|
||||
@ -78,5 +78,7 @@ export async function rotatePdf(
|
||||
const modifiedPdfBytes = await pdfDoc.save();
|
||||
const newFileName = pdfFile.name.replace('.pdf', '-rotated.pdf');
|
||||
|
||||
return new File([modifiedPdfBytes], newFileName, { type: 'application/pdf' });
|
||||
return new File([modifiedPdfBytes as any], newFileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
}
|
||||
|
||||
@ -70,5 +70,7 @@ export async function splitPdf(
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
const newFileName = pdfFile.name.replace('.pdf', '-extracted.pdf');
|
||||
return new File([newPdfBytes], newFileName, { type: 'application/pdf' });
|
||||
return new File([newPdfBytes as any], newFileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import { tool as stringCensor } from './censor/meta';
|
||||
import { tool as stringPasswordGenerator } from './password-generator/meta';
|
||||
import { tool as stringEncodeUrl } from './url-encode/meta';
|
||||
import { tool as StringDecodeUrl } from './url-decode/meta';
|
||||
import { tool as stringUnicode } from './unicode/meta';
|
||||
|
||||
export const stringTools = [
|
||||
stringSplit,
|
||||
@ -45,5 +46,6 @@ export const stringTools = [
|
||||
stringPasswordGenerator,
|
||||
stringEncodeUrl,
|
||||
StringDecodeUrl,
|
||||
stringUnicode,
|
||||
stringHiddenCharacterDetector
|
||||
];
|
||||
|
||||
123
src/pages/tools/string/unicode/index.tsx
Normal file
123
src/pages/tools/string/unicode/index.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { unicode } from './service';
|
||||
import { InitialValuesType } from './types';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
mode: 'encode',
|
||||
uppercase: false
|
||||
};
|
||||
|
||||
const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
{
|
||||
title: 'Encode to Unicode Escape',
|
||||
description: 'Encode plain text to Unicode escape sequences.',
|
||||
sampleText: 'Hello, World!',
|
||||
sampleResult:
|
||||
'\\u0048\\u0065\\u006c\\u006c\\u006f\\u002c\\u0020\\u0057\\u006f\\u0072\\u006c\\u0064\\u0021',
|
||||
sampleOptions: {
|
||||
mode: 'encode',
|
||||
uppercase: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Encode to Unicode Escape (Uppercase)',
|
||||
description: 'Encode plain text to uppercase Unicode escape sequences.',
|
||||
sampleText: 'Hello, World!',
|
||||
sampleResult:
|
||||
'\\u0048\\u0065\\u006c\\u006c\\u006f\\u002c\\u0020\\u0057\\u006f\\u0072\\u006c\\u0064\\u0021'.toUpperCase(),
|
||||
sampleOptions: {
|
||||
mode: 'encode',
|
||||
uppercase: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Decode Unicode Escape',
|
||||
description: 'Decode Unicode escape sequences back to plain text.',
|
||||
sampleText:
|
||||
'\\u0048\\u0065\\u006c\\u006c\\u006f\\u002c\\u0020\\u0057\\u006f\\u0072\\u006c\\u0064\\u0021',
|
||||
sampleResult: 'Hello, World!',
|
||||
sampleOptions: {
|
||||
mode: 'decode',
|
||||
uppercase: false
|
||||
}
|
||||
}
|
||||
];
|
||||
export default function Unicode({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('string');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
const compute = (values: InitialValuesType, input: string) => {
|
||||
setResult(unicode(input, values));
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: t('unicode.optionsTitle'),
|
||||
component: (
|
||||
<Box>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('mode', 'encode')}
|
||||
checked={values.mode === 'encode'}
|
||||
title={t('unicode.encode')}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('mode', 'decode')}
|
||||
checked={values.mode === 'decode'}
|
||||
title={t('unicode.decode')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('unicode.caseOptionsTitle'),
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
checked={values.uppercase}
|
||||
onChange={(value) => updateField('uppercase', value)}
|
||||
title={t('unicode.uppercase')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolTextInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={t('unicode.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult value={result} title={t('unicode.resultTitle')} />
|
||||
}
|
||||
initialValues={initialValues}
|
||||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{
|
||||
title: t('unicode.toolInfo.title'),
|
||||
description: t('unicode.toolInfo.description')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/string/unicode/meta.ts
Normal file
14
src/pages/tools/string/unicode/meta.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:unicode.title',
|
||||
description: 'string:unicode.description',
|
||||
shortDescription: 'string:unicode.shortDescription'
|
||||
},
|
||||
path: 'unicode',
|
||||
icon: 'mdi:unicode',
|
||||
keywords: ['unicode', 'encode', 'decode', 'escape', 'text'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
21
src/pages/tools/string/unicode/service.ts
Normal file
21
src/pages/tools/string/unicode/service.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { InitialValuesType } from './types';
|
||||
|
||||
export function unicode(input: string, options: InitialValuesType): string {
|
||||
if (!input) return '';
|
||||
if (options.mode === 'encode') {
|
||||
let result = '';
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
let hex = input.charCodeAt(i).toString(16);
|
||||
hex = ('0000' + hex).slice(-4);
|
||||
if (options.uppercase) {
|
||||
hex = hex.toUpperCase();
|
||||
}
|
||||
result += '\\u' + hex;
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return input.replace(/\\u([\dA-Fa-f]{4})/g, (match, grp) => {
|
||||
return String.fromCharCode(parseInt(grp, 16));
|
||||
});
|
||||
}
|
||||
}
|
||||
4
src/pages/tools/string/unicode/types.ts
Normal file
4
src/pages/tools/string/unicode/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type InitialValuesType = {
|
||||
mode: 'encode' | 'decode';
|
||||
uppercase: boolean;
|
||||
};
|
||||
178
src/pages/tools/string/unicode/unicode.service.test.ts
Normal file
178
src/pages/tools/string/unicode/unicode.service.test.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
import { unicode } from './service';
|
||||
|
||||
describe('unicode', () => {
|
||||
it('should encode an English string to lowercase hex correctly', () => {
|
||||
const input = 'Hello, World!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe(
|
||||
'\\u0048\\u0065\\u006c\\u006c\\u006f\\u002c\\u0020\\u0057\\u006f\\u0072\\u006c\\u0064\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode an English string to uppercase hex correctly', () => {
|
||||
const input = 'Hello, World!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe(
|
||||
'\\u0048\\u0065\\u006C\\u006C\\u006F\\u002C\\u0020\\u0057\\u006F\\u0072\\u006C\\u0064\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should decode an English lowercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u0048\\u0065\\u006c\\u006c\\u006f\\u002c\\u0020\\u0057\\u006f\\u0072\\u006c\\u0064\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('Hello, World!');
|
||||
});
|
||||
|
||||
it('should decode an English uppercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u0048\\u0065\\u006C\\u006C\\u006F\\u002C\\u0020\\u0057\\u006F\\u0072\\u006C\\u0064\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('Hello, World!');
|
||||
});
|
||||
|
||||
it('should encode a Korean string to lowercase hex correctly', () => {
|
||||
const input = '안녕하세요, 세계!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe(
|
||||
'\\uc548\\ub155\\ud558\\uc138\\uc694\\u002c\\u0020\\uc138\\uacc4\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode a Korean string to uppercase hex correctly', () => {
|
||||
const input = '안녕하세요, 세계!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe(
|
||||
'\\uC548\\uB155\\uD558\\uC138\\uC694\\u002C\\u0020\\uC138\\uACC4\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should decode a Korean lowercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\uc548\\ub155\\ud558\\uc138\\uc694\\u002c\\u0020\\uc138\\uacc4\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('안녕하세요, 세계!');
|
||||
});
|
||||
|
||||
it('should decode a Korean uppercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\uC548\\uB155\\uD558\\uC138\\uC694\\u002C\\u0020\\uC138\\uACC4\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('안녕하세요, 세계!');
|
||||
});
|
||||
|
||||
it('should encode a Japanese string to lowercase hex correctly', () => {
|
||||
const input = 'こんにちは、世界!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe(
|
||||
'\\u3053\\u3093\\u306b\\u3061\\u306f\\u3001\\u4e16\\u754c\\uff01'
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode a Japanese string to uppercase hex correctly', () => {
|
||||
const input = 'こんにちは、世界!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe(
|
||||
'\\u3053\\u3093\\u306B\\u3061\\u306F\\u3001\\u4E16\\u754C\\uFF01'
|
||||
);
|
||||
});
|
||||
|
||||
it('should decode a Japanese lowercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u3053\\u3093\\u306b\\u3061\\u306f\\u3001\\u4e16\\u754c\\uff01';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('こんにちは、世界!');
|
||||
});
|
||||
|
||||
it('should decode a Japanese uppercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u3053\\u3093\\u306B\\u3061\\u306F\\u3001\\u4E16\\u754C\\uFF01';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('こんにちは、世界!');
|
||||
});
|
||||
|
||||
it('should encode a Chinese string to lowercase hex correctly', () => {
|
||||
const input = '你好,世界!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe('\\u4f60\\u597d\\uff0c\\u4e16\\u754c\\uff01');
|
||||
});
|
||||
|
||||
it('should encode a Chinese string to uppercase hex correctly', () => {
|
||||
const input = '你好,世界!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe('\\u4F60\\u597D\\uFF0C\\u4E16\\u754C\\uFF01');
|
||||
});
|
||||
|
||||
it('should decode a Chinese lowercase hex string correctly', () => {
|
||||
const input = '\\u4f60\\u597d\\uff0c\\u4e16\\u754c\\uff01';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('你好,世界!');
|
||||
});
|
||||
|
||||
it('should decode a Chinese uppercase hex string correctly', () => {
|
||||
const input = '\\u4F60\\u597D\\uFF0C\\u4E16\\u754C\\uFF01';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('你好,世界!');
|
||||
});
|
||||
|
||||
it('should encode a Russian string to lowercase hex correctly', () => {
|
||||
const input = 'Привет, мир!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe(
|
||||
'\\u041f\\u0440\\u0438\\u0432\\u0435\\u0442\\u002c\\u0020\\u043c\\u0438\\u0440\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode a Russian string to uppercase hex correctly', () => {
|
||||
const input = 'Привет, мир!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe(
|
||||
'\\u041F\\u0440\\u0438\\u0432\\u0435\\u0442\\u002C\\u0020\\u043C\\u0438\\u0440\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should decode a Russian lowercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u041f\\u0440\\u0438\\u0432\\u0435\\u0442\\u002c\\u0020\\u043c\\u0438\\u0440\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('Привет, мир!');
|
||||
});
|
||||
|
||||
it('should decode a Russian uppercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u041F\\u0440\\u0438\\u0432\\u0435\\u0442\\u002C\\u0020\\u043C\\u0438\\u0440\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('Привет, мир!');
|
||||
});
|
||||
|
||||
it('should encode a Spanish string to lowercase hex correctly', () => {
|
||||
const input = '¡Hola, Mundo!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe(
|
||||
'\\u00a1\\u0048\\u006f\\u006c\\u0061\\u002c\\u0020\\u004d\\u0075\\u006e\\u0064\\u006f\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode a Spanish string to uppercase hex correctly', () => {
|
||||
const input = '¡Hola, Mundo!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe(
|
||||
'\\u00A1\\u0048\\u006F\\u006C\\u0061\\u002C\\u0020\\u004D\\u0075\\u006E\\u0064\\u006F\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should decode a Spanish lowercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u00a1\\u0048\\u006f\\u006c\\u0061\\u002c\\u0020\\u004d\\u0075\\u006e\\u0064\\u006f\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('¡Hola, Mundo!');
|
||||
});
|
||||
|
||||
it('should decode a Spanish uppercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u00A1\\u0048\\u006F\\u006C\\u0061\\u002C\\u0020\\u004D\\u0075\\u006E\\u0064\\u006F\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('¡Hola, Mundo!');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,40 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
import { convertTimeToDecimal } from './service';
|
||||
|
||||
describe('convert-time-to-decimal', () => {
|
||||
it('should convert time to decimal with default decimal places', () => {
|
||||
const input = '31:23:59';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '6' });
|
||||
expect(result).toBe('31.399722');
|
||||
});
|
||||
|
||||
it('should convert time to decimal with specified decimal places', () => {
|
||||
const input = '31:23:59';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '10' });
|
||||
expect(result).toBe('31.3997222222');
|
||||
});
|
||||
|
||||
it('should convert time to decimal with supplied format of HH:MM:SS', () => {
|
||||
const input = '13:25:30';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '6' });
|
||||
expect(result).toBe('13.425000');
|
||||
});
|
||||
|
||||
it('should convert time to decimal with supplied format of HH:MM', () => {
|
||||
const input = '13:25';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '6' });
|
||||
expect(result).toBe('13.416667');
|
||||
});
|
||||
|
||||
it('should convert time to decimal with supplied format of HH:MM:', () => {
|
||||
const input = '13:25';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '6' });
|
||||
expect(result).toBe('13.416667');
|
||||
});
|
||||
|
||||
it('should convert time to decimal with supplied format of HH.MM.SS', () => {
|
||||
const input = '13.25.30';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '6' });
|
||||
expect(result).toBe('13.425000');
|
||||
});
|
||||
});
|
||||
72
src/pages/tools/time/convert-time-to-decimal/index.tsx
Normal file
72
src/pages/tools/time/convert-time-to-decimal/index.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { convertTimeToDecimal } from './service';
|
||||
import { InitialValuesType } from './types';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
decimalPlaces: '6'
|
||||
};
|
||||
|
||||
const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
{
|
||||
title: 'Convert time to decimal',
|
||||
description:
|
||||
'This example shows how to convert a formatted time (HH:MM:SS) to a decimal version.',
|
||||
sampleText: '31:23:59',
|
||||
sampleResult: `31.399722`,
|
||||
sampleOptions: {
|
||||
decimalPlaces: '6'
|
||||
}
|
||||
}
|
||||
];
|
||||
export default function ConvertTimeToDecimal({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
const compute = (values: InitialValuesType, input: string) => {
|
||||
setResult(convertTimeToDecimal(input, values));
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> | null = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Decimal places',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={'How many decimal places should the result contain?'}
|
||||
value={values.decimalPlaces}
|
||||
onOwnChange={(val) => updateField('decimalPlaces', val)}
|
||||
type={'text'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={<ToolTextInput value={input} onChange={setInput} />}
|
||||
resultComponent={<ToolTextResult value={result} />}
|
||||
initialValues={initialValues}
|
||||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
15
src/pages/tools/time/convert-time-to-decimal/meta.ts
Normal file
15
src/pages/tools/time/convert-time-to-decimal/meta.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('time', {
|
||||
i18n: {
|
||||
name: 'time:convertTimeToDecimal.title',
|
||||
description: 'time:convertTimeToDecimal.description',
|
||||
shortDescription: 'time:convertTimeToDecimal.shortDescription',
|
||||
longDescription: 'time:convertTimeToDecimal.longDescription'
|
||||
},
|
||||
path: 'convert-time-to-decimal',
|
||||
icon: 'material-symbols-light:decimal-increase-rounded',
|
||||
keywords: ['convert', 'time', 'to', 'decimal'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
37
src/pages/tools/time/convert-time-to-decimal/service.ts
Normal file
37
src/pages/tools/time/convert-time-to-decimal/service.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { InitialValuesType } from './types';
|
||||
import { humanTimeValidation } from 'utils/time';
|
||||
|
||||
export function convertTimeToDecimal(
|
||||
input: string,
|
||||
options: InitialValuesType
|
||||
): string {
|
||||
if (!input) return '';
|
||||
|
||||
const dp = parseInt(options.decimalPlaces, 10);
|
||||
if (isNaN(dp) || dp < 0) {
|
||||
return 'Invalid decimal places value.';
|
||||
}
|
||||
|
||||
// Multiple lines processing
|
||||
const lines = input.split('\n');
|
||||
if (!lines) return '';
|
||||
|
||||
const result: string[] = [];
|
||||
|
||||
lines.forEach((line) => {
|
||||
line = line.trim();
|
||||
if (!line) return;
|
||||
|
||||
const { isValid, hours, minutes, seconds } = humanTimeValidation(line);
|
||||
|
||||
if (!isValid) {
|
||||
result.push('Incorrect input format use `HH:MM:(SS)` or `HH.MM.(SS )`.');
|
||||
return;
|
||||
}
|
||||
|
||||
const decimalTime = hours + minutes / 60 + seconds / 3600;
|
||||
result.push(decimalTime.toFixed(dp).toString());
|
||||
});
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
3
src/pages/tools/time/convert-time-to-decimal/types.ts
Normal file
3
src/pages/tools/time/convert-time-to-decimal/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type InitialValuesType = {
|
||||
decimalPlaces: string;
|
||||
};
|
||||
@ -1,3 +1,4 @@
|
||||
import { tool as timeConvertTimeToDecimal } from './convert-time-to-decimal/meta';
|
||||
import { tool as timeConvertUnixToDate } from './convert-unix-to-date/meta';
|
||||
import { tool as timeCrontabGuru } from './crontab-guru/meta';
|
||||
import { tool as timeBetweenDates } from './time-between-dates/meta';
|
||||
@ -17,5 +18,6 @@ export const timeTools = [
|
||||
timeBetweenDates,
|
||||
timeCrontabGuru,
|
||||
checkLeapYear,
|
||||
timeConvertUnixToDate
|
||||
timeConvertUnixToDate,
|
||||
timeConvertTimeToDecimal
|
||||
];
|
||||
|
||||
@ -101,7 +101,7 @@ export default function ChangeSpeed({
|
||||
const data = await ffmpeg.readFile(outputName);
|
||||
|
||||
// Create new file from processed data
|
||||
const blob = new Blob([data], { type: 'video/mp4' });
|
||||
const blob = new Blob([data as any], { type: 'video/mp4' });
|
||||
const newFile = new File(
|
||||
[blob],
|
||||
file.name.replace('.mp4', `-${newSpeed}x.mp4`),
|
||||
|
||||
@ -53,7 +53,7 @@ export async function compressVideo(
|
||||
}
|
||||
const compressedData = await ffmpeg.readFile(outputName);
|
||||
return new File(
|
||||
[new Blob([compressedData], { type: 'video/mp4' })],
|
||||
[new Blob([compressedData as any], { type: 'video/mp4' })],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_compressed_${options.width}p.mp4`,
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
|
||||
@ -60,7 +60,7 @@ export async function cropVideo(
|
||||
|
||||
const croppedData = await ffmpeg.readFile(outputName);
|
||||
return await new File(
|
||||
[new Blob([croppedData], { type: 'video/mp4' })],
|
||||
[new Blob([croppedData as any], { type: 'video/mp4' })],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_cropped.mp4`,
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
|
||||
@ -36,7 +36,7 @@ export async function flipVideo(
|
||||
|
||||
const flippedData = await ffmpeg.readFile(outputName);
|
||||
return new File(
|
||||
[new Blob([flippedData], { type: 'video/mp4' })],
|
||||
[new Blob([flippedData as any], { type: 'video/mp4' })],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_flipped.mp4`,
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
|
||||
@ -71,7 +71,7 @@ export default function ChangeSpeed({ title }: ToolComponentProps) {
|
||||
const data = await ffmpeg.readFile('output.gif');
|
||||
|
||||
// Create a new file from the processed data
|
||||
const blob = new Blob([data], { type: 'image/gif' });
|
||||
const blob = new Blob([data as any], { type: 'image/gif' });
|
||||
const newFile = new File(
|
||||
[blob],
|
||||
file.name.replace('.gif', `-${newSpeed}x.gif`),
|
||||
|
||||
@ -3,7 +3,7 @@ import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
path: 'loop',
|
||||
icon: 'material-symbols:loop',
|
||||
icon: 'ic:outline-loop',
|
||||
|
||||
keywords: ['video', 'loop', 'repeat', 'continuous'],
|
||||
component: lazy(() => import('./index')),
|
||||
|
||||
@ -34,7 +34,7 @@ export async function loopVideo(
|
||||
|
||||
const loopedData = await ffmpeg.readFile(outputName);
|
||||
return await new File(
|
||||
[new Blob([loopedData], { type: 'video/mp4' })],
|
||||
[new Blob([loopedData as any], { type: 'video/mp4' })],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_looped.mp4`,
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
|
||||
@ -112,7 +112,7 @@ export async function mergeVideos(
|
||||
throw new Error('Output file is empty or corrupted');
|
||||
}
|
||||
|
||||
return new Blob([mergedData], { type: 'video/mp4' });
|
||||
return new Blob([mergedData as any], { type: 'video/mp4' });
|
||||
} catch (error) {
|
||||
console.error('Error merging videos:', error);
|
||||
throw error instanceof Error
|
||||
|
||||
@ -37,7 +37,7 @@ export async function rotateVideo(
|
||||
|
||||
const rotatedData = await ffmpeg.readFile(outputName);
|
||||
return new File(
|
||||
[new Blob([rotatedData], { type: 'video/mp4' })],
|
||||
[new Blob([rotatedData as any], { type: 'video/mp4' })],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_rotated.mp4`,
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
|
||||
@ -67,7 +67,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
]);
|
||||
// Retrieve the processed file
|
||||
const trimmedData = await ffmpeg.readFile(outputName);
|
||||
const trimmedBlob = new Blob([trimmedData], { type: 'video/mp4' });
|
||||
const trimmedBlob = new Blob([trimmedData as any], { type: 'video/mp4' });
|
||||
const trimmedFile = new File(
|
||||
[trimmedBlob],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_trimmed.mp4`,
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { updateNumberField } from '@utils/string';
|
||||
import { InitialValuesType } from './types';
|
||||
import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
@ -13,9 +16,19 @@ import { fetchFile } from '@ffmpeg/util';
|
||||
const initialValues: InitialValuesType = {
|
||||
quality: 'mid',
|
||||
fps: '10',
|
||||
scale: '320:-1:flags=bicubic'
|
||||
scale: '320:-1:flags=bicubic',
|
||||
start: 0,
|
||||
end: 100
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
start: Yup.number().min(0, 'Start time must be positive'),
|
||||
end: Yup.number().min(
|
||||
Yup.ref('start'),
|
||||
'End time must be greater than start time'
|
||||
)
|
||||
});
|
||||
|
||||
export default function VideoToGif({
|
||||
title,
|
||||
longDescription
|
||||
@ -26,14 +39,16 @@ export default function VideoToGif({
|
||||
|
||||
const compute = (values: InitialValuesType, input: File | null) => {
|
||||
if (!input) return;
|
||||
const { fps, scale } = values;
|
||||
const { fps, scale, start, end } = values;
|
||||
let ffmpeg: FFmpeg | null = null;
|
||||
let ffmpegLoaded = false;
|
||||
|
||||
const convertVideoToGif = async (
|
||||
file: File,
|
||||
fps: string,
|
||||
scale: string
|
||||
scale: string,
|
||||
start: number,
|
||||
end: number
|
||||
): Promise<void> => {
|
||||
setLoading(true);
|
||||
|
||||
@ -58,6 +73,10 @@ export default function VideoToGif({
|
||||
await ffmpeg.exec([
|
||||
'-i',
|
||||
fileName,
|
||||
'-ss',
|
||||
start.toString(),
|
||||
'-to',
|
||||
end.toString(),
|
||||
'-vf',
|
||||
`fps=${fps},scale=${scale},palettegen`,
|
||||
'palette.png'
|
||||
@ -68,6 +87,10 @@ export default function VideoToGif({
|
||||
fileName,
|
||||
'-i',
|
||||
'palette.png',
|
||||
'-ss',
|
||||
start.toString(),
|
||||
'-to',
|
||||
end.toString(),
|
||||
'-filter_complex',
|
||||
`fps=${fps},scale=${scale}[x];[x][1:v]paletteuse`,
|
||||
outputName
|
||||
@ -75,7 +98,7 @@ export default function VideoToGif({
|
||||
|
||||
const data = await ffmpeg.readFile(outputName);
|
||||
|
||||
const blob = new Blob([data], { type: 'image/gif' });
|
||||
const blob = new Blob([data as any], { type: 'image/gif' });
|
||||
const convertedFile = new File([blob], outputName, {
|
||||
type: 'image/gif'
|
||||
});
|
||||
@ -92,7 +115,7 @@ export default function VideoToGif({
|
||||
}
|
||||
};
|
||||
|
||||
convertVideoToGif(input, fps, scale);
|
||||
convertVideoToGif(input, fps, scale, start, end);
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> | null = ({
|
||||
@ -141,6 +164,28 @@ export default function VideoToGif({
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Timestamps',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
onOwnChange={(value) =>
|
||||
updateNumberField(value, 'start', updateField)
|
||||
}
|
||||
value={values.start}
|
||||
label="Start Time"
|
||||
sx={{ mb: 2, backgroundColor: 'background.paper' }}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
onOwnChange={(value) =>
|
||||
updateNumberField(value, 'end', updateField)
|
||||
}
|
||||
value={values.end}
|
||||
label="End Time"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
@ -148,9 +193,22 @@ export default function VideoToGif({
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolVideoInput value={input} onChange={setInput} title="Input Video" />
|
||||
}
|
||||
renderCustomInput={({ start, end }, setFieldValue) => {
|
||||
return (
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
showTrimControls={true}
|
||||
onTrimChange={(start, end) => {
|
||||
setFieldValue('start', start);
|
||||
setFieldValue('end', end);
|
||||
}}
|
||||
trimStart={start}
|
||||
trimEnd={end}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult
|
||||
|
||||
@ -2,4 +2,6 @@ export type InitialValuesType = {
|
||||
quality: 'mid' | 'high' | 'low' | 'ultra';
|
||||
fps: string;
|
||||
scale: string;
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
58
src/utils/time.ts
Normal file
58
src/utils/time.ts
Normal file
@ -0,0 +1,58 @@
|
||||
type TimeValidationResult = {
|
||||
isValid: boolean;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates human-readable time format (HH:MM or HH:MM:SS)
|
||||
* Supports either ':' or '.' as a separator, but not both
|
||||
* @param {string} input - string time format
|
||||
* * @returns {{
|
||||
* isValid: boolean, // true if the input is a valid time
|
||||
* hours: number, // parsed hours (0 or greater)
|
||||
* minutes: number, // parsed minutes (0-59)
|
||||
* seconds: number // parsed seconds (0-59, 0 if not provided)
|
||||
* }}
|
||||
*/
|
||||
export function humanTimeValidation(input: string): TimeValidationResult {
|
||||
const result = { isValid: false, hours: 0, minutes: 0, seconds: 0 };
|
||||
|
||||
if (!input) return result;
|
||||
|
||||
input = input.trim();
|
||||
|
||||
// Operator use validation
|
||||
// use of one between these two operators '.' or ':'
|
||||
|
||||
const hasColon = input.includes(':');
|
||||
const hasDot = input.includes('.');
|
||||
|
||||
if (hasColon && hasDot) return result;
|
||||
|
||||
if (!hasColon && !hasDot) return result;
|
||||
|
||||
const separator = hasColon ? ':' : '.';
|
||||
|
||||
// Time parts validation
|
||||
|
||||
const parts = input.split(separator);
|
||||
|
||||
if (parts.length < 2 || parts.length > 3) return result;
|
||||
|
||||
const [h, m, s = '0'] = parts;
|
||||
|
||||
// every character should be a digit
|
||||
if (![h, m, s].every((x) => /^\d+$/.test(x))) return result;
|
||||
|
||||
const hours = parseInt(h);
|
||||
const minutes = parseInt(m);
|
||||
const seconds = parseInt(s);
|
||||
|
||||
if (minutes < 0 || minutes > 59) return result;
|
||||
if (seconds < 0 || seconds > 59) return result;
|
||||
if (hours < 0) return result;
|
||||
|
||||
return { isValid: true, hours, minutes, seconds };
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user