feat(mining-app): improve UX with non-blocking splash and skeleton loading

- Optimize splash page: reduce wait to 500ms, refresh token in background
- Cache SharedPreferences instance to avoid blocking API requests
- Add global 401 handler to auto-redirect to login page
- Create shimmer loading components (ShimmerLoading, ShimmerBox, skeletons)
- Replace CircularProgressIndicator with skeleton screens across all pages
- Add keepAlive + auto-invalidation (5min) to providers to reduce API calls
- Fix trading page: invalidate globalStateProvider after trade for data sync

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-12 21:48:31 -08:00
parent 5e16adc1ec
commit b1525bdfa6
39 changed files with 624 additions and 88 deletions

View File

@ -765,7 +765,8 @@
"Bash(git -C \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\" push)",
"Bash(git -C \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\" add frontend/mining-app/lib/presentation/pages/splash/splash_page.dart frontend/mining-app/lib/presentation/providers/user_providers.dart)",
"Bash(git -C \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\" commit -m \"$\\(cat <<''EOF''\nfix\\(mining-app\\): update splash page theme and fix token refresh\n\n- Update splash_page.dart to orange theme \\(#FF6B00\\) matching other pages\n- Change app name from \"榴莲挖矿\" to \"榴莲生态\"\n- Fix refreshTokenIfNeeded to properly throw on failure instead of\n silently calling logout \\(which caused Riverpod ref errors\\)\n- Clear local storage directly on refresh failure without remote API call\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(python3 -c \" import sys content = sys.stdin.read\\(\\) old = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' new = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' print\\(content.replace\\(old, new\\)\\) \")"
"Bash(python3 -c \" import sys content = sys.stdin.read\\(\\) old = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' new = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' print\\(content.replace\\(old, new\\)\\) \")",
"Bash(git rm:*)"
],
"deny": [],
"ask": []

Binary file not shown.

