Compare commits

..

No commits in common. "8bb62b59921070d69669f32483d5305bdf7a2637" and "3395f3dbd0e572ef9247f4da9ce174549654d93a" have entirely different histories.

14 changed files with 256 additions and 386 deletions

View File

@ -106,8 +106,6 @@
}, },
"loginEnterPassword": "Enter the code", "loginEnterPassword": "Enter the code",
"loginSuccess": "Logged in as {}", "loginSuccess": "Logged in as {}",
"authFactorDelete": "Delete Auth Factor",
"authFactorDeleteDescription": "Are you sure you want delete auth factor {}?",
"authFactorPassword": "Password", "authFactorPassword": "Password",
"authFactorPasswordDescription": "The password you set when you registered.", "authFactorPasswordDescription": "The password you set when you registered.",
"authFactorEmail": "Email verification code", "authFactorEmail": "Email verification code",
@ -581,8 +579,5 @@
"newsReadingFromReader": "You're reading from HyperNet.Reader", "newsReadingFromReader": "You're reading from HyperNet.Reader",
"newsReadingFromOriginal": "You're reading the original article", "newsReadingFromOriginal": "You're reading the original article",
"newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.", "newsDisclaimer": "This article is fetched from the Internet, we do not guarantee its authenticity, please judge for yourself. All content in this article belongs to the original author.",
"newsToday": "Today's News", "newsToday": "Today's News"
"totpPostSetup": "One More Thing",
"totpPostSetupDescription": "Scan the QR Code below with Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden or any of kind of authenticator app which supports TOTP.",
"totpNeverShare": "Never share this QR Code"
} }

View File

@ -89,8 +89,6 @@
}, },
"loginEnterPassword": "验证代码", "loginEnterPassword": "验证代码",
"loginSuccess": "登录为 {}", "loginSuccess": "登录为 {}",
"authFactorDelete": "删除验证因子",
"authFactorDeleteDescription": "你确定要删除 {} 验证因子吗?",
"authFactorPassword": "密码", "authFactorPassword": "密码",
"authFactorPasswordDescription": "注册时选择设置的密码。", "authFactorPasswordDescription": "注册时选择设置的密码。",
"authFactorEmail": "电邮一次性验证码", "authFactorEmail": "电邮一次性验证码",
@ -578,8 +576,5 @@
"newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章", "newsReadingFromReader": "你正在从 HyperNet.Reader 阅读文章",
"newsReadingFromOriginal": "你正在阅读原始文章", "newsReadingFromOriginal": "你正在阅读原始文章",
"newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。", "newsDisclaimer": "本文由 HyperNet.Reader 从互联网上获取,我们不担保其内容的真实性,请自行判断。本文章的所有内容版权归原作者所有。",
"newsToday": "快讯", "newsToday": "快讯"
"totpPostSetup": "还有一件事",
"totpPostSetupDescription": "使用 Google Authenticator, Microsoft Authenticator, 1Password, Authy, Bitwarden 或其他支持 TOTP 的验证器扫描本 QR Code 来添加。",
"totpNeverShare": "永远不要分享这个 QR Code"
} }

View File

@ -53,14 +53,14 @@ PODS:
- Firebase/Messaging (11.6.0): - Firebase/Messaging (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.6.0) - FirebaseMessaging (~> 11.6.0)
- firebase_analytics (11.4.1): - firebase_analytics (11.4.0):
- Firebase/Analytics (= 11.6.0) - Firebase/Analytics (= 11.6.0)
- firebase_core - firebase_core
- Flutter - Flutter
- firebase_core (3.10.1): - firebase_core (3.10.0):
- Firebase/CoreOnly (= 11.6.0) - Firebase/CoreOnly (= 11.6.0)
- Flutter - Flutter
- firebase_messaging (15.2.1): - firebase_messaging (15.2.0):
- Firebase/Messaging (= 11.6.0) - Firebase/Messaging (= 11.6.0)
- firebase_core - firebase_core
- Flutter - Flutter
@ -382,9 +382,9 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
firebase_analytics: 13ea4ad8a42c5060bad7e6694304dabb8b02fe7e firebase_analytics: 07bd7cfbac54bfcdccf2bb2530f9a65486f7ef3f
firebase_core: e2aa06dbd854d961f8ce46c2e20933bee1bf2d2b firebase_core: feb37e79f775c2bd08dd35e02d83678291317e10
firebase_messaging: 96cf6d67121b3f39746b2a4f29a26c0eee4af70e firebase_messaging: e2f0ba891b1509668c07f5099761518a5af8fe3c
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2

