Merge branch 'main' into pr/Srivarshan-T/216

This commit is contained in:
Chesterkxng 2025-12-10 10:55:56 +01:00
commit adbc389c5d
72 changed files with 5339 additions and 3027 deletions

View File

@ -17,7 +17,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '18' node-version: '20'
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Run tests - name: Run tests
@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Install Playwright Browsers - name: Install Playwright Browsers
@ -96,7 +96,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '18' node-version: '20'
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Build project - name: Build project

532
.idea/workspace.xml generated
View File

@ -4,12 +4,10 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <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 &quot;hireMe&quot; translation in navbar&#10;&#10;This commit corrects the translation of &quot;hireMe&quot; in the&#10;navigation bar across all supported languages. The order of&#10;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$/.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$/public/locales/es/translation.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/es/translation.json" 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$/public/locales/fr/translation.json" beforeDir="false" afterPath="$PROJECT_DIR$/public/locales/fr/translation.json" 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" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -43,7 +41,7 @@
<option name="PUSH_AUTO_UPDATE" value="true" /> <option name="PUSH_AUTO_UPDATE" value="true" />
<option name="RECENT_BRANCH_BY_REPOSITORY"> <option name="RECENT_BRANCH_BY_REPOSITORY">
<map> <map>
<entry key="$PROJECT_DIR$" value="fork/AshAnand34/tool/random-generators" /> <entry key="$PROJECT_DIR$" value="28f4c64d3044df927dc088435164e803e14f8794" />
</map> </map>
</option> </option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@ -55,15 +53,14 @@
&quot;assignee&quot;: &quot;iib0011&quot; &quot;assignee&quot;: &quot;iib0011&quot;
}, },
{ {
&quot;searchQuery&quot;: &quot;filter&quot;,
&quot;state&quot;: &quot;OPEN&quot; &quot;state&quot;: &quot;OPEN&quot;
}, },
{ {
&quot;searchQuery&quot;: &quot;filter&quot;,
&quot;state&quot;: &quot;OPEN&quot; &quot;state&quot;: &quot;OPEN&quot;
} }
], ],
&quot;lastFilter&quot;: { &quot;lastFilter&quot;: {
&quot;searchQuery&quot;: &quot;filter&quot;,
&quot;state&quot;: &quot;OPEN&quot; &quot;state&quot;: &quot;OPEN&quot;
} }
}</component> }</component>
@ -230,13 +227,6 @@
}, },
&quot;lastSeen&quot;: 1752158748013 &quot;lastSeen&quot;: 1752158748013
}, },
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6eqzP7&quot;,
&quot;number&quot;: 190
},
&quot;lastSeen&quot;: 1752404173008
},
{ {
&quot;id&quot;: { &quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6et6vx&quot;, &quot;id&quot;: &quot;PR_kwDOMJIfts6et6vx&quot;,
@ -260,10 +250,101 @@
}, },
{ {
&quot;id&quot;: { &quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6fo_ig&quot;, &quot;id&quot;: &quot;PR_kwDOMJIfts6rjINx&quot;,
&quot;number&quot;: 209 &quot;number&quot;: 259
}, },
&quot;lastSeen&quot;: 1753201966322 &quot;lastSeen&quot;: 1759434090574
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6qcP13&quot;,
&quot;number&quot;: 256
},
&quot;lastSeen&quot;: 1759434257615
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6ow8QZ&quot;,
&quot;number&quot;: 252
},
&quot;lastSeen&quot;: 1759434340504
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6myVeZ&quot;,
&quot;number&quot;: 247
},
&quot;lastSeen&quot;: 1759434588110
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6i5ZAq&quot;,
&quot;number&quot;: 239
},
&quot;lastSeen&quot;: 1759434599664
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6iiuGd&quot;,
&quot;number&quot;: 237
},
&quot;lastSeen&quot;: 1759434652702
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6gwm8n&quot;,
&quot;number&quot;: 230
},
&quot;lastSeen&quot;: 1759434669914
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6f5JeZ&quot;,
&quot;number&quot;: 220
},
&quot;lastSeen&quot;: 1759434706785
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6ftgWI&quot;,
&quot;number&quot;: 217
},
&quot;lastSeen&quot;: 1759434804548
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6XsHfL&quot;,
&quot;number&quot;: 128
},
&quot;lastSeen&quot;: 1759434870000
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6ec-tz&quot;,
&quot;number&quot;: 180
},
&quot;lastSeen&quot;: 1759434882113
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6fsi5n&quot;,
&quot;number&quot;: 216
},
&quot;lastSeen&quot;: 1759434902813
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6ZkP3F&quot;,
&quot;number&quot;: 142
},
&quot;lastSeen&quot;: 1759434918778
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6qcbuA&quot;,
&quot;number&quot;: 257
},
&quot;lastSeen&quot;: 1759438234107
} }
] ]
}</component> }</component>
@ -280,8 +361,8 @@
<setting file="file://$PROJECT_DIR$/node_modules/react-image-crop/dist/index.d.ts" root0="SKIP_INSPECTION" /> <setting file="file://$PROJECT_DIR$/node_modules/react-image-crop/dist/index.d.ts" root0="SKIP_INSPECTION" />
</component> </component>
<component name="KubernetesApiProvider">{ <component name="KubernetesApiProvider">{
&quot;isMigrated&quot;: true &quot;isMigrated&quot;: true
}</component> }</component>
<component name="MarkdownSettingsMigration"> <component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" /> <option name="stateVersion" value="1" />
</component> </component>
@ -298,65 +379,66 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent"><![CDATA[{ <component name="PropertiesComponent">{
"keyToString": { &quot;keyToString&quot;: {
"ASKED_ADD_EXTERNAL_FILES": "true", &quot;ASKED_ADD_EXTERNAL_FILES&quot;: &quot;true&quot;,
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true", &quot;ASKED_SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
"Docker.Dockerfile build.executor": "Run", &quot;Docker.Dockerfile build.executor&quot;: &quot;Run&quot;,
"Docker.Dockerfile.executor": "Run", &quot;Docker.Dockerfile.executor&quot;: &quot;Run&quot;,
"Node.js.add-i18n-to-meta.js.executor": "Run", &quot;Node.js.add-i18n-to-meta.js.executor&quot;: &quot;Run&quot;,
"Node.js.locize-upload.js.executor": "Run", &quot;Node.js.locize-upload.js.executor&quot;: &quot;Run&quot;,
"Node.js.update-i18n-from-meta.js.executor": "Run", &quot;Node.js.update-i18n-from-meta.js.executor&quot;: &quot;Run&quot;,
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run", &quot;Playwright.Create transparent PNG.should make png color transparent.executor&quot;: &quot;Run&quot;,
"Playwright.JoinText Component.executor": "Run", &quot;Playwright.JoinText Component.executor&quot;: &quot;Run&quot;,
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run", &quot;Playwright.JoinText Component.should merge text pieces with specified join character.executor&quot;: &quot;Run&quot;,
"RunOnceActivity.OpenProjectViewOnStart": "true", &quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"RunOnceActivity.git.unshallow": "true", &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
"Vitest.compute function (1).executor": "Run", &quot;Vitest.compute function (1).executor&quot;: &quot;Run&quot;,
"Vitest.compute function.executor": "Run", &quot;Vitest.compute function.executor&quot;: &quot;Run&quot;,
"Vitest.generatePassword.executor": "Run", &quot;Vitest.generatePassword.executor&quot;: &quot;Run&quot;,
"Vitest.mergeText.executor": "Run", &quot;Vitest.mergeText.executor&quot;: &quot;Run&quot;,
"Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run", &quot;Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor&quot;: &quot;Run&quot;,
"Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run", &quot;Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor&quot;: &quot;Run&quot;,
"Vitest.parsePageRanges.executor": "Run", &quot;Vitest.parsePageRanges.executor&quot;: &quot;Run&quot;,
"Vitest.removeDuplicateLines function.executor": "Run", &quot;Vitest.removeDuplicateLines function.executor&quot;: &quot;Run&quot;,
"Vitest.removeDuplicateLines function.newlines option.executor": "Run", &quot;Vitest.removeDuplicateLines function.newlines option.executor&quot;: &quot;Run&quot;,
"Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run", &quot;Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor&quot;: &quot;Run&quot;,
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run", &quot;Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor&quot;: &quot;Run&quot;,
"Vitest.replaceText function.executor": "Run", &quot;Vitest.replaceText function.executor&quot;: &quot;Run&quot;,
"Vitest.timeBetweenDates.executor": "Run", &quot;Vitest.timeBetweenDates.executor&quot;: &quot;Run&quot;,
"git-widget-placeholder": "#218 on fork/AshAnand34/tool/random-generators", &quot;git-widget-placeholder&quot;: &quot;main&quot;,
"ignore.virus.scanning.warn.message": "true", &quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
"kotlin-language-version-configured": "true", &quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools", &quot;last_opened_file_path&quot;: &quot;C:/Users/Ibrahima/IdeaProjects/omni-tools&quot;,
"node.js.detected.package.eslint": "true", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"node.js.detected.package.tslint": "true", &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"nodejs_package_manager_path": "npm", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"npm.build.executor": "Run", &quot;npm.build.executor&quot;: &quot;Run&quot;,
"npm.dev.executor": "Run", &quot;npm.dev.executor&quot;: &quot;Run&quot;,
"npm.i18n:extract.executor": "Run", &quot;npm.i18n:extract.executor&quot;: &quot;Run&quot;,
"npm.i18n:pull.executor": "Run", &quot;npm.i18n:pull.executor&quot;: &quot;Run&quot;,
"npm.i18n:push.executor": "Run", &quot;npm.i18n:push.executor&quot;: &quot;Run&quot;,
"npm.i18n:sync.executor": "Run", &quot;npm.i18n:sync.executor&quot;: &quot;Run&quot;,
"npm.lint.executor": "Run", &quot;npm.lint.executor&quot;: &quot;Run&quot;,
"npm.prebuild.executor": "Run", &quot;npm.prebuild.executor&quot;: &quot;Run&quot;,
"npm.script:create:tool.executor": "Run", &quot;npm.script:create:tool.executor&quot;: &quot;Run&quot;,
"npm.test.executor": "Run", &quot;npm.test.executor&quot;: &quot;Run&quot;,
"npm.test:e2e.executor": "Run", &quot;npm.test:e2e.executor&quot;: &quot;Run&quot;,
"npm.test:e2e:run.executor": "Run", &quot;npm.test:e2e:run.executor&quot;: &quot;Run&quot;,
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier", &quot;npm.typecheck.executor&quot;: &quot;Run&quot;,
"project.structure.last.edited": "Problems", &quot;prettierjs.PrettierConfiguration.Package&quot;: &quot;C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier&quot;,
"project.structure.proportion": "0.0", &quot;project.structure.last.edited&quot;: &quot;Problems&quot;,
"project.structure.side.proportion": "0.2", &quot;project.structure.proportion&quot;: &quot;0.0&quot;,
"settings.editor.selected.configurable": "preferences.pluginManager", &quot;project.structure.side.proportion&quot;: &quot;0.2&quot;,
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib", &quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;,
"ts.rename.search.for.js.occurrences": "false", &quot;ts.external.directory.path&quot;: &quot;C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib&quot;,
"vue.rearranger.settings.migration": "true" &quot;ts.rename.search.for.js.occurrences&quot;: &quot;false&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
} }
}]]></component> }</component>
<component name="ReactDesignerToolWindowState"> <component name="ReactDesignerToolWindowState">
<option name="myId2Visible"> <option name="myId2Visible">
<map> <map>
@ -383,19 +465,6 @@
</key> </key>
</component> </component>
<component name="RunManager" selected="npm.dev"> <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"> <configuration default="true" type="docker-deploy" factoryName="dockerfile" temporary="true">
<deployment type="dockerfile"> <deployment type="dockerfile">
<settings /> <settings />
@ -415,26 +484,6 @@
</envs> </envs>
<method v="2" /> <method v="2" />
</configuration> </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"> <configuration name="i18n:sync" type="js.build_tools.npm" temporary="true" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" /> <package-json value="$PROJECT_DIR$/package.json" />
<command value="run" /> <command value="run" />
@ -447,20 +496,50 @@
</envs> </envs>
<method v="2" /> <method v="2" />
</configuration> </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> <list>
<item itemvalue="npm.i18n:extract" /> <item itemvalue="npm.test" />
<item itemvalue="npm.i18n:pull" /> <item itemvalue="npm.test:e2e" />
<item itemvalue="npm.typecheck" />
<item itemvalue="npm.i18n:sync" /> <item itemvalue="npm.i18n:sync" />
<item itemvalue="npm.dev" /> <item itemvalue="npm.dev" />
<item itemvalue="Vitest.generatePassword" />
</list> </list>
<recent_temporary> <recent_temporary>
<list> <list>
<item itemvalue="npm.dev" /> <item itemvalue="npm.dev" />
<item itemvalue="npm.i18n:sync" /> <item itemvalue="npm.i18n:sync" />
<item itemvalue="Vitest.generatePassword" /> <item itemvalue="npm.test:e2e" />
<item itemvalue="npm.i18n:pull" /> <item itemvalue="npm.test" />
<item itemvalue="npm.i18n:extract" /> <item itemvalue="npm.typecheck" />
</list> </list>
</recent_temporary> </recent_temporary>
</component> </component>
@ -580,78 +659,8 @@
<workItem from="1753201599796" duration="4449000" /> <workItem from="1753201599796" duration="4449000" />
<workItem from="1753206561770" duration="119000" /> <workItem from="1753206561770" duration="119000" />
<workItem from="1753206717510" duration="3599000" /> <workItem from="1753206717510" duration="3599000" />
</task> <workItem from="1759497758761" duration="1012000" />
<task id="LOCAL-00201" summary="chore: rename from Omni Tools to OmniTools"> <workItem from="1759502144651" duration="1106000" />
<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>
</task> </task>
<task id="LOCAL-00210" summary="feat: convert to jpg"> <task id="LOCAL-00210" summary="feat: convert to jpg">
<option name="closed" value="true" /> <option name="closed" value="true" />
@ -973,7 +982,79 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1753210033390</updated> <updated>1753210033390</updated>
</task> </task>
<option name="localTasksCounter" value="250" /> <task id="LOCAL-00250" summary="feat: remove temperature conversion from generic-calc&#10;&#10;This commit removes the temperature conversion tool from the&#10;generic-calc tool. This is because the tool was causing issues.&#10;&#10;The following files were modified:&#10;- src/pages/tools/number/generic-calc/data/index.ts&#10;- src/pages/tools/number/generic-calc/data/temperature.ts&#10;- package.json&#10;- .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)&#10;&#10;This commit removes the onnxruntime-web package&#10;from package.json.&#10;```">
<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)&#10;&#10;This commit updates the Node.js versions used in the CI&#10;workflows to version 20. This ensures that the CI&#10;environment uses a more up-to-date and supported version&#10;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)&#10;&#10;Upgrades onnxruntime-web and onnxruntime-common to versions&#10;1.23.0. This includes updates to dependencies and related&#10;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 &quot;Buy me a coffee&quot; with &quot;Hire me&quot; (main)&#10;&#10;This commit replaces the &quot;Buy me a coffee&quot; button with a&#10;&quot;Hire me&quot; button in the navbar. It also updates the&#10;corresponding translation in the `en/translation.json` file.&#10;The icon has been changed to a job search icon, and the&#10;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)&#10;&#10;This commit includes the following changes:&#10;&#10;- Updated the workspace configuration in .idea/workspace.xml to&#10; reflect the recent npm script execution.&#10;- Modified the English translation file (translation.json) by&#10; reordering the &quot;navbar&quot; 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)&#10;&#10;This commit includes the following changes:&#10;&#10;- Updated the workspace configuration in .idea/workspace.xml to&#10; reflect the recent npm script execution.&#10;- Modified the English translation file (translation.json) by&#10; reordering the &quot;navbar&quot; keys.&#10;- Updated translation files in other languages with &quot;hireMe&quot;.">
<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 &quot;hireMe&quot; translation in navbar&#10;&#10;This commit corrects the translation of &quot;hireMe&quot; in the&#10;navigation bar across all supported languages. The order of&#10;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 /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@ -996,6 +1077,11 @@
<entry key="Branch"> <entry key="Branch">
<value> <value>
<list> <list>
<RecentGroup>
<option name="FILTER_VALUES">
<option value="origin/main" />
</option>
</RecentGroup>
<RecentGroup> <RecentGroup>
<option name="FILTER_VALUES"> <option name="FILTER_VALUES">
<option value="origin/examples" /> <option value="origin/examples" />
@ -1010,7 +1096,19 @@
<map> <map>
<entry key="MAIN"> <entry key="MAIN">
<value> <value>
<State /> <State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="origin/main" />
</list>
</value>
</entry>
</map>
</option>
</State>
</value> </value>
</entry> </entry>
</map> </map>
@ -1020,32 +1118,32 @@
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" /> <option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
<option name="CHECK_NEW_TODO" value="false" /> <option name="CHECK_NEW_TODO" value="false" />
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" /> <option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
<MESSAGE value="chore: i18n in meta" /> <MESSAGE value="feat: Upgrade Node.js versions in CI (main)&#10;&#10;This commit updates the Node.js versions used in the CI&#10;workflows to version 20. This ensures that the CI&#10;environment uses a more up-to-date and supported version&#10;of Node.js." />
<MESSAGE value="chore: add i18n to meta script" /> <MESSAGE value="feat" />
<MESSAGE value="chore: bundle translations at build time" /> <MESSAGE value="feat: Upgrade onnxruntime-web and onnxruntime-common (main)&#10;&#10;Up" />
<MESSAGE value="fix: tsc" /> <MESSAGE value="feat: Upgrade onnxruntime-web and onnxruntime-common (main)&#10;&#10;Upgrades onnxruntime-web and onnxruntime-common to versions&#10;1" />
<MESSAGE value="chore: remove unnecessary" /> <MESSAGE value="feat: Upgrade onnxruntime-web and onnxruntime-common (main)&#10;&#10;Upgrades onnxruntime-web and onnxruntime-common to versions&#10;1.23.0. This includes updates to dependencies and related&#10;packages.&#10;" />
<MESSAGE value="chore: saveMissing" /> <MESSAGE value="feat: Upgrade onnxruntime-web and onnxruntime-common (main)&#10;&#10;Upgrades onnxruntime-web and onnxruntime-common to versions&#10;1.23.0. This includes updates to dependencies and related&#10;packages." />
<MESSAGE value="fix: translation related behaviors" /> <MESSAGE value="```&#10;feat: Replace &quot;Buy me a coffee&quot; with &quot;Hire me&quot; (main)&#10;&#10;" />
<MESSAGE value="feat: password generator to test translation" /> <MESSAGE value="```&#10;feat: Replace &quot;Buy me a coffee&quot; with &quot;Hire me&quot; (main)&#10;&#10;This commit replaces the &quot;Buy me a coffee&quot; button with a&#10;&quot;Hire" />
<MESSAGE value="docs: translation docs" /> <MESSAGE value="```&#10;feat: Replace &quot;Buy me a coffee&quot; with &quot;Hire me&quot; (main)&#10;&#10;This commit replaces the &quot;Buy me a coffee&quot; button with a&#10;&quot;Hire me&quot; button in the navbar. It also updates the&#10;corresponding translation in the `en/translation.json` file.&#10;The icon has been changed to a" />
<MESSAGE value="fix: translations" /> <MESSAGE value="```&#10;feat: Replace &quot;Buy me a coffee&quot; with &quot;Hire me&quot; (main)&#10;&#10;This commit replaces the &quot;Buy me a coffee&quot; button with a&#10;&quot;Hire me&quot; button in the navbar. It also updates the&#10;corresponding translation in the `en/translation.json` file.&#10;The icon has been changed to a job search icon, and the&#10;link points to a Google Drive document.&#10;```" />
<MESSAGE value="chore: delete unused i18n json files" /> <MESSAGE value="chore: Replace &quot;Buy me a coffee&quot; with &quot;Hire me&quot; (main)&#10;&#10;This commit replaces the &quot;Buy me a coffee&quot; button with a&#10;&quot;Hire me&quot; button in the navbar. It also updates the&#10;corresponding translation in the `en/translation.json` file.&#10;The icon has been changed to a job search icon, and the&#10;link points to a Google Drive document." />
<MESSAGE value="fix: create-tool.mjs to use i18n object" /> <MESSAGE value="```&#10;feat: Update workspace and translation files (main)&#10;&#10;This commit includes the following changes:" />
<MESSAGE value="fix: show Use this tool only if medium breakpoint" /> <MESSAGE value="```&#10;feat: Update workspace and translation files (main)&#10;&#10;This commit includes the following changes:&#10;&#10;- Updated the workspace configuration in .idea/workspace.xml to&#10; reflect" />
<MESSAGE value="chore: sync locales" /> <MESSAGE value="```&#10;feat: Update workspace and translation files (main)&#10;&#10;This commit includes the following changes:&#10;&#10;- Updated the workspace configuration in .idea/workspace.xml to&#10; reflect the recent npm script execution.&#10;- Modified the English translation file (translation.json) by&#10; reordering the &quot;navbar&quot; keys.&#10;```&#10;" />
<MESSAGE value="fix: i18n" /> <MESSAGE value="feat: Update workspace and translation files (main)&#10;&#10;This commit includes the following changes:&#10;&#10;- Updated the workspace configuration in .idea/workspace.xml to&#10; reflect the recent npm script execution.&#10;- Modified the English translation file (translation.json) by&#10; reordering the &quot;navbar&quot; keys." />
<MESSAGE value="chore: remove prebuild" /> <MESSAGE value="```&#10;feat: Update workspace and translation files (main)&#10;&#10;This commit includes the following" />
<MESSAGE value="fix: broken translations" /> <MESSAGE value="```&#10;feat: Update workspace and translation files (main)&#10;&#10;This commit includes the following changes:&#10;&#10;- Updated the workspace configuration in .idea/workspace.xml to&#10; " />
<MESSAGE value="fix: i18n tsc" /> <MESSAGE value="```&#10;feat: Update workspace and translation files (main)&#10;&#10;This commit includes the following changes:&#10;&#10;- Updated the workspace configuration in .idea/workspace.xml to&#10; reflect the recent npm script execution.&#10;- Modified the English translation file (translation.json) by&#10; reordering the &quot;navbar&quot; keys.&#10;-" />
<MESSAGE value="chore: i18n pull dutch" /> <MESSAGE value="```&#10;feat: Update workspace and translation files (main)&#10;&#10;This commit includes the following changes:&#10;&#10;- Updated the workspace configuration in .idea/workspace.xml to&#10; reflect the recent npm script execution.&#10;- Modified the English translation file (translation.json) by&#10; reordering the &quot;navbar&quot; keys.&#10;- Updated translation files in other languages with &quot;hireMe&quot;.&#10;```&#10;" />
<MESSAGE value="chore: sync locize" /> <MESSAGE value="feat: Update workspace and translation files (main)&#10;&#10;This commit includes the following changes:&#10;&#10;- Updated the workspace configuration in .idea/workspace.xml to&#10; reflect the recent npm script execution.&#10;- Modified the English translation file (translation.json) by&#10; reordering the &quot;navbar&quot; keys.&#10;- Updated translation files in other languages with &quot;hireMe&quot;." />
<MESSAGE value="feat: language browser detection" /> <MESSAGE value="```" />
<MESSAGE value="fix: misc" /> <MESSAGE value="```&#10;fix(i18n): Correct &quot;hireMe&quot; translation in navbar" />
<MESSAGE value="chore: show only necessary tags on a category" /> <MESSAGE value="```&#10;fix(i18n): Correct &quot;hireMe&quot; translation in navbar&#10;&#10;This commit corrects the translation of &quot;hireMe&quot; in the&#10;navigation bar" />
<MESSAGE value="chore: CATEGORIES_USER_TYPES_MAPPINGS" /> <MESSAGE value="```&#10;fix(i18n): Correct &quot;hireMe&quot; translation in navbar&#10;&#10;This commit corrects the translation of &quot;hireMe&quot; in the&#10;navigation bar across all supported languages. The order of&#10;the elements was also fixed to be consistent.&#10;```&#10;" />
<MESSAGE value="chore: translate userTypes" /> <MESSAGE value="fix(i18n): Correct &quot;hireMe&quot; translation in navbar&#10;&#10;This commit corrects the translation of &quot;hireMe&quot; in the&#10;navigation bar across all supported languages. The order of&#10;the elements was also fixed to be consistent." />
<option name="LAST_COMMIT_MESSAGE" value="chore: translate userTypes" /> <option name="LAST_COMMIT_MESSAGE" value="fix(i18n): Correct &quot;hireMe&quot; translation in navbar&#10;&#10;This commit corrects the translation of &quot;hireMe&quot; in the&#10;navigation bar across all supported languages. The order of&#10;the elements was also fixed to be consistent." />
</component> </component>
<component name="VgoProject"> <component name="VgoProject">
<integration-enabled>false</integration-enabled> <integration-enabled>false</integration-enabled>

View File

@ -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> <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 /> <br /><br />
<a href="https://github.com/iib0011/omni-tools/releases"> <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>
<a href="https://hub.docker.com/r/iib0011/omni-tools"> <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" /> <img src="https://img.shields.io/docker/pulls/iib0011/omni-tools?style=for-the-badge&logo=docker" />

6668
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -59,6 +59,7 @@
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"jimp": "^0.22.12", "jimp": "^0.22.12",
"js-quantities": "^1.8.0", "js-quantities": "^1.8.0",
"jspdf": "^3.0.3",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lint-staged": "^15.4.3", "lint-staged": "^15.4.3",
"locize": "^4.0.14", "locize": "^4.0.14",
@ -68,6 +69,7 @@
"nerdamer-prime": "^1.2.4", "nerdamer-prime": "^1.2.4",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"omggif": "^1.0.10", "omggif": "^1.0.10",
"onnxruntime-web": "1.21.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.2.133", "pdfjs-dist": "^5.2.133",
"playwright": "^1.45.0", "playwright": "^1.45.0",
@ -81,6 +83,7 @@
"react-image-crop": "^11.0.7", "react-image-crop": "^11.0.7",
"react-konva": "^18.2.10", "react-konva": "^18.2.10",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"styled-components": "^6.1.19",
"tesseract.js": "^6.0.0", "tesseract.js": "^6.0.0",
"type-fest": "^4.35.0", "type-fest": "^4.35.0",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
@ -99,6 +102,7 @@
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/react-helmet": "^6.1.11", "@types/react-helmet": "^6.1.11",
"@types/trusted-types": "^1.0.6",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react-swc": "^3.7.0", "@vitejs/plugin-react-swc": "^3.7.0",
@ -114,6 +118,7 @@
"husky": "^9.0.11", "husky": "^9.0.11",
"i18next-locize-backend": "^7.0.4", "i18next-locize-backend": "^7.0.4",
"locize-cli": "^10.1.1", "locize-cli": "^10.1.1",
"monaco-editor": "^0.53.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "3.1.1", "prettier": "3.1.1",
"start-server-and-test": "^2.0.4", "start-server-and-test": "^2.0.4",

