diff --git a/sojorn_app/lib/models/group.dart b/sojorn_app/lib/models/group.dart index 9e457dd..ee62bd2 100644 --- a/sojorn_app/lib/models/group.dart +++ b/sojorn_app/lib/models/group.dart @@ -1,25 +1,6 @@ import 'package:equatable/equatable.dart'; - -enum GroupCategory { - general('General', 'general'), - hobby('Hobby', 'hobby'), - sports('Sports', 'sports'), - professional('Professional', 'professional'), - localBusiness('Local Business', 'local_business'), - support('Support', 'support'), - education('Education', 'education'); - - const GroupCategory(this.displayName, this.value); - final String displayName; - final String value; - - static GroupCategory fromString(String value) { - return GroupCategory.values.firstWhere( - (cat) => cat.value == value, - orElse: () => GroupCategory.general, - ); - } -} +import 'cluster.dart' show GroupCategory; +export 'cluster.dart' show GroupCategory; enum GroupRole { owner('Owner'), diff --git a/sojorn_app/lib/screens/beacon/enhanced_beacon_detail_screen.dart b/sojorn_app/lib/screens/beacon/enhanced_beacon_detail_screen.dart index 70dd42f..104dbd2 100644 --- a/sojorn_app/lib/screens/beacon/enhanced_beacon_detail_screen.dart +++ b/sojorn_app/lib/screens/beacon/enhanced_beacon_detail_screen.dart @@ -41,7 +41,7 @@ class _EnhancedBeaconDetailScreenState extends State options: MapOptions( initialCenter: LatLng(widget.beacon.lat, widget.beacon.lng), initialZoom: 15.0, - interactiveFlags: InteractiveFlag.none, + interactionOptions: const InteractionOptions(flags: InteractiveFlag.none), ), children: [ TileLayer( @@ -483,7 +483,7 @@ class _EnhancedBeaconDetailScreenState extends State color: AppTheme.navyBlue, shape: BoxShape.circle, ), - child: const Center( + child: Center( child: Text( '${index + 1}', style: TextStyle( @@ -759,7 +759,7 @@ class _EnhancedBeaconDetailScreenState extends State ], ), ), - const Icon( + Icon( Icons.arrow_forward_ios, color: Colors.grey[400], size: 16, @@ -778,7 +778,7 @@ class _EnhancedBeaconDetailScreenState extends State } void _callEmergency() async { - const url = 'tel:911'; + final url = Uri.parse('tel:911'); if (await canLaunchUrl(url)) { await launchUrl(url); } diff --git a/sojorn_app/lib/screens/clusters/clusters_screen.dart b/sojorn_app/lib/screens/clusters/clusters_screen.dart index 881d578..43a4d9c 100644 --- a/sojorn_app/lib/screens/clusters/clusters_screen.dart +++ b/sojorn_app/lib/screens/clusters/clusters_screen.dart @@ -768,7 +768,7 @@ class _CreateGroupFormState extends ConsumerState<_CreateGroupForm> { await api.createGroup( name: _nameCtrl.text.trim(), description: _descCtrl.text.trim(), - category: group_models.GroupCategory.general, + category: GroupCategory.general, isPrivate: _privacy, ); widget.onCreated(); diff --git a/sojorn_app/lib/screens/clusters/private_capsule_screen.dart b/sojorn_app/lib/screens/clusters/private_capsule_screen.dart index a7a87e1..1e17653 100644 --- a/sojorn_app/lib/screens/clusters/private_capsule_screen.dart +++ b/sojorn_app/lib/screens/clusters/private_capsule_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cryptography/cryptography.dart'; import '../../models/cluster.dart'; +import '../../providers/api_provider.dart'; import '../../services/api_service.dart'; import '../../services/auth_service.dart'; import '../../services/capsule_security_service.dart'; diff --git a/sojorn_app/lib/screens/home/feed_sojorn_screen.dart b/sojorn_app/lib/screens/home/feed_sojorn_screen.dart index dbcd10b..3c710b5 100644 --- a/sojorn_app/lib/screens/home/feed_sojorn_screen.dart +++ b/sojorn_app/lib/screens/home/feed_sojorn_screen.dart @@ -166,7 +166,7 @@ class _FeedsojornScreenState extends ConsumerState { } void _sharePost(Post post) { - final text = post.content.isNotEmpty ? post.content : 'Check this out on Sojorn'; + final text = post.body.isNotEmpty ? post.body : 'Check this out on Sojorn'; Share.share(text, subject: 'Shared from Sojorn'); } @@ -311,7 +311,7 @@ class _FeedsojornScreenState extends ConsumerState { const SizedBox(height: 4), Text( 'items: ${_feedItems.length} | ads: ${adIndices.length}', - style: const TextStyle( + style: TextStyle( color: SojornColors.basicWhite.withValues(alpha: 0.7), fontSize: 11, ), @@ -321,7 +321,7 @@ class _FeedsojornScreenState extends ConsumerState { adIndices.isEmpty ? 'ad positions: none' : 'ad positions: ${adIndices.join(', ')}', - style: const TextStyle( + style: TextStyle( color: SojornColors.basicWhite.withValues(alpha: 0.7), fontSize: 11, ), diff --git a/sojorn_app/lib/screens/quips/create/enhanced_quip_recorder_screen.dart b/sojorn_app/lib/screens/quips/create/enhanced_quip_recorder_screen.dart index d4102e7..7b775f7 100644 --- a/sojorn_app/lib/screens/quips/create/enhanced_quip_recorder_screen.dart +++ b/sojorn_app/lib/screens/quips/create/enhanced_quip_recorder_screen.dart @@ -42,7 +42,6 @@ class _EnhancedQuipRecorderScreenState extends State DateTime? _segmentStartTime; Timer? _progressTicker; Duration _currentSegmentDuration = Duration.zero; - Duration _totalRecordedDuration = Duration.zero; // Speed Control double _playbackSpeed = 1.0; @@ -166,7 +165,7 @@ class _EnhancedQuipRecorderScreenState extends State // Auto-stop at max duration Timer(const Duration(milliseconds: 100), () { - if (get _totalRecordedDuration >= _maxDuration) { + if (_totalRecordedDuration >= _maxDuration) { _stopRecording(); } }); @@ -187,7 +186,6 @@ class _EnhancedQuipRecorderScreenState extends State // Save current segment _segmentDurations.add(_currentSegmentDuration); - _totalRecordedDuration = get _totalRecordedDuration; } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pause recording'))); @@ -200,7 +198,7 @@ class _EnhancedQuipRecorderScreenState extends State try { await _cameraController!.resumeVideoRecording(); - setState(() => { + setState(() { _isPaused = false; _segmentStartTime = DateTime.now(); _currentSegmentDuration = Duration.zero; @@ -231,15 +229,13 @@ class _EnhancedQuipRecorderScreenState extends State if (videoFile != null) { setState(() => _isRecording = false); _isPaused = false; - + // Add segment if it has content if (_currentSegmentDuration.inMilliseconds > 500) { // Minimum 0.5 seconds - _recordedSegments.add(videoFile); + _recordedSegments.add(File(videoFile.path)); _segmentDurations.add(_currentSegmentDuration); } - _totalRecordedDuration = get _totalRecordedDuration; - // Auto-process if we have segments if (_recordedSegments.isNotEmpty) { _processVideo(); @@ -258,8 +254,7 @@ class _EnhancedQuipRecorderScreenState extends State setState(() => _isProcessing = true); try { - final videoStitchingService = VideoStitchingService(); - final finalFile = await videoStitchingService.stitchVideos( + final finalFile = await VideoStitchingService.stitchVideos( _recordedSegments, _segmentDurations, _selectedFilter, @@ -267,7 +262,7 @@ class _EnhancedQuipRecorderScreenState extends State _showTextOverlay ? { 'text': _overlayText, 'size': _textSize, - 'color': _textColor.value.toHex(), + 'color': '#${_textColor.value.toRadixString(16).padLeft(8, '0')}', 'position': _textPositionY, } : null, audioOverlayPath: _selectedAudio?.path, @@ -318,7 +313,7 @@ class _EnhancedQuipRecorderScreenState extends State try { final camera = _cameras.firstWhere( - (c) => c.lensDirection == (_isRearCamera ? CameraLensDirection.back : CameraDirection.front), + (c) => c.lensDirection == (_isRearCamera ? CameraLensDirection.back : CameraLensDirection.front), orElse: () => _cameras.first ); @@ -357,7 +352,6 @@ class _EnhancedQuipRecorderScreenState extends State _recordedSegments.clear(); _segmentDurations.clear(); _currentSegmentDuration = Duration.zero; - _totalRecordedDuration = Duration.zero; }); } @@ -481,7 +475,7 @@ class _EnhancedQuipRecorderScreenState extends State Container( margin: const EdgeInsets.only(bottom: 16), child: LinearProgressIndicator( - value: get _totalRecordedDuration.inMilliseconds / _maxDuration.inMilliseconds, + value: _totalRecordedDuration.inMilliseconds / _maxDuration.inMilliseconds, backgroundColor: Colors.white24, valueColor: AlwaysStoppedAnimation( _isPaused ? Colors.orange : Colors.red, @@ -495,7 +489,7 @@ class _EnhancedQuipRecorderScreenState extends State children: [ // Duration Text( - _formatDuration(get _totalRecordedDuration), + _formatDuration(_totalRecordedDuration), style: const TextStyle( color: Colors.white, fontSize: 16, @@ -732,29 +726,28 @@ class _EnhancedQuipRecorderScreenState extends State // Size selector Expanded( child: Slider( - value: _textSize, - min: 12, - max: 48, - divisions: 4, - label: '${_textSize.toInt()}', - labelStyle: const TextStyle(color: Colors.white70), - activeColor: AppTheme.navyBlue, - inactiveColor: Colors.white24, - onChanged: (value) => setState(() => _textSize = value), + value: _textSize, + min: 12, + max: 48, + divisions: 4, + label: '${_textSize.toInt()}', + activeColor: AppTheme.navyBlue, + inactiveColor: Colors.white24, + onChanged: (value) => setState(() => _textSize = value), + ), ), - ), const SizedBox(width: 16), // Position selector Expanded( child: Slider( - value: _textPositionY, - min: 0.0, - max: 1.0, - label: _textPositionY == 0.0 ? 'Top' : 'Bottom', - labelStyle: const TextStyle(color: Colors.white70), - activeColor: AppTheme.navyBlue, - inactiveColor: Colors.white24, - onChanged: (value) => setState(() => _textPositionY = value), + value: _textPositionY, + min: 0.0, + max: 1.0, + label: _textPositionY == 0.0 ? 'Top' : 'Bottom', + activeColor: AppTheme.navyBlue, + inactiveColor: Colors.white24, + onChanged: (value) => setState(() => _textPositionY = value), + ), ), ], ), diff --git a/sojorn_app/lib/screens/quips/create/quip_recorder_screen.dart b/sojorn_app/lib/screens/quips/create/quip_recorder_screen.dart index 9b7e428..a56b4af 100644 --- a/sojorn_app/lib/screens/quips/create/quip_recorder_screen.dart +++ b/sojorn_app/lib/screens/quips/create/quip_recorder_screen.dart @@ -196,7 +196,7 @@ class _QuipRecorderScreenState extends State if (_recordedSegments.length == 1) { finalFile = _recordedSegments.first; } else { - finalFile = await VideoStitchingService.stitchVideos(_recordedSegments); + finalFile = await VideoStitchingService.stitchVideosLegacy(_recordedSegments); } if (finalFile != null && mounted) { diff --git a/sojorn_app/lib/services/api_service.dart b/sojorn_app/lib/services/api_service.dart index b207035..d39fbbd 100644 --- a/sojorn_app/lib/services/api_service.dart +++ b/sojorn_app/lib/services/api_service.dart @@ -176,6 +176,16 @@ class ApiService { return _callGoApi(path, method: 'GET', queryParams: queryParams); } + /// Simple POST request helper + Future> post(String path, Map body) async { + return _callGoApi(path, method: 'POST', body: body); + } + + /// Simple DELETE request helper + Future> delete(String path) async { + return _callGoApi(path, method: 'DELETE'); + } + Future resendVerificationEmail(String email) async { await _callGoApi('/auth/resend-verification', diff --git a/sojorn_app/lib/services/audio_overlay_service.dart b/sojorn_app/lib/services/audio_overlay_service.dart index 3981248..03abcbc 100644 --- a/sojorn_app/lib/services/audio_overlay_service.dart +++ b/sojorn_app/lib/services/audio_overlay_service.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'media/ffmpeg.dart'; @@ -92,10 +93,10 @@ class AudioOverlayService { try { final command = "-i '${audioFile.path}' -f null -"; final session = await FFmpegKit.execute(command); - final logs = await session.getAllLogs(); - - for (final log in logs) { - final message = log.getMessage(); + final output = await session.getOutput() ?? ''; + final logs = output.split('\n'); + + for (final message in logs) { if (message.contains('Duration:')) { // Parse duration from FFmpeg output final durationMatch = RegExp(r'Duration: (\d{2}):(\d{2}):(\d{2}\.\d{2})').firstMatch(message); @@ -442,7 +443,7 @@ class _AudioOverlayControlsState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.fade_in, + Icons.volume_up, color: Colors.white, size: 16, ), @@ -475,7 +476,7 @@ class _AudioOverlayControlsState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.fade_out, + Icons.volume_down, color: Colors.white, size: 16, ), diff --git a/sojorn_app/lib/services/blocking_service.dart b/sojorn_app/lib/services/blocking_service.dart index 1165f8e..b969547 100644 --- a/sojorn_app/lib/services/blocking_service.dart +++ b/sojorn_app/lib/services/blocking_service.dart @@ -1,8 +1,11 @@ import 'dart:convert'; import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:share_plus/share_plus.dart'; class BlockingService { @@ -30,8 +33,8 @@ class BlockingService { await file.writeAsString(const JsonEncoder.withIndent(' ').convert(exportData)); // Share the file - final result = await Share.shareXFiles([file.path]); - return result.status == ShareResultStatus.done; + final result = await Share.shareXFiles([XFile(file.path)]); + return result.status == ShareResultStatus.success; } catch (e) { print('Error exporting blocked users to JSON: $e'); return false; @@ -54,8 +57,8 @@ class BlockingService { await file.writeAsString(csvContent.toString()); // Share the file - final result = await Share.shareXFiles([file.path]); - return result.status == ShareResultStatus.done; + final result = await Share.shareXFiles([XFile(file.path)]); + return result.status == ShareResultStatus.success; } catch (e) { print('Error exporting blocked users to CSV: $e'); return false; @@ -279,7 +282,7 @@ class _BlockManagementScreenState extends State { // This would typically come from your API service // For now, we'll use a placeholder final prefs = await SharedPreferences.getInstance(); - final blockedUsersJson = prefs.getString(_blockedUsersJsonKey); + final blockedUsersJson = prefs.getString(BlockingService._blockedUsersJsonKey); if (blockedUsersJson != null) { final blockedUsersList = jsonDecode(blockedUsersJson) as List; @@ -299,10 +302,10 @@ class _BlockManagementScreenState extends State { Future _saveBlockedUsers() async { try { final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_blockedUsersJsonKey, jsonEncode(_blockedUsers)); + await prefs.setString(BlockingService._blockedUsersJsonKey, jsonEncode(_blockedUsers)); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: 'Failed to save blocked users'), + const SnackBar(content: Text('Failed to save blocked users')), ); } } @@ -353,21 +356,21 @@ class _BlockManagementScreenState extends State { final validatedUsers = await BlockingService.validateBlockedUsers(importedUsers); setState(() { - _blockedUsers = {..._blockedUsers, ...validatedUsers}.toSet().toList()}; + _blockedUsers = {..._blockedUsers, ...validatedUsers}.toList(); }); await _saveBlockedUsers(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: 'Successfully imported ${validatedUsers.length} users', + content: Text('Successfully imported ${validatedUsers.length} users'), backgroundColor: Colors.green, ), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: 'Failed to import: $e', + content: Text('Failed to import: $e'), backgroundColor: Colors.red, ), ); @@ -397,7 +400,7 @@ class _BlockManagementScreenState extends State { actions: [ TextButton( onPressed: () => Navigator.pop(context), - user: const Text('Cancel'), + child: const Text('Cancel'), ), ], ), @@ -412,17 +415,17 @@ class _BlockManagementScreenState extends State { try { final success = await format.exportFunction!(_blockedUsers); - if (success) { + if (success == true) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: 'Successfully exported ${_blockedUsers.length} users', + content: Text('Successfully exported ${_blockedUsers.length} users'), backgroundColor: Colors.green, ), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: 'Export cancelled or failed', + content: const Text('Export cancelled or failed'), backgroundColor: Colors.orange, ), ); @@ -430,7 +433,7 @@ class _BlockManagementScreenState extends State { } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: 'Export failed: $e', + content: Text('Export failed: $e'), backgroundColor: Colors.red, ), ); diff --git a/sojorn_app/lib/services/e2ee_device_sync_service.dart b/sojorn_app/lib/services/e2ee_device_sync_service.dart index a764e08..9984693 100644 --- a/sojorn_app/lib/services/e2ee_device_sync_service.dart +++ b/sojorn_app/lib/services/e2ee_device_sync_service.dart @@ -1,869 +1,6 @@ -import 'dart:convert'; -import 'dart:typed_data'; -import 'package:crypto/crypto.dart'; -import 'package:encrypt/encrypt.dart'; -import 'package:pointycastle/export.dart'; -import 'package:qr_flutter/qr_flutter.dart'; -import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:sojorn/services/api_service.dart'; +// E2EE device sync service — stub (encrypt/pointycastle/qr_flutter not in pubspec) +// Full implementation deferred until those packages are added. class E2EEDeviceSyncService { - static const String _devicesKey = 'e2ee_devices'; - static const String _currentDeviceKey = 'e2ee_current_device'; - static const String _keysKey = 'e2ee_keys'; - - /// Device information for E2EE - class DeviceInfo { - final String id; - final String name; - final String type; // mobile, desktop, web - final String publicKey; - final DateTime lastSeen; - final bool isActive; - final Map? metadata; - - DeviceInfo({ - required this.id, - required this.name, - required this.type, - required this.publicKey, - required this.lastSeen, - this.isActive = true, - this.metadata, - }); - - factory DeviceInfo.fromJson(Map json) { - return DeviceInfo( - id: json['id'] ?? '', - name: json['name'] ?? '', - type: json['type'] ?? '', - publicKey: json['public_key'] ?? '', - lastSeen: DateTime.parse(json['last_seen']), - isActive: json['is_active'] ?? true, - metadata: json['metadata'], - ); - } - - Map toJson() { - return { - 'id': id, - 'name': name, - 'type': type, - 'public_key': publicKey, - 'last_seen': lastSeen.toIso8601String(), - 'is_active': isActive, - 'metadata': metadata, - }; - } - } - - /// E2EE key pair - class E2EEKeyPair { - final String privateKey; - final String publicKey; - final String keyId; - final DateTime createdAt; - final DateTime? expiresAt; - final String algorithm; // RSA, ECC, etc. - - E2EEKeyPair({ - required this.privateKey, - required this.publicKey, - required this.keyId, - required this.createdAt, - this.expiresAt, - this.algorithm = 'RSA', - }); - - factory E2EEKeyPair.fromJson(Map json) { - return E2EEKeyPair( - privateKey: json['private_key'] ?? '', - publicKey: json['public_key'] ?? '', - keyId: json['key_id'] ?? '', - createdAt: DateTime.parse(json['created_at']), - expiresAt: json['expires_at'] != null ? DateTime.parse(json['expires_at']) : null, - algorithm: json['algorithm'] ?? 'RSA', - ); - } - - Map toJson() { - return { - 'private_key': privateKey, - 'public_key': publicKey, - 'key_id': keyId, - 'created_at': createdAt.toIso8601String(), - 'expires_at': expiresAt?.toIso8601String(), - 'algorithm': algorithm, - }; - } - } - - /// QR code data for device verification - class QRVerificationData { - final String deviceId; - final String publicKey; - final String timestamp; - final String signature; - final String userId; - - QRVerificationData({ - required this.deviceId, - required this.publicKey, - required this.timestamp, - required this.signature, - required this.userId, - }); - - factory QRVerificationData.fromJson(Map json) { - return QRVerificationData( - deviceId: json['device_id'] ?? '', - publicKey: json['public_key'] ?? '', - timestamp: json['timestamp'] ?? '', - signature: json['signature'] ?? '', - userId: json['user_id'] ?? '', - ); - } - - Map toJson() { - return { - 'device_id': deviceId, - 'public_key': publicKey, - 'timestamp': timestamp, - 'signature': signature, - 'user_id': userId, - }; - } - - String toBase64() { - return base64Encode(utf8.encode(jsonEncode(toJson()))); - } - - factory QRVerificationData.fromBase64(String base64String) { - final json = jsonDecode(utf8.decode(base64Decode(base64String))); - return QRVerificationData.fromJson(json); - } - } - - /// Generate new E2EE key pair - static Future generateKeyPair() async { - try { - // Generate RSA key pair - final keyPair = RSAKeyGenerator().generateKeyPair(2048); - final privateKey = keyPair.privateKey as RSAPrivateKey; - final publicKey = keyPair.publicKey as RSAPublicKey; - - // Convert to PEM format - final privatePem = privateKey.toPem(); - final publicPem = publicKey.toPem(); - - // Generate key ID - final keyId = _generateKeyId(); - - return E2EEKeyPair( - privateKey: privatePem, - publicKey: publicPem, - keyId: keyId, - createdAt: DateTime.now(), - algorithm: 'RSA', - ); - } catch (e) { - throw Exception('Failed to generate E2EE key pair: $e'); - } - } - - /// Register current device - static Future registerDevice({ - required String userId, - required String deviceName, - required String deviceType, - Map? metadata, - }) async { - try { - // Generate key pair for this device - final keyPair = await generateKeyPair(); - - // Create device info - final device = DeviceInfo( - id: _generateDeviceId(), - name: deviceName, - type: deviceType, - publicKey: keyPair.publicKey, - lastSeen: DateTime.now(), - metadata: metadata, - ); - - // Save to local storage - await _saveCurrentDevice(device); - await _saveKeyPair(keyPair); - - // Register with server - await _registerDeviceWithServer(userId, device, keyPair); - - return device; - } catch (e) { - throw Exception('Failed to register device: $e'); - } - } - - /// Get QR verification data for current device - static Future getQRVerificationData(String userId) async { - try { - final device = await _getCurrentDevice(); - if (device == null) { - throw Exception('No device registered'); - } - - final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); - final signature = await _signData(device.id + timestamp + userId); - - return QRVerificationData( - deviceId: device.id, - publicKey: device.publicKey, - timestamp: timestamp, - signature: signature, - userId: userId, - ); - } catch (e) { - throw Exception('Failed to generate QR data: $e'); - } - } - - /// Verify and add device from QR code - static Future verifyAndAddDevice(String qrData, String currentUserId) async { - try { - final qrVerificationData = QRVerificationData.fromBase64(qrData); - - // Verify signature - final isValid = await _verifySignature( - qrVerificationData.deviceId + qrVerificationData.timestamp + qrVerificationData.userId, - qrVerificationData.signature, - qrVerificationData.publicKey, - ); - - if (!isValid) { - throw Exception('Invalid QR code signature'); - } - - // Check if timestamp is recent (within 5 minutes) - final timestamp = int.parse(qrVerificationData.timestamp); - final now = DateTime.now().millisecondsSinceEpoch(); - if (now - timestamp > 5 * 60 * 1000) { // 5 minutes - throw Exception('QR code expired'); - } - - // Add device to user's device list - final device = DeviceInfo( - id: qrVerificationData.deviceId, - name: 'QR Linked Device', - type: 'unknown', - publicKey: qrVerificationData.publicKey, - lastSeen: DateTime.now(), - ); - - await _addDeviceToUser(currentUserId, device); - - return true; - } catch (e) { - print('Failed to verify QR device: $e'); - return false; - } - } - - /// Sync keys between devices - static Future syncKeys(String userId) async { - try { - // Get all devices for user - final devices = await _getUserDevices(userId); - - // Get current device - final currentDevice = await _getCurrentDevice(); - if (currentDevice == null) { - throw Exception('No current device found'); - } - - // Sync keys with server - final response = await ApiService.instance.post('/api/e2ee/sync-keys', { - 'device_id': currentDevice.id, - 'devices': devices.map((d) => d.toJson()).toList(), - }); - - if (response['success'] == true) { - // Update local device list - final updatedDevices = (response['devices'] as List?) - ?.map((d) => DeviceInfo.fromJson(d as Map)) - .toList() ?? []; - - await _saveUserDevices(userId, updatedDevices); - return true; - } - - return false; - } catch (e) { - print('Failed to sync keys: $e'); - return false; - } - } - - /// Encrypt message for specific device - static Future encryptMessageForDevice({ - required String message, - required String targetDeviceId, - required String userId, - }) async { - try { - // Get target device's public key - final devices = await _getUserDevices(userId); - final targetDevice = devices.firstWhere( - (d) => d.id == targetDeviceId, - orElse: () => throw Exception('Target device not found'), - ); - - // Get current device's private key - final currentKeyPair = await _getCurrentKeyPair(); - if (currentKeyPair == null) { - throw Exception('No encryption keys available'); - } - - // Encrypt message - final encryptedData = await _encryptWithPublicKey( - message, - targetDevice.publicKey, - ); - - return encryptedData; - } catch (e) { - throw Exception('Failed to encrypt message: $e'); - } - } - - /// Decrypt message from any device - static Future decryptMessage({ - required String encryptedMessage, - required String userId, - }) async { - try { - // Get current device's private key - final currentKeyPair = await _getCurrentKeyPair(); - if (currentKeyPair == null) { - throw Exception('No decryption keys available'); - } - - // Decrypt message - final decryptedData = await _decryptWithPrivateKey( - encryptedMessage, - currentKeyPair.privateKey, - ); - - return decryptedData; - } catch (e) { - throw Exception('Failed to decrypt message: $e'); - } - } - - /// Remove device - static Future removeDevice(String userId, String deviceId) async { - try { - // Remove from server - final response = await ApiService.instance.delete('/api/e2ee/devices/$deviceId'); - - if (response['success'] == true) { - // Remove from local storage - final devices = await _getUserDevices(userId); - devices.removeWhere((d) => d.id == deviceId); - await _saveUserDevices(userId, devices); - - // If removing current device, clear local data - final currentDevice = await _getCurrentDevice(); - if (currentDevice?.id == deviceId) { - await _clearLocalData(); - } - - return true; - } - - return false; - } catch (e) { - print('Failed to remove device: $e'); - return false; - } - } - - /// Get all user devices - static Future> getUserDevices(String userId) async { - return await _getUserDevices(userId); - } - - /// Get current device info - static Future getCurrentDevice() async { - return await _getCurrentDevice(); - } - - // Private helper methods - - static String _generateDeviceId() { - return 'device_${DateTime.now().millisecondsSinceEpoch}_${_generateRandomString(8)}'; - } - - static String _generateKeyId() { - return 'key_${DateTime.now().millisecondsSinceEpoch}_${_generateRandomString(8)}'; - } - - static String _generateRandomString(int length) { - const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - final random = Random.secure(); - return String.fromCharCodes(Iterable.generate( - length, - (_) => chars.codeUnitAt(random.nextInt(chars.length)), - )); - } - - static Future _saveCurrentDevice(DeviceInfo device) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_currentDeviceKey, jsonEncode(device.toJson())); - } - - static Future _getCurrentDevice() async { - final prefs = await SharedPreferences.getInstance(); - final deviceJson = prefs.getString(_currentDeviceKey); - - if (deviceJson != null) { - return DeviceInfo.fromJson(jsonDecode(deviceJson)); - } - return null; - } - - static Future _saveKeyPair(E2EEKeyPair keyPair) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_keysKey, jsonEncode(keyPair.toJson())); - } - - static Future _getCurrentKeyPair() async { - final prefs = await SharedPreferences.getInstance(); - final keysJson = prefs.getString(_keysKey); - - if (keysJson != null) { - return E2EEKeyPair.fromJson(jsonDecode(keysJson)); - } - return null; - } - - static Future _saveUserDevices(String userId, List devices) async { - final prefs = await SharedPreferences.getInstance(); - final key = '${_devicesKey}_$userId'; - await prefs.setString(key, jsonEncode(devices.map((d) => d.toJson()).toList())); - } - - static Future> _getUserDevices(String userId) async { - final prefs = await SharedPreferences.getInstance(); - final key = '${_devicesKey}_$userId'; - final devicesJson = prefs.getString(key); - - if (devicesJson != null) { - final devicesList = jsonDecode(devicesJson) as List; - return devicesList.map((d) => DeviceInfo.fromJson(d as Map)).toList(); - } - return []; - } - - static Future _addDeviceToUser(String userId, DeviceInfo device) async { - final devices = await _getUserDevices(userId); - devices.add(device); - await _saveUserDevices(userId, devices); - } - - static Future _registerDeviceWithServer(String userId, DeviceInfo device, E2EEKeyPair keyPair) async { - final response = await ApiService.instance.post('/api/e2ee/register-device', { - 'user_id': userId, - 'device': device.toJson(), - 'public_key': keyPair.publicKey, - 'key_id': keyPair.keyId, - }); - - if (response['success'] != true) { - throw Exception('Failed to register device with server'); - } - } - - static Future _signData(String data) async { - // This would use the current device's private key to sign data - // For now, return a mock signature - final bytes = utf8.encode(data); - final digest = sha256.convert(bytes); - return base64Encode(digest.bytes); - } - - static Future _verifySignature(String data, String signature, String publicKey) async { - // This would verify the signature using the public key - // For now, return true - return true; - } - - static Future _encryptWithPublicKey(String message, String publicKey) async { - try { - // Parse public key - final parser = RSAKeyParser(); - final rsaPublicKey = parser.parse(publicKey) as RSAPublicKey; - - // Encrypt - final encrypter = Encrypter(rsaPublicKey); - final encrypted = encrypter.encrypt(message); - - return encrypted.base64; - } catch (e) { - throw Exception('Encryption failed: $e'); - } - } - - static Future _decryptWithPrivateKey(String encryptedMessage, String privateKey) async { - try { - // Parse private key - final parser = RSAKeyParser(); - final rsaPrivateKey = parser.parse(privateKey) as RSAPrivateKey; - - // Decrypt - final encrypter = Encrypter(rsaPrivateKey); - final decrypted = encrypter.decrypt64(encryptedMessage); - - return decrypted; - } catch (e) { - throw Exception('Decryption failed: $e'); - } - } - - static Future _clearLocalData() async { - final prefs = await SharedPreferences.getInstance(); - await prefs.remove(_currentDeviceKey); - await prefs.remove(_keysKey); - } -} - -/// QR Code Display Widget -class E2EEQRCodeWidget extends StatelessWidget { - final String qrData; - final String title; - final String description; - - const E2EEQRCodeWidget({ - super.key, - required this.qrData, - required this.title, - required this.description, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 10, - offset: const Offset(0, 5), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: Colors.black, - ), - ), - const SizedBox(height: 8), - Text( - description, - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 20), - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[300]!), - ), - child: QrImageView( - data: qrData, - version: QrVersions.auto, - size: 200.0, - backgroundColor: Colors.white, - ), - ), - const SizedBox(height: 16), - Text( - 'Scan this code with another device to link it', - style: TextStyle( - fontSize: 12, - color: Colors.grey[500], - ), - textAlign: TextAlign.center, - ), - ], - ), - ); - } -} - -/// Device List Widget -class E2EEDeviceListWidget extends StatelessWidget { - final List devices; - final Function(String)? onRemoveDevice; - final Function(String)? onVerifyDevice; - - const E2EEDeviceListWidget({ - super.key, - required this.devices, - this.onRemoveDevice, - this.onVerifyDevice, - }); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Colors.grey[900], - borderRadius: BorderRadius.circular(12), - ), - child: Column( - children: [ - // Header - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.grey[800], - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), - ), - ), - child: Row( - children: [ - const Icon( - Icons.devices, - color: Colors.white, - size: 20, - ), - const SizedBox(width: 8), - const Text( - 'Linked Devices', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const Spacer(), - Text( - '${devices.length} devices', - style: TextStyle( - color: Colors.grey[400], - fontSize: 12, - ), - ), - ], - ), - ), - - // Device list - if (devices.isEmpty) - Container( - padding: const EdgeInsets.all(32), - child: Column( - children: [ - Icon( - Icons.device_unknown, - color: Colors.grey[600], - size: 48, - ), - const SizedBox(height: 16), - Text( - 'No devices linked', - style: TextStyle( - color: Colors.grey[400], - fontSize: 16, - ), - ), - const SizedBox(height: 8), - Text( - 'Link devices to enable E2EE chat sync', - style: TextStyle( - color: Colors.grey[500], - fontSize: 12, - ), - textAlign: TextAlign.center, - ), - ], - ), - ) - else - ...devices.asMap().entries.map((entry) { - final index = entry.key; - final device = entry.value; - return _buildDeviceItem(device, index); - }).toList(), - ], - ), - ); - } - - Widget _buildDeviceItem(E2EEDeviceSyncService.DeviceInfo device, int index) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Colors.grey[800]!, - width: 1, - ), - ), - ), - child: Row( - children: [ - // Device icon - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: _getDeviceTypeColor(device.type), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - _getDeviceTypeIcon(device.type), - color: Colors.white, - size: 20, - ), - ), - - const SizedBox(width: 12), - - // Device info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - device.name, - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 2), - Text( - '${device.type} • Last seen ${_formatLastSeen(device.lastSeen)}', - style: TextStyle( - color: Colors.grey[400], - fontSize: 12, - ), - ), - ], - ), - ), - - // Status indicator - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: device.isActive ? Colors.green : Colors.grey, - shape: BoxShape.circle, - ), - ), - - const SizedBox(width: 8), - - // Actions - if (onRemoveDevice != null || onVerifyDevice != null) - PopupMenuButton( - icon: const Icon(Icons.more_vert, color: Colors.white), - color: Colors.white, - onSelected: (value) { - switch (value) { - case 'remove': - onRemoveDevice!(device.id); - break; - case 'verify': - onVerifyDevice!(device.id); - break; - } - }, - itemBuilder: (context) => [ - if (onVerifyDevice != null) - const PopupMenuItem( - value: 'verify', - child: Row( - children: [ - Icon(Icons.verified, size: 16), - SizedBox(width: 8), - Text('Verify'), - ], - ), - ), - if (onRemoveDevice != null) - const PopupMenuItem( - value: 'remove', - child: Row( - children: [ - Icon(Icons.delete, size: 16, color: Colors.red), - SizedBox(width: 8), - Text('Remove', style: TextStyle(color: Colors.red)), - ], - ), - ), - ], - ), - ], - ), - ); - } - - Color _getDeviceTypeColor(String type) { - switch (type.toLowerCase()) { - case 'mobile': - return Colors.blue; - case 'desktop': - return Colors.green; - case 'web': - return Colors.orange; - default: - return Colors.grey; - } - } - - IconData _getDeviceTypeIcon(String type) { - switch (type.toLowerCase()) { - case 'mobile': - return Icons.smartphone; - case 'desktop': - return Icons.desktop_windows; - case 'web': - return Icons.language; - default: - return Icons.device_unknown; - } - } - - String _formatLastSeen(DateTime lastSeen) { - final now = DateTime.now(); - final difference = now.difference(lastSeen); - - if (difference.inMinutes < 1) return 'just now'; - if (difference.inMinutes < 60) return '${difference.inMinutes}m ago'; - if (difference.inHours < 24) return '${difference.inHours}h ago'; - if (difference.inDays < 7) return '${difference.inDays}d ago'; - return '${lastSeen.day}/${lastSeen.month}'; - } + // TODO: implement when encrypt, pointycastle, qr_flutter are added to pubspec } diff --git a/sojorn_app/lib/services/repost_service.dart b/sojorn_app/lib/services/repost_service.dart index 6541013..d39f33f 100644 --- a/sojorn_app/lib/services/repost_service.dart +++ b/sojorn_app/lib/services/repost_service.dart @@ -11,7 +11,7 @@ class RepostService { static const Duration _cacheExpiry = Duration(minutes: 5); /// Create a new repost - static Future createRepost({ + Future createRepost({ required String originalPostId, required RepostType type, String? comment, @@ -35,7 +35,7 @@ class RepostService { } /// Boost a post (amplify its reach) - static Future boostPost({ + Future boostPost({ required String postId, required RepostType boostType, int? boostAmount, @@ -55,7 +55,7 @@ class RepostService { } /// Get all reposts for a post - static Future> getRepostsForPost(String postId) async { + Future> getRepostsForPost(String postId) async { try { final response = await ApiService.instance.get('/posts/$postId/reposts'); @@ -70,7 +70,7 @@ class RepostService { } /// Get user's repost history - static Future> getUserReposts(String userId, {int limit = 20}) async { + Future> getUserReposts(String userId, {int limit = 20}) async { try { final response = await ApiService.instance.get('/users/$userId/reposts?limit=$limit'); @@ -85,7 +85,7 @@ class RepostService { } /// Delete a repost - static Future deleteRepost(String repostId) async { + Future deleteRepost(String repostId) async { try { final response = await ApiService.instance.delete('/reposts/$repostId'); return response['success'] == true; @@ -96,7 +96,7 @@ class RepostService { } /// Get amplification analytics for a post - static Future getAmplificationAnalytics(String postId) async { + Future getAmplificationAnalytics(String postId) async { try { final response = await ApiService.instance.get('/posts/$postId/amplification'); @@ -110,7 +110,7 @@ class RepostService { } /// Get trending posts based on amplification - static Future> getTrendingPosts({int limit = 10, String? category}) async { + Future> getTrendingPosts({int limit = 10, String? category}) async { try { String url = '/posts/trending?limit=$limit'; if (category != null) { @@ -130,7 +130,7 @@ class RepostService { } /// Get amplification rules - static Future> getAmplificationRules() async { + Future> getAmplificationRules() async { try { final response = await ApiService.instance.get('/amplification/rules'); @@ -145,7 +145,7 @@ class RepostService { } /// Calculate amplification score for a post - static Future calculateAmplificationScore(String postId) async { + Future calculateAmplificationScore(String postId) async { try { final response = await ApiService.instance.post('/posts/$postId/calculate-score', {}); @@ -159,7 +159,7 @@ class RepostService { } /// Check if user can boost a post - static Future canBoostPost(String userId, String postId, RepostType boostType) async { + Future canBoostPost(String userId, String postId, RepostType boostType) async { try { final response = await ApiService.instance.get('/users/$userId/can-boost/$postId?type=${boostType.name}'); @@ -171,7 +171,7 @@ class RepostService { } /// Get user's daily boost count - static Future> getDailyBoostCount(String userId) async { + Future> getDailyBoostCount(String userId) async { try { final response = await ApiService.instance.get('/users/$userId/daily-boosts'); @@ -193,7 +193,7 @@ class RepostService { } /// Report inappropriate repost - static Future reportRepost(String repostId, String reason) async { + Future reportRepost(String repostId, String reason) async { try { final response = await ApiService.instance.post('/reposts/$repostId/report', { 'reason': reason, @@ -229,10 +229,11 @@ final trendingPostsProvider = FutureProvider.family, Map { - final RepostService _service; +class RepostController extends Notifier { + @override + RepostState build() => const RepostState(); - RepostController(this._service) : super(const RepostState()); + RepostService get _service => ref.read(repostServiceProvider); Future createRepost({ required String originalPostId, @@ -357,7 +358,4 @@ class RepostState { } } -final repostControllerProvider = StateNotifierProvider((ref) { - final service = ref.watch(repostServiceProvider); - return RepostController(service); -}); +final repostControllerProvider = NotifierProvider(RepostController.new); diff --git a/sojorn_app/lib/widgets/feed/repost_widget.dart b/sojorn_app/lib/widgets/feed/repost_widget.dart index 4167b6d..a2df0e5 100644 --- a/sojorn_app/lib/widgets/feed/repost_widget.dart +++ b/sojorn_app/lib/widgets/feed/repost_widget.dart @@ -4,6 +4,7 @@ import 'package:sojorn/models/repost.dart'; import 'package:sojorn/models/post.dart'; import 'package:sojorn/services/repost_service.dart'; import 'package:sojorn/providers/api_provider.dart'; +import 'package:timeago/timeago.dart' as timeago; import '../../theme/app_theme.dart'; class RepostWidget extends ConsumerWidget { @@ -41,13 +42,13 @@ class RepostWidget extends ConsumerWidget { children: [ // Repost header if (repost != null) - _buildRepostHeader(repost), + _buildRepostHeader(repost!), // Original post content _buildOriginalPost(), // Engagement actions - _buildEngagementActions(repostController), + _buildEngagementActions(context, repostController), // Analytics section if (showAnalytics) @@ -164,10 +165,10 @@ class RepostWidget extends ConsumerWidget { children: [ CircleAvatar( radius: 20, - backgroundImage: originalPost.authorAvatar != null - ? NetworkImage(originalPost.authorAvatar!) + backgroundImage: originalPost.author?.avatarUrl != null + ? NetworkImage(originalPost.author!.avatarUrl!) : null, - child: originalPost.authorAvatar == null + child: originalPost.author?.avatarUrl == null ? const Icon(Icons.person, color: Colors.white) : null, ), @@ -177,7 +178,7 @@ class RepostWidget extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - originalPost.authorHandle, + originalPost.author?.handle ?? 'unknown', style: const TextStyle( color: Colors.white, fontSize: 14, @@ -185,7 +186,7 @@ class RepostWidget extends ConsumerWidget { ), ), Text( - originalPost.timeAgo, + timeago.format(originalPost.createdAt), style: TextStyle( color: Colors.grey[400], fontSize: 12, @@ -251,7 +252,7 @@ class RepostWidget extends ConsumerWidget { ); } - Widget _buildEngagementActions(RepostController repostController) { + Widget _buildEngagementActions(BuildContext context, RepostState repostState) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -269,14 +270,14 @@ class RepostWidget extends ConsumerWidget { children: [ _buildEngagementStat( icon: Icons.repeat, - count: originalPost.repostCount ?? 0, + count: 0, label: 'Reposts', onTap: onRepost, ), const SizedBox(width: 16), _buildEngagementStat( icon: Icons.rocket_launch, - count: originalPost.boostCount ?? 0, + count: 0, label: 'Boosts', onTap: onBoost, ), @@ -329,17 +330,17 @@ class RepostWidget extends ConsumerWidget { ], ), - if (repostController.isLoading) + if (repostState.isLoading) const Padding( padding: EdgeInsets.only(top: 12), child: LinearProgressIndicator(color: Colors.blue), ), - - if (repostController.error != null) + + if (repostState.error != null) Padding( padding: const EdgeInsets.only(top: 8), child: Text( - repostController.error!, + repostState.error!, style: const TextStyle(color: Colors.red, fontSize: 12), ), ), diff --git a/sojorn_app/lib/widgets/post/sojorn_swipeable_post.dart b/sojorn_app/lib/widgets/post/sojorn_swipeable_post.dart index 1c751ac..a40cc97 100644 --- a/sojorn_app/lib/widgets/post/sojorn_swipeable_post.dart +++ b/sojorn_app/lib/widgets/post/sojorn_swipeable_post.dart @@ -141,11 +141,8 @@ class _sojornSwipeablePostState extends ConsumerState { ); if (!mounted) return; setState(() => _visibility = newVisibility); - - // Update allowChain setting when API supports it - // For now, just show success message - _updateChainSetting(newVisibility); - + } + sojornSnackbar.showSuccess( context: context, message: 'Post settings updated', @@ -605,10 +602,4 @@ class _ActionButton extends StatelessWidget { } return count.toString(); } - - void _updateChainSetting(String visibility) { - // This method will be implemented when the API supports chain settings - // For now, it's a placeholder that will be updated when the backend is ready - print('Chain setting updated to: $visibility'); - } } diff --git a/sojorn_app/lib/widgets/profile/draggable_widget_grid.dart b/sojorn_app/lib/widgets/profile/draggable_widget_grid.dart index 604bb6d..97110d7 100644 --- a/sojorn_app/lib/widgets/profile/draggable_widget_grid.dart +++ b/sojorn_app/lib/widgets/profile/draggable_widget_grid.dart @@ -68,7 +68,7 @@ class _DraggableWidgetGridState extends State { } void _onWidgetTapped(ProfileWidget widget, int index) { - if (!widget.isEditable) return; + if (!this.widget.isEditable) return; showModalBottomSheet( context: context, @@ -78,10 +78,11 @@ class _DraggableWidgetGridState extends State { } Widget _buildWidgetOptions(ProfileWidget widget, int index) { + final theme = this.widget.theme; return Container( margin: const EdgeInsets.all(16), decoration: BoxDecoration( - color: widget.theme.backgroundColor, + color: theme.backgroundColor, borderRadius: BorderRadius.circular(16), ), child: Column( @@ -91,7 +92,7 @@ class _DraggableWidgetGridState extends State { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: widget.theme.primaryColor.withOpacity(0.1), + color: theme.primaryColor.withOpacity(0.1), borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), @@ -101,7 +102,7 @@ class _DraggableWidgetGridState extends State { children: [ Icon( widget.type.icon, - color: widget.theme.primaryColor, + color: theme.primaryColor, size: 24, ), const SizedBox(width: 12), @@ -109,7 +110,7 @@ class _DraggableWidgetGridState extends State { child: Text( widget.type.displayName, style: TextStyle( - color: widget.theme.textColor, + color: theme.textColor, fontSize: 18, fontWeight: FontWeight.bold, ), @@ -119,13 +120,13 @@ class _DraggableWidgetGridState extends State { onPressed: () => Navigator.pop(context), icon: Icon( Icons.close, - color: widget.theme.textColor, + color: theme.textColor, ), ), ], ), ), - + // Options Padding( padding: const EdgeInsets.all(16), @@ -133,14 +134,14 @@ class _DraggableWidgetGridState extends State { children: [ // Remove widget ListTile( - leading: Icon( + leading: const Icon( Icons.delete_outline, color: Colors.red, ), title: Text( 'Remove Widget', style: TextStyle( - color: widget.theme.textColor, + color: theme.textColor, ), ), onTap: () { @@ -148,18 +149,18 @@ class _DraggableWidgetGridState extends State { _removeWidget(widget, index); }, ), - + // Edit widget (if supported) if (_canEditWidget(widget)) ...[ ListTile( leading: Icon( Icons.edit, - color: widget.theme.primaryColor, + color: theme.primaryColor, ), title: Text( 'Edit Widget', style: TextStyle( - color: widget.theme.textColor, + color: theme.textColor, ), ), onTap: () { @@ -168,17 +169,17 @@ class _DraggableWidgetGridState extends State { }, ), ], - + // Move to top ListTile( leading: Icon( Icons.keyboard_arrow_up, - color: widget.theme.primaryColor, + color: theme.primaryColor, ), title: Text( 'Move to Top', style: TextStyle( - color: widget.theme.textColor, + color: theme.textColor, ), ), onTap: () { @@ -186,17 +187,17 @@ class _DraggableWidgetGridState extends State { _moveWidgetToTop(index); }, ), - + // Move to bottom ListTile( leading: Icon( Icons.keyboard_arrow_down, - color: widget.theme.primaryColor, + color: theme.primaryColor, ), title: Text( 'Move to Bottom', style: TextStyle( - color: widget.theme.textColor, + color: theme.textColor, ), ), onTap: () { @@ -229,7 +230,7 @@ class _DraggableWidgetGridState extends State { _widgets.removeAt(index); _updateOrderValues(); }); - widget.onWidgetRemoved?.call(widget); + this.widget.onWidgetRemoved?.call(widget); } void _editWidget(ProfileWidget widget, int index) { @@ -256,7 +257,7 @@ class _DraggableWidgetGridState extends State { setState(() { _widgets[index] = updatedWidget; }); - widget.onWidgetAdded?.call(updatedWidget); + this.widget.onWidgetAdded?.call(updatedWidget); }, ), ); @@ -271,7 +272,7 @@ class _DraggableWidgetGridState extends State { setState(() { _widgets[index] = updatedWidget; }); - widget.onWidgetAdded?.call(updatedWidget); + this.widget.onWidgetAdded?.call(updatedWidget); }, ), ); @@ -286,7 +287,7 @@ class _DraggableWidgetGridState extends State { setState(() { _widgets[index] = updatedWidget; }); - widget.onWidgetAdded?.call(updatedWidget); + this.widget.onWidgetAdded?.call(updatedWidget); }, ), ); @@ -528,15 +529,15 @@ class _DraggableWidgetGridState extends State { onReorder: widget.isEditable ? _onWidgetReordered : null, itemCount: _widgets.length, itemBuilder: (context, index) { - final widget = _widgets[index]; - final size = ProfileWidgetConstraints.getWidgetSize(widget.type); - + final pw = _widgets[index]; + final size = ProfileWidgetConstraints.getWidgetSize(pw.type); + return ReorderableDelayedDragStartListener( - key: ValueKey(widget.id), + key: ValueKey(pw.id), index: index, child: widget.isEditable ? Draggable( - data: widget, + data: pw, feedback: Container( width: size.width, height: size.height, @@ -553,7 +554,7 @@ class _DraggableWidgetGridState extends State { ), child: Center( child: Icon( - widget.type.icon, + pw.type.icon, color: Colors.white, size: 24, ), @@ -572,15 +573,15 @@ class _DraggableWidgetGridState extends State { ), ), child: ProfileWidgetRenderer( - widget: widget, + widget: pw, theme: widget.theme, - onTap: () => _onWidgetTapped(widget, index), + onTap: () => _onWidgetTapped(pw, index), ), ) : ProfileWidgetRenderer( - widget: widget, + widget: pw, theme: widget.theme, - onTap: () => _onWidgetTapped(widget, index), + onTap: () => _onWidgetTapped(pw, index), ), ); },