View File

@ -1,10 +1,8 @@
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/auth.dart'; import 'package:surface/types/auth.dart';
@ -12,7 +10,7 @@ import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/loading_indicator.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
final Map<int, (String, String, IconData)> kFactorTypes = { final Map<int, (String, String, IconData)> _kFactorTypes = {
0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password), 0: ('authFactorPassword', 'authFactorPasswordDescription', Symbols.password),
1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email), 1: ('authFactorEmail', 'authFactorEmailDescription', Symbols.email),
2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer), 2: ('authFactorTOTP', 'authFactorTOTPDescription', Symbols.timer),
@ -27,12 +25,10 @@ class FactorSettingsScreen extends StatefulWidget {
} }
class _FactorSettingsScreenState extends State<FactorSettingsScreen> { class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
bool _isBusy = false;
List<SnAuthFactor>? _factors; List<SnAuthFactor>? _factors;
Future<void> _fetchFactors() async { Future<void> _fetchFactors() async {
try { try {
setState(() => _isBusy = true);
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/users/me/factors'); final resp = await sn.client.get('/cgi/id/users/me/factors');
_factors = List<SnAuthFactor>.from( _factors = List<SnAuthFactor>.from(
@ -42,7 +38,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
if (!mounted) return; if (!mounted) return;
context.showErrorDialog(err); context.showErrorDialog(err);
} finally { } finally {
setState(() => _isBusy = false); setState(() {});
} }
} }
@ -63,7 +59,7 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
LoadingIndicator( LoadingIndicator(
isActive: _isBusy, isActive: _factors == null,
), ),
ListTile( ListTile(
title: Text('authFactorAdd').tr(), title: Text('authFactorAdd').tr(),
@ -94,34 +90,10 @@ class _FactorSettingsScreenState extends State<FactorSettingsScreen> {
itemBuilder: (context, idx) { itemBuilder: (context, idx) {
final ele = _factors![idx]; final ele = _factors![idx];
return ListTile( return ListTile(
title: Text(kFactorTypes[ele.type]!.$1).tr(), title: Text(_kFactorTypes[ele.type]!.$1).tr(),
subtitle: Text(kFactorTypes[ele.type]!.$2).tr(), subtitle: Text(_kFactorTypes[ele.type]!.$2).tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 12), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: Icon(kFactorTypes[ele.type]!.$3), leading: Icon(_kFactorTypes[ele.type]!.$3),
trailing: IconButton(
icon: const Icon(Symbols.close),
onPressed: ele.type > 0
? () {
context
.showConfirmDialog(
'authFactorDelete'.tr(),
'authFactorDeleteDescription'.tr(args: [kFactorTypes[ele.type]!.$1.tr()]),
)
.then((val) async {
if (!val) return;
try {
if (!context.mounted) return;
final sn = context.read<SnNetworkProvider>();
await sn.client.delete('/cgi/id/users/me/factors/${ele.id}');
_fetchFactors();
} catch (err) {
if (!context.mounted) return;
context.showErrorDialog(err);
}
});
}
: null,
),
); );
}, },
), ),
@ -154,14 +126,7 @@ class _FactorNewDialogState extends State<_FactorNewDialog> {
final resp = await sn.client.post('/cgi/id/users/me/factors', data: { final resp = await sn.client.post('/cgi/id/users/me/factors', data: {
'type': _factorType, 'type': _factorType,
}); });
final factor = SnAuthFactor.fromJson(resp.data); // TODO show qrcode when creating totp factor
if (!mounted) return;
if (factor.type == 2) {
await showModalBottomSheet(
context: context,
builder: (context) => _FactorTotpFactorDialog(factor: factor),
);
}
if (!mounted) return; if (!mounted) return;
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
} catch (err) { } catch (err) {
@ -189,7 +154,7 @@ class _FactorNewDialogState extends State<_FactorNewDialog> {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
value: _factorType, value: _factorType,
items: kFactorTypes.entries.map( items: _kFactorTypes.entries.map(
(ele) { (ele) {
final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key); final contains = widget.currentlyHave.map((ele) => ele.type).contains(ele.key);
return DropdownMenuItem<int>( return DropdownMenuItem<int>(
@ -234,61 +199,3 @@ class _FactorNewDialogState extends State<_FactorNewDialog> {
); );
} }
} }
class _FactorTotpFactorDialog extends StatelessWidget {
final SnAuthFactor factor;
const _FactorTotpFactorDialog({super.key, required this.factor});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: Text(
'totpPostSetup',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge,
).tr().width(280),
),
const Gap(4),
Center(
child: Text(
'totpPostSetupDescription',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall,
).tr().width(280),
),
const Gap(16),
QrImageView(
padding: EdgeInsets.zero,
data: factor.config!['url'],
errorCorrectionLevel: QrErrorCorrectLevel.H,
version: QrVersions.auto,
size: 160,
gapless: true,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.circle,
color: Theme.of(context).colorScheme.onSurface,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Theme.of(context).colorScheme.onSurface,
),
),
const Gap(16),
Center(
child: Text(
'totpNeverShare',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
).tr().bold().width(280),
),
],
),
);
}
}