Before

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -5,9 +5,9 @@
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:fillColor="#FFF3F4F6"
android:pathData="M0 19.59c0-10.82 8.77-19.59 19.59-19.59h-0.01c10.82 0 19.59 8.77 19.59 19.59v0.82c0 10.82-8.77 19.59-19.59 19.59"/>
android:fillColor="#FFFFEDD5"
android:pathData="M0 20c0-11.05 8.95-20 20-20h0c11.05 0 20 8.95 20 20v0c0 11.05-8.95 20-20 20h0c-11.05 0-20-8.95-20-20z"/>
<path
android:fillColor="#FFFF6B00"
android:pathData="M24.41 14.84l1.46 1.46-4.88 4.87-3.28-3.28c-0.38-0.42-1.03-0.42-1.4 0l-6 6c-0.43 0.38-0.43 1.03 0 1.4 0.37 0.38 1.03 0.38 1.4 0l5.3-5.29 3.28 3.28c0.37 0.42 1.03 0.42 1.4 0l5.58-5.58 1.46 1.46c0.32 0.28 0.84 0.09 0.84-0.38v-4.26c0-0.29-0.19-0.52-0.47-0.52H24.8c-0.42 0-0.66 0.56-0.38 0.84Z"/>
android:pathData="M13.52 24.8l4.41-4.4 2.7 2.73c0.35 0.3 0.9 0.3 1.2-0.04l5.98-6.72c0.28-0.35 0.28-0.86-0.04-1.17-0.3-0.32-0.9-0.32-1.2 0.03l-5.32 5.98-2.73-2.73c-0.36-0.32-0.86-0.32-1.18 0l-5.07 5.07c-0.36 0.36-0.36 0.86 0 1.18l0.07 0.07c0.32 0.36 0.82 0.36 1.18 0Z"/>
</vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:fillColor="#FFDCFCE7"
android:pathData="M0 20c0-11.05 8.95-20 20-20h0c11.05 0 20 8.95 20 20v0c0 11.05-8.95 20-20 20h0c-11.05 0-20-8.95-20-20z"/>
<path
android:fillColor="#FF16A34A"
android:pathData="M22.9 18c-0.13-1.98-0.87-3.98-2.2-5.62-0.35-0.39-0.97-0.39-1.29 0-1.4 1.64-2.14 3.64-2.3 5.63 1.05 0.58 2.03 1.33 2.89 2.22 0.86-0.9 1.84-1.64 2.9-2.22ZM20 22.9c-1.64-2.5-4.3-4.23-7.34-4.5-0.55-0.08-0.98 0.39-0.94 0.9 0.39 4.02 3.05 7.34 6.64 8.63 0.55 0.2 1.1 0.31 1.64 0.43 0.59-0.12 1.13-0.27 1.64-0.43 3.63-1.29 6.29-4.61 6.64-8.63 0.08-0.51-0.39-0.98-0.9-0.9-3.08 0.27-5.74 2-7.38 4.5Z"/>
</vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:fillColor="#FFFFEDD5"
android:pathData="M0 20c0-11.05 8.95-20 20-20h0c11.05 0 20 8.95 20 20v0c0 11.05-8.95 20-20 20h0c-11.05 0-20-8.95-20-20z"/>
<path
android:fillColor="#FFFF6B00"
android:pathData="M15 11.68v5L18.32 20 15 23.32v0.04 4.96h10v-4.96-0.04L21.68 20 25 16.68v-5H15Zm8.32 12.07v2.93h-6.64v-2.93L20 20.43l3.32 3.32ZM20 19.57l-3.32-3.32v-2.93h6.64v2.93L20 19.57Z"/>
</vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:fillColor="#FFF9FAFB"
android:pathData="M0 8c0-4.42 3.58-8 8-8h20c4.42 0 8 3.58 8 8v20c0 4.42-3.58 8-8 8h-20c-4.42 0-8-3.58-8-8z"/>
<path
android:fillColor="#FF4B5563"
android:pathData="M18.67 17.33c-1.34-0.45-1.97-0.74-1.97-1.44 0-0.74 0.8-1.02 1.34-1.02 0.98 0 1.33 0.74 1.44 0.99l1.2-0.5c-0.15-0.35-0.64-1.44-1.94-1.68v-0.92h-1.48v0.95c-1.86 0.39-1.86 2.15-1.86 2.22 0 1.68 1.69 2.18 2.5 2.46 1.19 0.42 1.72 0.8 1.72 1.54 0 0.85-0.81 1.2-1.51 1.2-1.34 0-1.76-1.4-1.8-1.58l-1.23 0.52c0.46 1.62 1.69 2.08 2.18 2.22v0.95h1.48v-0.92c0.31-0.07 2.18-0.45 2.18-2.42 0-1.02-0.46-1.94-2.25-2.57Zm-7.42 7.42H9.74v-4.5h4.5v1.51h-1.87c1.24 1.8 3.27 2.99 5.63 2.99 3.73 0 6.75-3.02 6.75-6.75h1.51c0 4.57-3.69 8.26-8.26 8.26-2.78 0-5.27-1.4-6.75-3.51v2ZM9.74 18c0-4.57 3.69-8.26 8.26-8.26 2.78 0 5.27 1.4 6.75 3.51v-2h1.51v4.5h-4.5v-1.51h1.86c-1.23-1.8-3.26-2.99-5.62-2.99-3.73 0-6.75 3.02-6.75 6.75H9.74Z"/>
</vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:fillColor="#FFF9FAFB"
android:pathData="M0 8c0-4.42 3.58-8 8-8h20c4.42 0 8 3.58 8 8v20c0 4.42-3.58 8-8 8h-20c-4.42 0-8-3.58-8-8z"/>
<path
android:fillColor="#FF4B5563"
android:pathData="M13.89 16.49h-1.52v5.27h1.52V16.5Zm4.5 0h-1.52v5.27h1.52V16.5Zm6.36 6.75H10.51v1.51h14.24v-1.51Zm-1.86-6.75h-1.52v5.27h1.52V16.5Zm-5.28-5.03l3.9 2.04h-7.8l3.9-2.04Zm0-1.72l-7.1 3.76v1.51h14.24V13.5l-7.14-3.76Z"/>
</vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FFF3F4F6"
android:pathData="M0 16c0-8.84 7.16-16 16-16h16c8.84 0 16 7.16 16 16v16c0 8.84-7.16 16-16 16h-16c-8.84 0-16-7.16-16-16z"/>
<path
android:fillColor="#FFFF6B00"
android:pathData="M30 24.98h-5.02V30c0 0.56-0.42 0.98-0.98 0.98s-0.98-0.42-0.98-0.98v-5.02H18c-0.56 0-0.98-0.42-0.98-0.98s0.42-0.98 0.98-0.98h5.02V18c0-0.56 0.42-0.98 0.98-0.98s0.98 0.42 0.98 0.98v5.02H30c0.56 0 0.98 0.42 0.98 0.98s-0.42 0.98-0.98 0.98Z"/>
</vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FFF3F4F6"
android:pathData="M0 16c0-8.84 7.16-16 16-16h16c8.84 0 16 7.16 16 16v16c0 8.84-7.16 16-16 16h-16c-8.84 0-16-7.16-16-16z"/>
<path
android:fillColor="#FFFF6B00"
android:pathData="M30 24.98H18c-0.56 0-0.98-0.42-0.98-0.98s0.42-0.98 0.98-0.98h12c0.56 0 0.98 0.42 0.98 0.98s-0.42 0.98-0.98 0.98Z"/>
</vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FFF3F4F6"
android:pathData="M0 16c0-8.84 7.16-16 16-16h16c8.84 0 16 7.16 16 16v16c0 8.84-7.16 16-16 16h-16c-8.84 0-16-7.16-16-16z"/>
<path
android:fillColor="#FFFF6B00"
android:pathData="M18.14 23.86l-2.77 2.81c-0.18 0.19-0.18 0.47 0 0.7l2.77 2.77c0.33 0.33 0.84 0.1 0.84-0.33v-1.83h6c0.57 0 1.04-0.42 1.04-0.98s-0.47-0.98-1.04-0.98h-6v-1.83c0-0.42-0.51-0.66-0.84-0.33Zm14.53-3.19l-2.81-2.81c-0.28-0.33-0.84-0.1-0.84 0.33v1.83h-6c-0.57 0-1.04 0.42-1.04 0.98s0.47 0.98 1.04 0.98h6v1.83c0 0.42 0.51 0.66 0.84 0.33l2.77-2.81c0.23-0.19 0.23-0.47 0.04-0.66Z"/>
</vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#FFF3F4F6"
android:pathData="M0 16c0-8.84 7.16-16 16-16h16c8.84 0 16 7.16 16 16v16c0 8.84-7.16 16-16 16h-16c-8.84 0-16-7.16-16-16z"/>
<path
android:fillColor="#FFFF6B00"
android:pathData="M28.6 21H27v-5.02c0-0.51-0.47-0.98-0.98-0.98h-4.04C21.47 15 21 15.47 21 15.98V21h-1.6c-0.88 0-1.35 1.08-0.7 1.69l4.6 4.6c0.37 0.41 1.03 0.41 1.4 0l4.6-4.6c0.6-0.61 0.18-1.69-0.7-1.69Zm-11.58 9.98c0 0.57 0.42 1.04 0.98 1.04h12c0.56 0 0.98-0.47 0.98-1.04 0-0.51-0.42-0.98-0.98-0.98H18c-0.56 0-0.98 0.47-0.98 0.98Z"/>
</vector>

