Skip to content
Open
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
49 changes: 49 additions & 0 deletions .github/workflows/format-fix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Format Fix

on:
push:
branches:
- chatgpt/unitflow-dart-fixes-20260819

permissions:
contents: write

jobs:
format:
runs-on: ubuntu-latest
steps:
- name: Checkout branch
uses: actions/checkout@v4
with:
ref: chatgpt/unitflow-dart-fixes-20260819
fetch-depth: 0

- name: Install Rust formatter
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt

- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- name: Format Rust
run: cargo fmt --all

- name: Format Flutter sources and tests
run: dart format apps/unitflow_app/lib apps/unitflow_app/test

- name: Commit formatter output
shell: bash
run: |
if git diff --quiet; then
echo "No formatter changes required."
exit 0
fi
git config user.name "Sanskar"
git config user.email "sanskarin@outlook.in"
git add -A
git commit -m "style: apply automated formatters"
git push origin HEAD:chatgpt/unitflow-dart-fixes-20260819
22 changes: 17 additions & 5 deletions apps/unitflow_app/lib/app/app_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import '../features/converter/domain/conversion_engine.dart';
import '../features/converter/domain/unit_models.dart';

final class AppController extends ChangeNotifier {
AppController({required UserStateRepository repository}) : _repository = repository;
AppController({required UserStateRepository repository})
: _repository = repository;

final UserStateRepository _repository;
UserState _state = UserState();
Expand Down Expand Up @@ -76,7 +77,10 @@ final class AppController extends ChangeNotifier {
Future<void> togglePinnedPair(PinnedPair pair) {
final from = _engine.catalog.byId(pair.fromUnitId);
final to = _engine.catalog.byId(pair.toUnitId);
if (from == null || to == null || from.category != pair.category || to.category != pair.category) {
if (from == null ||
to == null ||
from.category != pair.category ||
to.category != pair.category) {
throw ArgumentError('Pinned pair references invalid units.');
}
final next = _state.pinnedPairs.toList();
Expand Down Expand Up @@ -127,7 +131,11 @@ final class AppController extends ChangeNotifier {
Future<void> addCustomUnit(CustomUnitData customUnit) {
final definition = customUnit.toUnitDefinition();
if (_engine.catalog.byId(definition.id) != null) {
throw ArgumentError.value(definition.id, 'id', 'unit identifier already exists');
throw ArgumentError.value(
definition.id,
'id',
'unit identifier already exists',
);
}
final next = <CustomUnitData>[..._state.customUnits, customUnit];
final newState = _state.copyWith(customUnits: next);
Expand All @@ -140,8 +148,12 @@ final class AppController extends ChangeNotifier {
if (existing.isEmpty) {
return Future<void>.value();
}
final nextCustom = _state.customUnits.where((item) => item.id != id).toList();
final nextFavorites = _state.favoriteUnitIds.where((item) => item != id).toSet();
final nextCustom = _state.customUnits
.where((item) => item.id != id)
.toList();
final nextFavorites = _state.favoriteUnitIds
.where((item) => item != id)
.toSet();
final nextPins = _state.pinnedPairs
.where((pair) => pair.fromUnitId != id && pair.toUnitId != id)
.toList();
Expand Down
32 changes: 20 additions & 12 deletions apps/unitflow_app/lib/app/app_shell.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,18 @@ final class _AppShellState extends State<AppShell> {
animation: widget.appController,
builder: (context, _) => CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.digit1, control: true): () => _select(0),
const SingleActivator(LogicalKeyboardKey.digit2, control: true): () => _select(1),
const SingleActivator(LogicalKeyboardKey.comma, control: true): () => _select(2),
const SingleActivator(LogicalKeyboardKey.digit1, meta: true): () => _select(0),
const SingleActivator(LogicalKeyboardKey.digit2, meta: true): () => _select(1),
const SingleActivator(LogicalKeyboardKey.comma, meta: true): () => _select(2),
const SingleActivator(LogicalKeyboardKey.digit1, control: true): () =>
_select(0),
const SingleActivator(LogicalKeyboardKey.digit2, control: true): () =>
_select(1),
const SingleActivator(LogicalKeyboardKey.comma, control: true): () =>
_select(2),
const SingleActivator(LogicalKeyboardKey.digit1, meta: true): () =>
_select(0),
const SingleActivator(LogicalKeyboardKey.digit2, meta: true): () =>
_select(1),
const SingleActivator(LogicalKeyboardKey.comma, meta: true): () =>
_select(2),
},
child: Focus(
autofocus: true,
Expand All @@ -54,7 +60,10 @@ final class _AppShellState extends State<AppShell> {
title: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.swap_calls, color: Theme.of(context).colorScheme.primary),
Icon(
Icons.swap_calls,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: AppSpacing.xs),
const Text('UnitFlow'),
],
Expand Down Expand Up @@ -149,10 +158,7 @@ final class _AppShellState extends State<AppShell> {
index: _selectedIndex,
children: <Widget>[
ConverterScreen(controller: _converterController),
LibraryScreen(
appController: widget.appController,
onOpenPair: _openPair,
),
LibraryScreen(appController: widget.appController, onOpenPair: _openPair),
SettingsScreen(
appController: widget.appController,
onOpenAbout: _openAbout,
Expand All @@ -173,6 +179,8 @@ final class _AppShellState extends State<AppShell> {
}

Future<void> _openAbout() => Navigator.of(context).push<void>(
MaterialPageRoute<void>(builder: (_) => const Scaffold(body: SafeArea(child: AboutScreen()))),
MaterialPageRoute<void>(
builder: (_) => const Scaffold(body: SafeArea(child: AboutScreen())),
),
);
}
9 changes: 7 additions & 2 deletions apps/unitflow_app/lib/app/theme/app_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ abstract final class AppTheme {
static ThemeData dark() => _build(Brightness.dark);

static ThemeData _build(Brightness brightness) {
final colors = ColorScheme.fromSeed(seedColor: _seed, brightness: brightness);
final colors = ColorScheme.fromSeed(
seedColor: _seed,
brightness: brightness,
);
final base = ThemeData(
useMaterial3: true,
brightness: brightness,
Expand Down Expand Up @@ -80,7 +83,9 @@ abstract final class AppTheme {
),
tooltipTheme: TooltipThemeData(
waitDuration: const Duration(milliseconds: 450),
textStyle: base.textTheme.bodySmall?.copyWith(color: colors.onInverseSurface),
textStyle: base.textTheme.bodySmall?.copyWith(
color: colors.onInverseSurface,
),
),
);
}
Expand Down
12 changes: 9 additions & 3 deletions apps/unitflow_app/lib/core/format/decimal_format.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,26 @@ final class DecimalDisplayFormatter {
whole = chunks.join(symbols.GROUP_SEP);
}

final fraction = parts.length == 2 ? '${symbols.DECIMAL_SEP}${parts[1]}' : '';
final fraction = parts.length == 2
? '${symbols.DECIMAL_SEP}${parts[1]}'
: '';
return '${negative ? '-' : ''}$whole$fraction';
}

String _localizeMantissa(String formatted, String localeName) {
final separator = NumberFormat.decimalPattern(localeName).symbols.DECIMAL_SEP;
final separator = NumberFormat.decimalPattern(localeName)
.symbols
.DECIMAL_SEP;
if (separator == '.') {
return formatted;
}
final exponentIndex = formatted.indexOf('e');
if (exponentIndex < 0) {
return formatted.replaceFirst('.', separator);
}
final mantissa = formatted.substring(0, exponentIndex).replaceFirst('.', separator);
final mantissa = formatted
.substring(0, exponentIndex)
.replaceFirst('.', separator);
return '$mantissa${formatted.substring(exponentIndex)}';
}

Expand Down
8 changes: 4 additions & 4 deletions apps/unitflow_app/lib/core/math/exact_decimal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ final class ExactDecimal implements Comparable<ExactDecimal> {
throw const FormatException('Invalid decimal input');
}

final match = RegExp(
r'^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$',
).firstMatch(input);
final match = RegExp(r'^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$')
.firstMatch(input);
if (match == null) {
throw const FormatException('Invalid decimal input');
}
Expand Down Expand Up @@ -61,7 +60,8 @@ final class ExactDecimal implements Comparable<ExactDecimal> {

const ExactDecimal._(this.coefficient, this.scale);

static const zero = ExactDecimal._(BigInt.zero, 0);
/// Canonical zero. `BigInt.zero` is a runtime getter, so this cannot be a Dart `const`.
static final ExactDecimal zero = ExactDecimal._(BigInt.zero, 0);

final BigInt coefficient;
final int scale;
Expand Down
49 changes: 37 additions & 12 deletions apps/unitflow_app/lib/core/persistence/user_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,17 @@ final class RecentConversion {
final from = value['fromUnitId'];
final to = value['toUnitId'];
final created = value['createdAt'];
if (input is! String || from is! String || to is! String || created is! String) {
if (input is! String ||
from is! String ||
to is! String ||
created is! String) {
return null;
}
final timestamp = DateTime.tryParse(created);
if (timestamp == null || from.isEmpty || to.isEmpty || input.length > 1024) {
if (timestamp == null ||
from.isEmpty ||
to.isEmpty ||
input.length > 1024) {
return null;
}
return RecentConversion(
Expand Down Expand Up @@ -79,15 +85,18 @@ final class CustomUnitData {
if (symbol.trim().isEmpty || symbol.length > 32) {
throw const FormatException('Custom unit symbol is invalid.');
}
if (aliases.length > 32 || aliases.any((value) => value.isEmpty || value.length > 64)) {
if (aliases.length > 32 ||
aliases.any((value) => value.isEmpty || value.length > 64)) {
throw const FormatException('Custom unit aliases are invalid.');
}
if (description.length > 512) {
throw const FormatException('Custom unit description is too long.');
}
final parsedScale = ExactDecimal.parse(scale);
if (parsedScale.compareTo(ExactDecimal.zero) <= 0) {
throw const FormatException('Custom unit scale must be greater than zero.');
throw const FormatException(
'Custom unit scale must be greater than zero.',
);
}
return UnitDefinition(
id: id,
Expand Down Expand Up @@ -176,10 +185,18 @@ final class UserState {
List<PinnedPair>? pinnedPairs,
List<RecentConversion>? recents,
List<CustomUnitData>? customUnits,
}) : favoriteUnitIds = Set<String>.unmodifiable(favoriteUnitIds ?? <String>{}),
pinnedPairs = List<PinnedPair>.unmodifiable(pinnedPairs ?? const <PinnedPair>[]),
recents = List<RecentConversion>.unmodifiable(recents ?? const <RecentConversion>[]),
customUnits = List<CustomUnitData>.unmodifiable(customUnits ?? const <CustomUnitData>[]);
}) : favoriteUnitIds = Set<String>.unmodifiable(
favoriteUnitIds ?? <String>{},
),
pinnedPairs = List<PinnedPair>.unmodifiable(
pinnedPairs ?? const <PinnedPair>[],
),
recents = List<RecentConversion>.unmodifiable(
recents ?? const <RecentConversion>[],
),
customUnits = List<CustomUnitData>.unmodifiable(
customUnits ?? const <CustomUnitData>[],
);

static const schemaVersion = 1;

Expand Down Expand Up @@ -223,9 +240,13 @@ final class UserState {
'useGrouping': useGrouping,
'onboardingComplete': onboardingComplete,
'favoriteUnitIds': favoriteUnitIds.toList(growable: false),
'pinnedPairs': pinnedPairs.map((pair) => pair.storageValue).toList(growable: false),
'pinnedPairs': pinnedPairs
.map((pair) => pair.storageValue)
.toList(growable: false),
'recents': recents.map((recent) => recent.toJson()).toList(growable: false),
'customUnits': customUnits.map((unit) => unit.toJson()).toList(growable: false),
'customUnits': customUnits
.map((unit) => unit.toJson())
.toList(growable: false),
};

static UserState fromJson(Map<String, Object?> json) {
Expand All @@ -245,8 +266,12 @@ final class UserState {
throw const FormatException('Invalid UnitFlow preferences.');
}

final theme = ThemePreference.values.where((item) => item.name == json['theme']).firstOrNull;
final notation = DecimalNotation.values.where((item) => item.name == json['notation']).firstOrNull;
final theme = ThemePreference.values
.where((item) => item.name == json['theme'])
.firstOrNull;
final notation = DecimalNotation.values
.where((item) => item.name == json['notation'])
.firstOrNull;
if (theme == null || notation == null) {
throw const FormatException('Invalid UnitFlow appearance settings.');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ abstract interface class UserStateRepository {
UserState importJson(String content);
}

final class SharedPreferencesUserStateRepository implements UserStateRepository {
final class SharedPreferencesUserStateRepository
implements UserStateRepository {
SharedPreferencesUserStateRepository({SharedPreferencesAsync? preferences})
: _preferences = preferences ?? SharedPreferencesAsync();

Expand Down Expand Up @@ -77,7 +78,8 @@ final class SharedPreferencesUserStateRepository implements UserStateRepository
}

final class MemoryUserStateRepository implements UserStateRepository {
MemoryUserStateRepository([UserState? initial]) : _state = initial ?? UserState();
MemoryUserStateRepository([UserState? initial])
: _state = initial ?? UserState();

UserState _state;

Expand Down
Loading