View File

@ -7,7 +7,6 @@ import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
import 'package:surface/screens/account/factor_settings.dart';
import 'package:surface/types/auth.dart'; import 'package:surface/types/auth.dart';
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/navigation/app_scaffold.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart';
@ -15,6 +14,11 @@ import 'package:url_launcher/url_launcher_string.dart';
import '../../providers/websocket.dart'; import '../../providers/websocket.dart';
final Map<int, (String label, IconData icon, bool isOtp)> _factorLabelMap = {
0: ('authFactorPassword'.tr(), Symbols.password, false),
1: ('authFactorEmail'.tr(), Symbols.email, true),
};
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
@ -208,9 +212,7 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> {
controller: _passwordController, controller: _passwordController,
obscureText: true, obscureText: true,
autofillHints: [ autofillHints: [
widget.factor!.type == 0 (_factorLabelMap[widget.factor!.type]?.$3 ?? true) ? AutofillHints.password : AutofillHints.oneTimeCode
? AutofillHints.password
: AutofillHints.oneTimeCode
], ],
decoration: InputDecoration( decoration: InputDecoration(
isDense: true, isDense: true,
@ -265,8 +267,7 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
bool _isBusy = false; bool _isBusy = false;
int? _factorPicked; int? _factorPicked;
Color get _unFocusColor => Color get _unFocusColor => Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round());
void _performGetFactorCode() async { void _performGetFactorCode() async {
if (_factorPicked == null) return; if (_factorPicked == null) return;
@ -327,11 +328,11 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> {
), ),
), ),
secondary: Icon( secondary: Icon(
kFactorTypes[x.type]?.$3 ?? Symbols.question_mark, _factorLabelMap[x.type]?.$2 ?? Symbols.question_mark,
), ),
title: Text( title: Text(
kFactorTypes[x.type]?.$1 ?? 'unknown', _factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(),
).tr(), ),
enabled: !widget.ticket!.factorTrail.contains(x.id), enabled: !widget.ticket!.factorTrail.contains(x.id),
value: _factorPicked == x.id, value: _factorPicked == x.id,
onChanged: (value) { onChanged: (value) {
@ -407,13 +408,11 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
try { try {
final sn = context.read<SnNetworkProvider>(); final sn = context.read<SnNetworkProvider>();
final lookupResp = final lookupResp = await sn.client.get('/cgi/id/users/lookup?probe=$username');
await sn.client.get('/cgi/id/users/lookup?probe=$username');
await sn.client.post('/cgi/id/users/me/password-reset', data: { await sn.client.post('/cgi/id/users/me/password-reset', data: {
'user_id': lookupResp.data['id'], 'user_id': lookupResp.data['id'],
}); });
if (mounted) if (mounted) context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
context.showModalDialog('done'.tr(), 'signinResetPasswordSent'.tr());
} catch (err) { } catch (err) {
if (mounted) context.showErrorDialog(err); if (mounted) context.showErrorDialog(err);
} finally { } finally {
@ -438,8 +437,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
widget.onTicket(result.ticket); widget.onTicket(result.ticket);
// Pull factors // Pull factors
final factorResp = final factorResp = await sn.client.get('/cgi/id/auth/factors', queryParameters: {
await sn.client.get('/cgi/id/auth/factors', queryParameters: {
'ticketId': result.ticket!.id.toString(), 'ticketId': result.ticket!.id.toString(),
}); });
widget.onFactor( widget.onFactor(
@ -533,10 +531,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> {
'termAcceptNextWithAgree'.tr(), 'termAcceptNextWithAgree'.tr(),
textAlign: TextAlign.end, textAlign: TextAlign.end,
style: Theme.of(context).textTheme.bodySmall!.copyWith( style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context) color: Theme.of(context).colorScheme.onSurface.withAlpha((255 * 0.75).round()),
.colorScheme
.onSurface
.withAlpha((255 * 0.75).round()),
), ),
), ),
Material( Material(

View File

@ -7,7 +7,6 @@ import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/screens/post/post_detail.dart';
@ -97,8 +96,6 @@ class _ExploreScreenState extends State<ExploreScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = context.read<ConfigProvider>();
return AppScaffold( return AppScaffold(
floatingActionButtonLocation: ExpandableFab.location, floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab( floatingActionButton: ExpandableFab(
@ -246,10 +243,8 @@ class _ExploreScreenState extends State<ExploreScreen> {
), ),
openColor: Colors.transparent, openColor: Colors.transparent,
openElevation: 0, openElevation: 0,
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(0.75),
transitionType: ContainerTransitionType.fade, transitionType: ContainerTransitionType.fade,
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
),
closedShape: const RoundedRectangleBorder( closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),

View File

@ -51,7 +51,7 @@ class HomeScreen extends StatefulWidget {
} }
class _HomeScreenState extends State<HomeScreen> { class _HomeScreenState extends State<HomeScreen> {
late final List<HomeScreenDashEntry> kCards = [ static const List<HomeScreenDashEntry> kCards = [
HomeScreenDashEntry( HomeScreenDashEntry(
name: 'dashEntryRecommendation', name: 'dashEntryRecommendation',
child: _HomeDashRecommendationPostWidget(), child: _HomeDashRecommendationPostWidget(),
@ -69,7 +69,7 @@ class _HomeScreenState extends State<HomeScreen> {
HomeScreenDashEntry( HomeScreenDashEntry(
name: 'dashEntryTodayNews', name: 'dashEntryTodayNews',
child: _HomeDashTodayNews(), child: _HomeDashTodayNews(),
cols: MediaQuery.of(context).size.width >= 640 ? 3 : 2, cols: 2,
), ),
]; ];
@ -293,7 +293,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
Text( Text(
_article!.title, _article!.title,
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18), style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 18),
maxLines: MediaQuery.of(context).size.width >= 640 ? 2 : 1, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Text( Text(

View File

@ -175,57 +175,54 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
), ),
if (_articleFragment != null && _isReadingFromReader) if (_articleFragment != null && _isReadingFromReader)
Expanded( Expanded(
child: Container( child: SingleChildScrollView(
constraints: BoxConstraints(maxWidth: 640), child: Column(
child: SingleChildScrollView( crossAxisAlignment: CrossAxisAlignment.start,
child: Column( spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start, children: [
spacing: 8, Text(_article!.title, style: Theme.of(context).textTheme.titleLarge),
children: [ Builder(builder: (context) {
Text(_article!.title, style: Theme.of(context).textTheme.titleLarge), final htmlDescription = parse(_article!.description);
Builder(builder: (context) { return Text(
final htmlDescription = parse(_article!.description); htmlDescription.children.map((ele) => ele.text.trim()).join(),
return Text( style: Theme.of(context).textTheme.bodyMedium,
htmlDescription.children.map((ele) => ele.text.trim()).join(), );
style: Theme.of(context).textTheme.bodyMedium, }),
); Builder(builder: (context) {
}), final date = _article!.publishedAt ?? _article!.createdAt;
Builder(builder: (context) { return Row(
final date = _article!.publishedAt ?? _article!.createdAt; spacing: 2,
return Row( children: [
spacing: 2, Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
children: [ Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!), Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(), ],
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!), ).opacity(0.75);
], }),
).opacity(0.75); Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75),
}), const Divider(),
Text('newsDisclaimer').tr().textStyle(Theme.of(context).textTheme.bodySmall!).opacity(0.75), ..._parseHtmlToWidgets(_articleFragment!.children),
const Divider(), const Divider(),
..._parseHtmlToWidgets(_articleFragment!.children), InkWell(
const Divider(), child: Row(
InkWell( mainAxisSize: MainAxisSize.min,
child: Row( children: [
mainAxisSize: MainAxisSize.min, Text(
children: [ 'Reference from original website',
Text( style: TextStyle(decoration: TextDecoration.underline),
'Reference from original website', ),
style: TextStyle(decoration: TextDecoration.underline), const Gap(4),
), Icon(Icons.launch, size: 16),
const Gap(4), ],
Icon(Icons.launch, size: 16), ).opacity(0.85),
], onTap: () {
).opacity(0.85), launchUrlString(_article!.url);
onTap: () { },
launchUrlString(_article!.url); ),
}, Gap(MediaQuery.of(context).padding.bottom),
), ],
Gap(MediaQuery.of(context).padding.bottom), ).padding(horizontal: 12, vertical: 16),
], ),
).padding(horizontal: 12, vertical: 16),
),
).center(),
) )
else if (_article != null) else if (_article != null)
Expanded( Expanded(

View File

@ -70,16 +70,11 @@ class _NewsScreenState extends State<NewsScreen> {
sliver: SliverAppBar( sliver: SliverAppBar(
leading: AutoAppBarLeading(), leading: AutoAppBarLeading(),
title: Text('screenNews').tr(), title: Text('screenNews').tr(),
floating: true,
snap: true,
bottom: TabBar( bottom: TabBar(
isScrollable: true, isScrollable: true,
tabs: [ tabs: [
Tab(child: Text('newsAllSources'.tr()).textColor(Theme.of(context).appBarTheme.foregroundColor)), Tab(child: Text('newsAllSources'.tr())),
for (final source in _sources!) for (final source in _sources!) Tab(child: Text(source.label)),
Tab(
child: Text(source.label).textColor(Theme.of(context).appBarTheme.foregroundColor),
),
], ],
), ),
), ),
@ -151,87 +146,80 @@ class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> {
return MediaQuery.removePadding( return MediaQuery.removePadding(
context: context, context: context,
removeTop: true, removeTop: true,
child: Center( child: RefreshIndicator(
child: Container( onRefresh: _fetchArticles,
constraints: BoxConstraints(maxWidth: 640), child: InfiniteList(
child: RefreshIndicator( isLoading: _isBusy,
onRefresh: _fetchArticles, itemCount: _articles.length,
child: InfiniteList( hasReachedMax: _totalCount != null && _articles.length >= _totalCount!,
isLoading: _isBusy, onFetchData: () {
itemCount: _articles.length, _fetchArticles();
hasReachedMax: _totalCount != null && _articles.length >= _totalCount!, },
onFetchData: () { itemBuilder: (context, index) {
_fetchArticles(); final article = _articles[index];
},
itemBuilder: (context, index) {
final article = _articles[index];
final baseUri = Uri.parse(article.url); final baseUri = Uri.parse(article.url);
final baseUrl = '${baseUri.scheme}://${baseUri.host}'; final baseUrl = '${baseUri.scheme}://${baseUri.host}';
final htmlDescription = parse(article.description); final htmlDescription = parse(article.description);
final date = article.publishedAt ?? article.createdAt; final date = article.publishedAt ?? article.createdAt;
return Card( return Card(
child: InkWell( child: InkWell(
radius: 8, radius: 8,
onTap: () { onTap: () {
GoRouter.of(context).pushNamed( GoRouter.of(context).pushNamed(
'newsDetail', 'newsDetail',
pathParameters: {'hash': article.hash}, pathParameters: {'hash': article.hash},
); );
}, },
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg')) if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg'))
ClipRRect( ClipRRect(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topRight: Radius.circular(8), topRight: Radius.circular(8),
topLeft: Radius.circular(8), topLeft: Radius.circular(8),
), ),
child: AspectRatio( child: AspectRatio(
aspectRatio: 16 / 9, aspectRatio: 16 / 9,
child: Container( child: Container(
color: Theme.of(context).colorScheme.surfaceContainer, color: Theme.of(context).colorScheme.surfaceContainer,
child: AutoResizeUniversalImage( child: AutoResizeUniversalImage(
article.thumbnail.startsWith('http') article.thumbnail.startsWith('http') ? article.thumbnail : '$baseUrl/${article.thumbnail}',
? article.thumbnail
: '$baseUrl/${article.thumbnail}',
),
),
), ),
), ),
const Gap(16), ),
Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16), ),
const Gap(8), const Gap(16),
Text(htmlDescription.children.map((ele) => ele.text.trim()).join()) Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!).padding(horizontal: 16),
.textStyle(Theme.of(context).textTheme.bodyMedium!) const Gap(8),
.padding(horizontal: 16), Text(htmlDescription.children.map((ele) => ele.text.trim()).join())
const Gap(8), .textStyle(Theme.of(context).textTheme.bodyMedium!)
Row( .padding(horizontal: 16),
spacing: 2, const Gap(8),
children: [ Row(
Text(widget.allSources.where((x) => x.id == article.source).first.label) spacing: 2,
.textStyle(Theme.of(context).textTheme.bodySmall!), children: [
], Text(widget.allSources.where((x) => x.id == article.source).first.label)
).opacity(0.75).padding(horizontal: 16), .textStyle(Theme.of(context).textTheme.bodySmall!),
Row(
spacing: 2,
children: [
Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).opacity(0.75).padding(horizontal: 16),
const Gap(16),
], ],
), ).opacity(0.75).padding(horizontal: 16),
), Row(
); spacing: 2,
}, children: [
), Text(DateFormat().format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
), Text(' · ').textStyle(Theme.of(context).textTheme.bodySmall!).bold(),
Text(RelativeTime(context).format(date)).textStyle(Theme.of(context).textTheme.bodySmall!),
],
).opacity(0.75).padding(horizontal: 16),
const Gap(16),
],
),
),
);
},
), ),
), ),
); );

View File

@ -196,71 +196,68 @@ class _AttachmentListState extends State<AttachmentList> {
); );
} }
return AspectRatio( return Container(
aspectRatio: widget.data[0]?.data['ratio']?.toDouble() ?? 1, constraints: BoxConstraints(maxHeight: constraints.maxHeight),
child: Container( child: ScrollConfiguration(
constraints: BoxConstraints(maxHeight: constraints.maxHeight), behavior: _AttachmentListScrollBehavior(),
child: ScrollConfiguration( child: ListView.separated(
behavior: _AttachmentListScrollBehavior(), padding: widget.padding,
child: ListView.separated( shrinkWrap: true,
padding: widget.padding, itemCount: widget.data.length,
shrinkWrap: true, itemBuilder: (context, idx) {
itemCount: widget.data.length, return Container(
itemBuilder: (context, idx) { constraints: constraints.copyWith(maxWidth: widget.maxWidth),
return Container( child: AspectRatio(
constraints: constraints.copyWith(maxWidth: widget.maxWidth), aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(),
child: AspectRatio( child: GestureDetector(
aspectRatio: (widget.data[idx]?.data['ratio'] ?? 1).toDouble(), onTap: () {
child: GestureDetector( if (widget.data[idx]?.mediaType != SnMediaType.image) return;
onTap: () { context.pushTransparentRoute(
if (widget.data[idx]?.mediaType != SnMediaType.image) return; AttachmentZoomView(
context.pushTransparentRoute( data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(),
AttachmentZoomView( initialIndex: idx,
data: widget.data.where((ele) => ele != null && ele.mediaType == SnMediaType.image).cast(), heroTags: heroTags,
initialIndex: idx, ),
heroTags: heroTags, backgroundColor: Colors.black.withOpacity(0.7),
), rootNavigator: true,
backgroundColor: Colors.black.withOpacity(0.7), );
rootNavigator: true, },
); child: Stack(
}, fit: StackFit.expand,
child: Stack( children: [
fit: StackFit.expand, Container(
children: [ decoration: BoxDecoration(
Container( color: backgroundColor,
decoration: BoxDecoration( border: Border(
color: backgroundColor, top: borderSide,
border: Border( bottom: borderSide,
top: borderSide,
bottom: borderSide,
),
borderRadius: AttachmentList.kDefaultRadius,
), ),
child: ClipRRect( borderRadius: AttachmentList.kDefaultRadius,
borderRadius: AttachmentList.kDefaultRadius, ),
child: AttachmentItem( child: ClipRRect(
data: widget.data[idx], borderRadius: AttachmentList.kDefaultRadius,
heroTag: heroTags[idx], child: AttachmentItem(
), data: widget.data[idx],
heroTag: heroTags[idx],
), ),
), ),
Positioned( ),
right: 8, Positioned(
bottom: 8, right: 8,
child: Chip( bottom: 8,
label: Text('${idx + 1}/${widget.data.length}'), child: Chip(
), label: Text('${idx + 1}/${widget.data.length}'),
), ),
], ),
), ],
), ),
), ),
); ),
}, );
separatorBuilder: (context, index) => const Gap(8), },
physics: const BouncingScrollPhysics(), separatorBuilder: (context, index) => const Gap(8),
scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(),
), scrollDirection: Axis.horizontal,
), ),
), ),
); );

View File

@ -5,9 +5,10 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:surface/providers/config.dart';
// Keep this import to make the web image render work // Keep this import to make the web image render work
import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'; import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart';
import 'package:surface/providers/config.dart';
class UniversalImage extends StatelessWidget { class UniversalImage extends StatelessWidget {
final String url; final String url;

View File

@ -22,14 +22,14 @@ PODS:
- Firebase/Messaging (11.6.0): - Firebase/Messaging (11.6.0):
- Firebase/CoreOnly - Firebase/CoreOnly
- FirebaseMessaging (~> 11.6.0) - FirebaseMessaging (~> 11.6.0)
- firebase_analytics (11.4.1): - firebase_analytics (11.4.0):
- Firebase/Analytics (= 11.6.0) - Firebase/Analytics (= 11.6.0)
- firebase_core - firebase_core
- FlutterMacOS - FlutterMacOS
- firebase_core (3.10.1): - firebase_core (3.10.0):
- Firebase/CoreOnly (~> 11.6.0) - Firebase/CoreOnly (~> 11.6.0)
- FlutterMacOS - FlutterMacOS
- firebase_messaging (15.2.1): - firebase_messaging (15.2.0):
- Firebase/CoreOnly (~> 11.6.0) - Firebase/CoreOnly (~> 11.6.0)
- Firebase/Messaging (~> 11.6.0) - Firebase/Messaging (~> 11.6.0)
- firebase_core - firebase_core
@ -296,9 +296,9 @@ SPEC CHECKSUMS:
file_saver: 44e6fbf666677faf097302460e214e977fdd977b file_saver: 44e6fbf666677faf097302460e214e977fdd977b
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
Firebase: 374a441a91ead896215703a674d58cdb3e9d772b Firebase: 374a441a91ead896215703a674d58cdb3e9d772b
firebase_analytics: 91efc58e8e37964469fdd59ad11ba36bc97e75d6 firebase_analytics: 5249f87da6fed852901581aab2602e0280ec2fdb
firebase_core: 75e003524565fb5bd80c9960bc5892e8475821cd firebase_core: 6d9bb8b0ea817e8fe0d928177d42275b45fdba6f
firebase_messaging: 082a385eb98b5bb843a566cb30404859c4bd6e25 firebase_messaging: ae8e88b586e4d50abc7cac5bacf74d21967fd226
FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7 FirebaseAnalytics: 7114c698cac995602e3b1b96663473e50d54d6e7
FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa FirebaseCore: 48b0dd707581cf9c1a1220da68223fb0a562afaa
FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _flutterfire_internals name: _flutterfire_internals
sha256: e4f2a7ef31b0ab2c89d2bde35ef3e6e6aff1dce5e66069c6540b0e9cfe33ee6b sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.50" version: "1.3.49"
_macros: _macros:
dependency: transitive dependency: transitive
description: dart description: dart
@ -338,10 +338,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.8" version: "2.3.7"
dart_webrtc: dart_webrtc:
dependency: "direct main" dependency: "direct main"
description: description:
@ -418,10 +418,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: easy_localization name: easy_localization
sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12" sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7+1" version: "3.0.7"
easy_localization_loader: easy_localization_loader:
dependency: "direct main" dependency: "direct main"
description: description:
@ -538,34 +538,34 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_analytics name: firebase_analytics
sha256: eac382bbcd5ae78c1d1ce5619d13f5a7424429f4bf55df9e3ad5110da34d1060 sha256: "498c6cb8468e348a556709c745d92a52173ab3a9b906aa0593393f0787f201ea"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.4.1" version: "11.4.0"
firebase_analytics_platform_interface: firebase_analytics_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_analytics_platform_interface name: firebase_analytics_platform_interface
sha256: a34db46c367265c4c961626e4b128bfb7b7e50958e7add4c27ba103f5f81b9b0 sha256: ccbb350554e98afdb4b59852689292d194d31232a2647b5012a66622b3711df9
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.3.1" version: "4.3.0"
firebase_analytics_web: firebase_analytics_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_analytics_web name: firebase_analytics_web
sha256: b6b4cef08e45e4c7d48476d9fc49fe9577081809a59026fe95b1a1b1eea165fa sha256: "68e1f18fc16482c211c658e739c25f015b202a260d9ad8249c6d3d7963b8105f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.10+7" version: "0.5.10+6"
firebase_core: firebase_core:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_core name: firebase_core
sha256: d851c1ca98fd5a4c07c747f8c65dacc2edd84a4d9ac055d32a5f0342529069f5 sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.10.1" version: "3.10.0"
firebase_core_platform_interface: firebase_core_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -586,26 +586,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: firebase_messaging name: firebase_messaging
sha256: e20ea2a0ecf9b0971575ab3ab42a6e285a94e50092c555b090c1a588a81b4d54 sha256: "48a8a59197c1c5174060ba9aa1e0036e9b5a0d28a0cc22d19c1fcabc67fafe3c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.2.1" version: "15.2.0"
firebase_messaging_platform_interface: firebase_messaging_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_platform_interface name: firebase_messaging_platform_interface
sha256: c57a92b5ae1857ef4fe4ae2e73452b44d32e984e15ab8b53415ea1bb514bdabd sha256: "9770a8e91f54296829dcaa61ce9b7c2f9ae9abbf99976dd3103a60470d5264dd"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.6.1" version: "4.6.0"
firebase_messaging_web: firebase_messaging_web:
dependency: transitive dependency: transitive
description: description:
name: firebase_messaging_web name: firebase_messaging_web
sha256: "83694a990d8525d6b01039240b97757298369622ca0253ad0ebcfed221bf8ee0" sha256: "329ca4ef45ec616abe6f1d5e58feed0934a50840a65aa327052354ad3c64ed77"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.10.1" version: "3.10.0"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -830,10 +830,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_webrtc name: flutter_webrtc
sha256: "4c76069f724f79dc6e8739c8f16c2ee00ca30a1f8e3aa75c0e830f8183278f03" sha256: "188401cc3275bc4f1f965babdff6cac612a4b46572f1e49f49db8af5361d5712"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.7" version: "0.12.6"
freezed: freezed:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -878,18 +878,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: glob name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.2"
go_router: go_router:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: daf3ff5570f55396b2d2c9bf8136d7db3a8acf208ac0cef92a3ae2beb9a81550 sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.7.1" version: "14.6.3"
google_fonts: google_fonts:
dependency: "direct main" dependency: "direct main"
description: description:
@ -950,10 +950,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.2.2"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -1030,10 +1030,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_macos name: image_picker_macos
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.1+2" version: "0.2.1+1"
image_picker_platform_interface: image_picker_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1710,18 +1710,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: c59819dacc6669a1165d54d2735a9543f136f9b3cec94ca65cea6ab8dffc422e sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.3.5"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: "650584dcc0a39856f369782874e562efd002a9c94aec032412c9eb81419cce1f" sha256: "138b7bbbc7f59c56236e426c37afb8f78cbc57b094ac64c440e0bb90e380a4f5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.4" version: "2.4.2"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
@ -2171,10 +2171,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: web_socket_channel name: web_socket_channel
sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "3.0.1"
webrtc_interface: webrtc_interface:
dependency: transitive dependency: transitive
description: description:
@ -2187,10 +2187,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.10.1" version: "5.10.0"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2.2.2+58 version: 2.2.2+57
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4