Basic daily sign in

This commit is contained in:
LittleSheep 2024-11-27 23:03:18 +08:00
parent dabb44635e
commit bd1d6b7be9
9 changed files with 665 additions and 21 deletions

View File

@ -238,5 +238,9 @@
"callScreenOff": "Turn off screen share",
"callScreenOn": "Turn on screen share",
"callMessageEnded": "Call lasted {}",
"callMessageStarted": "Call started"
"callMessageStarted": "Call started",
"dailyCheckIn": "Check In",
"dailyCheckInNone": "You haven't checked in today",
"dailyCheckAction": "Check in right now!",
"dailyCheckDetail": "Can't understand the talisman? Master, help me understand it!"
}

View File

@ -238,5 +238,9 @@
"callScreenOff": "关闭屏幕共享",
"callScreenOn": "开启屏幕共享",
"callMessageEnded": "通话持续了 {}",
"callMessageStarted": "通话开始了"
"callMessageStarted": "通话开始了",
"dailyCheckIn": "每日签到",
"dailyCheckInNone": "今日尚未签到",
"dailyCheckAction": "现在签到",
"dailyCheckDetail": "看不懂符?大师帮我解惑!"
}

View File

@ -70,6 +70,7 @@ final _appRoutes = [
path: '/search',
name: 'postSearch',
builder: (context, state) => const AppBackground(
isLessOptimization: true,
child: PostSearchScreen(),
),
),

View File

@ -1,7 +1,26 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:flutter/material.dart';
import 'package:surface/providers/sn_network.dart';
import 'package:surface/types/check_in.dart';
import 'package:surface/widgets/dialog.dart';
class HomeScreenDashEntry {
final String name;
final Widget child;
final int rows, cols;
const HomeScreenDashEntry({
required this.name,
required this.child,
this.rows = 1,
this.cols = 1,
});
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@ -11,29 +30,234 @@ class HomeScreen extends StatefulWidget {
}
class _HomeScreenState extends State<HomeScreen> {
static const List<HomeScreenDashEntry> kCards = [
HomeScreenDashEntry(
name: 'dashEntryCheckIn',
child: _HomeDashCheckInWidget(),
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("screenHome").tr(),
),
body: Column(
children: [
MaterialBanner(
leading: const Icon(Symbols.construction),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('nextVersionAlert').tr().bold(),
Text('nextVersionNotice').tr(),
body: SingleChildScrollView(
child: Column(
children: [
MaterialBanner(
leading: const Icon(Symbols.construction),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('nextVersionAlert').tr().bold(),
Text('nextVersionNotice').tr(),
],
).padding(vertical: 16),
actions: [
const SizedBox(),
],
).padding(vertical: 16),
actions: [
const SizedBox(),
],
),
],
),
StaggeredGrid.count(
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
children: kCards.map((card) {
return StaggeredGridTile.count(
crossAxisCellCount: card.cols,
mainAxisCellCount: card.rows,
child: card.child,
);
}).toList(),
).padding(all: 8),
],
),
),
);
}
}
class _HomeDashCheckInWidget extends StatefulWidget {
const _HomeDashCheckInWidget({super.key});
@override
State<_HomeDashCheckInWidget> createState() => _HomeDashCheckInWidgetState();
}
class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
bool _isBusy = false;
SnCheckInRecord? _todayRecord;
Future<void> _pullCheckIn() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.get('/cgi/id/check-in/today');
_todayRecord = SnCheckInRecord.fromJson(resp.data);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
Future<void> _doCheckIn() async {
setState(() => _isBusy = true);
try {
final sn = context.read<SnNetworkProvider>();
final resp = await sn.client.post('/cgi/id/check-in');
_todayRecord = SnCheckInRecord.fromJson(resp.data);
} catch (err) {
if (!mounted) return;
context.showErrorDialog(err);
} finally {
setState(() => _isBusy = false);
}
}
@override
void initState() {
super.initState();
_pullCheckIn();
}
@override
Widget build(BuildContext context) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: AnimatedSwitcher(
switchInCurve: Curves.fastOutSlowIn,
switchOutCurve: Curves.fastOutSlowIn,
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: _todayRecord == null
? Column(
key: Key('daily-check-in-overview-none'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'dailyCheckIn',
style: Theme.of(context).textTheme.titleLarge,
).tr(),
Text(
'dailyCheckInNone',
style: Theme.of(context).textTheme.bodyLarge,
).tr(),
],
)
: Column(
key: Key('daily-check-in-overview-has'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_todayRecord!.symbol,
style: GoogleFonts.notoSerifHk(
textStyle: Theme.of(context).textTheme.titleLarge,
),
),
Text(
'+${_todayRecord!.resultExperience} EXP',
style: Theme.of(context).textTheme.bodyLarge,
).tr(),
],
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
DateFormat('EEE\nMM/dd').format(DateTime.now().toUtc()),
).fontSize(13).opacity(0.75),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: AnimatedSwitcher(
switchInCurve: Curves.fastOutSlowIn,
switchOutCurve: Curves.fastOutSlowIn,
duration: const Duration(milliseconds: 300),
child: _todayRecord == null
? IconButton(
key: UniqueKey(),
tooltip: 'dailyCheckAction'.tr(),
icon: const Icon(Symbols.local_fire_department),
onPressed: _isBusy ? null : _doCheckIn,
)
: IconButton(
key: UniqueKey(),
tooltip: 'dailyCheckDetail'.tr(),
icon: const Icon(Symbols.help),
onPressed: _isBusy ? null : _doCheckIn,
),
),
),
],
),
],
).padding(all: 24),
);
}
}
class _HomeDashLinkWidget extends StatelessWidget {
final String title;
final String subtitle;
const _HomeDashLinkWidget({
super.key,
required this.title,
required this.subtitle,
});
@override
Widget build(BuildContext context) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge,
),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
Align(
alignment: Alignment.centerRight,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: IconButton(
icon: const Icon(Symbols.arrow_right_alt),
onPressed: () {},
),
),
)
],
).padding(all: 24),
);
}
}

