From f5aae1a4d1d1f5357e0d1db5275bb45209b23f36 Mon Sep 17 00:00:00 2001 From: petlyh <88139840+petlyh@users.noreply.github.com> Date: Wed, 19 Jun 2024 21:16:10 +0200 Subject: [PATCH] Large refactor Most of the core has been rewritten or refactored. Also includes a few UI improvements. --- .github/workflows/build_web.yml | 3 +- .metadata | 22 +- analysis_options.yaml | 5 +- lib/main.dart | 41 ++-- lib/models/byte_list.dart | 68 ++++++ lib/models/file_data.dart | 109 ---------- lib/models/models.dart | 10 - lib/models/offset.dart | 33 --- lib/models/offset_value_holder.dart | 17 -- lib/models/savefile.dart | 18 ++ lib/models/value.dart | 81 +++++++ lib/models/values.dart | 64 ++++++ lib/providers/data.dart | 42 ---- lib/providers/empty_savefile.dart | 12 ++ lib/providers/savefile_controller.dart | 70 ++++++ lib/providers/savefile_repository.dart | 62 ++++++ lib/providers/values.dart | 126 ----------- lib/providers/values_controller.dart | 114 ++++++++++ lib/widgets/buttons.dart | 107 ++++----- lib/widgets/main_panel.dart | 113 ++++++---- lib/widgets/spinbox.dart | 57 ++--- lib/widgets/top_bar.dart | 8 +- linux/CMakeLists.txt | 6 + linux/my_application.cc | 20 ++ pubspec.lock | 286 +++++++++++++++---------- pubspec.yaml | 22 +- web/index.html | 23 +- windows/CMakeLists.txt | 8 +- windows/flutter/CMakeLists.txt | 7 +- windows/runner/Runner.rc | 2 +- windows/runner/utils.cpp | 4 +- 31 files changed, 909 insertions(+), 651 deletions(-) create mode 100644 lib/models/byte_list.dart delete mode 100644 lib/models/file_data.dart delete mode 100644 lib/models/models.dart delete mode 100644 lib/models/offset.dart delete mode 100644 lib/models/offset_value_holder.dart create mode 100644 lib/models/savefile.dart create mode 100644 lib/models/value.dart create mode 100644 lib/models/values.dart delete mode 100644 lib/providers/data.dart create mode 100644 lib/providers/empty_savefile.dart create mode 100644 lib/providers/savefile_controller.dart create mode 100644 lib/providers/savefile_repository.dart delete mode 100644 lib/providers/values.dart create mode 100644 lib/providers/values_controller.dart diff --git a/.github/workflows/build_web.yml b/.github/workflows/build_web.yml index 2dcc243..c4e66be 100644 --- a/.github/workflows/build_web.yml +++ b/.github/workflows/build_web.yml @@ -10,6 +10,7 @@ on: - "linux/**" - "windows/**" - ".github/**" + - "test/**" permissions: contents: write @@ -21,7 +22,7 @@ jobs: steps: - uses: subosito/flutter-action@v2 with: - flutter-version: "3.13.7" + flutter-version: "3.22.2" channel: "stable" - name: Checkout source code diff --git a/.metadata b/.metadata index a046fac..5f3dc5c 100644 --- a/.metadata +++ b/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: 796c8ef79279f9c774545b3771238c3098dbefab - channel: stable + revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49" + channel: "stable" project_type: app @@ -13,17 +13,17 @@ project_type: app migration: platforms: - platform: root - create_revision: 796c8ef79279f9c774545b3771238c3098dbefab - base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - platform: linux - create_revision: 796c8ef79279f9c774545b3771238c3098dbefab - base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - platform: web - create_revision: 796c8ef79279f9c774545b3771238c3098dbefab - base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 - platform: windows - create_revision: 796c8ef79279f9c774545b3771238c3098dbefab - base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 # User provided section diff --git a/analysis_options.yaml b/analysis_options.yaml index c4ec4db..6ad347f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,4 @@ -include: package:flutter_lints/flutter.yaml +include: package:lint/strict.yaml linter: rules: @@ -8,5 +8,8 @@ analyzer: language: strict-casts: true strict-raw-types: true + strong-mode: + implicit-casts: false + implicit-dynamic: false plugins: - custom_lint diff --git a/lib/main.dart b/lib/main.dart index ec0b0ce..6e6ebe1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,6 @@ -import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:mk8se/models/models.dart"; +import "package:mk8se/providers/empty_savefile.dart"; import "package:mk8se/widgets/main_panel.dart"; import "package:mk8se/widgets/top_bar.dart"; import "package:package_info_plus/package_info_plus.dart"; @@ -9,15 +8,21 @@ import "package:url_launcher/url_launcher_string.dart"; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await FileData.initEmpty(); - runApp(const ProviderScope(child: App())); + + final emptySavefile = await readEmptySavefile(); + + runApp( + ProviderScope( + overrides: [emptySavefileProvider.overrideWithValue(emptySavefile)], + child: const App(), + ), + ); } class App extends StatelessWidget { const App({super.key}); - // disable splash animations on web - static const splashFactory = kIsWeb ? NoSplash.splashFactory : null; + static const splashFactory = NoSplash.splashFactory; @override Widget build(BuildContext context) { @@ -33,7 +38,9 @@ class App extends StatelessWidget { splashFactory: splashFactory, brightness: Brightness.dark, colorScheme: ColorScheme.fromSeed( - seedColor: Colors.blue, brightness: Brightness.dark), + seedColor: Colors.blue, + brightness: Brightness.dark, + ), useMaterial3: true, visualDensity: VisualDensity.compact, ), @@ -50,19 +57,21 @@ class MainPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - scrolledUnderElevation: 0, + forceMaterialTransparency: true, toolbarHeight: 48, titleSpacing: 4, title: const TopRow(), actions: [ IconButton( - tooltip: "Source Code", - onPressed: () => launchUrlString(repoUrl), - icon: const Icon(Icons.code)), + tooltip: "Source Code", + onPressed: () => launchUrlString(repoUrl), + icon: const Icon(Icons.code), + ), IconButton( - tooltip: "Info", - onPressed: () => showInfo(context), - icon: const Icon(Icons.info_outline)), + tooltip: "Info", + onPressed: () => showInfo(context), + icon: const Icon(Icons.info_outline), + ), const SizedBox(width: 4), ], ), @@ -70,7 +79,7 @@ class MainPage extends StatelessWidget { ); } - void showInfo(BuildContext context) async { + Future showInfo(BuildContext context) async { final packageInfo = await PackageInfo.fromPlatform(); if (!context.mounted) { @@ -83,4 +92,4 @@ class MainPage extends StatelessWidget { applicationLegalese: "Licensed under AGPLv3.", ); } -} \ No newline at end of file +} diff --git a/lib/models/byte_list.dart b/lib/models/byte_list.dart new file mode 100644 index 0000000..2a77b7d --- /dev/null +++ b/lib/models/byte_list.dart @@ -0,0 +1,68 @@ +import "dart:math"; +import "dart:typed_data"; + +import "package:built_collection/built_collection.dart"; +import "package:collection/collection.dart"; +import "package:meta/meta.dart"; + +@immutable +extension type const ByteList._(BuiltList _list) implements Iterable { + ByteList.fromUint8List(Uint8List uint8List) : _list = uint8List.build(); + + ByteList.fromByteData(ByteData byteData) + : _list = byteData.buffer.asUint8List().build(); + + @redeclare + List toList() => _list.toList(); + + Uint8List toUint8List() => Uint8List.fromList(toList()); + + List toSublist(int start, [int? end]) => + _list.sublist(start, end).toList(); + + int readInt8(int offset) => _list[offset]; + + int readInt32(int offset) => _decodeBase(toSublist(offset, offset + 4), 256); + + ByteList withInt8At(int number, int offset) => ByteList._( + _list.rebuild( + (b) => b.replaceRange( + offset, + offset + 1, + [number.clamp(0, 255)], + ), + ), + ); + + ByteList withInt32At(int number, int offset) => ByteList._( + _list.rebuild( + (b) => b.replaceRange( + offset, + offset + 4, + _encodeBase(number, 4, 256), + ), + ), + ); +} + +/// Encodes [number] to base [radix] with [digitCount] digits. +/// +/// Result is [Endian.big]. +Iterable _encodeBase(int number, int digitCount, int radix) { + final digitValue = pow(radix, digitCount - 1).toInt(); + final digit = (number / digitValue).truncate(); + + return [ + digit, + if (digitCount > 1) + ..._encodeBase(number - (digit * digitValue), digitCount - 1, radix), + ]; +} + +/// Interprets a list of digits of base [radix] as a an integer. +/// +/// [input] is interpreted as [Endian.big]. +int _decodeBase(List input, int radix) => input.reversed.reduceIndexed( + (index, previous, element) => + (previous + element.clamp(0, radix - 1) * pow(radix, index)).toInt(), + ); diff --git a/lib/models/file_data.dart b/lib/models/file_data.dart deleted file mode 100644 index 55f3fa0..0000000 --- a/lib/models/file_data.dart +++ /dev/null @@ -1,109 +0,0 @@ -part of "models.dart"; - -class FileData { - static const magicNumber = 0x43545553; - static const deluxeMagicNumber = 0x53555443; - - bool get isMagicValid => readInt(0x00, 4) == magicNumber; - bool get isDeluxeSave => readInt(0x00, 4) == deluxeMagicNumber; - - static const checksumOffset = NumberOffset(0x38); - static const checksumDataStart = 0x48; - - static late Uint8List _emptyData; - - static Future initEmpty() async { - final bytes = await rootBundle.load("assets/userdata-empty.dat"); - _emptyData = Uint8List.view(bytes.buffer); - } - - static FileData empty() => FileData(_emptyData); - - Uint8List _data; - ByteData get byteData => _data.buffer.asByteData(); - - final String path; - bool get isSaveableInPlace => path.isNotEmpty; - - FileData(this._data, [this.path = ""]); - - static Future fromFile(String filepath) async { - final bytes = await File(filepath).readAsBytes(); - return fromBytes(bytes, filepath); - } - - static FileData fromBytes(Uint8List bytes, [String filepath = ""]) { - final fileData = FileData(bytes, filepath); - - if (bytes.isEmpty) { - throw ArgumentError("File is empty"); - } - - if (!fileData.isMagicValid) { - if (fileData.isDeluxeSave) { - throw ArgumentError("File is a Mario Kart 8 Deluxe save file"); - } - - throw ArgumentError("File is not a Mario Kart 8 save file"); - } - - return fileData; - } - - Future toFile(String filepath) async { - _writeChecksum(); - await File(filepath).writeAsBytes(_data); - } - - void downloadWeb() { - _writeChecksum(); - AnchorElement() - ..href = Uri.dataFromBytes(_data, mimeType: "application/octet-stream") - .toString() - ..download = "userdata.dat" - ..style.display = "none" - ..click(); - } - - Future toFileInPlace() async { - if (path.isEmpty) { - throw StateError( - "Data cannot be saved in place because it wasn't opened from a file"); - } - - await toFile(path); - } - - int readInt(int offset, int byteCount) { - return switch (byteCount) { - 1 => byteData.getUint8(offset), - 2 => byteData.getUint16(offset, Endian.big), - 4 => byteData.getUint32(offset, Endian.big), - _ => throw ArgumentError( - "byteCount argument has an illegal value: $byteCount"), - }; - } - - void writeInt(int offset, int byteCount, int value) { - final writeBytes = switch (byteCount) { - 1 => Uint8List(1)..buffer.asByteData().setInt8(0, value), - 2 => Uint8List(2)..buffer.asByteData().setInt16(0, value, Endian.big), - 4 => Uint8List(4)..buffer.asByteData().setInt32(0, value, Endian.big), - _ => throw ArgumentError( - "byteCount argument has an illegal value: $byteCount"), - }; - - final list = _data.toList(); - list.replaceRange(offset, offset + byteCount, writeBytes); - _data = Uint8List.fromList(list); - } - - T readOffset(Offset offset) => offset.read(this); - void writeOffset(Offset offset, T value) => offset.write(this, value); - - /// Calculates and writes the save file checksum. - void _writeChecksum() { - final checksum = Crc32.calculate(_data.sublist(checksumDataStart)); - writeOffset(checksumOffset, checksum); - } -} diff --git a/lib/models/models.dart b/lib/models/models.dart deleted file mode 100644 index 8a0d702..0000000 --- a/lib/models/models.dart +++ /dev/null @@ -1,10 +0,0 @@ -import "dart:io"; -import "dart:typed_data"; - -import "package:crc32_checksum/crc32_checksum.dart"; -import "package:flutter/services.dart" show rootBundle; -import "package:universal_html/html.dart" show AnchorElement; - -part "file_data.dart"; -part "offset.dart"; -part "offset_value_holder.dart"; diff --git a/lib/models/offset.dart b/lib/models/offset.dart deleted file mode 100644 index e7e7395..0000000 --- a/lib/models/offset.dart +++ /dev/null @@ -1,33 +0,0 @@ -part of "models.dart"; - -abstract class Offset { - /// the offset at which the data is located - final int offset; - - const Offset(this.offset); - - T read(FileData fileData); - void write(FileData fileData, T value); -} - -class NumberOffset extends Offset { - const NumberOffset(super.offset); - - @override - int read(FileData fileData) => fileData.readInt(offset, 4); - - @override - void write(FileData fileData, int value) => - fileData.writeInt(offset, 4, value); -} - -class UnlockableOffset extends Offset { - const UnlockableOffset(super.offset); - - @override - bool read(FileData fileData) => fileData.readInt(offset, 1) > 0; - - @override - void write(FileData fileData, bool value) => - fileData.writeInt(offset, 1, value ? 3 : 0); -} diff --git a/lib/models/offset_value_holder.dart b/lib/models/offset_value_holder.dart deleted file mode 100644 index ad9d4ee..0000000 --- a/lib/models/offset_value_holder.dart +++ /dev/null @@ -1,17 +0,0 @@ -part of "models.dart"; - -class OffsetValueHolder { - final Offset offset; - T value; - - OffsetValueHolder(this.offset, this.value); - - static OffsetValueHolder unlockable(int offset) => - OffsetValueHolder(UnlockableOffset(offset), false); - - static OffsetValueHolder number(int offset) => - OffsetValueHolder(NumberOffset(offset), 0); - - void read(FileData data) => value = data.readOffset(offset); - void write(FileData data) => data.writeOffset(offset, value); -} diff --git a/lib/models/savefile.dart b/lib/models/savefile.dart new file mode 100644 index 0000000..8a26dd4 --- /dev/null +++ b/lib/models/savefile.dart @@ -0,0 +1,18 @@ +import "dart:typed_data"; + +import "package:mk8se/models/byte_list.dart"; + +class Savefile { + final ByteList bytes; + final String? path; + + const Savefile(this.bytes, {this.path}); + + bool get isSaveableInPlace => path != null; + + Uint8List get uint8List => bytes.toUint8List(); + + String get url => Uri.dataFromBytes(bytes.toList()).toString(); + + Savefile withBytes(ByteList newBytes) => Savefile(newBytes, path: path); +} diff --git a/lib/models/value.dart b/lib/models/value.dart new file mode 100644 index 0000000..9e9687d --- /dev/null +++ b/lib/models/value.dart @@ -0,0 +1,81 @@ +import "package:meta/meta.dart"; +import "package:mk8se/models/byte_list.dart"; + +@immutable +sealed class Value { + final String name; + final int offset; + + const Value({required this.name, required this.offset}); + + /// Returns a copy of this [Value] containing data decoded from [bytes] at [offset]. + Value read(ByteList bytes); + + /// Returns a copy of [bytes] with the encoded data of this [Value] at [offset]. + ByteList write(ByteList bytes); +} + +class UnlockableValue extends Value { + final bool isUnlocked; + + const UnlockableValue(String name, int offset) + : this._(name: name, offset: offset, isUnlocked: false); + + const UnlockableValue._({ + required super.name, + required super.offset, + required this.isUnlocked, + }); + + UnlockableValue withValue(bool value) => + UnlockableValue._(name: name, offset: offset, isUnlocked: value); + + @override + Value read(ByteList bytes) => withValue(bytes.readInt8(offset) > 0x00); + + @override + ByteList write(ByteList bytes) => + bytes.withInt8At(isUnlocked ? 0x03 : 0x00, offset); + + @override + bool operator ==(Object other) => + other is UnlockableValue && + name == other.name && + offset == other.offset && + isUnlocked == other.isUnlocked; + + @override + int get hashCode => Object.hash(name, offset, isUnlocked); +} + +class NumberValue extends Value { + final int number; + + const NumberValue(String name, int offset) + : this._(name: name, offset: offset, number: 0); + + const NumberValue._({ + required super.name, + required super.offset, + required this.number, + }); + + NumberValue withValue(int value) => + NumberValue._(name: name, offset: offset, number: value); + + @override + Value read(ByteList bytes) => withValue(bytes.readInt32(offset)); + + @override + ByteList write(ByteList bytes) => bytes.withInt32At(number, offset); + + @override + bool operator ==(Object other) => + other is NumberValue && + name == other.name && + offset == other.offset && + number == other.number; + + @override + int get hashCode => Object.hash(name, offset, number); +} diff --git a/lib/models/values.dart b/lib/models/values.dart new file mode 100644 index 0000000..a5e7cc8 --- /dev/null +++ b/lib/models/values.dart @@ -0,0 +1,64 @@ +import "package:built_collection/built_collection.dart"; +import "package:collection/collection.dart"; +import "package:meta/meta.dart"; +import "package:mk8se/models/byte_list.dart"; +import "package:mk8se/models/value.dart"; + +@immutable +extension type const Values(BuiltList _list) + implements Iterable { + Iterable get allValues => map((category) => category.values).flattened; + + Values read(ByteList bytes) => + _withMappedValues((value) => value.read(bytes)); + + ByteList write(ByteList bytes) => + allValues.fold(bytes, (currentBytes, value) => value.write(currentBytes)); + + Values withReplacedValue(Value newValue) => _withMappedValues( + (value) => value.name == newValue.name ? newValue : value, + ); + + Values withAllUnlocked() => _withMappedValues( + (value) => value is UnlockableValue && !value.isUnlocked + ? value.withValue(true) + : value, + ); + + Category? getCategory(String categoryName) => + _list.firstWhereOrNull((c) => c.name == categoryName); + + Value? getValue(String categoryName, String valueName) => + getCategory(categoryName) + ?.values + .firstWhereOrNull((v) => v.name == valueName); + + Values _withMappedValues(Value Function(Value) f) => Values( + _list.rebuild( + (b) => b.map( + (category) => + category.withValues(category.values.rebuild((b2) => b2.map(f))), + ), + ), + ); +} + +@immutable +class Category { + final String name; + final BuiltList values; + + const Category({required this.name, required this.values}); + + Iterable get valueNames => values.map((v) => v.name); + + Category withValues(Iterable newValues) => + Category(name: name, values: newValues.toBuiltList()); + + @override + bool operator ==(Object other) => + other is Category && name == other.name && values == other.values; + + @override + int get hashCode => Object.hash(name, values); +} diff --git a/lib/providers/data.dart b/lib/providers/data.dart deleted file mode 100644 index 220e634..0000000 --- a/lib/providers/data.dart +++ /dev/null @@ -1,42 +0,0 @@ -import "package:flutter/foundation.dart"; -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:mk8se/models/models.dart"; -import "package:mk8se/providers/values.dart"; - -final dataProvider = NotifierProvider(DataNotifier.new); - -class DataNotifier extends Notifier { - @override - FileData build() => FileData.empty(); - - Future loadEmpty() async { - state = FileData.empty(); - ref.read(valuesProvider.notifier).readData(state); - } - - Future load(String filepath) async { - state = await FileData.fromFile(filepath); - ref.read(valuesProvider.notifier).readData(state); - } - - void loadBytes(Uint8List bytes) { - state = FileData.fromBytes(bytes); - ref.read(valuesProvider.notifier).readData(state); - } - - Future save() async { - ref.read(valuesProvider.notifier).writeData(state); - - if (kIsWeb) { - state.downloadWeb(); - return; - } - - await state.toFileInPlace(); - } - - Future saveAs(String filepath) async { - ref.read(valuesProvider.notifier).writeData(state); - await state.toFile(filepath); - } -} diff --git a/lib/providers/empty_savefile.dart b/lib/providers/empty_savefile.dart new file mode 100644 index 0000000..12c1ccd --- /dev/null +++ b/lib/providers/empty_savefile.dart @@ -0,0 +1,12 @@ +import "package:mk8se/models/savefile.dart"; +import "package:mk8se/providers/savefile_repository.dart"; +import "package:riverpod/riverpod.dart"; + +final emptySavefileProvider = Provider((_) { + throw Exception("emptySavefileProvider is not initialized"); +}); + +Future readEmptySavefile() async { + const key = "assets/userdata-empty.dat"; + return await SavefileRepository.readAsset(key); +} diff --git a/lib/providers/savefile_controller.dart b/lib/providers/savefile_controller.dart new file mode 100644 index 0000000..bda3a95 --- /dev/null +++ b/lib/providers/savefile_controller.dart @@ -0,0 +1,70 @@ +import "package:crc32_checksum/crc32_checksum.dart"; +import "package:file_picker/file_picker.dart"; +import "package:flutter/foundation.dart"; +import "package:mk8se/models/byte_list.dart"; +import "package:mk8se/models/savefile.dart"; +import "package:mk8se/providers/empty_savefile.dart"; +import "package:mk8se/providers/savefile_repository.dart"; +import "package:mk8se/providers/values_controller.dart"; +import "package:riverpod/riverpod.dart"; + +final savefileControllerProvider = + NotifierProvider(SavefileController.new); + +class SavefileController extends Notifier { + static const _checksumOffset = 0x38; + static const _checksumDataOffset = 0x48; + + @override + Savefile build() => ref.read(emptySavefileProvider); + + Future load(String path) async => + state = await SavefileRepository.read(path); + + void loadBytes(Uint8List bytes) => + state = Savefile(ByteList.fromUint8List(bytes)); + + Future saveInPlace() async => + await SavefileRepository.writeInPlace(_getUpdatedSavefile()); + + Future saveTo(String path) async => + await SavefileRepository.write(_getUpdatedSavefile(), path); + + void saveWeb() => SavefileRepository.saveWeb(_getUpdatedSavefile()); + + /// Downloads the savefile if on web, otherwise saves it in place. + Future savePlatform() async => kIsWeb ? saveWeb() : await saveInPlace(); + + /// Opens a file picker dialog and loads the selected file. + Future loadFileDialog() async { + final result = await FilePicker.platform + .pickFiles(type: FileType.custom, allowedExtensions: ["dat"]); + + if (result != null && result.files.isNotEmpty) { + return kIsWeb + ? loadBytes(result.files.first.bytes!) + : await load(result.paths.first!); + } + } + + /// Opens a file picker dialog and writes the file to the selected path. + Future saveFileDialog() async { + final filepath = await FilePicker.platform + .saveFile(type: FileType.custom, allowedExtensions: ["dat"]); + + if (filepath != null) { + await ref.read(savefileControllerProvider.notifier).saveTo(filepath); + } + } + + Savefile _getUpdatedSavefile() { + final values = ref.read(valuesControllerProvider); + final bytes = values.write(state.bytes); + final checksum = _calculateChecksum(bytes); + final updatedBytes = bytes.withInt32At(checksum, _checksumOffset); + return state.withBytes(updatedBytes); + } + + int _calculateChecksum(ByteList bytes) => + Crc32.calculate(bytes.toSublist(_checksumDataOffset)); +} diff --git a/lib/providers/savefile_repository.dart b/lib/providers/savefile_repository.dart new file mode 100644 index 0000000..3f5e3fa --- /dev/null +++ b/lib/providers/savefile_repository.dart @@ -0,0 +1,62 @@ +import "dart:io"; + +import "package:flutter/services.dart"; +import "package:mk8se/models/byte_list.dart"; +import "package:mk8se/models/savefile.dart"; +import "package:universal_html/html.dart" show AnchorElement; + +final class SavefileRepository { + const SavefileRepository._(); + + static Future read(String path) async { + final bytes = await File(path).readAsBytes(); + final byteList = ByteList.fromUint8List(bytes); + _validateSavedata(byteList); + return Savefile(byteList, path: path); + } + + static Future readAsset(String key) async { + final byteData = await rootBundle.load(key); + final byteList = ByteList.fromByteData(byteData); + _validateSavedata(byteList); + return Savefile(byteList); + } + + static Future write(Savefile savefile, String path) async { + await File(path).writeAsBytes(savefile.uint8List); + } + + static Future writeInPlace(Savefile savefile) async { + if (savefile.path == null) { + throw ArgumentError( + "The provided savefile cannot be written to in-place because it doesn't have a path associated with it", + ); + } + + return await write(savefile, savefile.path!); + } + + static void saveWeb(Savefile savefile) => AnchorElement(href: savefile.url) + ..download = "userdata.dat" + ..style.display = "none" + ..click(); +} + +void _validateSavedata(ByteList byteList) { + const magicNumber = 0x43545553; + const deluxeMagicNumber = 0x53555443; + + if (byteList.isEmpty || byteList.length < 4) { + throw ArgumentError("File is empty"); + } + + final magicBytes = byteList.readInt32(0x00); + + if (magicBytes != magicNumber) { + if (magicBytes == deluxeMagicNumber) { + throw ArgumentError("File is a Mario Kart 8 Deluxe save file"); + } + + throw ArgumentError("File is not a Mario Kart 8 save file"); + } +} diff --git a/lib/providers/values.dart b/lib/providers/values.dart deleted file mode 100644 index f18ada2..0000000 --- a/lib/providers/values.dart +++ /dev/null @@ -1,126 +0,0 @@ -import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:mk8se/models/models.dart"; - -typedef Values = Map>>; - -final valuesProvider = - NotifierProvider(ValuesNotifier.new); - -class ValuesNotifier extends Notifier { - @override - Values build() => values; - - void _forEach(Function(OffsetValueHolder holder) func) { - for (final category in state.keys) { - for (final entry in state[category]!.keys) { - func(state[category]![entry]!); - } - } - } - - void setValue(OffsetValueHolder holder, T value) { - holder.value = value; - ref.notifyListeners(); - } - - void unlockAll() { - _forEach((holder) { - if (holder.offset.runtimeType != UnlockableOffset) { - return; - } - - holder.value = true; - }); - - ref.notifyListeners(); - } - - void readData(FileData data) { - _forEach((holder) => holder.read(data)); - ref.notifyListeners(); - } - - void writeData(FileData data) { - _forEach((holder) => holder.write(data)); - } - - List>> getCategoryEntries( - String category) => - state[category]!.entries.toList(); -} - -Values values = { - "Stats": { - "Coins": OffsetValueHolder.number(0x1530), - "Drifts": OffsetValueHolder.number(0x153C), - "Jump Boosts": OffsetValueHolder.number(0x1538), - "Mini-Turbos": OffsetValueHolder.number(0x1544), - "Super Mini-Turbos": OffsetValueHolder.number(0x1548), - "Balloons Popped": OffsetValueHolder.number(0x154C), - "Own Balloons Popped": OffsetValueHolder.number(0x1550), - }, - "Characters": { - "Rosalina": OffsetValueHolder.unlockable(0x1AA4), - "Metal Mario": OffsetValueHolder.unlockable(0x1AA5), - "Pink Gold Peach": OffsetValueHolder.unlockable(0x1AA6), - "Lakitu": OffsetValueHolder.unlockable(0x1AA7), - "Baby Rosalina": OffsetValueHolder.unlockable(0x1AAD), - "Larry": OffsetValueHolder.unlockable(0x1AAE), - "Lemmy": OffsetValueHolder.unlockable(0x1AAF), - "Wendy": OffsetValueHolder.unlockable(0x1AB0), - "Ludwig": OffsetValueHolder.unlockable(0x1AB1), - "Iggy": OffsetValueHolder.unlockable(0x1AB2), - "Roy": OffsetValueHolder.unlockable(0x1AB3), - "Morton": OffsetValueHolder.unlockable(0x1AB4), - "Mii": OffsetValueHolder.unlockable(0x1AB5), - }, - "Karts": { - "Pipe Frame": OffsetValueHolder.unlockable(0x1AD9), - "Steel Diver": OffsetValueHolder.unlockable(0x1ADB), - "Cat Cruiser": OffsetValueHolder.unlockable(0x1ADC), - "Circuit Special": OffsetValueHolder.unlockable(0x1ADD), - "Tri-Speeder": OffsetValueHolder.unlockable(0x1ADE), - "Prancer": OffsetValueHolder.unlockable(0x1AE0), - "Landship": OffsetValueHolder.unlockable(0x1AE2), - "Bounder": OffsetValueHolder.unlockable(0x1AE3), - "Sports Coupé": OffsetValueHolder.unlockable(0x1AE4), - "Gold Kart": OffsetValueHolder.unlockable(0x1AE5), - }, - "Bikes": { - "Comet": OffsetValueHolder.unlockable(0x1AE7), - "The Duke": OffsetValueHolder.unlockable(0x1AE9), - "Flame Rider": OffsetValueHolder.unlockable(0x1AEA), - "Varmint": OffsetValueHolder.unlockable(0x1AEB), - "Mr Scooty": OffsetValueHolder.unlockable(0x1AEC), - "Jet Bike": OffsetValueHolder.unlockable(0x1AED), - "Yoshi Bike": OffsetValueHolder.unlockable(0x1AEE), - "Wild Wiggler": OffsetValueHolder.unlockable(0x1AF0), - "Teddy Buggy": OffsetValueHolder.unlockable(0x1AF1), - }, - "Tires": { - "Slick": OffsetValueHolder.unlockable(0x1B1C), - "Metal": OffsetValueHolder.unlockable(0x1B1D), - "Button": OffsetValueHolder.unlockable(0x1B1E), - "Off-Road": OffsetValueHolder.unlockable(0x1B1F), - "Sponge": OffsetValueHolder.unlockable(0x1B20), - "Cushion": OffsetValueHolder.unlockable(0x1B22), - "Normal Blue": OffsetValueHolder.unlockable(0x1B23), - "Funky Monster": OffsetValueHolder.unlockable(0x1B24), - "Azure Roller": OffsetValueHolder.unlockable(0x1B25), - "Crimson Slim": OffsetValueHolder.unlockable(0x1B26), - "Cyber Slick": OffsetValueHolder.unlockable(0x1B27), - "Retro Off-Road": OffsetValueHolder.unlockable(0x1B28), - "Gold Wheels": OffsetValueHolder.unlockable(0x1B29), - }, - "Gliders": { - "Cloud Glider": OffsetValueHolder.unlockable(0x1B59), - "Wario Wing": OffsetValueHolder.unlockable(0x1B5A), - "Waddle Wing": OffsetValueHolder.unlockable(0x1B5B), - "Peach Parasol": OffsetValueHolder.unlockable(0x1B5C), - "Flower Glider": OffsetValueHolder.unlockable(0x1B5F), - "Bowser Kite": OffsetValueHolder.unlockable(0x1B60), - "Plane Glider": OffsetValueHolder.unlockable(0x1B61), - "MKTV Parafoil": OffsetValueHolder.unlockable(0x1B62), - "Gold Glider": OffsetValueHolder.unlockable(0x1B63), - }, -}; diff --git a/lib/providers/values_controller.dart b/lib/providers/values_controller.dart new file mode 100644 index 0000000..83a21f6 --- /dev/null +++ b/lib/providers/values_controller.dart @@ -0,0 +1,114 @@ +import "package:built_collection/built_collection.dart"; +import "package:mk8se/models/value.dart"; +import "package:mk8se/models/values.dart"; +import "package:mk8se/providers/savefile_controller.dart"; +import "package:riverpod/riverpod.dart"; + +final valuesControllerProvider = + NotifierProvider(ValuesController.new); + +class ValuesController extends Notifier { + @override + Values build() => + _initialValues.read(ref.watch(savefileControllerProvider).bytes); + + void setValue(Value value) => state = state.withReplacedValue(value); + + void unlockAll() => state = state.withAllUnlocked(); +} + +final Values _initialValues = Values( + BuiltList.of([ + Category( + name: "Stats", + values: BuiltList.of([ + const NumberValue("Coins", 0x1530), + const NumberValue("Drifts", 0x153C), + const NumberValue("Jump Boosts", 0x1538), + const NumberValue("Mini-Turbos", 0x1544), + const NumberValue("Super Mini-Turbos", 0x1548), + const NumberValue("Balloons Popped", 0x154C), + const NumberValue("Own Balloons Popped", 0x1550), + ]), + ), + Category( + name: "Characters", + values: BuiltList.of([ + const UnlockableValue("Rosalina", 0x1AA4), + const UnlockableValue("Metal Mario", 0x1AA5), + const UnlockableValue("Pink Gold Peach", 0x1AA6), + const UnlockableValue("Lakitu", 0x1AA7), + const UnlockableValue("Baby Rosalina", 0x1AAD), + const UnlockableValue("Larry", 0x1AAE), + const UnlockableValue("Lemmy", 0x1AAF), + const UnlockableValue("Wendy", 0x1AB0), + const UnlockableValue("Ludwig", 0x1AB1), + const UnlockableValue("Iggy", 0x1AB2), + const UnlockableValue("Roy", 0x1AB3), + const UnlockableValue("Morton", 0x1AB4), + const UnlockableValue("Mii", 0x1AB5), + ]), + ), + Category( + name: "Karts", + values: BuiltList.of([ + const UnlockableValue("Pipe Frame", 0x1AD9), + const UnlockableValue("Steel Diver", 0x1ADB), + const UnlockableValue("Cat Cruiser", 0x1ADC), + const UnlockableValue("Circuit Special", 0x1ADD), + const UnlockableValue("Tri-Speeder", 0x1ADE), + const UnlockableValue("Prancer", 0x1AE0), + const UnlockableValue("Landship", 0x1AE2), + const UnlockableValue("Bounder", 0x1AE3), + const UnlockableValue("Sports Coupé", 0x1AE4), + const UnlockableValue("Gold Kart", 0x1AE5), + ]), + ), + Category( + name: "Bikes", + values: BuiltList.of([ + const UnlockableValue("Comet", 0x1AE7), + const UnlockableValue("The Duke", 0x1AE9), + const UnlockableValue("Flame Rider", 0x1AEA), + const UnlockableValue("Varmint", 0x1AEB), + const UnlockableValue("Mr Scooty", 0x1AEC), + const UnlockableValue("Jet Bike", 0x1AED), + const UnlockableValue("Yoshi Bike", 0x1AEE), + const UnlockableValue("Wild Wiggler", 0x1AF0), + const UnlockableValue("Teddy Buggy", 0x1AF1), + ]), + ), + Category( + name: "Tires", + values: BuiltList.of([ + const UnlockableValue("Slick", 0x1B1C), + const UnlockableValue("Metal", 0x1B1D), + const UnlockableValue("Button", 0x1B1E), + const UnlockableValue("Off-Road", 0x1B1F), + const UnlockableValue("Sponge", 0x1B20), + const UnlockableValue("Cushion", 0x1B22), + const UnlockableValue("Normal Blue", 0x1B23), + const UnlockableValue("Funky Monster", 0x1B24), + const UnlockableValue("Azure Roller", 0x1B25), + const UnlockableValue("Crimson Slim", 0x1B26), + const UnlockableValue("Cyber Slick", 0x1B27), + const UnlockableValue("Retro Off-Road", 0x1B28), + const UnlockableValue("Gold Wheels", 0x1B29), + ]), + ), + Category( + name: "Gliders", + values: BuiltList.of([ + const UnlockableValue("Cloud Glider", 0x1B59), + const UnlockableValue("Wario Wing", 0x1B5A), + const UnlockableValue("Waddle Wing", 0x1B5B), + const UnlockableValue("Peach Parasol", 0x1B5C), + const UnlockableValue("Flower Glider", 0x1B5F), + const UnlockableValue("Bowser Kite", 0x1B60), + const UnlockableValue("Plane Glider", 0x1B61), + const UnlockableValue("MKTV Parafoil", 0x1B62), + const UnlockableValue("Gold Glider", 0x1B63), + ]), + ), + ]), +); diff --git a/lib/widgets/buttons.dart b/lib/widgets/buttons.dart index fb155f6..763d5cb 100644 --- a/lib/widgets/buttons.dart +++ b/lib/widgets/buttons.dart @@ -1,9 +1,9 @@ -import "package:file_picker/file_picker.dart"; -import "package:flutter/foundation.dart"; +import "dart:async"; + import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:mk8se/providers/data.dart"; -import "package:mk8se/providers/values.dart"; +import "package:mk8se/providers/savefile_controller.dart"; +import "package:mk8se/providers/values_controller.dart"; abstract class CustomButton extends ConsumerWidget { const CustomButton({super.key, this.enabled = true}); @@ -13,9 +13,9 @@ abstract class CustomButton extends ConsumerWidget { String get name; IconData get icon; - Future onTap(WidgetRef ref); + FutureOr onTap(WidgetRef ref); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { try { await onTap(ref); } catch (e) { @@ -24,24 +24,24 @@ abstract class CustomButton extends ConsumerWidget { } showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Error"), - content: Text(e.toString()), - actions: [ - TextButton( - child: const Text("Close"), - onPressed: () => Navigator.of(context).pop(), - ), - ], - )); + context: context, + builder: (context) => AlertDialog( + title: const Text("Error"), + content: Text(e.toString()), + actions: [ + TextButton( + child: const Text("Close"), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ); } } static final style = ButtonStyle( - padding: - MaterialStateProperty.all(const EdgeInsets.symmetric(horizontal: 8)), - shape: MaterialStateProperty.all( + padding: WidgetStateProperty.all(const EdgeInsets.symmetric(horizontal: 8)), + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(4), ), @@ -64,90 +64,69 @@ abstract class CustomButton extends ConsumerWidget { class EmptyButton extends CustomButton { @override - final String name = "New"; + String get name => "New"; @override - final IconData icon = Icons.insert_drive_file_outlined; + IconData get icon => Icons.insert_drive_file_outlined; const EmptyButton({super.key}); @override - Future onTap(WidgetRef ref) async { - await ref.read(dataProvider.notifier).loadEmpty(); + void onTap(WidgetRef ref) { + ref.invalidate(savefileControllerProvider); + // Invalidate the values provider to make sure the values are reset + // even if the loaded savefile was already the empty savefile. + ref.invalidate(valuesControllerProvider); } } class LoadButton extends CustomButton { @override - final String name = "Load"; + String get name => "Load"; @override - final IconData icon = Icons.upload_file; + IconData get icon => Icons.upload_file; const LoadButton({super.key}); @override - Future onTap(WidgetRef ref) async { - final result = await FilePicker.platform - .pickFiles(type: FileType.custom, allowedExtensions: ["dat"]); - - if (result == null || result.files.isEmpty) { - return; - } - - if (kIsWeb) { - final bytes = result.files.first.bytes!; - ref.read(dataProvider.notifier).loadBytes(bytes); - return; - } - - final filepath = result.paths.first!; - await ref.read(dataProvider.notifier).load(filepath); - } + Future onTap(WidgetRef ref) async => + await ref.read(savefileControllerProvider.notifier).loadFileDialog(); } class SaveButton extends CustomButton { @override - final String name = "Save"; + String get name => "Save"; @override - final IconData icon = Icons.save_alt; + IconData get icon => Icons.save_alt; const SaveButton({super.key, super.enabled}); @override - Future onTap(WidgetRef ref) async { - await ref.read(dataProvider.notifier).save(); - } + Future onTap(WidgetRef ref) async => + await ref.read(savefileControllerProvider.notifier).savePlatform(); } class SaveAsButton extends CustomButton { @override - final String name = "Save As"; + String get name => "Save As"; @override - final IconData icon = Icons.save_alt; + IconData get icon => Icons.save_alt; const SaveAsButton({super.key}); @override - Future onTap(WidgetRef ref) async { - final filepath = await FilePicker.platform - .saveFile(type: FileType.custom, allowedExtensions: ["dat"]); - - if (filepath == null) { - return; - } - - await ref.read(dataProvider.notifier).saveAs(filepath); - } + Future onTap(WidgetRef ref) async => + await ref.read(savefileControllerProvider.notifier).saveFileDialog(); } class UnlockAllButton extends CustomButton { @override - final String name = "Unlock All"; + String get name => "Unlock All"; @override - final IconData icon = Icons.lock_open; + IconData get icon => Icons.lock_open; const UnlockAllButton({super.key}); @override - Future onTap(WidgetRef ref) async => - ref.read(valuesProvider.notifier).unlockAll(); + void onTap(WidgetRef ref) => + ref.read(valuesControllerProvider.notifier).unlockAll(); } diff --git a/lib/widgets/main_panel.dart b/lib/widgets/main_panel.dart index 0861eba..b174289 100644 --- a/lib/widgets/main_panel.dart +++ b/lib/widgets/main_panel.dart @@ -1,11 +1,12 @@ +import "package:built_collection/built_collection.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:mk8se/models/models.dart"; -import "package:mk8se/providers/values.dart"; +import "package:mk8se/models/value.dart"; +import "package:mk8se/providers/values_controller.dart"; import "package:mk8se/widgets/spinbox.dart"; final selectedCategoryProvider = StateProvider( - (ref) => values.keys.first, + (ref) => ref.read(valuesControllerProvider).first.name, ); class MainPanel extends ConsumerWidget { @@ -16,7 +17,7 @@ class MainPanel extends ConsumerWidget { return const Row( mainAxisSize: MainAxisSize.min, children: [ - Expanded(flex: 1, child: CategoriesPanel()), + Expanded(child: CategoriesPanel()), VerticalDivider(width: 0), Expanded(flex: 2, child: ValuesEditorPanel()), ], @@ -27,18 +28,26 @@ class MainPanel extends ConsumerWidget { class CategoriesPanel extends ConsumerWidget { const CategoriesPanel({super.key}); + void Function() setSelected(WidgetRef ref, String category) => + () => ref.read(selectedCategoryProvider.notifier).state = category; + @override Widget build(BuildContext context, WidgetRef ref) { - final categories = values.keys; + final categories = ref.read(valuesControllerProvider); + final selectedCategory = ref.watch(selectedCategoryProvider); return ListView( children: categories - .map((category) => ListTile( - title: Text(category, style: const TextStyle(fontSize: 14)), - onTap: () => ref.read(selectedCategoryProvider.notifier).state = - category, - selected: category == ref.watch(selectedCategoryProvider), - )) + .map( + (category) => ListTile( + title: Text( + category.name, + style: const TextStyle(fontSize: 14), + ), + onTap: setSelected(ref, category.name), + selected: category.name == selectedCategory, + ), + ) .toList(), ); } @@ -47,41 +56,63 @@ class CategoriesPanel extends ConsumerWidget { class ValuesEditorPanel extends ConsumerWidget { const ValuesEditorPanel({super.key}); - Widget? valueEditor( - WidgetRef ref, MapEntry> entry) { - final holder = entry.value; - final offsetType = holder.offset.runtimeType; - - if (offsetType == UnlockableOffset) { - return Checkbox( - value: holder.value as bool, - onChanged: (value) => - ref.read(valuesProvider.notifier).setValue(holder, value)); - } else if (offsetType == NumberOffset) { - return SpinBox( - value: holder.value as int, - onChange: (value) => - ref.read(valuesProvider.notifier).setValue(holder, value)); - } - return null; - } - @override Widget build(BuildContext context, WidgetRef ref) { - final category = ref.watch(selectedCategoryProvider); - ref.watch(valuesProvider); - final entries = - ref.watch(valuesProvider.notifier).getCategoryEntries(category); + final categoryName = ref.watch(selectedCategoryProvider); + + // Converted to BuiltList so RiverPod can check for equality + // and prevent unnecessary rebuilds + final valueNames = ref.watch( + valuesControllerProvider + .select((v) => BuiltList.of(v.getCategory(categoryName)!.valueNames)), + ); return ListView( - children: entries - .map((entry) => ListTile( - contentPadding: const EdgeInsets.only(left: 16, right: 8), - dense: true, - title: Text(entry.key, style: const TextStyle(fontSize: 14)), - trailing: - Transform.scale(scale: 0.9, child: valueEditor(ref, entry)))) + // Key prevents checkboxes being reused and animated when + // switching to a different category page. + key: ValueKey(categoryName), + children: valueNames + .map((valueName) => ValueEntry(categoryName, valueName)) .toList(), ); } } + +class ValueEntry extends ConsumerWidget { + const ValueEntry(this.categoryName, this.valueName, {super.key}); + + final String categoryName; + final String valueName; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final value = ref.watch( + valuesControllerProvider + .select((v) => v.getValue(categoryName, valueName)!), + ); + + final valuesController = ref.read(valuesControllerProvider.notifier); + + return ListTile( + contentPadding: const EdgeInsets.only(left: 16, right: 8), + dense: true, + title: Text(valueName, style: const TextStyle(fontSize: 14)), + onTap: value is UnlockableValue + ? () => valuesController.setValue(value.withValue(!value.isUnlocked)) + : null, + trailing: Transform.scale( + scale: 0.9, + child: switch (value) { + UnlockableValue() => Checkbox( + value: value.isUnlocked, + onChanged: (v) => valuesController.setValue(value.withValue(v!)), + ), + NumberValue() => SpinBox( + value: value.number, + onChange: (v) => valuesController.setValue(value.withValue(v)), + ), + }, + ), + ); + } +} diff --git a/lib/widgets/spinbox.dart b/lib/widgets/spinbox.dart index 7b759ef..40ab390 100644 --- a/lib/widgets/spinbox.dart +++ b/lib/widgets/spinbox.dart @@ -29,34 +29,41 @@ class SpinBox extends StatelessWidget { final cursorPosition = value.toString().length; return Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - tooltip: "Decrease", - onPressed: () => onNumberChange(value - 1), - icon: const Icon(Icons.remove)), - SizedBox( - width: 100, - child: TextField( - enableInteractiveSelection: false, - textAlign: TextAlign.center, - controller: TextEditingController.fromValue(TextEditingValue( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + tooltip: "Decrease", + onPressed: () => onNumberChange(value - 1), + icon: const Icon(Icons.remove), + ), + SizedBox( + width: 100, + child: TextField( + enableInteractiveSelection: false, + textAlign: TextAlign.center, + controller: TextEditingController.fromValue( + TextEditingValue( text: value.toString(), selection: TextSelection( - baseOffset: cursorPosition, extentOffset: cursorPosition), - )), - onChanged: onTextChange, - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly - ], // Only numbers can be entered + baseOffset: cursorPosition, + extentOffset: cursorPosition, + ), + ), ), + onChanged: onTextChange, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], // Only numbers can be entered ), - IconButton( - tooltip: "Increase", - onPressed: () => onNumberChange(value + 1), - icon: const Icon(Icons.add)), - ]); + ), + IconButton( + tooltip: "Increase", + onPressed: () => onNumberChange(value + 1), + icon: const Icon(Icons.add), + ), + ], + ); } } diff --git a/lib/widgets/top_bar.dart b/lib/widgets/top_bar.dart index 116ee25..57c57b9 100644 --- a/lib/widgets/top_bar.dart +++ b/lib/widgets/top_bar.dart @@ -1,16 +1,16 @@ import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; -import "package:mk8se/providers/data.dart"; - -import "buttons.dart"; +import "package:mk8se/providers/savefile_controller.dart"; +import "package:mk8se/widgets/buttons.dart"; class TopRow extends ConsumerWidget { const TopRow({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final isSaveableInPlace = ref.watch(dataProvider).isSaveableInPlace; + final isSaveableInPlace = + ref.watch(savefileControllerProvider).isSaveableInPlace; return Row( children: [ diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index dfc073a..56ce2e0 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -123,6 +123,12 @@ foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) COMPONENT Runtime) endforeach(bundled_library) +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/linux/my_application.cc b/linux/my_application.cc index 4d162a5..de89898 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -81,6 +81,24 @@ static gboolean my_application_local_command_line(GApplication* application, gch return TRUE; } +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + // Implements GObject::dispose. static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); @@ -91,6 +109,8 @@ static void my_application_dispose(GObject* object) { static void my_application_class_init(MyApplicationClass* klass) { G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } diff --git a/pubspec.lock b/pubspec.lock index d625057..3752b3d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.4.1" analyzer_plugin: dependency: transitive description: @@ -29,18 +29,18 @@ packages: dependency: transitive description: name: archive - sha256: "7e0d52067d05f2e0324268097ba723b71cb41ac8a6a2b24d1edf9c536b987b03" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.6" + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + built_collection: + dependency: "direct main" + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" characters: dependency: transitive description: @@ -93,10 +101,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -106,13 +114,13 @@ packages: source: hosted version: "1.1.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -129,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + url: "https://pub.dev" + source: hosted + version: "0.3.4+1" crypto: dependency: transitive description: @@ -145,38 +161,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" custom_lint: - dependency: transitive + dependency: "direct dev" description: name: custom_lint - sha256: "837821e4619c167fd5a547b03bb2fc6be7e65b800ec75528848429705c31ceba" + sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.6.4" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "3537d50202568994a6f42b1f2953aed6292fc5ecf83e45237af73f64aff2be72" + sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.6.4" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "3bdebdd52a42b4d6e5be9cd833ad1ecfbbc23e1020ca537060e54085497aea9c" + sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.6.3" dart_style: dependency: transitive description: name: dart_style - sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334 + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.6" fake_async: dependency: transitive description: @@ -189,10 +213,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -205,10 +229,18 @@ packages: dependency: "direct main" description: name: file_picker - sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 + sha256: "2ca051989f69d1b2ca012b2cf3ccf78c70d40144f0861ff2c063493f7c8c3d45" + url: "https://pub.dev" + source: hosted + version: "8.0.5" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -226,26 +258,26 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "4.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c + sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.0.20" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: e667e406a74d67715f1fa0bd941d9ded49aff72f3a9f4440a36aece4e8d457a7 + sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.5.1" flutter_test: dependency: "direct dev" description: flutter @@ -276,10 +308,10 @@ packages: dependency: transitive description: name: hotreloader - sha256: "728c0613556c1d153f7e7f4a367cffacc3f5a677d7f6497a1c2b35add4e6dacf" + sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "4.2.0" html: dependency: transitive description: @@ -292,10 +324,10 @@ packages: dependency: transitive description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" http_parser: dependency: transitive description: @@ -308,34 +340,58 @@ packages: dependency: transitive description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.3" - js: + version: "4.2.0" + json_annotation: dependency: transitive description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "0.6.7" - json_annotation: + version: "4.9.0" + leak_tracker: dependency: transitive description: - name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lint: + dependency: "direct dev" + description: + name: lint + sha256: d758a5211fce7fd3f5e316f804daefecdc34c7e53559716125e6da7388ae8565 + url: "https://pub.dev" + source: hosted + version: "2.3.0" lints: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "4.0.0" logging: dependency: transitive description: @@ -348,26 +404,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: - dependency: transitive + dependency: "direct main" description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.12.0" package_config: dependency: transitive description: @@ -380,50 +436,42 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "8.0.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" path: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" petitparser: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d - url: "https://pub.dev" - source: hosted - version: "2.1.6" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "2.1.8" pub_semver: dependency: transitive description: @@ -436,34 +484,34 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" riverpod: dependency: "direct main" description: name: riverpod - sha256: "494bf2cfb4df30000273d3052bdb1cc1de738574c6b678f0beb146ea56f5e208" + sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.5.1" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: f5770a33ce528a5a1ce6e3fe09b33c989c1cbcc74baaca9bdf96de6df1d840b1 + sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.5.1" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "858e4754e0c95f670edd2390c8fd512ef9565c4176ef3e88826a451dfdc8ddf3" + sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.10" rxdart: dependency: transitive description: @@ -485,14 +533,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -505,10 +561,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -537,10 +593,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.7.0" typed_data: dependency: transitive description: @@ -569,74 +625,74 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.1.14" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330 + sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.3.3" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f" + sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.3.0" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5" + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.3.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.1.1" uuid: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.4.0" vector_math: dependency: transitive description: @@ -649,10 +705,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "14.2.1" watcher: dependency: transitive description: @@ -665,26 +721,26 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.5.1" win32: dependency: transitive description: name: win32 - sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.0.9" + version: "5.5.1" xml: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" yaml: dependency: transitive description: @@ -694,5 +750,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0 <4.0.0" - flutter: ">=3.13.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 50572f5..fbe19b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,27 +5,31 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=3.0.5 <4.0.0' + sdk: '>=3.3.0 <4.0.0' dependencies: + built_collection: ^5.1.1 + collection: ^1.18.0 + crc32_checksum: ^0.0.2 + cupertino_icons: ^1.0.8 + file_picker: ^8.0.3 flutter: sdk: flutter - - crc32_checksum: ^0.0.2 flutter_riverpod: ^2.4.3 + meta: ^1.12.0 + package_info_plus: ^8.0.0 riverpod: ^2.4.3 - file_picker: ^5.5.0 - url_launcher: ^6.1.14 - package_info_plus: ^4.1.0 universal_html: ^2.2.4 + url_launcher: ^6.1.14 dev_dependencies: + custom_lint: ^0.6.4 + flutter_launcher_icons: ^0.13.1 + flutter_lints: ^4.0.0 flutter_test: sdk: flutter - - flutter_lints: ^2.0.0 + lint: ^2.3.0 riverpod_lint: ^2.3.0 - flutter_launcher_icons: ^0.13.1 flutter: uses-material-design: true diff --git a/web/index.html b/web/index.html index 278ee7d..a866c21 100644 --- a/web/index.html +++ b/web/index.html @@ -31,29 +31,8 @@ MK8SE - - - - - + diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 5408a59..5b02fc3 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -8,7 +8,7 @@ set(BINARY_NAME "MK8SE") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. -cmake_policy(SET CMP0063 NEW) +cmake_policy(VERSION 3.14...3.25) # Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) @@ -87,6 +87,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 3f71e17..efb62eb 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 85e0bd7..4d52775 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -93,7 +93,7 @@ BEGIN VALUE "FileDescription", "MK8SE" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "MK8SE" "\0" - VALUE "LegalCopyright", "Copyright (C) 2023 io.github.petlyh. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 io.github.petlyh. All rights reserved." "\0" VALUE "OriginalFilename", "mk8se.exe" "\0" VALUE "ProductName", "MK8SE" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp index fc55c57..259d85b 100644 --- a/windows/runner/utils.cpp +++ b/windows/runner/utils.cpp @@ -45,13 +45,13 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { if (utf16_string == nullptr) { return std::string(); } - int target_length = ::WideCharToMultiByte( + unsigned int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, nullptr, 0, nullptr, nullptr) -1; // remove the trailing null character int input_length = (int)wcslen(utf16_string); std::string utf8_string; - if (target_length <= 0 || target_length > utf8_string.max_size()) { + if (target_length == 0 || target_length > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length);