fox_utils 0.1.7: clipboard, drag & drop, Android file saving
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
dc16bba054
46 files changed
+4738
No files matched your search
Executable
+420
@@ -0,0 +1,420 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GradientText extends StatelessWidget {
|
||||
const GradientText(this.text, {super.key, required this.gradient});
|
||||
|
||||
final Widget text;
|
||||
final Gradient gradient;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShaderMask(
|
||||
blendMode: BlendMode.srcIn,
|
||||
shaderCallback: (bounds) => gradient.createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)),
|
||||
child: text,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum CornerSide { topLeft, topRight, bottomLeft, bottomRight }
|
||||
|
||||
class SingleScoopedCornerClipper extends CustomClipper<Path> {
|
||||
SingleScoopedCornerClipper({required this.radius, this.side = CornerSide.topRight});
|
||||
|
||||
final double radius;
|
||||
final CornerSide side;
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final path = Path();
|
||||
final w = size.width;
|
||||
final h = size.height;
|
||||
|
||||
// Start at top-left
|
||||
path.moveTo(0, 0);
|
||||
|
||||
switch (side) {
|
||||
case CornerSide.topRight:
|
||||
// Draw top edge to where the curve starts (right side)
|
||||
path.lineTo(w - radius, 0);
|
||||
// Concave arc
|
||||
path.arcToPoint(Offset(w, radius), radius: Radius.circular(radius), clockwise: false);
|
||||
// Draw remainder of edges
|
||||
path.lineTo(w, h);
|
||||
path.lineTo(0, h);
|
||||
break;
|
||||
|
||||
case CornerSide.bottomRight:
|
||||
// Draw right edge to where the curve starts (bottom side)
|
||||
path.lineTo(w, h - radius);
|
||||
// Concave arc
|
||||
path.arcToPoint(Offset(w - radius, h), radius: Radius.circular(radius), clockwise: false);
|
||||
// Draw remainder of edges
|
||||
path.lineTo(0, h);
|
||||
break;
|
||||
|
||||
case CornerSide.bottomLeft:
|
||||
// Draw bottom edge to where the curve starts (left side)
|
||||
path.lineTo(radius, h);
|
||||
// Concave arc
|
||||
path.arcToPoint(Offset(0, h - radius), radius: Radius.circular(radius), clockwise: false);
|
||||
// Draw remainder of edges
|
||||
path.lineTo(0, 0); // Already moved here, just closing path
|
||||
break;
|
||||
|
||||
case CornerSide.topLeft:
|
||||
// Draw left edge to where the curve starts (top side)
|
||||
path.lineTo(0, radius);
|
||||
// Concave arc
|
||||
path.arcToPoint(Offset(radius, 0), radius: Radius.circular(radius), clockwise: false);
|
||||
// Draw remainder of edges
|
||||
path.lineTo(w, 0);
|
||||
path.lineTo(w, h);
|
||||
path.lineTo(0, h);
|
||||
break;
|
||||
}
|
||||
|
||||
path.close();
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(SingleScoopedCornerClipper oldClipper) => oldClipper.radius != radius || oldClipper.side != side;
|
||||
}
|
||||
|
||||
extension IntBytesExtension on int {
|
||||
/// Converts to Kilobytes (1024 bytes)
|
||||
int get kb => this * 1024;
|
||||
|
||||
/// Converts to Megabytes (1024 * 1024 bytes)
|
||||
int get mb => this * 1024 * 1024;
|
||||
|
||||
/// Converts to Gigabytes (1024 * 1024 * 1024 bytes)
|
||||
int get gb => this * 1024 * 1024 * 1024;
|
||||
|
||||
/// Converts to Terabytes (1024^4 bytes)
|
||||
int get tb => this * 1024 * 1024 * 1024 * 1024;
|
||||
}
|
||||
|
||||
String formatBytes(int bytes, {int decimals = 2, bool useBinary = true}) {
|
||||
if (bytes <= 0) return "0 B";
|
||||
|
||||
// Choose between binary (1024) or decimal (1000) base
|
||||
final base = useBinary ? 1024.0 : 1000.0;
|
||||
final units = useBinary
|
||||
? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||
: ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
final exp = (log(bytes) / log(base)).floor();
|
||||
final size = bytes / pow(base, exp);
|
||||
|
||||
// Clamp the exponent to avoid index out of bounds
|
||||
final unitIndex = exp.clamp(0, units.length - 1);
|
||||
|
||||
return '${size.toStringAsFixed(decimals)} ${units[unitIndex]}';
|
||||
}
|
||||
|
||||
// Extension method for easier usage
|
||||
extension BytesFormatter on int {
|
||||
String toReadableSize({int decimals = 2}) => formatBytes(this, decimals: decimals);
|
||||
}
|
||||
|
||||
extension DateTimeExtension on DateTime {
|
||||
String timeAgo() {
|
||||
final diff = DateTime.now().difference(this);
|
||||
if (diff.inDays >= 1) return '${diff.inDays} days ago';
|
||||
if (diff.inHours >= 1) return '${diff.inHours} hours ago';
|
||||
if (diff.inMinutes >= 1) return '${diff.inMinutes} minutes ago';
|
||||
return 'Just now';
|
||||
}
|
||||
|
||||
String toSmartDateWithTime() {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(this);
|
||||
|
||||
// Helper for "14:30" format
|
||||
final clockTime = '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
|
||||
|
||||
String datePart;
|
||||
|
||||
// 1. Just now
|
||||
if (diff.inMinutes < 1) {
|
||||
datePart = 'just now';
|
||||
}
|
||||
// 2. Minute precision (up to 1 hour)
|
||||
else if (diff.inHours < 1) {
|
||||
datePart = '${diff.inMinutes}m ago';
|
||||
}
|
||||
// 3. Hour precision (up to 24 hours)
|
||||
else if (diff.inDays < 1) {
|
||||
datePart = '${diff.inHours}h ago';
|
||||
}
|
||||
// 4. Compact date (older than a day)
|
||||
else if (year == now.year) {
|
||||
datePart = '${_getMonth(month)} $day';
|
||||
} else {
|
||||
datePart = '${_getMonth(month)} $day, $year';
|
||||
}
|
||||
|
||||
// Combine them: "5m ago at 14:30" or "Jan 2 at 07:11"
|
||||
return '$datePart at $clockTime';
|
||||
}
|
||||
|
||||
String _getMonth(int month) {
|
||||
return ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][month - 1];
|
||||
}
|
||||
|
||||
String _getWeekday(int day) {
|
||||
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][day - 1];
|
||||
}
|
||||
}
|
||||
|
||||
extension CompactDateTime on DateTime {
|
||||
String toCompactPostDate() {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(this);
|
||||
|
||||
// 1. Less than a minute: "Just now"
|
||||
if (difference.inMinutes < 1) {
|
||||
return 'now';
|
||||
}
|
||||
|
||||
// 2. Less than an hour: "5m", "59m"
|
||||
if (difference.inHours < 1) {
|
||||
return '${difference.inMinutes}m';
|
||||
}
|
||||
|
||||
// 3. Less than a day: "2h", "23h"
|
||||
if (difference.inDays < 1) {
|
||||
return '${difference.inHours}h';
|
||||
}
|
||||
|
||||
// 4. Less than a week: "Mon", "Tue"
|
||||
if (difference.inDays < 7) {
|
||||
return _getWeekday(weekday);
|
||||
}
|
||||
|
||||
// 5. This calendar year: "Mar 12"
|
||||
if (year == now.year) {
|
||||
return '${_getMonth(month)} $day';
|
||||
}
|
||||
|
||||
// 6. Different year: "Mar 2024"
|
||||
return '${_getMonth(month)} $year';
|
||||
}
|
||||
|
||||
// Helper for short weekdays
|
||||
String _getWeekday(int day) {
|
||||
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return days[day - 1];
|
||||
}
|
||||
|
||||
// Helper for short months
|
||||
String _getMonth(int month) {
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
return months[month - 1];
|
||||
}
|
||||
}
|
||||
|
||||
extension TimeGradientColor on DateTime {
|
||||
Color toSkyColor(Color baseColor) {
|
||||
// Define the color transition for a full 24-hour cycle
|
||||
const skyline = LinearGradient(
|
||||
colors: [
|
||||
Color(0xFF0F172A), // 00:00 - Deep Night
|
||||
Color(0xFF334155), // 06:00 - Dawn
|
||||
Color(0xFF7DD3FC), // 12:00 - Noon
|
||||
Color(0xFFFB923C), // 18:00 - Sunset
|
||||
Color(0xFF0F172A), // 24:00 - Back to Night
|
||||
],
|
||||
stops: [0.0, 0.25, 0.5, 0.75, 1.0],
|
||||
);
|
||||
|
||||
// Calculate the day's progress (0.0 to 1.0)
|
||||
final double dayProgress = (hour * 60 + minute) / (24 * 60);
|
||||
|
||||
// Sample the color from the gradient
|
||||
final Color sampledColor = _sampleGradient(skyline, dayProgress);
|
||||
|
||||
// Blend it subtly with your theme's surface color (15% intensity)
|
||||
return Color.lerp(baseColor, sampledColor, 0.15)!;
|
||||
}
|
||||
|
||||
// Helper to interpolate color from a LinearGradient
|
||||
Color _sampleGradient(LinearGradient gradient, double t) {
|
||||
if (t <= 0) return gradient.colors.first;
|
||||
if (t >= 1) return gradient.colors.last;
|
||||
|
||||
final int segmentCount = gradient.colors.length - 1;
|
||||
final double segmentValue = t * segmentCount;
|
||||
final int index = segmentValue.floor();
|
||||
final double weight = segmentValue - index;
|
||||
|
||||
return Color.lerp(gradient.colors[index], gradient.colors[index + 1], weight)!;
|
||||
}
|
||||
}
|
||||
|
||||
extension NullableProgress01 on ImageChunkEvent? {
|
||||
/// 0.0 → 1.0 while loading; null when unknown.
|
||||
double? get progress {
|
||||
final chunk = this;
|
||||
if (chunk == null) return null;
|
||||
|
||||
final total = chunk.expectedTotalBytes;
|
||||
return (total != null && total > 0) ? (chunk.cumulativeBytesLoaded / total).clamp(0.0, 1.0) : null;
|
||||
}
|
||||
}
|
||||
|
||||
extension ObjectExt<T> on T {
|
||||
R let<R>(R Function(T e) block) => block(this);
|
||||
}
|
||||
|
||||
extension ColorUtils on Color {
|
||||
// ---------------------------------------------------------
|
||||
// 1. Brightness Manipulation (HSL Based)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
/// Darkens the color by a given [amount] (0.0 to 1.0).
|
||||
///
|
||||
/// Defaults to 10% (0.1).
|
||||
/// Uses HSL calculation to preserve color hue.
|
||||
// Color darken([double amount = 0.1]) {
|
||||
// assert(amount >= 0 && amount <= 1, 'Amount must be between 0 and 1');
|
||||
|
||||
// final hsl = HSLColor.fromColor(this);
|
||||
// final newLightness = (hsl.lightness - amount).clamp(0.0, 1.0);
|
||||
|
||||
// return hsl.withLightness(newLightness).toColor();
|
||||
// }
|
||||
|
||||
// /// Lightens the color by a given [amount] (0.0 to 1.0).
|
||||
// ///
|
||||
// /// Defaults to 10% (0.1).
|
||||
// /// Uses HSL calculation to preserve color hue.
|
||||
// Color lighten([double amount = 0.1]) {
|
||||
// assert(amount >= 0 && amount <= 1, 'Amount must be between 0 and 1');
|
||||
|
||||
// final hsl = HSLColor.fromColor(this);
|
||||
// final newLightness = (hsl.lightness + amount).clamp(0.0, 1.0);
|
||||
|
||||
// return hsl.withLightness(newLightness).toColor();
|
||||
// }
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 2. Opacity & Color Mixing
|
||||
// ---------------------------------------------------------
|
||||
|
||||
/// Returns a new color with the specified opacity.
|
||||
/// Shortcut for `withOpacity`.
|
||||
Color withOpacityValue(double opacity) {
|
||||
return withOpacity(opacity);
|
||||
}
|
||||
|
||||
/// Blends this color with another color.
|
||||
///
|
||||
/// [amount] defines how much of [color] to mix in (0.0 to 1.0).
|
||||
/// 0.0 results in the original color, 1.0 results in the input color.
|
||||
Color mix(Color color, [double amount = 0.5]) {
|
||||
assert(amount >= 0 && amount <= 1, 'Amount must be between 0 and 1');
|
||||
|
||||
return Color.lerp(this, color, amount)!;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 3. Color Properties & Conversion
|
||||
// ---------------------------------------------------------
|
||||
|
||||
/// Returns the color as a Hex string (e.g. "#FF0000").
|
||||
/// Includes alpha channel if [includeAlpha] is true.
|
||||
String toHexString({bool includeAlpha = false}) {
|
||||
if (includeAlpha) {
|
||||
return '#${value.toRadixString(16).padLeft(8, '0').toUpperCase()}';
|
||||
}
|
||||
return '#${value.toRadixString(16).substring(2).padLeft(6, '0').toUpperCase()}';
|
||||
}
|
||||
|
||||
/// Calculates the relative luminance.
|
||||
/// Useful for determining if the color is "light" or "dark".
|
||||
bool get isDark => computeLuminance() < 0.5;
|
||||
|
||||
bool get isLight => !isDark;
|
||||
|
||||
/// Returns a contrasting text color (Black or White)
|
||||
/// suitable for reading on top of this color.
|
||||
Color get contrastingColor => isDark ? Colors.white : Colors.black;
|
||||
}
|
||||
|
||||
/// Optional: Extension on String to parse Hex colors easily.
|
||||
extension StringColorUtils on String {
|
||||
/// Parses a hex string to a Color.
|
||||
/// Supports formats: "FF0000", "#FF0000", "0xFF0000", "#FFFF0000".
|
||||
Color toColor() {
|
||||
var hexColor = replaceAll('#', '').replaceAll('0x', '');
|
||||
|
||||
if (hexColor.length == 6) {
|
||||
hexColor = 'FF$hexColor'; // Add full opacity if alpha is missing
|
||||
}
|
||||
|
||||
return Color(int.parse(hexColor, radix: 16));
|
||||
}
|
||||
}
|
||||
|
||||
enum Side { top, left, bottom, right }
|
||||
|
||||
extension NumBorderRadiusExtension on num {
|
||||
/// Creates a circular radius where specific sides are flattened (radius = 0).
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// // A pill shape flattened on the left side
|
||||
/// Container(
|
||||
/// decoration: BoxDecoration(
|
||||
/// borderRadius: 50.circularExcept({Side.left}),
|
||||
/// color: Colors.blue,
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
BorderRadius circularExcept(Set<Side> excludedSides) {
|
||||
final double r = toDouble();
|
||||
|
||||
// If a side is in the exclusion set, its corners become 0.
|
||||
// Otherwise, they take the radius [r].
|
||||
|
||||
// Top-Left is affected by Top OR Left
|
||||
final tl = (excludedSides.contains(Side.top) || excludedSides.contains(Side.left)) ? 0.0 : r;
|
||||
|
||||
// Top-Right is affected by Top OR Right
|
||||
final tr = (excludedSides.contains(Side.top) || excludedSides.contains(Side.right)) ? 0.0 : r;
|
||||
|
||||
// Bottom-Left is affected by Bottom OR Left
|
||||
final bl = (excludedSides.contains(Side.bottom) || excludedSides.contains(Side.left)) ? 0.0 : r;
|
||||
|
||||
// Bottom-Right is affected by Bottom OR Right
|
||||
final br = (excludedSides.contains(Side.bottom) || excludedSides.contains(Side.right)) ? 0.0 : r;
|
||||
|
||||
return BorderRadius.only(
|
||||
topLeft: Radius.circular(tl),
|
||||
topRight: Radius.circular(tr),
|
||||
bottomLeft: Radius.circular(bl),
|
||||
bottomRight: Radius.circular(br),
|
||||
);
|
||||
}
|
||||
|
||||
BorderRadius get circular {
|
||||
return BorderRadius.circular(this.toDouble());
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: Extension on BorderRadius itself if you want to modify an existing one
|
||||
extension BorderRadiusModifier on BorderRadius {
|
||||
BorderRadius exclude(Set<Side> sides) {
|
||||
return copyWith(
|
||||
topLeft: sides.contains(Side.top) || sides.contains(Side.left) ? Radius.zero : topLeft,
|
||||
topRight: sides.contains(Side.top) || sides.contains(Side.right) ? Radius.zero : topRight,
|
||||
bottomLeft: sides.contains(Side.bottom) || sides.contains(Side.left) ? Radius.zero : bottomLeft,
|
||||
bottomRight: sides.contains(Side.bottom) || sides.contains(Side.right) ? Radius.zero : bottomRight,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user