31
lib/types/check_in.dart Normal file
View File

@ -0,0 +1,31 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'check_in.freezed.dart';
part 'check_in.g.dart';
@freezed
class SnCheckInRecord with _$SnCheckInRecord {
const SnCheckInRecord._();
const factory SnCheckInRecord({
required int id,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
required int resultTier,
required int resultExperience,
required List<int> resultModifiers,
required int accountId,
}) = _SnCheckInRecord;
factory SnCheckInRecord.fromJson(Map<String, dynamic> json) =>
_$SnCheckInRecordFromJson(json);
String get symbol => switch (resultTier) {
0 => '大凶',
1 => '',
2 => '中平',
3 => '',
_ => '大吉',
};
}

View File

@ -0,0 +1,334 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'check_in.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
SnCheckInRecord _$SnCheckInRecordFromJson(Map<String, dynamic> json) {
return _SnCheckInRecord.fromJson(json);
}
/// @nodoc
mixin _$SnCheckInRecord {
int get id => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
DateTime? get deletedAt => throw _privateConstructorUsedError;
int get resultTier => throw _privateConstructorUsedError;
int get resultExperience => throw _privateConstructorUsedError;
List<int> get resultModifiers => throw _privateConstructorUsedError;
int get accountId => throw _privateConstructorUsedError;
/// Serializes this SnCheckInRecord to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SnCheckInRecord
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SnCheckInRecordCopyWith<SnCheckInRecord> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SnCheckInRecordCopyWith<$Res> {
factory $SnCheckInRecordCopyWith(
SnCheckInRecord value, $Res Function(SnCheckInRecord) then) =
_$SnCheckInRecordCopyWithImpl<$Res, SnCheckInRecord>;
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
int resultTier,
int resultExperience,
List<int> resultModifiers,
int accountId});
}
/// @nodoc
class _$SnCheckInRecordCopyWithImpl<$Res, $Val extends SnCheckInRecord>
implements $SnCheckInRecordCopyWith<$Res> {
_$SnCheckInRecordCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SnCheckInRecord
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? resultTier = null,
Object? resultExperience = null,
Object? resultModifiers = null,
Object? accountId = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
resultTier: null == resultTier
? _value.resultTier
: resultTier // ignore: cast_nullable_to_non_nullable
as int,
resultExperience: null == resultExperience
? _value.resultExperience
: resultExperience // ignore: cast_nullable_to_non_nullable
as int,
resultModifiers: null == resultModifiers
? _value.resultModifiers
: resultModifiers // ignore: cast_nullable_to_non_nullable
as List<int>,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$SnCheckInRecordImplCopyWith<$Res>
implements $SnCheckInRecordCopyWith<$Res> {
factory _$$SnCheckInRecordImplCopyWith(_$SnCheckInRecordImpl value,
$Res Function(_$SnCheckInRecordImpl) then) =
__$$SnCheckInRecordImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int id,
DateTime createdAt,
DateTime updatedAt,
DateTime? deletedAt,
int resultTier,
int resultExperience,
List<int> resultModifiers,
int accountId});
}
/// @nodoc
class __$$SnCheckInRecordImplCopyWithImpl<$Res>
extends _$SnCheckInRecordCopyWithImpl<$Res, _$SnCheckInRecordImpl>
implements _$$SnCheckInRecordImplCopyWith<$Res> {
__$$SnCheckInRecordImplCopyWithImpl(
_$SnCheckInRecordImpl _value, $Res Function(_$SnCheckInRecordImpl) _then)
: super(_value, _then);
/// Create a copy of SnCheckInRecord
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? deletedAt = freezed,
Object? resultTier = null,
Object? resultExperience = null,
Object? resultModifiers = null,
Object? accountId = null,
}) {
return _then(_$SnCheckInRecordImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
deletedAt: freezed == deletedAt
? _value.deletedAt
: deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
resultTier: null == resultTier
? _value.resultTier
: resultTier // ignore: cast_nullable_to_non_nullable
as int,
resultExperience: null == resultExperience
? _value.resultExperience
: resultExperience // ignore: cast_nullable_to_non_nullable
as int,
resultModifiers: null == resultModifiers
? _value._resultModifiers
: resultModifiers // ignore: cast_nullable_to_non_nullable
as List<int>,
accountId: null == accountId
? _value.accountId
: accountId // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SnCheckInRecordImpl extends _SnCheckInRecord {
const _$SnCheckInRecordImpl(
{required this.id,
required this.createdAt,
required this.updatedAt,
required this.deletedAt,
required this.resultTier,
required this.resultExperience,
required final List<int> resultModifiers,
required this.accountId})
: _resultModifiers = resultModifiers,
super._();
factory _$SnCheckInRecordImpl.fromJson(Map<String, dynamic> json) =>
_$$SnCheckInRecordImplFromJson(json);
@override
final int id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
final DateTime? deletedAt;
@override
final int resultTier;
@override
final int resultExperience;
final List<int> _resultModifiers;
@override
List<int> get resultModifiers {
if (_resultModifiers is EqualUnmodifiableListView) return _resultModifiers;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_resultModifiers);
}
@override
final int accountId;
@override
String toString() {
return 'SnCheckInRecord(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, resultTier: $resultTier, resultExperience: $resultExperience, resultModifiers: $resultModifiers, accountId: $accountId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SnCheckInRecordImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.deletedAt, deletedAt) ||
other.deletedAt == deletedAt) &&
(identical(other.resultTier, resultTier) ||
other.resultTier == resultTier) &&
(identical(other.resultExperience, resultExperience) ||
other.resultExperience == resultExperience) &&
const DeepCollectionEquality()
.equals(other._resultModifiers, _resultModifiers) &&
(identical(other.accountId, accountId) ||
other.accountId == accountId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
createdAt,
updatedAt,
deletedAt,
resultTier,
resultExperience,
const DeepCollectionEquality().hash(_resultModifiers),
accountId);
/// Create a copy of SnCheckInRecord
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SnCheckInRecordImplCopyWith<_$SnCheckInRecordImpl> get copyWith =>
__$$SnCheckInRecordImplCopyWithImpl<_$SnCheckInRecordImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SnCheckInRecordImplToJson(
this,
);
}
}
abstract class _SnCheckInRecord extends SnCheckInRecord {
const factory _SnCheckInRecord(
{required final int id,
required final DateTime createdAt,
required final DateTime updatedAt,
required final DateTime? deletedAt,
required final int resultTier,
required final int resultExperience,
required final List<int> resultModifiers,
required final int accountId}) = _$SnCheckInRecordImpl;
const _SnCheckInRecord._() : super._();
factory _SnCheckInRecord.fromJson(Map<String, dynamic> json) =
_$SnCheckInRecordImpl.fromJson;
@override
int get id;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
@override
DateTime? get deletedAt;
@override
int get resultTier;
@override
int get resultExperience;
@override
List<int> get resultModifiers;
@override
int get accountId;
/// Create a copy of SnCheckInRecord
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SnCheckInRecordImplCopyWith<_$SnCheckInRecordImpl> get copyWith =>
throw _privateConstructorUsedError;
}

