sojorn/sojorn_app/lib/services/blocking_service.dart
Patrick Britton 56a9dd032f feat: Add enhanced video moderation with frame extraction and implement placeholder UI methods
- Add VideoProcessor service to PostHandler for frame-based video moderation
- Implement multi-frame extraction and Azure OpenAI Vision analysis for video content
- Enhance VideoStitchingService with filters, speed control, and text overlays
- Add image upload dialogs for group avatar and banner in GroupCreationModal
- Implement navigation placeholders for mentions, hashtags, and URLs in sojornRichText
2026-02-17 13:32:58 -06:00

656 lines
20 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:file_picker/file_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:share_plus/share_plus.dart';
class BlockingService {
static const String _blockedUsersKey = 'blocked_users';
static const String _blockedUsersJsonKey = 'blocked_users_json';
static const String _blockedUsersCsvKey = 'blocked_users_csv';
/// Export blocked users to JSON file
static Future<bool> exportBlockedUsersToJson(List<String> blockedUserIds) async {
try {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/blocked_users_${DateTime.now().millisecondsSinceEpoch}.json');
final exportData = {
'exported_at': DateTime.now().toIso8601String(),
'version': '2.0',
'platform': 'sojorn',
'total_blocked': blockedUserIds.length,
'blocked_users': blockedUserIds.map((id) => {
'user_id': id,
'blocked_at': DateTime.now().toIso8601String(),
}).toList(),
};
await file.writeAsString(const JsonEncoder.withIndent(' ').convert(exportData));
// Share the file
final result = await Share.shareXFiles([file.path]);
return result.status == ShareResultStatus.done;
} catch (e) {
print('Error exporting blocked users to JSON: $e');
return false;
}
}
/// Export blocked users to CSV file
static Future<bool> exportBlockedUsersToCsv(List<String> blockedUserIds) async {
try {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/blocked_users_${DateTime.now().millisecondsSinceEpoch}.csv');
final csvContent = StringBuffer();
csvContent.writeln('user_id,blocked_at');
for (final userId in blockedUserIds) {
csvContent.writeln('$userId,${DateTime.now().toIso8601String()}');
}
await file.writeAsString(csvContent.toString());
// Share the file
final result = await Share.shareXFiles([file.path]);
return result.status == ShareResultStatus.done;
} catch (e) {
print('Error exporting blocked users to CSV: $e');
return false;
}
}
/// Import blocked users from JSON file
static Future<List<String>> importBlockedUsersFromJson() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
allowMultiple: false,
);
if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
final content = await file.readAsString();
final data = jsonDecode(content) as Map<String, dynamic>;
if (data['blocked_users'] != null) {
final blockedUsers = (data['blocked_users'] as List<dynamic>)
.map((user) => user['user_id'] as String)
.toList();
return blockedUsers;
}
}
} catch (e) {
print('Error importing blocked users from JSON: $e');
}
return [];
}
/// Import blocked users from CSV file
static Future<List<String>> importBlockedUsersFromCsv() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['csv'],
allowMultiple: false,
);
if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
final lines = await file.readAsLines();
if (lines.isNotEmpty) {
// Skip header line
final blockedUsers = lines.skip(1)
.where((line) => line.isNotEmpty)
.map((line) => line.split(',')[0].trim())
.toList();
return blockedUsers;
}
}
} catch (e) {
print('Error importing blocked users from CSV: $e');
}
return [];
}
/// Import from Twitter/X format
static Future<List<String>> importFromTwitterX() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['csv'],
allowMultiple: false,
);
if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
final lines = await file.readAsLines();
if (lines.isNotEmpty) {
// Twitter/X CSV format: screen_name, name, description, following, followers, tweets, account_created_at
final blockedUsers = lines.skip(1)
.where((line) => line.isNotEmpty)
.map((line) => line.split(',')[0].trim()) // screen_name
.toList();
return blockedUsers;
}
}
} catch (e) {
print('Error importing from Twitter/X: $e');
}
return [];
}
/// Import from Mastodon format
static Future<List<String>> importFromMastodon() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['csv'],
allowMultiple: false,
);
if (result != null && result.files.single.path != null) {
final file = File(result.files.single.path!);
final lines = await file.readAsLines();
if (lines.isNotEmpty) {
// Mastodon CSV format: account_id, username, display_name, domain, note, created_at
final blockedUsers = lines.skip(1)
.where((line) => line.isNotEmpty)
.map((line) => line.split(',')[1].trim()) // username
.toList();
return blockedUsers;
}
}
} catch (e) {
print('Error importing from Mastodon: $e');
}
return [];
}
/// Get supported platform formats
static List<PlatformFormat> getSupportedFormats() {
return [
PlatformFormat(
name: 'Sojorn JSON',
description: 'Native Sojorn format with full metadata',
extension: 'json',
importFunction: importBlockedUsersFromJson,
exportFunction: exportBlockedUsersToJson,
),
PlatformFormat(
name: 'CSV',
description: 'Universal CSV format',
extension: 'csv',
importFunction: importBlockedUsersFromCsv,
exportFunction: exportBlockedUsersToCsv,
),
PlatformFormat(
name: 'Twitter/X',
description: 'Twitter/X export format',
extension: 'csv',
importFunction: importFromTwitterX,
exportFunction: null, // Export not supported for Twitter/X
),
PlatformFormat(
name: 'Mastodon',
description: 'Mastodon export format',
extension: 'csv',
importFunction: importFromMastodon,
exportFunction: null, // Export not supported for Mastodon
),
];
}
/// Validate blocked users list
static Future<List<String>> validateBlockedUsers(List<String> blockedUserIds) async {
final validUsers = <String>[];
for (final userId in blockedUserIds) {
if (userId.isNotEmpty && userId.length <= 50) { // Basic validation
validUsers.add(userId);
}
}
return validUsers;
}
/// Get import/export statistics
static Map<String, dynamic> getStatistics(List<String> blockedUserIds) {
return {
'total_blocked': blockedUserIds.length,
'export_formats_available': getSupportedFormats().length,
'last_updated': DateTime.now().toIso8601String(),
'platforms_supported': ['Twitter/X', 'Mendation', 'CSV', 'JSON'],
};
}
}
class PlatformFormat {
final String name;
final String description;
final String extension;
final Future<List<String>> Function()? importFunction;
final Future<bool>? Function(List<String>)? exportFunction;
PlatformFormat({
required this.name,
required this.description,
required this.extension,
this.importFunction,
this.exportFunction,
});
}
class BlockManagementScreen extends StatefulWidget {
const BlockManagementScreen({super.key});
@override
State<BlockManagementScreen> createState() => _BlockManagementScreenState();
}
class _BlockManagementScreenState extends State<BlockManagementScreen> {
List<String> _blockedUsers = [];
bool _isLoading = false;
String? _errorMessage;
@override
void initState() {
super.initState();
_loadBlockedUsers();
}
Future<void> _loadBlockedUsers() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
// 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);
if (blockedUsersJson != null) {
final blockedUsersList = jsonDecode(blockedUsersJson) as List<dynamic>;
_blockedUsers = blockedUsersList.cast<String>();
}
} catch (e) {
setState(() {
_errorMessage = 'Failed to load blocked users';
});
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _saveBlockedUsers() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_blockedUsersJsonKey, jsonEncode(_blockedUsers));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: 'Failed to save blocked users'),
);
}
}
void _showImportDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Import Block List'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Choose the format of your block list:'),
const SizedBox(height: 16),
...BlockingService.getSupportedFormats().map((format) => ListTile(
leading: Icon(
format.importFunction != null ? Icons.file_download : Icons.file_upload,
color: format.importFunction != null ? Colors.green : Colors.grey,
),
title: Text(format.name),
subtitle: Text(format.description),
trailing: format.importFunction != null
? const Icon(Icons.arrow_forward_ios, color: Colors.grey)
: null,
onTap: format.importFunction != null
? () => _importFromFormat(format)
: null,
)).toList(),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
],
),
);
}
Future<void> _importFromFormat(PlatformFormat format) async {
Navigator.pop(context);
setState(() => _isLoading = true);
try {
final importedUsers = await format.importFunction!();
final validatedUsers = await BlockingService.validateBlockedUsers(importedUsers);
setState(() {
_blockedUsers = {..._blockedUsers, ...validatedUsers}.toSet().toList()};
});
await _saveBlockedUsers();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: 'Successfully imported ${validatedUsers.length} users',
backgroundColor: Colors.green,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: 'Failed to import: $e',
backgroundColor: Colors.red,
),
);
} finally {
setState(() => _isLoading = false);
}
}
void _showExportDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Export Block List'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Choose export format:'),
const SizedBox(height: 16),
...BlockingService.getSupportedFormats().where((format) => format.exportFunction != null).map((format) => ListTile(
leading: Icon(Icons.file_upload, color: Colors.blue),
title: Text(format.name),
subtitle: Text(format.description),
onTap: () => _exportToFormat(format),
)).toList(),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
user: const Text('Cancel'),
),
],
),
);
}
Future<void> _exportToFormat(PlatformFormat format) async {
Navigator.pop(context);
setState(() => _isLoading = true);
try {
final success = await format.exportFunction!(_blockedUsers);
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: 'Successfully exported ${_blockedUsers.length} users',
backgroundColor: Colors.green,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: 'Export cancelled or failed',
backgroundColor: Colors.orange,
),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: 'Export failed: $e',
backgroundColor: Colors.red,
),
);
} finally {
setState(() => _isLoading = false);
}
}
void _showBulkBlockDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Bulk Block'),
content: const Text('Enter usernames to block (one per line):'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_showBulkBlockInput();
},
child: const Text('Next'),
),
],
),
);
}
void _showBulkBlockInput() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Bulk Block'),
content: TextField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'user1\nuser2\nuser3',
),
maxLines: 10,
onChanged: (value) {
// This would typically validate usernames
},
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
// Process bulk block here
},
child: const Text('Block Users'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
title: const Text(
'Block Management',
style: TextStyle(color: Colors.white),
),
iconTheme: const IconThemeData(color: Colors.white),
actions: [
IconButton(
onPressed: _showImportDialog,
icon: const Icon(Icons.file_download, color: Colors.white),
tooltip: 'Import',
),
IconButton(
onPressed: _showExportDialog,
icon: const Icon(Icons.file_upload, color: Colors.white),
tooltip: 'Export',
),
IconButton(
onPressed: _showBulkBlockDialog,
icon: const Icon(Icons.group_add, color: Colors.white),
tooltip: 'Bulk Block',
),
],
),
body: _isLoading
? const Center(
child: CircularProgressIndicator(color: Colors.white),
)
: _errorMessage != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 48,
),
const SizedBox(height: 16),
Text(
_errorMessage!,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
],
),
)
: _blockedUsers.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.block,
color: Colors.grey,
size: 48,
),
const SizedBox(height: 16),
const Text(
'No blocked users',
style: TextStyle(
color: Colors.grey,
fontSize: 18,
),
),
const SizedBox(height: 8),
const Text(
'Import an existing block list or start blocking users',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
textAlign: TextAlign.center,
),
],
),
)
: Column(
children: [
// Statistics
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Statistics',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'Total Blocked: ${_blockedUsers.length}',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
'Last Updated: ${DateTime.now().toIso8601String()}',
style: TextStyle(
color: Colors.grey[400],
fontSize: 12,
),
),
],
),
),
// Blocked users list
Expanded(
child: ListView.builder(
itemCount: _blockedUsers.length,
itemBuilder: (context, index) {
final userId = _blockedUsers[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: Colors.grey[700],
child: const Icon(
Icons.person,
color: Colors.white,
),
),
title: Text(
userId,
style: const TextStyle(color: Colors.white),
),
trailing: IconButton(
icon: const Icon(Icons.close, color: Colors.red),
onPressed: () {
setState(() {
_blockedUsers.removeAt(index);
});
_saveBlockedUsers();
},
),
);
},
),
),
],
),
);
}
}