View File

@ -63,6 +63,11 @@
"shortDescription": "Konvertieren Sie PDF in PNG-Bilder", "shortDescription": "Konvertieren Sie PDF in PNG-Bilder",
"title": "PDF zu PNG" "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": { "protectPdf": {
"description": "Fügen Sie Ihren PDF-Dateien sicher in Ihrem Browser einen Passwortschutz hinzu", "description": "Fügen Sie Ihren PDF-Dateien sicher in Ihrem Browser einen Passwortschutz hinzu",
"shortDescription": "PDF-Dateien sicher mit einem Passwort schützen", "shortDescription": "PDF-Dateien sicher mit einem Passwort schützen",

View File

@ -121,6 +121,7 @@
}, },
"navbar": { "navbar": {
"buyMeACoffee": "Kauf mir einen Kaffee", "buyMeACoffee": "Kauf mir einen Kaffee",
"hireMe": "Stellen Sie mich ein",
"home": "Heim", "home": "Heim",
"tools": "Werkzeuge" "tools": "Werkzeuge"
}, },

View File

@ -63,6 +63,11 @@
"shortDescription": "Convert PDF into PNG images", "shortDescription": "Convert PDF into PNG images",
"title": "PDF to PNG" "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": { "protectPdf": {
"description": "Add password protection to your PDF files securely in your browser", "description": "Add password protection to your PDF files securely in your browser",
"shortDescription": "Password protect PDF files securely", "shortDescription": "Password protect PDF files securely",

View File

@ -307,5 +307,21 @@
"shortDescription": "Quickly URL-escape a string.", "shortDescription": "Quickly URL-escape a string.",
"title": "String URL encoder" "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."
}
} }
} }

