Merge pull request #170 from krawieck/feature/json-serializable-stores

This commit is contained in:
Filip Krawczyk 2021-02-25 21:38:09 +01:00 committed by GitHub
commit d516e635ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 464 additions and 134 deletions

View File

@ -1,21 +1,23 @@
## Unreleased
WARNING: due to some internal changes your local settings will be reset (logged out of accounts, removed instances, theme back to default)
### Added
- Added page with saved posts/comments. It can be accessed from the profile tab under the bookmark icon
- Added modlog page. Can be visited in the context of an instance or community from the about tab
- Added inbox page, that can be accessed by tapping bell in the home tab
- Added abillity to send private messages
- Added page with saved posts/comments. It can be accessed from the profile tab under the bookmark icon
- Added ability to send private messages
- Added modlog page. Can be visited in the context of an instance or community from the about tab
### Changed
- Titles on some pages, have an appear affect when scrolling down
- Long pressing comments now have a ripple effect
- Long pressing comments now has a ripple effect
- Nerd stuff now contains more nerd stuff
### Fixed
- Time of posts is now displayed properly. Unless you live in UTC zone, then you won't notice a difference.
- Time of posts is now displayed properly. Unless you live in UTC zone, then you won't notice a difference
- Fixed a bug where links would not work on Android 11
## v0.2.3 - 2021-02-09

View File

@ -19,21 +19,14 @@ import 'util/extensions/brightness.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final configStore = ConfigStore();
await configStore.load();
final accountsStore = AccountsStore();
await accountsStore.load();
final configStore = await ConfigStore.load();
final accountsStore = await AccountsStore.load();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: configStore,
),
ChangeNotifierProvider.value(
value: accountsStore,
),
ChangeNotifierProvider.value(value: configStore),
ChangeNotifierProvider.value(value: accountsStore),
],
child: const MyApp(),
),

View File

@ -373,7 +373,7 @@ class PrivateMessageTile extends HookWidget {
final val = await showCupertinoModalPopup<PrivateMessageView>(
context: context,
builder: (_) => WriteMessagePage.edit(pmv.value));
if (pmv != null) pmv.value = val;
if (val != null) pmv.value = val;
},
),
TileAction(

View File

@ -2,73 +2,57 @@ import 'dart:collection';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:lemmy_api_client/v2.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../util/unawaited.dart';
import 'shared_pref_keys.dart';
part 'accounts_store.g.dart';
/// Store that manages all accounts
@JsonSerializable()
class AccountsStore extends ChangeNotifier {
static const prefsKey = 'v1:AccountsStore';
static final _prefs = SharedPreferences.getInstance();
/// Map containing JWT tokens of specific users.
/// If a token is in this map, the user is considered logged in
/// for that account.
/// `tokens['instanceHost']['username']`
HashMap<String, HashMap<String, Jwt>> _tokens;
@protected
@JsonKey(defaultValue: {'lemmy.ml': {}})
Map<String, Map<String, Jwt>> tokens;
/// default account for a given instance
/// map where keys are instanceHosts and values are usernames
HashMap<String, String> _defaultAccounts;
@protected
@JsonKey(defaultValue: {})
Map<String, String> defaultAccounts;
/// default account for the app
/// It is in a form of `username@instanceHost`
String _defaultAccount;
@protected
String defaultAccount;
Future<void> load() async {
final prefs = await SharedPreferences.getInstance();
static Future<AccountsStore> load() async {
final prefs = await _prefs;
// I barely understand what I did. Long story short it casts a
// raw json into a nested ObservableMap
nestedMapsCast<T>(T f(String jwt)) => HashMap.of(
(jsonDecode(prefs.getString(SharedPrefKeys.tokens) ??
'{"lemmy.ml":{}}') as Map<String, dynamic>)
?.map(
(k, e) => MapEntry(
k,
HashMap.of(
(e as Map<String, dynamic>)?.map(
(k, e) => MapEntry(k, e == null ? null : f(e as String)),
),
),
),
),
);
// set saved settings or create defaults
_tokens = nestedMapsCast((json) => Jwt(json));
_defaultAccount = prefs.getString(SharedPrefKeys.defaultAccount);
_defaultAccounts = HashMap.of(Map.castFrom(
jsonDecode(prefs.getString(SharedPrefKeys.defaultAccounts) ?? 'null')
as Map<dynamic, dynamic> ??
{},
));
notifyListeners();
return _$AccountsStoreFromJson(
jsonDecode(prefs.getString(prefsKey) ?? '{}') as Map<String, dynamic>,
);
}
Future<void> save() async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _prefs;
await prefs.setString(SharedPrefKeys.defaultAccount, _defaultAccount);
await prefs.setString(
SharedPrefKeys.defaultAccounts, jsonEncode(_defaultAccounts));
await prefs.setString(SharedPrefKeys.tokens, jsonEncode(_tokens));
await prefs.setString(prefsKey, jsonEncode(_$AccountsStoreToJson(this)));
}
/// automatically sets default accounts
void _assignDefaultAccounts() {
// remove dangling defaults
_defaultAccounts.entries
defaultAccounts.entries
.map((dft) {
final instance = dft.key;
final username = dft.value;
@ -79,21 +63,21 @@ class AccountsStore extends ChangeNotifier {
}
})
.toList()
.forEach(_defaultAccounts.remove);
if (_defaultAccount != null) {
final instance = _defaultAccount.split('@')[1];
final username = _defaultAccount.split('@')[0];
.forEach(defaultAccounts.remove);
if (defaultAccount != null) {
final instance = defaultAccount.split('@')[1];
final username = defaultAccount.split('@')[0];
// if instance or username doesn't exist, remove
if (!instances.contains(instance) ||
!usernamesFor(instance).contains(username)) {
_defaultAccount = null;
defaultAccount = null;
}
}
// set local defaults
for (final instanceHost in instances) {
// if this instance is not in defaults
if (!_defaultAccounts.containsKey(instanceHost)) {
if (!defaultAccounts.containsKey(instanceHost)) {
// select first account in this instance, if any
if (!isAnonymousFor(instanceHost)) {
setDefaultAccountFor(instanceHost, usernamesFor(instanceHost).first);
@ -102,7 +86,7 @@ class AccountsStore extends ChangeNotifier {
}
// set global default
if (_defaultAccount == null) {
if (defaultAccount == null) {
// select first account of first instance
for (final instanceHost in instances) {
// select first account in this instance, if any
@ -114,19 +98,19 @@ class AccountsStore extends ChangeNotifier {
}
String get defaultUsername {
if (_defaultAccount == null) {
if (defaultAccount == null) {
return null;
}
return _defaultAccount.split('@')[0];
return defaultAccount.split('@')[0];
}
String get defaultInstanceHost {
if (_defaultAccount == null) {
if (defaultAccount == null) {
return null;
}
return _defaultAccount.split('@')[1];
return defaultAccount.split('@')[1];
}
String defaultUsernameFor(String instanceHost) {
@ -134,16 +118,16 @@ class AccountsStore extends ChangeNotifier {
return null;
}
return _defaultAccounts[instanceHost];
return defaultAccounts[instanceHost];
}
Jwt get defaultToken {
if (_defaultAccount == null) {
if (defaultAccount == null) {
return null;
}
final userTag = _defaultAccount.split('@');
return _tokens[userTag[1]][userTag[0]];
final userTag = defaultAccount.split('@');
return tokens[userTag[1]][userTag[0]];
}
Jwt defaultTokenFor(String instanceHost) {
@ -151,28 +135,20 @@ class AccountsStore extends ChangeNotifier {
return null;
}
return _tokens[instanceHost][_defaultAccounts[instanceHost]];
return tokens[instanceHost][defaultAccounts[instanceHost]];
}
/// returns token for user of a certain id
Jwt tokenForId(String instanceHost, int userId) =>
_tokens.containsKey(instanceHost)
? _tokens[instanceHost]
.values
.firstWhere((val) => val.payload.id == userId, orElse: () => null)
: null;
Jwt tokenFor(String instanceHost, String username) {
if (!usernamesFor(instanceHost).contains(username)) {
return null;
}
return _tokens[instanceHost][username];
return tokens[instanceHost][username];
}
/// sets globally default account
void setDefaultAccount(String instanceHost, String username) {
_defaultAccount = '$username@$instanceHost';
defaultAccount = '$username@$instanceHost';
notifyListeners();
save();
@ -180,7 +156,7 @@ class AccountsStore extends ChangeNotifier {
/// sets default account for given instance
void setDefaultAccountFor(String instanceHost, String username) {
_defaultAccounts[instanceHost] = username;
defaultAccounts[instanceHost] = username;
notifyListeners();
save();
@ -193,20 +169,20 @@ class AccountsStore extends ChangeNotifier {
return true;
}
return _tokens[instanceHost].isEmpty;
return tokens[instanceHost].isEmpty;
}
/// `true` if no added instance has an account assigned to it
bool get hasNoAccount => loggedInInstances.isEmpty;
Iterable<String> get instances => _tokens.keys;
Iterable<String> get instances => tokens.keys;
Iterable<String> get loggedInInstances =>
instances.where((e) => !isAnonymousFor(e));
/// Usernames that are assigned to a given instance
Iterable<String> usernamesFor(String instanceHost) =>
_tokens[instanceHost].keys;
tokens[instanceHost].keys;
/// adds a new account
/// if it's the first account ever the account is
@ -230,7 +206,7 @@ class AccountsStore extends ChangeNotifier {
final userData =
await lemmy.run(GetSite(auth: token.raw)).then((value) => value.myUser);
_tokens[instanceHost][userData.name] = token;
tokens[instanceHost][userData.name] = token;
_assignDefaultAccounts();
notifyListeners();
@ -257,7 +233,7 @@ class AccountsStore extends ChangeNotifier {
}
}
_tokens[instanceHost] = HashMap();
tokens[instanceHost] = HashMap();
_assignDefaultAccounts();
notifyListeners();
@ -266,7 +242,7 @@ class AccountsStore extends ChangeNotifier {
/// This also removes all accounts assigned to this instance
void removeInstance(String instanceHost) {
_tokens.remove(instanceHost);
tokens.remove(instanceHost);
_assignDefaultAccounts();
notifyListeners();
@ -274,7 +250,7 @@ class AccountsStore extends ChangeNotifier {
}
void removeAccount(String instanceHost, String username) {
_tokens[instanceHost].remove(username);
tokens[instanceHost].remove(username);
_assignDefaultAccounts();
notifyListeners();

View File

@ -0,0 +1,32 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'accounts_store.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AccountsStore _$AccountsStoreFromJson(Map<String, dynamic> json) {
return AccountsStore()
..tokens = (json['tokens'] as Map<String, dynamic>)?.map(
(k, e) => MapEntry(
k,
(e as Map<String, dynamic>)?.map(
(k, e) =>
MapEntry(k, e == null ? null : Jwt.fromJson(e as String)),
)),
) ??
{'lemmy.ml': {}}
..defaultAccounts = (json['defaultAccounts'] as Map<String, dynamic>)?.map(
(k, e) => MapEntry(k, e as String),
) ??
{}
..defaultAccount = json['defaultAccount'] as String;
}
Map<String, dynamic> _$AccountsStoreToJson(AccountsStore instance) =>
<String, dynamic>{
'tokens': instance.tokens,
'defaultAccounts': instance.defaultAccounts,
'defaultAccount': instance.defaultAccount,
};

View File

@ -1,12 +1,20 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'shared_pref_keys.dart';
part 'config_store.g.dart';
/// Store managing user-level configuration such as theme or language
@JsonSerializable()
class ConfigStore extends ChangeNotifier {
static const prefsKey = 'v1:ConfigStore';
static final _prefs = SharedPreferences.getInstance();
ThemeMode _theme;
@JsonKey(defaultValue: ThemeMode.system)
ThemeMode get theme => _theme;
set theme(ThemeMode theme) {
_theme = theme;
@ -15,6 +23,7 @@ class ConfigStore extends ChangeNotifier {
}
bool _amoledDarkMode;
@JsonKey(defaultValue: false)
bool get amoledDarkMode => _amoledDarkMode;
set amoledDarkMode(bool amoledDarkMode) {
_amoledDarkMode = amoledDarkMode;
@ -22,23 +31,17 @@ class ConfigStore extends ChangeNotifier {
save();
}
Future<void> load() async {
final prefs = await SharedPreferences.getInstance();
// load saved settings or create defaults
theme =
_themeModeFromString(prefs.getString(SharedPrefKeys.theme) ?? 'system');
amoledDarkMode = prefs.getBool(SharedPrefKeys.amoledDarkMode) ?? false;
notifyListeners();
static Future<ConfigStore> load() async {
final prefs = await _prefs;
return _$ConfigStoreFromJson(
jsonDecode(prefs.getString(prefsKey) ?? '{}') as Map<String, dynamic>,
);
}
Future<void> save() async {
final prefs = await SharedPreferences.getInstance();
final prefs = await _prefs;
await prefs.setString(SharedPrefKeys.theme, describeEnum(theme));
await prefs.setBool(SharedPrefKeys.amoledDarkMode, amoledDarkMode);
await prefs.setString(prefsKey, jsonEncode(_$ConfigStoreToJson(this)));
}
}
/// converts string to ThemeMode
ThemeMode _themeModeFromString(String theme) =>
ThemeMode.values.firstWhere((e) => describeEnum(e) == theme);

View File

@ -0,0 +1,58 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'config_store.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ConfigStore _$ConfigStoreFromJson(Map<String, dynamic> json) {
return ConfigStore()
..theme = _$enumDecodeNullable(_$ThemeModeEnumMap, json['theme']) ??
ThemeMode.system
..amoledDarkMode = json['amoledDarkMode'] as bool ?? false;
}
Map<String, dynamic> _$ConfigStoreToJson(ConfigStore instance) =>
<String, dynamic>{
'theme': _$ThemeModeEnumMap[instance.theme],
'amoledDarkMode': instance.amoledDarkMode,
};
T _$enumDecode<T>(
Map<T, dynamic> enumValues,
dynamic source, {
T unknownValue,
}) {
if (source == null) {
throw ArgumentError('A value must be provided. Supported values: '
'${enumValues.values.join(', ')}');
}
final value = enumValues.entries
.singleWhere((e) => e.value == source, orElse: () => null)
?.key;
if (value == null && unknownValue == null) {
throw ArgumentError('`$source` is not one of the supported values: '
'${enumValues.values.join(', ')}');
}
return value ?? unknownValue;
}
T _$enumDecodeNullable<T>(
Map<T, dynamic> enumValues,
dynamic source, {
T unknownValue,
}) {
if (source == null) {
return null;
}
return _$enumDecode<T>(enumValues, source, unknownValue: unknownValue);
}
const _$ThemeModeEnumMap = {
ThemeMode.system: 'system',
ThemeMode.light: 'light',
ThemeMode.dark: 'dark',
};

View File

@ -1,8 +0,0 @@
/// Collection of string constants that are keys to SharedPreferences
class SharedPrefKeys {
static const tokens = 'tokens';
static const defaultAccount = 'defaultAccount';
static const defaultAccounts = 'defaultAccounts';
static const theme = 'theme';
static const amoledDarkMode = 'amoledDarkMode';
}

View File

@ -29,10 +29,10 @@ class CommentWidget extends HookWidget {
final int depth;
final CommentTree commentTree;
final bool detached;
final UserMentionView userMentionView;
final bool wasVoted;
final bool canBeMarkedAsRead;
final bool hideOnRead;
final int userMentionId;
static const colors = [
Colors.pink,
@ -48,9 +48,9 @@ class CommentWidget extends HookWidget {
this.detached = false,
this.canBeMarkedAsRead = false,
this.hideOnRead = false,
}) : wasVoted =
(commentTree.comment.myVote ?? VoteType.none) != VoteType.none,
userMentionView = null;
this.userMentionId,
}) : wasVoted =
(commentTree.comment.myVote ?? VoteType.none) != VoteType.none;
CommentWidget.fromCommentView(
CommentView cv, {
@ -64,14 +64,15 @@ class CommentWidget extends HookWidget {
);
CommentWidget.fromUserMentionView(
this.userMentionView, {
this.hideOnRead = false,
}) : commentTree =
CommentTree(CommentView.fromJson(userMentionView.toJson())),
depth = 0,
wasVoted = (userMentionView.myVote ?? VoteType.none) != VoteType.none,
detached = true,
canBeMarkedAsRead = true;
UserMentionView userMentionView, {
bool hideOnRead = false,
}) : this(
CommentTree(CommentView.fromJson(userMentionView.toJson())),
hideOnRead: hideOnRead,
canBeMarkedAsRead: true,
detached: true,
userMentionId: userMentionView.userMention.id,
);
_showCommentInfo(BuildContext context) {
final com = commentTree.comment;
@ -278,6 +279,7 @@ class CommentWidget extends HookWidget {
_MarkAsRead(
commentTree.comment,
onChanged: (val) => isRead.value = val,
userMentionId: userMentionId,
),
if (detached)
TileAction(
@ -409,17 +411,20 @@ class CommentWidget extends HookWidget {
class _MarkAsRead extends HookWidget {
final CommentView commentView;
final ValueChanged<bool> onChanged;
final int userMentionId;
const _MarkAsRead(this.commentView, {this.onChanged});
const _MarkAsRead(
this.commentView, {
@required this.onChanged,
@required this.userMentionId,
}) : assert(commentView != null);
@override
Widget build(BuildContext context) {
final accStore = useAccountsStore();
final comment = commentView.comment;
final recipient = commentView.recipient;
final instanceHost = commentView.instanceHost;
final post = commentView.post;
final isRead = useState(comment.read);
final delayedRead = useDelayedLoading();
@ -431,9 +436,7 @@ class _MarkAsRead extends HookWidget {
query: MarkCommentAsRead(
commentId: comment.id,
read: !isRead.value,
auth: recipient != null
? accStore.tokenFor(instanceHost, recipient.name)?.raw
: accStore.tokenForId(instanceHost, post.creatorId)?.raw,
auth: accStore.defaultTokenFor(instanceHost)?.raw,
),
onSuccess: (val) {
isRead.value = val.commentView.comment.read;
@ -441,10 +444,26 @@ class _MarkAsRead extends HookWidget {
},
);
Future<void> handleMarkMentionAsSeen() => delayedAction<UserMentionView>(
context: context,
delayedLoading: delayedRead,
instanceHost: instanceHost,
query: MarkUserMentionAsRead(
userMentionId: userMentionId,
read: !isRead.value,
auth: accStore.defaultTokenFor(instanceHost)?.raw,
),
onSuccess: (val) {
isRead.value = val.userMention.read;
onChanged?.call(isRead.value);
},
);
return TileAction(
icon: Icons.check,
delayedLoading: delayedRead,
onPressed: handleMarkAsSeen,
onPressed:
userMentionId != null ? handleMarkMentionAsSeen : handleMarkAsSeen,
iconColor: isRead.value ? Theme.of(context).accentColor : null,
tooltip: 'mark as ${isRead.value ? 'un' : ''}read',
);

View File

@ -1,6 +1,20 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "14.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "0.41.2"
archive:
dependency: transitive
description:
@ -29,6 +43,62 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
build:
dependency: transitive
description:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.2"
build_config:
dependency: transitive
description:
name: build_config
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.5"
build_daemon:
dependency: transitive
description:
name: build_daemon
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.7"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.3"
build_runner:
dependency: "direct dev"
description:
name: build_runner
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.1"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.7"
built_collection:
dependency: transitive
description:
name: built_collection
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.2"
built_value:
dependency: transitive
description:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "7.1.0"
cached_network_image:
dependency: "direct main"
description:
@ -50,6 +120,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-nullsafety.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
clock:
dependency: transitive
description:
@ -57,6 +141,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0-nullsafety.1"
code_builder:
dependency: transitive
description:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "3.6.0"
collection:
dependency: transitive
description:
@ -85,6 +176,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
dart_style:
dependency: transitive
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.12"
effective_dart:
dependency: "direct dev"
description:
@ -120,6 +218,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "5.2.1"
fixnum:
dependency: transitive
description:
name: fixnum
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.11"
flutter:
dependency: "direct main"
description: flutter
@ -205,6 +310,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
graphs:
dependency: transitive
description:
name: graphs
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
http:
dependency: transitive
description:
@ -212,6 +331,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.2"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.0"
http_parser:
dependency: transitive
description:
@ -247,13 +373,34 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
json_annotation:
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.5"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.2"
json_annotation:
dependency: "direct main"
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.1"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
url: "https://pub.dartlang.org"
source: hosted
version: "3.5.1"
latinize:
dependency: transitive
description:
@ -268,6 +415,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.0"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.4"
markdown:
dependency: "direct main"
description:
@ -296,6 +450,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0-nullsafety.3"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7"
modal_bottom_sheet:
dependency: "direct main"
description:
@ -310,6 +471,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
node_interop:
dependency: transitive
description:
name: node_interop
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
node_io:
dependency: transitive
description:
name: node_io
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
octo_image:
dependency: transitive
description:
@ -317,6 +492,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.0"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
package_info:
dependency: "direct main"
description:
@ -401,6 +583,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
process:
dependency: transitive
description:
@ -415,6 +604,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "4.3.3"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.8"
quiver:
dependency: transitive
description:
name: quiver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
rxdart:
dependency: transitive
description:
@ -464,11 +674,32 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.2+3"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.9"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.4+1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_gen:
dependency: transitive
description:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.10+3"
source_span:
dependency: transitive
description:
@ -504,6 +735,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.1"
stream_transform:
dependency: transitive
description:
name: stream_transform
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
string_scanner:
dependency: transitive
description:
@ -539,6 +777,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.29"
timing:
dependency: transitive
description:
name: timing
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1+3"
typed_data:
dependency: transitive
description:
@ -602,6 +847,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0-nullsafety.3"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+15"
web_socket_channel:
dependency: transitive
description:

View File

@ -46,6 +46,7 @@ dependencies:
fuzzy: <1.0.0
lemmy_api_client: ^0.12.0
matrix4_transform: ^1.1.7
json_annotation: ^3.1.1
flutter:
sdk: flutter
@ -59,6 +60,8 @@ dev_dependencies:
sdk: flutter
effective_dart: ^1.0.0
flutter_launcher_icons: ^0.8.1
json_serializable: ^3.5.1
build_runner: ^1.11.1
flutter_icons:
android: true