🎨 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,22 @@
String getAbuseReportTypeString(int type) {
switch (type) {
case 0:
return 'Copyright';
case 1:
return 'Harassment';
case 2:
return 'Impersonation';
case 3:
return 'Offensive Content';
case 4:
return 'Spam';
case 5:
return 'Privacy Violation';
case 6:
return 'Illegal Content';
case 7:
return 'Other';
default:
return 'Unknown';
}
}

View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:island/accounts/accounts_models/account.dart';
String? getActivityTitle(String? label, Map<String, dynamic>? meta) {
if (meta == null) return label;
if (meta['assets']?['large_text'] is String) {
return meta['assets']?['large_text'];
}
return label;
}
String? getActivitySubtitle(Map<String, dynamic>? meta) {
if (meta == null) return null;
if (meta['assets']?['small_text'] is String) {
return meta['assets']?['small_text'];
}
return null;
}
InlineSpan getActivityFullMessage(SnAccountStatus? status) {
if (status?.meta == null) return TextSpan(text: 'No activity details available');
final meta = status!.meta!;
final List<InlineSpan> spans = [];
if (meta.containsKey('assets') && meta['assets'] is Map) {
final assets = meta['assets'] as Map<String, dynamic>;
if (assets.containsKey('large_text')) {
spans.add(TextSpan(text: assets['large_text'], style: TextStyle(fontWeight: FontWeight.bold)));
}
if (assets.containsKey('small_text')) {
if (spans.isNotEmpty) spans.add(TextSpan(text: '\n'));
spans.add(TextSpan(text: assets['small_text']));
}
}
String normalText = '';
if (meta.containsKey('details')) {
normalText += 'Details: ${meta['details']}\n';
}
if (meta.containsKey('state')) {
normalText += 'State: ${meta['state']}\n';
}
if (meta.containsKey('timestamps') && meta['timestamps'] is Map) {
final ts = meta['timestamps'] as Map<String, dynamic>;
if (ts.containsKey('start') && ts['start'] is int) {
final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000);
normalText += 'Started: ${start.toLocal()}\n';
}
if (ts.containsKey('end') && ts['end'] is int) {
final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000);
normalText += 'Ends: ${end.toLocal()}\n';
}
}
if (meta.containsKey('party') && meta['party'] is Map) {
final party = meta['party'] as Map<String, dynamic>;
if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) {
final size = party['size'] as List;
normalText += 'Party: ${size[0]}/${size[1]}\n';
}
}
if (meta.containsKey('instance')) {
normalText += 'Instance: ${meta['instance']}\n';
}
// Add other keys if present
meta.forEach((key, value) {
if (!['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(key)) {
normalText += '$key: $value\n';
}
});
if (normalText.isNotEmpty) {
if (spans.isNotEmpty) spans.add(TextSpan(text: '\n'));
spans.add(TextSpan(text: normalText.trimRight()));
}
return TextSpan(children: spans);
}
Widget buildActivityDetails(SnAccountStatus? status) {
if (status?.meta == null) return Text('No activity details available');
final meta = status!.meta!;
final List<Widget> children = [];
if (meta.containsKey('assets') && meta['assets'] is Map) {
final assets = meta['assets'] as Map<String, dynamic>;
if (assets.containsKey('large_text')) {
children.add(Text(assets['large_text']));
}
if (assets.containsKey('small_text')) {
children.add(Text(assets['small_text']));
}
}
if (meta.containsKey('details')) {
children.add(Text('Details: ${meta['details']}'));
}
if (meta.containsKey('state')) {
children.add(Text('State: ${meta['state']}'));
}
if (meta.containsKey('timestamps') && meta['timestamps'] is Map) {
final ts = meta['timestamps'] as Map<String, dynamic>;
if (ts.containsKey('start') && ts['start'] is int) {
final start = DateTime.fromMillisecondsSinceEpoch(ts['start'] * 1000);
children.add(Text('Started: ${start.toLocal()}'));
}
if (ts.containsKey('end') && ts['end'] is int) {
final end = DateTime.fromMillisecondsSinceEpoch(ts['end'] * 1000);
children.add(Text('Ends: ${end.toLocal()}'));
}
}
if (meta.containsKey('party') && meta['party'] is Map) {
final party = meta['party'] as Map<String, dynamic>;
if (party.containsKey('size') && party['size'] is List && party['size'].length >= 2) {
final size = party['size'] as List;
children.add(Text('Party: ${size[0]}/${size[1]}'));
}
}
if (meta.containsKey('instance')) {
children.add(Text('Instance: ${meta['instance']}'));
}
// Add other keys if present
children.addAll(meta.entries.where((e) => !['details', 'state', 'timestamps', 'assets', 'party', 'secrets', 'instance'].contains(e.key)).map((e) => Text('${e.key}: ${e.value}')));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: children,
);
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:island/drive/drive_models/file.dart';
import 'package:island/drive/drive_widgets/cloud_files.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
/// Returns an appropriate icon widget for the given file based on its MIME type
Widget getFileIcon(
SnCloudFile file, {
required double size,
bool tinyPreview = true,
}) {
final itemType = file.mimeType?.split('/').firstOrNull;
final mimeType = file.mimeType ?? '';
final extension = file.name.split('.').lastOrNull?.toLowerCase() ?? '';
// For images, show the actual image thumbnail
if (itemType == 'image' && tinyPreview) {
return CloudImageWidget(file: file);
}
// Return icon based on MIME type or file extension
final icon = switch ((itemType, mimeType, extension)) {
('image', _, _) => Symbols.image,
('audio', _, _) => Symbols.audio_file,
('video', _, _) => Symbols.video_file,
('application', 'application/pdf', _) => Symbols.picture_as_pdf,
('application', 'application/zip', _) => Symbols.archive,
('application', 'application/x-rar-compressed', _) => Symbols.archive,
(
'application',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
_,
) ||
('application', 'application/msword', _) => Symbols.description,
(
'application',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
_,
) ||
('application', 'application/vnd.ms-excel', _) => Symbols.table_chart,
(
'application',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
_,
) ||
('application', 'application/vnd.ms-powerpoint', _) => Symbols.slideshow,
('text', _, _) => Symbols.article,
('application', _, 'js') ||
('application', _, 'dart') ||
('application', _, 'py') ||
('application', _, 'java') ||
('application', _, 'cpp') ||
('application', _, 'c') ||
('application', _, 'cs') => Symbols.code,
('application', _, 'json') ||
('application', _, 'xml') => Symbols.data_object,
(_, _, 'md') => Symbols.article,
(_, _, 'html') => Symbols.web,
(_, _, 'css') => Symbols.css,
_ => Symbols.description, // Default icon
};
return Icon(icon, size: size, fill: 1).center();
}

