diff --git a/.coding-aider-plans/refactor_tools_toolcontent.md b/.coding-aider-plans/refactor_tools_toolcontent.md
deleted file mode 100644
index 997ac4b..0000000
--- a/.coding-aider-plans/refactor_tools_toolcontent.md
+++ /dev/null
@@ -1,20 +0,0 @@
-[Coding Aider Plan]
-
-## Overview
-This plan outlines the refactoring of existing tools to utilize a `ToolContent` component. This will standardize the structure and styling of tool content across the application, improving maintainability and user experience.
-
-## Problem Description
-Currently, some tools directly render their content without using a common `ToolContent` component. This leads to inconsistencies in styling, layout, and overall structure. It also makes it harder to apply global changes or updates to the tool content areas.
-
-## Goals
-- Identify tools that do not currently use `ToolContent`.
-- Implement `ToolContent` in these tools.
-- Ensure consistent styling and layout across all tools.
-
-## Additional Notes and Constraints
-- The `ToolContent` component should be flexible enough to accommodate the different types of content used by each tool.
-- Ensure that the refactoring does not introduce any regressions or break existing functionality.
-- Consider creating a subplan if the number of tools requiring changes is large or if individual tools require complex modifications.
-
-## References
-- Existing tools that already use `ToolContent` can serve as examples.
diff --git a/.coding-aider-plans/refactor_tools_toolcontent_checklist.md b/.coding-aider-plans/refactor_tools_toolcontent_checklist.md
deleted file mode 100644
index 50ef392..0000000
--- a/.coding-aider-plans/refactor_tools_toolcontent_checklist.md
+++ /dev/null
@@ -1,9 +0,0 @@
-[Coding Aider Plan - Checklist]
-
-- [ ] Create `ToolContent` component if it doesn't exist.
-- [ ] Identify tools that do not use `ToolContent`.
-- [x] For each identified tool:
- - [x] Implement `ToolContent` wrapper.
- - [ ] Adjust styling as needed to match existing design.
- - [ ] Test the tool to ensure it functions correctly.
-- [ ] Review all modified tools to ensure consistency.
diff --git a/.coding-aider-plans/refactor_tools_toolcontent_context.yaml b/.coding-aider-plans/refactor_tools_toolcontent_context.yaml
deleted file mode 100644
index 3fba323..0000000
--- a/.coding-aider-plans/refactor_tools_toolcontent_context.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-files:
-- path: src\pages\tools\list\duplicate\index.tsx
- readOnly: false
-- path: src\pages\tools\list\index.ts
- readOnly: false
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 1a4f227..b67311d 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,9 +4,17 @@
-
+
+
+
+
+
+
+
+
-
+
+
@@ -23,7 +31,7 @@
@@ -151,56 +159,56 @@
- {
+ "keyToString": {
+ "ASKED_ADD_EXTERNAL_FILES": "true",
+ "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
+ "Docker.Dockerfile build.executor": "Run",
+ "Docker.Dockerfile.executor": "Run",
+ "Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
+ "Playwright.JoinText Component.executor": "Run",
+ "Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
+ "RunOnceActivity.OpenProjectViewOnStart": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "Vitest.compute function (1).executor": "Run",
+ "Vitest.compute function.executor": "Run",
+ "Vitest.mergeText.executor": "Run",
+ "Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
+ "Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
+ "Vitest.parsePageRanges.executor": "Run",
+ "Vitest.removeDuplicateLines function.executor": "Run",
+ "Vitest.removeDuplicateLines function.newlines option.executor": "Run",
+ "Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
+ "Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
+ "Vitest.replaceText function.executor": "Run",
+ "Vitest.timeBetweenDates.executor": "Run",
+ "git-widget-placeholder": "main",
+ "ignore.virus.scanning.warn.message": "true",
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/@types",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "npm.build.executor": "Run",
+ "npm.dev.executor": "Run",
+ "npm.lint.executor": "Run",
+ "npm.prebuild.executor": "Run",
+ "npm.script:create:tool.executor": "Run",
+ "npm.test.executor": "Run",
+ "npm.test:e2e.executor": "Run",
+ "npm.test:e2e:run.executor": "Run",
+ "prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
+ "project.structure.last.edited": "Problems",
+ "project.structure.proportion": "0.0",
+ "project.structure.side.proportion": "0.2",
+ "settings.editor.selected.configurable": "refactai_advanced_settings",
+ "ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
@@ -838,8 +850,6 @@
-
-
@@ -863,7 +873,9 @@
-
+
+
+
diff --git a/src/components/result/ToolFileResult.tsx b/src/components/result/ToolFileResult.tsx
index 7b07578..f0f7619 100644
--- a/src/components/result/ToolFileResult.tsx
+++ b/src/components/result/ToolFileResult.tsx
@@ -15,7 +15,7 @@ export default function ToolFileResult({
}: {
title?: string;
value: File | null;
- extension: string;
+ extension?: string;
loading?: boolean;
loadingText?: string;
}) {
@@ -50,9 +50,11 @@ export default function ToolFileResult({
const handleDownload = () => {
if (value) {
- const hasExtension = value.name.includes('.');
- const filename = hasExtension ? value.name : `${value.name}.${extension}`;
-
+ let filename: string = value.name;
+ if (extension) {
+ const hasExtension = filename.includes('.');
+ filename = hasExtension ? filename : `${filename}.${extension}`;
+ }
const blob = new Blob([value], { type: value.type });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
diff --git a/src/pages/tools/image/generic/compress/index.tsx b/src/pages/tools/image/generic/compress/index.tsx
new file mode 100644
index 0000000..64a07e1
--- /dev/null
+++ b/src/pages/tools/image/generic/compress/index.tsx
@@ -0,0 +1,123 @@
+import React, { useContext, useState } from 'react';
+import { InitialValuesType } from './types';
+import { compressImage } from './service';
+import ToolContent from '@components/ToolContent';
+import ToolImageInput from '@components/input/ToolImageInput';
+import { ToolComponentProps } from '@tools/defineTool';
+import ToolFileResult from '@components/result/ToolFileResult';
+import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
+import { Box } from '@mui/material';
+import Typography from '@mui/material/Typography';
+import { CustomSnackBarContext } from '../../../../../contexts/CustomSnackBarContext';
+import { updateNumberField } from '@utils/string';
+
+const initialValues: InitialValuesType = {
+ maxFileSizeInMB: 1.0,
+ quality: 80
+};
+
+export default function CompressImage({ title }: ToolComponentProps) {
+ const [input, setInput] = useState(null);
+ const [result, setResult] = useState(null);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [originalSize, setOriginalSize] = useState(null); // Store original file size
+ const [compressedSize, setCompressedSize] = useState(null); // Store compressed file size
+ const { showSnackBar } = useContext(CustomSnackBarContext);
+
+ const compute = async (values: InitialValuesType, input: File | null) => {
+ if (!input) return;
+
+ setOriginalSize(input.size);
+ try {
+ setIsProcessing(true);
+
+ const compressed = await compressImage(input, values);
+
+ if (compressed) {
+ setResult(compressed);
+ setCompressedSize(compressed.size);
+ } else {
+ showSnackBar('Failed to compress image. Please try again.', 'error');
+ }
+ } catch (err) {
+ console.error('Error in compression:', err);
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ return (
+
+ }
+ resultComponent={
+
+ }
+ initialValues={initialValues}
+ getGroups={({ values, updateField }) => [
+ {
+ title: 'Compression options',
+ component: (
+
+
+ updateNumberField(value, 'maxFileSizeInMB', updateField)
+ }
+ value={values.maxFileSizeInMB}
+ />
+
+ updateNumberField(value, 'quality', updateField)
+ }
+ value={values.quality}
+ />
+
+ )
+ },
+ {
+ title: 'File sizes',
+ component: (
+
+
+ {originalSize !== null && (
+
+ Original Size: {(originalSize / 1024).toFixed(2)} KB
+
+ )}
+ {compressedSize !== null && (
+
+ Compressed Size: {(compressedSize / 1024).toFixed(2)} KB
+
+ )}
+
+
+ )
+ }
+ ]}
+ compute={compute}
+ setInput={setInput}
+ />
+ );
+}
diff --git a/src/pages/tools/image/generic/compress/meta.ts b/src/pages/tools/image/generic/compress/meta.ts
new file mode 100644
index 0000000..fb34481
--- /dev/null
+++ b/src/pages/tools/image/generic/compress/meta.ts
@@ -0,0 +1,14 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+
+export const tool = defineTool('image-generic', {
+ name: 'Compress Image',
+ path: 'compress',
+ component: lazy(() => import('./index')),
+ icon: 'material-symbols-light:compress-rounded',
+ description:
+ 'Compress images to reduce file size while maintaining reasonable quality.',
+ shortDescription:
+ 'Compress images to reduce file size while maintaining reasonable quality.',
+ keywords: ['image', 'compress', 'reduce', 'quality']
+});
diff --git a/src/pages/tools/image/generic/compress/service.ts b/src/pages/tools/image/generic/compress/service.ts
new file mode 100644
index 0000000..b1a6f80
--- /dev/null
+++ b/src/pages/tools/image/generic/compress/service.ts
@@ -0,0 +1,30 @@
+import { InitialValuesType } from './types';
+import imageCompression from 'browser-image-compression';
+
+export const compressImage = async (
+ file: File,
+ options: InitialValuesType
+): Promise => {
+ try {
+ const { maxFileSizeInMB, quality } = options;
+
+ // Configuration for the compression library
+ const compressionOptions = {
+ maxSizeMB: maxFileSizeInMB,
+ maxWidthOrHeight: 1920, // Reasonable default for most use cases
+ useWebWorker: true,
+ initialQuality: quality / 100 // Convert percentage to decimal
+ };
+
+ // Compress the image
+ const compressedFile = await imageCompression(file, compressionOptions);
+
+ // Create a new file with the original name
+ return new File([compressedFile], file.name, {
+ type: compressedFile.type
+ });
+ } catch (error) {
+ console.error('Error compressing image:', error);
+ return null;
+ }
+};
diff --git a/src/pages/tools/image/generic/compress/types.ts b/src/pages/tools/image/generic/compress/types.ts
new file mode 100644
index 0000000..15e8381
--- /dev/null
+++ b/src/pages/tools/image/generic/compress/types.ts
@@ -0,0 +1,4 @@
+export interface InitialValuesType {
+ maxFileSizeInMB: number;
+ quality: number;
+}
diff --git a/src/pages/tools/image/generic/index.ts b/src/pages/tools/image/generic/index.ts
index 15f2930..07c41cc 100644
--- a/src/pages/tools/image/generic/index.ts
+++ b/src/pages/tools/image/generic/index.ts
@@ -1,3 +1,4 @@
import { tool as resizeImage } from './resize/meta';
+import { tool as compressImage } from './compress/meta';
-export const imageGenericTools = [resizeImage];
+export const imageGenericTools = [resizeImage, compressImage];