View File

@ -113,5 +113,11 @@
"zeroPaddingDescription": "Make all time components always be two digits wide.", "zeroPaddingDescription": "Make all time components always be two digits wide.",
"zeroPrintDescription": "Display the dropped parts as zero values \"00\".", "zeroPrintDescription": "Display the dropped parts as zero values \"00\".",
"zeroPrintTruncatedParts": "Zero-print Truncated Parts" "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."
} }
} }

View File

@ -133,6 +133,7 @@
}, },
"navbar": { "navbar": {
"buyMeACoffee": "Buy me a coffee", "buyMeACoffee": "Buy me a coffee",
"hireMe": "Hire me",
"home": "Home", "home": "Home",
"tools": "Tools" "tools": "Tools"
}, },

View File

@ -63,6 +63,11 @@
"shortDescription": "Convertir PDF a imágenes PNG", "shortDescription": "Convertir PDF a imágenes PNG",
"title": "PDF a 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": { "protectPdf": {
"description": "Agregue protección con contraseña a sus archivos PDF de forma segura en su navegador", "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", "shortDescription": "Proteger con contraseña los archivos PDF de forma segura",

View File

@ -122,7 +122,8 @@
"navbar": { "navbar": {
"buyMeACoffee": "Invítame a un café", "buyMeACoffee": "Invítame a un café",
"home": "Hogar", "home": "Hogar",
"tools": "Herramientas" "tools": "Herramientas",
"hireMe": "Contrátame"
}, },
"number": { "number": {
"generate": { "generate": {

View File

@ -63,6 +63,11 @@
"shortDescription": "Convertir des PDF en images PNG", "shortDescription": "Convertir des PDF en images PNG",
"title": "PDF en 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": { "protectPdf": {
"description": "Ajoutez une protection par mot de passe à vos fichiers PDF en toute sécurité dans votre navigateur", "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", "shortDescription": "Protégez les fichiers PDF en toute sécurité avec un mot de passe",

View File

@ -121,6 +121,7 @@
}, },
"navbar": { "navbar": {
"buyMeACoffee": "Offre-moi un café", "buyMeACoffee": "Offre-moi un café",
"hireMe": "Embauchez-moi",
"home": "Maison", "home": "Maison",
"tools": "Outils" "tools": "Outils"
}, },

View File

@ -82,6 +82,11 @@
"shortDescription": "PDF को PNG छवियों में परिवर्तित करें", "shortDescription": "PDF को PNG छवियों में परिवर्तित करें",
"title": "पीडीएफ से पीएनजी" "title": "पीडीएफ से पीएनजी"
}, },
"convertToPdf": {
"title": "PDF में छवि बदलें",
"description": "विभिन्न छवि प्रारूपों (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) को PDF में परिवर्तित करें और छवि का आकार और पृष्ठ की अभिविन्यास समायोजित करें।",
"shortDescription": "छवियों को PDF में परिवर्तित करें, आकार और अभिविन्यास समायोजित करने योग्य"
},
"protectPdf": { "protectPdf": {
"allowCopying": "कॉपी करने की अनुमति दें", "allowCopying": "कॉपी करने की अनुमति दें",
"allowModification": "संशोधन की अनुमति दें", "allowModification": "संशोधन की अनुमति दें",

View File

@ -121,6 +121,7 @@
}, },
"navbar": { "navbar": {
"buyMeACoffee": "मुझे कॉफी खरीदें", "buyMeACoffee": "मुझे कॉफी खरीदें",
"hireMe": "मुझे नौकरी दें",
"home": "होम", "home": "होम",
"tools": "टूल्स" "tools": "टूल्स"
}, },

View File

@ -63,6 +63,11 @@
"shortDescription": "PDFをPNG画像に変換する", "shortDescription": "PDFをPNG画像に変換する",
"title": "PDFからPNGへ" "title": "PDFからPNGへ"
}, },
"convertToPdf": {
"description": "様々な画像形式PNG、GIF、JPG、TIF、PSD、SVG、WEBP、HEIC、RAWをPDFに変換します。画像サイズとページの向きを調整できます。",
"shortDescription": "画像を PDF に変換し、画像のサイズやページの向きを調整できます。",
"title": "画像を PDF に変換"
},
"protectPdf": { "protectPdf": {
"description": "ブラウザで安全にPDFファイルにパスワード保護を追加します", "description": "ブラウザで安全にPDFファイルにパスワード保護を追加します",
"shortDescription": "PDFファイルをパスワードで安全に保護する", "shortDescription": "PDFファイルをパスワードで安全に保護する",

View File

@ -121,6 +121,7 @@
}, },
"navbar": { "navbar": {
"buyMeACoffee": "コーヒーを買ってください", "buyMeACoffee": "コーヒーを買ってください",
"hireMe": "私を雇ってください",
"home": "家", "home": "家",
"tools": "ツール" "tools": "ツール"
}, },

View File

@ -63,6 +63,11 @@
"shortDescription": "PDF naar PNG-afbeeldingen converteren", "shortDescription": "PDF naar PNG-afbeeldingen converteren",
"title": "PDF naar PNG" "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": { "protectPdf": {
"description": "Voeg wachtwoordbeveiliging toe aan uw PDF-bestanden veilig in uw browser", "description": "Voeg wachtwoordbeveiliging toe aan uw PDF-bestanden veilig in uw browser",
"shortDescription": "PDF-bestanden veilig met een wachtwoord beveiligen", "shortDescription": "PDF-bestanden veilig met een wachtwoord beveiligen",

View File

@ -121,6 +121,7 @@
}, },
"navbar": { "navbar": {
"buyMeACoffee": "Koop me een koffie", "buyMeACoffee": "Koop me een koffie",
"hireMe": "Neem mij aan",
"home": "Thuis", "home": "Thuis",
"tools": "Hulpmiddelen" "tools": "Hulpmiddelen"
}, },

View File

@ -63,6 +63,11 @@
"shortDescription": "Converter PDF em imagens PNG", "shortDescription": "Converter PDF em imagens PNG",
"title": "PDF para 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": { "protectPdf": {
"description": "Adicione proteção por senha aos seus arquivos PDF com segurança no seu navegador", "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", "shortDescription": "Proteja arquivos PDF com senha com segurança",

View File

@ -121,6 +121,7 @@
}, },
"navbar": { "navbar": {
"buyMeACoffee": "Compre-me um café", "buyMeACoffee": "Compre-me um café",
"hireMe": "Me contrate",
"home": "Lar", "home": "Lar",
"tools": "Ferramentas" "tools": "Ferramentas"
}, },

View File

@ -63,6 +63,11 @@
"shortDescription": "Конвертировать PDF в изображения PNG", "shortDescription": "Конвертировать PDF в изображения PNG",
"title": "PDF в PNG" "title": "PDF в PNG"
}, },
"convertToPdf": {
"title": "Изображения в PDF",
"description": "Преобразовать различные форматы изображений (PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW) в PDF с возможностью масштабирования изображения и выбора ориентации страницы.",
"shortDescription": "Преобразовать изображения в PDF с управлением масштабом и ориентацией"
},
"protectPdf": { "protectPdf": {
"description": "Добавьте надежную защиту паролем для ваших PDF-файлов в браузере.", "description": "Добавьте надежную защиту паролем для ваших PDF-файлов в браузере.",
"shortDescription": "Надежная защита паролем PDF-файлов", "shortDescription": "Надежная защита паролем PDF-файлов",

View File

@ -35,12 +35,12 @@
"title": "Инструменты для работы с изображениями" "title": "Инструменты для работы с изображениями"
}, },
"json": { "json": {
"description": "Инструменты для работы со структурами данных JSON — упрощайте и минимизируйте объекты JSON, выравнивайте массивы JSON, преобразуйте значения JSON в строки, анализируйте данные и многое другое.", "description": "Инструменты для работы со структурой данных JSON — упрощайте и минимизируйте объекты JSON, выравнивайте массивы JSON, преобразуйте значения JSON в строки, анализируйте данные и многое другое.",
"title": "Инструменты JSON" "title": "JSON-инструменты"
}, },
"list": { "list": {
"description": "Инструменты для работы со списками — сортировка, обратный порядок, рандомизация списков, поиск уникальных и повторяющихся элементов списка, изменение разделителей элементов списка и многое другое.", "description": "Инструменты для работы со списками — сортировка, обратный порядок, рандомизация списков, поиск уникальных и повторяющихся элементов списка, изменение разделителей элементов списка и многое другое.",
"title": "Список инструментов" "title": "Инструменты для работы со списками"
}, },
"number": { "number": {
"description": "Инструменты для работы с числами — генерация числовых последовательностей, преобразование чисел в слова и слов в числа, сортировка, округление, разложение чисел на множители и многое другое.", "description": "Инструменты для работы с числами — генерация числовых последовательностей, преобразование чисел в слова и слов в числа, сортировка, округление, разложение чисел на множители и многое другое.",
@ -52,18 +52,18 @@
}, },
"png": { "png": {
"description": "Инструменты для работы с изображениями PNG — конвертируйте PNG в JPG, создавайте прозрачные PNG, изменяйте цвета PNG, обрезайте, поворачивайте, изменяйте размер PNG и многое другое.", "description": "Инструменты для работы с изображениями PNG — конвертируйте PNG в JPG, создавайте прозрачные PNG, изменяйте цвета PNG, обрезайте, поворачивайте, изменяйте размер PNG и многое другое.",
"title": "Инструменты PNG" "title": "PNG-инструменты"
}, },
"seeAll": "Смотреть все {{title}}", "seeAll": "Посмотреть {{title}}",
"string": { "string": {
"description": "Инструменты для работы с текстом — преобразование текста в изображения, поиск и замена текста, разделение текста на фрагменты, объединение текстовых строк, повтор текста и многое другое.", "description": "Инструменты для работы с текстом — преобразование текста в изображения, поиск и замена текста, разделение текста на фрагменты, объединение текстовых строк, повтор текста и многое другое.",
"title": "Текстовые инструменты" "title": "Текстовые инструменты"
}, },
"time": { "time": {
"description": "Инструменты для работы со временем и датой — расчет разницы во времени, преобразование часовых поясов, форматирование дат, создание последовательностей дат и многое другое.", "description": "Инструменты для работы со временем и датой — расчет разницы во времени, преобразование часовых поясов, форматирование дат, создание последовательностей дат и многое другое.",
"title": "Инструменты времени" "title": "Инструменты для работы со временем"
}, },
"try": "Пытаться {{title}}", "try": "Запустить {{title}}",
"video": { "video": {
"description": "Инструменты для работы с видео — извлечение кадров из видео, создание GIF-файлов из видео, конвертация видео в различные форматы и многое другое.", "description": "Инструменты для работы с видео — извлечение кадров из видео, создание GIF-файлов из видео, конвертация видео в различные форматы и многое другое.",
"title": "Видео инструменты" "title": "Видео инструменты"
@ -81,20 +81,20 @@
} }
}, },
"hero": { "hero": {
"brand": "ОмниИнструменты", "brand": "OmniTools",
"description": "Повысьте свою производительность с OmniTools — лучшим набором инструментов для быстрого решения задач! Получите доступ к тысячам удобных утилит для редактирования изображений, текста, списков и данных — и всё это прямо в браузере.", "description": "Повысьте свою производительность с OmniTools — лучшим набором инструментов для быстрого решения задач! Получите доступ к тысячам удобных утилит для редактирования изображений, текста, списков и данных — и всё это прямо в браузере.",
"examples": { "examples": {
"calculateNumberSum": "Вычислить сумму чисел", "calculateNumberSum": "Вычислить сумму чисел",
"changeGifSpeed": "Изменить скорость GIF-анимации", "changeGifSpeed": "Изменить скорость GIF-анимации",
"compressPng": "Сжать PNG", "compressPng": "Сжать PNG",
"createTransparentImage": "Создать прозрачное изображение", "createTransparentImage": "Создать прозрачное изображение",
"prettifyJson": "Упрощение JSON", "prettifyJson": "Отформатировать JSON",
"sortList": "Сортировать список", "sortList": "Сортировать список",
"splitPdf": "Разделить PDF", "splitPdf": "Разделить PDF",
"splitText": "Разделить текст", "splitText": "Разделить текст",
"trimVideo": "Обрезать видео" "trimVideo": "Обрезать видео"
}, },
"searchPlaceholder": "Искать все инструменты", "searchPlaceholder": "Найти инструмент…",
"title": "Выполняйте задачи быстро с помощью" "title": "Выполняйте задачи быстро с помощью"
}, },
"inputFooter": { "inputFooter": {
@ -110,7 +110,7 @@
}, },
"reverse": { "reverse": {
"description": "Это очень простое браузерное приложение, которое выводит все элементы списка в обратном порядке. Элементы ввода можно разделять любым символом, и вы также можете изменить разделитель для элементов перевёрнутого списка.", "description": "Это очень простое браузерное приложение, которое выводит все элементы списка в обратном порядке. Элементы ввода можно разделять любым символом, и вы также можете изменить разделитель для элементов перевёрнутого списка.",
"name": "Обеспечить регресс", "name": "Вывести в обратном порядке",
"shortDescription": "Быстро перевернуть список" "shortDescription": "Быстро перевернуть список"
}, },
"sort": { "sort": {
@ -121,6 +121,7 @@
}, },
"navbar": { "navbar": {
"buyMeACoffee": "Купи мне кофе", "buyMeACoffee": "Купи мне кофе",
"hireMe": "Нанять меня",
"home": "Дом", "home": "Дом",
"tools": "Инструменты" "tools": "Инструменты"
}, },