37
lib/types/check_in.g.dart Normal file
View File

@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'check_in.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$SnCheckInRecordImpl _$$SnCheckInRecordImplFromJson(
Map<String, dynamic> json) =>
_$SnCheckInRecordImpl(
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(),
resultModifiers: (json['result_modifiers'] as List<dynamic>)
.map((e) => (e as num).toInt())
.toList(),
accountId: (json['account_id'] as num).toInt(),
);
Map<String, dynamic> _$$SnCheckInRecordImplToJson(
_$SnCheckInRecordImpl instance) =>
<String, dynamic>{
'id': instance.id,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'result_tier': instance.resultTier,
'result_experience': instance.resultExperience,
'result_modifiers': instance.resultModifiers,
'account_id': instance.accountId,
};

View File

@ -612,10 +612,10 @@ packages:
dependency: "direct main"
description:
name: flutter_markdown
sha256: "999a4e3cb3e1532a971c86d6c73a480264f6a687959d4887cb4e2990821827e4"
sha256: "255b00afa1a7bad19727da6a7780cf3db6c3c12e68d302d85e0ff1fdf173db9e"
url: "https://pub.dev"
source: hosted
version: "0.7.4+2"
version: "0.7.4+3"
flutter_native_splash:
dependency: "direct dev"
description:
@ -640,6 +640,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.3"
flutter_staggered_grid_view:
dependency: "direct main"
description:
name: flutter_staggered_grid_view
sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -1014,10 +1022,10 @@ packages:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: "1dea2aef1c83434f832f14341a5ffa1254e76b68e4d90333f95f8a2643bf1024"
sha256: "8a57be605b8bc3fd57005eb776ede61f569214e48834258fb02ab80c7034b82c"
url: "https://pub.dev"
source: hosted
version: "4.2799.0"
version: "4.2800.0"
media_kit:
dependency: "direct main"
description:

View File

@ -91,6 +91,7 @@ dependencies:
livekit_client: ^2.3.0
wakelock_plus: ^1.2.8
permission_handler: ^11.3.1
flutter_staggered_grid_view: ^0.7.0
dev_dependencies:
flutter_test: