295 lines
9.7 KiB
Dart
295 lines
9.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:gap/gap.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:island/models/account.dart';
|
|
import 'package:island/pods/network.dart';
|
|
import 'package:island/pods/userinfo.dart';
|
|
import 'package:island/screens/account/credits.dart';
|
|
import 'package:island/services/time.dart';
|
|
import 'package:island/widgets/account/leveling_progress.dart';
|
|
import 'package:island/widgets/account/stellar_program_tab.dart';
|
|
import 'package:island/widgets/app_scaffold.dart';
|
|
import 'package:easy_localization/easy_localization.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
|
|
import 'package:styled_widget/styled_widget.dart';
|
|
|
|
part 'leveling.g.dart';
|
|
|
|
@riverpod
|
|
class LevelingHistoryNotifier extends _$LevelingHistoryNotifier
|
|
with CursorPagingNotifierMixin<SnExperienceRecord> {
|
|
static const int _pageSize = 20;
|
|
|
|
@override
|
|
Future<CursorPagingData<SnExperienceRecord>> build() => fetch(cursor: null);
|
|
|
|
@override
|
|
Future<CursorPagingData<SnExperienceRecord>> fetch({
|
|
required String? cursor,
|
|
}) async {
|
|
final client = ref.read(apiClientProvider);
|
|
final offset = cursor == null ? 0 : int.parse(cursor);
|
|
|
|
final queryParams = {'offset': offset, 'take': _pageSize};
|
|
|
|
final response = await client.get(
|
|
'/id/accounts/me/leveling',
|
|
queryParameters: queryParams,
|
|
);
|
|
final total = int.parse(response.headers.value('X-Total') ?? '0');
|
|
final List<dynamic> data = response.data;
|
|
final records =
|
|
data.map((json) => SnExperienceRecord.fromJson(json)).toList();
|
|
|
|
final hasMore = offset + records.length < total;
|
|
final nextCursor = hasMore ? (offset + records.length).toString() : null;
|
|
|
|
return CursorPagingData(
|
|
items: records,
|
|
hasMore: hasMore,
|
|
nextCursor: nextCursor,
|
|
);
|
|
}
|
|
}
|
|
|
|
class LevelingScreen extends HookConsumerWidget {
|
|
const LevelingScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final user = ref.watch(userInfoProvider);
|
|
|
|
if (user.value == null) {
|
|
return AppScaffold(
|
|
appBar: AppBar(title: Text('levelingProgress'.tr())),
|
|
body: const Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
|
|
return DefaultTabController(
|
|
length: 3,
|
|
child: AppScaffold(
|
|
appBar: AppBar(
|
|
title: Text('levelingProgress'.tr()),
|
|
bottom: TabBar(
|
|
tabs: [
|
|
Tab(
|
|
child: Text(
|
|
'leveling'.tr(),
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
),
|
|
),
|
|
),
|
|
Tab(
|
|
child: Text(
|
|
'socialCredits'.tr(),
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
),
|
|
),
|
|
),
|
|
Tab(
|
|
child: Text(
|
|
'stellarProgram'.tr(),
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: Theme.of(context).appBarTheme.foregroundColor!,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
body: TabBarView(
|
|
children: [
|
|
_buildLevelingTab(context, ref, user.value!),
|
|
const SocialCreditsTab(),
|
|
const StellarProgramTab(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLevelingTab(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
SnAccount user,
|
|
) {
|
|
final currentLevel = user.profile.level;
|
|
final currentExp = user.profile.experience;
|
|
final progress = user.profile.levelingProgress;
|
|
|
|
return Center(
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
const SliverGap(20),
|
|
|
|
// Current Progress Card
|
|
SliverToBoxAdapter(
|
|
child: LevelingProgressCard(
|
|
level: currentLevel,
|
|
experience: currentExp,
|
|
progress: progress,
|
|
),
|
|
),
|
|
const SliverGap(24),
|
|
|
|
// Level Stairs Graph
|
|
SliverToBoxAdapter(
|
|
child: Text(
|
|
'levelProgress'.tr(),
|
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SliverGap(16),
|
|
|
|
SliverToBoxAdapter(
|
|
child: Card(
|
|
margin: EdgeInsets.zero,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
'${'levelingProgressLevel'.tr(args: [currentLevel.toString()])} / 120',
|
|
textAlign: TextAlign.start,
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
const Gap(8),
|
|
LinearProgressIndicator(
|
|
value: currentLevel / 120,
|
|
minHeight: 10,
|
|
stopIndicatorRadius: 0,
|
|
trackGap: 0,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
backgroundColor:
|
|
Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
borderRadius: BorderRadius.circular(32),
|
|
),
|
|
],
|
|
).padding(horizontal: 16, top: 16, bottom: 12),
|
|
),
|
|
),
|
|
const SliverGap(16),
|
|
// Leveling History
|
|
SliverToBoxAdapter(
|
|
child: Text(
|
|
'levelingHistory'.tr(),
|
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SliverGap(8),
|
|
PagingHelperSliverView(
|
|
provider: levelingHistoryNotifierProvider,
|
|
futureRefreshable: levelingHistoryNotifierProvider.future,
|
|
notifierRefreshable: levelingHistoryNotifierProvider.notifier,
|
|
contentBuilder:
|
|
(data, widgetCount, endItemView) => SliverList.builder(
|
|
itemCount: widgetCount,
|
|
itemBuilder: (context, index) {
|
|
if (index == widgetCount - 1) {
|
|
return endItemView;
|
|
}
|
|
final record = data.items[index];
|
|
return ListTile(
|
|
title: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(record.reason),
|
|
Row(
|
|
spacing: 4,
|
|
children: [
|
|
Text(
|
|
record.createdAt.formatRelative(context),
|
|
).fontSize(13),
|
|
Text('·').fontSize(13).bold(),
|
|
Text(
|
|
record.createdAt.formatSystem(),
|
|
).fontSize(13),
|
|
],
|
|
).opacity(0.8),
|
|
],
|
|
),
|
|
subtitle: Row(
|
|
spacing: 8,
|
|
children: [
|
|
Text(
|
|
'${record.delta > 0 ? '+' : ''}${record.delta} EXP',
|
|
),
|
|
if (record.bonusMultiplier != 1.0)
|
|
Text('x${record.bonusMultiplier}'),
|
|
],
|
|
),
|
|
minTileHeight: 56,
|
|
contentPadding: EdgeInsets.symmetric(horizontal: 4),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
SliverGap(20),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class LevelStairsPainter extends CustomPainter {
|
|
final int currentLevel;
|
|
final int totalLevels;
|
|
final Color primaryColor;
|
|
final Color surfaceColor;
|
|
final Color onSurfaceColor;
|
|
final double stairHeight;
|
|
final double stairWidth;
|
|
|
|
LevelStairsPainter({
|
|
required this.currentLevel,
|
|
required this.totalLevels,
|
|
required this.primaryColor,
|
|
required this.surfaceColor,
|
|
required this.onSurfaceColor,
|
|
required this.stairHeight,
|
|
required this.stairWidth,
|
|
});
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final paint =
|
|
Paint()
|
|
..color = surfaceColor.withOpacity(0.2)
|
|
..strokeWidth = 1.5
|
|
..style = PaintingStyle.stroke;
|
|
|
|
// Draw connecting lines between stairs
|
|
for (int i = 0; i < totalLevels - 1; i++) {
|
|
final startX = 20.0 + (i * (stairWidth + 8)) + stairWidth;
|
|
final startHeight =
|
|
40.0 + (i * 15.0); // Progressive height for current stair
|
|
final startY = size.height - (20.0 + startHeight);
|
|
|
|
final endX = 20.0 + ((i + 1) * (stairWidth + 8));
|
|
final endHeight =
|
|
40.0 + ((i + 1) * 15.0); // Progressive height for next stair
|
|
final endY = size.height - (20.0 + endHeight);
|
|
|
|
canvas.drawLine(Offset(startX, startY), Offset(endX, endY), paint);
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
}
|