import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../engine/brush.dart'; /// OneNote-style independent pen slot: brush + color + thickness together. /// /// Selecting a slot restores all three; color dots / thickness controls edit /// only the active slot. class PenSlot { const PenSlot({ required this.id, required this.brush, required this.color, required this.width, }); final String id; /// Brush kind for this slot (fountain / ballpoint / pencil — not highlighter). final BrushKind brush; final Color color; /// Stroke width as a fraction of page width. final double width; PenSlot copyWith({ String? id, BrushKind? brush, Color? color, double? width, }) { return PenSlot( id: id ?? this.id, brush: brush ?? this.brush, color: color ?? this.color, width: width ?? this.width, ); } Map toJson() => { 'id': id, 'brush': brush.name, 'color': color.toARGB32(), 'width': width, }; factory PenSlot.fromJson(Map json) { final brushName = json['brush'] as String? ?? ''; return PenSlot( id: json['id'] as String? ?? 'slot_0', brush: BrushKind.values.asNameMap()[brushName] ?? BrushKind.fountainPen, color: Color(json['color'] as int? ?? 0xFF000000), width: (json['width'] as num?)?.toDouble() ?? 0.006, ); } @override bool operator ==(Object other) => identical(this, other) || other is PenSlot && runtimeType == other.runtimeType && id == other.id && brush == other.brush && color.toARGB32() == other.color.toARGB32() && width == other.width; @override int get hashCode => Object.hash(id, brush, color.toARGB32(), width); } /// Default pen slots seeded OneNote-style (fountain / ballpoint / pencil). List kDefaultPenSlots() => const [ PenSlot( id: 'slot_0', brush: BrushKind.fountainPen, color: Colors.black, width: 0.006, ), PenSlot( id: 'slot_1', brush: BrushKind.ballpoint, color: Colors.blue, width: 0.0022, ), PenSlot( id: 'slot_2', brush: BrushKind.pencil, color: Colors.green, width: 0.003, ), ]; /// S / M / L thickness presets (page-width fractions) for the toolbar picker. const double kThicknessSmall = 0.0022; const double kThicknessMedium = 0.006; const double kThicknessLarge = 0.012; /// Allowed range for slot stroke width (page-width fraction). const double kPenSlotWidthMin = 0.001; const double kPenSlotWidthMax = 0.05; /// Manages independent [PenSlot]s with SharedPreferences persistence. /// /// Load with [PenSlotsController.load], then listen via [ChangeNotifier]. class PenSlotsController extends ChangeNotifier { PenSlotsController._(this._prefs, this._slots, this._activeId); /// SharedPreferences key for the slots JSON blob. static const prefsKey = 'pen_slots_v1'; final SharedPreferences _prefs; List _slots; String _activeId; List get slots => List.unmodifiable(_slots); String get activeId => _activeId; PenSlot get active { for (final s in _slots) { if (s.id == _activeId) return s; } return _slots.first; } /// Loads persisted slots, or seeds [kDefaultPenSlots] on first run / corrupt /// JSON. static Future load() async { final prefs = await SharedPreferences.getInstance(); final raw = prefs.getString(prefsKey); var slots = kDefaultPenSlots(); var activeId = slots.first.id; if (raw != null) { try { final map = jsonDecode(raw) as Map; final list = map['slots'] as List?; if (list != null && list.isNotEmpty) { slots = [ for (final e in list) PenSlot.fromJson(e as Map), ]; } final storedActive = map['activeId'] as String?; if (storedActive != null && slots.any((s) => s.id == storedActive)) { activeId = storedActive; } else { activeId = slots.first.id; } } catch (_) { slots = kDefaultPenSlots(); activeId = slots.first.id; } } return PenSlotsController._(prefs, slots, activeId); } Future _persist() async { await _prefs.setString( prefsKey, jsonEncode({ 'activeId': _activeId, 'slots': [for (final s in _slots) s.toJson()], }), ); } int _indexOfActive() { final i = _slots.indexWhere((s) => s.id == _activeId); return i >= 0 ? i : 0; } void _replaceActive(PenSlot next) { final i = _indexOfActive(); _slots = [..._slots]..[i] = next; } /// Selects [id] as the active slot (restores brush + color + width). Future select(String id) async { if (!_slots.any((s) => s.id == id)) return; if (_activeId == id) return; _activeId = id; notifyListeners(); await _persist(); } /// Sets the active slot's color. Future setActiveColor(Color c) async { _replaceActive(active.copyWith(color: c)); notifyListeners(); await _persist(); } /// Sets the active slot's stroke width (clamped). Future setActiveWidth(double w) async { final clamped = w.clamp(kPenSlotWidthMin, kPenSlotWidthMax); _replaceActive(active.copyWith(width: clamped)); notifyListeners(); await _persist(); } /// Sets the active slot's brush kind. Future setActiveBrush(BrushKind b) async { if (b == BrushKind.highlighter) return; _replaceActive(active.copyWith(brush: b)); notifyListeners(); await _persist(); } }