View File

@ -63,6 +63,11 @@
"shortDescription": "将 PDF 转换为 PNG 图像", "shortDescription": "将 PDF 转换为 PNG 图像",
"title": "PDF 转 PNG" "title": "PDF 转 PNG"
}, },
"convertToPdf": {
"title": "将图像转换为 PDF",
"description": "将各种图像格式PNG, GIF, JPG, TIF, PSD, SVG, WEBP, HEIC, RAW转换为 PDF并可调整图像大小和页面方向。",
"shortDescription": "将图像转换为 PDF可调整大小和方向"
},
"protectPdf": { "protectPdf": {
"description": "在浏览器中安全地为 PDF 文件添加密码保护", "description": "在浏览器中安全地为 PDF 文件添加密码保护",
"shortDescription": "使用密码安全地保护 PDF 文件", "shortDescription": "使用密码安全地保护 PDF 文件",

View File

@ -121,6 +121,7 @@
}, },
"navbar": { "navbar": {
"buyMeACoffee": "请我喝杯咖啡", "buyMeACoffee": "请我喝杯咖啡",
"hireMe": "雇用我",
"home": "家", "home": "家",
"tools": "工具" "tools": "工具"
}, },

View File

@ -127,7 +127,10 @@ const Navbar: React.FC<NavbarProps> = ({
></iframe>, ></iframe>,
<Button <Button
onClick={() => { 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' }} sx={{ borderRadius: '100px' }}
variant={'contained'} variant={'contained'}
@ -135,11 +138,11 @@ const Navbar: React.FC<NavbarProps> = ({
<Icon <Icon
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
fontSize={25} fontSize={25}
icon={'mdi:heart-outline'} icon={'hugeicons:job-search'}
/> />
} }
> >
{t('navbar.buyMeACoffee')} {t('navbar.hireMe')}
</Button> </Button>
]; ];
const drawerList = ( const drawerList = (

View File

@ -41,7 +41,7 @@ export async function changeAudioSpeed(
const outputName = `output.${outputFormat}`; const outputName = `output.${outputFormat}`;
await ffmpeg.writeFile(fileName, await fetchFile(input)); await ffmpeg.writeFile(fileName, await fetchFile(input));
const audioFilter = computeAudioFilter(newSpeed); const audioFilter = computeAudioFilter(newSpeed);
let args = ['-i', fileName, '-filter:a', audioFilter]; const args = ['-i', fileName, '-filter:a', audioFilter];
if (outputFormat === 'mp3') { if (outputFormat === 'mp3') {
args.push('-b:a', '192k', '-f', 'mp3', outputName); args.push('-b:a', '192k', '-f', 'mp3', outputName);
} else if (outputFormat === 'aac') { } else if (outputFormat === 'aac') {
@ -64,7 +64,7 @@ export async function changeAudioSpeed(
let mimeType = 'audio/mp3'; let mimeType = 'audio/mp3';
if (outputFormat === 'aac') mimeType = 'audio/aac'; if (outputFormat === 'aac') mimeType = 'audio/aac';
if (outputFormat === 'wav') mimeType = 'audio/wav'; 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( const newFile = new File(
[blob], [blob],
fileName.replace(/\.[^/.]+$/, `-${newSpeed}x.${outputFormat}`), fileName.replace(/\.[^/.]+$/, `-${newSpeed}x.${outputFormat}`),

View File

@ -57,7 +57,7 @@ export async function extractAudioFromVideo(
return new File( return new File(
[ [
new Blob([extractedAudio], { new Blob([extractedAudio as any], {
type: `audio/${configuredOutputAudioFormat}` type: `audio/${configuredOutputAudioFormat}`
}) })
], ],

View File

@ -105,7 +105,7 @@ export async function mergeAudioFiles(
return new File( return new File(
[ [
new Blob([mergedAudio], { new Blob([mergedAudio as any], {
type: mimeType type: mimeType
}) })
], ],

View File

@ -98,7 +98,7 @@ export async function trimAudio(
return new File( return new File(
[ [
new Blob([trimmedAudio], { new Blob([trimmedAudio as any], {
type: mimeType type: mimeType
}) })
], ],

View File

@ -40,7 +40,7 @@ const initialValues: InitialValuesType = {
// WiFi // WiFi
wifiSsid: '', wifiSsid: '',
wifiPassword: '', wifiPassword: '',
wifiEncryption: 'WPA/WPA2', wifiEncryption: 'WPA',
// vCard // vCard
vCardName: '', vCardName: '',
@ -353,7 +353,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
label="Encryption Type" label="Encryption Type"
margin="normal" margin="normal"
> >
<MenuItem value="WPA/WPA2">WPA/WPA2</MenuItem> <MenuItem value="WPA">WPA</MenuItem>
<MenuItem value="WEP">WEP</MenuItem> <MenuItem value="WEP">WEP</MenuItem>
<MenuItem value="None">None</MenuItem> <MenuItem value="None">None</MenuItem>
</TextField> </TextField>

View File

@ -7,7 +7,7 @@ export type QRCodeType =
| 'WiFi' | 'WiFi'
| 'vCard'; | 'vCard';
export type WifiEncryptionType = 'WPA/WPA2' | 'WEP' | 'None'; export type WifiEncryptionType = 'WPA' | 'WEP' | 'None';
export interface InitialValuesType { export interface InitialValuesType {
qrCodeType: QRCodeType; qrCodeType: QRCodeType;

View File

@ -148,7 +148,7 @@ export const processImage = async (
const data = await ffmpeg.readFile('output.gif'); const data = await ffmpeg.readFile('output.gif');
// Create a new File object // 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) { } catch (error) {
console.error('Error processing GIF with FFmpeg:', error); console.error('Error processing GIF with FFmpeg:', error);
// Fall back to canvas method if FFmpeg processing fails // Fall back to canvas method if FFmpeg processing fails

View File

@ -66,7 +66,7 @@ export const processImage = async (
// Read the output file // Read the output file
const data = await ffmpeg.readFile('output.' + file.name.split('.').pop()); 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) { } catch (error) {
console.error('Error processing image:', error); console.error('Error processing image:', error);
return null; return null;

View File

@ -3,6 +3,7 @@ import voltageDropInWire from './voltageDropInWire';
import sphereArea from './sphereArea'; import sphereArea from './sphereArea';
import sphereVolume from './sphereVolume'; import sphereVolume from './sphereVolume';
import slackline from './slackline'; import slackline from './slackline';
export default [ export default [
ohmslaw, ohmslaw,
voltageDropInWire, voltageDropInWire,

View File

@ -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();
});
});

View 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" />
}
/>
);
}

View 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'))
});

View 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);
}
}

View 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
};

View File

@ -7,6 +7,7 @@ import { tool as compressPdfTool } from './compress-pdf/meta';
import { tool as protectPdfTool } from './protect-pdf/meta'; import { tool as protectPdfTool } from './protect-pdf/meta';
import { meta as pdfToEpub } from './pdf-to-epub/meta'; import { meta as pdfToEpub } from './pdf-to-epub/meta';
import { tool as pdfEditor } from './editor/meta'; import { tool as pdfEditor } from './editor/meta';
import { tool as convertToPdf } from './convert-to-pdf/meta';
export const pdfTools: DefinedTool[] = [ export const pdfTools: DefinedTool[] = [
pdfEditor, pdfEditor,
@ -16,5 +17,6 @@ export const pdfTools: DefinedTool[] = [
protectPdfTool, protectPdfTool,
mergePdf, mergePdf,
pdfToEpub, pdfToEpub,
pdfPdfToPng pdfPdfToPng,
convertToPdf
]; ];

View File

@ -70,7 +70,9 @@ export async function splitPdf(
const newPdfBytes = await newPdf.save(); const newPdfBytes = await newPdf.save();
const newFileName = pdfFile.name.replace('.pdf', '-extracted.pdf'); 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 mergedPdfBytes = await mergedPdf.save();
const mergedFileName = 'merged.pdf'; const mergedFileName = 'merged.pdf';
return new File([mergedPdfBytes], mergedFileName, { return new File([mergedPdfBytes as any], mergedFileName, {
type: 'application/pdf' type: 'application/pdf'
}); });
} }

View File

@ -28,7 +28,7 @@ export async function convertPdfToPngImages(pdfFile: File): Promise<{
canvas.width = viewport.width; canvas.width = viewport.width;
canvas.height = viewport.height; 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) => const blob = await new Promise<Blob>((resolve) =>
canvas.toBlob((b) => b && resolve(b), 'image/png') canvas.toBlob((b) => b && resolve(b), 'image/png')

View File

@ -78,5 +78,7 @@ export async function rotatePdf(
const modifiedPdfBytes = await pdfDoc.save(); const modifiedPdfBytes = await pdfDoc.save();
const newFileName = pdfFile.name.replace('.pdf', '-rotated.pdf'); 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'
});
} }

View File

@ -70,5 +70,7 @@ export async function splitPdf(
const newPdfBytes = await newPdf.save(); const newPdfBytes = await newPdf.save();
const newFileName = pdfFile.name.replace('.pdf', '-extracted.pdf'); 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'
});
} }

View File

@ -21,6 +21,7 @@ import { tool as stringCensor } from './censor/meta';
import { tool as stringPasswordGenerator } from './password-generator/meta'; import { tool as stringPasswordGenerator } from './password-generator/meta';
import { tool as stringEncodeUrl } from './url-encode/meta'; import { tool as stringEncodeUrl } from './url-encode/meta';
import { tool as StringDecodeUrl } from './url-decode/meta'; import { tool as StringDecodeUrl } from './url-decode/meta';
import { tool as stringUnicode } from './unicode/meta';
export const stringTools = [ export const stringTools = [
stringSplit, stringSplit,
@ -45,5 +46,6 @@ export const stringTools = [
stringPasswordGenerator, stringPasswordGenerator,
stringEncodeUrl, stringEncodeUrl,
StringDecodeUrl, StringDecodeUrl,
stringUnicode,
stringHiddenCharacterDetector stringHiddenCharacterDetector
]; ];

View 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')
}}
/>
);
}

View 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'))
});

View 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));
});
}
}

