fix: Resolve Flutter compile errors across 15 files
- Fix GroupCategory enum duplication (group.dart re-exports from cluster.dart) - Fix stitchVideos arg count (use stitchVideosLegacy in quip_recorder) - Fix enhanced_quip_recorder: getter/field conflict, XFile→File, static calls, CameraLensDirection, setState syntax - Fix blocking_service: imports, ShareXFiles, SnackBar content, nullable bool - Fix audio_overlay_service: path_provider import, getOutput() stub, fade icons - Fix enhanced_beacon_detail_screen: flutter_map v8 interactionOptions, const constraints, Uri.parse - Fix private_capsule_screen: add api_provider import - Stub e2ee_device_sync_service (packages not in pubspec) - Add generic post()/delete() helpers to api_service - Fix repost_service: remove static, migrate StateNotifier→Notifier (Riverpod 3.x) - Fix repost_widget: Repost? null assertion, RepostState type, Post field names, timeago, context param - Fix sojorn_swipeable_post: close if-block, remove undefined _updateChainSetting - Fix draggable_widget_grid: use this.widget for isEditable/theme/callbacks when ProfileWidget param shadows Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
91ff0dc060
commit
0753fd91e6
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class _EnhancedBeaconDetailScreenState extends State<EnhancedBeaconDetailScreen>
|
|||
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<EnhancedBeaconDetailScreen>
|
|||
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<EnhancedBeaconDetailScreen>
|
|||
],
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: Colors.grey[400],
|
||||
size: 16,
|
||||
|
|
@ -778,7 +778,7 @@ class _EnhancedBeaconDetailScreenState extends State<EnhancedBeaconDetailScreen>
|
|||
}
|
||||
|
||||
void _callEmergency() async {
|
||||
const url = 'tel:911';
|
||||
final url = Uri.parse('tel:911');
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ class _FeedsojornScreenState extends ConsumerState<FeedsojornScreen> {
|
|||
}
|
||||
|
||||
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<FeedsojornScreen> {
|
|||
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<FeedsojornScreen> {
|
|||
adIndices.isEmpty
|
||||
? 'ad positions: none'
|
||||
: 'ad positions: ${adIndices.join(', ')}',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
color: SojornColors.basicWhite.withValues(alpha: 0.7),
|
||||
fontSize: 11,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
|
|||
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<EnhancedQuipRecorderScreen>
|
|||
|
||||
// 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<EnhancedQuipRecorderScreen>
|
|||
|
||||
// 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<EnhancedQuipRecorderScreen>
|
|||
|
||||
try {
|
||||
await _cameraController!.resumeVideoRecording();
|
||||
setState(() => {
|
||||
setState(() {
|
||||
_isPaused = false;
|
||||
_segmentStartTime = DateTime.now();
|
||||
_currentSegmentDuration = Duration.zero;
|
||||
|
|
@ -231,15 +229,13 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
|
|||
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<EnhancedQuipRecorderScreen>
|
|||
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<EnhancedQuipRecorderScreen>
|
|||
_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<EnhancedQuipRecorderScreen>
|
|||
|
||||
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<EnhancedQuipRecorderScreen>
|
|||
_recordedSegments.clear();
|
||||
_segmentDurations.clear();
|
||||
_currentSegmentDuration = Duration.zero;
|
||||
_totalRecordedDuration = Duration.zero;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -481,7 +475,7 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
|
|||
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<Color>(
|
||||
_isPaused ? Colors.orange : Colors.red,
|
||||
|
|
@ -495,7 +489,7 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
|
|||
children: [
|
||||
// Duration
|
||||
Text(
|
||||
_formatDuration(get _totalRecordedDuration),
|
||||
_formatDuration(_totalRecordedDuration),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
|
|
@ -732,29 +726,28 @@ class _EnhancedQuipRecorderScreenState extends State<EnhancedQuipRecorderScreen>
|
|||
// 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ class _QuipRecorderScreenState extends State<QuipRecorderScreen>
|
|||
if (_recordedSegments.length == 1) {
|
||||
finalFile = _recordedSegments.first;
|
||||
} else {
|
||||
finalFile = await VideoStitchingService.stitchVideos(_recordedSegments);
|
||||
finalFile = await VideoStitchingService.stitchVideosLegacy(_recordedSegments);
|
||||
}
|
||||
|
||||
if (finalFile != null && mounted) {
|
||||
|
|
|
|||
|
|
@ -176,6 +176,16 @@ class ApiService {
|
|||
return _callGoApi(path, method: 'GET', queryParams: queryParams);
|
||||
}
|
||||
|
||||
/// Simple POST request helper
|
||||
Future<Map<String, dynamic>> post(String path, Map<String, dynamic> body) async {
|
||||
return _callGoApi(path, method: 'POST', body: body);
|
||||
}
|
||||
|
||||
/// Simple DELETE request helper
|
||||
Future<Map<String, dynamic>> delete(String path) async {
|
||||
return _callGoApi(path, method: 'DELETE');
|
||||
}
|
||||
|
||||
|
||||
Future<void> resendVerificationEmail(String email) async {
|
||||
await _callGoApi('/auth/resend-verification',
|
||||
|
|
|
|||
|
|
@ -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<AudioOverlayControls> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.fade_in,
|
||||
Icons.volume_up,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
|
|
@ -475,7 +476,7 @@ class _AudioOverlayControlsState extends State<AudioOverlayControls> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.fade_out,
|
||||
Icons.volume_down,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<BlockManagementScreen> {
|
|||
// 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<dynamic>;
|
||||
|
|
@ -299,10 +302,10 @@ class _BlockManagementScreenState extends State<BlockManagementScreen> {
|
|||
Future<void> _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<BlockManagementScreen> {
|
|||
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<BlockManagementScreen> {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
user: const Text('Cancel'),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -412,17 +415,17 @@ class _BlockManagementScreenState extends State<BlockManagementScreen> {
|
|||
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<BlockManagementScreen> {
|
|||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: 'Export failed: $e',
|
||||
content: Text('Export failed: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<String, dynamic>? 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
|
||||
return QRVerificationData(
|
||||
deviceId: json['device_id'] ?? '',
|
||||
publicKey: json['public_key'] ?? '',
|
||||
timestamp: json['timestamp'] ?? '',
|
||||
signature: json['signature'] ?? '',
|
||||
userId: json['user_id'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> 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<E2EEKeyPair> 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<DeviceInfo> registerDevice({
|
||||
required String userId,
|
||||
required String deviceName,
|
||||
required String deviceType,
|
||||
Map<String, dynamic>? 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<QRVerificationData> 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<bool> 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<bool> 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<dynamic>?)
|
||||
?.map((d) => DeviceInfo.fromJson(d as Map<String, dynamic>))
|
||||
.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<String> 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<String> 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<bool> 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<List<DeviceInfo>> getUserDevices(String userId) async {
|
||||
return await _getUserDevices(userId);
|
||||
}
|
||||
|
||||
/// Get current device info
|
||||
static Future<DeviceInfo?> 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<void> _saveCurrentDevice(DeviceInfo device) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_currentDeviceKey, jsonEncode(device.toJson()));
|
||||
}
|
||||
|
||||
static Future<DeviceInfo?> _getCurrentDevice() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final deviceJson = prefs.getString(_currentDeviceKey);
|
||||
|
||||
if (deviceJson != null) {
|
||||
return DeviceInfo.fromJson(jsonDecode(deviceJson));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<void> _saveKeyPair(E2EEKeyPair keyPair) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_keysKey, jsonEncode(keyPair.toJson()));
|
||||
}
|
||||
|
||||
static Future<E2EEKeyPair?> _getCurrentKeyPair() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final keysJson = prefs.getString(_keysKey);
|
||||
|
||||
if (keysJson != null) {
|
||||
return E2EEKeyPair.fromJson(jsonDecode(keysJson));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<void> _saveUserDevices(String userId, List<DeviceInfo> devices) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final key = '${_devicesKey}_$userId';
|
||||
await prefs.setString(key, jsonEncode(devices.map((d) => d.toJson()).toList()));
|
||||
}
|
||||
|
||||
static Future<List<DeviceInfo>> _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<dynamic>;
|
||||
return devicesList.map((d) => DeviceInfo.fromJson(d as Map<String, dynamic>)).toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
static Future<void> _addDeviceToUser(String userId, DeviceInfo device) async {
|
||||
final devices = await _getUserDevices(userId);
|
||||
devices.add(device);
|
||||
await _saveUserDevices(userId, devices);
|
||||
}
|
||||
|
||||
static Future<void> _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<String> _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<bool> _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<String> _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<String> _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<void> _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<E2EEDeviceSyncService.DeviceInfo> 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<String>(
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class RepostService {
|
|||
static const Duration _cacheExpiry = Duration(minutes: 5);
|
||||
|
||||
/// Create a new repost
|
||||
static Future<Repost?> createRepost({
|
||||
Future<Repost?> createRepost({
|
||||
required String originalPostId,
|
||||
required RepostType type,
|
||||
String? comment,
|
||||
|
|
@ -35,7 +35,7 @@ class RepostService {
|
|||
}
|
||||
|
||||
/// Boost a post (amplify its reach)
|
||||
static Future<bool> boostPost({
|
||||
Future<bool> boostPost({
|
||||
required String postId,
|
||||
required RepostType boostType,
|
||||
int? boostAmount,
|
||||
|
|
@ -55,7 +55,7 @@ class RepostService {
|
|||
}
|
||||
|
||||
/// Get all reposts for a post
|
||||
static Future<List<Repost>> getRepostsForPost(String postId) async {
|
||||
Future<List<Repost>> 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<List<Repost>> getUserReposts(String userId, {int limit = 20}) async {
|
||||
Future<List<Repost>> 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<bool> deleteRepost(String repostId) async {
|
||||
Future<bool> 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<AmplificationAnalytics?> getAmplificationAnalytics(String postId) async {
|
||||
Future<AmplificationAnalytics?> 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<List<Post>> getTrendingPosts({int limit = 10, String? category}) async {
|
||||
Future<List<Post>> 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<List<FeedAmplificationRule>> getAmplificationRules() async {
|
||||
Future<List<FeedAmplificationRule>> 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<int> calculateAmplificationScore(String postId) async {
|
||||
Future<int> 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<bool> canBoostPost(String userId, String postId, RepostType boostType) async {
|
||||
Future<bool> 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<Map<RepostType, int>> getDailyBoostCount(String userId) async {
|
||||
Future<Map<RepostType, int>> 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<bool> reportRepost(String repostId, String reason) async {
|
||||
Future<bool> 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<List<Post>, Map<String, dyna
|
|||
return service.getTrendingPosts(limit: limit, category: category);
|
||||
});
|
||||
|
||||
class RepostController extends StateNotifier<RepostState> {
|
||||
final RepostService _service;
|
||||
class RepostController extends Notifier<RepostState> {
|
||||
@override
|
||||
RepostState build() => const RepostState();
|
||||
|
||||
RepostController(this._service) : super(const RepostState());
|
||||
RepostService get _service => ref.read(repostServiceProvider);
|
||||
|
||||
Future<void> createRepost({
|
||||
required String originalPostId,
|
||||
|
|
@ -357,7 +358,4 @@ class RepostState {
|
|||
}
|
||||
}
|
||||
|
||||
final repostControllerProvider = StateNotifierProvider<RepostController, RepostState>((ref) {
|
||||
final service = ref.watch(repostServiceProvider);
|
||||
return RepostController(service);
|
||||
});
|
||||
final repostControllerProvider = NotifierProvider<RepostController, RepostState>(RepostController.new);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -141,11 +141,8 @@ class _sojornSwipeablePostState extends ConsumerState<sojornSwipeablePost> {
|
|||
);
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
|
|||
}
|
||||
|
||||
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<DraggableWidgetGrid> {
|
|||
}
|
||||
|
||||
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<DraggableWidgetGrid> {
|
|||
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<DraggableWidgetGrid> {
|
|||
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<DraggableWidgetGrid> {
|
|||
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<DraggableWidgetGrid> {
|
|||
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<DraggableWidgetGrid> {
|
|||
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<DraggableWidgetGrid> {
|
|||
_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<DraggableWidgetGrid> {
|
|||
},
|
||||
),
|
||||
],
|
||||
|
||||
|
||||
// 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<DraggableWidgetGrid> {
|
|||
_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<DraggableWidgetGrid> {
|
|||
_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<DraggableWidgetGrid> {
|
|||
setState(() {
|
||||
_widgets[index] = updatedWidget;
|
||||
});
|
||||
widget.onWidgetAdded?.call(updatedWidget);
|
||||
this.widget.onWidgetAdded?.call(updatedWidget);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -271,7 +272,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
|
|||
setState(() {
|
||||
_widgets[index] = updatedWidget;
|
||||
});
|
||||
widget.onWidgetAdded?.call(updatedWidget);
|
||||
this.widget.onWidgetAdded?.call(updatedWidget);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -286,7 +287,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
|
|||
setState(() {
|
||||
_widgets[index] = updatedWidget;
|
||||
});
|
||||
widget.onWidgetAdded?.call(updatedWidget);
|
||||
this.widget.onWidgetAdded?.call(updatedWidget);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -528,15 +529,15 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
|
|||
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<ProfileWidget>(
|
||||
data: widget,
|
||||
data: pw,
|
||||
feedback: Container(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
|
|
@ -553,7 +554,7 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
|
|||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
widget.type.icon,
|
||||
pw.type.icon,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
|
|
@ -572,15 +573,15 @@ class _DraggableWidgetGridState extends State<DraggableWidgetGrid> {
|
|||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue