Compare commits
No commits in common. "bc128cef3d805559dd3c2e6aba58b535907531d9" and "d680a4499b70412264271f6f663301414980f41e" have entirely different histories.
bc128cef3d
...
d680a4499b
25 changed files with 160 additions and 2922 deletions
|
|
@ -8,7 +8,7 @@ services:
|
||||||
restart: 'unless-stopped'
|
restart: 'unless-stopped'
|
||||||
|
|
||||||
gitea:
|
gitea:
|
||||||
image: 'data.forgejo.org/forgejo/runner:7'
|
image: 'data.forgejo.org/forgejo/runner:4.0.0'
|
||||||
links:
|
links:
|
||||||
- docker-in-docker
|
- docker-in-docker
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
"game": {
|
|
||||||
"title": "SHITMAN",
|
|
||||||
"subtitle": "Lorteprank Mesteren",
|
|
||||||
"description": "Snig rundt og efterlad brændende lorteposer\nudea at blive opdaget!"
|
|
||||||
},
|
|
||||||
"menu": {
|
|
||||||
"start_mission": "START MISSION",
|
|
||||||
"infinite_mode": "UENDELIG TILSTAND",
|
|
||||||
"settings": "INDSTILLINGER",
|
|
||||||
"main_menu": "HOVEDMENU",
|
|
||||||
"resume": "FORTSÆT",
|
|
||||||
"back_to_menu": "TILBAGE TIL MENU"
|
|
||||||
},
|
|
||||||
"gameplay": {
|
|
||||||
"hidden": "Skjult",
|
|
||||||
"visible": "Synlig",
|
|
||||||
"detected": "OPDAGET!",
|
|
||||||
"find_target": "Find målhus",
|
|
||||||
"place_poop": "Placer lortepose",
|
|
||||||
"ring_doorbell": "Ring på døren",
|
|
||||||
"escape": "FLYGT!",
|
|
||||||
"mission_complete": "Mission Fuldført!",
|
|
||||||
"mission_failed": "Mission Mislykkedes!"
|
|
||||||
},
|
|
||||||
"controls": {
|
|
||||||
"move": "WASD/Piletaster: Bevæg",
|
|
||||||
"place_bag": "MELLEMRUM: Placer lortepose",
|
|
||||||
"ring_bell": "E: Ring på døren",
|
|
||||||
"pause": "ESC: Pause"
|
|
||||||
},
|
|
||||||
"ui": {
|
|
||||||
"paused": "PAUSERET",
|
|
||||||
"debug_mode": "Debug Tilstand:",
|
|
||||||
"close": "Luk"
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"placing_poop": "Placerer lortepose på position",
|
|
||||||
"attempting_doorbell": "Forsøger at ringe på døren",
|
|
||||||
"poop_lit": "Ding dong! Lorteposen brænder! LØB!",
|
|
||||||
"need_poop_first": "Skal placere lortepose først!",
|
|
||||||
"not_near_door": "Ikke tæt på målhusets dør",
|
|
||||||
"near_target": "Tæt på målhus med lortepose placeret!",
|
|
||||||
"player_detected": "Spiller opdaget! Mission mislykkedes!",
|
|
||||||
"new_target": "Nyt mål valgt",
|
|
||||||
"poop_burning": "Lorteposen brænder nu!",
|
|
||||||
"poop_extinguished": "Lorteposen er brændt ud"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
"game": {
|
|
||||||
"title": "SHITMAN",
|
|
||||||
"subtitle": "Der Kackhaufen-Streich-Meister",
|
|
||||||
"description": "Schleich herum und hinterlasse brennende Kacktüten\nohne erwischt zu werden!"
|
|
||||||
},
|
|
||||||
"menu": {
|
|
||||||
"start_mission": "MISSION STARTEN",
|
|
||||||
"infinite_mode": "ENDLOS MODUS",
|
|
||||||
"settings": "EINSTELLUNGEN",
|
|
||||||
"main_menu": "HAUPTMENÜ",
|
|
||||||
"resume": "FORTSETZEN",
|
|
||||||
"back_to_menu": "ZURÜCK ZUM MENÜ"
|
|
||||||
},
|
|
||||||
"gameplay": {
|
|
||||||
"hidden": "Versteckt",
|
|
||||||
"visible": "Sichtbar",
|
|
||||||
"detected": "ENTDECKT!",
|
|
||||||
"find_target": "Zielhaus finden",
|
|
||||||
"place_poop": "Kacktüte platzieren",
|
|
||||||
"ring_doorbell": "Klingeln",
|
|
||||||
"escape": "FLUCHT!",
|
|
||||||
"mission_complete": "Mission Erfolgreich!",
|
|
||||||
"mission_failed": "Mission Gescheitert!"
|
|
||||||
},
|
|
||||||
"controls": {
|
|
||||||
"move": "WASD/Pfeiltasten: Bewegen",
|
|
||||||
"place_bag": "LEERTASTE: Kacktüte platzieren",
|
|
||||||
"ring_bell": "E: Klingeln",
|
|
||||||
"pause": "ESC: Pause"
|
|
||||||
},
|
|
||||||
"ui": {
|
|
||||||
"paused": "PAUSIERT",
|
|
||||||
"debug_mode": "Debug Modus:",
|
|
||||||
"close": "Schließen"
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"placing_poop": "Kacktüte wird platziert an Position",
|
|
||||||
"attempting_doorbell": "Versuche zu klingeln",
|
|
||||||
"poop_lit": "Ding dong! Kacktüte brennt! LAUF!",
|
|
||||||
"need_poop_first": "Kacktüte muss zuerst platziert werden!",
|
|
||||||
"not_near_door": "Nicht in der Nähe der Zielhaustür",
|
|
||||||
"near_target": "In der Nähe des Zielhauses mit platzierter Kacktüte!",
|
|
||||||
"player_detected": "Spieler entdeckt! Mission gescheitert!",
|
|
||||||
"new_target": "Neues Ziel ausgewählt",
|
|
||||||
"poop_burning": "Kacktüte brennt jetzt!",
|
|
||||||
"poop_extinguished": "Kacktüte ist ausgebrannt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
{
|
|
||||||
"game": {
|
|
||||||
"title": "SHITMAN",
|
|
||||||
"subtitle": "The Poop Prank Master",
|
|
||||||
"description": "Sneak around and leave flaming poop bags\nwithout getting caught!"
|
|
||||||
},
|
|
||||||
"menu": {
|
|
||||||
"start_mission": "START MISSION",
|
|
||||||
"infinite_mode": "INFINITE MODE",
|
|
||||||
"settings": "SETTINGS",
|
|
||||||
"main_menu": "MAIN MENU",
|
|
||||||
"resume": "RESUME",
|
|
||||||
"back_to_menu": "BACK TO MENU"
|
|
||||||
},
|
|
||||||
"gameplay": {
|
|
||||||
"hidden": "Hidden",
|
|
||||||
"visible": "Visible",
|
|
||||||
"detected": "DETECTED!",
|
|
||||||
"find_target": "Find target house",
|
|
||||||
"place_poop": "Place poop bag",
|
|
||||||
"ring_doorbell": "Ring doorbell",
|
|
||||||
"escape": "ESCAPE!",
|
|
||||||
"mission_complete": "Mission Complete!",
|
|
||||||
"mission_failed": "Mission Failed!"
|
|
||||||
},
|
|
||||||
"controls": {
|
|
||||||
"move": "WASD/Arrow Keys: Move",
|
|
||||||
"place_bag": "SPACE: Place poop bag",
|
|
||||||
"ring_bell": "E: Ring doorbell",
|
|
||||||
"pause": "ESC: Pause"
|
|
||||||
},
|
|
||||||
"ui": {
|
|
||||||
"paused": "PAUSED",
|
|
||||||
"debug_mode": "Debug Mode:",
|
|
||||||
"close": "Close"
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"placing_poop": "Placing poop bag at position",
|
|
||||||
"attempting_doorbell": "Attempting to ring doorbell",
|
|
||||||
"poop_lit": "Ding dong! Poop bag is lit! RUN!",
|
|
||||||
"need_poop_first": "Need to place poop bag first!",
|
|
||||||
"not_near_door": "Not near target house door",
|
|
||||||
"near_target": "Near target house with poop bag placed!",
|
|
||||||
"player_detected": "Player detected! Mission failed!",
|
|
||||||
"new_target": "New target selected",
|
|
||||||
"poop_burning": "Poop bag is now on fire!",
|
|
||||||
"poop_extinguished": "Poop bag has burned out"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
abstract interface class Serializable {
|
|
||||||
/// Converts the object to a JSON string representation.
|
|
||||||
/// This method should be implemented by all classes that mixin Serializable.
|
|
||||||
String toJson();
|
|
||||||
|
|
||||||
/// Creates an object from a JSON string representation.
|
|
||||||
/// This method should be implemented by all classes that mixin Serializable.
|
|
||||||
Serializable.fromJson(String json);
|
|
||||||
|
|
||||||
/// Converts the object to a map representation.
|
|
||||||
/// This method is useful for converting the object to a format that can be
|
|
||||||
/// easily serialized or stored.
|
|
||||||
Map<String, dynamic> toMap();
|
|
||||||
|
|
||||||
/// Creates an object from a map representation.
|
|
||||||
/// This method is useful for converting the object from a format that can be
|
|
||||||
/// easily serialized or stored.
|
|
||||||
Serializable.fromMap(Map<String, dynamic> map);
|
|
||||||
}
|
|
||||||
|
|
@ -1,236 +0,0 @@
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
class Neighborhood extends Component {
|
|
||||||
static const double streetWidth = 60.0;
|
|
||||||
static const double houseSize = 80.0;
|
|
||||||
static const double yardSize = 40.0;
|
|
||||||
|
|
||||||
List<House> houses = [];
|
|
||||||
late List<Vector2> streetPaths;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
generateNeighborhood();
|
|
||||||
}
|
|
||||||
|
|
||||||
void generateNeighborhood() {
|
|
||||||
houses.clear();
|
|
||||||
removeAll(children);
|
|
||||||
|
|
||||||
// Create a simple 3x3 grid of houses
|
|
||||||
final random = Random();
|
|
||||||
|
|
||||||
for (int row = 0; row < 3; row++) {
|
|
||||||
for (int col = 0; col < 3; col++) {
|
|
||||||
// Skip center for street intersection
|
|
||||||
if (row == 1 && col == 1) continue;
|
|
||||||
|
|
||||||
final housePosition = Vector2(
|
|
||||||
col * (houseSize + streetWidth) + streetWidth,
|
|
||||||
row * (houseSize + streetWidth) + streetWidth,
|
|
||||||
);
|
|
||||||
|
|
||||||
final house = House(
|
|
||||||
position: housePosition,
|
|
||||||
isTarget: false, // Target will be set separately
|
|
||||||
houseType: random.nextInt(3), // 3 different house types
|
|
||||||
);
|
|
||||||
|
|
||||||
houses.add(house);
|
|
||||||
add(house);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate street paths
|
|
||||||
generateStreetPaths();
|
|
||||||
}
|
|
||||||
|
|
||||||
void generateStreetPaths() {
|
|
||||||
streetPaths = [];
|
|
||||||
|
|
||||||
// Horizontal streets
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
streetPaths.add(Vector2(0, i * (houseSize + streetWidth)));
|
|
||||||
streetPaths.add(Vector2(800, i * (houseSize + streetWidth)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertical streets
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
streetPaths.add(Vector2(i * (houseSize + streetWidth), 0));
|
|
||||||
streetPaths.add(Vector2(i * (houseSize + streetWidth), 600));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
House? getRandomHouse() {
|
|
||||||
if (houses.isEmpty) return null;
|
|
||||||
final random = Random();
|
|
||||||
return houses[random.nextInt(houses.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void render(Canvas canvas) {
|
|
||||||
super.render(canvas);
|
|
||||||
|
|
||||||
// Draw streets
|
|
||||||
final streetPaint = Paint()
|
|
||||||
..color = const Color(0xFF333333);
|
|
||||||
|
|
||||||
// Horizontal streets
|
|
||||||
for (int row = 0; row <= 3; row++) {
|
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(
|
|
||||||
0,
|
|
||||||
row * (houseSize + streetWidth) - streetWidth / 2,
|
|
||||||
800,
|
|
||||||
streetWidth
|
|
||||||
),
|
|
||||||
streetPaint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertical streets
|
|
||||||
for (int col = 0; col <= 3; col++) {
|
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(
|
|
||||||
col * (houseSize + streetWidth) - streetWidth / 2,
|
|
||||||
0,
|
|
||||||
streetWidth,
|
|
||||||
600
|
|
||||||
),
|
|
||||||
streetPaint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class House extends RectangleComponent {
|
|
||||||
bool isTarget;
|
|
||||||
int houseType;
|
|
||||||
bool hasLights = false;
|
|
||||||
bool hasSecurityCamera = false;
|
|
||||||
bool hasWatchDog = false;
|
|
||||||
|
|
||||||
Vector2? doorPosition;
|
|
||||||
Vector2? yardCenter;
|
|
||||||
|
|
||||||
House({
|
|
||||||
required Vector2 position,
|
|
||||||
required this.isTarget,
|
|
||||||
required this.houseType,
|
|
||||||
}) : super(
|
|
||||||
position: position,
|
|
||||||
size: Vector2.all(Neighborhood.houseSize),
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
|
|
||||||
// Set house color based on type
|
|
||||||
paint = Paint()..color = _getHouseColor();
|
|
||||||
|
|
||||||
// Calculate door and yard positions
|
|
||||||
doorPosition = position + Vector2(size.x / 2, size.y);
|
|
||||||
yardCenter = position + size / 2;
|
|
||||||
|
|
||||||
// Randomly add security features
|
|
||||||
final random = Random();
|
|
||||||
hasLights = random.nextBool();
|
|
||||||
hasSecurityCamera = random.nextDouble() < 0.3;
|
|
||||||
hasWatchDog = random.nextDouble() < 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color _getHouseColor() {
|
|
||||||
switch (houseType) {
|
|
||||||
case 0:
|
|
||||||
return isTarget ? const Color(0xFFFF6B6B) : const Color(0xFF8B4513); // Brown/Red if target
|
|
||||||
case 1:
|
|
||||||
return isTarget ? const Color(0xFFFF6B6B) : const Color(0xFF4682B4); // Blue/Red if target
|
|
||||||
case 2:
|
|
||||||
return isTarget ? const Color(0xFFFF6B6B) : const Color(0xFF228B22); // Green/Red if target
|
|
||||||
default:
|
|
||||||
return const Color(0xFF696969);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void render(Canvas canvas) {
|
|
||||||
super.render(canvas);
|
|
||||||
|
|
||||||
// Draw door
|
|
||||||
final doorPaint = Paint()
|
|
||||||
..color = const Color(0xFF654321);
|
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(size.x / 2 - 8, size.y - 4, 16, 4),
|
|
||||||
doorPaint,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Draw windows
|
|
||||||
final windowPaint = Paint()
|
|
||||||
..color = hasLights ? const Color(0xFFFFFF00) : const Color(0xFF87CEEB);
|
|
||||||
|
|
||||||
// Left window
|
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(size.x * 0.2, size.y * 0.3, 12, 12),
|
|
||||||
windowPaint,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Right window
|
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(size.x * 0.7, size.y * 0.3, 12, 12),
|
|
||||||
windowPaint,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Draw security features
|
|
||||||
if (hasSecurityCamera) {
|
|
||||||
final cameraPaint = Paint()
|
|
||||||
..color = const Color(0xFF000000);
|
|
||||||
canvas.drawCircle(
|
|
||||||
Offset(size.x * 0.9, size.y * 0.1),
|
|
||||||
4,
|
|
||||||
cameraPaint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasWatchDog) {
|
|
||||||
// Draw dog house in yard
|
|
||||||
final dogHousePaint = Paint()
|
|
||||||
..color = const Color(0xFF8B4513);
|
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(-20, size.y + 10, 15, 15),
|
|
||||||
dogHousePaint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw target indicator
|
|
||||||
if (isTarget) {
|
|
||||||
final targetPaint = Paint()
|
|
||||||
..color = const Color(0xFFFF0000)
|
|
||||||
..style = PaintingStyle.stroke
|
|
||||||
..strokeWidth = 3.0;
|
|
||||||
canvas.drawCircle(
|
|
||||||
Offset(size.x / 2, size.y / 2),
|
|
||||||
size.x / 2 + 10,
|
|
||||||
targetPaint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double getDetectionRadius() {
|
|
||||||
double radius = 50.0;
|
|
||||||
if (hasLights) radius += 20.0;
|
|
||||||
if (hasSecurityCamera) radius += 40.0;
|
|
||||||
if (hasWatchDog) radius += 30.0;
|
|
||||||
return radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool canDetectPlayer(Vector2 playerPosition, double playerStealthLevel) {
|
|
||||||
final distance = (playerPosition - yardCenter!).length;
|
|
||||||
final detectionRadius = getDetectionRadius() * (1.0 - playerStealthLevel);
|
|
||||||
|
|
||||||
return distance < detectionRadius;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame/events.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:shitman/game/shitman_game.dart';
|
|
||||||
import 'package:shitman/game/components/poop_bag.dart';
|
|
||||||
import 'package:shitman/game/components/neighborhood.dart';
|
|
||||||
|
|
||||||
class Player extends RectangleComponent with HasGameReference<ShitmanGame> {
|
|
||||||
static const double speed = 100.0;
|
|
||||||
static const double playerSize = 32.0;
|
|
||||||
|
|
||||||
Vector2 velocity = Vector2.zero();
|
|
||||||
bool hasPoopBag = true;
|
|
||||||
bool isHidden = false;
|
|
||||||
double stealthLevel = 0.0; // 0.0 = fully visible, 1.0 = completely hidden
|
|
||||||
PoopBag? placedPoopBag;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
|
|
||||||
// Create a simple colored rectangle as player
|
|
||||||
size = Vector2.all(playerSize);
|
|
||||||
position = Vector2(400, 300); // Start in center
|
|
||||||
|
|
||||||
// Set player color
|
|
||||||
paint = Paint()..color = const Color(0xFF0000FF); // Blue player
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void handleInput(Set<LogicalKeyboardKey> keysPressed) {
|
|
||||||
velocity = Vector2.zero();
|
|
||||||
|
|
||||||
// Movement controls
|
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowUp) ||
|
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyW)) {
|
|
||||||
velocity.y -= speed;
|
|
||||||
}
|
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowDown) ||
|
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyS)) {
|
|
||||||
velocity.y += speed;
|
|
||||||
}
|
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) ||
|
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyA)) {
|
|
||||||
velocity.x -= speed;
|
|
||||||
}
|
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.arrowRight) ||
|
|
||||||
keysPressed.contains(LogicalKeyboardKey.keyD)) {
|
|
||||||
velocity.x += speed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void handleAction(LogicalKeyboardKey key) {
|
|
||||||
// Action controls
|
|
||||||
if (key == LogicalKeyboardKey.space) {
|
|
||||||
placePoop();
|
|
||||||
}
|
|
||||||
if (key == LogicalKeyboardKey.keyE) {
|
|
||||||
ringDoorbell();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void updateStealthLevel(double dt) {
|
|
||||||
// Simple stealth calculation - can be enhanced later
|
|
||||||
// For now, player is more hidden when moving slowly or not at all
|
|
||||||
if (velocity.length < 50) {
|
|
||||||
stealthLevel = (stealthLevel + dt * 0.5).clamp(0.0, 1.0);
|
|
||||||
} else {
|
|
||||||
stealthLevel = (stealthLevel - dt * 1.5).clamp(0.0, 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
isHidden = stealthLevel > 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
void placePoop() {
|
|
||||||
if (!hasPoopBag) return;
|
|
||||||
|
|
||||||
debugPrint('Placing poop bag at $position');
|
|
||||||
|
|
||||||
// Create and place the poop bag
|
|
||||||
placedPoopBag = PoopBag();
|
|
||||||
placedPoopBag!.position = position + Vector2(playerSize / 2, playerSize + 10);
|
|
||||||
game.world.add(placedPoopBag!);
|
|
||||||
|
|
||||||
hasPoopBag = false;
|
|
||||||
|
|
||||||
// Check if near target house
|
|
||||||
checkMissionProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ringDoorbell() {
|
|
||||||
debugPrint('Attempting to ring doorbell');
|
|
||||||
|
|
||||||
// Check if near target house door
|
|
||||||
if (game.targetHouse.isPlayerNearTarget(position)) {
|
|
||||||
if (placedPoopBag != null) {
|
|
||||||
// Light the poop bag on fire
|
|
||||||
placedPoopBag!.lightOnFire();
|
|
||||||
debugPrint('Ding dong! Poop bag is lit! RUN!');
|
|
||||||
|
|
||||||
// Start escape timer - player has limited time to escape
|
|
||||||
startEscapeSequence();
|
|
||||||
} else {
|
|
||||||
debugPrint('Need to place poop bag first!');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debugPrint('Not near target house door');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void startEscapeSequence() {
|
|
||||||
// TODO: Implement escape mechanics
|
|
||||||
// For now, automatically complete mission after a delay
|
|
||||||
Future.delayed(Duration(seconds: 3), () {
|
|
||||||
if (game.gameState == GameState.playing) {
|
|
||||||
game.completeCurrentMission();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void checkMissionProgress() {
|
|
||||||
// Check if near target house and has placed poop bag
|
|
||||||
final targetPos = game.targetHouse.getTargetPosition();
|
|
||||||
if (targetPos != null && placedPoopBag != null) {
|
|
||||||
final distance = (position - targetPos).length;
|
|
||||||
if (distance < 80) {
|
|
||||||
debugPrint('Near target house with poop bag placed!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void getDetected() {
|
|
||||||
debugPrint('Player detected! Mission failed!');
|
|
||||||
game.failMission();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void update(double dt) {
|
|
||||||
super.update(dt);
|
|
||||||
|
|
||||||
// Apply movement
|
|
||||||
if (velocity.length > 0) {
|
|
||||||
velocity = velocity.normalized() * speed;
|
|
||||||
position += velocity * dt;
|
|
||||||
|
|
||||||
// Keep player on screen (basic bounds checking)
|
|
||||||
position.x = position.x.clamp(0, 800 - size.x);
|
|
||||||
position.y = position.y.clamp(0, 600 - size.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update stealth level based on environment
|
|
||||||
updateStealthLevel(dt);
|
|
||||||
|
|
||||||
// Check for detection by houses
|
|
||||||
checkForDetection();
|
|
||||||
}
|
|
||||||
|
|
||||||
void checkForDetection() {
|
|
||||||
final neighborhood = game.world.children.whereType<Neighborhood>().firstOrNull;
|
|
||||||
if (neighborhood == null) return;
|
|
||||||
|
|
||||||
for (final house in neighborhood.houses) {
|
|
||||||
if (house.canDetectPlayer(position, stealthLevel)) {
|
|
||||||
getDetected();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void render(Canvas canvas) {
|
|
||||||
// Update paint color based on stealth level
|
|
||||||
paint = Paint()
|
|
||||||
..color = isHidden ?
|
|
||||||
const Color(0xFF00FF00).withOpacity(0.7) : // Green when hidden
|
|
||||||
const Color(0xFF0000FF).withOpacity(0.9); // Blue when visible
|
|
||||||
|
|
||||||
super.render(canvas);
|
|
||||||
|
|
||||||
// Draw stealth indicator in debug mode
|
|
||||||
if (game.debugMode) {
|
|
||||||
final stealthPaint = Paint()
|
|
||||||
..color = Color.lerp(const Color(0xFFFF0000), const Color(0xFF00FF00), stealthLevel)!;
|
|
||||||
canvas.drawRect(
|
|
||||||
Rect.fromLTWH(-5, -10, size.x + 10, 5),
|
|
||||||
stealthPaint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame/effects.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
enum PoopBagState { placed, lit, burning, extinguished }
|
|
||||||
|
|
||||||
class PoopBag extends CircleComponent {
|
|
||||||
PoopBagState state = PoopBagState.placed;
|
|
||||||
double burnTimer = 0.0;
|
|
||||||
static const double burnDuration = 3.0; // seconds to burn
|
|
||||||
static const double bagSize = 16.0;
|
|
||||||
|
|
||||||
late Vector2 smokeOffset;
|
|
||||||
List<SmokeParticle> smokeParticles = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
|
|
||||||
radius = bagSize / 2;
|
|
||||||
paint = Paint()..color = const Color(0xFF8B4513); // Brown color
|
|
||||||
|
|
||||||
smokeOffset = Vector2(0, -radius - 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
void lightOnFire() {
|
|
||||||
if (state == PoopBagState.placed) {
|
|
||||||
state = PoopBagState.lit;
|
|
||||||
burnTimer = 0.0;
|
|
||||||
|
|
||||||
// Add flame effect
|
|
||||||
add(
|
|
||||||
ScaleEffect.to(
|
|
||||||
Vector2.all(1.2),
|
|
||||||
EffectController(duration: 0.5, infinite: true, reverseDuration: 0.5),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
debugPrint('Poop bag is now on fire!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void update(double dt) {
|
|
||||||
super.update(dt);
|
|
||||||
|
|
||||||
if (state == PoopBagState.lit) {
|
|
||||||
burnTimer += dt;
|
|
||||||
|
|
||||||
// Generate smoke particles
|
|
||||||
if (burnTimer % 0.2 < dt) { // Every 0.2 seconds
|
|
||||||
generateSmokeParticle();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if fully burned
|
|
||||||
if (burnTimer >= burnDuration) {
|
|
||||||
state = PoopBagState.burning;
|
|
||||||
extinguish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update smoke particles
|
|
||||||
smokeParticles.removeWhere((particle) {
|
|
||||||
particle.update(dt);
|
|
||||||
return particle.shouldRemove;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void generateSmokeParticle() {
|
|
||||||
final random = Random();
|
|
||||||
final particle = SmokeParticle(
|
|
||||||
position: position + smokeOffset + Vector2(
|
|
||||||
random.nextDouble() * 10 - 5,
|
|
||||||
random.nextDouble() * 5,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
smokeParticles.add(particle);
|
|
||||||
}
|
|
||||||
|
|
||||||
void extinguish() {
|
|
||||||
state = PoopBagState.extinguished;
|
|
||||||
removeAll(children.whereType<Effect>());
|
|
||||||
|
|
||||||
// Change to burnt color
|
|
||||||
paint = Paint()..color = const Color(0xFF2F2F2F);
|
|
||||||
|
|
||||||
debugPrint('Poop bag has burned out');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void render(Canvas canvas) {
|
|
||||||
super.render(canvas);
|
|
||||||
|
|
||||||
// Draw flame effect when lit
|
|
||||||
if (state == PoopBagState.lit) {
|
|
||||||
final flamePaint = Paint()
|
|
||||||
..color = Color.lerp(
|
|
||||||
const Color(0xFFFF4500),
|
|
||||||
const Color(0xFFFFD700),
|
|
||||||
sin(burnTimer * 10) * 0.5 + 0.5,
|
|
||||||
)!;
|
|
||||||
|
|
||||||
// Draw flickering flame
|
|
||||||
canvas.drawCircle(
|
|
||||||
Offset(0, -radius - 5),
|
|
||||||
radius * 0.6 + sin(burnTimer * 15) * 2,
|
|
||||||
flamePaint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render smoke particles
|
|
||||||
for (final particle in smokeParticles) {
|
|
||||||
particle.render(canvas);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isNearPosition(Vector2 targetPosition, {double threshold = 30.0}) {
|
|
||||||
return (position - targetPosition).length < threshold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SmokeParticle {
|
|
||||||
Vector2 position;
|
|
||||||
Vector2 velocity;
|
|
||||||
double life;
|
|
||||||
double maxLife;
|
|
||||||
bool shouldRemove = false;
|
|
||||||
|
|
||||||
SmokeParticle({required this.position})
|
|
||||||
: velocity = Vector2(
|
|
||||||
Random().nextDouble() * 20 - 10,
|
|
||||||
-Random().nextDouble() * 30 - 20,
|
|
||||||
),
|
|
||||||
life = 2.0,
|
|
||||||
maxLife = 2.0;
|
|
||||||
|
|
||||||
void update(double dt) {
|
|
||||||
position += velocity * dt;
|
|
||||||
velocity *= 0.98; // Slight air resistance
|
|
||||||
life -= dt;
|
|
||||||
|
|
||||||
if (life <= 0) {
|
|
||||||
shouldRemove = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void render(Canvas canvas) {
|
|
||||||
final alpha = (life / maxLife).clamp(0.0, 1.0);
|
|
||||||
final smokePaint = Paint()
|
|
||||||
..color = Color(0xFF666666).withOpacity(alpha * 0.3);
|
|
||||||
|
|
||||||
canvas.drawCircle(
|
|
||||||
Offset(position.x, position.y),
|
|
||||||
6.0 * (1.0 - life / maxLife),
|
|
||||||
smokePaint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:shitman/game/components/neighborhood.dart';
|
|
||||||
|
|
||||||
class TargetHouse extends Component {
|
|
||||||
House? currentTarget;
|
|
||||||
bool missionActive = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
selectNewTarget();
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectNewTarget() {
|
|
||||||
// Find the neighborhood component
|
|
||||||
final neighborhood = parent?.children.whereType<Neighborhood>().firstOrNull;
|
|
||||||
if (neighborhood == null) return;
|
|
||||||
|
|
||||||
// Clear previous target
|
|
||||||
if (currentTarget != null) {
|
|
||||||
currentTarget!.isTarget = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select random house as new target
|
|
||||||
currentTarget = neighborhood.getRandomHouse();
|
|
||||||
if (currentTarget != null) {
|
|
||||||
currentTarget!.isTarget = true;
|
|
||||||
missionActive = true;
|
|
||||||
debugPrint('New target selected at ${currentTarget!.position}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void completeMission() {
|
|
||||||
if (currentTarget != null) {
|
|
||||||
currentTarget!.isTarget = false;
|
|
||||||
currentTarget = null;
|
|
||||||
}
|
|
||||||
missionActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isPlayerNearTarget(Vector2 playerPosition, {double threshold = 50.0}) {
|
|
||||||
if (currentTarget?.doorPosition == null) return false;
|
|
||||||
|
|
||||||
final distance = (playerPosition - currentTarget!.doorPosition!).length;
|
|
||||||
return distance < threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector2? getTargetPosition() {
|
|
||||||
return currentTarget?.doorPosition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
import 'package:flame/game.dart';
|
|
||||||
import 'package:flame/components.dart';
|
|
||||||
import 'package:flame/events.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:shitman/game/components/player.dart';
|
|
||||||
import 'package:shitman/game/components/neighborhood.dart';
|
|
||||||
import 'package:shitman/game/components/target_house.dart';
|
|
||||||
import 'package:shitman/settings/app_settings.dart';
|
|
||||||
|
|
||||||
/// Shitman Game
|
|
||||||
/// A 2D top-down "hitman" style game, but instead of assassinating people,
|
|
||||||
/// your objective is to place flaming bags of dog poop on the doorsteps of
|
|
||||||
/// your targets without getting caught.
|
|
||||||
|
|
||||||
enum GameState { mainMenu, playing, paused, gameOver, missionComplete }
|
|
||||||
|
|
||||||
class ShitmanGame extends FlameGame with HasKeyboardHandlerComponents, HasCollisionDetection, AppSettings {
|
|
||||||
late Player player;
|
|
||||||
late Neighborhood neighborhood;
|
|
||||||
late TargetHouse targetHouse;
|
|
||||||
late CameraComponent gameCamera;
|
|
||||||
|
|
||||||
GameState gameState = GameState.mainMenu;
|
|
||||||
@override
|
|
||||||
bool debugMode = false;
|
|
||||||
int missionScore = 0;
|
|
||||||
int totalMissions = 0;
|
|
||||||
bool infiniteMode = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> onLoad() async {
|
|
||||||
await super.onLoad();
|
|
||||||
await initSettings();
|
|
||||||
|
|
||||||
// Setup camera
|
|
||||||
gameCamera = CameraComponent.withFixedResolution(
|
|
||||||
world: world,
|
|
||||||
width: 800,
|
|
||||||
height: 600,
|
|
||||||
);
|
|
||||||
addAll([gameCamera, world]);
|
|
||||||
|
|
||||||
// Initialize debug mode from settings
|
|
||||||
debugMode = appSettings.getBool('game.debug_mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
void startGame() {
|
|
||||||
gameState = GameState.playing;
|
|
||||||
initializeLevel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void startInfiniteMode() {
|
|
||||||
infiniteMode = true;
|
|
||||||
overlays.remove('MainMenu');
|
|
||||||
overlays.add('InGameUI');
|
|
||||||
startGame();
|
|
||||||
}
|
|
||||||
|
|
||||||
void stopGame() {
|
|
||||||
gameState = GameState.mainMenu;
|
|
||||||
world.removeAll(world.children);
|
|
||||||
missionScore = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void pauseGame() {
|
|
||||||
gameState = GameState.paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
void resumeGame() {
|
|
||||||
gameState = GameState.playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
void initializeLevel() {
|
|
||||||
// Clear previous level
|
|
||||||
world.removeAll(world.children);
|
|
||||||
|
|
||||||
// Create neighborhood
|
|
||||||
neighborhood = Neighborhood();
|
|
||||||
world.add(neighborhood);
|
|
||||||
|
|
||||||
// Create target house
|
|
||||||
targetHouse = TargetHouse();
|
|
||||||
world.add(targetHouse);
|
|
||||||
|
|
||||||
// Create player
|
|
||||||
player = Player();
|
|
||||||
world.add(player);
|
|
||||||
|
|
||||||
// Setup camera to follow player
|
|
||||||
gameCamera.follow(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
void completeCurrentMission() {
|
|
||||||
gameState = GameState.missionComplete;
|
|
||||||
missionScore += 100;
|
|
||||||
totalMissions++;
|
|
||||||
|
|
||||||
if (infiniteMode) {
|
|
||||||
// Generate new mission after delay
|
|
||||||
Future.delayed(Duration(seconds: 2), () {
|
|
||||||
initializeLevel();
|
|
||||||
gameState = GameState.playing;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void failMission() {
|
|
||||||
gameState = GameState.gameOver;
|
|
||||||
// TODO: Show game over screen
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
KeyEventResult onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
|
|
||||||
if (gameState != GameState.playing) return KeyEventResult.ignored;
|
|
||||||
|
|
||||||
// Handle pause
|
|
||||||
if (keysPressed.contains(LogicalKeyboardKey.escape)) {
|
|
||||||
pauseGame();
|
|
||||||
overlays.add('PauseMenu');
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle player input
|
|
||||||
player.handleInput(keysPressed);
|
|
||||||
|
|
||||||
// Handle action keys on key down
|
|
||||||
if (event is KeyDownEvent) {
|
|
||||||
player.handleAction(event.logicalKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void update(double dt) {
|
|
||||||
super.update(dt);
|
|
||||||
|
|
||||||
// Only update game logic when playing
|
|
||||||
if (gameState != GameState.playing) return;
|
|
||||||
|
|
||||||
// Game-specific update logic here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
137
lib/main.dart
137
lib/main.dart
|
|
@ -1,23 +1,7 @@
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flame/game.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:nes_ui/nes_ui.dart';
|
import 'package:nes_ui/nes_ui.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:shitman/game/shitman_game.dart';
|
|
||||||
import 'package:shitman/ui/in_game_ui.dart';
|
|
||||||
|
|
||||||
class AnyInputScrollBehavior extends MaterialScrollBehavior {
|
|
||||||
// Override behavior methods and getters like dragDevices
|
|
||||||
@override
|
|
||||||
Set<PointerDeviceKind> get dragDevices => {
|
|
||||||
PointerDeviceKind.touch,
|
|
||||||
PointerDeviceKind.mouse,
|
|
||||||
PointerDeviceKind.trackpad,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
LicenseRegistry.addLicense(() async* {
|
LicenseRegistry.addLicense(() async* {
|
||||||
|
|
@ -25,46 +9,105 @@ void main() {
|
||||||
yield LicenseEntryWithLineBreaks(['google_fonts'], license);
|
yield LicenseEntryWithLineBreaks(['google_fonts'], license);
|
||||||
});
|
});
|
||||||
|
|
||||||
runApp(
|
runApp(const MyApp());
|
||||||
EasyLocalization(
|
|
||||||
supportedLocales: [Locale('en'), Locale('da'), Locale('de')],
|
|
||||||
path: 'assets/translations',
|
|
||||||
fallbackLocale: Locale('en'),
|
|
||||||
startLocale: Locale('en'),
|
|
||||||
useOnlyLangCode: true,
|
|
||||||
useFallbackTranslations: true,
|
|
||||||
child: Shitman(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Shitman extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const Shitman({super.key});
|
const MyApp({super.key});
|
||||||
|
|
||||||
// This widget is the root of your application.
|
// This widget is the root of your application.
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
localizationsDelegates: context.localizationDelegates,
|
|
||||||
supportedLocales: context.supportedLocales,
|
|
||||||
scrollBehavior: AnyInputScrollBehavior(),
|
|
||||||
locale: context.locale,
|
|
||||||
theme: flutterNesTheme(brightness: Brightness.dark),
|
theme: flutterNesTheme(brightness: Brightness.dark),
|
||||||
themeMode: ThemeMode.dark,
|
themeMode: ThemeMode.dark,
|
||||||
home: GameWidget(
|
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||||
game: ShitmanGame(),
|
);
|
||||||
initialActiveOverlays: const ['MainMenu'],
|
}
|
||||||
overlayBuilderMap: {
|
}
|
||||||
InGameUI.overlayID: (context, game) =>
|
|
||||||
InGameUI(game as ShitmanGame),
|
class MyHomePage extends StatefulWidget {
|
||||||
MainMenuUI.overlayID: (context, game) =>
|
const MyHomePage({super.key, required this.title});
|
||||||
MainMenuUI(game as ShitmanGame),
|
|
||||||
SettingsUI.overlayID: (context, game) =>
|
// This widget is the home page of your application. It is stateful, meaning
|
||||||
SettingsUI(game as ShitmanGame),
|
// that it has a State object (defined below) that contains fields that affect
|
||||||
PauseMenuUI.overlayID: (context, game) =>
|
// how it looks.
|
||||||
PauseMenuUI(game as ShitmanGame),
|
|
||||||
},
|
// This class is the configuration for the state. It holds the values (in this
|
||||||
),
|
// case the title) provided by the parent (in this case the App widget) and
|
||||||
|
// used by the build method of the State. Fields in a Widget subclass are
|
||||||
|
// always marked "final".
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyHomePage> createState() => _MyHomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
|
int _counter = 0;
|
||||||
|
|
||||||
|
void _incrementCounter() {
|
||||||
|
setState(() {
|
||||||
|
// This call to setState tells the Flutter framework that something has
|
||||||
|
// changed in this State, which causes it to rerun the build method below
|
||||||
|
// so that the display can reflect the updated values. If we changed
|
||||||
|
// _counter without calling setState(), then the build method would not be
|
||||||
|
// called again, and so nothing would appear to happen.
|
||||||
|
_counter++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// This method is rerun every time setState is called, for instance as done
|
||||||
|
// by the _incrementCounter method above.
|
||||||
|
//
|
||||||
|
// The Flutter framework has been optimized to make rerunning build methods
|
||||||
|
// fast, so that you can just rebuild anything that needs updating rather
|
||||||
|
// than having to individually change instances of widgets.
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
// TRY THIS: Try changing the color here to a specific color (to
|
||||||
|
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||||
|
// change color while the other colors stay the same.
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||||
|
// Here we take the value from the MyHomePage object that was created by
|
||||||
|
// the App.build method, and use it to set our appbar title.
|
||||||
|
title: Text(widget.title),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
// Center is a layout widget. It takes a single child and positions it
|
||||||
|
// in the middle of the parent.
|
||||||
|
child: Column(
|
||||||
|
// Column is also a layout widget. It takes a list of children and
|
||||||
|
// arranges them vertically. By default, it sizes itself to fit its
|
||||||
|
// children horizontally, and tries to be as tall as its parent.
|
||||||
|
//
|
||||||
|
// Column has various properties to control how it sizes itself and
|
||||||
|
// how it positions its children. Here we use mainAxisAlignment to
|
||||||
|
// center the children vertically; the main axis here is the vertical
|
||||||
|
// axis because Columns are vertical (the cross axis would be
|
||||||
|
// horizontal).
|
||||||
|
//
|
||||||
|
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||||
|
// action in the IDE, or press "p" in the console), to see the
|
||||||
|
// wireframe for each widget.
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('You have pushed the button this many times:'),
|
||||||
|
Text(
|
||||||
|
'$_counter',
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: _incrementCounter,
|
||||||
|
tooltip: 'Increment',
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
), // This trailing comma makes auto-formatting nicer for build methods.
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import 'game.dart';
|
|
||||||
import 'colors.dart';
|
|
||||||
import 'settings_manager.dart';
|
|
||||||
|
|
||||||
mixin class AppSettings {
|
|
||||||
final Settings appSettings = Settings();
|
|
||||||
static bool _isInitialized = false;
|
|
||||||
|
|
||||||
Future<void> initSettings() async {
|
|
||||||
if (_isInitialized) return;
|
|
||||||
_isInitialized = true;
|
|
||||||
appSettings.register(gameSettings);
|
|
||||||
appSettings.register(colorSettings);
|
|
||||||
await appSettings.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import 'setting.dart';
|
|
||||||
import 'settings_group.dart';
|
|
||||||
|
|
||||||
/// Color settings, themes, etc.
|
|
||||||
final colorSettings = SettingsGroup(
|
|
||||||
key: 'colors',
|
|
||||||
items: [
|
|
||||||
/// Base color for game UI
|
|
||||||
IntSetting(
|
|
||||||
key: 'team_a_color',
|
|
||||||
defaultValue: 0xFF0000FF, // Blue
|
|
||||||
userConfigurable: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
/// Exception thrown when a requested setting is not found.
|
|
||||||
///
|
|
||||||
/// This occurs when:
|
|
||||||
/// - Accessing a setting that doesn't exist in the group
|
|
||||||
/// - Using an invalid storage key format
|
|
||||||
/// - Referencing a setting before it's been registered
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// try {
|
|
||||||
/// Settings.getBool('nonexistent.setting');
|
|
||||||
/// } catch (e) {
|
|
||||||
/// if (e is SettingNotFoundException) {
|
|
||||||
/// print('Setting not found: ${e.message}');
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
class SettingNotFoundException implements Exception {
|
|
||||||
/// Descriptive error message explaining what setting was not found.
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
/// Creates a new [SettingNotFoundException] with the given [message].
|
|
||||||
const SettingNotFoundException(this.message);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'SettingNotFoundException: $message';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exception thrown when attempting to modify a non-configurable setting.
|
|
||||||
///
|
|
||||||
/// Settings can be marked as non-configurable by setting `userConfigurable: false`.
|
|
||||||
/// This is useful for system settings or read-only configuration values.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// final systemSetting = BoolSetting(
|
|
||||||
/// key: 'systemFlag',
|
|
||||||
/// defaultValue: true,
|
|
||||||
/// userConfigurable: false, // This setting cannot be modified by users
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
class SettingNotConfigurableException implements Exception {
|
|
||||||
/// Descriptive error message explaining which setting cannot be configured.
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
/// Creates a new [SettingNotConfigurableException] with the given [message].
|
|
||||||
const SettingNotConfigurableException(this.message);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'SettingNotConfigurableException: $message';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exception thrown when a setting value fails validation.
|
|
||||||
///
|
|
||||||
/// This occurs when a validator function returns false for a given value.
|
|
||||||
/// Validators are useful for ensuring data integrity and business rules.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// final volumeSetting = DoubleSetting(
|
|
||||||
/// key: 'volume',
|
|
||||||
/// defaultValue: 0.5,
|
|
||||||
/// validator: (value) => value >= 0.0 && value <= 1.0,
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// // This will throw SettingValidationException
|
|
||||||
/// await Settings.setDouble('audio.volume', 1.5);
|
|
||||||
/// ```
|
|
||||||
class SettingValidationException implements Exception {
|
|
||||||
/// Descriptive error message explaining the validation failure.
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
/// Creates a new [SettingValidationException] with the given [message].
|
|
||||||
const SettingValidationException(this.message);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'SettingValidationException: $message';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exception thrown when attempting to access settings before initialization.
|
|
||||||
///
|
|
||||||
/// The settings framework requires asynchronous initialization before use.
|
|
||||||
/// Always await `Settings.init()` or individual `readyFuture` properties
|
|
||||||
/// before accessing setting values.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// // Wrong - may throw SettingsNotReadyException
|
|
||||||
/// bool value = Settings.getBool('game.sound');
|
|
||||||
///
|
|
||||||
/// // Correct - wait for initialization
|
|
||||||
/// await Settings.init();
|
|
||||||
/// bool value = Settings.getBool('game.sound');
|
|
||||||
/// ```
|
|
||||||
class SettingsNotReadyException implements Exception {
|
|
||||||
/// Descriptive error message explaining the readiness issue.
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
/// Creates a new [SettingsNotReadyException] with the given [message].
|
|
||||||
const SettingsNotReadyException(this.message);
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'SettingsNotReadyException: $message';
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import 'setting.dart';
|
|
||||||
import 'settings_group.dart';
|
|
||||||
|
|
||||||
/// Game settings group containing all game-related preferences.
|
|
||||||
final gameSettings = SettingsGroup(
|
|
||||||
key: 'game',
|
|
||||||
items: [
|
|
||||||
/// Debug mode, additional elements to help with development
|
|
||||||
BoolSetting(key: 'debug_mode', defaultValue: false),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
@ -1,343 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:async';
|
|
||||||
import 'package:shitman/attributes/serializable.dart';
|
|
||||||
|
|
||||||
/// Enum for supported setting value types.
|
|
||||||
///
|
|
||||||
/// This enum is used internally to track the type of each setting
|
|
||||||
/// and ensure proper type casting during storage and retrieval operations.
|
|
||||||
enum SettingType {
|
|
||||||
/// Boolean true/false values
|
|
||||||
bool,
|
|
||||||
|
|
||||||
/// Integer numeric values
|
|
||||||
int,
|
|
||||||
|
|
||||||
/// Double-precision floating point values
|
|
||||||
double,
|
|
||||||
|
|
||||||
/// String text values
|
|
||||||
string,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Abstract base class for all setting types.
|
|
||||||
///
|
|
||||||
/// This class defines the common interface and functionality for all settings,
|
|
||||||
/// including type safety, validation, change notifications, and metadata.
|
|
||||||
///
|
|
||||||
/// Type parameter [T] ensures compile-time type safety for setting values.
|
|
||||||
///
|
|
||||||
/// Example usage:
|
|
||||||
/// ```dart
|
|
||||||
/// // Create a validated volume setting
|
|
||||||
/// final volumeSetting = DoubleSetting(
|
|
||||||
/// key: 'volume',
|
|
||||||
/// defaultValue: 0.5,
|
|
||||||
/// validator: (value) => value >= 0.0 && value <= 1.0,
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// // Listen for changes
|
|
||||||
/// volumeSetting.stream.listen((newValue) {
|
|
||||||
/// print('Volume changed to: $newValue');
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Concrete implementations:
|
|
||||||
/// - [BoolSetting] for boolean values
|
|
||||||
/// - [IntSetting] for integer values
|
|
||||||
/// - [DoubleSetting] for floating-point values
|
|
||||||
/// - [StringSetting] for text values
|
|
||||||
abstract class Setting<T> implements Serializable {
|
|
||||||
/// Internal stream controller for broadcasting value changes.
|
|
||||||
/// Uses broadcast to allow multiple listeners.
|
|
||||||
final StreamController<T> _controller = StreamController<T>.broadcast();
|
|
||||||
|
|
||||||
/// Unique identifier for this setting within its group.
|
|
||||||
///
|
|
||||||
/// Keys should be descriptive and follow camelCase convention.
|
|
||||||
/// Examples: 'soundEnabled', 'maxRetries', 'serverUrl'
|
|
||||||
final String key;
|
|
||||||
|
|
||||||
/// The data type of this setting's value.
|
|
||||||
///
|
|
||||||
/// Used internally for type checking and storage operations.
|
|
||||||
/// Automatically set by concrete implementations.
|
|
||||||
final SettingType type;
|
|
||||||
|
|
||||||
/// The default value used when the setting hasn't been explicitly set.
|
|
||||||
///
|
|
||||||
/// This value is used during initialization and reset operations.
|
|
||||||
/// Must match the generic type parameter [T].
|
|
||||||
final T defaultValue;
|
|
||||||
|
|
||||||
/// Whether this setting can be modified by user code.
|
|
||||||
///
|
|
||||||
/// When false, attempts to modify the setting will throw
|
|
||||||
/// [SettingNotConfigurableException]. Useful for system settings
|
|
||||||
/// or read-only configuration values.
|
|
||||||
///
|
|
||||||
/// Defaults to true.
|
|
||||||
final bool userConfigurable;
|
|
||||||
|
|
||||||
/// Optional function to validate setting values before storage.
|
|
||||||
///
|
|
||||||
/// The validator receives the new value and should return:
|
|
||||||
/// - `true` if the value is valid
|
|
||||||
/// - `false` if the value should be rejected
|
|
||||||
///
|
|
||||||
/// When validation fails, [SettingValidationException] is thrown.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// validator: (value) => value >= 0 && value <= 100
|
|
||||||
/// ```
|
|
||||||
final bool Function(T)? validator;
|
|
||||||
|
|
||||||
/// Stream that emits new values when the setting changes.
|
|
||||||
///
|
|
||||||
/// This stream uses broadcast semantics, allowing multiple listeners.
|
|
||||||
/// The stream emits the new value immediately after it's stored.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// setting.stream.listen((newValue) {
|
|
||||||
/// print('Setting changed to: $newValue');
|
|
||||||
/// updateUI(newValue);
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
Stream<T> get stream => _controller.stream;
|
|
||||||
|
|
||||||
/// Creates a new setting with the specified configuration.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
/// - [key]: Unique identifier within the settings group
|
|
||||||
/// - [type]: Data type of the setting value
|
|
||||||
/// - [defaultValue]: Initial/reset value for the setting
|
|
||||||
/// - [userConfigurable]: Whether the setting can be modified (default: true)
|
|
||||||
/// - [validator]: Optional validation function for new values
|
|
||||||
Setting({
|
|
||||||
required this.key,
|
|
||||||
required this.type,
|
|
||||||
required this.defaultValue,
|
|
||||||
this.userConfigurable = true,
|
|
||||||
this.validator,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Internal method to notify all stream listeners of a value change.
|
|
||||||
///
|
|
||||||
/// This method is called automatically by the settings framework
|
|
||||||
/// after a value has been successfully stored. Application code
|
|
||||||
/// should not call this method directly.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
/// - [value]: The new value that was stored
|
|
||||||
void notifyChange(T value) {
|
|
||||||
_controller.add(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Internal method to validate a value using the validator function.
|
|
||||||
///
|
|
||||||
/// Returns true if no validator is provided or if the validator
|
|
||||||
/// function returns true. Returns false if validation fails.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
/// - [value]: The value to validate
|
|
||||||
///
|
|
||||||
/// Returns: true if valid, false if invalid
|
|
||||||
bool validate(T value) {
|
|
||||||
return validator?.call(value) ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dispose of the stream controller and release resources.
|
|
||||||
///
|
|
||||||
/// This method should be called when the setting is no longer needed
|
|
||||||
/// to prevent memory leaks. It's automatically called by the settings
|
|
||||||
/// framework when disposing of setting groups.
|
|
||||||
///
|
|
||||||
/// After calling dispose, the [stream] will no longer emit events.
|
|
||||||
void dispose() {
|
|
||||||
_controller.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts the setting to a map.
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'key': key,
|
|
||||||
'type': type.name,
|
|
||||||
'defaultValue': defaultValue,
|
|
||||||
'userConfigurable': userConfigurable,
|
|
||||||
// Todo: convert validator to use validation classes (e.g. RangeValidator)
|
|
||||||
'validator': null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a setting from a map representation.
|
|
||||||
Setting.fromMap(Map<String, dynamic> map)
|
|
||||||
: key = map['key'] as String,
|
|
||||||
type = SettingType.values.firstWhere(
|
|
||||||
(e) => e.name == map['type'],
|
|
||||||
orElse:
|
|
||||||
() => throw ArgumentError('Invalid setting type: ${map['type']}'),
|
|
||||||
),
|
|
||||||
defaultValue = map['defaultValue'] as T,
|
|
||||||
userConfigurable = map['userConfigurable'] as bool? ?? true,
|
|
||||||
validator = null;
|
|
||||||
|
|
||||||
/// Converts the setting to a JSON string representation.
|
|
||||||
@override
|
|
||||||
String toJson() {
|
|
||||||
return jsonEncode(toMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a setting from a JSON string representation.
|
|
||||||
Setting.fromJson(String json)
|
|
||||||
: this.fromMap(jsonDecode(json) as Map<String, dynamic>);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A setting that stores boolean (true/false) values.
|
|
||||||
///
|
|
||||||
/// This is a concrete implementation of [Setting] specialized for boolean values.
|
|
||||||
/// Commonly used for feature flags, toggles, and binary preferences.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// final soundEnabled = BoolSetting(
|
|
||||||
/// key: 'soundEnabled',
|
|
||||||
/// defaultValue: true,
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// final debugMode = BoolSetting(
|
|
||||||
/// key: 'debugMode',
|
|
||||||
/// defaultValue: false,
|
|
||||||
/// userConfigurable: false, // System setting
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
class BoolSetting extends Setting<bool> {
|
|
||||||
/// Creates a new boolean setting.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
/// - [key]: Unique identifier for this setting
|
|
||||||
/// - [defaultValue]: Initial boolean value (true or false)
|
|
||||||
/// - [userConfigurable]: Whether users can modify this setting (default: true)
|
|
||||||
/// - [validator]: Optional validation function for boolean values
|
|
||||||
BoolSetting({
|
|
||||||
required super.key,
|
|
||||||
required super.defaultValue,
|
|
||||||
super.userConfigurable,
|
|
||||||
super.validator,
|
|
||||||
}) : super(type: SettingType.bool);
|
|
||||||
|
|
||||||
/// Converts the boolean value to a JSON string representation.
|
|
||||||
@override
|
|
||||||
String toJson() {
|
|
||||||
return defaultValue.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a boolean setting from a JSON string representation.
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A setting that stores integer numeric values.
|
|
||||||
///
|
|
||||||
/// This is a concrete implementation of [Setting] specialized for integer values.
|
|
||||||
/// Useful for counts, limits, indices, and whole number preferences.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// final maxRetries = IntSetting(
|
|
||||||
/// key: 'maxRetries',
|
|
||||||
/// defaultValue: 3,
|
|
||||||
/// validator: (value) => value >= 0 && value <= 10,
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// final fontSize = IntSetting(
|
|
||||||
/// key: 'fontSize',
|
|
||||||
/// defaultValue: 14,
|
|
||||||
/// validator: (value) => value >= 8 && value <= 72,
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
class IntSetting extends Setting<int> {
|
|
||||||
/// Creates a new integer setting.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
/// - [key]: Unique identifier for this setting
|
|
||||||
/// - [defaultValue]: Initial integer value
|
|
||||||
/// - [userConfigurable]: Whether users can modify this setting (default: true)
|
|
||||||
/// - [validator]: Optional validation function (e.g., range checking)
|
|
||||||
IntSetting({
|
|
||||||
required super.key,
|
|
||||||
required super.defaultValue,
|
|
||||||
super.userConfigurable,
|
|
||||||
super.validator,
|
|
||||||
}) : super(type: SettingType.int);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A setting that stores double-precision floating-point values.
|
|
||||||
///
|
|
||||||
/// This is a concrete implementation of [Setting] specialized for decimal values.
|
|
||||||
/// Perfect for percentages, ratios, measurements, and precise numeric settings.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// final volume = DoubleSetting(
|
|
||||||
/// key: 'volume',
|
|
||||||
/// defaultValue: 0.8,
|
|
||||||
/// validator: (value) => value >= 0.0 && value <= 1.0,
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// final animationSpeed = DoubleSetting(
|
|
||||||
/// key: 'animationSpeed',
|
|
||||||
/// defaultValue: 1.0,
|
|
||||||
/// validator: (value) => value > 0.0 && value <= 5.0,
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
class DoubleSetting extends Setting<double> {
|
|
||||||
/// Creates a new double setting.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
/// - [key]: Unique identifier for this setting
|
|
||||||
/// - [defaultValue]: Initial floating-point value
|
|
||||||
/// - [userConfigurable]: Whether users can modify this setting (default: true)
|
|
||||||
/// - [validator]: Optional validation function (e.g., range checking)
|
|
||||||
DoubleSetting({
|
|
||||||
required super.key,
|
|
||||||
required super.defaultValue,
|
|
||||||
super.userConfigurable,
|
|
||||||
super.validator,
|
|
||||||
}) : super(type: SettingType.double);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A setting that stores string text values.
|
|
||||||
///
|
|
||||||
/// This is a concrete implementation of [Setting] specialized for text values.
|
|
||||||
/// Ideal for names, URLs, file paths, themes, and textual preferences.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// final theme = StringSetting(
|
|
||||||
/// key: 'theme',
|
|
||||||
/// defaultValue: 'light',
|
|
||||||
/// validator: (value) => ['light', 'dark', 'auto'].contains(value),
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// final serverUrl = StringSetting(
|
|
||||||
/// key: 'serverUrl',
|
|
||||||
/// defaultValue: 'https://api.example.com',
|
|
||||||
/// validator: (value) => Uri.tryParse(value) != null,
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
class StringSetting extends Setting<String> {
|
|
||||||
/// Creates a new string setting.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
/// - [key]: Unique identifier for this setting
|
|
||||||
/// - [defaultValue]: Initial text value
|
|
||||||
/// - [userConfigurable]: Whether users can modify this setting (default: true)
|
|
||||||
/// - [validator]: Optional validation function (e.g., format checking)
|
|
||||||
StringSetting({
|
|
||||||
required super.key,
|
|
||||||
required super.defaultValue,
|
|
||||||
super.userConfigurable,
|
|
||||||
super.validator,
|
|
||||||
}) : super(type: SettingType.string);
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
/// =============================================================================
|
|
||||||
/// MODULAR SETTINGS FRAMEWORK
|
|
||||||
/// =============================================================================
|
|
||||||
///
|
|
||||||
/// A comprehensive, type-safe settings management framework for Flutter/Dart
|
|
||||||
/// applications.
|
|
||||||
///
|
|
||||||
/// Usage Example:
|
|
||||||
/// ```dart
|
|
||||||
/// // Define your settings
|
|
||||||
/// final gameSettings = SettingsBase(
|
|
||||||
/// key: 'game',
|
|
||||||
/// items: SettingsGroup(items: [
|
|
||||||
/// BoolSetting(key: 'soundEnabled', defaultValue: true),
|
|
||||||
/// DoubleSetting(
|
|
||||||
/// key: 'volume',
|
|
||||||
/// defaultValue: 0.8,
|
|
||||||
/// validator: (v) => v >= 0.0 && v <= 1.0,
|
|
||||||
/// ),
|
|
||||||
/// ]),
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// // Register and initialize
|
|
||||||
/// Settings.register(gameSettings);
|
|
||||||
/// await Settings.init();
|
|
||||||
///
|
|
||||||
/// // Use settings
|
|
||||||
/// bool sound = Settings.getBool('game.soundEnabled');
|
|
||||||
/// await Settings.setBool('game.soundEnabled', false);
|
|
||||||
///
|
|
||||||
/// // Listen for changes
|
|
||||||
/// gameSettings.items['soundEnabled']!.stream.listen((value) {
|
|
||||||
/// print('Sound enabled changed to: $value');
|
|
||||||
/// });
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// =============================================================================
|
|
||||||
library;
|
|
||||||
|
|
||||||
export 'settings_manager.dart';
|
|
||||||
export 'exceptions.dart';
|
|
||||||
export 'setting.dart';
|
|
||||||
export 'settings_group.dart';
|
|
||||||
export 'settings_store.dart';
|
|
||||||
export 'game.dart';
|
|
||||||
|
|
@ -1,468 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'exceptions.dart';
|
|
||||||
import 'settings_store.dart';
|
|
||||||
import 'setting.dart';
|
|
||||||
|
|
||||||
/// A comprehensive settings group that manages related settings with persistence,
|
|
||||||
/// initialization, and type-safe access.
|
|
||||||
///
|
|
||||||
/// [SettingsGroup] extends [UnmodifiableMapBase] to provide convenient
|
|
||||||
/// map-like access to settings while managing their persistence and validation.
|
|
||||||
/// Each group has a unique key namespace and handles its own initialization.
|
|
||||||
///
|
|
||||||
/// Usage pattern:
|
|
||||||
/// ```dart
|
|
||||||
/// // 1. Define your settings group
|
|
||||||
/// final gameSettings = SettingsGroup(
|
|
||||||
/// key: 'game',
|
|
||||||
/// items: [
|
|
||||||
/// BoolSetting(key: 'soundEnabled', defaultValue: true),
|
|
||||||
/// DoubleSetting(key: 'volume', defaultValue: 0.8),
|
|
||||||
/// ],
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// // 2. Register with the global settings manager
|
|
||||||
/// Settings.register(gameSettings);
|
|
||||||
///
|
|
||||||
/// // 3. Wait for initialization
|
|
||||||
/// await gameSettings.readyFuture;
|
|
||||||
///
|
|
||||||
/// // 4. Use the settings
|
|
||||||
/// bool soundEnabled = gameSettings.get<bool>('soundEnabled');
|
|
||||||
/// await gameSettings.setValue('volume', 0.5);
|
|
||||||
/// ```
|
|
||||||
class SettingsGroup extends UnmodifiableMapBase<String, Setting> {
|
|
||||||
/// Reference to the singleton settings store for persistence.
|
|
||||||
///
|
|
||||||
/// This store handles the actual reading and writing of values
|
|
||||||
/// to SharedPreferences with caching for performance.
|
|
||||||
late final SettingsStore _store;
|
|
||||||
|
|
||||||
/// Unique identifier for this settings group.
|
|
||||||
///
|
|
||||||
/// This key is used as a namespace prefix for all settings in this group.
|
|
||||||
/// For example, if key is 'game' and a setting key is 'volume',
|
|
||||||
/// the stored key becomes 'game.volume'.
|
|
||||||
///
|
|
||||||
/// Should be descriptive and unique across your application.
|
|
||||||
final String key;
|
|
||||||
|
|
||||||
/// Immutable set of all settings contained in this group.
|
|
||||||
///
|
|
||||||
/// This set is created during construction and cannot be modified afterward.
|
|
||||||
/// It contains all the setting objects that belong to this group.
|
|
||||||
late final Set<Setting<dynamic>> items;
|
|
||||||
|
|
||||||
/// Internal cache of setting keys for efficient lookups.
|
|
||||||
///
|
|
||||||
/// This set contains the string keys of all settings in the group,
|
|
||||||
/// providing O(1) key existence checks and fast iteration.
|
|
||||||
late final Set<String> _keys;
|
|
||||||
|
|
||||||
/// Internal flag tracking initialization status.
|
|
||||||
bool _ready = false;
|
|
||||||
|
|
||||||
/// Public property indicating whether this settings group is ready for use.
|
|
||||||
///
|
|
||||||
/// When false, accessing setting values will throw [SettingsNotReadyException].
|
|
||||||
/// When true, all settings have been loaded and are available synchronously.
|
|
||||||
bool get ready => _ready;
|
|
||||||
|
|
||||||
/// Internal completer that completes when initialization finishes.
|
|
||||||
late Completer<bool> _readyCompleter;
|
|
||||||
|
|
||||||
/// Future that completes when all settings in this group are initialized.
|
|
||||||
///
|
|
||||||
/// Await this future before accessing setting values to ensure they've
|
|
||||||
/// been loaded from storage. The future completes with true on success
|
|
||||||
/// or throws an exception if initialization fails.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// await gameSettings.readyFuture;
|
|
||||||
/// // Now safe to access settings synchronously
|
|
||||||
/// bool soundEnabled = gameSettings.get<bool>('soundEnabled');
|
|
||||||
/// ```
|
|
||||||
Future<bool> get readyFuture => _readyCompleter.future;
|
|
||||||
|
|
||||||
/// Creates a new settings group with the given key and settings.
|
|
||||||
///
|
|
||||||
/// The provided [items] are converted to an immutable set, and their
|
|
||||||
/// keys are extracted for efficient access. Duplicate keys within
|
|
||||||
/// the same group are not allowed and will cause undefined behavior.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
/// - [key]: Unique identifier for this settings group
|
|
||||||
/// - [items]: Collection of settings to include in this group
|
|
||||||
/// - [forceRegularSharedPreferences]: Whether to force regular SharedPreferences (for testing)
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// final group = SettingsGroup(
|
|
||||||
/// key: 'game',
|
|
||||||
/// items: [
|
|
||||||
/// BoolSetting(key: 'notifications', defaultValue: true),
|
|
||||||
/// IntSetting(key: 'timeout', defaultValue: 30),
|
|
||||||
/// ],
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
SettingsGroup({
|
|
||||||
required this.key,
|
|
||||||
required Iterable<Setting> items,
|
|
||||||
bool forceRegularSharedPreferences = false,
|
|
||||||
}) {
|
|
||||||
this.items = Set<Setting>.from(items);
|
|
||||||
_keys = items.map((item) => item.key).toSet();
|
|
||||||
_store = SettingsStore(
|
|
||||||
forceRegularSharedPreferences: forceRegularSharedPreferences,
|
|
||||||
);
|
|
||||||
_readyCompleter = Completer<bool>();
|
|
||||||
// Initialize the settings in the storage if they haven't been set yet.
|
|
||||||
_init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new settings group optimized for testing.
|
|
||||||
/// This constructor forces the use of regular SharedPreferences instead
|
|
||||||
/// of SharedPreferencesWithCache to avoid test compatibility issues.
|
|
||||||
SettingsGroup.forTesting({
|
|
||||||
required this.key,
|
|
||||||
required Iterable<Setting> items,
|
|
||||||
}) {
|
|
||||||
this.items = Set<Setting>.from(items);
|
|
||||||
_keys = items.map((item) => item.key).toSet();
|
|
||||||
_store = SettingsStore(forceRegularSharedPreferences: true);
|
|
||||||
_readyCompleter = Completer<bool>();
|
|
||||||
// Initialize the settings in the storage if they haven't been set yet.
|
|
||||||
_init();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Retrieves a setting by its key.
|
|
||||||
///
|
|
||||||
/// This operator provides map-like access to settings within the group.
|
|
||||||
/// The return type is [Setting<dynamic>] to accommodate different setting types.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
/// - [key]: The string key of the setting to retrieve
|
|
||||||
///
|
|
||||||
/// Returns: The setting object with the specified key or null if not found.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// Setting volumeSetting = audioGroup['volume'];
|
|
||||||
/// BoolSetting enabledSetting = audioGroup['enabled'] as BoolSetting;
|
|
||||||
/// ```
|
|
||||||
@override
|
|
||||||
Setting<dynamic>? operator [](Object? key) {
|
|
||||||
try {
|
|
||||||
return items.firstWhere((item) => item.key == key);
|
|
||||||
} catch (_) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an iterable of all setting keys in this group.
|
|
||||||
///
|
|
||||||
/// This property provides the keys needed for map-like iteration
|
|
||||||
/// and key existence checking.
|
|
||||||
///
|
|
||||||
/// Returns: Iterable containing all setting keys as strings
|
|
||||||
@override
|
|
||||||
Iterable<String> get keys => _keys;
|
|
||||||
|
|
||||||
/// Returns the number of settings in this group.
|
|
||||||
///
|
|
||||||
/// This count includes all settings regardless of their type
|
|
||||||
/// or configurability status.
|
|
||||||
///
|
|
||||||
/// Returns: Integer count of settings in the group
|
|
||||||
@override
|
|
||||||
int get length => _keys.length;
|
|
||||||
|
|
||||||
/// Initializes the settings by checking if they are set in the storage.
|
|
||||||
/// If not, it sets them with their default values.
|
|
||||||
/// This is called in the constructor to ensure settings are ready to use.
|
|
||||||
/// It waits for the store to be ready before proceeding, but there is no
|
|
||||||
/// guarantee that the settings are initialized before the first access.
|
|
||||||
/// If you need to ensure settings are initialized before use, you should
|
|
||||||
/// await the [readyFuture] before accessing any settings.
|
|
||||||
Future<void> _init() async {
|
|
||||||
try {
|
|
||||||
if (!_store.ready) {
|
|
||||||
await _store.readyFuture;
|
|
||||||
}
|
|
||||||
for (final Setting setting in items) {
|
|
||||||
final storageKey = _storageKey(setting.key);
|
|
||||||
if (!_store.prefs.containsKey(storageKey)) {
|
|
||||||
// If the setting is not set, initialize it with the default value.
|
|
||||||
await _set(storageKey, setting, null, force: true);
|
|
||||||
} else {
|
|
||||||
// Validate existing value and reset to default if invalid
|
|
||||||
try {
|
|
||||||
final currentValue = _get(setting);
|
|
||||||
if (setting.validator != null && !setting.validate(currentValue)) {
|
|
||||||
await _set(storageKey, setting, null, force: true);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// If there's an error reading the current value, reset to default
|
|
||||||
await _set(storageKey, setting, null, force: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ready = true;
|
|
||||||
_readyCompleter.complete(true);
|
|
||||||
} catch (error) {
|
|
||||||
_ready = false;
|
|
||||||
_readyCompleter.completeError(error);
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the value of a setting by its key.
|
|
||||||
Future<void> setValue<T>(String key, T value) async {
|
|
||||||
await _waitUntilReady();
|
|
||||||
final setting = this[key];
|
|
||||||
if (setting == null) {
|
|
||||||
throw SettingNotFoundException(
|
|
||||||
'No setting in ${this.key} found for key: $key',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final storageKey = _storageKey(setting.key);
|
|
||||||
if (!setting.userConfigurable) {
|
|
||||||
throw SettingNotConfigurableException(
|
|
||||||
'Setting $storageKey is not user configurable',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the value if a validator is provided
|
|
||||||
if (setting is Setting<T> && !setting.validate(value)) {
|
|
||||||
throw SettingValidationException(
|
|
||||||
'Invalid value for setting $storageKey: $value',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _set(storageKey, setting, value);
|
|
||||||
|
|
||||||
// Notify change listeners
|
|
||||||
if (setting is Setting<T>) {
|
|
||||||
setting.notifyChange(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience method to get a typed value of a setting by its key.
|
|
||||||
/// Throws an error if the setting is not found or if the type does not match.
|
|
||||||
T get<T>(String key) {
|
|
||||||
_readySync();
|
|
||||||
if (T == dynamic) {
|
|
||||||
return getValue(key);
|
|
||||||
}
|
|
||||||
final setting = this[key];
|
|
||||||
if (setting == null) {
|
|
||||||
throw SettingNotFoundException(
|
|
||||||
'No setting in ${this.key} found for key: $key',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (setting is! Setting<T>) {
|
|
||||||
throw ArgumentError(
|
|
||||||
'Setting $key is not of type ${T.runtimeType}, but ${setting.type}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _get<T>(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the value of a setting by its key.
|
|
||||||
dynamic getValue(String key) {
|
|
||||||
_readySync();
|
|
||||||
final setting = this[key];
|
|
||||||
if (setting == null) {
|
|
||||||
throw SettingNotFoundException(
|
|
||||||
'No setting in ${this.key} found for key: $key',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _get(setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensures that the settings are ready before accessing them.
|
|
||||||
/// Throws a [SettingsNotReadyException] if the settings are not ready.
|
|
||||||
void _readySync() {
|
|
||||||
if (!_ready) {
|
|
||||||
throw SettingsNotReadyException(
|
|
||||||
'Settings are not ready. Please await readyFuture.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Waits until the settings are ready.
|
|
||||||
/// This is useful for asynchronous operations that need to ensure
|
|
||||||
/// settings are initialized.
|
|
||||||
Future<void> _waitUntilReady() async {
|
|
||||||
if (!_ready) {
|
|
||||||
await _readyCompleter.future;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a storage key for the given key in this settings group.
|
|
||||||
/// This is used to namespace the settings keys to avoid conflicts.
|
|
||||||
/// For example, if the group key is "game" and the setting key is
|
|
||||||
/// "fullscreen", the storage key will be "game.fullscreen".
|
|
||||||
String _storageKey(String key) {
|
|
||||||
return "${this.key}.$key";
|
|
||||||
}
|
|
||||||
|
|
||||||
T _validateOrDefault<T>(Setting<T> setting, T? value) {
|
|
||||||
if (value == null) return setting.defaultValue;
|
|
||||||
if (setting.validator != null && !setting.validate(value)) {
|
|
||||||
// return default value if validation fails
|
|
||||||
return setting.defaultValue;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the value of a setting by its key and type.
|
|
||||||
/// Throws an error if the setting is not found or if the type does not match.
|
|
||||||
/// This method is used internally to retrieve the value of a setting.
|
|
||||||
T _get<T>(Setting<T> setting) {
|
|
||||||
final storageKey = _storageKey(setting.key);
|
|
||||||
if (!_store.prefs.containsKey(storageKey)) {
|
|
||||||
// If not found in storage, return default value
|
|
||||||
return setting.defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (T) {
|
|
||||||
case const (bool):
|
|
||||||
final value = _store.prefs.getBool(storageKey);
|
|
||||||
// validate the value if a validator is provided
|
|
||||||
return _validateOrDefault(setting, value as T);
|
|
||||||
case const (int):
|
|
||||||
final value = _store.prefs.getInt(storageKey);
|
|
||||||
return _validateOrDefault(setting, value as T);
|
|
||||||
case const (double):
|
|
||||||
final value = _store.prefs.getDouble(storageKey);
|
|
||||||
return _validateOrDefault(setting, value as T);
|
|
||||||
case const (String):
|
|
||||||
final value = _store.prefs.getString(storageKey);
|
|
||||||
return _validateOrDefault(setting, value as T);
|
|
||||||
default:
|
|
||||||
throw ArgumentError('Unsupported setting type: ${T.runtimeType}');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// If there's a type mismatch or other error, return default value
|
|
||||||
return setting.defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the value of a setting by its key and type.
|
|
||||||
/// Throws an error if the setting is not found or if the type does not match.
|
|
||||||
/// This method is used internally to set the value of a setting.
|
|
||||||
/// If [force] is true, it will set the value even if the setting is
|
|
||||||
/// not user configurable.
|
|
||||||
/// If [value] is null, it will use the default value of the setting.
|
|
||||||
/// If the setting is not user configurable and [force] is false,
|
|
||||||
/// it will throw an error.
|
|
||||||
Future<void> _set<T>(
|
|
||||||
String storageKey,
|
|
||||||
Setting<T> setting,
|
|
||||||
T? value, {
|
|
||||||
bool force = false,
|
|
||||||
}) async {
|
|
||||||
if (!force && !setting.userConfigurable) {
|
|
||||||
throw SettingNotConfigurableException(
|
|
||||||
'Setting $storageKey is not user configurable',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!force && !_store.prefs.containsKey(storageKey)) {
|
|
||||||
throw SettingNotFoundException('No setting found for: $storageKey');
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (T) {
|
|
||||||
case const (bool):
|
|
||||||
value ??= setting.defaultValue;
|
|
||||||
return _setBool(storageKey, value as bool);
|
|
||||||
case const (int):
|
|
||||||
value ??= setting.defaultValue;
|
|
||||||
return _setInt(storageKey, value as int);
|
|
||||||
case const (double):
|
|
||||||
value ??= setting.defaultValue;
|
|
||||||
return _setDouble(storageKey, value as double);
|
|
||||||
case const (String):
|
|
||||||
value ??= setting.defaultValue;
|
|
||||||
return _setString(storageKey, value as String);
|
|
||||||
case const (dynamic):
|
|
||||||
// If the type is dynamic, we can return any value.
|
|
||||||
// This is a fallback for when the type is not known at compile time.
|
|
||||||
// it is less efficient, but let's face it, you probably should not be
|
|
||||||
// updating settings 1000s of times per second.
|
|
||||||
value ??= setting.defaultValue;
|
|
||||||
|
|
||||||
switch (setting.type) {
|
|
||||||
case SettingType.bool:
|
|
||||||
return _setBool(storageKey, value as bool);
|
|
||||||
case SettingType.int:
|
|
||||||
return _setInt(storageKey, value as int);
|
|
||||||
case SettingType.double:
|
|
||||||
return _setDouble(storageKey, value as double);
|
|
||||||
case SettingType.string:
|
|
||||||
return _setString(storageKey, value as String);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
throw ArgumentError('Unsupported setting type: ${T.runtimeType}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a boolean value for the given storage key.
|
|
||||||
Future<void> _setBool(String storageKey, bool value) async {
|
|
||||||
await _store.prefs.setBool(storageKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets an integer value for the given storage key.
|
|
||||||
Future<void> _setInt(String storageKey, int value) async {
|
|
||||||
await _store.prefs.setInt(storageKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a double value for the given storage key.
|
|
||||||
Future<void> _setDouble(String storageKey, double value) async {
|
|
||||||
await _store.prefs.setDouble(storageKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a string value for the given storage key.
|
|
||||||
Future<void> _setString(String storageKey, String value) async {
|
|
||||||
await _store.prefs.setString(storageKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset a setting to its default value.
|
|
||||||
Future<void> reset(String key) async {
|
|
||||||
await _waitUntilReady();
|
|
||||||
final setting = this[key];
|
|
||||||
if (setting == null) {
|
|
||||||
throw SettingNotFoundException(
|
|
||||||
'No setting in ${this.key} found for key: $key',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final storageKey = _storageKey(setting.key);
|
|
||||||
await _set(storageKey, setting, null, force: true);
|
|
||||||
|
|
||||||
// Notify change listeners
|
|
||||||
setting.notifyChange(setting.defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset all settings in this group to their default values.
|
|
||||||
Future<void> resetAll() async {
|
|
||||||
await _waitUntilReady();
|
|
||||||
for (final setting in items) {
|
|
||||||
final storageKey = _storageKey(setting.key);
|
|
||||||
await _set(storageKey, setting, null, force: true);
|
|
||||||
setting.notifyChange(setting.defaultValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dispose all stream controllers for settings in this group.
|
|
||||||
void dispose() {
|
|
||||||
for (final setting in items) {
|
|
||||||
setting.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,350 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'settings_group.dart';
|
|
||||||
import 'exceptions.dart';
|
|
||||||
|
|
||||||
/// Global settings manager providing centralized access to all setting groups.
|
|
||||||
///
|
|
||||||
/// The [Settings] class serves as the main entry point for the settings framework,
|
|
||||||
/// offering static methods for registration, initialization, and access to settings
|
|
||||||
/// across your entire application. It manages multiple [SettingsGroup] instances and
|
|
||||||
/// provides both individual and batch operations.
|
|
||||||
///
|
|
||||||
/// ## Overview
|
|
||||||
///
|
|
||||||
/// The settings framework follows a hierarchical structure:
|
|
||||||
/// ```
|
|
||||||
/// Settings (Global Manager)
|
|
||||||
/// │
|
|
||||||
/// ├── SettingsGroup (Group: "game")
|
|
||||||
/// │ ├── BoolSetting ("soundEnabled")
|
|
||||||
/// │ └── DoubleSetting ("volume")
|
|
||||||
/// │
|
|
||||||
/// └── SettingsGroup (Group: "ui")
|
|
||||||
/// ├── StringSetting ("theme")
|
|
||||||
/// └── IntSetting ("fontSize")
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ## Usage Pattern
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// // 1. Define your setting groups
|
|
||||||
/// final gameSettings = SettingsGroup(
|
|
||||||
/// key: 'game',
|
|
||||||
/// items: [
|
|
||||||
/// BoolSetting(key: 'soundEnabled', defaultValue: true),
|
|
||||||
/// DoubleSetting(key: 'volume', defaultValue: 0.8),
|
|
||||||
/// ],
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// final uiSettings = SettingsGroup(
|
|
||||||
/// key: 'ui',
|
|
||||||
/// items: [
|
|
||||||
/// StringSetting(key: 'theme', defaultValue: 'light'),
|
|
||||||
/// IntSetting(key: 'fontSize', defaultValue: 14),
|
|
||||||
/// ],
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// // 2. Register all groups
|
|
||||||
/// Settings.register(gameSettings);
|
|
||||||
/// Settings.register(uiSettings);
|
|
||||||
///
|
|
||||||
/// // 3. Initialize the entire settings system
|
|
||||||
/// await Settings.init();
|
|
||||||
///
|
|
||||||
/// // 4. Access settings using dot notation
|
|
||||||
/// bool soundEnabled = Settings.getBool('game.soundEnabled');
|
|
||||||
/// String theme = Settings.getString('ui.theme');
|
|
||||||
///
|
|
||||||
/// // 5. Modify settings with automatic validation
|
|
||||||
/// await Settings.setBool('game.soundEnabled', false);
|
|
||||||
/// await Settings.setString('ui.theme', 'dark');
|
|
||||||
///
|
|
||||||
/// // 6. Batch operations for efficiency
|
|
||||||
/// await Settings.setMultiple({
|
|
||||||
/// 'game.volume': 0.5,
|
|
||||||
/// 'ui.fontSize': 16,
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// // 7. Reset operations
|
|
||||||
/// await Settings.resetSetting('game.volume'); // Reset single setting
|
|
||||||
/// await Settings.resetGroup('ui'); // Reset entire group
|
|
||||||
/// await Settings.resetAll(); // Reset everything
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ## Storage Key Format
|
|
||||||
///
|
|
||||||
/// Settings are stored using a namespaced key format: `groupKey.settingKey`
|
|
||||||
/// - `game.soundEnabled` → boolean setting in the game group
|
|
||||||
/// - `ui.theme` → string setting in the ui group
|
|
||||||
/// - `network.timeout` → integer setting in the network group
|
|
||||||
///
|
|
||||||
/// This prevents key conflicts between different setting groups and provides
|
|
||||||
/// logical organization of related settings.
|
|
||||||
class Settings {
|
|
||||||
/// Internal registry of all settings groups keyed by their group names.
|
|
||||||
///
|
|
||||||
/// This map stores all registered [SettingsGroup] instances, providing
|
|
||||||
/// fast lookup by group key. Groups must be registered before use.
|
|
||||||
static final Map<String, SettingsGroup> _settings = {};
|
|
||||||
|
|
||||||
/// Initializes all registered settings groups concurrently.
|
|
||||||
///
|
|
||||||
/// This method waits for all registered settings groups to complete their
|
|
||||||
/// asynchronous initialization. It's essential to call this method before
|
|
||||||
/// accessing any setting values to ensure they've been loaded from storage.
|
|
||||||
///
|
|
||||||
/// The initialization process:
|
|
||||||
/// 1. Waits for the underlying SharedPreferences to be ready
|
|
||||||
/// 2. Loads existing values from storage for each setting
|
|
||||||
/// 3. Creates default values for settings that don't exist yet
|
|
||||||
/// 4. Marks all groups as ready for synchronous access
|
|
||||||
///
|
|
||||||
/// Returns: Future that completes when all settings are initialized
|
|
||||||
///
|
|
||||||
/// Throws: Exception if any settings group fails to initialize
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// // Register your settings groups first
|
|
||||||
/// Settings.register(gameSettings);
|
|
||||||
/// Settings.register(uiSettings);
|
|
||||||
///
|
|
||||||
/// // Then initialize everything
|
|
||||||
/// await Settings.init();
|
|
||||||
///
|
|
||||||
/// // Now safe to use settings synchronously
|
|
||||||
/// bool soundEnabled = Settings.getBool('game.soundEnabled');
|
|
||||||
/// ```
|
|
||||||
Future<void> init() async {
|
|
||||||
final futures = _settings.values.map((settings) => settings.readyFuture);
|
|
||||||
await Future.wait(futures);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a map of all registered settings groups.
|
|
||||||
Map<String, SettingsGroup> get groups => _settings;
|
|
||||||
|
|
||||||
/// Returns a list of all registered settings groups keys.
|
|
||||||
List<String> get groupKeys => _settings.keys.toList();
|
|
||||||
|
|
||||||
/// Allow access to settings by key using dynamic getters.
|
|
||||||
/// This allows you to access settings like:
|
|
||||||
/// Settings.game.fullscreen, Settings.game.soundVolume, etc.
|
|
||||||
@override
|
|
||||||
SettingsGroup noSuchMethod(Invocation invocation) {
|
|
||||||
if (invocation.isGetter) {
|
|
||||||
final key = invocation.memberName.toString();
|
|
||||||
if (_settings.containsKey(key)) {
|
|
||||||
return _settings[key]!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw NoSuchMethodError.withInvocation(this, invocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate and get the parts of a storage key.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid.
|
|
||||||
static ({String group, String setting}) _parseStorageKey(String storageKey) {
|
|
||||||
final parts = storageKey.split('.');
|
|
||||||
if (parts.length < 2) {
|
|
||||||
throw ArgumentError('Invalid storage key: $storageKey');
|
|
||||||
}
|
|
||||||
return (group: parts.first, setting: parts.sublist(1).join('.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Getters =====
|
|
||||||
|
|
||||||
/// Override the accessor to allow dynamic access to settings
|
|
||||||
/// using the `[]` operator.
|
|
||||||
dynamic operator [](String key) {
|
|
||||||
return get<dynamic>(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Registers a settings group with the global settings manager.
|
|
||||||
///
|
|
||||||
/// Each settings group must be registered before the system can be initialized.
|
|
||||||
/// Groups are identified by their unique key, and duplicate keys are not allowed.
|
|
||||||
///
|
|
||||||
/// This method should be called during application startup, before calling [init].
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
/// - [settings]: The SettingsGroup instance to register
|
|
||||||
///
|
|
||||||
/// Throws: [ArgumentError] if a group with the same key already exists
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```dart
|
|
||||||
/// final gameSettings = SettingsGroup(key: 'game', items: [...]);
|
|
||||||
/// final uiSettings = SettingsGroup(key: 'ui', items: [...]);
|
|
||||||
///
|
|
||||||
/// Settings.register(gameSettings);
|
|
||||||
/// Settings.register(uiSettings);
|
|
||||||
///
|
|
||||||
/// await Settings.init(); // Initialize after all groups are registered
|
|
||||||
/// ```
|
|
||||||
void register(SettingsGroup settings) {
|
|
||||||
if (_settings.containsKey(settings.key)) {
|
|
||||||
throw ArgumentError('Settings with key ${settings.key} already exists');
|
|
||||||
}
|
|
||||||
_settings[settings.key] = settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a settings group by its key.
|
|
||||||
SettingsGroup getGroup(String key) {
|
|
||||||
if (!_settings.containsKey(key)) {
|
|
||||||
throw SettingNotFoundException('No settings group found for key: $key');
|
|
||||||
}
|
|
||||||
return _settings[key]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a setting by its storage key and type.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid or
|
|
||||||
/// if the setting is not found.
|
|
||||||
T get<T>(String storageKey) {
|
|
||||||
// Split the storage key to get the group key and setting key.
|
|
||||||
final id = _parseStorageKey(storageKey);
|
|
||||||
|
|
||||||
final group = getGroup(id.group);
|
|
||||||
return group.get<T>(id.setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helpers for typed access to settings.
|
|
||||||
// These methods are for convenience to access settings without
|
|
||||||
// ending up with a dynamic value.
|
|
||||||
|
|
||||||
/// Gets a boolean setting by its storage key.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid or
|
|
||||||
/// if the setting is not found or is not of type bool.
|
|
||||||
bool getBool(String storageKey) {
|
|
||||||
return get<bool>(storageKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a double setting by its storage key.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid or
|
|
||||||
/// if the setting is not found or is not of type int.
|
|
||||||
int getInt(String storageKey) {
|
|
||||||
return get<int>(storageKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a double setting by its storage key.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid or
|
|
||||||
/// if the setting is not found or is not of type double.
|
|
||||||
double getDouble(String storageKey) {
|
|
||||||
return get<double>(storageKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a string setting by its storage key.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid or
|
|
||||||
/// if the setting is not found or is not of type string.
|
|
||||||
String getString(String storageKey) {
|
|
||||||
return get<String>(storageKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Setters =====
|
|
||||||
|
|
||||||
/// Sets a setting value by its storage key.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid or
|
|
||||||
/// if the setting is not found or is not user configurable.
|
|
||||||
Future<void> setValue(String storageKey, dynamic value) async {
|
|
||||||
final id = _parseStorageKey(storageKey);
|
|
||||||
final group = getGroup(id.group);
|
|
||||||
await group.setValue(id.setting, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a setting value by its storage key and type.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid or
|
|
||||||
/// if the setting is not found or is not user configurable.
|
|
||||||
Future<void> set<T>(String storageKey, T value) async {
|
|
||||||
final id = _parseStorageKey(storageKey);
|
|
||||||
final group = getGroup(id.group);
|
|
||||||
await group.setValue<T>(id.setting, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a boolean setting value by its storage key.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid or
|
|
||||||
/// if the setting is not found or is not user configurable.
|
|
||||||
Future<void> setBool(String storageKey, bool value) async {
|
|
||||||
await set<bool>(storageKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets an integer setting value by its storage key.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid or
|
|
||||||
/// if the setting is not found or is not user configurable.
|
|
||||||
Future<void> setInt(String storageKey, int value) async {
|
|
||||||
await set<int>(storageKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a double setting value by its storage key.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid or
|
|
||||||
/// if the setting is not found or is not user configurable.
|
|
||||||
Future<void> setDouble(String storageKey, double value) async {
|
|
||||||
await set<double>(storageKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a string setting value by its storage key.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
/// Throws an [ArgumentError] if the storage key is invalid or
|
|
||||||
/// if the setting is not found or is not user configurable.
|
|
||||||
Future<void> setString(String storageKey, String value) async {
|
|
||||||
await set<String>(storageKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets multiple settings values in a batch operation.
|
|
||||||
/// The [settings] map should contain storage keys as keys and values as values.
|
|
||||||
/// This is more efficient than setting values individually.
|
|
||||||
Future<void> setMultiple(Map<String, dynamic> settings) async {
|
|
||||||
final futures = <Future<void>>[];
|
|
||||||
for (final entry in settings.entries) {
|
|
||||||
futures.add(setValue(entry.key, entry.value));
|
|
||||||
}
|
|
||||||
await Future.wait(futures);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset a setting to its default value by storage key.
|
|
||||||
/// The [storageKey] should be in the format "groupKey.settingKey".
|
|
||||||
Future<void> resetSetting(String storageKey) async {
|
|
||||||
final id = _parseStorageKey(storageKey);
|
|
||||||
final group = getGroup(id.group);
|
|
||||||
await group.reset(id.setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset all settings in a group to their default values.
|
|
||||||
/// The [groupKey] should be the key of the settings group.
|
|
||||||
Future<void> resetGroup(String groupKey) async {
|
|
||||||
final group = getGroup(groupKey);
|
|
||||||
await group.resetAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset all settings across all groups to their default values.
|
|
||||||
Future<void> resetAll() async {
|
|
||||||
final futures = _settings.values.map((group) => group.resetAll());
|
|
||||||
await Future.wait(futures);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Dispose all settings groups and their stream controllers.
|
|
||||||
void dispose() {
|
|
||||||
for (final group in _settings.values) {
|
|
||||||
group.dispose();
|
|
||||||
}
|
|
||||||
_settings.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear all registered settings groups (for testing purposes).
|
|
||||||
void clearAll() {
|
|
||||||
dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'Settings{groups: ${_settings.keys.join(', ')}}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'exceptions.dart';
|
|
||||||
|
|
||||||
/// A singleton store that manages the underlying SharedPreferences with caching.
|
|
||||||
///
|
|
||||||
/// This class provides a centralized, cached interface to SharedPreferences,
|
|
||||||
/// eliminating the need for repeated async calls during normal operation.
|
|
||||||
/// The store initializes asynchronously but provides synchronous access
|
|
||||||
/// once ready, improving performance for frequent setting access.
|
|
||||||
///
|
|
||||||
/// The store is used internally by the settings framework and typically
|
|
||||||
/// doesn't need to be accessed directly by application code.
|
|
||||||
///
|
|
||||||
/// In test environments, it automatically falls back to regular SharedPreferences
|
|
||||||
/// to ensure compatibility with test mocking frameworks.
|
|
||||||
///
|
|
||||||
/// Example internal usage:
|
|
||||||
/// ```dart
|
|
||||||
/// final store = SettingsStore();
|
|
||||||
/// await store.readyFuture; // Wait for initialization
|
|
||||||
/// bool value = store.prefs.getBool('some.key') ?? false;
|
|
||||||
/// ```
|
|
||||||
class SettingsStore {
|
|
||||||
/// The singleton instance of the settings store.
|
|
||||||
static SettingsStore? _instance;
|
|
||||||
|
|
||||||
/// Internal flag tracking whether the store is ready for use.
|
|
||||||
bool _ready = false;
|
|
||||||
|
|
||||||
/// Public getter indicating if the store has been initialized and is ready.
|
|
||||||
/// When true, the [prefs] getter can be used synchronously.
|
|
||||||
bool get ready => _ready;
|
|
||||||
|
|
||||||
/// Future that completes when the store is fully initialized.
|
|
||||||
/// Await this future before accessing settings to ensure proper initialization.
|
|
||||||
late final Future<bool> readyFuture;
|
|
||||||
|
|
||||||
/// Factory constructor that returns the singleton instance.
|
|
||||||
/// Multiple calls to this constructor return the same instance.
|
|
||||||
factory SettingsStore({bool forceRegularSharedPreferences = false}) {
|
|
||||||
_instance ??= SettingsStore._internal(forceRegularSharedPreferences);
|
|
||||||
return _instance!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The SharedPreferences instance (cached or regular depending on environment).
|
|
||||||
/// Only accessible after initialization is complete.
|
|
||||||
late final dynamic _prefs;
|
|
||||||
|
|
||||||
/// Whether we're using the cached version or regular SharedPreferences.
|
|
||||||
bool _isUsingCache = true;
|
|
||||||
|
|
||||||
/// Private constructor that initializes SharedPreferences.
|
|
||||||
///
|
|
||||||
/// In debug mode or when [forceRegularSharedPreferences] is true, uses regular
|
|
||||||
/// SharedPreferences for better test compatibility. In release mode, uses
|
|
||||||
/// SharedPreferencesWithCache for better performance.
|
|
||||||
///
|
|
||||||
/// This constructor:
|
|
||||||
/// 1. Creates a completer for the ready future
|
|
||||||
/// 2. Chooses appropriate SharedPreferences implementation based on environment
|
|
||||||
/// 3. Sets up success and error handling
|
|
||||||
/// 4. Marks the store as ready when initialization completes
|
|
||||||
SettingsStore._internal(bool forceRegularSharedPreferences) {
|
|
||||||
final completer = Completer<bool>();
|
|
||||||
readyFuture = completer.future;
|
|
||||||
|
|
||||||
// In debug mode or when forced, use regular SharedPreferences for better test compatibility
|
|
||||||
// In release mode, use SharedPreferencesWithCache for better performance
|
|
||||||
final useRegularSharedPreferences =
|
|
||||||
forceRegularSharedPreferences || kDebugMode;
|
|
||||||
|
|
||||||
if (useRegularSharedPreferences) {
|
|
||||||
_isUsingCache = false;
|
|
||||||
SharedPreferences.getInstance()
|
|
||||||
.then((prefs) {
|
|
||||||
_prefs = prefs;
|
|
||||||
_ready = true;
|
|
||||||
completer.complete(true);
|
|
||||||
})
|
|
||||||
.catchError((error) {
|
|
||||||
_ready = false;
|
|
||||||
completer.completeError(error);
|
|
||||||
throw Exception('Failed to initialize SharedPreferences: $error');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_isUsingCache = true;
|
|
||||||
SharedPreferencesWithCache.create(
|
|
||||||
cacheOptions: const SharedPreferencesWithCacheOptions(),
|
|
||||||
)
|
|
||||||
.then((prefs) {
|
|
||||||
_prefs = prefs;
|
|
||||||
_ready = true;
|
|
||||||
completer.complete(true);
|
|
||||||
})
|
|
||||||
.catchError((error) {
|
|
||||||
// If SharedPreferencesWithCache fails, fall back to regular SharedPreferences
|
|
||||||
_isUsingCache = false;
|
|
||||||
SharedPreferences.getInstance()
|
|
||||||
.then((fallbackPrefs) {
|
|
||||||
_prefs = fallbackPrefs;
|
|
||||||
_ready = true;
|
|
||||||
completer.complete(true);
|
|
||||||
})
|
|
||||||
.catchError((fallbackError) {
|
|
||||||
_ready = false;
|
|
||||||
completer.completeError(fallbackError);
|
|
||||||
throw Exception(
|
|
||||||
'Failed to initialize any SharedPreferences: $fallbackError',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset the singleton instance (useful for testing).
|
|
||||||
static void reset() {
|
|
||||||
_instance = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Provides access to the SharedPreferences instance.
|
|
||||||
///
|
|
||||||
/// This getter should only be called after the store is ready.
|
|
||||||
/// Use [ready] to check readiness or await [readyFuture] to ensure
|
|
||||||
/// the store is initialized before accessing this property.
|
|
||||||
///
|
|
||||||
/// Returns either SharedPreferencesWithCache (production) or
|
|
||||||
/// SharedPreferences (test environment) depending on initialization.
|
|
||||||
///
|
|
||||||
/// Throws: SettingsNotReadyException if accessed before initialization completes.
|
|
||||||
dynamic get prefs {
|
|
||||||
if (!_ready) {
|
|
||||||
throw SettingsNotReadyException(
|
|
||||||
'SettingsStore is not ready. Please await readyFuture first.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return _prefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if using SharedPreferencesWithCache, false if using regular SharedPreferences.
|
|
||||||
bool get isUsingCache => _isUsingCache;
|
|
||||||
}
|
|
||||||
|
|
@ -1,314 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:nes_ui/nes_ui.dart';
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:shitman/game/shitman_game.dart';
|
|
||||||
import 'package:shitman/settings/app_settings.dart';
|
|
||||||
|
|
||||||
class InGameUI extends StatelessWidget with AppSettings {
|
|
||||||
static const String overlayID = 'InGameUI';
|
|
||||||
final ShitmanGame game;
|
|
||||||
|
|
||||||
InGameUI(this.game, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
color: Colors.transparent,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
// Top HUD
|
|
||||||
Positioned(
|
|
||||||
top: 20,
|
|
||||||
left: 20,
|
|
||||||
right: 20,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
// Stealth indicator
|
|
||||||
NesContainer(
|
|
||||||
backgroundColor: Colors.black87,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.visibility_off, size: 16),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('gameplay.hidden'.tr(), style: TextStyle(color: Colors.green)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Mission objective
|
|
||||||
NesContainer(
|
|
||||||
backgroundColor: Colors.black87,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Text('gameplay.find_target'.tr(), style: TextStyle(color: Colors.white)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Pause button
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.pause, color: Colors.white),
|
|
||||||
onPressed: () => game.overlays.add(PauseMenuUI.overlayID),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Bottom controls hint
|
|
||||||
Positioned(
|
|
||||||
bottom: 20,
|
|
||||||
left: 20,
|
|
||||||
right: 20,
|
|
||||||
child: NesContainer(
|
|
||||||
backgroundColor: Colors.black54,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text('controls.move'.tr(), style: TextStyle(color: Colors.white)),
|
|
||||||
Text('controls.place_bag'.tr(), style: TextStyle(color: Colors.white)),
|
|
||||||
Text('controls.ring_bell'.tr(), style: TextStyle(color: Colors.white)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MainMenuUI extends StatelessWidget with AppSettings {
|
|
||||||
static const String overlayID = 'MainMenu';
|
|
||||||
final ShitmanGame game;
|
|
||||||
|
|
||||||
MainMenuUI(this.game, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
color: Colors.black,
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// Game title
|
|
||||||
Text(
|
|
||||||
'game.title'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
|
||||||
color: Colors.orange,
|
|
||||||
fontSize: 48,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'game.subtitle'.tr(),
|
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 16),
|
|
||||||
),
|
|
||||||
SizedBox(height: 40),
|
|
||||||
|
|
||||||
// Menu buttons
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
NesButton(
|
|
||||||
type: NesButtonType.primary,
|
|
||||||
onPressed: () {
|
|
||||||
game.overlays.remove(MainMenuUI.overlayID);
|
|
||||||
game.overlays.add(InGameUI.overlayID);
|
|
||||||
game.startGame();
|
|
||||||
},
|
|
||||||
child: Text('menu.start_mission'.tr()),
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
NesButton(
|
|
||||||
type: NesButtonType.normal,
|
|
||||||
onPressed: () {
|
|
||||||
game.overlays.remove(MainMenuUI.overlayID);
|
|
||||||
game.overlays.add(SettingsUI.overlayID);
|
|
||||||
},
|
|
||||||
child: Text('menu.settings'.tr()),
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
NesButton(
|
|
||||||
type: NesButtonType.normal,
|
|
||||||
onPressed: () => game.startInfiniteMode(),
|
|
||||||
child: Text('menu.infinite_mode'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 40),
|
|
||||||
Text(
|
|
||||||
'game.description'.tr(),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(color: Colors.white54, fontSize: 12),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingsUI extends StatelessWidget with AppSettings {
|
|
||||||
static const String overlayID = 'Settings';
|
|
||||||
final ShitmanGame game;
|
|
||||||
|
|
||||||
SettingsUI(this.game, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
color: Colors.black87,
|
|
||||||
child: Center(
|
|
||||||
child: NesContainer(
|
|
||||||
backgroundColor: Colors.black,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(24.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'menu.settings'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.close, color: Colors.white),
|
|
||||||
onPressed: () {
|
|
||||||
game.overlays.remove(SettingsUI.overlayID);
|
|
||||||
game.overlays.add(MainMenuUI.overlayID);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Language selector
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text('Language / Sprog / Sprache:', style: TextStyle(color: Colors.white)),
|
|
||||||
DropdownButton<String>(
|
|
||||||
value: context.locale.languageCode,
|
|
||||||
dropdownColor: Colors.black,
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
items: [
|
|
||||||
DropdownMenuItem(value: 'en', child: Text('English', style: TextStyle(color: Colors.white))),
|
|
||||||
DropdownMenuItem(value: 'da', child: Text('Dansk', style: TextStyle(color: Colors.white))),
|
|
||||||
DropdownMenuItem(value: 'de', child: Text('Deutsch', style: TextStyle(color: Colors.white))),
|
|
||||||
],
|
|
||||||
onChanged: (String? newValue) {
|
|
||||||
if (newValue != null) {
|
|
||||||
context.setLocale(Locale(newValue));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Debug mode toggle
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text('ui.debug_mode'.tr(), style: TextStyle(color: Colors.white)),
|
|
||||||
NesCheckBox(
|
|
||||||
value: false, // TODO: Connect to settings
|
|
||||||
onChange: (value) {
|
|
||||||
// TODO: Update settings
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
SizedBox(height: 40),
|
|
||||||
Center(
|
|
||||||
child: NesButton(
|
|
||||||
type: NesButtonType.primary,
|
|
||||||
onPressed: () {
|
|
||||||
game.overlays.remove(SettingsUI.overlayID);
|
|
||||||
game.overlays.add(MainMenuUI.overlayID);
|
|
||||||
},
|
|
||||||
child: Text('menu.back_to_menu'.tr()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PauseMenuUI extends StatelessWidget {
|
|
||||||
static const String overlayID = 'PauseMenu';
|
|
||||||
final ShitmanGame game;
|
|
||||||
|
|
||||||
const PauseMenuUI(this.game, {super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
color: Colors.black54,
|
|
||||||
child: Center(
|
|
||||||
child: NesContainer(
|
|
||||||
backgroundColor: Colors.black,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(24.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'ui.paused'.tr(),
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 24),
|
|
||||||
|
|
||||||
NesButton(
|
|
||||||
type: NesButtonType.primary,
|
|
||||||
onPressed: () => game.overlays.remove(PauseMenuUI.overlayID),
|
|
||||||
child: Text('menu.resume'.tr()),
|
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
NesButton(
|
|
||||||
type: NesButtonType.normal,
|
|
||||||
onPressed: () {
|
|
||||||
game.overlays.remove(PauseMenuUI.overlayID);
|
|
||||||
game.overlays.remove(InGameUI.overlayID);
|
|
||||||
game.overlays.add(SettingsUI.overlayID);
|
|
||||||
},
|
|
||||||
child: Text('menu.settings'.tr()),
|
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
NesButton(
|
|
||||||
type: NesButtonType.warning,
|
|
||||||
onPressed: () {
|
|
||||||
game.overlays.remove(PauseMenuUI.overlayID);
|
|
||||||
game.overlays.remove(InGameUI.overlayID);
|
|
||||||
game.overlays.add(MainMenuUI.overlayID);
|
|
||||||
game.stopGame();
|
|
||||||
},
|
|
||||||
child: Text('menu.main_menu'.tr()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,9 +6,7 @@ import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
106
pubspec.lock
106
pubspec.lock
|
|
@ -1,14 +1,6 @@
|
||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
args:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: args
|
|
||||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.7.0"
|
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -65,22 +57,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
easy_localization:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: easy_localization
|
|
||||||
sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.7+1"
|
|
||||||
easy_logger:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: easy_logger
|
|
||||||
sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.2"
|
|
||||||
equatable:
|
equatable:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -105,14 +81,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
file:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: file
|
|
||||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "7.0.1"
|
|
||||||
flame:
|
flame:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -134,11 +102,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.0.0"
|
||||||
flutter_localizations:
|
|
||||||
dependency: transitive
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
flutter_mini_sprite:
|
flutter_mini_sprite:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -152,11 +115,6 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_web_plugins:
|
|
||||||
dependency: transitive
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
google_fonts:
|
google_fonts:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -181,14 +139,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
intl:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: intl
|
|
||||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.20.2"
|
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -349,62 +299,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
shared_preferences:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: shared_preferences
|
|
||||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.5.3"
|
|
||||||
shared_preferences_android:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shared_preferences_android
|
|
||||||
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.4.10"
|
|
||||||
shared_preferences_foundation:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shared_preferences_foundation
|
|
||||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.5.4"
|
|
||||||
shared_preferences_linux:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shared_preferences_linux
|
|
||||||
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.4.1"
|
|
||||||
shared_preferences_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shared_preferences_platform_interface
|
|
||||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.4.1"
|
|
||||||
shared_preferences_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shared_preferences_web
|
|
||||||
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.4.3"
|
|
||||||
shared_preferences_windows:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shared_preferences_windows
|
|
||||||
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.4.1"
|
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
|
||||||
73
pubspec.yaml
73
pubspec.yaml
|
|
@ -1,29 +1,92 @@
|
||||||
name: shitman
|
name: shitman
|
||||||
description: "Hitman, but with shit."
|
description: "Hitman, but with shit."
|
||||||
publish_to: 'none'
|
# The following line prevents the package from being accidentally published to
|
||||||
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
# The following defines the version and build number for your application.
|
||||||
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.0
|
sdk: ^3.7.0
|
||||||
|
|
||||||
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
# dependencies can be manually updated by changing the version numbers below to
|
||||||
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
|
# versions available, run `flutter pub outdated`.
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
flame: ^1.30.1
|
flame: ^1.30.1
|
||||||
nes_ui: ^0.25.0
|
nes_ui: ^0.25.0
|
||||||
google_fonts: ^6.2.1
|
google_fonts: ^6.2.1
|
||||||
easy_localization: ^3.0.7+1
|
|
||||||
shared_preferences: ^2.2.2
|
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
|
# package. See that file for information about deactivating specific lint
|
||||||
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^5.0.0
|
flutter_lints: ^5.0.0
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
|
uses-material-design: false
|
||||||
|
|
||||||
|
# To add assets to your application, add an assets section, like this:
|
||||||
assets:
|
assets:
|
||||||
- assets/google_fonts/
|
- assets/google_fonts/
|
||||||
- assets/translations/
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|
||||||
|
# For details regarding adding assets from package dependencies, see
|
||||||
|
# https://flutter.dev/to/asset-from-package
|
||||||
|
|
||||||
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/to/font-from-package
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import 'package:shitman/main.dart';
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
// Build our app and trigger a frame.
|
// Build our app and trigger a frame.
|
||||||
await tester.pumpWidget(const Shitman());
|
await tester.pumpWidget(const MyApp());
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
// Verify that our counter starts at 0.
|
||||||
expect(find.text('0'), findsOneWidget);
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue