Compare commits

..

9 Commits

Author SHA1 Message Date
1b41c847a6 Custom fonts 2025-02-27 22:35:12 +08:00
b1af6c2c97 🐛 Optimize and fix profile page loading issue 2025-02-27 22:11:53 +08:00
8e76ff3f84 Optimize user loading api usage 2025-02-27 20:51:47 +08:00
bd26602299 Code highlighting 2025-02-26 23:29:02 +08:00
52ab1d0d10 🐛 Fix chat last message displaying inconsistences 2025-02-26 00:29:35 +08:00
f746e06f65 ⚗️ Experimental user first badge showing on chat 2025-02-26 00:25:42 +08:00
d11069a2be 🐛 Bug fixes on notification page 2025-02-26 00:00:53 +08:00
d6dc487d9e Latex Rendering, closed #9 2025-02-25 23:49:48 +08:00
a07c7cdede 🐛 Fix infinite loading own sticker 2025-02-25 22:56:30 +08:00
21 changed files with 450 additions and 109 deletions

View File

@ -0,0 +1,18 @@
meta {
name: Deal Abuse Report
type: http
seq: 3
}
put {
url: {{endpoint}}/cgi/id/reports/abuse/3/status
body: json
auth: inherit
}
body:json {
{
"status": "processed",
"message": "相关附件已经进行评级处理,未来会将该项权限下放到帖主以及社区成员。"
}
}

View File

@ -203,6 +203,11 @@
"other": "{} comments"
},
"settingsAppearance": "Appearance",
"settingsCustomFonts": "Custom Fonts",
"settingsCustomFontsDescription": "Set custom fonts for the application.",
"settingsCustomFontFamily": "Custom Font Family",
"settingsCustomFontFamilyHint": "Use comma to separate fonts, higher priority comes first",
"settingsCustomFontApplied": "Custom font has been applied.",
"settingsDisplayLanguage": "Display Language",
"settingsDisplayLanguageDescription": "Set the application language.",
"settingsDisplayLanguageSystem": "Follow System",

View File

@ -201,6 +201,11 @@
"other": "{} 条评论"
},
"settingsAppearance": "外观",
"settingsCustomFonts": "自定义字体",
"settingsCustomFontsDescription": "设置应用程序使用的字体。",
"settingsCustomFontFamily": "应用字体",
"settingsCustomFontFamilyHint": "使用英文逗号分割每一种字体,越前优先级越高",
"settingsCustomFontApplied": "自定义字体已经应用。",
"settingsDisplayLanguage": "显示语言",
"settingsDisplayLanguageDescription": "设置应用程序使用的语言",
"settingsDisplayLanguageSystem": "跟随系统",

View File

@ -201,6 +201,11 @@
"other": "{} 條評論"
},
"settingsAppearance": "外觀",
"settingsCustomFonts": "自定義字體",
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
"settingsCustomFontFamily": "應用字體",
"settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
"settingsCustomFontApplied": "自定義字體已經應用。",
"settingsDisplayLanguage": "顯示語言",
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
"settingsDisplayLanguageSystem": "跟隨系統",

View File

@ -201,6 +201,11 @@
"other": "{} 條評論"
},
"settingsAppearance": "外觀",
"settingsCustomFonts": "自定義字體",
"settingsCustomFontsDescription": "設置應用程序使用的字體。",
"settingsCustomFontFamily": "應用字體",
"settingsCustomFontFamilyHint": "使用英文逗號分割每一種字體,越前優先級越高",
"settingsCustomFontApplied": "自定義字體已經應用。",
"settingsDisplayLanguage": "顯示語言",
"settingsDisplayLanguageDescription": "設置應用程序使用的語言",
"settingsDisplayLanguageSystem": "跟隨系統",

View File

@ -235,7 +235,7 @@ PODS:
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.49.0)
- sqlite3 (~> 3.49.1)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/perf-threadsafe
@ -445,7 +445,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
video_compress: fce97e4fb1dfd88175aa07d2ffc8a2f297f87fbe

View File