View File

@ -1,10 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:fillColor="#FF1F2937"
android:pathData="M21 12.23C21 6.75 16.73 3 12 3c-4.69 0-9 3.66-9 9.28-0.6 0.33-0.98 0.99-0.98 1.74v1.96C2.02 17.11 2.9 18 3.98 18c0.57 0 1.04-0.47 1.04-0.98v-4.83c0-3.85 2.95-7.17 6.75-7.27 3.98-0.14 7.21 3.05 7.21 6.99v7.07H12c-0.56 0-0.98 0.47-0.98 1.04 0 0.51 0.42 0.98 0.98 0.98h6.98c1.13 0 2.02-0.9 2.02-2.02v-1.21c0.6-0.29 0.98-0.9 0.98-1.64v-2.3c0-0.7-0.37-1.31-0.98-1.6ZM8.02 12.98C8.02 12.47 8.44 12 9 12s0.98 0.47 0.98 0.98c0 0.57-0.42 1.04-0.98 1.04s-0.98-0.47-0.98-1.04Zm6 0c0-0.51 0.42-0.98 0.98-0.98s0.98 0.47 0.98 0.98c0 0.57-0.42 1.04-0.98 1.04s-0.98-0.47-0.98-1.04ZM18 11.02C17.53 8.2 15.05 6 12.05 6 9 6 5.77 8.53 6 12.47c2.48-1.03 4.36-3.24 4.88-5.9 1.3 2.62 3.98 4.45 7.12 4.45Z"/>
android:fillColor="#FFFFFFFF"
android:pathData="M0 18c0-9.94 8.06-18 18-18h0c9.94 0 18 8.06 18 18v0c0 9.94-8.06 18-18 18h0c-9.94 0-18-8.06-18-18z"/>
<path
android:fillColor="#FF4B5563"
android:pathData="M24.21 18.82c0-0.27 0.04-0.55 0.04-0.82 0-0.27-0.04-0.55-0.04-0.82l1.76-1.37c0.16-0.11 0.2-0.35 0.08-0.54l-1.68-2.9c-0.08-0.11-0.2-0.19-0.35-0.19-0.04 0-0.12 0-0.16 0.04l-2.07 0.82c-0.43-0.31-0.9-0.63-1.4-0.82l-0.32-2.19c-0.04-0.2-0.2-0.35-0.39-0.35h-3.36c-0.2 0-0.35 0.16-0.39 0.35l-0.31 2.19c-0.51 0.2-0.98 0.5-1.4 0.82l-2.08-0.82c-0.08-0.04-0.12-0.04-0.16-0.04-0.15 0-0.27 0.08-0.35 0.2l-1.68 2.89c-0.11 0.2-0.07 0.43 0.08 0.54l1.76 1.37c0 0.27-0.04 0.55-0.04 0.82 0 0.27 0.04 0.55 0.04 0.82l-1.76 1.37c-0.15 0.11-0.2 0.35-0.08 0.54l1.68 2.9c0.08 0.11 0.2 0.19 0.35 0.19 0.04 0 0.12 0 0.16-0.04l2.07-0.82c0.43 0.31 0.9 0.63 1.4 0.82l0.32 2.19c0.04 0.2 0.2 0.35 0.39 0.35h3.36c0.2 0 0.35-0.16 0.39-0.35l0.31-2.19c0.51-0.2 0.98-0.5 1.4-0.82l2.08 0.82c0.08 0.04 0.12 0.04 0.16 0.04 0.15 0 0.27-0.08 0.35-0.2l1.68-2.89c0.11-0.2 0.07-0.43-0.08-0.54l-1.76-1.37Zm-1.68-1.45c0.04 0.28 0.04 0.47 0.04 0.63 0 0.16 0 0.35-0.04 0.63l-0.12 0.93 0.75 0.59 0.9 0.7-0.6 0.98-1.05-0.43-0.86-0.31-0.74 0.54c-0.35 0.28-0.7 0.47-1.05 0.63L18.9 22.6l-0.16 0.94-0.15 1.13H17.4l-0.15-1.13-0.12-0.94-0.9-0.35c-0.35-0.16-0.7-0.35-1.01-0.63l-0.78-0.54-0.86 0.35-1.06 0.43-0.58-1.02 0.9-0.7 0.74-0.59-0.12-0.93c-0.04-0.28-0.04-0.47-0.04-0.63 0-0.16 0-0.35 0.04-0.63l0.12-0.93-0.75-0.59-0.9-0.7 0.6-0.98 1.05 0.43 0.86 0.31 0.74-0.54c0.35-0.28 0.7-0.47 1.05-0.63l0.86-0.35 0.16-0.94 0.15-1.13h1.18l0.15 1.13 0.12 0.94 0.9 0.35c0.35 0.16 0.7 0.35 1.01 0.63l0.75 0.54 0.9-0.35 1.05-0.43 0.58 1.02-0.9 0.7-0.74 0.59 0.12 0.93ZM18 14.69c-1.84 0-3.32 1.48-3.32 3.32 0 1.84 1.48 3.32 3.32 3.32 1.84 0 3.32-1.48 3.32-3.32 0-1.84-1.48-3.32-3.32-3.32Zm0 5c-0.9 0-1.68-0.78-1.68-1.68 0-0.9 0.78-1.68 1.68-1.68 0.9 0 1.68 0.78 1.68 1.68 0 0.9-0.78 1.68-1.68 1.68Z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="20dp"
android:viewportWidth="14"
android:viewportHeight="20">
<path
android:fillColor="#FF9CA3AF"
android:pathData="M7 6.5c2.21 0 4.18 1.23 5.14 3.2-0.96 1.97-2.93 3.23-5.14 3.23-2.21 0-4.18-1.26-5.14-3.23C2.82 7.73 4.79 6.5 7 6.5Zm0-1.18c-2.93 0-5.41 1.84-6.43 4.38 1.02 2.57 3.5 4.37 6.43 4.37 2.93 0 5.41-1.8 6.43-4.37C12.4 7.16 9.93 5.32 7 5.32Zm0 2.93c0.8 0 1.45 0.66 1.45 1.45 0 0.82-0.66 1.48-1.45 1.48-0.8 0-1.45-0.66-1.45-1.48 0-0.8 0.66-1.45 1.45-1.45Zm0-1.18c-1.45 0-2.63 1.18-2.63 2.63S5.55 12.32 7 12.32s2.63-1.17 2.63-2.62c0-1.45-1.18-2.63-2.63-2.63Z"/>
</vector>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="20dp"
android:viewportWidth="14"
android:viewportHeight="20">
<path
android:fillColor="#FFFF6B00"
android:pathData="M7 4.18c-3.23 0-5.82 2.6-5.82 5.82 0 3.23 2.6 5.82 5.82 5.82 3.23 0 5.82-2.6 5.82-5.82 0-3.23-2.6-5.82-5.82-5.82Zm0 8.75c-0.33 0-0.57-0.28-0.57-0.6V10c0-0.33 0.24-0.57 0.57-0.57 0.33 0 0.57 0.24 0.57 0.57v2.32c0 0.33-0.24 0.6-0.57 0.6Zm0.57-4.68H6.43V7.07h1.14v1.18Z"/>
</vector>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:width="20dp"
android:height="20dp"
android:viewportWidth="14"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="#FFFF6B00"
android:pathData="M5.41 6.91c-0.21 0.22-0.21 0.6 0 0.82L7.68 10l-2.27 2.27c-0.21 0.22-0.21 0.6 0 0.82 0.25 0.22 0.6 0.22 0.82 0l2.68-2.68c0.25-0.22 0.25-0.6 0-0.82L6.23 6.91c-0.21-0.22-0.57-0.22-0.82 0Z"/>
android:fillColor="#FF4B5563"
android:pathData="M10 18.32c0.9 0 1.68-0.74 1.68-1.64H8.32c0 0.9 0.78 1.64 1.68 1.64Zm5-5V9.18c0-2.58-1.37-4.73-3.75-5.27V3.32c0-0.66-0.55-1.25-1.25-1.25S8.75 2.66 8.75 3.32v0.59C6.37 4.45 5 6.6 5 9.18v4.14L3.32 15v0.82h13.36V15L15 13.32Zm-1.68 0.86H6.68v-5c0-2.07 1.25-3.75 3.32-3.75s3.32 1.68 3.32 3.75v5Z"/>
</vector>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:width="14dp"
android:height="20dp"
android:viewportWidth="14"
android:viewportHeight="20">
<path
android:fillColor="#FFFF6B00"
android:pathData="M18.98 5.02h-1.96V3.98c0-0.51-0.47-0.98-1.04-0.98H8.02C7.45 3 6.98 3.47 6.98 3.98v1.04H5.02C3.89 5.02 3 5.9 3 6.98v1.04c0 2.53 1.92 4.59 4.4 4.92 0.62 1.5 1.97 2.62 3.62 2.95V19h-3c-0.57 0-1.04 0.46-1.04 1.03 0 0.51 0.47 0.98 1.04 0.98h7.96c0.57 0 1.04-0.47 1.04-0.98 0-0.57-0.47-1.04-1.04-1.04h-3V15.9c1.64-0.33 3-1.45 3.61-2.95C19.08 12.6 21 10.54 21 8.02V6.98c0-1.07-0.9-1.96-2.02-1.96Zm-13.96 3V6.98h1.96v3.85C5.86 10.4 5.02 9.28 5.02 8.02Zm13.96 0c0 1.26-0.84 2.39-1.96 2.8V6.99h1.96v1.04Z"/>
android:fillColor="#FF10B981"
android:pathData="M9.82 7l0.84 0.84-2.84 2.84-1.91-1.91c-0.22-0.25-0.6-0.25-0.82 0l-3.5 3.5c-0.25 0.22-0.25 0.6 0 0.82 0.21 0.22 0.6 0.22 0.82 0L5.5 10l1.91 1.91c0.22 0.25 0.6 0.25 0.82 0l3.25-3.25 0.85 0.85c0.2 0.16 0.5 0.05 0.5-0.22V6.8c0-0.16-0.12-0.3-0.28-0.3h-2.51C9.79 6.5 9.65 6.83 9.82 7Z"/>
</vector>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="32">
<path
android:fillColor="#FF9CA3AF"
android:pathData="M6.14 11.86l-2.76 2.81c-0.2 0.19-0.2 0.47 0 0.7l2.76 2.77c0.33 0.33 0.84 0.1 0.84-0.33v-1.83h6c0.57 0 1.04-0.42 1.04-0.98s-0.47-0.98-1.04-0.98h-6v-1.83c0-0.42-0.51-0.66-0.84-0.33Zm14.53-3.19l-2.81-2.81c-0.28-0.33-0.84-0.1-0.84 0.33v1.83h-6c-0.57 0-1.04 0.42-1.04 0.98s0.47 0.98 1.04 0.98h6v1.83c0 0.42 0.51 0.66 0.84 0.33l2.77-2.81c0.23-0.19 0.23-0.47 0.04-0.66Z"/>
android:pathData="M15.98 16.98c3.1-2.8 6-5.43 6-7.68 0-1.83-1.45-3.28-3.28-3.28-1.03 0-2.06 0.46-2.72 1.21-0.65-0.75-1.64-1.21-2.67-1.21-1.87 0-3.33 1.45-3.33 3.28 0 2.25 2.91 4.87 6 7.68Zm-2.67-9c0.42 0 0.9 0.24 1.17 0.57l1.5 1.78 1.55-1.78c0.28-0.33 0.75-0.57 1.17-0.57 0.75 0 1.32 0.57 1.32 1.32 0 1.12-2.07 3.18-4.04 5.01C14.06 12.48 12 10.42 12 9.3c0-0.75 0.56-1.32 1.31-1.32Zm5.67 12h-1.96c0-1.17-0.75-2.25-1.88-2.67l-6.19-2.3H0.98v10.97h6v-1.4l7.04 1.92 7.96-2.48v-1.04c0-1.64-1.3-3-3-3ZM3 24.02v-7.04h2.02v7.04H3Zm10.97 0.37l-6.99-1.92v-5.49h1.64l5.82 2.2c0.33 0.1 0.56 0.43 0.56 0.8 0 0-1.97-0.04-2.3-0.14l-2.39-0.8-0.6 1.93 2.34 0.8c0.51 0.14 1.07 0.23 1.6 0.23h5.33c0.43 0 0.75 0.23 0.94 0.56l-5.95 1.83Z"/>
</vector>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="32">
<path
android:fillColor="#FF9CA3AF"
android:pathData="M9.98 15.98V8.02C9.98 6.89 10.88 6 12 6h9V5.02C21 3.89 20.1 3 18.98 3H5.02C3.89 3 3 3.9 3 5.02v13.96C3 20.11 3.9 21 5.02 21h13.96c1.13 0 2.02-0.9 2.02-2.02V18h-9c-1.13 0-2.02-0.9-2.02-2.02Zm3-7.96C12.47 8.02 12 8.44 12 9v6c0 0.56 0.47 0.98 0.98 0.98h9V8.02h-9Zm3 5.48c-0.8 0-1.5-0.66-1.5-1.5s0.7-1.5 1.5-1.5c0.85 0 1.5 0.66 1.5 1.5s-0.65 1.5-1.5 1.5Z"/>
android:pathData="M6.98 15.02L3 19l3.98 3.98v-3h7.04v-1.96H6.98v-3ZM21 13l-3.98-3.98v3H9.98v1.96h7.04v3L21 13Z"/>
</vector>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:viewportHeight="32">
<path
android:fillColor="#FF9CA3AF"
android:pathData="M12 12c2.2 0 3.98-1.78 3.98-3.98S14.2 3.98 12 3.98 8.02 5.81 8.02 8.02C8.02 10.22 9.8 12 12 12Zm0 2.02c-2.67 0-8.02 1.3-8.02 3.98v0.98c0 0.57 0.47 1.04 1.04 1.04h13.96c0.57 0 1.04-0.47 1.04-1.04V18c0-2.67-5.35-3.98-8.02-3.98Z"/>
android:pathData="M12 10c1.08 0 2.02 0.9 2.02 2.02 0 1.07-0.94 1.96-2.02 1.96s-2.02-0.89-2.02-1.96C9.98 10.89 10.92 10 12 10Zm0 9.98c2.72 0 5.81 1.32 6 2.02H6c0.23-0.7 3.33-2.02 6-2.02Zm0-12c-2.2 0-3.98 1.83-3.98 4.04C8.02 14.22 9.8 16 12 16s3.98-1.78 3.98-3.98S14.2 7.98 12 7.98Zm0 10.04c-2.67 0-8.02 1.3-8.02 3.98v2.02h16.04V22c0-2.67-5.35-3.98-8.02-3.98Z"/>
</vector>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
android:width="14dp"
android:height="20dp"
android:viewportWidth="14"
android:viewportHeight="20">
<path
android:fillColor="#FFFF6B00"
android:pathData="M8.02 2.02C6.89 2.02 6 2.9 6 3.98v3.2c0 0.5 0.23 1.02 0.6 1.4L9.99 12l-3.37 3.42C6.23 15.8 6 16.32 6 16.82v3.2c0 1.07 0.9 1.96 2.02 1.96h7.96c1.13 0 2.02-0.89 2.02-1.96v-3.2c0-0.5-0.19-1.02-0.56-1.4L14.02 12l3.37-3.42C17.81 8.2 18 7.68 18 7.18v-3.2c0-1.07-0.9-1.96-2.02-1.96H8.02Zm7.96 14.9v2.06c0 0.57-0.42 1.04-0.98 1.04H9c-0.56 0-0.98-0.47-0.98-1.04v-2.06c0-0.28 0.09-0.51 0.28-0.7l3.7-3.7 3.7 3.7c0.2 0.19 0.28 0.42 0.28 0.7Z"/>
android:fillColor="#FFD1D5DB"
android:pathData="M3.64 14.81l1.04 1.01L10.5 10 4.68 4.18l-1.04 1L8.42 10l-4.78 4.81Z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="10dp"
android:viewportWidth="12"
android:viewportHeight="10">
<path
android:fillColor="#FF10B981"
android:pathData="M4.45 8.75c-0.15 0-0.27-0.14-0.25-0.27l0.39-2.64H3.13c-0.38 0-0.14-0.31-0.14-0.33 0.53-0.94 1.33-2.3 2.36-4.14 0.04-0.08 0.12-0.12 0.22-0.12 0.13 0 0.25 0.14 0.23 0.27L5.43 4.16h1.45c0.17 0 0.27 0.08 0.17 0.27l-2.4 4.2C4.6 8.71 4.53 8.75 4.45 8.75Z"/>
</vector>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="24dp"
android:viewportWidth="14"
android:viewportHeight="24">
<path
android:fillColor="#FFD1D5DB"
android:pathData="M3.64 18.81l1.04 1.01L10.5 14 4.68 8.18l-1.04 1L8.42 14l-4.78 4.81Z"/>
</vector>

