✨ Daily sign
This commit is contained in:
@ -1,10 +1,15 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
|
||||
class PostListController extends GetxController {
|
||||
late final SharedPreferences _prefs;
|
||||
|
||||
String? author;
|
||||
|
||||
/// The polling source modifier.
|
||||
@ -19,9 +24,16 @@ class PostListController extends GetxController {
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
PostListController({this.author}) {
|
||||
_initPreferences();
|
||||
_initPagingController();
|
||||
}
|
||||
|
||||
void _initPreferences() {
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
_prefs = prefs;
|
||||
});
|
||||
}
|
||||
|
||||
/// Initialize a compatibility layer to paging controller
|
||||
void _initPagingController() {
|
||||
pagingController.addPageRequestListener(_onPagingControllerRequest);
|
||||
@ -96,6 +108,13 @@ class PostListController extends GetxController {
|
||||
final idx = <dynamic>{};
|
||||
postList.retainWhere((x) => idx.add(x.id));
|
||||
|
||||
var lastId = postList.map((x) => x.id).reduce(max);
|
||||
if (_prefs.containsKey('feed_last_read_at')) {
|
||||
final storedId = _prefs.getInt('feed_last_read_at') ?? 0;
|
||||
lastId = max(storedId, lastId);
|
||||
}
|
||||
_prefs.setInt('feed_last_read_at', lastId);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ import 'package:solian/bootstrapper.dart';
|
||||
import 'package:solian/firebase_options.dart';
|
||||
import 'package:solian/platform.dart';
|
||||
import 'package:solian/providers/attachment_uploader.dart';
|
||||
import 'package:solian/providers/daily_sign.dart';
|
||||
import 'package:solian/providers/link_expander.dart';
|
||||
import 'package:solian/providers/stickers.dart';
|
||||
import 'package:solian/providers/theme_switcher.dart';
|
||||
@ -129,6 +130,7 @@ class SolianApp extends StatelessWidget {
|
||||
Get.lazyPut(() => RealmProvider());
|
||||
Get.lazyPut(() => ChatCallProvider());
|
||||
Get.lazyPut(() => AttachmentUploaderController());
|
||||
Get.lazyPut(() => LinkExpandController());
|
||||
Get.lazyPut(() => LinkExpandProvider());
|
||||
Get.lazyPut(() => DailySignProvider());
|
||||
}
|
||||
}
|
||||
|
48
lib/models/daily_sign.dart
Normal file
48
lib/models/daily_sign.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:solian/models/account.dart';
|
||||
|
||||
part 'daily_sign.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class DailySignRecord {
|
||||
int id;
|
||||
DateTime createdAt;
|
||||
DateTime updatedAt;
|
||||
DateTime? deletedAt;
|
||||
Account account;
|
||||
int resultTier;
|
||||
int resultExperience;
|
||||
int accountId;
|
||||
|
||||
DailySignRecord({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.deletedAt,
|
||||
required this.resultTier,
|
||||
required this.resultExperience,
|
||||
required this.account,
|
||||
required this.accountId,
|
||||
});
|
||||
|
||||
factory DailySignRecord.fromJson(Map<String, dynamic> json) =>
|
||||
_$DailySignRecordFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$DailySignRecordToJson(this);
|
||||
|
||||
String get symbol => switch (resultTier) {
|
||||
0 => '大\n凶',
|
||||
1 => '凶',
|
||||
2 => '中\n平',
|
||||
3 => '吉',
|
||||
_ => '大\n吉',
|
||||
};
|
||||
|
||||
String get overviewSuggestion => switch (resultTier) {
|
||||
0 => '诸事不宜',
|
||||
1 => '有些不宜',
|
||||
2 => '平平淡淡',
|
||||
3 => '有些事宜',
|
||||
_ => '诸事皆宜',
|
||||
};
|
||||
}
|
33
lib/models/daily_sign.g.dart
Normal file
33
lib/models/daily_sign.g.dart
Normal file
@ -0,0 +1,33 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'daily_sign.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
DailySignRecord _$DailySignRecordFromJson(Map<String, dynamic> json) =>
|
||||
DailySignRecord(
|
||||
id: (json['id'] as num).toInt(),
|
||||
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),
|
||||
resultTier: (json['result_tier'] as num).toInt(),
|
||||
resultExperience: (json['result_experience'] as num).toInt(),
|
||||
account: Account.fromJson(json['account'] as Map<String, dynamic>),
|
||||
accountId: (json['account_id'] as num).toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$DailySignRecordToJson(DailySignRecord instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'account': instance.account.toJson(),
|
||||
'result_tier': instance.resultTier,
|
||||
'result_experience': instance.resultExperience,
|
||||
'account_id': instance.accountId,
|
||||
};
|
37
lib/providers/daily_sign.dart
Normal file
37
lib/providers/daily_sign.dart
Normal file
@ -0,0 +1,37 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:solian/exceptions/request.dart';
|
||||
import 'package:solian/exceptions/unauthorized.dart';
|
||||
import 'package:solian/models/daily_sign.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
|
||||
class DailySignProvider extends GetxController {
|
||||
Future<DailySignRecord?> getToday() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('id');
|
||||
|
||||
final resp = await client.get('/daily/today');
|
||||
if (resp.statusCode != 200 && resp.statusCode != 404) {
|
||||
throw RequestException(resp);
|
||||
} else if (resp.statusCode == 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DailySignRecord.fromJson(resp.body);
|
||||
}
|
||||
|
||||
Future<DailySignRecord> signToday() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (auth.isAuthorized.isFalse) throw const UnauthorizedException();
|
||||
|
||||
final client = auth.configureClient('id');
|
||||
|
||||
final resp = await client.post('/daily', {});
|
||||
if (resp.statusCode != 200) {
|
||||
throw RequestException(resp);
|
||||
}
|
||||
|
||||
return DailySignRecord.fromJson(resp.body);
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import 'package:get/get.dart';
|
||||
import 'package:solian/models/link.dart';
|
||||
import 'package:solian/services.dart';
|
||||
|
||||
class LinkExpandController extends GetxController {
|
||||
class LinkExpandProvider extends GetxController {
|
||||
final Map<String, LinkMeta?> _cachedResponse = {};
|
||||
|
||||
Future<LinkMeta?> expandLink(String url) async {
|
||||
|
@ -2,9 +2,19 @@ import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:solian/exts.dart';
|
||||
import 'package:solian/models/daily_sign.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/content/posts.dart';
|
||||
import 'package:solian/providers/daily_sign.dart';
|
||||
import 'package:solian/providers/websocket.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:solian/screens/account/notification.dart';
|
||||
import 'package:solian/widgets/posts/post_list.dart';
|
||||
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
@ -15,6 +25,57 @@ class DashboardScreen extends StatefulWidget {
|
||||
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
late final WebSocketProvider _ws = Get.find();
|
||||
late final PostProvider _posts = Get.find();
|
||||
late final DailySignProvider _dailySign = Get.find();
|
||||
|
||||
List<Post>? _currentPosts;
|
||||
|
||||
Future<void> _pullPosts() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final resp = await _posts.listRecommendations(0);
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
if (prefs.containsKey('feed_last_read_at')) {
|
||||
final id = prefs.getInt('feed_last_read_at')!;
|
||||
setState(() {
|
||||
_currentPosts = result.data
|
||||
?.map((e) => Post.fromJson(e))
|
||||
.where((x) => x.id > id)
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool _signingDaily = true;
|
||||
DailySignRecord? _signRecord;
|
||||
|
||||
Future<void> _pullDaily() async {
|
||||
try {
|
||||
_signRecord = await _dailySign.getToday();
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
|
||||
setState(() => _signingDaily = false);
|
||||
}
|
||||
|
||||
Future<void> _signDaily() async {
|
||||
setState(() => _signingDaily = true);
|
||||
|
||||
try {
|
||||
_signRecord = await _dailySign.signToday();
|
||||
} catch (e) {
|
||||
context.showErrorDialog(e);
|
||||
}
|
||||
|
||||
setState(() => _signingDaily = false);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pullPosts();
|
||||
_pullDaily();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -28,7 +89,65 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
Text('today'.tr, style: Theme.of(context).textTheme.headlineSmall),
|
||||
Text(DateFormat('yyyy/MM/dd').format(DateTime.now())),
|
||||
],
|
||||
).paddingOnly(top: 8, left: 18, right: 18),
|
||||
).paddingOnly(top: 8, left: 18, right: 18, bottom: 12),
|
||||
Card(
|
||||
child: ListTile(
|
||||
leading: AnimatedSwitcher(
|
||||
switchInCurve: Curves.fastOutSlowIn,
|
||||
switchOutCurve: Curves.fastOutSlowIn,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _signRecord == null
|
||||
? Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('dd').format(DateTime.now()),
|
||||
style:
|
||||
GoogleFonts.robotoMono(fontSize: 22, height: 1.2),
|
||||
),
|
||||
Text(
|
||||
DateFormat('yy/MM').format(DateTime.now()),
|
||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Text(
|
||||
_signRecord!.symbol,
|
||||
style: GoogleFonts.notoSerifHk(fontSize: 20, height: 1),
|
||||
).paddingSymmetric(horizontal: 9),
|
||||
).paddingOnly(left: 4),
|
||||
title: _signRecord == null
|
||||
? const Text('诸事不宜')
|
||||
: Text(_signRecord!.overviewSuggestion),
|
||||
subtitle: _signRecord == null
|
||||
? const Text('今日未拜访佛祖')
|
||||
: Text('+${_signRecord!.resultExperience} EXP'),
|
||||
trailing: AnimatedSwitcher(
|
||||
switchInCurve: Curves.fastOutSlowIn,
|
||||
switchOutCurve: Curves.fastOutSlowIn,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _signRecord == null
|
||||
? IconButton(
|
||||
tooltip: '上香求签',
|
||||
icon: const Icon(Icons.local_fire_department),
|
||||
onPressed: _signingDaily ? null : _signDaily,
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
const Divider(thickness: 0.3).paddingSymmetric(vertical: 8),
|
||||
Obx(
|
||||
() => Column(
|
||||
@ -108,8 +227,72 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
),
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
],
|
||||
),
|
||||
).paddingOnly(bottom: 12),
|
||||
),
|
||||
if (_currentPosts?.isNotEmpty ?? false)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'feed'.tr,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 18),
|
||||
),
|
||||
Text(
|
||||
'notificationUnreadCount'.trParams({
|
||||
'count': (_currentPosts?.length ?? 0).toString(),
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
onPressed: () {
|
||||
AppRouter.instance.goNamed('feed');
|
||||
},
|
||||
),
|
||||
],
|
||||
).paddingOnly(left: 18, right: 18, bottom: 8),
|
||||
SizedBox(
|
||||
height: 360,
|
||||
width: width,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _currentPosts!.length,
|
||||
itemBuilder: (context, idx) {
|
||||
final item = _currentPosts![idx];
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Card(
|
||||
child: Card(
|
||||
child: PostListEntryWidget(
|
||||
item: item,
|
||||
isClickable: true,
|
||||
isShowEmbed: true,
|
||||
isNestedClickable: true,
|
||||
onUpdate: (_) {
|
||||
_pullPosts();
|
||||
},
|
||||
backgroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceContainerLow,
|
||||
),
|
||||
),
|
||||
).paddingSymmetric(horizontal: 8),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class _FeedScreenState extends State<FeedScreen>
|
||||
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
|
||||
return [
|
||||
SliverAppBar(
|
||||
title: AppBarTitle('home'.tr),
|
||||
title: AppBarTitle('feed'.tr),
|
||||
centerTitle: false,
|
||||
floating: true,
|
||||
toolbarHeight: SolianTheme.toolbarHeight(context),
|
||||
|
@ -60,7 +60,7 @@ class LinkExpansion extends StatelessWidget {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
final LinkExpandController expandController = Get.find();
|
||||
final LinkExpandProvider expandController = Get.find();
|
||||
|
||||
return Wrap(
|
||||
children: matches.map((x) {
|
||||
|
@ -4,9 +4,14 @@ import 'package:get/utils.dart';
|
||||
abstract class AppNavigation {
|
||||
static List<AppNavigationDestination> destinations = [
|
||||
AppNavigationDestination(
|
||||
icon: Icons.home,
|
||||
label: 'home'.tr,
|
||||
page: 'home',
|
||||
icon: Icons.dashboard,
|
||||
label: 'dashboard'.tr,
|
||||
page: 'dashboard',
|
||||
),
|
||||
AppNavigationDestination(
|
||||
icon: Icons.newspaper,
|
||||
label: 'feed'.tr,
|
||||
page: 'feed',
|
||||
),
|
||||
AppNavigationDestination(
|
||||
icon: Icons.workspaces,
|
||||
|
Reference in New Issue
Block a user