import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../app_config.dart'; import '../im/im_bridge.dart'; import '../im/tcp_client.dart'; class MenuNode { MenuNode({required this.name, required this.code, this.path, this.icon, this.children = const []}); final String name; final String code; final String? path; final String? icon; final List children; factory MenuNode.fromJson(Map j) { final kids = (j['children'] as List? ?? []).whereType().map((e) => MenuNode.fromJson(Map.from(e))).toList(); return MenuNode( name: '${j['name'] ?? ''}', code: '${j['code'] ?? ''}', path: j['path']?.toString(), icon: j['icon']?.toString(), children: kids, ); } } class SessionStore extends ChangeNotifier { String apiBase = AppConfig.defaultApiBase; String imHost = AppConfig.defaultImHost; int imPort = AppConfig.defaultImPort; String accessToken = ''; String refreshToken = ''; Map user = {}; List menus = []; bool loaded = false; final ImTcpClient im = ImTcpClient(); final ImBridge bridge = ImBridge(); String get userId => '${user['id'] ?? ''}'; String get displayName => '${user['displayName'] ?? user['username'] ?? ''}'; List get roles => (user['roles'] as List? ?? []).map((e) => '$e').toList(); List get permissions => (user['permissions'] as List? ?? []).map((e) => '$e').toList(); bool get signedIn => accessToken.isNotEmpty && userId.isNotEmpty; bool canSee(String code) { if (roles.contains('admin') || roles.contains('owner')) return true; return permissions.contains(code) || permissions.any((p) => p.startsWith('$code:')); } Future load() async { final p = await SharedPreferences.getInstance(); apiBase = p.getString('apiBase') ?? AppConfig.defaultApiBase; imHost = p.getString('imHost') ?? AppConfig.defaultImHost; imPort = p.getInt('imPort') ?? AppConfig.defaultImPort; accessToken = p.getString('accessToken') ?? ''; refreshToken = p.getString('refreshToken') ?? ''; final u = p.getString('user'); if (u != null && u.isNotEmpty) { try { user = Map.from(jsonDecode(u) as Map); } catch (_) { user = {}; } } loaded = true; notifyListeners(); } Future saveEndpoints({required String api, required String host, required int port}) async { apiBase = api.trim().isEmpty ? AppConfig.defaultApiBase : api.trim(); imHost = host.trim().isEmpty ? AppConfig.defaultImHost : host.trim(); imPort = port <= 0 ? AppConfig.defaultImPort : port; final p = await SharedPreferences.getInstance(); await p.setString('apiBase', apiBase); await p.setString('imHost', imHost); await p.setInt('imPort', imPort); notifyListeners(); } Future applyLogin(Map data) async { accessToken = '${data['accessToken'] ?? ''}'; refreshToken = '${data['refreshToken'] ?? ''}'; user = Map.from(data['user'] as Map? ?? {}); final p = await SharedPreferences.getInstance(); await p.setString('accessToken', accessToken); await p.setString('refreshToken', refreshToken); await p.setString('user', jsonEncode(user)); notifyListeners(); await connectIm(); } Future setTokens(String access, String refresh) async { final changed = access != accessToken; accessToken = access; refreshToken = refresh; final p = await SharedPreferences.getInstance(); await p.setString('accessToken', access); await p.setString('refreshToken', refresh); notifyListeners(); if (changed && signedIn) await connectIm(force: true); } Future patchUser(Map next) async { user = next; final p = await SharedPreferences.getInstance(); await p.setString('user', jsonEncode(user)); notifyListeners(); } Future setMenus(List items) async { menus = items; notifyListeners(); } Future connectIm({bool force = false}) async { if (!signedIn) return; if (force) im.disconnect(); await bridge.init(); await im.connect( host: imHost, port: imPort, userId: userId, token: accessToken, ); } Future clear() async { im.disconnect(); await bridge.release(); accessToken = ''; refreshToken = ''; user = {}; menus = []; final p = await SharedPreferences.getInstance(); await p.remove('accessToken'); await p.remove('refreshToken'); await p.remove('user'); notifyListeners(); } @override void dispose() { im.disconnect(); im.dispose(); super.dispose(); } }