Skip to content

feat: Swipable profile card - #247

Draft
kevinlee-06 wants to merge 4 commits into
mainfrom
feat/swipable-profile-card
Draft

feat: Swipable profile card#247
kevinlee-06 wants to merge 4 commits into
mainfrom
feat/swipable-profile-card

Conversation

@kevinlee-06

Copy link
Copy Markdown
Contributor

這個 PR 新增可左右滑動的卡片,用來取代原本點擊翻面的功能。

@rileychh-dokploy-riley-ntut-npc

rileychh-dokploy-riley-ntut-npc Bot commented Mar 31, 2026

Copy link
Copy Markdown

Dokploy Preview Deployment

Name Status Preview Updated (UTC)
API Docs ✅ Done Preview URL 2026-04-01T10:40:32.893Z

@rileychh
rileychh requested a review from Copilot March 31, 2026 14:04
@github-actions

github-actions Bot commented Mar 31, 2026

Copy link
Copy Markdown

PR Preview Builds

Build Number: 838
Commit: c3e9f03
Message: refactor: optimize profile card rendering with RepaintBoundary, ValueNotifier, and image caching

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

此 PR 目標是將個人頁面的 ProfileCard 互動由「點擊翻面」改為「左右滑動切換」,並加入卡片背面資訊(含 QR code)以強化卡片的資訊承載能力與視覺效果。

Changes:

  • 新增 ProfileCard 的左右滑動 PageView(正面/背面)。
  • 背面新增玻璃擬態樣式與以學號生成的 QR code 顯示。
  • 調整 ProfileScreen/AnimatedShellContainer 的 clipBehavior 以避免陰影/溢出被裁切。

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pubspec.yaml 新增 qr_flutter 依賴以支援 QR code 生成/渲染
pubspec.lock 鎖定 qr_flutter 與其轉移依賴版本
lib/shells/animated_shell_container.dart Stack 改為 Clip.none,避免分頁內容陰影/溢出被裁切
lib/screens/main/profile/profile_screen.dart 將 ProfileCard 移出原本的 SliverPadding,配合新的卡片滑動呈現
lib/screens/main/profile/profile_card.dart 實作可滑動的 ProfileCard(Front/Back)、新增 QR code 與玻璃擬態背景框架

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +57 to 89
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: switch ((profileAsync, registrationAsync)) {
// NOT_LOGIN state: not logged in
(AsyncData(value: null), _) => _ProfileCardFrame(
child: Center(child: Text(t.general.notLoggedIn)),
),
),