View File

@ -0,0 +1,4 @@
export type InitialValuesType = {
mode: 'encode' | 'decode';
uppercase: boolean;
};

View 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!');
});
});

View File

@ -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');
});
});

View 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 }}
/>
);
}

View 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'))
});

View 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');
}

View File

@ -0,0 +1,3 @@
export type InitialValuesType = {
decimalPlaces: string;
};

View File

@ -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 timeConvertUnixToDate } from './convert-unix-to-date/meta';
import { tool as timeCrontabGuru } from './crontab-guru/meta'; import { tool as timeCrontabGuru } from './crontab-guru/meta';
import { tool as timeBetweenDates } from './time-between-dates/meta'; import { tool as timeBetweenDates } from './time-between-dates/meta';
@ -17,5 +18,6 @@ export const timeTools = [
timeBetweenDates, timeBetweenDates,
timeCrontabGuru, timeCrontabGuru,
checkLeapYear, checkLeapYear,
timeConvertUnixToDate timeConvertUnixToDate,
timeConvertTimeToDecimal
]; ];

View File

@ -101,7 +101,7 @@ export default function ChangeSpeed({
const data = await ffmpeg.readFile(outputName); const data = await ffmpeg.readFile(outputName);
// Create new file from processed data // 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( const newFile = new File(
[blob], [blob],
file.name.replace('.mp4', `-${newSpeed}x.mp4`), file.name.replace('.mp4', `-${newSpeed}x.mp4`),

View File

@ -53,7 +53,7 @@ export async function compressVideo(
} }
const compressedData = await ffmpeg.readFile(outputName); const compressedData = await ffmpeg.readFile(outputName);
return new File( 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`, `${input.name.replace(/\.[^/.]+$/, '')}_compressed_${options.width}p.mp4`,
{ type: 'video/mp4' } { type: 'video/mp4' }
); );

View File

@ -60,7 +60,7 @@ export async function cropVideo(
const croppedData = await ffmpeg.readFile(outputName); const croppedData = await ffmpeg.readFile(outputName);
return await new File( return await new File(
[new Blob([croppedData], { type: 'video/mp4' })], [new Blob([croppedData as any], { type: 'video/mp4' })],
`${input.name.replace(/\.[^/.]+$/, '')}_cropped.mp4`, `${input.name.replace(/\.[^/.]+$/, '')}_cropped.mp4`,
{ type: 'video/mp4' } { type: 'video/mp4' }
); );

View File

@ -36,7 +36,7 @@ export async function flipVideo(
const flippedData = await ffmpeg.readFile(outputName); const flippedData = await ffmpeg.readFile(outputName);
return new File( return new File(
[new Blob([flippedData], { type: 'video/mp4' })], [new Blob([flippedData as any], { type: 'video/mp4' })],
`${input.name.replace(/\.[^/.]+$/, '')}_flipped.mp4`, `${input.name.replace(/\.[^/.]+$/, '')}_flipped.mp4`,
{ type: 'video/mp4' } { type: 'video/mp4' }
); );

View File

@ -71,7 +71,7 @@ export default function ChangeSpeed({ title }: ToolComponentProps) {
const data = await ffmpeg.readFile('output.gif'); const data = await ffmpeg.readFile('output.gif');
// Create a new file from the processed data // 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( const newFile = new File(
[blob], [blob],
file.name.replace('.gif', `-${newSpeed}x.gif`), file.name.replace('.gif', `-${newSpeed}x.gif`),

View File

@ -3,7 +3,7 @@ import { lazy } from 'react';
export const tool = defineTool('video', { export const tool = defineTool('video', {
path: 'loop', path: 'loop',
icon: 'material-symbols:loop', icon: 'ic:outline-loop',
keywords: ['video', 'loop', 'repeat', 'continuous'], keywords: ['video', 'loop', 'repeat', 'continuous'],
component: lazy(() => import('./index')), component: lazy(() => import('./index')),

View File

@ -34,7 +34,7 @@ export async function loopVideo(
const loopedData = await ffmpeg.readFile(outputName); const loopedData = await ffmpeg.readFile(outputName);
return await new File( return await new File(
[new Blob([loopedData], { type: 'video/mp4' })], [new Blob([loopedData as any], { type: 'video/mp4' })],
`${input.name.replace(/\.[^/.]+$/, '')}_looped.mp4`, `${input.name.replace(/\.[^/.]+$/, '')}_looped.mp4`,
{ type: 'video/mp4' } { type: 'video/mp4' }
); );

View File

@ -112,7 +112,7 @@ export async function mergeVideos(
throw new Error('Output file is empty or corrupted'); 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) { } catch (error) {
console.error('Error merging videos:', error); console.error('Error merging videos:', error);
throw error instanceof Error throw error instanceof Error

View File

@ -37,7 +37,7 @@ export async function rotateVideo(
const rotatedData = await ffmpeg.readFile(outputName); const rotatedData = await ffmpeg.readFile(outputName);
return new File( return new File(
[new Blob([rotatedData], { type: 'video/mp4' })], [new Blob([rotatedData as any], { type: 'video/mp4' })],
`${input.name.replace(/\.[^/.]+$/, '')}_rotated.mp4`, `${input.name.replace(/\.[^/.]+$/, '')}_rotated.mp4`,
{ type: 'video/mp4' } { type: 'video/mp4' }
); );

View File

@ -67,7 +67,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
]); ]);
// Retrieve the processed file // Retrieve the processed file
const trimmedData = await ffmpeg.readFile(outputName); 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( const trimmedFile = new File(
[trimmedBlob], [trimmedBlob],
`${input.name.replace(/\.[^/.]+$/, '')}_trimmed.mp4`, `${input.name.replace(/\.[^/.]+$/, '')}_trimmed.mp4`,

View File

@ -1,8 +1,11 @@
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import React, { useState } from 'react'; import React, { useState } from 'react';
import * as Yup from 'yup';
import ToolContent from '@components/ToolContent'; import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool'; import { ToolComponentProps } from '@tools/defineTool';
import { GetGroupsType } from '@components/options/ToolOptions'; import { GetGroupsType } from '@components/options/ToolOptions';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import { updateNumberField } from '@utils/string';
import { InitialValuesType } from './types'; import { InitialValuesType } from './types';
import ToolVideoInput from '@components/input/ToolVideoInput'; import ToolVideoInput from '@components/input/ToolVideoInput';
import ToolFileResult from '@components/result/ToolFileResult'; import ToolFileResult from '@components/result/ToolFileResult';
@ -13,9 +16,19 @@ import { fetchFile } from '@ffmpeg/util';
const initialValues: InitialValuesType = { const initialValues: InitialValuesType = {
quality: 'mid', quality: 'mid',
fps: '10', 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({ export default function VideoToGif({
title, title,
longDescription longDescription
@ -26,14 +39,16 @@ export default function VideoToGif({
const compute = (values: InitialValuesType, input: File | null) => { const compute = (values: InitialValuesType, input: File | null) => {
if (!input) return; if (!input) return;
const { fps, scale } = values; const { fps, scale, start, end } = values;
let ffmpeg: FFmpeg | null = null; let ffmpeg: FFmpeg | null = null;
let ffmpegLoaded = false; let ffmpegLoaded = false;
const convertVideoToGif = async ( const convertVideoToGif = async (
file: File, file: File,
fps: string, fps: string,
scale: string scale: string,
start: number,
end: number
): Promise<void> => { ): Promise<void> => {
setLoading(true); setLoading(true);
@ -58,6 +73,10 @@ export default function VideoToGif({
await ffmpeg.exec([ await ffmpeg.exec([
'-i', '-i',
fileName, fileName,
'-ss',
start.toString(),
'-to',
end.toString(),
'-vf', '-vf',
`fps=${fps},scale=${scale},palettegen`, `fps=${fps},scale=${scale},palettegen`,
'palette.png' 'palette.png'
@ -68,6 +87,10 @@ export default function VideoToGif({
fileName, fileName,
'-i', '-i',
'palette.png', 'palette.png',
'-ss',
start.toString(),
'-to',
end.toString(),
'-filter_complex', '-filter_complex',
`fps=${fps},scale=${scale}[x];[x][1:v]paletteuse`, `fps=${fps},scale=${scale}[x];[x][1:v]paletteuse`,
outputName outputName
@ -75,7 +98,7 @@ export default function VideoToGif({
const data = await ffmpeg.readFile(outputName); 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, { const convertedFile = new File([blob], outputName, {
type: 'image/gif' 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 = ({ const getGroups: GetGroupsType<InitialValuesType> | null = ({
@ -141,6 +164,28 @@ export default function VideoToGif({
/> />
</Box> </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 <ToolContent
title={title} title={title}
input={input} input={input}
inputComponent={ renderCustomInput={({ start, end }, setFieldValue) => {
<ToolVideoInput value={input} onChange={setInput} title="Input Video" /> 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={ resultComponent={
loading ? ( loading ? (
<ToolFileResult <ToolFileResult

View File

@ -2,4 +2,6 @@ export type InitialValuesType = {
quality: 'mid' | 'high' | 'low' | 'ultra'; quality: 'mid' | 'high' | 'low' | 'ultra';
fps: string; fps: string;
scale: string; scale: string;
start: number;
end: number;
}; };

58
src/utils/time.ts Normal file
View 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 };
}