Introduce cuite reaction

This commit is contained in:
2025-09-23 23:39:58 +08:00
parent 866674ddde
commit 4293daaa2f
7 changed files with 167 additions and 80 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

View File

@@ -31,6 +31,29 @@ import 'package:share_plus/share_plus.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:super_context_menu/super_context_menu.dart'; import 'package:super_context_menu/super_context_menu.dart';
const kAvailableStickers = {'angry', 'clap', 'confuse', 'pray', 'thumb_up'};
bool _getReactionImageAvailable(String symbol) {
return kAvailableStickers.contains(symbol);
}
Widget _buildReactionIcon(String symbol, double size, {double iconSize = 24}) {
if (_getReactionImageAvailable(symbol)) {
return Image.asset(
'assets/images/stickers/$symbol.png',
width: size,
height: size,
fit: BoxFit.contain,
alignment: Alignment.bottomCenter,
);
} else {
return Text(
kReactionTemplates[symbol]?.icon ?? '',
style: TextStyle(fontSize: iconSize),
);
}
}
class PostActionableItem extends HookConsumerWidget { class PostActionableItem extends HookConsumerWidget {
final SnPost item; final SnPost item;
final EdgeInsets? padding; final EdgeInsets? padding;
@@ -490,57 +513,61 @@ class PostItem extends HookConsumerWidget {
trailing: trailing:
isCompact isCompact
? null ? null
: IconButton( : SizedBox(
icon: width: 36,
mostReaction == null height: 36,
? const Icon(Symbols.add_reaction) child: IconButton(
: Badge( icon:
label: Center( mostReaction == null
child: Text( ? const Icon(Symbols.add_reaction)
'x${item.reactionsCount[mostReaction]}', : Badge(
style: const TextStyle(fontSize: 11), label: Center(
textAlign: TextAlign.center, child: Text(
'x${item.reactionsCount[mostReaction]}',
style: const TextStyle(fontSize: 11),
textAlign: TextAlign.center,
),
), ),
offset: const Offset(4, 20),
backgroundColor: Theme.of(
context,
).colorScheme.primary.withOpacity(0.75),
textColor:
Theme.of(context).colorScheme.onPrimary,
child: _buildReactionIcon(
mostReaction,
32,
).padding(bottom: 2),
), ),
offset: const Offset(4, 20), style: ButtonStyle(
backgroundColor: Theme.of( backgroundColor: WidgetStatePropertyAll(
(item.reactionsMade[mostReaction] ?? false)
? Theme.of(
context, context,
).colorScheme.primary.withOpacity(0.75), ).colorScheme.primary.withOpacity(0.5)
textColor: : null,
Theme.of(context).colorScheme.onPrimary, ),
child: Text( ),
kReactionTemplates[mostReaction]?.icon ?? '', onPressed: () {
style: const TextStyle(fontSize: 20), showModalBottomSheet(
), context: context,
), useRootNavigator: true,
style: ButtonStyle( builder: (BuildContext context) {
backgroundColor: WidgetStatePropertyAll( return _PostReactionSheet(
(item.reactionsMade[mostReaction] ?? false) reactionsCount: item.reactionsCount,
? Theme.of( reactionsMade: item.reactionsMade,
context, onReact: (symbol, attitude) {
).colorScheme.primary.withOpacity(0.5) reactPost(symbol, attitude);
: null, },
);
},
);
},
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -3,
vertical: -3,
), ),
),
onPressed: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
builder: (BuildContext context) {
return _PostReactionSheet(
reactionsCount: item.reactionsCount,
reactionsMade: item.reactionsMade,
onReact: (symbol, attitude) {
reactPost(symbol, attitude);
},
);
},
);
},
padding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -3,
vertical: -3,
), ),
), ),
), ),
@@ -611,7 +638,7 @@ class PostReactionList extends HookConsumerWidget {
} }
return SizedBox( return SizedBox(
height: 28, height: 40,
child: ListView( child: ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: padding ?? EdgeInsets.zero, padding: padding ?? EdgeInsets.zero,
@@ -649,7 +676,7 @@ class PostReactionList extends HookConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 8),
child: ActionChip( child: ActionChip(
avatar: Text(kReactionTemplates[symbol]?.icon ?? '?'), avatar: _buildReactionIcon(symbol, 24),
label: Row( label: Row(
spacing: 4, spacing: 4,
children: [ children: [
@@ -786,37 +813,96 @@ class _PostReactionSheet extends StatelessWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final symbol = allReactions[index]; final symbol = allReactions[index];
final count = reactionsCount[symbol] ?? 0; final count = reactionsCount[symbol] ?? 0;
return Card( final hasImage = _getReactionImageAvailable(symbol);
margin: const EdgeInsets.symmetric(vertical: 4), return Badge(
color: label: Text('x$count'),
(reactionsMade[symbol] ?? false) isLabelVisible: count > 0,
? Theme.of(context).colorScheme.primaryContainer textColor: Theme.of(context).colorScheme.onPrimary,
: Theme.of(context).colorScheme.surfaceContainerLowest, backgroundColor: Theme.of(context).colorScheme.primary,
child: InkWell( offset: Offset(0, 0),
borderRadius: const BorderRadius.all(Radius.circular(8)), child: Card(
onTap: () { margin: const EdgeInsets.symmetric(vertical: 4),
onReact(symbol, attitude); color: Theme.of(context).colorScheme.surfaceContainerLowest,
Navigator.pop(context); child: InkWell(
}, borderRadius: const BorderRadius.all(Radius.circular(8)),
child: Column( onTap: () {
mainAxisAlignment: MainAxisAlignment.center, onReact(symbol, attitude);
children: [ Navigator.pop(context);
Text( },
kReactionTemplates[symbol]?.icon ?? '', child: Container(
textAlign: TextAlign.center, decoration:
).fontSize(24), hasImage
Text( ? BoxDecoration(
ReactInfo.getTranslationKey(symbol), borderRadius: BorderRadius.circular(8),
textAlign: TextAlign.center, image: DecorationImage(
).tr(), image: AssetImage(
if (count > 0) 'assets/images/stickers/$symbol.png',
Text( ),
'x$count', fit: BoxFit.cover,
textAlign: TextAlign.center, colorFilter:
).bold().padding(bottom: 4) (reactionsMade[symbol] ?? false)
else ? ColorFilter.mode(
const Gap(20), Theme.of(context)
], .colorScheme
.primaryContainer
.withOpacity(0.7),
BlendMode.srcATop,
)
: null,
),
)
: null,
child: Stack(
fit: StackFit.expand,
children: [
if (hasImage)
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Theme.of(context)
.colorScheme
.surfaceContainerHighest
.withOpacity(0.7),
Colors.transparent,
],
stops: [0.0, 0.3],
),
),
),
Column(
mainAxisAlignment:
hasImage
? MainAxisAlignment.end
: MainAxisAlignment.center,
children: [
if (!hasImage) _buildReactionIcon(symbol, 36),
Text(
ReactInfo.getTranslationKey(symbol),
textAlign: TextAlign.center,
style: TextStyle(
color: hasImage ? Colors.white : null,
shadows:
hasImage
? [
const Shadow(
blurRadius: 4,
offset: Offset(0.5, 0.5),
color: Colors.black,
),
]
: null,
),
).tr(),
if (hasImage) const Gap(4),
],
),
],
),
),
), ),
), ),
); );

View File

@@ -189,6 +189,7 @@ flutter:
- assets/i18n/ - assets/i18n/
- assets/images/ - assets/images/
- assets/images/oidc/ - assets/images/oidc/
- assets/images/stickers/
- assets/icons/ - assets/icons/
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see