@ -18,6 +18,7 @@ const kAppNotifyWithHaptic = 'app_notify_with_haptic';
const kAppExpandPostLink = 'app_expand_post_link';
const kAppExpandChatLink = 'app_expand_chat_link';
const kAppRealmCompactView = 'app_realm_compact_view';
const kAppCustomFonts = 'app_custom_fonts';
const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityLowest': FilterQuality.none,

View File

@ -13,8 +13,16 @@ class ThemeProvider extends ChangeNotifier {
});
}
void reloadTheme({Color? seedColorOverride, bool? useMaterial3}) {
createAppThemeSet(seedColorOverride: seedColorOverride, useMaterial3: useMaterial3).then((value) {
void reloadTheme({
Color? seedColorOverride,
bool? useMaterial3,
String? customFonts,
}) {
createAppThemeSet(
seedColorOverride: seedColorOverride,
useMaterial3: useMaterial3,
customFonts: customFonts,
).then((value) {
theme = value;
notifyListeners();
});

View File

@ -27,8 +27,11 @@ class UserDirectoryProvider {
plannedQuery.add(item);
}
}
final resp = await _sn.client.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
final respDecoded = resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList();
if (plannedQuery.isEmpty) return out;
final resp = await _sn.client
.get('/cgi/id/users', queryParameters: {'id': plannedQuery.join(',')});
final respDecoded =
resp.data.map((e) => SnAccount.fromJson(e)).cast<SnAccount>().toList();
var sideIdx = 0;
for (var idx = 0; idx < out.length; idx++) {
if (out[idx] != null) continue;

View File

@ -43,7 +43,8 @@ class UserScreen extends StatefulWidget {
State<UserScreen> createState() => _UserScreenState();
}
class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateMixin {
class _UserScreenState extends State<UserScreen>
with SingleTickerProviderStateMixin {
late final ScrollController _scrollController = ScrollController();
SnAccount? _account;
@ -64,13 +65,18 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
}
}
Future<List<SnCheckInRecord>> _getCheckInRecords() async {
List<SnCheckInRecord>? _records;
Future<void> _getCheckInRecords() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
return List.from(
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
);
final resp =
await sn.client.get('/cgi/id/users/${widget.name}/check-in?take=14');
setState(() {
_records = List.from(
resp.data['data']?.map((x) => SnCheckInRecord.fromJson(x)) ?? [],
);
});
} catch (err) {
if (mounted) context.showErrorDialog(err);
rethrow;
@ -98,7 +104,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
Future<void> _fetchPublishers() async {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/co/publishers?user=${widget.name}');
final resp =
await sn.client.get('/cgi/co/publishers?user=${widget.name}');
_publishers = List<SnPublisher>.from(
resp.data?.map((e) => SnPublisher.fromJson(e)) ?? [],
);
@ -144,7 +151,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
'related': _account!.name,
});
if (!mounted) return;
context.showSnackbar('userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
context.showSnackbar(
'userBlocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -160,9 +168,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
try {
final rel = context.read<SnRelationshipProvider>();
await rel.updateRelationship(_account!.id, 1, _accountRelationship?.permNodes ?? {});
await rel.updateRelationship(
_account!.id, 1, _accountRelationship?.permNodes ?? {});
if (!mounted) return;
context.showSnackbar('userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
context.showSnackbar(
'userUnblocked'.tr(args: ['@${_account?.name ?? 'unknown'}']));
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -188,12 +198,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
double _appBarBlur = 0.0;
late final _appBarWidth = MediaQuery.of(context).size.width;
late final _appBarHeight = (_appBarWidth * kBannerAspectRatio).roundToDouble();
late final _appBarHeight =
(_appBarWidth * kBannerAspectRatio).roundToDouble();
void _updateAppBarBlur() {
if (_scrollController.offset > _appBarHeight) return;
setState(() {
_appBarBlur = (_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
_appBarBlur =
(_scrollController.offset / _appBarHeight * 10).clamp(0.0, 10.0);
});
}
@ -205,6 +217,7 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
_fetchStatus();
_fetchPublishers();
_getCheckInRecords();
try {
final rel = context.read<SnRelationshipProvider>();
@ -260,18 +273,20 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
text: TextSpan(children: [
TextSpan(
text: _account!.nick,
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.white,
shadows: labelShadows,
),
style:
Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.white,
shadows: labelShadows,
),
),
const TextSpan(text: '\n'),
TextSpan(
text: '@${_account!.name}',
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Colors.white,
shadows: labelShadows,
),
style:
Theme.of(context).textTheme.bodySmall!.copyWith(
color: Colors.white,
shadows: labelShadows,
),
),
]),
),
@ -339,7 +354,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
PopupMenuButton(
padding: EdgeInsets.zero,
style: ButtonStyle(
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
visualDensity:
VisualDensity(horizontal: -4, vertical: -4),
),
itemBuilder: (context) => [
PopupMenuItem(
@ -399,7 +415,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
Symbols.circle,
fill: 1,
size: 16,
color: (_status?.isOnline ?? false) ? Colors.green : Colors.grey,
color: (_status?.isOnline ?? false)
? Colors.green
: Colors.grey,
).padding(all: 4),
const Gap(8),
Text(
@ -409,7 +427,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
: 'accountStatusOffline'.tr()
: 'loading'.tr(),
),
if (_status != null && !_status!.isOnline && _status!.lastSeenAt != null)
if (_status != null &&
!_status!.isOnline &&
_status!.lastSeenAt != null)
Text(
'accountStatusLastSeen'.tr(args: [
_status!.lastSeenAt != null
@ -429,11 +449,14 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
(ele) => Tooltip(
richMessage: TextSpan(
children: [
TextSpan(text: kBadgesMeta[ele.type]?.$1.tr() ?? 'unknown'.tr()),
TextSpan(
text: kBadgesMeta[ele.type]?.$1.tr() ??
'unknown'.tr()),
if (ele.metadata['title'] != null)
TextSpan(
text: '\n${ele.metadata['title']}',
style: const TextStyle(fontWeight: FontWeight.bold),
style: const TextStyle(
fontWeight: FontWeight.bold),
),
TextSpan(text: '\n'),
TextSpan(
@ -442,7 +465,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
],
),
child: Icon(
kBadgesMeta[ele.type]?.$2 ?? Symbols.question_mark,
kBadgesMeta[ele.type]?.$2 ??
Symbols.question_mark,
color: kBadgesMeta[ele.type]?.$3,
fill: 1,
),
@ -458,7 +482,9 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
children: [
const Icon(Symbols.calendar_add_on),
const Gap(8),
Text('publisherJoinedAt').tr(args: [DateFormat('y/M/d').format(_account!.createdAt)]),
Text('publisherJoinedAt').tr(args: [
DateFormat('y/M/d').format(_account!.createdAt)
]),
],
),
Row(
@ -491,17 +517,24 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
children: [
const Icon(Symbols.star),
const Gap(8),
Text('Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
Text(
'Lv${getLevelFromExp(_account?.profile?.experience ?? 0)}'),
const Gap(8),
Text(calcLevelUpProgressLevel(_account?.profile?.experience ?? 0)).fontSize(11).opacity(0.5),
Text(calcLevelUpProgressLevel(
_account?.profile?.experience ?? 0))
.fontSize(11)
.opacity(0.5),
const Gap(8),
Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 160),
child: LinearProgressIndicator(
value: calcLevelUpProgress(_account?.profile?.experience ?? 0),
value: calcLevelUpProgress(
_account?.profile?.experience ?? 0),
borderRadius: BorderRadius.circular(8),
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
backgroundColor: Theme.of(context)
.colorScheme
.surfaceContainer,
).alignment(Alignment.centerLeft),
),
],
@ -514,21 +547,23 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
SliverToBoxAdapter(child: const Divider()),
const SliverGap(12),
SliverToBoxAdapter(
child: FutureBuilder<List<SnCheckInRecord>>(
future: _getCheckInRecords(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const SizedBox.shrink();
if (snapshot.data!.length <= 1) {
child: Builder(
builder: (context) {
if (_records == null) return const SizedBox.shrink();
if (_records!.length <= 1) {
return Text(
'accountCheckInNoRecords',
textAlign: TextAlign.center,
).tr().fontWeight(FontWeight.bold).center().padding(horizontal: 20, vertical: 8);
)
.tr()
.fontWeight(FontWeight.bold)
.center()
.padding(horizontal: 20, vertical: 8);
}
final records = snapshot.data!;
return SizedBox(
width: double.infinity,
height: 240,
child: CheckInRecordChart(records: records),
child: CheckInRecordChart(records: _records!),
).padding(
right: 24,
left: 16,
@ -544,7 +579,11 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('accountBadge').bold().fontSize(17).tr().padding(horizontal: 20, bottom: 4),
Text('accountBadge')
.bold()
.fontSize(17)
.tr()
.padding(horizontal: 20, bottom: 4),
SizedBox(
height: 80,
width: double.infinity,
@ -558,7 +597,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
child: Card(
child: ListTile(
leading: Icon(
kBadgesMeta[badge.type]?.$2 ?? Symbols.question_mark,
kBadgesMeta[badge.type]?.$2 ??
Symbols.question_mark,
color: kBadgesMeta[badge.type]?.$3,
fill: 1,
),
@ -568,7 +608,8 @@ class _UserScreenState extends State<UserScreen> with SingleTickerProviderStateM
subtitle: badge.metadata['title'] != null
? Text(badge.metadata['title'])
: Text(
DateFormat('y/M/d').format(badge.createdAt),
DateFormat('y/M/d')
.format(badge.createdAt),
),
),
),
@ -664,7 +705,8 @@ class CheckInRecordChart extends StatelessWidget {
),
)
.toList(),
getTooltipColor: (_) => Theme.of(context).colorScheme.surfaceContainerHigh,
getTooltipColor: (_) =>
Theme.of(context).colorScheme.surfaceContainerHigh,
),
),
titlesData: FlTitlesData(

View File

@ -74,18 +74,20 @@ class _ChatScreenState extends State<ChatScreen> {
if (!mounted) return;
final ud = context.read<UserDirectoryProvider>();
final idSet = <int>{};
for (final channel in channels) {
if (channel.type == 1) {
await ud.listAccount(
idSet.addAll(
channel.members
?.cast<SnChannelMember?>()
.map((ele) => ele?.accountId)
.where((ele) => ele != null)
.toSet() ??
{},
.cast<int>() ??
[],
);
}
}
if (idSet.isNotEmpty) await ud.listAccount(idSet);
if (mounted) setState(() => _channels = channels);
})
@ -290,10 +292,34 @@ class _ChatScreenState extends State<ChatScreen> {
],
),
subtitle: lastMessage != null
? Text(
'${ud.getAccountFromCache(lastMessage.sender.accountId)?.nick}: ${lastMessage.body['text'] ?? 'Unable preview'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
? Row(
children: [
Expanded(
child: Text(
lastMessage.body['text'] ??
'Unable preview',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const Gap(4),
Text(
DateFormat(
lastMessage.createdAt.toLocal().day ==
DateTime.now().day
? 'HH:mm'
: lastMessage.createdAt
.toLocal()
.year ==
DateTime.now().year
? 'MM/dd'
: 'yy/MM/dd',
).format(lastMessage.createdAt.toLocal()),
style: GoogleFonts.robotoMono(
fontSize: 12,
),
),
],
)
: Text(
channel.description,
@ -336,6 +362,8 @@ class _ChatScreenState extends State<ChatScreen> {
'unknown'.tr()),
backgroundColor:
Theme.of(context).colorScheme.primary,
textColor:
Theme.of(context).colorScheme.onPrimary,
),
const Gap(6),
Expanded(
@ -346,6 +374,7 @@ class _ChatScreenState extends State<ChatScreen> {
overflow: TextOverflow.ellipsis,
),
),
const Gap(4),
Text(
DateFormat(
lastMessage.createdAt.toLocal().day ==

View File

@ -29,6 +29,7 @@ const Map<String, IconData> kNotificationTopicIcons = {
'passport.security.otp': Symbols.password,
'interactive.subscription': Symbols.subscriptions,
'interactive.feedback': Symbols.add_reaction,
'interactive.reply': Symbols.reply,
'messaging.callStart': Symbols.call_received,
'wallet.transaction.new': Symbols.receipt,
};
@ -57,10 +58,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
try {
final sn = context.read<SnNetworkProvider>();
final nty = context.read<NotificationProvider>();
final resp = await sn.client.get('/cgi/id/notifications?take=10');
final resp =
await sn.client.get('/cgi/id/notifications', queryParameters: {
'take': 10,
'offset': _notifications.length,
});
_totalCount = resp.data['count'];
_notifications.addAll(
resp.data['data']?.map((e) => SnNotification.fromJson(e)).cast<SnNotification>() ?? [],
resp.data['data']
?.map((e) => SnNotification.fromJson(e))
.cast<SnNotification>() ??
[],
);
nty.updateTray();
} catch (err) {
@ -186,7 +194,8 @@ class _NotificationScreenState extends State<NotificationScreen> {
_fetchNotifications();
},
isLoading: _isBusy,
hasReachedMax: _totalCount != null && _notifications.length >= _totalCount!,
hasReachedMax: _totalCount != null &&
_notifications.length >= _totalCount!,
itemBuilder: (context, idx) {
final nty = _notifications[idx];
return Row(
@ -218,13 +227,17 @@ class _NotificationScreenState extends State<NotificationScreen> {
isAutoWarp: true,
),
),
if (['interactive.reply', 'interactive.feedback', 'interactive.subscription']
.contains(nty.topic) &&
if ([
'interactive.reply',
'interactive.feedback',
'interactive.subscription'
].contains(nty.topic) &&
nty.metadata['related_post'] != null)
GestureDetector(
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8)),
borderRadius: const BorderRadius.all(
Radius.circular(8)),
border: Border.all(
color: Theme.of(context).dividerColor,
width: 1,
@ -243,7 +256,9 @@ class _NotificationScreenState extends State<NotificationScreen> {
GoRouter.of(context).pushNamed(
'postDetail',
pathParameters: {
'slug': nty.metadata['related_post']!['id'].toString(),
'slug': nty
.metadata['related_post']!['id']
.toString(),
},
);
},
@ -272,8 +287,10 @@ class _NotificationScreenState extends State<NotificationScreen> {
IconButton(
icon: const Icon(Symbols.check),
padding: EdgeInsets.all(0),
visualDensity: const VisualDensity(horizontal: -4, vertical: -4),
onPressed: _isSubmitting ? null : () => _markOneAsRead(nty),
visualDensity:
const VisualDensity(horizontal: -4, vertical: -4),
onPressed:
_isSubmitting ? null : () => _markOneAsRead(nty),
),
],
).padding(horizontal: 16);

View File

@ -48,6 +48,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
late final SharedPreferences _prefs;
String _docBasepath = '/';
final TextEditingController _customFontController = TextEditingController();
final TextEditingController _serverUrlController = TextEditingController();
@override
@ -62,11 +63,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
final config = context.read<ConfigProvider>();
_prefs = config.prefs;
_serverUrlController.text = config.serverUrl;
if (_prefs.getString(kAppCustomFonts) != null) {
_customFontController.text = _prefs.getString(kAppCustomFonts) ?? '';
}
}
@override
void dispose() {
_serverUrlController.dispose();
_customFontController.dispose();
super.dispose();
}
@ -330,6 +335,47 @@ class _SettingsScreenState extends State<SettingsScreen> {
setState(() {});
},
),
ListTile(
leading: const Icon(Symbols.font_download),
title: Text('settingsCustomFonts').tr(),
subtitle: Text('settingsCustomFontsDescription').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 14),
trailing: IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
icon: const Icon(Icons.clear),
onPressed: () {
_prefs.remove(kAppCustomFonts);
context.showSnackbar('settingsCustomFontApplied'.tr());
final theme = context.read<ThemeProvider>();
_customFontController.clear();
theme.reloadTheme();
},
),
),
TextField(
controller: _customFontController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'settingsCustomFontFamily'.tr(),
helperText: 'settingsCustomFontFamilyHint'.tr(),
prefixIcon: const Icon(Symbols.format_paint),
suffixIcon: IconButton(
icon: const Icon(Symbols.save),
onPressed: () {
_prefs.setString(
kAppCustomFonts,
_customFontController.text,
);
context.showSnackbar('settingsCustomFontApplied'.tr());
final theme = context.read<ThemeProvider>();
theme.reloadTheme();
},
),
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
).padding(horizontal: 16, top: 8, bottom: 4),
],
),
Column(

View File

@ -179,7 +179,9 @@ class _StickerScreenState extends State<StickerScreen>
child: InfiniteList(
itemCount: _packs.length,
onFetchData: _fetchPacks,
hasReachedMax: _totalCount != null && _packs.length >= _totalCount!,
hasReachedMax:
(_totalCount != null && _packs.length >= _totalCount!) ||
_tabController.index == 2,
isLoading: _isBusy,
itemBuilder: (context, idx) {
final pack = _packs[idx];

View File

@ -11,10 +11,19 @@ class ThemeSet {
ThemeSet({required this.light, required this.dark});
}
Future<ThemeSet> createAppThemeSet({Color? seedColorOverride, bool? useMaterial3}) async {
Future<ThemeSet> createAppThemeSet(
{Color? seedColorOverride, bool? useMaterial3, String? customFonts}) async {
return ThemeSet(
light: await createAppTheme(Brightness.light, useMaterial3: useMaterial3),
dark: await createAppTheme(Brightness.dark, useMaterial3: useMaterial3),
light: await createAppTheme(
Brightness.light,
useMaterial3: useMaterial3,
customFonts: customFonts,
),
dark: await createAppTheme(
Brightness.dark,
useMaterial3: useMaterial3,
customFonts: customFonts,
),
);
}
@ -22,24 +31,35 @@ Future<ThemeData> createAppTheme(
Brightness brightness, {
Color? seedColorOverride,
bool? useMaterial3,
String? customFonts,
}) async {
final prefs = await SharedPreferences.getInstance();
final seedColorString = prefs.getInt(kAppColorSchemeStoreKey);
final seedColor = seedColorString != null ? Color(seedColorString) : Colors.indigo;
final seedColor =
seedColorString != null ? Color(seedColorString) : Colors.indigo;
final colorScheme = ColorScheme.fromSeed(
seedColor: seedColorOverride ?? seedColor,
brightness: brightness,
);
final hasAppBarTransparent = prefs.getBool(kAppbarTransparentStoreKey) ?? false;
final useM3 = useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
final hasAppBarTransparent =
prefs.getBool(kAppbarTransparentStoreKey) ?? false;
final useM3 =
useMaterial3 ?? (prefs.getBool(kMaterialYouToggleStoreKey) ?? true);
final inUseFonts = (customFonts ?? prefs.getString(kAppCustomFonts))
?.split(',')
.map((ele) => ele.trim())
.toList();
return ThemeData(
useMaterial3: useM3,
colorScheme: colorScheme,
brightness: brightness,
fontFamily: inUseFonts?.firstOrNull,
fontFamilyFallback: inUseFonts?.sublist(1),
iconTheme: IconThemeData(
fill: 0,
weight: 400,
@ -52,8 +72,10 @@ Future<ThemeData> createAppTheme(
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: hasAppBarTransparent ? 0 : null,
backgroundColor: hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
foregroundColor: hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
backgroundColor:
hasAppBarTransparent ? Colors.transparent : colorScheme.primary,
foregroundColor:
hasAppBarTransparent ? colorScheme.onSurface : colorScheme.onPrimary,
),
pageTransitionsTheme: PageTransitionsTheme(
builders: {

View File

@ -10,6 +10,7 @@ class AccountImage extends StatelessWidget {
final Color? foregroundColor;
final double? radius;
final Widget? fallbackWidget;
final Widget? badge;
const AccountImage({
super.key,
@ -18,6 +19,7 @@ class AccountImage extends StatelessWidget {
this.foregroundColor,
this.radius,
this.fallbackWidget,
this.badge,
});
@override
@ -26,26 +28,36 @@ class AccountImage extends StatelessWidget {
final url = sn.getAttachmentUrl(content ?? '');
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
return CircleAvatar(
key: Key('attachment-${content.hashCode}'),
radius: radius,
backgroundColor: backgroundColor,
backgroundImage: (content?.isNotEmpty ?? false)
? ResizeImage(
UniversalImage.provider(url),
width: ((radius ?? 20) * devicePixelRatio * 2).round(),
height: ((radius ?? 20) * devicePixelRatio * 2).round(),
policy: ResizeImagePolicy.fit,
)
: null,
child: (content?.isEmpty ?? true)
? (fallbackWidget ??
Icon(
Symbols.account_circle,
size: radius != null ? radius! * 1.2 : 24,
color: foregroundColor,
))
: null,
return Stack(
children: [
CircleAvatar(
key: Key('attachment-${content.hashCode}'),
radius: radius,
backgroundColor: backgroundColor,
backgroundImage: (content?.isNotEmpty ?? false)
? ResizeImage(
UniversalImage.provider(url),
width: ((radius ?? 20) * devicePixelRatio * 2).round(),
height: ((radius ?? 20) * devicePixelRatio * 2).round(),
policy: ResizeImagePolicy.fit,
)
: null,
child: (content?.isEmpty ?? true)
? (fallbackWidget ??
Icon(
Symbols.account_circle,
size: radius != null ? radius! * 1.2 : 24,
color: foregroundColor,
))
: null,
),
if (badge != null)
Positioned(
right: -4,
bottom: -4,
child: badge!,
),
],
);
}
}

View File

@ -11,6 +11,7 @@ import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/user_directory.dart';
import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/account/profile_page.dart';
import 'package:surface/types/chat.dart';
import 'package:surface/widgets/account/account_image.dart';
import 'package:surface/widgets/account/account_popover.dart';
@ -105,6 +106,22 @@ class ChatMessage extends StatelessWidget {
GestureDetector(
child: AccountImage(
content: user?.avatar,
badge: (user?.badges.isNotEmpty ?? false)
? Icon(
kBadgesMeta[user!.badges.first.type]?.$2 ??
Symbols.question_mark,
color: kBadgesMeta[user.badges.first.type]?.$3,
fill: 1,
size: 18,
shadows: [
Shadow(
offset: Offset(1, 1),
blurRadius: 5.0,
color: Color.fromARGB(150, 0, 0, 0),
),
],
)
: null,
),
onTap: () {
if (user == null) return;

View File

@ -1,6 +1,9 @@
import 'package:dismissible_page/dismissible_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_highlight/flutter_highlight.dart';
import 'package:flutter_highlight/theme_map.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_markdown_latex/flutter_markdown_latex.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:markdown/markdown.dart' as markdown;
@ -72,21 +75,27 @@ class MarkdownTextContent extends StatelessWidget {
),
code: GoogleFonts.robotoMono(height: 1),
),
builders: {},
builders: {
'latex': LatexElementBuilder(),
'code': HighlightBuilder(),
},
softLineBreak: true,
extensionSet: markdown.ExtensionSet(
<markdown.BlockSyntax>[
markdown.CodeBlockSyntax(),
...markdown.ExtensionSet.gitHubFlavored.blockSyntaxes,
markdown.CodeBlockSyntax(),
markdown.FencedCodeBlockSyntax(),
LatexBlockSyntax(),
],
<markdown.InlineSyntax>[
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes,
if (isAutoWarp) markdown.LineBreakSyntax(),
_UserNameCardInlineSyntax(),
_CustomEmoteInlineSyntax(context),
markdown.AutolinkSyntax(),
markdown.AutolinkExtensionSyntax(),
markdown.CodeSyntax(),
...markdown.ExtensionSet.gitHubFlavored.inlineSyntaxes
LatexInlineSyntax(),
],
),
onTapLink: (text, href, title) async {
@ -260,3 +269,56 @@ class _CustomEmoteInlineSyntax extends markdown.InlineSyntax {
return true;
}
}
class HighlightBuilder extends MarkdownElementBuilder {
@override
Widget? visitElementAfterWithContext(
BuildContext context,
markdown.Element element,
TextStyle? preferredStyle,
TextStyle? parentStyle,
) {
final isDark = Theme.of(context).brightness == Brightness.dark;
if (element.attributes['class'] == null &&
!element.textContent.trim().contains('\n')) {
return Container(
padding:
EdgeInsets.only(top: 0.0, right: 4.0, bottom: 1.75, left: 4.0),
margin: EdgeInsets.symmetric(horizontal: 2.0),
color: Colors.black12,
child: Text(
element.textContent,
style: GoogleFonts.robotoMono(textStyle: preferredStyle),
));
} else {
var language = 'plaintext';
final pattern = RegExp(r'^language-(.+)$');
if (element.attributes['class'] != null &&
pattern.hasMatch(element.attributes['class'] ?? '')) {
language =
pattern.firstMatch(element.attributes['class'] ?? '')?.group(1) ??
'plaintext';
}
return ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: HighlightView(
element.textContent.trim(),
language: language,
theme: {
...(isDark ? themeMap['a11y-dark']! : themeMap['a11y-light']!),
'root': (isDark
? TextStyle(
backgroundColor: Colors.transparent,
color: Color(0xfff8f8f2))
: TextStyle(
backgroundColor: Colors.transparent,
color: Color(0xff545454)))
},
padding: EdgeInsets.all(12),
textStyle: GoogleFonts.robotoMono(textStyle: preferredStyle),
),
);
}
}
}

View File

@ -193,7 +193,7 @@ PODS:
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.49.0)
- sqlite3 (~> 3.49.1)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/perf-threadsafe
@ -378,7 +378,7 @@ SPEC CHECKSUMS:
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: 069c435986dd4b63461aecd68f4b30be4a9e9daa
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
video_compress: c896234f100791b5fef7f049afa38f6d2ef7b42f

View File

@ -405,10 +405,10 @@ packages:
dependency: transitive
description:
name: dio_web_adapter
sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
dismissible_page:
dependency: "direct main"
description:
@ -525,10 +525,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: "6f6bfa8797f296965bdc3e1f702574ab49a540c19b9237b401e7c2b25dfe594c"
sha256: "9467b7c4eedf0bd4c9306b0ec12455b278f6366962be061d0978a446c103c111"
url: "https://pub.dev"
source: hosted
version: "9.0.0"
version: "9.0.1"
file_saver:
dependency: "direct main"
description:
@ -710,6 +710,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
flutter_highlight:
dependency: "direct main"
description:
name: flutter_highlight
sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
flutter_inappwebview:
dependency: "direct main"
description:
@ -803,6 +811,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.6+2"
flutter_markdown_latex:
dependency: "direct main"
description:
name: flutter_markdown_latex
sha256: "839e76a84abb3632ffcebbd450cf93c7e9894af65622527d23f0084cee1bfd04"
url: "https://pub.dev"
source: hosted
version: "0.3.4"
flutter_math_fork:
dependency: transitive
description:
name: flutter_math_fork
sha256: "284bab89b2fbf1bc3a0baf13d011c1dd324d004e35d177626b77f2fc056366ac"
url: "https://pub.dev"
source: hosted
version: "0.7.3"
flutter_native_splash:
dependency: "direct dev"
description:
@ -941,6 +965,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
highlight:
dependency: transitive
description:
name: highlight
sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
home_widget:
dependency: "direct main"
description:
@ -1393,18 +1425,18 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35"
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
url: "https://pub.dev"
source: hosted
version: "8.2.1"
version: "8.3.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76"
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.2.0"
pasteboard:
dependency: "direct main"
description:
@ -1950,10 +1982,10 @@ packages:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "57fafacd815c981735406215966ff7caaa8eab984b094f52e692accefcbd9233"
sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3"
url: "https://pub.dev"
source: hosted
version: "0.5.30"
version: "0.5.31"
sqlparser:
dependency: transitive
description:
@ -2050,6 +2082,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.2"
tuple:
dependency: transitive
description:
name: tuple
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
url: "https://pub.dev"
source: hosted
version: "2.0.2"
typed_data:
dependency: transitive
description:

View File

@ -128,6 +128,8 @@ dependencies:
drift: ^2.25.1
drift_flutter: ^0.2.4
local_notifier: ^0.1.6
flutter_markdown_latex: ^0.3.4
flutter_highlight: ^0.7.0
dev_dependencies:
flutter_test: