added oled theme and theme presets
Android CI / Build and Test (pull_request) Failing after 11m39s
Flutter / build (BalanceFlutter/build/ios/iphoneos/*.app, macos-latest, ios) (push) Canceled after 0s
Flutter / build (BalanceFlutter/build/macos/Build/Products/Release/*.app, macos-latest, macos) (push) Canceled after 0s
Flutter / build (BalanceFlutter/build/windows/x64/runner/Release, windows-latest, windows) (push) Canceled after 0s
Flutter / watchos (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (push) Canceled after 0s
Flutter / checks (push) Successful in 25m8s
Flutter / build (BalanceFlutter/build/app/outputs/flutter-apk/*.apk, ubuntu-latest, android) (push) Failing after 38m6s
Flutter / build (BalanceFlutter/build/linux/x64/release/bundle, ubuntu-latest, linux) (push) Successful in 32m37s
Flutter / build (BalanceFlutter/build/web, ubuntu-latest, web) (push) Successful in 31m39s
iOS CI / Build and Test SwiftUI App (pull_request) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build Unsigned iOS IPA (pull_request) Canceled after 0s
Build Unsigned iOS and macOS Apps / Build macOS ZIP (pull_request) Canceled after 0s
Flutter / build (BalanceFlutter/build/ios/iphoneos/*.app, macos-latest, ios) (pull_request) Canceled after 0s
Flutter / build (BalanceFlutter/build/macos/Build/Products/Release/*.app, macos-latest, macos) (pull_request) Canceled after 0s
Flutter / build (BalanceFlutter/build/windows/x64/runner/Release, windows-latest, windows) (pull_request) Canceled after 0s
Flutter / watchos (pull_request) Canceled after 0s
Flutter / checks (pull_request) Successful in 24m59s
Flutter / build (BalanceFlutter/build/app/outputs/flutter-apk/*.apk, ubuntu-latest, android) (pull_request) Failing after 31m24s
Flutter / build (BalanceFlutter/build/linux/x64/release/bundle, ubuntu-latest, linux) (pull_request) Successful in 32m34s
Flutter / build (BalanceFlutter/build/web, ubuntu-latest, web) (pull_request) Successful in 24m54s

This commit is contained in:
wt committed 2026-10-28 16:57:34 +07:00
1 parent 2c278044bb
commit a2aa714a83
16 files changed
+436 -42

No files matched your search

+3 -8
View File
@@ -2,14 +2,9 @@ name: Android CI
on:
push:
branches:
- main
- develop
- issue-*
branches: ["main"]
pull_request:
branches:
- main
- develop
branches: ["main"]
jobs:
build:
@@ -23,7 +18,7 @@ jobs:
# Step 2: Set up JDK 17 with Gradle Caching
- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: "zulu"
java-version: "17"
+77
View File
@@ -0,0 +1,77 @@
name: Flutter
on:
push:
paths: ['BalanceFlutter/**', '.github/workflows/flutter.yml', 'Balance/BalanceWatch/**', 'Balance/Balance.xcodeproj/**']
pull_request:
paths: ['BalanceFlutter/**', '.github/workflows/flutter.yml', 'Balance/BalanceWatch/**', 'Balance/Balance.xcodeproj/**']
workflow_dispatch:
env:
FLUTTER_VERSION: '3.47.8'
jobs:
checks:
runs-on: ubuntu-latest
defaults:
run:
working-directory: BalanceFlutter
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- run: flutter pub get
- run: flutter analyze
- run: flutter test
build:
strategy:
fail-fast: false
matrix:
include:
- {platform: web, os: ubuntu-latest, artifact: 'BalanceFlutter/build/web'}
- {platform: android, os: ubuntu-latest, artifact: 'BalanceFlutter/build/app/outputs/flutter-apk/*.apk'}
- {platform: linux, os: ubuntu-latest, artifact: 'BalanceFlutter/build/linux/x64/release/bundle'}
- {platform: windows, os: windows-latest, artifact: 'BalanceFlutter/build/windows/x64/runner/Release'}
- {platform: ios, os: macos-latest, artifact: 'BalanceFlutter/build/ios/iphoneos/*.app'}
- {platform: macos, os: macos-latest, artifact: 'BalanceFlutter/build/macos/Build/Products/Release/*.app'}
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
working-directory: BalanceFlutter
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
if: matrix.platform == 'android'
with:
distribution: temurin
java-version: '17'
- name: Linux dependencies
if: matrix.platform == 'linux'
run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libstdc++-12-dev libsecret-1-dev libjsoncpp-dev
- uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- run: bash scripts/build.sh ${{ matrix.platform }}
- uses: actions/upload-artifact@v4
with:
name: balance-${{ matrix.platform }}
path: ${{ matrix.artifact }}
if-no-files-found: error
watchos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- run: bash BalanceFlutter/scripts/build.sh watchos
- uses: actions/upload-artifact@v4
with:
name: balance-watchos
path: BalanceFlutter/build/watchos/Build/Products/*watchsimulator/*.app
if-no-files-found: error
+2 -7
View File
@@ -2,14 +2,9 @@ name: iOS CI
on:
push:
branches:
- main
- develop
- issue-*
branches: [ main ]
pull_request:
branches:
- main
- develop
branches: [ main ]
jobs:
build-and-test:
Vendored Regular → Executable
View File
File mode changed.
+4
View File
@@ -135,3 +135,7 @@ These checks do **not** prove actual Android OS wake-up, force-stop/reboot behav
The web helper uses SDK `dart:js_interop` only. The audit compiled `scripts/probe_web_lock.dart` to JavaScript and ran it against Node 24's native `navigator.locks`: two separate owner instances serialized, ownership released/reacquired, duplicate acquire/release rejected, insecure origin rejected, unsupported API rejected. The existing three Android channel tests still pass, and targeted analyzer checks pass. This exercises native Web Locks Promise semantics, not a real browser two-tab IndexedDB/keychain scenario; that integration remains an explicit runtime check. The platform owner rebuilds the full release web bundle after this change.
Desktop multiple independent app processes are not coordinated by this Web/Android helper. Standard desktop runners have one foreground AppStore; simultaneous desktop instances require a separately tested OS file-lock implementation before they can be claimed safe.
## Appearance update 4.1
Flutter supports system/light/dark mode plus five persisted presets (forest, indigo, ocean, rose, OLED). Selecting OLED initially enables dark mode; system mode subsequently follows OS brightness. Pure-black surfaces are checked both as theme colors and actual rendered pixels. Total Flutter tests: 62 passing. The native Watch client retains its existing appearance controls.
+6 -1
View File
@@ -9,13 +9,18 @@
- Аналитика за 1, 3 и 12 календарных месяцев, доходы/расходы, результат, сбережения, средний расход, крупнейшая категория, динамика и структура расходов.
- Месячные бюджеты по категориям, остаток и превышение; выбор месяца.
- Встроенные и собственные категории, 50 значков Material с совместимыми серверными именами, 30 эмодзи и собственный эмодзи, 24 цвета. Переименование обновляет связанные записи; удаление сохраняет историю.
- RUB, EUR, USD, SEK; системная, светлая и тёмная тема; CSV-экспорт.
- RUB, EUR, USD, SEK; системная, светлая и тёмная тема; пресеты Лес, Индиго, Океан, Роза и OLED; CSV-экспорт.
- OLED использует чистый чёрный фон `#000000` в тёмном режиме (включая карточки, навигацию и диалоги). При выборе OLED включается тёмная тема; затем можно выбрать системный режим. Режим и пресет сохраняются локально.
- Локальная работа без аккаунта; сервер, регистрация, вход, выход, refresh, ручная и автоматическая синхронизация, полная пересинхронизация.
- Транзакционное сохранение данных и курсора, очередь изменений и удалений, повтор после сетевого сбоя; отдельные хранилища аккаунтов.
- Адаптивная навигация: нижняя панель на телефоне, боковая на широком экране.
![Интерфейс на компьютере](docs/desktop.png)
OLED-пресет:
![OLED настройки](docs/oled-settings.png)
## Платформы
| Платформа | Реализация / сборка |
+8
View File
@@ -29,3 +29,11 @@
## Принятые различия
Material 3 и адаптивная навигация; денежные значения в целых сотых (старые доли менее сотой округляются); границы периодов `[start,end)`; отдельные данные аккаунтов; график динамики — пары столбцов. CloudKit удалён по прямому запросу. Финансовые payload, UUID, даты обновления, корректировки, категории, бюджеты и удаления совместимы с исходным сервером.
## Обновление 4.1 — режимы и пресеты
Добавлены Лес, Индиго, Океан, Роза и OLED. Режим (system/light/dark) и пресет сохраняются независимо в локальных настройках. При выборе OLED сначала включается dark; затем разрешено выбрать system. В тёмном OLED все основные Material surfaces, карточки, app bar, навигация и диалоги используют `#000000`, overlay tint отключён.
Общий запуск после изменений: 62 Flutter-теста PASS; анализатор No issues found. Новые проверки: точные цвета OLED, разные акценты пресетов, сохранение/перезапуск, реакция системного режима на OS brightness, выбор на экране 320px и проверка чёрных пикселей снимков 390/1280px.
APK 4.1 использует новый тестовый ключ, поскольку временный ключ предыдущей тестовой сборки не сохранился после смены среды. Прямая установка поверх APK 4.0 с другой подписью невозможна. Перед заменой старой тестовой сборки синхронизируйте её с Go-сервером и убедитесь в сохранности данных. Для последующих сборок ключ 4.1 сохранён отдельно от репозитория и исходного архива. Для публикации нужен собственный release keystore.
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

+15
View File
@@ -0,0 +1,15 @@
/// Device-local appearance preferences; these never enter finance sync payloads.
enum ColorPreset {
forest('forest', 'Лес', 0xff356859),
indigo('indigo', 'Индиго', 0xff4f46a5),
ocean('ocean', 'Океан', 0xff006b9a),
rose('rose', 'Роза', 0xff984563),
oled('oled', 'OLED — чёрный', 0xff51bda0);
const ColorPreset(this.id, this.label, this.seedArgb);
final String id, label;
final int seedArgb;
static ColorPreset fromId(String? id) =>
values.firstWhere((preset) => preset.id == id, orElse: () => forest);
}
@@ -7,6 +7,7 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
import '../domain/finance.dart';
import '../domain/appearance.dart';
import 'storage.dart';
import 'runtime_lock.dart';
import 'sync_api.dart';
@@ -62,6 +63,7 @@ class AppStore extends ChangeNotifier with WidgetsBindingObserver {
String serverUrl = '';
String currency = 'RUB';
String theme = 'system';
String themePreset = 'forest';
AuthSession? session;
String? sessionPersistenceWarning;
String get email => session?.email ?? '';
@@ -121,6 +123,7 @@ class AppStore extends ChangeNotifier with WidgetsBindingObserver {
serverUrl = _preferences!.getString('serverUrl') ?? serverUrl;
currency = _preferences!.getString('currency') ?? currency;
theme = _preferences!.getString('theme') ?? theme;
themePreset = ColorPreset.fromId(_preferences!.getString('themePreset')).id;
_deviceId = _preferences!.getString('deviceId') ?? newId();
await _preferences!.setString('deviceId', _deviceId);
_storage ??= await LedgerStorage.open();
@@ -147,6 +150,7 @@ class AppStore extends ChangeNotifier with WidgetsBindingObserver {
}
currency = _preferences!.getString('currency') ?? 'RUB';
theme = _preferences!.getString('theme') ?? 'system';
themePreset = ColorPreset.fromId(_preferences!.getString('themePreset')).id;
_deviceId = _preferences!.getString('deviceId') ?? _deviceId;
await _restoreSession();
await _loadScope();
@@ -481,10 +485,26 @@ class AppStore extends ChangeNotifier with WidgetsBindingObserver {
_notify();
});
Future<void> setTheme(String value) => _exclusive(() async {
if (!const ['system', 'light', 'dark'].contains(value)) {
throw ArgumentError('Неизвестный режим темы');
}
theme = value;
await _preferences!.setString('theme', value);
_notify();
});
Future<void> setThemePreset(String value) => _exclusive(() async {
if (!ColorPreset.values.any((preset) => preset.id == value)) {
throw ArgumentError('Неизвестный пресет');
}
await _preferences!.setString('themePreset', value);
themePreset = value;
if (value == ColorPreset.oled.id) {
await _preferences!.setString('theme', 'dark');
theme = 'dark';
}
_notify();
});
Future<void> setServerUrl(String value) => _exclusive(() async {
final normalized = value.trim().replaceAll(RegExp(r'/+$'), '');
final uri = Uri.tryParse(normalized);
+51 -25
View File
@@ -6,6 +6,8 @@ import 'package:intl/intl.dart';
import 'package:file_selector/file_selector.dart';
import 'package:share_plus/share_plus.dart';
import '../domain/finance.dart';
import '../domain/appearance.dart';
import 'app_theme.dart';
import '../services/app_store.dart';
import 'design.dart';
import 'editors.dart';
@@ -22,8 +24,14 @@ class BalanceApp extends StatelessWidget {
locale: const Locale('ru'),
supportedLocales: const [Locale('ru'), Locale('en')],
localizationsDelegates: GlobalMaterialLocalizations.delegates,
theme: makeTheme(Brightness.light),
darkTheme: makeTheme(Brightness.dark),
theme: BalanceThemes.build(
Brightness.light,
ColorPreset.fromId(store.themePreset),
),
darkTheme: BalanceThemes.build(
Brightness.dark,
ColorPreset.fromId(store.themePreset),
),
themeMode: store.theme == 'dark'
? ThemeMode.dark
: store.theme == 'light'
@@ -34,28 +42,6 @@ class BalanceApp extends StatelessWidget {
: const Scaffold(body: Center(child: CircularProgressIndicator())),
),
);
ThemeData makeTheme(Brightness brightness) {
final scheme = ColorScheme.fromSeed(
seedColor: const Color(0xff356859),
brightness: brightness,
);
return ThemeData(
useMaterial3: true,
fontFamily: 'BalanceRoboto',
colorScheme: scheme,
scaffoldBackgroundColor: scheme.surface,
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
cardTheme: CardThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
),
appBarTheme: AppBarTheme(
backgroundColor: scheme.surface,
centerTitle: false,
),
);
}
}
class BalanceShell extends StatefulWidget {
@@ -1105,6 +1091,7 @@ class _BalanceShellState extends State<BalanceShell> {
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
key: ValueKey('theme-mode-${store.theme}'),
initialValue: store.theme,
decoration: const InputDecoration(labelText: 'Тема'),
items: const [
@@ -1116,6 +1103,45 @@ class _BalanceShellState extends State<BalanceShell> {
if (v != null) run(() => store.setTheme(v));
},
),
const SizedBox(height: 20),
Text(
'Цветовой пресет',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: ColorPreset.values
.map(
(preset) => ChoiceChip(
label: Text(preset.label),
avatar: CircleAvatar(
radius: 9,
backgroundColor: preset == ColorPreset.oled
? Colors.black
: Color(preset.seedArgb),
child: preset == ColorPreset.oled
? const Icon(
Icons.nightlight_round,
size: 13,
color: Colors.white,
)
: null,
),
selected: store.themePreset == preset.id,
onSelected: (_) =>
run(() => store.setThemePreset(preset.id)),
),
)
.toList(),
),
if (store.themePreset == 'oled') ...[
const SizedBox(height: 12),
const Text(
'OLED: чистый чёрный фон #000000 в тёмном режиме. При выборе пресета тёмная тема включается автоматически. Можно затем выбрать системный режим.',
),
],
const SizedBox(height: 12),
Text(
'Валюта задаёт формат сумм. Конвертация валют не выполняется.',
@@ -1288,7 +1314,7 @@ class _BalanceShellState extends State<BalanceShell> {
onPressed: () => showAboutDialog(
context: context,
applicationName: 'Баланс',
applicationVersion: '4.0.0',
applicationVersion: '4.1.0',
applicationIcon: const Icon(
Icons.account_balance_wallet_outlined,
size: 40,
+65
View File
@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import '../domain/appearance.dart';
class BalanceThemes {
static ThemeData build(Brightness brightness, ColorPreset preset) {
final oled = brightness == Brightness.dark && preset == ColorPreset.oled;
var scheme = ColorScheme.fromSeed(
seedColor: Color(preset.seedArgb),
brightness: brightness,
);
if (oled) {
scheme = scheme.copyWith(
surface: Colors.black,
surfaceDim: Colors.black,
surfaceBright: Colors.black,
surfaceContainerLowest: Colors.black,
surfaceContainerLow: Colors.black,
surfaceContainer: Colors.black,
surfaceContainerHigh: Colors.black,
surfaceContainerHighest: Colors.black,
surfaceTint: Colors.transparent,
primaryContainer: Colors.black,
onPrimaryContainer: scheme.onSurface,
secondaryContainer: const Color(0xff171b19),
outlineVariant: const Color(0xff303633),
);
}
return ThemeData(
useMaterial3: true,
fontFamily: 'BalanceRoboto',
colorScheme: scheme,
scaffoldBackgroundColor: scheme.surface,
canvasColor: scheme.surface,
applyElevationOverlayColor: !oled,
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
cardTheme: CardThemeData(
surfaceTintColor: oled ? Colors.transparent : null,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
side: oled
? BorderSide(color: scheme.outlineVariant)
: BorderSide.none,
),
),
appBarTheme: AppBarTheme(
backgroundColor: scheme.surface,
surfaceTintColor: oled ? Colors.transparent : null,
centerTitle: false,
),
navigationBarTheme: NavigationBarThemeData(
backgroundColor: oled ? Colors.black : null,
surfaceTintColor: oled ? Colors.transparent : null,
),
navigationRailTheme: NavigationRailThemeData(
backgroundColor: oled ? Colors.black : null,
),
dialogTheme: DialogThemeData(
backgroundColor: oled ? Colors.black : null,
surfaceTintColor: oled ? Colors.transparent : null,
),
);
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
name: balance_flutter
description: Local-first personal finance with BalanceServer synchronization.
publish_to: none
version: 4.0.0+1
version: 4.1.0+2
environment:
sdk: '>=3.8.0 <4.0.0'
dependencies:
+140
View File
@@ -0,0 +1,140 @@
import 'package:balance_flutter/domain/appearance.dart';
import 'package:balance_flutter/services/app_store.dart';
import 'package:balance_flutter/services/storage.dart';
import 'package:balance_flutter/ui/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'widget_test.dart' as fixtures;
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(fixtures.mockStorage);
test('OLED uses pure black for surfaces, cards, navigation and dialogs', () {
final theme = BalanceThemes.build(Brightness.dark, ColorPreset.oled);
final scheme = theme.colorScheme;
expect(theme.brightness, Brightness.dark);
for (final surface in [
theme.scaffoldBackgroundColor,
theme.canvasColor,
scheme.surface,
scheme.surfaceDim,
scheme.surfaceBright,
scheme.surfaceContainerLowest,
scheme.surfaceContainerLow,
scheme.surfaceContainer,
scheme.surfaceContainerHigh,
scheme.surfaceContainerHighest,
scheme.primaryContainer,
theme.appBarTheme.backgroundColor,
theme.navigationBarTheme.backgroundColor,
theme.navigationRailTheme.backgroundColor,
theme.dialogTheme.backgroundColor,
]) {
expect(surface, Colors.black);
}
expect(theme.cardTheme.surfaceTintColor, Colors.transparent);
expect(theme.applyElevationOverlayColor, isFalse);
expect(scheme.onSurface.computeLuminance(), greaterThan(.6));
expect(scheme.primary.computeLuminance(), greaterThan(.3));
});
test('presets have distinct accents and preserve light/dark mode', () {
final accents = <Color>{};
for (final preset in ColorPreset.values) {
final light = BalanceThemes.build(Brightness.light, preset);
final dark = BalanceThemes.build(Brightness.dark, preset);
expect(light.brightness, Brightness.light);
expect(dark.brightness, Brightness.dark);
expect(light.scaffoldBackgroundColor, isNot(Colors.black));
accents.add(light.colorScheme.primary);
}
expect(accents.length, ColorPreset.values.length);
});
test(
'preset and mode survive restart; OLED initially selects dark',
() async {
final storage = MemorySnapshotStorage();
final store = AppStore(storage: storage, background: true);
await store.initialize();
expect(store.themePreset, 'forest');
expect(store.theme, 'system');
await store.setTheme('light');
await store.setThemePreset('oled');
expect(store.theme, 'dark');
await store.setTheme('system');
final reopened = AppStore(storage: storage, background: true);
await reopened.initialize();
expect(reopened.themePreset, 'oled');
expect(reopened.theme, 'system');
await reopened.setThemePreset('rose');
expect(reopened.theme, 'system');
await expectLater(
reopened.setThemePreset('unknown'),
throwsArgumentError,
);
await store.close();
await reopened.close();
store.dispose();
reopened.dispose();
},
);
testWidgets('system mode responds to OS brightness with OLED selected', (
tester,
) async {
final store = (await tester.runAsync(
() => fixtures.createStore(seeded: false),
))!;
await tester.runAsync(() async {
await store.setThemePreset('oled');
await store.setTheme('system');
});
tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
addTearDown(tester.platformDispatcher.clearPlatformBrightnessTestValue);
await fixtures.boot(tester, store, const Size(390, 844));
ThemeData theme() => Theme.of(tester.element(find.byType(Scaffold).first));
expect(theme().scaffoldBackgroundColor, Colors.black);
tester.platformDispatcher.platformBrightnessTestValue = Brightness.light;
await tester.pumpAndSettle();
expect(theme().brightness, Brightness.light);
expect(theme().scaffoldBackgroundColor, isNot(Colors.black));
await tester.runAsync(() => store.setTheme('dark'));
await tester.pumpAndSettle();
expect(theme().scaffoldBackgroundColor, Colors.black);
});
testWidgets(
'OLED can be selected in narrow settings and updates mode control',
(tester) async {
await fixtures.loadReadableFont();
final store = (await tester.runAsync(
() => fixtures.createStore(seeded: false),
))!;
await fixtures.boot(tester, store, const Size(320, 568));
await fixtures.navigate(tester, 4);
final oled = find.widgetWithText(ChoiceChip, 'OLED — чёрный');
await tester.ensureVisible(oled);
await tester.pumpAndSettle();
await tester.tap(oled);
await tester.runAsync(() async {
await Future<void>.delayed(const Duration(milliseconds: 50));
});
await tester.pumpAndSettle();
expect(store.themePreset, 'oled');
expect(store.theme, 'dark');
expect(
tester.widget<MaterialApp>(find.byType(MaterialApp)).themeMode,
ThemeMode.dark,
);
expect(
Theme.of(
tester.element(find.byType(Scaffold).first),
).scaffoldBackgroundColor,
Colors.black,
);
expect(tester.takeException(), isNull);
},
);
}
@@ -0,0 +1,44 @@
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'widget_test.dart' as fixtures;
void main() {
setUpAll(fixtures.loadReadableFont);
setUp(fixtures.mockStorage);
for (final size in [const Size(390, 844), const Size(1280, 900)]) {
testWidgets('OLED renders black pixels at $size', (tester) async {
final store = (await tester.runAsync(() => fixtures.createStore()))!;
await tester.runAsync(() => store.setThemePreset('oled'));
final key = GlobalKey();
await fixtures.boot(tester, store, size, boundaryKey: key);
for (final page in [0, 4]) {
await fixtures.navigate(tester, page);
await tester.runAsync(() async {
final boundary =
key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await boundary.toImage(pixelRatio: 1);
final rgba = await image.toByteData(
format: ui.ImageByteFormat.rawRgba,
);
// Left edge of the scaffold/rail below the app bar, away from text.
final pixel = (110 * image.width) * 4;
expect(rgba!.getUint8(pixel), 0);
expect(rgba.getUint8(pixel + 1), 0);
expect(rgba.getUint8(pixel + 2), 0);
expect(rgba.getUint8(pixel + 3), 255);
final png = await image.toByteData(format: ui.ImageByteFormat.png);
final dir = Directory('/tmp/balance-oled-preview')
..createSync(recursive: true);
File(
'${dir.path}/${size.width.toInt()}-${page == 0 ? 'overview' : 'settings'}.png',
).writeAsBytesSync(png!.buffer.asUint8List());
image.dispose();
});
expect(tester.takeException(), isNull);
}
});
}
}