// DATA state: show profile content (even if refreshing)
(
AsyncValue(value: final profile, hasValue: true),
AsyncValue(value: final registration, hasValue: true),
)
when profile != null =>
ProfileContent(
profile: profile,
registration: registration,
avatarFile: avatarAsync.value,

// ERROR state: show error message on card
(AsyncError(:final error), _) ||
(_, AsyncError(:final error)) => _ProfileCardFrame(
child: Center(child: Text('Error: $error')),
),

// LOADING state: show skeleton
_ => const AppSkeleton(
child: ProfileContent(
profile: _placeholderProfile,
registration: _placeholderSemester,
// DATA state: show profile content (even if refreshing)
(
AsyncValue(value: final profile, hasValue: true),
AsyncValue(value: final registration, hasValue: true),
)
when profile != null =>
_ProfileCardPager(
profile: profile,
registration: registration,
avatarFile: avatarAsync.value,
),

// LOADING state: show skeleton
_ => const AppSkeleton(
child: _ProfileCardFront(
profile: _placeholderProfile,
registration: _placeholderSemester,
),
),

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_ProfileCardPager adds 16px horizontal padding per page, but the NOT_LOGIN / ERROR / LOADING branches render _ProfileCardFrame full-width. This will cause the card width to “jump” when data loads (and looks inconsistent across states). Consider applying the same horizontal padding for all states (e.g., wrap the entire switch result with symmetric horizontal padding, or move padding into _ProfileCardFrame) and remove duplication inside the pager.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已將 16px 的水平 padding 統一移入 _ProfileCardFrame 內部

builder: (context, constraints) {
final width = constraints.maxWidth;
// Total horizontal padding is 16 + 16 = 32
final cardWidth = width - _spacing;

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cardWidth = width - _spacing can become 0/negative when the available width is small (split-screen, small devices, layout tests), which then produces an invalid height and can trigger layout assertions. Clamp cardWidth to a minimum > 0 (or derive height from constraints.biggest/AspectRatio instead of manual math).

Suggested change
final cardWidth = width - _spacing;
final cardWidth = (width - _spacing).clamp(1.0, double.infinity);

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修

Widget build(BuildContext context) {
    return Stack(
      children: [
        const IgnorePointer(
          child: Padding(
            padding: EdgeInsets.symmetric(horizontal: 16.0),
            child: AspectRatio(
              aspectRatio: 1016 / 638,
              child: SizedBox.expand(),
...

Comment on lines +132 to +139
// Re-initialize controller only if width changes
if (_pageController == null || _lastWidth != width) {
_pageController?.dispose();
_pageController = PageController(
viewportFraction: 1.0,
);
_lastWidth = width;
}

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recreating and disposing the PageController inside build when width changes will reset the current page to 0 on rotation/resizing and can cause extra churn during rebuilds. Prefer initializing the controller once (e.g., in initState) and, if you must recreate it, preserve the current page index before disposing and pass it as initialPage.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已將 PageController 的初始化從 build 移至 initState,並補上了對應的 dispose

Comment on lines +480 to +493
class _ProfileCardFrame extends StatelessWidget {
const _ProfileCardFrame({required this.childBuilder});
const _ProfileCardFrame({
this.childBuilder,
this.child,
this.isGlassy = false,
});

final Widget Function(
BuildContext context,
BoxConstraints constraints,
BorderRadius borderRadius,
)
)?
childBuilder;
final Widget? child;

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_ProfileCardFrame allows both child and childBuilder to be null, but later force-unwraps childBuilder!, which would crash at runtime if the widget is ever constructed incorrectly. Add constructor asserts to guarantee exactly one of child / childBuilder is provided (or make one required) to fail fast and avoid a null-deref.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 _ProfileCardFrame 的 constructor 中加入了 assert,強制要求 child 或 childBuilder 必須擇一提供。

class _ProfileCardFrame extends StatefulWidget {
  const _ProfileCardFrame({
    this.childBuilder,
    this.child,
    this.isGlassy = false,
  }) : assert(
         (child == null) != (childBuilder == null),
         'Exactly one of child or childBuilder must be provided',
       );

child: PageView(
controller: _pageController,
clipBehavior: Clip.none,
allowImplicitScrolling: true,

Copilot AI Mar 31, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PageView(allowImplicitScrolling: true) will keep adjacent pages alive/painted; combined with the back side’s BackdropFilter blur this can noticeably increase GPU cost during vertical scrolling. Consider disabling implicit scrolling here (or isolating the glassy page with a RepaintBoundary / reducing blur complexity) to avoid performance regressions on mid-range devices.

Suggested change
allowImplicitScrolling: true,

Copilot uses AI. Check for mistakes.

@kevinlee-06 kevinlee-06 Apr 1, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已將 allowImplicitScrolling 關閉,並透過圖層隔離的策略,確保背景的玻璃模糊效果被完全快取在獨立圖層中

@kevinlee-06
kevinlee-06 force-pushed the feat/swipable-profile-card branch from 2ba00f4 to c0ee87a Compare March 31, 2026 14:11
@kevinlee-06
kevinlee-06 marked this pull request as ready for review April 1, 2026 10:55
@rileychh
rileychh marked this pull request as draft April 27, 2026 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants