Skip to content

Commit

Permalink
Add directory support, add in place file optimizations
Browse files Browse the repository at this point in the history
  • Loading branch information
blopker committed Feb 19, 2024
1 parent ae79110 commit 65f2a5e
Show file tree
Hide file tree
Showing 17 changed files with 620 additions and 197 deletions.
22 changes: 19 additions & 3 deletions lib/compressor.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:alic/imagefiles.dart';
import 'package:alic/src/rust/api/simple.dart';
import 'package:alic/workqueue.dart';
import 'package:flutter/foundation.dart';

import './config.dart';

void compressor(ImageFile imageFile, void Function(ImageFile) callback) {
// Compress the image file by adding it to the queue, then run the callback when done.
final config = Config.signal.value;
final ext = imageFile.path.split('.').last;
final outPath = imageFile.path.replaceAll('.$ext', '.min.$ext');
final outPath = imageFile.path.replaceAll('.$ext', '${config.postfix}.$ext');

workQueue.add(() async {
callback(imageFile.copyWith(status: ImageFileStatus.compressing));
final timer = Stopwatch()..start();
var result = await imgcompress(path: imageFile.path, outPath: outPath);

var result = await imgcompress(
path: imageFile.path,
outPath: outPath,
jpegQuality: config.qualityJPEG,
pngQuality: config.qualityPNG,
gifQuality: config.qualityGIF,
webpQuality: config.qualityWEBP);
timer.stop();
debugPrint(
'Compressed ${imageFile.file} in ${timer.elapsedMilliseconds}ms');
Expand All @@ -33,6 +43,12 @@ void compressor(ImageFile imageFile, void Function(ImageFile) callback) {
));
return;
}
// Success!
if (!config.enablePostfix) {
// If postfix is disabled, replace the original file with the optimized one
File(imageFile.path).delete();
File(outPath).rename(imageFile.path);
}
callback(imageFile.copyWith(
sizeAfterOptimization: sizeAfterOptimization,
status: ImageFileStatus.success,
Expand Down
27 changes: 23 additions & 4 deletions lib/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,34 @@ class ConfigData {
final int qualityPNG;
final int qualityWEBP;
final int qualityGIF;
final bool enablePostfix;
final String postfix;

const ConfigData({
required this.lossy,
required this.qualityJPEG,
required this.qualityPNG,
required this.qualityWEBP,
required this.qualityGIF,
required this.enablePostfix,
required this.postfix,
});

static validateQuality(dynamic quality, int defaultValue) {
if (quality is int) {
return quality.clamp(10, 100);
}
return defaultValue;
}

ConfigData.fromJson(Map<String, dynamic> json)
: lossy = json['lossy'] ?? true,
qualityJPEG = json['qualityJPEG'] ?? 80,
qualityPNG = json['qualityPNG'] ?? 80,
qualityWEBP = json['qualityWEBP'] ?? 60,
qualityGIF = json['qualityGIF'] ?? 80;
qualityJPEG = validateQuality(json['qualityJPEG'], 80),
qualityPNG = validateQuality(json['qualityPNG'], 80),
qualityWEBP = validateQuality(json['qualityWEBP'], 60),
qualityGIF = validateQuality(json['qualityGIF'], 80),
enablePostfix = json['enablePostfix'] ?? true,
postfix = json['postfix'] ?? '.min';

Map<String, dynamic> toJson() => {
'lossy': lossy,
Expand All @@ -44,13 +57,17 @@ class ConfigData {
int? qualityPNG,
int? qualityWEBP,
int? qualityGIF,
bool? enablePostfix,
String? postfix,
}) {
return ConfigData(
lossy: lossy ?? this.lossy,
qualityJPEG: qualityJPEG ?? this.qualityJPEG,
qualityPNG: qualityPNG ?? this.qualityPNG,
qualityWEBP: qualityWEBP ?? this.qualityWEBP,
qualityGIF: qualityGIF ?? this.qualityGIF,
enablePostfix: enablePostfix ?? this.enablePostfix,
postfix: postfix ?? this.postfix,
);
}
}
Expand All @@ -65,13 +82,15 @@ class Config {
static void init() {
signal.value = readConfig();
signals.effect(() {
print('Config changed: ${signal.value}');
writeConfig(signal.value);
});
}

static ConfigData readConfig() {
ensureConfigExists();
var configData = configFile.readAsStringSync();
print('Read config: $configData');
return ConfigData.fromJson(jsonDecode(configData));
}

Expand Down
62 changes: 58 additions & 4 deletions lib/dropper.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:super_clipboard/super_clipboard.dart';
import 'package:super_drag_and_drop/super_drag_and_drop.dart';
Expand Down Expand Up @@ -68,16 +71,21 @@ class _MyDropRegionState extends State<MyDropRegion> {
onPerformDrop: (event) async {
debugPrint('onPerformDrop');
final items = event.session.items;
final mixedPaths = <String>[];
for (var item in items) {
for (var format in formats) {
if (item.canProvide(format)) {
final reader = item.dataReader!;
reader.getValue(Formats.fileUri, (value) {
widget.onDrop(value!.toFilePath());
});
var uri = await _getValueFromItem(item);
if (uri != null) {
mixedPaths.add(uri);
}
}
}
}
final paths = await _resolvePaths(mixedPaths);
for (var path in paths) {
widget.onDrop(path);
}
},
child: Stack(children: [
widget.child,
Expand All @@ -104,3 +112,49 @@ class _MyDropRegionState extends State<MyDropRegion> {
);
}
}

Future<String?> _getValueFromItem(DropItem item) async {
final reader = item.dataReader!;
final completer = Completer<String?>();
reader.getValue(Formats.fileUri, (value) {
if (value == null) {
completer.complete(null);
} else {
completer.complete(value.toFilePath());
}
});
return completer.future;
}

Future<List<String>> _resolvePaths(List<String> paths) async {
List<String> resolvedPaths = [];
for (var path in paths) {
if (path.endsWith('/')) {
resolvedPaths.addAll(await _getImagesFromDirectory(path)
.then((imageFiles) => imageFiles.map((e) => e.path).toList()));
} else {
resolvedPaths.add(path);
}
}
return resolvedPaths;
}

// Return a list of images from a directory, recursively
Future<List<File>> _getImagesFromDirectory(String path) async {
var dir = Directory(path);

List<File> imageFiles = [];

await for (var entity in dir.list(recursive: true, followLinks: false)) {
if (entity is File && _isImage(entity)) {
imageFiles.add(entity);
}
}

return imageFiles;
}

bool _isImage(File file) {
var ext = file.path.split('.').last;
return ['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext.toLowerCase());
}
76 changes: 48 additions & 28 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import 'package:alic/dropper.dart';
import 'package:alic/imagefiles.dart';
import 'package:alic/settings.dart';
import 'package:alic/src/rust/frb_generated.dart';
import 'package:alic/table.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:signals/signals_flutter.dart';
import 'package:window_manager/window_manager.dart';

import './config.dart';
import 'glass.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await RustLib.init();
await windowManager.ensureInitialized();
// Config.init();
Config.init();

WindowOptions windowOptions = const WindowOptions(
minimumSize: Size(600, 400),
Expand All @@ -39,15 +41,32 @@ class MyApp extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)
.copyWith(background: const Color(0xFF1B1B1B)),
textTheme: const TextTheme(
bodyLarge: TextStyle(color: Colors.white),
bodyMedium: TextStyle(color: Colors.white),
bodySmall: TextStyle(color: Colors.white),
headlineLarge: TextStyle(color: Colors.white),
headlineMedium: TextStyle(color: Colors.white),
headlineSmall: TextStyle(color: Colors.white),
titleLarge: TextStyle(color: Colors.white),
titleMedium: TextStyle(color: Colors.white),
titleSmall: TextStyle(color: Colors.white),
bodyLarge: TextStyle(color: Colors.white70),
bodyMedium: TextStyle(color: Colors.white70),
bodySmall: TextStyle(color: Colors.white70),
headlineLarge: TextStyle(color: Colors.white70),
headlineMedium: TextStyle(color: Colors.white70),
headlineSmall: TextStyle(color: Colors.white70),
titleLarge: TextStyle(color: Colors.white70),
titleMedium: TextStyle(color: Colors.white70),
titleSmall: TextStyle(color: Colors.white70),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: Colors.white70,
),
),
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
foregroundColor: Colors.white70,
)),
sliderTheme: const SliderThemeData(
activeTrackColor: Colors.white70,
inactiveTrackColor: Colors.white70,
thumbColor: Colors.white70,
overlayColor: Colors.white38,
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 10),
overlayShape: RoundSliderOverlayShape(overlayRadius: 10),
),
),
home: const HomePage(),
Expand Down Expand Up @@ -175,31 +194,32 @@ class BottomBar extends StatelessWidget {
),
Row(
children: [
// IconButton(
// constraints:
// const BoxConstraints.tightFor(width: 37, height: 37),
// onPressed: () {
// showDialog(
// context: context,
// builder: (context) {
// return const SettingsWidget();
// });
// },
// icon: const Icon(Icons.settings),
// iconSize: 20,
// color: Colors.white70,
// padding: const EdgeInsets.all(0),
// ),
IconButton(
constraints:
const BoxConstraints.tightFor(width: 37, height: 37),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return const SettingsWidget();
});
},
icon: const Icon(Icons.settings),
iconSize: 20,
padding: const EdgeInsets.all(0),
),
Watch(
(_) => TextButton.icon(
style: TextButton.styleFrom(
disabledIconColor: Colors.white30,
disabledForegroundColor: Colors.white30,
foregroundColor: Colors.white70,
),
onPressed: ImageFiles.signal.isEmpty
? null
: () {
ImageFiles.removeDone();
},
style: TextButton.styleFrom(
foregroundColor: Colors.white70,
),
icon: const Icon(
Icons.close,
size: 20,
Expand Down
Loading

0 comments on commit 65f2a5e

Please sign in to comment.