🎨 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 @@
export 'user_list_item.dart';

View File

@@ -0,0 +1,163 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:island/core/models/activitypub.dart';
import 'package:material_symbols_icons/symbols.dart';
class ApActorListItem extends StatelessWidget {
final SnActivityPubActor actor;
final bool isFollowing;
final bool isLoading;
final VoidCallback? onFollow;
final VoidCallback? onUnfollow;
final VoidCallback? onTap;
const ApActorListItem({
super.key,
required this.actor,
this.isFollowing = false,
this.isLoading = false,
this.onFollow,
this.onUnfollow,
this.onTap,
});
String _getDisplayName() {
if (actor.displayName?.isNotEmpty ?? false) {
return actor.displayName!;
}
if (actor.username?.isNotEmpty ?? false) {
return actor.username!;
}
return actor.id.split('@').lastOrNull ?? 'Unknown';
}
String _getUsername() {
if (actor.username?.isNotEmpty ?? false) {
return '${actor.username}@${actor.instance.domain}';
}
return actor.id;
}
String _getInstanceDomain() {
final parts = actor.id.split('@');
if (parts.length >= 3) {
return parts[2];
}
return '';
}
bool _isLocal() {
// For now, assume all searched actors are remote
// This could be determined by checking if the domain matches local instance
return false;
}
@override
Widget build(BuildContext context) {
final displayName = _getDisplayName();
final username = _getUsername();
final instanceDomain = _getInstanceDomain();
final isLocal = _isLocal();
return ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 12),
leading: Stack(
children: [
CircleAvatar(
backgroundImage: actor.avatarUrl != null
? CachedNetworkImageProvider(actor.avatarUrl!)
: null,
radius: 24,
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
child: actor.avatarUrl == null
? Icon(
Symbols.person,
color: Theme.of(context).colorScheme.onSurfaceVariant,
)
: null,
),
if (!isLocal)
Positioned(
right: 0,
bottom: 0,
child: CircleAvatar(
backgroundImage: actor.instance.iconUrl != null
? CachedNetworkImageProvider(actor.instance.iconUrl!)
: null,
radius: 8,
backgroundColor: Theme.of(context).colorScheme.primary,
child: actor.instance.iconUrl == null
? Icon(
Symbols.public,
size: 12,
color: Theme.of(context).colorScheme.onPrimary,
)
: null,
),
),
],
),
title: Row(
children: [
Flexible(child: Text(displayName)),
if (!isLocal && instanceDomain.isNotEmpty) const SizedBox(width: 6),
if (!isLocal && instanceDomain.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Text(
instanceDomain,
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
),
],
),
subtitle: Row(
spacing: 8,
children: [
Text(username),
if (actor.summary?.isNotEmpty ?? false)
Expanded(
child: Text(
actor.summary!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
),
if (actor.type.isNotEmpty) Text(actor.type),
],
),
trailing: isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: isFollowing
? OutlinedButton(
onPressed: onUnfollow,
style: OutlinedButton.styleFrom(
minimumSize: const Size(88, 36),
padding: const EdgeInsets.symmetric(horizontal: 12),
),
child: const Text('Unfollow'),
)
: FilledButton(
onPressed: onFollow,
style: FilledButton.styleFrom(
minimumSize: const Size(88, 36),
padding: const EdgeInsets.symmetric(horizontal: 12),
),
child: const Text('Follow'),
),
onTap: onTap,
);
}
}

View File

@@ -0,0 +1,60 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:island/core/models/activitypub.dart';
import 'package:material_symbols_icons/symbols.dart';
class ActorPictureWidget extends StatelessWidget {
final SnActivityPubActor actor;
final double radius;
const ActorPictureWidget({super.key, required this.actor, this.radius = 16});
@override
Widget build(BuildContext context) {
final avatarUrl = actor.avatarUrl;
if (avatarUrl == null) {
return CircleAvatar(
radius: radius,
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
child: Icon(
Symbols.person,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
);
}
return Stack(
children: [
CircleAvatar(
backgroundImage: CachedNetworkImageProvider(avatarUrl),
radius: radius,
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
child: avatarUrl.isNotEmpty
? null
: Icon(
Symbols.person,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Positioned(
right: 0,
bottom: 0,
child: CircleAvatar(
backgroundImage: actor.instance.iconUrl != null
? CachedNetworkImageProvider(actor.instance.iconUrl!)
: null,
radius: radius * 0.4,
backgroundColor: Theme.of(context).colorScheme.primary,
child: actor.instance.iconUrl == null
? Icon(
Symbols.public,
size: radius * 0.6,
color: Theme.of(context).colorScheme.onPrimary,
)
: null,
),
),
],
);
}
}

View File

@@ -0,0 +1,128 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:island/core/models/activitypub.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:relative_time/relative_time.dart';
class ActivityPubUserListItem extends StatelessWidget {
final SnActivityPubUser user;
final bool isFollowing;
final bool isLoading;
final VoidCallback? onFollow;
final VoidCallback? onUnfollow;
final VoidCallback? onTap;
const ActivityPubUserListItem({
super.key,
required this.user,
this.isFollowing = false,
this.isLoading = false,
this.onFollow,
this.onUnfollow,
this.onTap,
});
@override
Widget build(BuildContext context) {
return ListTile(
contentPadding: const EdgeInsets.only(left: 16, right: 12),
leading: Stack(
children: [
CircleAvatar(
backgroundImage: CachedNetworkImageProvider(user.avatarUrl),
radius: 24,
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
),
if (!user.isLocal)
Positioned(
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
shape: BoxShape.circle,
),
child: Icon(
Symbols.public,
size: 12,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
),
title: Row(
children: [
Flexible(child: Text(user.displayName)),
if (!user.isLocal) const SizedBox(width: 6),
if (!user.isLocal)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Text(
user.instanceDomain,
style: TextStyle(
fontSize: 10,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('@${user.username}'),
if (user.bio.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
user.bio,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'Followed ${RelativeTime(context).format(user.followedAt)}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
trailing: isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
)
: isFollowing
? OutlinedButton(
onPressed: onUnfollow,
style: OutlinedButton.styleFrom(
minimumSize: const Size(88, 36),
padding: const EdgeInsets.symmetric(horizontal: 12),
),
child: const Text('Unfollow'),
)
: FilledButton(
onPressed: onFollow,
style: FilledButton.styleFrom(
minimumSize: const Size(88, 36),
padding: const EdgeInsets.symmetric(horizontal: 12),
),
child: const Text('Follow'),
),
onTap: onTap,
);
}
}