Enhanced profile edit

This commit is contained in:
2025-03-02 20:37:30 +08:00
parent 66aef44281
commit 72e6a6a1f6
22 changed files with 717 additions and 304 deletions

View File

@ -108,8 +108,7 @@ void main() async {
}
if (!kIsWeb && Platform.isAndroid) {
final ImagePickerPlatform imagePickerImplementation =
ImagePickerPlatform.instance;
final ImagePickerPlatform imagePickerImplementation = ImagePickerPlatform.instance;
if (imagePickerImplementation is ImagePickerAndroid) {
imagePickerImplementation.useAndroidPhotoPicker = true;
}
@ -228,8 +227,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
if (prefs.containsKey('first_boot_time')) {
final rawTime = prefs.getString('first_boot_time');
final time = DateTime.tryParse(rawTime ?? '');
if (time != null &&
time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
final inAppReview = InAppReview.instance;
if (prefs.getBool('rating_requested') == true) return;
if (await inAppReview.isAvailable()) {
@ -260,18 +258,12 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
final remoteVersionString = resp.data?['tag_name'] ?? '0.0.0+0';
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
final localVersion = Version.parse(localVersionString.split('+').first);
final remoteBuildNumber =
int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber =
int.tryParse(localVersionString.split('+').last) ?? 0;
logging.info(
"[Update] Local: $localVersionString, Remote: $remoteVersionString");
if ((remoteVersion > localVersion ||
remoteBuildNumber > localBuildNumber) &&
mounted) {
final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0;
logging.info("[Update] Local: $localVersionString, Remote: $remoteVersionString");
if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) {
final config = context.read<ConfigProvider>();
config.setUpdate(
remoteVersionString, resp.data?['body'] ?? 'No changelog');
config.setUpdate(remoteVersionString, resp.data?['body'] ?? 'No changelog');
logging.info("[Update] Update available: $remoteVersionString");
}
} catch (e) {
@ -363,9 +355,7 @@ class _AppSplashScreenState extends State<_AppSplashScreen> with TrayListener {
Future<void> _trayInitialization() async {
if (kIsWeb || Platform.isAndroid || Platform.isIOS) return;
final icon = Platform.isWindows
? 'assets/icon/tray-icon.ico'
: 'assets/icon/tray-icon.png';
final icon = Platform.isWindows ? 'assets/icon/tray-icon.ico' : 'assets/icon/tray-icon.png';
final appVersion = await PackageInfo.fromPlatform();
trayManager.addListener(this);

View File

@ -45,8 +45,7 @@ class AccountScreen extends StatelessWidget {
? Stack(
fit: StackFit.expand,
children: [
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner),
fit: BoxFit.cover),
AutoResizeUniversalImage(sn.getAttachmentUrl(ua.user!.banner), fit: BoxFit.cover),
Positioned(
top: 0,
left: 0,
@ -80,9 +79,7 @@ class AccountScreen extends StatelessWidget {
],
),
body: SingleChildScrollView(
child: ua.isAuthorized
? _AuthorizedAccountScreen()
: _UnauthorizedAccountScreen(),
child: ua.isAuthorized ? _AuthorizedAccountScreen() : _UnauthorizedAccountScreen(),
),
);
}
@ -118,15 +115,19 @@ class _AuthorizedAccountScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(ua.user!.nick)
.textStyle(Theme.of(context).textTheme.titleLarge!),
Text(ua.user!.nick).textStyle(Theme.of(context).textTheme.titleLarge!),
const Gap(4),
Text('@${ua.user!.name}')
.textStyle(Theme.of(context).textTheme.bodySmall!),
Text('@${ua.user!.name}').textStyle(Theme.of(context).textTheme.bodySmall!),
],
),
Text(ua.user!.description)
.textStyle(Theme.of(context).textTheme.bodyMedium!),
Text(
(ua.user!.profile?.description.isNotEmpty ?? false)
? ua.user!.profile!.description
: 'userNoDescription'.tr(),
style: (ua.user!.profile?.description.isEmpty ?? true)
? TextStyle(fontStyle: FontStyle.italic)
: null,
).textStyle(Theme.of(context).textTheme.bodyMedium!),
],
),
);
@ -225,9 +226,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget {
child: Icon(Symbols.waving_hand, size: 28),
),
const Gap(8),
Text('accountIntroTitle')
.tr()
.textStyle(Theme.of(context).textTheme.titleLarge!),
Text('accountIntroTitle').tr().textStyle(Theme.of(context).textTheme.titleLarge!),
Text('accountIntroSubtitle').tr(),
],
).padding(all: 20),

View File

@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:gap/gap.dart';
import 'package:image_picker/image_picker.dart';
import 'package:material_symbols_icons/symbols.dart';
@ -36,11 +37,16 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _descriptionController = TextEditingController();
final _timezoneController = TextEditingController();
final _genderController = TextEditingController();
final _pronounsController = TextEditingController();
final _locationController = TextEditingController();
final _birthdayController = TextEditingController();
String? _avatar;
String? _banner;
DateTime? _birthday;
List<(String, String)>? _links;
bool _isBusy = false;
@ -51,15 +57,21 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
final prof = ua.user!;
_usernameController.text = prof.name;
_nicknameController.text = prof.nick;
_descriptionController.text = prof.description;
_descriptionController.text = prof.profile!.description;
_firstNameController.text = prof.profile!.firstName;
_lastNameController.text = prof.profile!.lastName;
_timezoneController.text = prof.profile!.timeZone;
_genderController.text = prof.profile!.gender;
_pronounsController.text = prof.profile!.pronouns;
_locationController.text = prof.profile!.location;
_avatar = prof.avatar;
_banner = prof.banner;
if (prof.profile!.birthday != null) {
_links = prof.profile!.links.entries.map((ele) => (ele.key, ele.value)).toList();
_birthday = prof.profile!.birthday?.toLocal();
if(_birthday != null) {
_birthdayController.text = DateFormat(_kDateFormat).format(
prof.profile!.birthday!.toLocal(),
);
prof.profile!.birthday!.toLocal(),
);
}
}
@ -166,7 +178,14 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
'description': _descriptionController.value.text,
'first_name': _firstNameController.value.text,
'last_name': _lastNameController.value.text,
'time_zone': _timezoneController.value.text,
'gender': _genderController.value.text,
'pronouns': _pronounsController.value.text,
'location': _locationController.value.text,
'birthday': _birthday?.toUtc().toIso8601String(),
'links': {
for (final link in _links!.where((ele) => ele.$1.isNotEmpty && ele.$2.isNotEmpty)) link.$1: link.$2
},
},
);
@ -197,6 +216,10 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
_firstNameController.dispose();
_lastNameController.dispose();
_descriptionController.dispose();
_timezoneController.dispose();
_genderController.dispose();
_pronounsController.dispose();
_locationController.dispose();
_birthdayController.dispose();
super.dispose();
}
@ -262,6 +285,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
).padding(horizontal: padding),
const Gap(8 + 28),
Column(
spacing: 4,
children: [
TextField(
readOnly: true,
@ -271,16 +295,16 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
labelText: 'fieldUsername'.tr(),
helperText: 'fieldUsernameCannotEditHint'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _nicknameController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldNickname'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
Row(
children: [
Flexible(
@ -291,6 +315,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(),
labelText: 'fieldFirstName'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
const Gap(8),
@ -302,11 +327,38 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(),
labelText: 'fieldLastName'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
],
),
Row(
children: [
Flexible(
flex: 1,
child: TextField(
controller: _genderController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldGender'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
const Gap(4),
Flexible(
flex: 1,
child: TextField(
controller: _pronounsController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldPronouns'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
],
),
const Gap(4),
TextField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
@ -316,8 +368,51 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
border: const UnderlineInputBorder(),
labelText: 'fieldDescription'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: TextField(
controller: _timezoneController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldTimeZone'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
const Gap(4),
StyledWidget(IconButton(
icon: const Icon(Symbols.calendar_month),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
_timezoneController.text = await FlutterTimezone.getLocalTimezone();
},
)).padding(top: 6),
const Gap(4),
StyledWidget(IconButton(
icon: const Icon(Symbols.clear),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
_timezoneController.clear();
},
)).padding(top: 6),
],
),
TextField(
controller: _locationController,
decoration: InputDecoration(
border: const UnderlineInputBorder(),
labelText: 'fieldLocation'.tr(),
),
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const Gap(4),
TextField(
controller: _birthdayController,
readOnly: true,
@ -327,6 +422,75 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
),
onTap: () => _selectBirthday(),
),
if (_links != null)
Card(
margin: const EdgeInsets.only(top: 16, bottom: 4),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
'fieldLinks'.tr(),
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 17),
),
),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
icon: const Icon(Symbols.add),
onPressed: () {
setState(() => _links!.add(('', '')));
},
),
],
),
const Gap(8),
for (var idx = 0; idx < _links!.length; idx++)
Row(
children: [
Flexible(
flex: 1,
child: TextFormField(
initialValue: _links![idx].$1,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'fieldLinkName'.tr(),
),
onChanged: (value) {
_links![idx] = (value, _links![idx].$2);
},
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
const Gap(8),
Flexible(
flex: 1,
child: TextFormField(
initialValue: _links![idx].$2,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
labelText: 'fieldLinkUrl'.tr(),
),
onChanged: (value) {
_links![idx] = (_links![idx].$1, value);
},
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
),
),
],
),
],
),
),
),
],
).padding(horizontal: padding + 8),
const Gap(12),
@ -340,6 +504,7 @@ class _ProfileEditScreenState extends State<ProfileEditScreen> {
),
],
).padding(horizontal: padding),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),

View File