View File

@ -6,6 +6,13 @@ import '../error/exceptions.dart';
class ApiClient {
final Dio dio;
// SharedPreferences
static SharedPreferences? _prefsCache;
static Future<SharedPreferences>? _prefsFuture;
// 401
static void Function()? onUnauthorized;
ApiClient({required this.dio}) {
dio.options = BaseOptions(
baseUrl: AppConstants.baseUrl,
@ -20,17 +27,38 @@ class ApiClient {
// token
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
// token
final prefs = await SharedPreferences.getInstance();
// 使 SharedPreferences
final prefs = await _getPrefs();
final token = prefs.getString('access_token');
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) {
// 401
if (error.response?.statusCode == 401) {
onUnauthorized?.call();
}
handler.next(error);
},
));
}
// SharedPreferences
static Future<SharedPreferences> _getPrefs() async {
if (_prefsCache != null) return _prefsCache!;
_prefsFuture ??= SharedPreferences.getInstance();
_prefsCache = await _prefsFuture;
return _prefsCache!;
}
// /
static void clearPrefsCache() {
_prefsCache = null;
_prefsFuture = null;
}
Future<Response> get(
String path, {
Map<String, dynamic>? queryParameters,

View File

@ -4,6 +4,9 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'core/di/injection.dart';
import 'core/router/app_router.dart';
import 'core/constants/app_colors.dart';
import 'core/network/api_client.dart';
import 'core/router/routes.dart';
import 'presentation/providers/user_providers.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -17,11 +20,31 @@ void main() async {
runApp(const ProviderScope(child: MiningApp()));
}
class MiningApp extends ConsumerWidget {
class MiningApp extends ConsumerStatefulWidget {
const MiningApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
ConsumerState<MiningApp> createState() => _MiningAppState();
}
class _MiningAppState extends ConsumerState<MiningApp> {
@override
void initState() {
super.initState();
// 401
ApiClient.onUnauthorized = _handleUnauthorized;
}
void _handleUnauthorized() {
//
ref.read(userNotifierProvider.notifier).logout();
//
final router = ref.read(appRouterProvider);
router.go(Routes.login);
}
@override
Widget build(BuildContext context) {
final router = ref.watch(appRouterProvider);
return MaterialApp.router(

View File

@ -4,6 +4,7 @@ import '../../../core/constants/app_colors.dart';
import '../../../core/utils/format_utils.dart';
import '../../providers/user_providers.dart';
import '../../providers/mining_providers.dart';
import '../../widgets/shimmer_loading.dart';
class AssetPage extends ConsumerWidget {
const AssetPage({super.key});
@ -14,7 +15,6 @@ class AssetPage extends ConsumerWidget {
static const Color _grayText = Color(0xFF6B7280);
static const Color _darkText = Color(0xFF1F2937);
static const Color _bgGray = Color(0xFFF3F4F6);
static const Color _lightGray = Color(0xFFF9FAFB);
static const Color _riverBed = Color(0xFF4B5563);
static const Color _serenade = Color(0xFFFFF7ED);
static const Color _feta = Color(0xFFF0FDF4);
@ -64,7 +64,7 @@ class AssetPage extends ConsumerWidget {
//
accountAsync.when(
data: (account) => _buildAssetList(account),
loading: () => _buildLoadingCard(),
loading: () => _buildAssetListSkeleton(),
error: (_, __) => const SizedBox.shrink(),
),
const SizedBox(height: 24),
@ -74,7 +74,7 @@ class AssetPage extends ConsumerWidget {
//
accountAsync.when(
data: (account) => _buildAccountList(account),
loading: () => _buildLoadingCard(),
loading: () => _buildAssetListSkeleton(),
error: (_, __) => const SizedBox.shrink(),
),
const SizedBox(height: 100),
@ -767,13 +767,18 @@ class AssetPage extends ConsumerWidget {
}
Widget _buildLoadingCard() {
return Container(
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: const Center(child: CircularProgressIndicator()),
return const AssetCardSkeleton();
}
Widget _buildAssetListSkeleton() {
return Column(
children: const [
AssetItemSkeleton(),
SizedBox(height: 16),
AssetItemSkeleton(),
SizedBox(height: 16),
AssetItemSkeleton(),
],
);
}

View File

@ -7,6 +7,7 @@ import '../../../domain/entities/contribution.dart';
import '../../../domain/entities/contribution_record.dart';
import '../../providers/user_providers.dart';
import '../../providers/contribution_providers.dart';
import '../../widgets/shimmer_loading.dart';
class ContributionPage extends ConsumerWidget {
const ContributionPage({super.key});
@ -75,7 +76,7 @@ class ContributionPage extends ConsumerWidget {
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
loading: () => const PageSkeleton(),
error: (error, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -382,12 +383,16 @@ class ContributionPage extends ConsumerWidget {
}).toList(),
);
},
loading: () => const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
loading: () => Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: ShimmerLoading(
child: Column(
children: const [
ShimmerBox(height: 48),
SizedBox(height: 12),
ShimmerBox(height: 48),
],
),
),
),
error: (error, _) => Padding(

View File

@ -25,28 +25,21 @@ class _SplashPageState extends ConsumerState<SplashPage> {
}
Future<void> _initialize() async {
await Future.delayed(const Duration(seconds: 2));
// 500ms
await Future.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
//
final userState = ref.read(userNotifierProvider);
if (userState.isLoggedIn) {
// token
try {
await ref.read(userNotifierProvider.notifier).refreshTokenIfNeeded();
if (mounted) {
context.go(Routes.contribution);
}
} catch (e) {
// token刷新失败
if (mounted) {
context.go(Routes.login);
}
// token
if (mounted) {
context.go(Routes.contribution);
}
// token API 401
ref.read(userNotifierProvider.notifier).refreshTokenIfNeeded();
} else {
//
context.go(Routes.login);
}
}

View File

@ -5,6 +5,7 @@ import '../../../core/utils/format_utils.dart';
import '../../providers/mining_providers.dart';
import '../../providers/user_providers.dart';
import '../../providers/trading_providers.dart';
import '../../widgets/shimmer_loading.dart';
class TradingPage extends ConsumerStatefulWidget {
const TradingPage({super.key});
@ -767,14 +768,25 @@ class _TradingPageState extends ConsumerState<TradingPage> {
}
Widget _buildLoadingCard() {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
return ShimmerLoading(
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
ShimmerBox(width: 100, height: 16),
SizedBox(height: 12),
ShimmerBox(width: 150, height: 28),
SizedBox(height: 8),
ShimmerBox(width: 80, height: 14),
],
),
),
child: const Center(child: CircularProgressIndicator()),
);
}
@ -830,7 +842,9 @@ class _TradingPageState extends ConsumerState<TradingPage> {
);
if (success) {
_amountController.clear();
//
ref.invalidate(shareAccountProvider(accountSequence));
ref.invalidate(globalStateProvider);
}
}
}

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/contribution.dart';
import '../../domain/entities/contribution_record.dart';
@ -15,8 +16,21 @@ final contributionRepositoryProvider = Provider<ContributionRepository>((ref) {
final contributionProvider = FutureProvider.family<Contribution?, String>(
(ref, accountSequence) async {
//
if (accountSequence.isEmpty) return null;
final useCase = ref.watch(getUserContributionUseCaseProvider);
final result = await useCase(accountSequence);
// provider
ref.keepAlive();
// 5
final timer = Timer(const Duration(minutes: 5), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(contribution) => contribution,
@ -52,12 +66,25 @@ class ContributionRecordsParams {
/// Provider
final contributionRecordsProvider = FutureProvider.family<ContributionRecordsPage?, ContributionRecordsParams>(
(ref, params) async {
//
if (params.accountSequence.isEmpty) return null;
final repository = ref.watch(contributionRepositoryProvider);
final result = await repository.getContributionRecords(
params.accountSequence,
page: params.page,
pageSize: params.pageSize,
);
// provider
ref.keepAlive();
// 5
final timer = Timer(const Duration(minutes: 5), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(records) => records,

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../domain/entities/share_account.dart';
import '../../domain/entities/global_state.dart';
@ -14,11 +15,24 @@ final getGlobalStateUseCaseProvider = Provider<GetGlobalState>((ref) {
return getIt<GetGlobalState>();
});
// State Providers
// State Providers - 使 keepAlive
final shareAccountProvider = FutureProvider.family<ShareAccount?, String>(
(ref, accountSequence) async {
//
if (accountSequence.isEmpty) return null;
final useCase = ref.watch(getShareAccountUseCaseProvider);
final result = await useCase(accountSequence);
// provider rebuild
ref.keepAlive();
// 5
final timer = Timer(const Duration(minutes: 5), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(account) => account,
@ -29,6 +43,16 @@ final shareAccountProvider = FutureProvider.family<ShareAccount?, String>(
final globalStateProvider = FutureProvider<GlobalState?>((ref) async {
final useCase = ref.watch(getGlobalStateUseCaseProvider);
final result = await useCase();
// provider
ref.keepAlive();
// 5
final timer = Timer(const Duration(minutes: 5), () {
ref.invalidateSelf();
});
ref.onDispose(() => timer.cancel());
return result.fold(
(failure) => throw Exception(failure.message),
(state) => state,

View File

@ -0,0 +1,289 @@
import 'package:flutter/material.dart';
/// -
class ShimmerLoading extends StatefulWidget {
final Widget child;
final bool isLoading;
const ShimmerLoading({
super.key,
required this.child,
this.isLoading = true,
});
@override
State<ShimmerLoading> createState() => _ShimmerLoadingState();
}
class _ShimmerLoadingState extends State<ShimmerLoading>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
_animation = Tween<double>(begin: -2, end: 2).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOutSine),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!widget.isLoading) return widget.child;
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return ShaderMask(
shaderCallback: (bounds) {
return LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: const [
Color(0xFFEBEBF4),
Color(0xFFF4F4F4),
Color(0xFFEBEBF4),
],
stops: [
_animation.value - 1,
_animation.value,
_animation.value + 1,
].map((e) => e.clamp(0.0, 1.0)).toList(),
).createShader(bounds);
},
blendMode: BlendMode.srcATop,
child: widget.child,
);
},
);
}
}
///
class ShimmerBox extends StatelessWidget {
final double? width;
final double height;
final double borderRadius;
const ShimmerBox({
super.key,
this.width,
required this.height,
this.borderRadius = 8,
});
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: const Color(0xFFE5E7EB),
borderRadius: BorderRadius.circular(borderRadius),
),
);
}
}
///
class AssetCardSkeleton extends StatelessWidget {
const AssetCardSkeleton({super.key});
@override
Widget build(BuildContext context) {
return ShimmerLoading(
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 30,
offset: const Offset(0, 8),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ShimmerBox(width: 80, height: 16),
const SizedBox(height: 12),
const ShimmerBox(width: 180, height: 36),
const SizedBox(height: 8),
const ShimmerBox(width: 120, height: 14),
const SizedBox(height: 12),
const ShimmerBox(width: 100, height: 24, borderRadius: 12),
],
),
),
);
}
}
///
class AssetItemSkeleton extends StatelessWidget {
const AssetItemSkeleton({super.key});
@override
Widget build(BuildContext context) {
return ShimmerLoading(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Row(
children: [
const ShimmerBox(width: 40, height: 40, borderRadius: 20),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ShimmerBox(width: 60, height: 16),
const SizedBox(height: 8),
const ShimmerBox(width: 100, height: 20),
const SizedBox(height: 4),
const ShimmerBox(width: 80, height: 12),
],
),
),
const ShimmerBox(width: 14, height: 20),
],
),
),
);
}
}
///
class EarningsStatsSkeleton extends StatelessWidget {
const EarningsStatsSkeleton({super.key});
@override
Widget build(BuildContext context) {
return ShimmerLoading(
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
child: Column(
children: [
Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: const Color(0xFFE5E7EB),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
const ShimmerBox(width: 60, height: 16),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Column(
children: const [
ShimmerBox(width: 50, height: 12),
SizedBox(height: 8),
ShimmerBox(width: 70, height: 16),
],
),
),
Container(
width: 1,
height: 40,
color: const Color(0xFFE5E7EB),
),
Expanded(
child: Column(
children: const [
ShimmerBox(width: 50, height: 12),
SizedBox(height: 8),
ShimmerBox(width: 70, height: 16),
],
),
),
Container(
width: 1,
height: 40,
color: const Color(0xFFE5E7EB),
),
Expanded(
child: Column(
children: const [
ShimmerBox(width: 50, height: 12),
SizedBox(height: 8),
ShimmerBox(width: 70, height: 16),
],
),
),
],
),
],
),
),
);
}
}
///
class PageSkeleton extends StatelessWidget {
const PageSkeleton({super.key});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: const [
AssetCardSkeleton(),
SizedBox(height: 24),
AssetItemSkeleton(),
SizedBox(height: 16),
AssetItemSkeleton(),
SizedBox(height: 16),
AssetItemSkeleton(),
SizedBox(height: 24),
EarningsStatsSkeleton(),
],
),
);
}
}