View File

@@ -0,0 +1,15 @@
String formatFileSize(int bytes) {
if (bytes <= 0) return '0 B';
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
}
if (bytes < 1024 * 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
}
if (bytes < 1024 * 1024 * 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024 * 1024 * 1024)).toStringAsFixed(2)} TB';
}
return '${(bytes / (1024 * 1024 * 1024 * 1024 * 1024)).toStringAsFixed(2)} PB';
}

View File

@@ -0,0 +1,30 @@
String _upperCamelToLowerSnake(String input) {
final regex = RegExp(r'(?<=[a-z0-9])([A-Z])');
return input
.replaceAllMapped(regex, (match) => '_${match.group(0)}')
.toLowerCase();
}
Map<String, dynamic> convertMapKeysToSnakeCase(Map<String, dynamic> input) {
final result = <String, dynamic>{};
input.forEach((key, value) {
final newKey = _upperCamelToLowerSnake(key);
if (value is Map<String, dynamic>) {
result[newKey] = convertMapKeysToSnakeCase(value);
} else if (value is List) {
result[newKey] =
value.map((item) {
if (item is Map<String, dynamic>) {
return convertMapKeysToSnakeCase(item);
}
return item;
}).toList();
} else {
result[newKey] = value;
}
});
return result;
}

View File

@@ -0,0 +1,71 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/posts/posts_models/post.dart';
import 'package:island/core/config.dart';
import 'package:island/shared/widgets/alert.dart';
import 'package:island/posts/posts_widgets/post/post_item_screenshot.dart';
import 'package:island/posts/posts_widgets/post/post_shared.dart';
import 'package:path_provider/path_provider.dart' show getTemporaryDirectory;
import 'package:screenshot/screenshot.dart';
import 'package:share_plus/share_plus.dart';
import 'package:island/core/services/analytics_service.dart';
/// Shares a post as a screenshot image
Future<void> sharePostAsScreenshot(
BuildContext context,
WidgetRef ref,
SnPost post,
) async {
if (kIsWeb) return;
final screenshotController = ScreenshotController();
showLoadingModal(context);
await screenshotController
.captureFromWidget(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(
ref.watch(sharedPreferencesProvider),
),
repliesProvider(
post.id,
).overrideWithValue(ref.watch(repliesProvider(post.id))),
],
child: Directionality(
textDirection: TextDirection.ltr,
child: SizedBox(
width: 520,
child: PostItemScreenshot(item: post, isFullPost: true),
),
),
),
context: context,
pixelRatio: MediaQuery.of(context).devicePixelRatio,
delay: const Duration(seconds: 1),
)
.then((Uint8List? image) async {
if (image == null) return;
final directory = await getTemporaryDirectory();
final imagePath = await File('${directory.path}/image.png').create();
await imagePath.writeAsBytes(image);
if (!context.mounted) return;
hideLoadingModal(context);
final box = context.findRenderObject() as RenderBox?;
await Share.shareXFiles([
XFile(imagePath.path),
], sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
})
.catchError((err) {
if (context.mounted) hideLoadingModal(context);
showErrorAlert(err);
})
.whenComplete(() {
final postTypeStr = post.type == 0 ? 'regular' : 'article';
AnalyticsService().logPostShared(post.id, 'screenshot', postTypeStr);
});
}

14
lib/core/utils/text.dart Normal file
View File

@@ -0,0 +1,14 @@
extension StringExtension on String {
String capitalizeEachWord() {
if (isEmpty) return this;
return split(' ')
.map(
(word) =>
word.isNotEmpty
? '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}'
: '',
)
.join(' ');
}
}