@ -406,7 +406,7 @@ class _UserScreenState extends State<UserScreen>
],
).padding(right: 8),
const Gap(12),
Text(_account!.description).padding(horizontal: 8),
Text(_account!.profile!.description).padding(horizontal: 8),
const Gap(4),
Card(
child: Row(

View File

@ -97,7 +97,7 @@ class _AccountPublisherEditScreenState extends State<AccountPublisherEditScreen>
_banner = ua.user!.banner;
_nickController.text = ua.user!.nick;
_nameController.text = ua.user!.name;
_descriptionController.text = ua.user!.description;
_descriptionController.text = ua.user!.profile!.description;
setState(() {});
}

View File

@ -109,7 +109,7 @@ class _PublisherNewPersonalState extends State<_PublisherNewPersonal> {
_nameController.text = ua.user!.name;
_nickController.text = ua.user!.nick;
_descriptionController.text = ua.user!.description;
_descriptionController.text = ua.user!.profile!.description;
}
@override

View File

@ -16,7 +16,6 @@ abstract class SnAccount with _$SnAccount {
required List<SnAccountContact>? contacts,
@Default("") String avatar,
@Default("") String banner,
required String description,
required String name,
required String nick,
@Default({}) Map<String, dynamic> permNodes,
@ -57,15 +56,21 @@ abstract class SnAccountContact with _$SnAccountContact {
abstract class SnAccountProfile with _$SnAccountProfile {
const factory SnAccountProfile({
required int id,
required int accountId,
required DateTime? birthday,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required int experience,
required String firstName,
required String lastName,
required String description,
required String timeZone,
required String location,
required String pronouns,
required String gender,
@Default({}) Map<String, String> links,
required int experience,
required DateTime? lastSeenAt,
required DateTime updatedAt,
required DateTime? birthday,
required int accountId,
}) = _SnAccountProfile;
factory SnAccountProfile.fromJson(Map<String, Object?> json) =>

View File

@ -23,7 +23,6 @@ mixin _$SnAccount {
List<SnAccountContact>? get contacts;
String get avatar;
String get banner;
String get description;
String get name;
String get nick;
Map<String, dynamic> get permNodes;
@ -63,8 +62,6 @@ mixin _$SnAccount {
const DeepCollectionEquality().equals(other.contacts, contacts) &&
(identical(other.avatar, avatar) || other.avatar == avatar) &&
(identical(other.banner, banner) || other.banner == banner) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.nick, nick) || other.nick == nick) &&
const DeepCollectionEquality().equals(other.permNodes, permNodes) &&
@ -96,7 +93,6 @@ mixin _$SnAccount {
const DeepCollectionEquality().hash(contacts),
avatar,
banner,
description,
name,
nick,
const DeepCollectionEquality().hash(permNodes),
@ -112,7 +108,7 @@ mixin _$SnAccount {
@override
String toString() {
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
}
}
@ -130,7 +126,6 @@ abstract mixin class $SnAccountCopyWith<$Res> {
List<SnAccountContact>? contacts,
String avatar,
String banner,
String description,
String name,
String nick,
Map<String, dynamic> permNodes,
@ -166,7 +161,6 @@ class _$SnAccountCopyWithImpl<$Res> implements $SnAccountCopyWith<$Res> {
Object? contacts = freezed,
Object? avatar = null,
Object? banner = null,
Object? description = null,
Object? name = null,
Object? nick = null,
Object? permNodes = null,
@ -212,10 +206,6 @@ class _$SnAccountCopyWithImpl<$Res> implements $SnAccountCopyWith<$Res> {
? _self.banner
: banner // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _self.description
: description // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _self.name
: name // ignore: cast_nullable_to_non_nullable
@ -290,7 +280,6 @@ class _SnAccount extends SnAccount {
required final List<SnAccountContact>? contacts,
this.avatar = "",
this.banner = "",
required this.description,
required this.name,
required this.nick,
final Map<String, dynamic> permNodes = const {},
@ -336,8 +325,6 @@ class _SnAccount extends SnAccount {
@JsonKey()
final String banner;
@override
final String description;
@override
final String name;
@override
final String nick;
@ -406,8 +393,6 @@ class _SnAccount extends SnAccount {
const DeepCollectionEquality().equals(other._contacts, _contacts) &&
(identical(other.avatar, avatar) || other.avatar == avatar) &&
(identical(other.banner, banner) || other.banner == banner) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.nick, nick) || other.nick == nick) &&
const DeepCollectionEquality()
@ -440,7 +425,6 @@ class _SnAccount extends SnAccount {
const DeepCollectionEquality().hash(_contacts),
avatar,
banner,
description,
name,
nick,
const DeepCollectionEquality().hash(_permNodes),
@ -456,7 +440,7 @@ class _SnAccount extends SnAccount {
@override
String toString() {
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, description: $description, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
return 'SnAccount(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, confirmedAt: $confirmedAt, contacts: $contacts, avatar: $avatar, banner: $banner, name: $name, nick: $nick, permNodes: $permNodes, language: $language, profile: $profile, badges: $badges, suspendedAt: $suspendedAt, affiliatedId: $affiliatedId, affiliatedTo: $affiliatedTo, automatedBy: $automatedBy, automatedId: $automatedId)';
}
}
@ -477,7 +461,6 @@ abstract mixin class _$SnAccountCopyWith<$Res>
List<SnAccountContact>? contacts,
String avatar,
String banner,
String description,
String name,
String nick,
Map<String, dynamic> permNodes,
@ -514,7 +497,6 @@ class __$SnAccountCopyWithImpl<$Res> implements _$SnAccountCopyWith<$Res> {
Object? contacts = freezed,
Object? avatar = null,
Object? banner = null,
Object? description = null,
Object? name = null,
Object? nick = null,
Object? permNodes = null,
@ -560,10 +542,6 @@ class __$SnAccountCopyWithImpl<$Res> implements _$SnAccountCopyWith<$Res> {
? _self.banner
: banner // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _self.description
: description // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _self.name
: name // ignore: cast_nullable_to_non_nullable
@ -954,15 +932,21 @@ class __$SnAccountContactCopyWithImpl<$Res>
/// @nodoc
mixin _$SnAccountProfile {
int get id;
int get accountId;
DateTime? get birthday;
DateTime get createdAt;
DateTime get updatedAt;
DateTime? get deletedAt;
int get experience;
String get firstName;
String get lastName;
String get description;
String get timeZone;
String get location;
String get pronouns;
String get gender;
Map<String, String> get links;
int get experience;
DateTime? get lastSeenAt;
DateTime get updatedAt;
DateTime? get birthday;
int get accountId;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@ -981,24 +965,34 @@ mixin _$SnAccountProfile {
(other.runtimeType == runtimeType &&
other is SnAccountProfile &&
(identical(other.id, id) || other.id == id) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId) &&
(identical(other.birthday, birthday) ||
other.birthday == birthday) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.experience, experience) ||
other.experience == experience) &&
(identical(other.firstName, firstName) ||
other.firstName == firstName) &&
(identical(other.lastName, lastName) ||
other.lastName == lastName) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.timeZone, timeZone) ||
other.timeZone == timeZone) &&
(identical(other.location, location) ||
other.location == location) &&
(identical(other.pronouns, pronouns) ||
other.pronouns == pronouns) &&
(identical(other.gender, gender) || other.gender == gender) &&
const DeepCollectionEquality().equals(other.links, links) &&
(identical(other.experience, experience) ||
other.experience == experience) &&
(identical(other.lastSeenAt, lastSeenAt) ||
other.lastSeenAt == lastSeenAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt));
(identical(other.birthday, birthday) ||
other.birthday == birthday) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@ -1006,19 +1000,25 @@ mixin _$SnAccountProfile {
int get hashCode => Object.hash(
runtimeType,
id,
accountId,
birthday,
createdAt,
updatedAt,
deletedAt,
experience,
firstName,
lastName,
description,
timeZone,
location,
pronouns,
gender,
const DeepCollectionEquality().hash(links),
experience,
lastSeenAt,
updatedAt);
birthday,
accountId);
@override
String toString() {
return 'SnAccountProfile(id: $id, accountId: $accountId, birthday: $birthday, createdAt: $createdAt, deletedAt: $deletedAt, experience: $experience, firstName: $firstName, lastName: $lastName, lastSeenAt: $lastSeenAt, updatedAt: $updatedAt)';
return 'SnAccountProfile(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, firstName: $firstName, lastName: $lastName, description: $description, timeZone: $timeZone, location: $location, pronouns: $pronouns, gender: $gender, links: $links, experience: $experience, lastSeenAt: $lastSeenAt, birthday: $birthday, accountId: $accountId)';
}
}
@ -1030,15 +1030,21 @@ abstract mixin class $SnAccountProfileCopyWith<$Res> {
@useResult
$Res call(
{int id,
int accountId,
DateTime? birthday,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
int experience,
String firstName,
String lastName,
String description,
String timeZone,
String location,
String pronouns,
String gender,
Map<String, String> links,
int experience,
DateTime? lastSeenAt,
DateTime updatedAt});
DateTime? birthday,
int accountId});
}
/// @nodoc
@ -1055,41 +1061,39 @@ class _$SnAccountProfileCopyWithImpl<$Res>
@override
$Res call({
Object? id = null,
Object? accountId = null,
Object? birthday = freezed,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? experience = null,
Object? firstName = null,
Object? lastName = null,
Object? description = null,
Object? timeZone = null,
Object? location = null,
Object? pronouns = null,
Object? gender = null,
Object? links = null,
Object? experience = null,
Object? lastSeenAt = freezed,
Object? updatedAt = null,
Object? birthday = freezed,
Object? accountId = null,
}) {
return _then(_self.copyWith(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as int,
accountId: null == accountId
? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
birthday: freezed == birthday
? _self.birthday
: birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,
createdAt: null == createdAt
? _self.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _self.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _self.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
experience: null == experience
? _self.experience
: experience // ignore: cast_nullable_to_non_nullable
as int,
firstName: null == firstName
? _self.firstName
: firstName // ignore: cast_nullable_to_non_nullable
@ -1098,14 +1102,46 @@ class _$SnAccountProfileCopyWithImpl<$Res>
? _self.lastName
: lastName // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _self.description
: description // ignore: cast_nullable_to_non_nullable
as String,
timeZone: null == timeZone
? _self.timeZone
: timeZone // ignore: cast_nullable_to_non_nullable
as String,
location: null == location
? _self.location
: location // ignore: cast_nullable_to_non_nullable
as String,
pronouns: null == pronouns
? _self.pronouns
: pronouns // ignore: cast_nullable_to_non_nullable
as String,
gender: null == gender
? _self.gender
: gender // ignore: cast_nullable_to_non_nullable
as String,
links: null == links
? _self.links
: links // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
experience: null == experience
? _self.experience
: experience // ignore: cast_nullable_to_non_nullable
as int,
lastSeenAt: freezed == lastSeenAt
? _self.lastSeenAt
: lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
updatedAt: null == updatedAt
? _self.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
birthday: freezed == birthday
? _self.birthday
: birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,
accountId: null == accountId
? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
@ -1115,38 +1151,64 @@ class _$SnAccountProfileCopyWithImpl<$Res>
class _SnAccountProfile implements SnAccountProfile {
const _SnAccountProfile(
{required this.id,
required this.accountId,
required this.birthday,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.experience,
required this.firstName,
required this.lastName,
required this.description,
required this.timeZone,
required this.location,
required this.pronouns,
required this.gender,
final Map<String, String> links = const {},
required this.experience,
required this.lastSeenAt,
required this.updatedAt});
required this.birthday,
required this.accountId})
: _links = links;
factory _SnAccountProfile.fromJson(Map<String, dynamic> json) =>
_$SnAccountProfileFromJson(json);
@override
final int id;
@override
final int accountId;
@override
final DateTime? birthday;
@override
final DateTime createdAt;
@override
final DateTime? deletedAt;
final DateTime updatedAt;
@override
final int experience;
final DateTime? deletedAt;
@override
final String firstName;
@override
final String lastName;
@override
final String description;
@override
final String timeZone;
@override
final String location;
@override
final String pronouns;
@override
final String gender;
final Map<String, String> _links;
@override
@JsonKey()
Map<String, String> get links {
if (_links is EqualUnmodifiableMapView) return _links;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_links);
}
@override
final int experience;
@override
final DateTime? lastSeenAt;
@override
final DateTime updatedAt;
final DateTime? birthday;
@override
final int accountId;
/// Create a copy of SnAccountProfile
/// with the given fields replaced by the non-null parameter values.
@ -1169,24 +1231,34 @@ class _SnAccountProfile implements SnAccountProfile {
(other.runtimeType == runtimeType &&
other is _SnAccountProfile &&
(identical(other.id, id) || other.id == id) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId) &&
(identical(other.birthday, birthday) ||
other.birthday == birthday) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.experience, experience) ||
other.experience == experience) &&
(identical(other.firstName, firstName) ||
other.firstName == firstName) &&
(identical(other.lastName, lastName) ||
other.lastName == lastName) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.timeZone, timeZone) ||
other.timeZone == timeZone) &&
(identical(other.location, location) ||
other.location == location) &&
(identical(other.pronouns, pronouns) ||
other.pronouns == pronouns) &&
(identical(other.gender, gender) || other.gender == gender) &&
const DeepCollectionEquality().equals(other._links, _links) &&
(identical(other.experience, experience) ||
other.experience == experience) &&
(identical(other.lastSeenAt, lastSeenAt) ||
other.lastSeenAt == lastSeenAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt));
(identical(other.birthday, birthday) ||
other.birthday == birthday) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@ -1194,19 +1266,25 @@ class _SnAccountProfile implements SnAccountProfile {
int get hashCode => Object.hash(
runtimeType,
id,
accountId,
birthday,
createdAt,
updatedAt,
deletedAt,
experience,
firstName,
lastName,
description,
timeZone,
location,
pronouns,
gender,
const DeepCollectionEquality().hash(_links),
experience,
lastSeenAt,
updatedAt);
birthday,
accountId);
@override
String toString() {
return 'SnAccountProfile(id: $id, accountId: $accountId, birthday: $birthday, createdAt: $createdAt, deletedAt: $deletedAt, experience: $experience, firstName: $firstName, lastName: $lastName, lastSeenAt: $lastSeenAt, updatedAt: $updatedAt)';
return 'SnAccountProfile(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, firstName: $firstName, lastName: $lastName, description: $description, timeZone: $timeZone, location: $location, pronouns: $pronouns, gender: $gender, links: $links, experience: $experience, lastSeenAt: $lastSeenAt, birthday: $birthday, accountId: $accountId)';
}
}
@ -1220,15 +1298,21 @@ abstract mixin class _$SnAccountProfileCopyWith<$Res>
@useResult
$Res call(
{int id,
int accountId,
DateTime? birthday,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
int experience,
String firstName,
String lastName,
String description,
String timeZone,
String location,
String pronouns,
String gender,
Map<String, String> links,
int experience,
DateTime? lastSeenAt,
DateTime updatedAt});
DateTime? birthday,
int accountId});
}
/// @nodoc
@ -1245,41 +1329,39 @@ class __$SnAccountProfileCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
$Res call({
Object? id = null,
Object? accountId = null,
Object? birthday = freezed,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? experience = null,
Object? firstName = null,
Object? lastName = null,
Object? description = null,
Object? timeZone = null,
Object? location = null,
Object? pronouns = null,
Object? gender = null,
Object? links = null,
Object? experience = null,
Object? lastSeenAt = freezed,
Object? updatedAt = null,
Object? birthday = freezed,
Object? accountId = null,
}) {
return _then(_SnAccountProfile(
id: null == id
? _self.id
: id // ignore: cast_nullable_to_non_nullable
as int,
accountId: null == accountId
? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
birthday: freezed == birthday
? _self.birthday
: birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,
createdAt: null == createdAt
? _self.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _self.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _self.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
experience: null == experience
? _self.experience
: experience // ignore: cast_nullable_to_non_nullable
as int,
firstName: null == firstName
? _self.firstName
: firstName // ignore: cast_nullable_to_non_nullable
@ -1288,14 +1370,46 @@ class __$SnAccountProfileCopyWithImpl<$Res>
? _self.lastName
: lastName // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _self.description
: description // ignore: cast_nullable_to_non_nullable
as String,
timeZone: null == timeZone
? _self.timeZone
: timeZone // ignore: cast_nullable_to_non_nullable
as String,
location: null == location
? _self.location
: location // ignore: cast_nullable_to_non_nullable
as String,
pronouns: null == pronouns
? _self.pronouns
: pronouns // ignore: cast_nullable_to_non_nullable
as String,
gender: null == gender
? _self.gender
: gender // ignore: cast_nullable_to_non_nullable
as String,
links: null == links
? _self._links
: links // ignore: cast_nullable_to_non_nullable
as Map<String, String>,
experience: null == experience
? _self.experience
: experience // ignore: cast_nullable_to_non_nullable
as int,
lastSeenAt: freezed == lastSeenAt
? _self.lastSeenAt
: lastSeenAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
updatedAt: null == updatedAt
? _self.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
birthday: freezed == birthday
? _self.birthday
: birthday // ignore: cast_nullable_to_non_nullable
as DateTime?,
accountId: null == accountId
? _self.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
));
}
}

