Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"onCommand:extension.compressFolder",
"onCommand:extension.getCompressionCount"
],
"main": "./out/extension.js",
"main": "./out/src/extension.js",
"contributes": {
"configuration": [
{
Expand Down
9 changes: 9 additions & 0 deletions src/commands/compressFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Uri } from 'vscode';
import { CompressionService } from '../services/compressionService';

/**
* Command handler for compressing a single file
*/
export function compressFileCommand(file: Uri): void {
CompressionService.compressImage(file);
}
17 changes: 17 additions & 0 deletions src/commands/compressFolder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import vscode = require('vscode');
import { Uri } from 'vscode';
import { CompressionService } from '../services/compressionService';

/**
* Command handler for compressing all images in a folder
*/
export function compressFolderCommand(folder: Uri): void {
vscode.workspace
.findFiles(
new vscode.RelativePattern(
folder.path,
`**/*.{png,jpg,jpeg,webp}`
)
)
.then((files: Uri[]) => files.forEach(CompressionService.compressImage));
}
64 changes: 64 additions & 0 deletions src/commands/compressGitStage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import vscode = require('vscode');
import { Uri } from 'vscode';
import { spawnSync } from 'child_process';
import { CompressionService } from '../services/compressionService';

/**
* Compress staged image files in git
*/
function compressStageFiles(editorPath: string): void {
try {
const lines = spawnSync(
'git',
['diff', '--staged', '--diff-filter=ACMR', '--name-only', '-z'],
{ encoding: 'utf-8', cwd: editorPath }
);

const files = lines.stdout
.replace(/\u0000$/, '')
.split('\u0000')
.filter((f: string) => /\.(png|jpg|jpeg|webp)$/.test(f));

if (files.length === 0) {
vscode.window.showInformationMessage(
`TinyPNG: No images found in the git stage.`
);
return;
}

files.forEach((f: string) =>
CompressionService.compressImage(Uri.parse(`${editorPath}/${f}`))
);
} catch (err) {
vscode.window.showErrorMessage(
`TinyPNG: ${(err as Error).message}`
);
}
}

/**
* Command handler for compressing git staged images
*/
export function compressGitStageCommand(): void {
const folders = vscode.workspace.workspaceFolders;

if (!folders) {
vscode.window.showInformationMessage(
`TinyPNG: No editor path found.`
);
return;
}

if (folders.length <= 1) {
compressStageFiles(folders[0].uri.fsPath);
return;
}

const folderNames = folders.map((folder) => folder.name);
vscode.window.showQuickPick(folderNames).then((folderName: string | undefined) => {
const folder = folders.find((f) => f.name === folderName);
if (folder) {
compressStageFiles(folder.uri.fsPath);
}
});
}
14 changes: 14 additions & 0 deletions src/commands/getCompressionCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import vscode = require('vscode');
import { CompressionService } from '../services/compressionService';

/**
* Command handler for getting the compression count
*/
export function getCompressionCountCommand(): void {
CompressionService.validate(() => {
const count = CompressionService.getCompressionCount();
vscode.window.showInformationMessage(
`TinyPNG: You already used ${count} compression(s) this month.`
);
});
}
54 changes: 54 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import vscode = require('vscode');
import { CompressionService } from './services/compressionService';
import { compressFileCommand } from './commands/compressFile';
import { compressFolderCommand } from './commands/compressFolder';
import { compressGitStageCommand } from './commands/compressGitStage';
import { getCompressionCountCommand } from './commands/getCompressionCount';
import { handleValidationError } from './utils/errorHandler';

/**
* Activate the extension
*/
export function activate(context: vscode.ExtensionContext): void {
// Initialize the compression service with API key
CompressionService.initialize();

// Validate the API key
CompressionService.validate(
() => console.log('Validation successful!'),
handleValidationError
);

// Register command: Compress single file
const disposableCompressFile = vscode.commands.registerCommand(
'extension.compressFile',
compressFileCommand
);
context.subscriptions.push(disposableCompressFile);

// Register command: Compress folder
const disposableCompressFolder = vscode.commands.registerCommand(
'extension.compressFolder',
compressFolderCommand
);
context.subscriptions.push(disposableCompressFolder);

// Register command: Get compression count
const disposableCompressionCount = vscode.commands.registerCommand(
'extension.getCompressionCount',
getCompressionCountCommand
);
context.subscriptions.push(disposableCompressionCount);

// Register command: Compress git staged files
const disposableCompressGitStage = vscode.commands.registerCommand(
'extension.compressGitStage',
compressGitStageCommand
);
context.subscriptions.push(disposableCompressGitStage);
}

/**
* Deactivate the extension
*/
export function deactivate(): void {}
77 changes: 77 additions & 0 deletions src/services/compressionService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import vscode = require('vscode');
import tinify = require('tinify');
import { Uri } from 'vscode';
import { generateDestinationPath } from '../utils/fileUtils';
import { handleCompressionError } from '../utils/errorHandler';
import { ConfigService } from './configService';

/**
* Service for handling image compression operations
*/
export class CompressionService {
/**
* Initialize the TinyPNG API with the API key
*/
public static initialize(): void {
const apiKey = ConfigService.getApiKey();
if (apiKey) {
tinify.key = apiKey;
}
}

/**
* Validate the TinyPNG API key
*/
public static validate(
onSuccess: () => void = () => {},
onFailure?: (error: Error) => void

Check warning on line 27 in src/services/compressionService.ts

View workflow job for this annotation

GitHub Actions / build-and-test (21.x)

'error' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 27 in src/services/compressionService.ts

View workflow job for this annotation

GitHub Actions / build-and-test (20.x)

'error' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 27 in src/services/compressionService.ts

View workflow job for this annotation

GitHub Actions / build-and-test (22.x)

'error' is defined but never used. Allowed unused args must match /^_/u
): void {
tinify.validate((err: Error | null) => {
if (err) {
if (onFailure) {
onFailure(err);
}
} else {
onSuccess();
}
});
}

/**
* Compress a single image file
*/
public static compressImage(file: Uri): void {
const shouldOverwrite = ConfigService.shouldOverwrite();
const postfix = ConfigService.getCompressedFilePostfix();

const destinationFilePath = generateDestinationPath(
file,
shouldOverwrite,
postfix
);

const statusBarItem = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Left
);
statusBarItem.text = `Compressing file ${file.fsPath}...`;
statusBarItem.show();

tinify.fromFile(file.fsPath).toFile(destinationFilePath, (error: Error | null) => {
statusBarItem.hide();
if (error) {
handleCompressionError(error);
} else {
vscode.window.showInformationMessage(
`Successfully compressed ${file.fsPath} to ${destinationFilePath}!`
);
}
});
}

/**
* Get the current compression count
*/
public static getCompressionCount(): number {
return tinify.compressionCount || 0;
}
}
49 changes: 49 additions & 0 deletions src/services/configService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import vscode = require('vscode');
import { TinyPngConfig } from '../types';

/**
* Service for managing TinyPNG extension configuration
*/
export class ConfigService {
private static readonly CONFIG_SECTION = 'tinypng';

/**
* Get the current TinyPNG configuration
*/
public static getConfig(): TinyPngConfig {
const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION);

return {
apiKey: config.get<string>('apiKey'),
forceOverwrite: config.get<boolean>('forceOverwrite') || false,
compressedFilePostfix: config.get<string>('compressedFilePostfix') || '.min'
};
}

/**
* Get the API key from configuration
*/
public static getApiKey(): string | undefined {
return vscode.workspace
.getConfiguration(this.CONFIG_SECTION)
.get<string>('apiKey');
}

/**
* Check if files should be overwritten
*/
public static shouldOverwrite(): boolean {
return vscode.workspace
.getConfiguration(this.CONFIG_SECTION)
.get<boolean>('forceOverwrite') || false;
}

/**
* Get the postfix for compressed files
*/
public static getCompressedFilePostfix(): string {
return vscode.workspace
.getConfiguration(this.CONFIG_SECTION)
.get<string>('compressedFilePostfix') || '.min';
}
}
29 changes: 29 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Uri } from 'vscode';

/**
* Configuration for TinyPNG extension
*/
export interface TinyPngConfig {
apiKey?: string;
forceOverwrite: boolean;
compressedFilePostfix: string;
}

/**
* Result of a compression operation
*/
export interface CompressionResult {
success: boolean;
sourcePath: string;
destinationPath: string;
error?: Error;
}

/**
* Options for compressing an image
*/
export interface CompressionOptions {
file: Uri;
shouldOverwrite: boolean;
postfix: string;
}
42 changes: 42 additions & 0 deletions src/utils/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import vscode = require('vscode');
import tinify = require('tinify');

/**
* Handle errors from TinyPNG API
*/
export function handleCompressionError(error: Error): void {
if (error instanceof tinify.AccountError) {
console.error('Authentication failed. Have you set the API Key?');
vscode.window.showErrorMessage(
'Authentication failed. Have you set the API Key?'
);
} else if (error instanceof tinify.ClientError) {
console.error('Ooops, there is an error. Please check your source image and settings.');
vscode.window.showErrorMessage(
'Ooops, there is an error. Please check your source image and settings.'
);
} else if (error instanceof tinify.ServerError) {
console.error('TinyPNG API is currently not available.');
vscode.window.showErrorMessage(
'TinyPNG API is currently not available.'
);
} else if (error instanceof tinify.ConnectionError) {
console.error('Network issue occurred. Please check your internet connectivity.');
vscode.window.showErrorMessage(
'Network issue occurred. Please check your internet connectivity.'
);
} else {
console.error(error.message);
vscode.window.showErrorMessage(error.message);
}
}

/**
* Handle validation errors
*/
export function handleValidationError(error: Error): void {
console.error(error.message);
vscode.window.showInformationMessage(
'TinyPNG: API validation failed. Be sure that you filled out tinypng.apiKey setting already.'
);
}
17 changes: 17 additions & 0 deletions src/utils/fileUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import path = require('path');
import { Uri } from 'vscode';

/**
* Generate the destination file path for a compressed image
*/
export function generateDestinationPath(file: Uri, shouldOverwrite: boolean, postfix: string): string {
if (shouldOverwrite) {
return file.fsPath;
}

const parsedPath = path.parse(file.fsPath);
return path.join(
parsedPath.dir,
`${parsedPath.name}${postfix}${parsedPath.ext}`
);
}
Loading