Compare commits

...

4 Commits

Author SHA1 Message Date
01fa228e45 ♻️ Refactor markdown sticker rendering 2025-10-26 03:04:02 +08:00
1e71ad33a6 💄 Optimize UX 2025-10-26 03:00:09 +08:00
92c0260ecd 👽 Remove outdated field in sticker 2025-10-26 03:00:01 +08:00
0a161ad255 🐛 Dozens of bug fixes 2025-10-26 02:49:23 +08:00
11 changed files with 138 additions and 73 deletions

View File

@@ -10,7 +10,6 @@ sealed class SnSticker with _$SnSticker {
const factory SnSticker({
required String id,
required String slug,
required String imageId,
required SnCloudFile image,
required String packId,
required SnStickerPack? pack,

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnSticker {
String get id; String get slug; String get imageId; SnCloudFile get image; String get packId; SnStickerPack? get pack; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String get slug; SnCloudFile get image; String get packId; SnStickerPack? get pack; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $SnStickerCopyWith<SnSticker> get copyWith => _$SnStickerCopyWithImpl<SnSticker>
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSticker&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.imageId, imageId) || other.imageId == imageId)&&(identical(other.image, image) || other.image == image)&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.pack, pack) || other.pack == pack)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSticker&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.image, image) || other.image == image)&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.pack, pack) || other.pack == pack)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,imageId,image,packId,pack,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,slug,image,packId,pack,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnSticker(id: $id, slug: $slug, imageId: $imageId, image: $image, packId: $packId, pack: $pack, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnSticker(id: $id, slug: $slug, image: $image, packId: $packId, pack: $pack, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -48,7 +48,7 @@ abstract mixin class $SnStickerCopyWith<$Res> {
factory $SnStickerCopyWith(SnSticker value, $Res Function(SnSticker) _then) = _$SnStickerCopyWithImpl;
@useResult
$Res call({
String id, String slug, String imageId, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String slug, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -65,11 +65,10 @@ class _$SnStickerCopyWithImpl<$Res>
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? imageId = null,Object? image = null,Object? packId = null,Object? pack = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? image = null,Object? packId = null,Object? pack = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,imageId: null == imageId ? _self.imageId : imageId // ignore: cast_nullable_to_non_nullable
as String,image: null == image ? _self.image : image // ignore: cast_nullable_to_non_nullable
as SnCloudFile,packId: null == packId ? _self.packId : packId // ignore: cast_nullable_to_non_nullable
as String,pack: freezed == pack ? _self.pack : pack // ignore: cast_nullable_to_non_nullable
@@ -179,10 +178,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, String imageId, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String slug, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnSticker() when $default != null:
return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.slug,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
@@ -200,10 +199,10 @@ return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, String imageId, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String slug, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnSticker():
return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);}
return $default(_that.id,_that.slug,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -217,10 +216,10 @@ return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, String imageId, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String slug, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnSticker() when $default != null:
return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.slug,_that.image,_that.packId,_that.pack,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
@@ -232,12 +231,11 @@ return $default(_that.id,_that.slug,_that.imageId,_that.image,_that.packId,_that
@JsonSerializable()
class _SnSticker implements SnSticker {
const _SnSticker({required this.id, required this.slug, required this.imageId, required this.image, required this.packId, required this.pack, required this.createdAt, required this.updatedAt, required this.deletedAt});
const _SnSticker({required this.id, required this.slug, required this.image, required this.packId, required this.pack, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnSticker.fromJson(Map<String, dynamic> json) => _$SnStickerFromJson(json);
@override final String id;
@override final String slug;
@override final String imageId;
@override final SnCloudFile image;
@override final String packId;
@override final SnStickerPack? pack;
@@ -258,16 +256,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSticker&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.imageId, imageId) || other.imageId == imageId)&&(identical(other.image, image) || other.image == image)&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.pack, pack) || other.pack == pack)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSticker&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.image, image) || other.image == image)&&(identical(other.packId, packId) || other.packId == packId)&&(identical(other.pack, pack) || other.pack == pack)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,imageId,image,packId,pack,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,slug,image,packId,pack,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnSticker(id: $id, slug: $slug, imageId: $imageId, image: $image, packId: $packId, pack: $pack, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnSticker(id: $id, slug: $slug, image: $image, packId: $packId, pack: $pack, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -278,7 +276,7 @@ abstract mixin class _$SnStickerCopyWith<$Res> implements $SnStickerCopyWith<$Re
factory _$SnStickerCopyWith(_SnSticker value, $Res Function(_SnSticker) _then) = __$SnStickerCopyWithImpl;
@override @useResult
$Res call({
String id, String slug, String imageId, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String slug, SnCloudFile image, String packId, SnStickerPack? pack, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -295,11 +293,10 @@ class __$SnStickerCopyWithImpl<$Res>
/// Create a copy of SnSticker
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? imageId = null,Object? image = null,Object? packId = null,Object? pack = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? image = null,Object? packId = null,Object? pack = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnSticker(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,imageId: null == imageId ? _self.imageId : imageId // ignore: cast_nullable_to_non_nullable
as String,image: null == image ? _self.image : image // ignore: cast_nullable_to_non_nullable
as SnCloudFile,packId: null == packId ? _self.packId : packId // ignore: cast_nullable_to_non_nullable
as String,pack: freezed == pack ? _self.pack : pack // ignore: cast_nullable_to_non_nullable

View File

@@ -9,7 +9,6 @@ part of 'sticker.dart';
_SnSticker _$SnStickerFromJson(Map<String, dynamic> json) => _SnSticker(
id: json['id'] as String,
slug: json['slug'] as String,
imageId: json['image_id'] as String,
image: SnCloudFile.fromJson(json['image'] as Map<String, dynamic>),
packId: json['pack_id'] as String,
pack:
@@ -28,7 +27,6 @@ Map<String, dynamic> _$SnStickerToJson(_SnSticker instance) =>
<String, dynamic>{
'id': instance.id,
'slug': instance.slug,
'image_id': instance.imageId,
'image': instance.image.toJson(),
'pack_id': instance.packId,
'pack': instance.pack?.toJson(),

View File

@@ -238,10 +238,17 @@ class ChatListBodyWidget extends HookConsumerWidget {
room: item,
isDirect: item.type == 1,
onTap: () {
context.pushNamed(
'chatRoom',
pathParameters: {'id': item.id},
);
if (isWideScreen(context)) {
context.replaceNamed(
'chatRoom',
pathParameters: {'id': item.id},
);
} else {
context.pushNamed(
'chatRoom',
pathParameters: {'id': item.id},
);
}
},
);
},

View File

@@ -198,7 +198,7 @@ class StickerPackDetailContent extends HookConsumerWidget {
),
),
child: CloudImageWidget(
fileId: sticker.imageId,
fileId: sticker.image.id,
fit: BoxFit.contain,
),
),
@@ -318,7 +318,7 @@ class StickerForm extends HookConsumerWidget {
final formKey = useMemoized(() => GlobalKey<FormState>(), []);
final image = useState<String?>(id == null ? '' : sticker.value?.imageId);
final image = useState<String?>(id == null ? '' : sticker.value?.image.id);
final imageController = useTextEditingController(text: image.value);
final slugController = useTextEditingController(
text: id == null ? '' : sticker.value?.slug,
@@ -326,8 +326,8 @@ class StickerForm extends HookConsumerWidget {
useEffect(() {
if (sticker.value != null) {
image.value = sticker.value!.imageId;
imageController.text = sticker.value!.imageId;
image.value = sticker.value!.image.id;
imageController.text = sticker.value!.image.id;
slugController.text = sticker.value!.slug;
}
return null;

View File

@@ -168,7 +168,7 @@ class MarketplaceStickerPackDetailScreen extends HookConsumerWidget {
child: AspectRatio(
aspectRatio: 1,
child: CloudImageWidget(
fileId: sticker.imageId,
fileId: sticker.image.id,
fit: BoxFit.contain,
),
),

View File

@@ -613,7 +613,7 @@ class ChatInput extends HookConsumerWidget {
width: 28,
height: 28,
child: CloudImageWidget(
fileId: sticker.imageId,
fileId: sticker.image.id,
),
),
);

View File

@@ -47,7 +47,6 @@ class MarkdownTextContent extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final baseUrl = ref.watch(serverUrlProvider);
final doesEnlargeSticker = useMemoized(() {
// Check if content only contains one sticker by matching the sticker pattern
final stickerPattern = RegExp(stickerRegex);
@@ -88,6 +87,14 @@ class MarkdownTextContent extends HookConsumerWidget {
onToggle: () => spoilerRevealed.value = !spoilerRevealed.value,
);
final baseUrl = ref.watch(serverUrlProvider);
final stickerGenerator = StickerGenerator(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
isEnlarged: doesEnlargeSticker,
baseUrl: baseUrl,
);
return MarkdownBlock(
data: content,
selectable: isSelectable,
@@ -186,28 +193,6 @@ class MarkdownTextContent extends HookConsumerWidget {
).clipRRect(all: 8),
),
);
case 'stickers':
final size = doesEnlargeSticker ? 96.0 : 24.0;
final stickerUri =
'$baseUrl/sphere/stickers/lookup/${uri.pathSegments[0]}/open';
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(
Radius.circular(8),
),
),
child: UniversalImage(
uri: stickerUri,
width: size,
height: size,
fit: BoxFit.contain,
noCacheOptimization: true,
),
),
);
}
}
final content = ClipRRect(
@@ -228,7 +213,12 @@ class MarkdownTextContent extends HookConsumerWidget {
generator: MarkdownTextContent.buildGenerator(
isDark: isDark,
linesMargin: linesMargin,
generators: [mentionGenerator, highlightGenerator, spoilerGenerator],
generators: [
mentionGenerator,
highlightGenerator,
spoilerGenerator,
stickerGenerator,
],
),
);
}
@@ -284,9 +274,8 @@ class _StickerInlineSyntax extends markdown.InlineSyntax {
@override
bool onMatch(markdown.InlineParser parser, Match match) {
final placeholder = match[1]!;
final image = markdown.Element.text('img', '')
..attributes['src'] = Uri.encodeFull('solian://stickers/$placeholder');
parser.addNode(image);
final element = markdown.Element('sticker', [markdown.Text(placeholder)]);
parser.addNode(element);
return true;
}
@@ -559,3 +548,68 @@ class SpoilerSpanNode extends SpanNode {
);
}
}
class StickerGenerator extends SpanNodeGeneratorWithTag {
StickerGenerator({
required Color backgroundColor,
required Color foregroundColor,
required bool isEnlarged,
required String baseUrl,
}) : super(
tag: 'sticker',
generator: (
markdown.Element element,
MarkdownConfig config,
WidgetVisitor visitor,
) {
return StickerSpanNode(
placeholder: element.textContent,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
isEnlarged: isEnlarged,
baseUrl: baseUrl,
);
},
);
}
class StickerSpanNode extends SpanNode {
final String placeholder;
final Color backgroundColor;
final Color foregroundColor;
final bool isEnlarged;
final String baseUrl;
StickerSpanNode({
required this.placeholder,
required this.backgroundColor,
required this.foregroundColor,
required this.isEnlarged,
required this.baseUrl,
});
@override
InlineSpan build() {
final size = isEnlarged ? 96.0 : 24.0;
final stickerUri = '$baseUrl/sphere/stickers/lookup/$placeholder/open';
return WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Container(
decoration: BoxDecoration(
color: backgroundColor.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: UniversalImage(
uri: stickerUri,
width: size,
height: size,
fit: BoxFit.contain,
noCacheOptimization: true,
),
),
),
);
}
}

View File

@@ -202,7 +202,7 @@ class ComposeFormFields extends HookConsumerWidget {
child: SizedBox(
width: 28,
height: 28,
child: CloudImageWidget(fileId: sticker.imageId),
child: CloudImageWidget(fileId: sticker.image.id),
),
);
break;

View File

@@ -580,7 +580,8 @@ class PostHeader extends StatelessWidget {
children: [
Flexible(
child:
item.publisher.account != null
(item.publisher.account != null &&
item.publisher.type == 0)
? AccountName(
account: item.publisher.account!,
textOverride: item.publisher.nick,
@@ -592,7 +593,10 @@ class PostHeader extends StatelessWidget {
overflow: TextOverflow.ellipsis,
).bold(),
),
if (item.publisher.verification != null)
if ((item.publisher.account?.profile.verification !=
null &&
item.publisher.type == 0) &&
item.publisher.verification != null)
VerificationMark(mark: item.publisher.verification!),
if (item.realm == null)
Flexible(
@@ -752,15 +756,21 @@ class PostBody extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Symbols.edit, size: 16),
Text(
'editedAt'.tr(
args: [
Tooltip(
message:
!isFullPost && isRelativeTime
? item.editedAt!.formatRelative(context)
: item.editedAt!.formatSystem(),
],
),
).fontSize(13),
? item.editedAt!.formatSystem()
: item.editedAt!.formatRelative(context),
child: Text(
'editedAt'.tr(
args: [
!isFullPost && isRelativeTime
? item.editedAt!.formatRelative(context)
: item.editedAt!.formatSystem(),
],
),
).fontSize(13),
),
],
),
);

View File

@@ -263,7 +263,7 @@ class _StickersGrid extends StatelessWidget {
child: AspectRatio(
aspectRatio: 1,
child: CloudImageWidget(
fileId: sticker.imageId,
fileId: sticker.image.id,
fit: BoxFit.contain,
),
),