View File

@ -21,7 +21,6 @@ _SnAccount _$SnAccountFromJson(Map<String, dynamic> json) => _SnAccount(
.toList(),
avatar: json['avatar'] as String? ?? "",
banner: json['banner'] as String? ?? "",
description: json['description'] as String,
name: json['name'] as String,
nick: json['nick'] as String,
permNodes: json['perm_nodes'] as Map<String, dynamic>? ?? const {},
@ -52,7 +51,6 @@ Map<String, dynamic> _$SnAccountToJson(_SnAccount instance) =>
'contacts': instance.contacts?.map((e) => e.toJson()).toList(),
'avatar': instance.avatar,
'banner': instance.banner,
'description': instance.description,
'name': instance.name,
'nick': instance.nick,
'perm_nodes': instance.permNodes,
@ -101,35 +99,50 @@ Map<String, dynamic> _$SnAccountContactToJson(_SnAccountContact instance) =>
_SnAccountProfile _$SnAccountProfileFromJson(Map<String, dynamic> json) =>
_SnAccountProfile(
id: (json['id'] as num).toInt(),
accountId: (json['account_id'] as num).toInt(),
birthday: json['birthday'] == null
? null
: DateTime.parse(json['birthday'] as String),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt: json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
experience: (json['experience'] as num).toInt(),
firstName: json['first_name'] as String,
lastName: json['last_name'] as String,
description: json['description'] as String,
timeZone: json['time_zone'] as String,
location: json['location'] as String,
pronouns: json['pronouns'] as String,
gender: json['gender'] as String,
links: (json['links'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
) ??
const {},
experience: (json['experience'] as num).toInt(),
lastSeenAt: json['last_seen_at'] == null
? null
: DateTime.parse(json['last_seen_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
birthday: json['birthday'] == null
? null
: DateTime.parse(json['birthday'] as String),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$SnAccountProfileToJson(_SnAccountProfile instance) =>
<String, dynamic>{
'id': instance.id,
'account_id': instance.accountId,
'birthday': instance.birthday?.toIso8601String(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'experience': instance.experience,
'first_name': instance.firstName,
'last_name': instance.lastName,
'description': instance.description,
'time_zone': instance.timeZone,
'location': instance.location,
'pronouns': instance.pronouns,
'gender': instance.gender,
'links': instance.links,
'experience': instance.experience,
'last_seen_at': instance.lastSeenAt?.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'birthday': instance.birthday?.toIso8601String(),
'account_id': instance.accountId,
};
_SnRelationship _$SnRelationshipFromJson(Map<String, dynamic> json) =>

View File

@ -92,10 +92,9 @@ class OpenablePostItem extends StatelessWidget {
openColor: Colors.transparent,
openElevation: 0,
transitionType: ContainerTransitionType.fade,
closedColor:
Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
),
closedColor: Theme.of(context).colorScheme.surfaceContainerLow.withOpacity(
cfg.prefs.getBool(kAppBackgroundStoreKey) == true ? 0.75 : 1,
),
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
@ -136,11 +135,9 @@ class PostItem extends StatelessWidget {
final box = context.findRenderObject() as RenderBox?;
final url = 'https://solsynth.dev/posts/${data.id}';
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
Share.shareUri(Uri.parse(url),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
Share.shareUri(Uri.parse(url), sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
} else {
Share.share(url,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
Share.share(url, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
}
}
@ -158,8 +155,7 @@ class PostItem extends StatelessWidget {
child: MultiProvider(
providers: [
Provider<SnNetworkProvider>(create: (_) => context.read()),
ChangeNotifierProvider<ConfigProvider>(
create: (_) => context.read()),
ChangeNotifierProvider<ConfigProvider>(create: (_) => context.read()),
],
child: ResponsiveBreakpoints.builder(
breakpoints: ResponsiveBreakpoints.of(context).breakpoints,
@ -187,8 +183,7 @@ class PostItem extends StatelessWidget {
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
} else {
await FileSaver.instance.saveFile(
name: 'Solar Network Post #${data.id}.png', file: imageFile);
await FileSaver.instance.saveFile(name: 'Solar Network Post #${data.id}.png', file: imageFile);
}
await imageFile.delete();
@ -202,9 +197,7 @@ class PostItem extends StatelessWidget {
final isAuthor = ua.isAuthorized && data.publisher.accountId == ua.user?.id;
// Video full view
if (showFullPost &&
data.type == 'video' &&
ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
if (showFullPost && data.type == 'video' && ResponsiveBreakpoints.of(context).largerThan(TABLET)) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -224,8 +217,7 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) {}
},
).padding(bottom: 8),
if (data.preload?.video != null)
_PostVideoPlayer(data: data).padding(bottom: 8),
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(bottom: 8),
_PostHeadline(data: data).padding(horizontal: 4, bottom: 8),
_PostFeaturedComment(data: data),
_PostBottomAction(
@ -273,8 +265,7 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) {}
},
).padding(horizontal: 12, top: 8, bottom: 8),
if (data.preload?.video != null)
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 4, left: 12, right: 12),
@ -317,13 +308,8 @@ class PostItem extends StatelessWidget {
],
),
),
Text('postArticle')
.tr()
.fontSize(13)
.opacity(0.75)
.padding(horizontal: 24, bottom: 8),
_PostFeaturedComment(data: data, maxWidth: maxWidth)
.padding(horizontal: 12),
Text('postArticle').tr().fontSize(13).opacity(0.75).padding(horizontal: 24, bottom: 8),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
_PostBottomAction(
data: data,
showComments: showComments,
@ -338,8 +324,7 @@ class PostItem extends StatelessWidget {
}
final displayableAttachments = data.preload?.attachments
?.where((ele) =>
ele?.mediaType != SnMediaType.image || data.type != 'article')
?.where((ele) => ele?.mediaType != SnMediaType.image || data.type != 'article')
.toList();
final cfg = context.read<ConfigProvider>();
@ -364,13 +349,9 @@ class PostItem extends StatelessWidget {
if (onDeleted != null) onDeleted!();
},
).padding(horizontal: 12, vertical: 8),
if (data.preload?.video != null)
_PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
if (data.type == 'question')
_PostQuestionHint(data: data)
.padding(horizontal: 16, bottom: 8),
if (data.body['title'] != null ||
data.body['description'] != null)
if (data.preload?.video != null) _PostVideoPlayer(data: data).padding(horizontal: 12, bottom: 8),
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
if (data.body['title'] != null || data.body['description'] != null)
_PostHeadline(
data: data,
isEnlarge: data.type == 'article' && showFullPost,
@ -384,8 +365,7 @@ class PostItem extends StatelessWidget {
if (data.repostTo != null)
_PostQuoteContent(child: data.repostTo!).padding(
horizontal: 12,
bottom:
data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
bottom: data.preload?.attachments?.isNotEmpty ?? false ? 12 : 0,
),
if (data.visibility > 0)
_PostVisibilityHint(data: data).padding(
@ -397,9 +377,7 @@ class PostItem extends StatelessWidget {
horizontal: 16,
vertical: 4,
),
if (data.tags.isNotEmpty)
_PostTagsList(data: data)
.padding(horizontal: 16, top: 4, bottom: 6),
if (data.tags.isNotEmpty) _PostTagsList(data: data).padding(horizontal: 16, top: 4, bottom: 6),
],
),
),
@ -412,16 +390,12 @@ class PostItem extends StatelessWidget {
fit: showFullPost ? BoxFit.cover : BoxFit.contain,
padding: const EdgeInsets.symmetric(horizontal: 12),
),
if (data.preload?.poll != null)
PostPoll(poll: data.preload!.poll!)
.padding(horizontal: 12, vertical: 4),
if (data.body['content'] != null &&
(cfg.prefs.getBool(kAppExpandPostLink) ?? true))
if (data.preload?.poll != null) PostPoll(poll: data.preload!.poll!).padding(horizontal: 12, vertical: 4),
if (data.body['content'] != null && (cfg.prefs.getBool(kAppExpandPostLink) ?? true))
LinkPreviewWidget(
text: data.body['content'],
).padding(horizontal: 4),
_PostFeaturedComment(data: data, maxWidth: maxWidth)
.padding(horizontal: 12),
_PostFeaturedComment(data: data, maxWidth: maxWidth).padding(horizontal: 12),
Container(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Column(
@ -483,8 +457,7 @@ class PostShareImageWidget extends StatelessWidget {
showMenu: false,
isRelativeDate: false,
).padding(horizontal: 16, bottom: 8),
if (data.type == 'question')
_PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
if (data.type == 'question') _PostQuestionHint(data: data).padding(horizontal: 16, bottom: 8),
_PostHeadline(
data: data,
isEnlarge: data.type == 'article',
@ -499,8 +472,7 @@ class PostShareImageWidget extends StatelessWidget {
child: data.repostTo!,
isRelativeDate: false,
).padding(horizontal: 16, bottom: 8),
if (data.type != 'article' &&
(data.preload?.attachments?.isNotEmpty ?? false))
if (data.type != 'article' && (data.preload?.attachments?.isNotEmpty ?? false))
StyledWidget(AttachmentList(
data: data.preload!.attachments!,
columned: true,
@ -509,8 +481,7 @@ class PostShareImageWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (data.visibility > 0) _PostVisibilityHint(data: data),
if (data.body['content_truncated'] == true)
_PostTruncatedHint(data: data),
if (data.body['content_truncated'] == true) _PostTruncatedHint(data: data),
],
).padding(horizontal: 16),
_PostBottomAction(
@ -570,8 +541,7 @@ class PostShareImageWidget extends StatelessWidget {
version: QrVersions.auto,
size: 100,
gapless: true,
embeddedImage:
AssetImage('assets/icon/icon-light-radius.png'),
embeddedImage: AssetImage('assets/icon/icon-light-radius.png'),
embeddedImageStyle: QrEmbeddedImageStyle(
size: Size(28, 28),
),
@ -602,11 +572,9 @@ class _PostQuestionHint extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
children: [
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle,
size: 20),
Icon(data.body['answer'] == null ? Symbols.help : Symbols.check_circle, size: 20),
const Gap(4),
if (data.body['answer'] == null &&
data.body['reward']?.toDouble() != null)
if (data.body['answer'] == null && data.body['reward']?.toDouble() != null)
Text('postQuestionUnansweredWithReward'.tr(args: [
'${data.body['reward']}',
])).opacity(0.75)
@ -642,9 +610,7 @@ class _PostBottomAction extends StatelessWidget {
);
final String? mostTypicalReaction = data.metric.reactionList.isNotEmpty
? data.metric.reactionList.entries
.reduce((a, b) => a.value > b.value ? a : b)
.key
? data.metric.reactionList.entries.reduce((a, b) => a.value > b.value ? a : b).key
: null;
return Row(
@ -658,8 +624,7 @@ class _PostBottomAction extends StatelessWidget {
InkWell(
child: Row(
children: [
if (mostTypicalReaction == null ||
kTemplateReactions[mostTypicalReaction] == null)
if (mostTypicalReaction == null || kTemplateReactions[mostTypicalReaction] == null)
Icon(Symbols.add_reaction, size: 20, color: iconColor)
else
Text(
@ -671,8 +636,7 @@ class _PostBottomAction extends StatelessWidget {
),
),
const Gap(8),
if (data.totalUpvote > 0 &&
data.totalUpvote >= data.totalDownvote)
if (data.totalUpvote > 0 && data.totalUpvote >= data.totalDownvote)
Text('postReactionUpvote').plural(
data.totalUpvote,
)
@ -691,12 +655,8 @@ class _PostBottomAction extends StatelessWidget {
data: data,
onChanged: (value, attr, delta) {
onChanged(data.copyWith(
totalUpvote: attr == 1
? data.totalUpvote + delta
: data.totalUpvote,
totalDownvote: attr == 2
? data.totalDownvote + delta
: data.totalDownvote,
totalUpvote: attr == 1 ? data.totalUpvote + delta : data.totalUpvote,
totalDownvote: attr == 2 ? data.totalDownvote + delta : data.totalDownvote,
metric: data.metric.copyWith(reactionList: value),
));
},
@ -803,7 +763,7 @@ class _PostHeadline extends StatelessWidget {
children: [
Text(
'articleWrittenAt'.tr(
args: [DateFormat('y/M/d HH:mm').format(data.createdAt)],
args: [DateFormat('y/M/d HH:mm').format(data.createdAt.toLocal())],
),
style: TextStyle(fontSize: 13),
),
@ -811,7 +771,7 @@ class _PostHeadline extends StatelessWidget {
if (data.editedAt != null)
Text(
'articleEditedAt'.tr(
args: [DateFormat('y/M/d HH:mm').format(data.editedAt!)],
args: [DateFormat('y/M/d HH:mm').format(data.editedAt!.toLocal())],
),
style: TextStyle(fontSize: 13),
),
@ -944,10 +904,8 @@ class _PostContentHeader extends StatelessWidget {
const Gap(4),
Text(
isRelativeDate
? RelativeTime(context)
.format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm')
.format(data.publishedAt ?? data.createdAt),
? RelativeTime(context).format((data.publishedAt ?? data.createdAt).toLocal())
: DateFormat('y/M/d HH:mm').format((data.publishedAt ?? data.createdAt).toLocal()),
).fontSize(13),
],
).opacity(0.8),
@ -965,10 +923,8 @@ class _PostContentHeader extends StatelessWidget {
const Gap(4),
Text(
isRelativeDate
? RelativeTime(context)
.format(data.publishedAt ?? data.createdAt)
: DateFormat('y/M/d HH:mm')
.format(data.publishedAt ?? data.createdAt),
? RelativeTime(context).format((data.publishedAt ?? data.createdAt).toLocal())
: DateFormat('y/M/d HH:mm').format((data.publishedAt ?? data.createdAt).toLocal()),
).fontSize(13),
],
).opacity(0.8),
@ -1151,8 +1107,7 @@ class _PostContentBody extends StatelessWidget {
if (data.body['content'] == null) return const SizedBox.shrink();
final content = MarkdownTextContent(
isAutoWarp: data.type == 'story',
isEnlargeSticker:
RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
isEnlargeSticker: RegExp(r"^:([-\w]+):$").hasMatch(data.body['content'] ?? ''),
textScaler: isEnlarge ? TextScaler.linear(1.1) : null,
content: data.body['content'],
attachments: data.preload?.attachments,
@ -1201,12 +1156,10 @@ class _PostQuoteContent extends StatelessWidget {
onDeleted: () {},
).padding(bottom: 4),
_PostContentBody(data: child),
if (child.visibility > 0)
_PostVisibilityHint(data: child).padding(top: 4),
if (child.visibility > 0) _PostVisibilityHint(data: child).padding(top: 4),
],
).padding(horizontal: 16),
if (child.type != 'article' &&
(child.preload?.attachments?.isNotEmpty ?? false))
if (child.type != 'article' && (child.preload?.attachments?.isNotEmpty ?? false))
ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(8),
@ -1357,9 +1310,7 @@ class _PostTruncatedHint extends StatelessWidget {
const Gap(4),
Text('postReadEstimate').tr(args: [
'${Duration(
seconds: (data.body['content_length'] as num).toDouble() *
60 ~/
kHumanReadSpeed,
seconds: (data.body['content_length'] as num).toDouble() * 60 ~/ kHumanReadSpeed,
).inSeconds}s',
]),
],
@ -1398,8 +1349,7 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
// If this is a answered question, fetch the answer instead
if (widget.data.type == 'question' && widget.data.body['answer'] != null) {
final sn = context.read<SnNetworkProvider>();
final resp =
await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
final resp = await sn.client.get('/cgi/co/posts/${widget.data.body['answer']}');
_isAnswer = true;
setState(() => _featuredComment = SnPost.fromJson(resp.data));
return;
@ -1407,11 +1357,9 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get(
'/cgi/co/posts/${widget.data.id}/replies/featured',
queryParameters: {
'take': 1,
});
final resp = await sn.client.get('/cgi/co/posts/${widget.data.id}/replies/featured', queryParameters: {
'take': 1,
});
setState(() => _featuredComment = SnPost.fromJson(resp.data[0]));
} catch (err) {
if (!mounted) return;
@ -1440,9 +1388,7 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
width: double.infinity,
child: Material(
borderRadius: const BorderRadius.all(Radius.circular(8)),
color: _isAnswer
? Colors.green.withOpacity(0.5)
: Theme.of(context).colorScheme.surfaceContainerHigh,
color: _isAnswer ? Colors.green.withOpacity(0.5) : Theme.of(context).colorScheme.surfaceContainerHigh,
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(8)),
onTap: () {
@ -1462,17 +1408,11 @@ class _PostFeaturedCommentState extends State<_PostFeaturedComment> {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Gap(2),
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion,
size: 20),
Icon(_isAnswer ? Symbols.task_alt : Symbols.prompt_suggestion, size: 20),
const Gap(10),
Text(
_isAnswer
? 'postQuestionAnswerTitle'
: 'postFeaturedComment',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 15),
_isAnswer ? 'postQuestionAnswerTitle' : 'postFeaturedComment',
style: Theme.of(context).textTheme.titleMedium!.copyWith(fontSize: 15),
).tr(),
],
),
@ -1610,8 +1550,7 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
}
RegExp cleanThinkingRegExp = RegExp(r'<think>[\s\S]*?</think>');
setState(
() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
setState(() => _response = out.replaceAll(cleanThinkingRegExp, '').trim());
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
@ -1634,16 +1573,11 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
children: [
const Icon(Symbols.book_4_spark, size: 24),
const Gap(16),
Text('postGetInsightTitle',
style: Theme.of(context).textTheme.titleLarge)
.tr(),
Text('postGetInsightTitle', style: Theme.of(context).textTheme.titleLarge).tr(),
],
).padding(horizontal: 20, top: 16, bottom: 12),
const Gap(4),
Text('postGetInsightDescription',
style: Theme.of(context).textTheme.bodySmall)
.tr()
.padding(horizontal: 20),
Text('postGetInsightDescription', style: Theme.of(context).textTheme.bodySmall).tr().padding(horizontal: 20),
const Gap(4),
if (_response == null)
Expanded(
@ -1661,16 +1595,12 @@ class _PostGetInsightPopupState extends State<_PostGetInsightPopup> {
leading: const Icon(Symbols.info),
title: Text('aiThinkingProcess'.tr()),
tilePadding: const EdgeInsets.symmetric(horizontal: 20),
collapsedBackgroundColor:
Theme.of(context).colorScheme.surfaceContainerHigh,
collapsedBackgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh,
minTileHeight: 32,
children: [
SelectableText(
_thinkingProcess!,
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(fontStyle: FontStyle.italic),
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontStyle: FontStyle.italic),
).padding(horizontal: 20, vertical: 8),
],
).padding(vertical: 8),
@ -1707,8 +1637,7 @@ class _PostVideoPlayer extends StatelessWidget {
aspectRatio: 16 / 9,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: AttachmentItem(
data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
child: AttachmentItem(data: data.preload!.video!, heroTag: 'post-video-${data.id}'),
),
),
);