🎨 Use feature based folder structure

This commit is contained in:
2026-02-06 00:37:02 +08:00
parent 62a3ea26e3
commit 862e3b451b
539 changed files with 8406 additions and 5056 deletions

View File

@@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/accounts/accounts_models/abuse_report.dart';
import 'package:island/accounts/accounts_models/abuse_report_type.dart';
import 'package:island/accounts/abuse_report_service.dart';
import 'package:island/shared/widgets/app_scaffold.dart';
import 'package:styled_widget/styled_widget.dart';
class AbuseReportDetailScreen extends ConsumerStatefulWidget {
final String reportId;
const AbuseReportDetailScreen({super.key, required this.reportId});
@override
ConsumerState<AbuseReportDetailScreen> createState() =>
_AbuseReportDetailScreenState();
}
class _AbuseReportDetailScreenState
extends ConsumerState<AbuseReportDetailScreen> {
Future<SnAbuseReport>? _reportFuture;
@override
void initState() {
super.initState();
_reportFuture = ref
.read(abuseReportServiceProvider)
.getReport(widget.reportId);
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: const Text('Abuse Report Details')),
body: FutureBuilder<SnAbuseReport>(
future: _reportFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
final report = snapshot.data!;
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow(context, 'Report ID', report.id),
_buildDetailRow(
context,
'Resource Identifier',
report.resourceIdentifier,
),
_buildDetailRow(
context,
'Type',
AbuseReportType.fromValue(report.type).displayName,
),
_buildDetailRow(context, 'Reason', report.reason),
_buildDetailRow(
context,
'Resolved At',
report.resolvedAt?.toString() ?? 'N/A',
),
_buildDetailRow(
context,
'Resolution',
report.resolution ?? 'N/A',
),
_buildDetailRow(context, 'Account ID', report.accountId),
_buildDetailRow(
context,
'Created At',
report.createdAt.toString(),
),
_buildDetailRow(
context,
'Updated At',
report.updatedAt.toString(),
),
],
),
);
} else {
return const Center(child: Text('No data'));
}
},
),
);
}
Widget _buildDetailRow(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: Theme.of(context).textTheme.titleMedium).bold(),
Text(value, style: Theme.of(context).textTheme.bodyLarge),
],
),
);
}
}

View File

@@ -0,0 +1,154 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/accounts/accounts_models/abuse_report.dart';
import 'package:island/accounts/accounts_models/abuse_report_type.dart';
import 'package:island/accounts/abuse_report_service.dart';
import 'package:island/core/services/time.dart';
import 'package:island/reports/reports_widgets/safety/abuse_report_helper.dart';
import 'package:island/shared/widgets/app_scaffold.dart';
class AbuseReportListScreen extends ConsumerStatefulWidget {
const AbuseReportListScreen({super.key});
@override
ConsumerState<AbuseReportListScreen> createState() =>
_AbuseReportListScreenState();
}
class _AbuseReportListScreenState extends ConsumerState<AbuseReportListScreen> {
Future<List<SnAbuseReport>>? _reportsFuture;
@override
void initState() {
super.initState();
_reportsFuture = ref.read(abuseReportServiceProvider).getReports();
}
@override
Widget build(BuildContext context) {
return AppScaffold(
appBar: AppBar(title: Text('abuseReports').tr()),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
showAbuseReportSheet(context, resourceIdentifier: 'unidentified');
},
),
body: FutureBuilder<List<SnAbuseReport>>(
future: _reportsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.hasData) {
final reports = snapshot.data!;
return ListView.builder(
padding: EdgeInsets.zero,
itemCount: reports.length,
itemBuilder: (context, index) {
final report = reports[index];
return Card(
elevation: 2,
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: InkWell(
onTap: () {
context.pushNamed(
'reportDetail',
pathParameters: {'id': report.id},
);
},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
report.reason,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'ID',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
report.id,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Type',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
AbuseReportType.fromValue(
report.type,
).displayName,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Created at',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
'${report.createdAt.formatRelative(context)} · ${report.createdAt.formatSystem()}',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Status',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
report.resolvedAt != null
? 'Resolved'
: 'Unresolved',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: report.resolvedAt != null
? Colors.green
: Colors.orange,
),
),
],
),
],
),
),
),
);
},
);
} else {
return const Center(child: Text('No data'));
}
},
),
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:island/reports/reports_widgets/safety/abuse_report_sheet.dart';
/// Helper function to show the safety report sheet
///
/// [context] - The build context
/// [resourceIdentifier] - The identifier of the resource being reported (e.g., post ID, user ID, etc.)
/// [initialReason] - Optional initial reason text to pre-fill the form
Future<void> showAbuseReportSheet(
BuildContext context, {
required String resourceIdentifier,
String? initialReason,
}) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => AbuseReportSheet(
resourceIdentifier: resourceIdentifier,
initialReason: initialReason,
),
);
}

View File

@@ -0,0 +1,182 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/core/network.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:island/core/widgets/content/sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
class AbuseReportSheet extends HookConsumerWidget {
final String resourceIdentifier;
final String? initialReason;
const AbuseReportSheet({
super.key,
required this.resourceIdentifier,
this.initialReason,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final reasonController = useTextEditingController(
text: initialReason ?? '',
);
final selectedType = useState<int>(0);
final isSubmitting = useState<bool>(false);
final reportTypes = [
{'value': 0, 'label': 'abuseReportTypeCopyright'.tr()},
{'value': 1, 'label': 'abuseReportTypeHarassment'.tr()},
{'value': 2, 'label': 'abuseReportTypeImpersonation'.tr()},
{'value': 3, 'label': 'abuseReportTypeOffensiveContent'.tr()},
{'value': 4, 'label': 'abuseReportTypeSpam'.tr()},
{'value': 5, 'label': 'abuseReportTypePrivacyViolation'.tr()},
{'value': 6, 'label': 'abuseReportTypeIllegalContent'.tr()},
{'value': 7, 'label': 'abuseReportTypeOther'.tr()},
];
Future<void> submitReport() async {
isSubmitting.value = true;
try {
final client = ref.read(apiClientProvider);
await client.post(
'/pass/safety/reports',
data: {
'resource_identifier': resourceIdentifier,
'type': selectedType.value,
'reason': reasonController.text.trim(),
},
);
if (context.mounted) {
Navigator.of(context).pop();
showDialog(
context: context,
builder: (contextDialog) => AlertDialog(
icon: const Icon(
Icons.check_circle,
color: Colors.green,
size: 36,
),
title: Text('abuseReportSuccessTitle'.tr()),
content: Text('abuseReportSuccess'.tr()),
actions: [
TextButton(
onPressed: () {
Navigator.of(contextDialog).pop();
Navigator.of(context).pop();
},
child: const Text('OK'),
),
],
),
);
}
} catch (err) {
showErrorAlert(err);
} finally {
isSubmitting.value = false;
}
}
return SheetScaffold(
titleText: 'abuseReportTitle'.tr(),
child: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Information text
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Symbols.info,
size: 20,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const Gap(8),
Expanded(
child: Text(
'abuseReportDescription'.tr(),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
),
const Gap(24),
// Report type selection
Text(
'abuseReportType'.tr(),
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
),
const Gap(12),
...reportTypes.map((type) {
return RadioListTile<int>(
value: type['value'] as int,
groupValue: selectedType.value,
onChanged: (value) => selectedType.value = value!,
title: Text(type['label'] as String),
contentPadding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
);
}),
const Gap(24),
// Reason text field
Text(
'abuseReportReason'.tr(),
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
),
const Gap(8),
TextField(
controller: reasonController,
maxLines: 4,
decoration: InputDecoration(
hintText: 'abuseReportReasonHint'.tr(),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
),
),
const Gap(24),
// Submit button
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: isSubmitting.value ? null : submitReport,
child: isSubmitting.value
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text('abuseReportSubmit'.tr()),
),
),
const Gap(16),
],
),
),
);
}
}