Skip to content

feat: integrate enhanced login features from submodule and add QR/cookie UI#105

Open
Zhenghao-Wen wants to merge 45 commits into2697a:feature/new-uifrom
Zhenghao-Wen:build
Open

feat: integrate enhanced login features from submodule and add QR/cookie UI#105
Zhenghao-Wen wants to merge 45 commits into2697a:feature/new-uifrom
Zhenghao-Wen:build

Conversation

@Zhenghao-Wen
Copy link
Copy Markdown

@Zhenghao-Wen Zhenghao-Wen commented Feb 25, 2026

Description

This PR adapts the app to use the improved bujuan_music_api submodule (see companion PR in that repo). Key changes:

  1. Submodule Update: Pointed bujuan_music_api to the latest commit that includes:
    • message field in LoginEntity for better error feedback.
    • Full QR code login support.
    • Persistent cookie management.
  2. Login Page Redesign:
    • Added QR code login panel (with qr_flutter).
    • Added manual cookie login option with a dialog.
    • Displays server error messages from LoginEntity.message.
    • Uses SharedPreferences to persist cookies from QR/manual login across app restarts.
    • Maintains the app's green theme (#1ED760) consistently (e.g., dialog borders and cursor).
  3. Startup: On app launch, any saved cookie is restored via BujuanMusicManager.setCookie().
  4. Dependencies: Uncommented shared_preferences: ^2.0.0 in pubspec.yaml (was previously commented out).

Testing

  • Verified phone number + captcha login still works.
  • Tested QR code login: scanning with mobile, automatic cookie storage, and session persistence after app restart.
  • Tested manual cookie input with both valid and invalid cookies.
  • All other features (home page, playback, etc.) remain functional.

Notes

  • This PR depends on the companion PR in bujuan_music_api. It is recommended to merge that first, then update the submodule reference here to the merged commit.
  • The UI text remains in English to match the original style.

Please review. Thanks!

Actually, this is my first pull request and my fork may no longer be maintained.

Dont' forget check my PR of the submodule (bujuan_music_api)!

Summary by CodeRabbit

  • New Features

    • QR code login with polling and SMS/code flow; cookie-based login with persistent storage and automatic load at startup.
    • Multi-platform build support (Android, iOS, macOS, Windows, Linux, Web).
  • Bug Fixes

    • Improved login error handling and user messaging.
    • Compilation and build stability fixes.
  • Chores

    • App display name changed to "Bujuan".
    • Migrated API dependency to remote source; updated dev dependencies and build configuration.
    • Temporary lyrics provider removed.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8da7dc49-20f5-4d03-ac3e-e079fa0cce54

📥 Commits

Reviewing files that changed from the base of the PR and between 4b5b59a and 8b69c9c.

📒 Files selected for processing (1)
  • README.md
✅ Files skipped from review due to trivial changes (1)
  • README.md

📝 Walkthrough

Walkthrough

Adds multi-platform GitHub Actions for Flutter builds, updates branding to "Bujuan", migrates bujuan_music_api to a git dependency, refactors login with phone/QR/cookie flows, removes the lyrics provider, and loads a stored cookie at startup.

Changes

Cohort / File(s) Summary
CI Workflows
.github/workflows/build_new.yml, .github/workflows/web.yml
New multi-platform build workflow (Android ABI splits, iOS/macOS/Windows/Linux/Web) with artifact uploads; separate web build workflow with Flutter caching and pub/cache handling.
Android Build & Manifest
android/app/build.gradle, android/app/src/main/AndroidManifest.xml
Added ABI splits and release signing via env vars; changed app label from bujuan_music to Bujuan.
iOS / macOS Branding & Plugins
ios/Runner/Info.plist, macos/Flutter/GeneratedPluginRegistrant.swift
iOS CFBundleName changed to Bujuan; removed PathProvider plugin registration for macOS.
Dependency & Submodule Changes
pubspec.yaml, flutter, bujuanbuild, plugin/bujuan_music_api
Switched bujuan_music_api to git dependency (repo + ref); added qr_flutter; updated dev deps; updated submodule refs.
Home Provider & Generated Hashes
lib/pages/home/provider.dart, lib/pages/home/provider.g.dart, lib/pages/main/provider.g.dart, lib/pages/playlist/provider.g.dart
Replaced usage of RecommendNewSongEntity with RecommendSongEntity and recommendSongs(); adapted field access (dt/ar/al/mv); updated generated provider hash constants.
Login UI & Flows
lib/pages/login/login_page.dart
Large refactor: added phone (SMS/code), QR (key, image, polling), and cookie paste login flows; new UI state, controllers, SharedPreferences usage, and error/loading handling.
Startup Cookie Load
lib/main.dart
Added _loadManualCookie() to read manual_cookie from SharedPreferences and set it on BujuanMusicManager at startup.
Lyrics Provider Removal
lib/pages/play/provider.dart, lib/pages/play/provider.g.dart
Removed getMediaLyric provider and LRC parsing; deleted generated provider file.
Build README / Changelog
README.md, CHANGELOG.md
Updated README title/description and changelog entries describing CI, dependency, and login changes.

Sequence Diagram(s)

sequenceDiagram
    participant UI as App UI
    participant Local as SharedPreferences
    participant API as BujuanMusicManager (API)
    participant QRsrv as QR Auth Server

    UI->>Local: check saved manual_cookie
    Local-->>UI: manual_cookie (if present)
    UI->>API: setCookie(manual_cookie)
    API-->>UI: auth validation (user info)

    UI->>API: request QR key
    API-->>UI: qr_key + qr_image_url
    UI->>UI: display QR image
    loop poll
      UI->>QRsrv: poll qr status (qr_key)
      QRsrv-->>UI: status (pending/confirmed/expired)
    end
    QRsrv-->>API: on-confirm provide cookie/token
    API-->>UI: user info on success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #103 — Fixes compile errors by switching recommend_new_song_entity usage to recommend_song_entity and updating method calls (addresses reported missing entity/method mismatch).

Poem

A rabbit hops through CI and code, so spry, 🐰
QR glows, SMS pings, cookies tucked nearby,
Bujuan blooms with builds on every shore,
Lyrics paused, new logins knock on the door,
I nibble tests and watch the artifacts fly. ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: integrating enhanced login features and adding QR/cookie UI, which aligns with the significant refactoring of the login page and new workflow additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pubspec.yaml (1)

30-57: ⚠️ Potential issue | 🔴 Critical

Add shared_preferences dependency — code imports are unresolved.

The codebase imports shared_preferences in at least two files (lib/main.dart and lib/pages/login/login_page.dart), but the dependency is missing from pubspec.yaml. This will cause import resolution failures and prevent the app from building or running.

Fix
   nb_utils: ^7.1.8
   flutter_lyric: ^3.0.2
+  shared_preferences: ^2.0.0
   qr_flutter: ^4.1.0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pubspec.yaml` around lines 30 - 57, The project is missing the
shared_preferences dependency referenced by imports in lib/main.dart and
lib/pages/login/login_page.dart; add a shared_preferences entry under
dependencies in pubspec.yaml (e.g., shared_preferences: ^2.0.15), save the file
and run flutter pub get to fetch the package so the unresolved imports in the
files (imports of shared_preferences) are resolved.
🧹 Nitpick comments (4)
.github/workflows/web.yml (1)

22-24: Glob-based pub-cache wipe is brittle and partially redundant with cache: true.

rm -rf ~/.pub-cache/git/bujuan_music_api-* runs after flutter-action has already restored the pub cache. While the ordering is correct (cache restore → wipe specific dir → pub get), the glob bujuan_music_api-* could inadvertently match unrelated packages that share the same prefix. A more targeted approach pins the exact cache key:

-     - name: Clear submodule cache
-       run: rm -rf ~/.pub-cache/git/bujuan_music_api-*
+     - name: Clear bujuan_music_api pub cache
+       run: |
+         find ~/.pub-cache/git -maxdepth 1 -name 'bujuan_music_api-*' -type d -exec rm -rf {} + || true

Alternatively, disable cache: true on the flutter-action step for this workflow since you want a fresh fetch every run.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/web.yml around lines 22 - 24, The glob-based wipe in the
"Clear submodule cache" step (rm -rf ~/.pub-cache/git/bujuan_music_api-*) is
brittle and may match unrelated packages; either replace this step with a
targeted removal that uses the exact cache key for the bujuan_music_api repo, or
disable the flutter-action cache by setting its cache: false so the action does
a fresh fetch each run; update the workflow to reference the specific unique
cache key or toggle the flutter-action's cache option (the flutter-action step
and the "Clear submodule cache" rm command are the symbols to change).
lib/pages/play/provider.dart (1)

5-5: Untracked TODO — consider opening an issue.

The TODO comment doesn't link to any issue tracker entry, making it easy to lose. If the lyrics feature restoration is planned, opening a GitHub issue for it would ensure it's tracked.

Would you like me to draft the issue description for restoring the getMediaLyric provider and parseLrc helper?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/play/provider.dart` at line 5, The TODO comment "恢复歌词 provider" is
untracked—open a GitHub issue to track restoring the getMediaLyric provider and
parseLrc helper, then replace the inline TODO in lib/pages/play/provider.dart
with a short TODO that references the created issue number/URL; the issue should
include a description of desired behavior, acceptance criteria (unit tests for
parseLrc, integration with getMediaLyric, sample LRC parsing scenarios), and any
migration notes so future readers can follow the work.
lib/pages/home/provider.dart (1)

14-23: Remove duplicate recommendSongs() call in newAlbum.

Lines 14 and 16 invoke the same endpoint with identical parameters, causing unnecessary API latency. Both responses are cast to RecommendSongEntity at lines 30 and 31, then both extract .data?.dailySongs.

Eliminate line 16's recommendSongFuture, remove it from Future.wait(), and reuse the first response for both songEntity and recommendSongEntity.

💡 Suggested refactor
   var recommendResourceFuture = BujuanMusicManager().recommendResource();
   var songsFuture = BujuanMusicManager().recommendSongs();
   var topArtistFuture = BujuanMusicManager().topArtist(limit: 10);
-  var recommendSongFuture = BujuanMusicManager().recommendSongs();

   var list = await Future.wait([
     recommendResourceFuture,
     topArtistFuture,
     songsFuture,
-    recommendSongFuture,
   ]);
   return compute(_buildHomeData, list);
 }
 ...
   var songEntity = list[2] as RecommendSongEntity;
-  var recommendSongEntity = list[3] as RecommendSongEntity;
+  var recommendSongEntity = songEntity;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/home/provider.dart` around lines 14 - 23, The code calls
BujuanMusicManager().recommendSongs() twice (variables recommendSongFuture and
songsFuture) causing duplicate API requests; remove the duplicate
recommendSongFuture declaration and drop it from the Future.wait([...]) array,
then after awaiting 'list' reuse the songsFuture result when populating both
songEntity and recommendSongEntity (i.e. assign recommendSongEntity = songEntity
or pull .data?.dailySongs from the songsFuture response) so only one
recommendSongs() call is made; update any references to recommendSongFuture
accordingly.
android/app/build.gradle (1)

41-53: Gate signing config assignment when environment variables are missing.

The current code assigns storePassword, keyAlias, and keyPassword from environment variables without fallbacks. When these variables are unset (typical in local development), AGP will fail the release build with "missing required property" errors. The suggested approach of conditionally assigning the signingConfig only when all required properties are available allows unsigned release builds locally, rather than failing the build.

💡 Gate signing config by env availability
+    def hasReleaseSigning =
+            System.getenv("KEYSTORE_PATH") &&
+            System.getenv("KEYSTORE_PASSWORD") &&
+            System.getenv("KEY_ALIAS") &&
+            System.getenv("KEY_PASSWORD")
+
     buildTypes {
         release {
-            signingConfig signingConfigs.release
+            if (hasReleaseSigning) {
+                signingConfig signingConfigs.release
+            }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/build.gradle` around lines 41 - 53, The release signing currently
sets storePassword, keyAlias, and keyPassword directly from env vars
(storePassword, keyAlias, keyPassword) which causes AGP to fail when they are
unset; update the buildTypes.release block to conditionally assign signingConfig
signingConfigs.release only when all required env variables
(KEYSTORE_PATH/KEYSTORE_PASSWORD/KEY_ALIAS/KEY_PASSWORD) are present (e.g.,
check System.getenv(...) != null for each) so local/CI environments without
these vars can build unsigned releases, leaving signingConfigs.release unchanged
otherwise; reference the existing signingConfigs.release and the env var names
when implementing the guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/build_new.yml:
- Around line 42-45: Remove exporting sensitive secrets to the job-wide
environment: only set KEYSTORE_PATH in GITHUB_ENV and stop writing
KEYSTORE_PASSWORD, KEY_ALIAS, and KEY_PASSWORD there. Instead, keep
KEYSTORE_PASSWORD, KEY_ALIAS, and KEY_PASSWORD as step-level secrets consumed
directly by the Build APKs step (use the step's env or with: secrets.* so they
aren't exposed to downstream steps). Update the workflow to reference
KEYSTORE_PATH from GITHUB_ENV and pass the other three values from secrets only
where needed (e.g., the build/sign step) to prevent job-wide exposure.

In @.github/workflows/web.yml:
- Around line 29-36: The workflow currently only warns when .g.dart files are
missing in the submodule ("Verify generated files in submodule") which lets the
subsequent "Build Web" step run and fail; update the job to either (A) run
flutter pub run build_runner build (or flutter pub get then build_runner) in the
workspace of the git dependency before the "Build Web" step to generate missing
*.g.dart files, or (B) change the verify step to exit non-zero when the ls check
fails so the workflow stops early; target the verify step name and the
subsequent "Build Web" step when implementing the fix so generation runs or the
job fails before building.

In `@flutter`:
- Line 1: The flutter git submodule is incomplete (no .gitmodules) and pinned to
commit 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 while pubspec.yaml depends on
bujuan_music_api via ref: login_test, creating dual-dependency drift; fix by
either fully configuring the submodule (add a .gitmodules entry for the flutter
submodule and run git submodule init/update) or remove the submodule entry from
the index if you don't intend to keep it, then ensure bujuan_music_api is merged
to main and update pubspec.yaml to pin the dependency to the explicit commit SHA
that matches the submodule (replace ref: login_test with commit:
582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536) and, if retaining the submodule,
update the submodule pointer to that same SHA so both resolution paths reference
the identical commit.

In `@lib/main.dart`:
- Around line 47-54: The _loadManualCookie function currently reads a plaintext
auth cookie from SharedPreferences and passes it to
BujuanMusicManager().setCookie; replace this with secure storage by switching
from SharedPreferences to flutter_secure_storage (or platform keychain/keystore)
for both read and write paths so credentials aren't stored in plaintext. Update
_loadManualCookie to read using FlutterSecureStorage (same key 'manual_cookie'),
and also update the write location in login_page.dart where the cookie is saved
to use the same secure API; ensure null/empty checks remain and that
BujuanMusicManager().setCookie is called only after retrieving the secure value.

In `@lib/pages/home/provider.dart`:
- Around line 30-31: The code force-casts results of
BujuanMusicManager.recommendSongs() to non-nullable RecommendSongEntity
(variables songEntity and recommendSongEntity and other similar casts at the
indicated locations), which can throw when the method returns null; change those
casts to nullable types (RecommendSongEntity?) and update usages to use
null-aware operators (?., ?? or early-null checks) or guard returns so you
safely handle null responses from recommendSongs() instead of force-casting to
non-nullable.

In `@lib/pages/login/login_page.dart`:
- Around line 476-477: Update the misleading button label in the login_page
widget: replace the Text('Get an SMS QR code') string with a clear action like
Text('Send SMS verification code') (or 'Send SMS code') in the button widget so
the UI reflects that the action sends an SMS verification code rather than
providing a QR code.
- Around line 71-90: Several async callbacks in the login page (notably
_sendCode, _resendCode, _loginWithCode, _loginWithCookie and the Timer.periodic
loop inside _initQrLogin) perform UI/context work after awaits without verifying
the widget is still mounted; add guarded mounted checks immediately after every
await and before any call to setState(), _showTopSnackBar(), Navigator calls or
other context-dependent operations (e.g., in _sendCode after the
BujuanMusicManager().sendSmsCode await, in every await inside the Timer.periodic
callback, and before setting _isSendingCode, _lastSentPhone, showing
sheets/snackbars, or mutating state), and ensure the Timer.periodic loop cancels
itself or returns early if mounted is false to avoid lifecycle exceptions.
- Around line 232-289: The periodic QR check can overlap because the
Timer.periodic callback is async; add an in-flight guard (e.g., a private bool
_isQrChecking) and check it at the top of the Timer.periodic callback used to
poll in _qrTimer and return immediately if true, then set _isQrChecking = true
before calling BujuanMusicManager().qrCodeLogin and ensure you clear it in a
finally block (set to false) after the await/processing so no concurrent
qrCodeLogin calls happen; keep the existing expiry handling (timer.cancel /
state updates) but ensure the guard is cleared on all paths including errors.
- Around line 257-258: Replace plaintext SharedPreferences usage for the
'manual_cookie' key with encrypted FlutterSecureStorage: where you currently
call SharedPreferences.getInstance() and prefs.setString('manual_cookie',
result.cookie!), instantiate a FlutterSecureStorage (e.g., final storage =
FlutterSecureStorage()) and use storage.write(key: 'manual_cookie', value:
result.cookie) and storage.read(key: 'manual_cookie') for reads; update all
occurrences referenced (the spots using SharedPreferences, prefs, setString and
the 'manual_cookie' key at the other locations) and add the import for
flutter_secure_storage plus appropriate async/error handling around read/write
operations.

In `@lib/pages/play/provider.dart`:
- Around line 2-3: Remove the dangling part directive and unused import: delete
the line containing part 'provider.g.dart' and the import
'package:riverpod_annotation/riverpod_annotation.dart' from provider.dart (or
re-add them only if you restore the `@riverpod-annotated` lyrics provider and its
generated file); this eliminates the compile-time reference to the deleted
provider.g.dart and removes the unused riverpod_annotation import.

In `@pubspec.yaml`:
- Around line 41-43: The git dependency entry referencing bujuan_music_api
currently uses a mutable branch ref ('ref: login_test') which makes builds
non-reproducible; update the pubspec.yaml git dependency (the block with "git:
url: https://github.com/Zhenghao-Wen/bujuan_music_api.git" and "ref:
login_test") to use the exact commit SHA (or a released tag) instead of the
branch name, then run pub get to refresh lockfiles and commit the updated
lockfile so CI uses the pinned commit.

---

Outside diff comments:
In `@pubspec.yaml`:
- Around line 30-57: The project is missing the shared_preferences dependency
referenced by imports in lib/main.dart and lib/pages/login/login_page.dart; add
a shared_preferences entry under dependencies in pubspec.yaml (e.g.,
shared_preferences: ^2.0.15), save the file and run flutter pub get to fetch the
package so the unresolved imports in the files (imports of shared_preferences)
are resolved.

---

Nitpick comments:
In @.github/workflows/web.yml:
- Around line 22-24: The glob-based wipe in the "Clear submodule cache" step (rm
-rf ~/.pub-cache/git/bujuan_music_api-*) is brittle and may match unrelated
packages; either replace this step with a targeted removal that uses the exact
cache key for the bujuan_music_api repo, or disable the flutter-action cache by
setting its cache: false so the action does a fresh fetch each run; update the
workflow to reference the specific unique cache key or toggle the
flutter-action's cache option (the flutter-action step and the "Clear submodule
cache" rm command are the symbols to change).

In `@android/app/build.gradle`:
- Around line 41-53: The release signing currently sets storePassword, keyAlias,
and keyPassword directly from env vars (storePassword, keyAlias, keyPassword)
which causes AGP to fail when they are unset; update the buildTypes.release
block to conditionally assign signingConfig signingConfigs.release only when all
required env variables (KEYSTORE_PATH/KEYSTORE_PASSWORD/KEY_ALIAS/KEY_PASSWORD)
are present (e.g., check System.getenv(...) != null for each) so local/CI
environments without these vars can build unsigned releases, leaving
signingConfigs.release unchanged otherwise; reference the existing
signingConfigs.release and the env var names when implementing the guard.

In `@lib/pages/home/provider.dart`:
- Around line 14-23: The code calls BujuanMusicManager().recommendSongs() twice
(variables recommendSongFuture and songsFuture) causing duplicate API requests;
remove the duplicate recommendSongFuture declaration and drop it from the
Future.wait([...]) array, then after awaiting 'list' reuse the songsFuture
result when populating both songEntity and recommendSongEntity (i.e. assign
recommendSongEntity = songEntity or pull .data?.dailySongs from the songsFuture
response) so only one recommendSongs() call is made; update any references to
recommendSongFuture accordingly.

In `@lib/pages/play/provider.dart`:
- Line 5: The TODO comment "恢复歌词 provider" is untracked—open a GitHub issue to
track restoring the getMediaLyric provider and parseLrc helper, then replace the
inline TODO in lib/pages/play/provider.dart with a short TODO that references
the created issue number/URL; the issue should include a description of desired
behavior, acceptance criteria (unit tests for parseLrc, integration with
getMediaLyric, sample LRC parsing scenarios), and any migration notes so future
readers can follow the work.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44698e4 and 4b5b59a.

⛔ Files ignored due to path filters (27)
  • android/app/src/main/res/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png
  • android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png is excluded by !**/*.png
  • ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png is excluded by !**/*.png
  • pubspec.lock is excluded by !**/*.lock
  • web/favicon.png is excluded by !**/*.png
  • web/icons/Icon-192.png is excluded by !**/*.png
  • web/icons/Icon-512.png is excluded by !**/*.png
  • web/icons/Icon-maskable-192.png is excluded by !**/*.png
  • web/icons/Icon-maskable-512.png is excluded by !**/*.png
  • windows/runner/resources/app_icon.ico is excluded by !**/*.ico
📒 Files selected for processing (19)
  • .github/workflows/build_new.yml
  • .github/workflows/web.yml
  • CHANGELOG.md
  • android/app/build.gradle
  • android/app/src/main/AndroidManifest.xml
  • bujuanbuild
  • flutter
  • ios/Runner/Info.plist
  • lib/main.dart
  • lib/pages/home/provider.dart
  • lib/pages/home/provider.g.dart
  • lib/pages/login/login_page.dart
  • lib/pages/main/provider.g.dart
  • lib/pages/play/provider.dart
  • lib/pages/play/provider.g.dart
  • lib/pages/playlist/provider.g.dart
  • macos/Flutter/GeneratedPluginRegistrant.swift
  • plugin/bujuan_music_api
  • pubspec.yaml
💤 Files with no reviewable changes (3)
  • lib/pages/play/provider.g.dart
  • plugin/bujuan_music_api
  • macos/Flutter/GeneratedPluginRegistrant.swift

Comment on lines +42 to +45
echo "KEYSTORE_PATH=${{ runner.temp }}/keystore.jks" >> $GITHUB_ENV
echo "KEYSTORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> $GITHUB_ENV
echo "KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}" >> $GITHUB_ENV
echo "KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}" >> $GITHUB_ENV
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

GitHub Actions GITHUB_ENV environment variable scope availability subsequent steps

💡 Result:

Writing NAME=VALUE to the file pointed to by GITHUB_ENV sets (or updates) an environment variable for all subsequent steps in the same job. It is not available earlier steps, and the step that writes to GITHUB_ENV does not see the new value (within that same step). [1]

So this works:

- name: Set var
  run: echo "MY_VAR=hello" >> "$GITHUB_ENV"

- name: Use var (later step)
  run: echo "$MY_VAR"

But if you need to use the value within the same step, assign it to a normal shell variable in that step as well.

Also, GITHUB_ENV is for within a job; to pass values between jobs, use job outputs instead. [2]

Sources
[1] GitHub Docs — Workflow commands: “Setting an environment variable” (GITHUB_ENV subsequent-step behavior) (docs.github.com)
[2] GitHub Docs — “Store information in variables” (passing values between steps vs jobs) (docs.github.com)

Citations:


🏁 Script executed:

# Find and examine the workflow file
find . -name "build_new.yml" -type f

Repository: 2697a/bujuan

Length of output: 89


🏁 Script executed:

cat -n ./.github/workflows/build_new.yml

Repository: 2697a/bujuan

Length of output: 9756


Avoid exporting signing secrets through GITHUB_ENV.

Lines 43–45 write sensitive values into the job-wide environment, making them available to all downstream steps. Since the Build APKs step directly accesses secrets and subsequent steps don't need these values, keep only the keystore path in global scope.

💡 Limit global env to keystore path only
       - name: Decode Keystore
         run: |
           echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > ${{ runner.temp }}/keystore.jks
           echo "KEYSTORE_PATH=${{ runner.temp }}/keystore.jks" >> $GITHUB_ENV
-          echo "KEYSTORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> $GITHUB_ENV
-          echo "KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}" >> $GITHUB_ENV
-          echo "KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}" >> $GITHUB_ENV
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build_new.yml around lines 42 - 45, Remove exporting
sensitive secrets to the job-wide environment: only set KEYSTORE_PATH in
GITHUB_ENV and stop writing KEYSTORE_PASSWORD, KEY_ALIAS, and KEY_PASSWORD
there. Instead, keep KEYSTORE_PASSWORD, KEY_ALIAS, and KEY_PASSWORD as
step-level secrets consumed directly by the Build APKs step (use the step's env
or with: secrets.* so they aren't exposed to downstream steps). Update the
workflow to reference KEYSTORE_PATH from GITHUB_ENV and pass the other three
values from secrets only where needed (e.g., the build/sign step) to prevent
job-wide exposure.

Comment on lines +29 to +36
# 验证子模块中是否包含必要的 .g.dart 文件
- name: Verify generated files in submodule
run: |
echo "Checking for .g.dart files in submodule..."
ls -la ~/.pub-cache/git/bujuan_music_api-*/lib/api/song/entity/*.g.dart || echo "::warning::No .g.dart files found in submodule"

- name: Build Web
run: flutter build web --release
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing build_runner step — build will fail if .g.dart files are absent from the dependency.

The "Verify generated files" step emits a ::warning:: but does not fail, so the workflow proceeds to flutter build web --release even when no .g.dart files are present. If the login_test branch of bujuan_music_api does not pre-commit generated files, the Flutter build will fail with unresolved types from json_serializable/riverpod_generator.

Either:

  • Pre-commit all .g.dart files into the bujuan_music_api git dependency, or
  • Add a build_runner step and promote the verify step to a hard failure:
⚙️ Proposed fix
+     - name: Generate code
+       run: flutter pub run build_runner build --delete-conflicting-outputs
+
      - name: Verify generated files in submodule
        run: |
          echo "Checking for .g.dart files in submodule..."
-         ls -la ~/.pub-cache/git/bujuan_music_api-*/lib/api/song/entity/*.g.dart || echo "::warning::No .g.dart files found in submodule"
+         ls -la ~/.pub-cache/git/bujuan_music_api-*/lib/api/song/entity/*.g.dart

      - name: Build Web
        run: flutter build web --release
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# 验证子模块中是否包含必要的 .g.dart 文件
- name: Verify generated files in submodule
run: |
echo "Checking for .g.dart files in submodule..."
ls -la ~/.pub-cache/git/bujuan_music_api-*/lib/api/song/entity/*.g.dart || echo "::warning::No .g.dart files found in submodule"
- name: Build Web
run: flutter build web --release
# 验证子模块中是否包含必要的 .g.dart 文件
- name: Generate code
run: flutter pub run build_runner build --delete-conflicting-outputs
- name: Verify generated files in submodule
run: |
echo "Checking for .g.dart files in submodule..."
ls -la ~/.pub-cache/git/bujuan_music_api-*/lib/api/song/entity/*.g.dart
- name: Build Web
run: flutter build web --release
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/web.yml around lines 29 - 36, The workflow currently only
warns when .g.dart files are missing in the submodule ("Verify generated files
in submodule") which lets the subsequent "Build Web" step run and fail; update
the job to either (A) run flutter pub run build_runner build (or flutter pub get
then build_runner) in the workspace of the git dependency before the "Build Web"
step to generate missing *.g.dart files, or (B) change the verify step to exit
non-zero when the ls check fails so the workflow stops early; target the verify
step name and the subsequent "Build Web" step when implementing the fix so
generation runs or the job fails before building.

@@ -0,0 +1 @@
Subproject commit 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Step 1: Check if flutter file exists and list its contents
echo "=== Checking flutter submodule file ==="
if [ -f "flutter" ]; then
  cat -n flutter
  echo ""
  echo "=== File details ==="
  file flutter
else
  echo "flutter file not found"
fi

echo ""
echo "=== Checking if flutter is a git submodule ==="
git config --file .gitmodules --get-regexp path | grep flutter

echo ""
echo "=== Git submodules configuration ==="
if [ -f ".gitmodules" ]; then
  cat -n .gitmodules
fi

Repository: 2697a/bujuan

Length of output: 204


🏁 Script executed:

#!/bin/bash
# Step 2: Search for pubspec.yaml and check bujuan_music_api dependency
echo "=== Locating pubspec.yaml ==="
fd pubspec.yaml

echo ""
echo "=== Checking pubspec.yaml for bujuan_music_api dependency ==="
fd pubspec.yaml -x cat -n {} \;

Repository: 2697a/bujuan

Length of output: 5670


🏁 Script executed:

#!/bin/bash
# Check for flutter directory and its contents
echo "=== Checking for flutter directory ==="
if [ -d "flutter" ]; then
  echo "flutter directory found"
  ls -la flutter/
else
  echo "flutter directory not found"
fi

echo ""
echo "=== Checking git status for flutter-related entries ==="
git ls-files | grep -i flutter

echo ""
echo "=== Checking .gitmodules history ==="
git log --oneline -n 20 -- .gitmodules 2>/dev/null || echo "No history available"

echo ""
echo "=== Checking for any recent deletions of flutter ==="
git status --short

Repository: 2697a/bujuan

Length of output: 1060


🏁 Script executed:

#!/bin/bash
# Check git history to see if flutter submodule was ever committed
echo "=== Checking git log for flutter-related changes ==="
git log --all --oneline --name-status | grep -i flutter | head -20

Repository: 2697a/bujuan

Length of output: 875


🏁 Script executed:

#!/bin/bash
echo "=== Contents of flutter directory ==="
ls -la flutter/
echo ""
echo "=== File count in flutter directory ==="
find flutter -type f | wc -l
echo ""
echo "=== Is flutter a git submodule? ==="
git config --file .gitmodules --get-regexp ^ || echo "No submodule configuration found"

Repository: 2697a/bujuan

Length of output: 317


🏁 Script executed:

#!/bin/bash
# Check git cat-file for the flutter entry to see what it actually is
echo "=== Git object details for flutter ==="
git ls-files -s flutter
echo ""
echo "=== Checking if flutter is tracked as submodule ==="
git config --file .gitmodules --list

Repository: 2697a/bujuan

Length of output: 299


Submodule pinned to an unmerged feature-branch commit with incomplete configuration — resolve before merging.

The submodule flutter is tracked in git's index (mode 160000) at commit 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536, which resides on the login_test feature branch. However, the .gitmodules configuration file is missing, leaving the submodule in a broken/incomplete state. The flutter directory exists but is uninitialized.

Additionally, there is a dual-dependency problem creating divergence risk:

  1. The git submodule is pinned to 582a0e7
  2. pubspec.yaml references https://github.com/Zhenghao-Wen/bujuan_music_api.git with ref: login_test (a mutable branch name)

If the login_test branch advances past 582a0e7, these two dependency resolution paths will resolve different versions of the API, causing build or runtime inconsistencies.

Before merging:

  1. Complete the submodule configuration by adding proper .gitmodules entry, or remove the incomplete submodule entry entirely.
  2. Ensure the companion bujuan_music_api PR is merged to its main branch.
  3. Pin pubspec.yaml's bujuan_music_api dependency to an explicit commit SHA instead of the mutable ref: login_test.
  4. Update the submodule pointer (if retained) to match the pinned commit.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flutter` at line 1, The flutter git submodule is incomplete (no .gitmodules)
and pinned to commit 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536 while pubspec.yaml
depends on bujuan_music_api via ref: login_test, creating dual-dependency drift;
fix by either fully configuring the submodule (add a .gitmodules entry for the
flutter submodule and run git submodule init/update) or remove the submodule
entry from the index if you don't intend to keep it, then ensure
bujuan_music_api is merged to main and update pubspec.yaml to pin the dependency
to the explicit commit SHA that matches the submodule (replace ref: login_test
with commit: 582a0e7c5581dc0ca5f7bfd8662bb8db6f59d536) and, if retaining the
submodule, update the submodule pointer to that same SHA so both resolution
paths reference the identical commit.

Comment on lines +47 to +54
Future<void> _loadManualCookie() async {
final prefs = await SharedPreferences.getInstance();
final cookie = prefs.getString('manual_cookie');
if (cookie != null && cookie.isNotEmpty) {
BujuanMusicManager().setCookie(cookie);
debugPrint('Loaded manual cookie from storage');
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sensitive credential stored in plaintext SharedPreferences — consider encryption.

The manual_cookie value is a raw session cookie (auth credential) written and read from SharedPreferences in plaintext. On rooted Android devices or with ADB backup enabled, this key-value pair is trivially extractable.

Consider using flutter_secure_storage to store the cookie under the OS keychain/keystore instead of SharedPreferences. This would require updating both the write path in login_page.dart and the read path here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/main.dart` around lines 47 - 54, The _loadManualCookie function currently
reads a plaintext auth cookie from SharedPreferences and passes it to
BujuanMusicManager().setCookie; replace this with secure storage by switching
from SharedPreferences to flutter_secure_storage (or platform keychain/keystore)
for both read and write paths so credentials aren't stored in plaintext. Update
_loadManualCookie to read using FlutterSecureStorage (same key 'manual_cookie'),
and also update the write location in login_page.dart where the cookie is saved
to use the same secure API; ensure null/empty checks remain and that
BujuanMusicManager().setCookie is called only after retrieving the secure value.

Comment on lines +30 to +31
var songEntity = list[2] as RecommendSongEntity; // 改用旧实体
var recommendSongEntity = list[3] as RecommendSongEntity; // 改用旧实体
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the provider.dart file
find . -type f -name "provider.dart" | grep -E "home|pages"

Repository: 2697a/bujuan

Length of output: 277


🏁 Script executed:

# Also check the structure to understand the codebase
git ls-files | head -20

Repository: 2697a/bujuan

Length of output: 777


🏁 Script executed:

# Read the provider.dart file to see the context
cat -n ./lib/pages/home/provider.dart

Repository: 2697a/bujuan

Length of output: 3310


🏁 Script executed:

# Search for BujuanMusicManager and recommendSongs() definition
rg "BujuanMusicManager|recommendSongs" --type dart -A 3 -B 1

Repository: 2697a/bujuan

Length of output: 10763


🏁 Script executed:

# Search for the BujuanMusicManager class definition
rg "class BujuanMusicManager" --type dart -A 20

Repository: 2697a/bujuan

Length of output: 38


🏁 Script executed:

# Search for recommendSongs method signature in the codebase
rg "recommendSongs\(\)" --type dart -B 2 -A 5

Repository: 2697a/bujuan

Length of output: 1292


🏁 Script executed:

# Check if bujuan_music_api is a package in this repo or external
find . -type f -name "pubspec.yaml" | head -5

Repository: 2697a/bujuan

Length of output: 70


🏁 Script executed:

# Check pubspec.yaml to see bujuan_music_api dependency
cat ./pubspec.yaml | grep -A 10 -B 2 "bujuan_music_api"

Repository: 2697a/bujuan

Length of output: 434


🌐 Web query:

site:github.com/Zhenghao-Wen/bujuan_music_api recommendSongs return type

💡 Result:

I couldn’t determine the recommendSongs return type from the web UI, because:

  • The repo exists and is public, but GitHub isn’t exposing the Dart source listing/content in the pages I can access here. (github.com)
  • GitHub’s code search for repo:Zhenghao-Wen/bujuan_music_api recommendSongs is blocked unless you’re signed in, so I can’t locate the symbol that way either. (github.com)

How to get it quickly on your side:

git clone https://github.com/Zhenghao-Wen/bujuan_music_api.git
cd bujuan_music_api
git checkout login_test
rg "recommendSongs" -n lib
# then open the matching .dart file and read the function signature

Citations:


🏁 Script executed:

# Clone the bujuan_music_api repo and check the recommendSongs signature
cd /tmp && git clone https://github.com/Zhenghao-Wen/bujuan_music_api.git
cd bujuan_music_api
git checkout login_test
rg "recommendSongs" -n lib --type dart

Repository: 2697a/bujuan

Length of output: 563


Handle nullable recommendSongs() responses before casting.

BujuanMusicManager.recommendSongs() returns Future<RecommendSongEntity?>, but lines 30–31 force-cast to non-nullable RecommendSongEntity, causing a runtime crash if null is returned. Line 60 correctly uses nullable access (?.), establishing the inconsistency.

Change casts to nullable and propagate with null-safe operators:

Suggested fix
-  var songEntity = list[2] as RecommendSongEntity;
-  var recommendSongEntity = list[3] as RecommendSongEntity;
+  var songEntity = list[2] as RecommendSongEntity?;
+  var recommendSongEntity = list[3] as RecommendSongEntity?;

-  var songs = songEntity.data?.dailySongs ?? [];
+  var songs = songEntity?.data?.dailySongs ?? [];
...
-  var recommend = recommendSongEntity.data?.dailySongs ?? [];
+  var recommend = recommendSongEntity?.data?.dailySongs ?? [];

Also applies to: Lines 33, 48

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/home/provider.dart` around lines 30 - 31, The code force-casts
results of BujuanMusicManager.recommendSongs() to non-nullable
RecommendSongEntity (variables songEntity and recommendSongEntity and other
similar casts at the indicated locations), which can throw when the method
returns null; change those casts to nullable types (RecommendSongEntity?) and
update usages to use null-aware operators (?., ?? or early-null checks) or guard
returns so you safely handle null responses from recommendSongs() instead of
force-casting to non-nullable.

Comment on lines +232 to +289
_qrTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
if (_qrKey == null) return;
try {
final result = await BujuanMusicManager().qrCodeLogin(key: _qrKey!);
if (result == null) return;

switch (result.code) {
case 800:
_showTopSnackBar('QR code expired, please refresh');
timer.cancel();
setState(() {
_isQrMode = false;
_qrKey = null;
});
break;
case 801:
case 802:
// waiting
break;
case 803:
timer.cancel();
if (result.cookie != null && result.cookie!.isNotEmpty) {
// 保存到内存
BujuanMusicManager().setCookie(result.cookie!);
// 保存到 SharedPreferences
final prefs = await SharedPreferences.getInstance();
await prefs.setString('manual_cookie', result.cookie!);
try {
final userInfo = await BujuanMusicManager().userInfo();
if (userInfo?.profile != null) {
await setValue(AppConfig.userInfoKey, userInfo!.profile!.toJson());
if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst);
context.replace(AppRouter.home);
}
} else {
_showTopSnackBar('Failed to get user info with this cookie');
await prefs.remove('manual_cookie');
BujuanMusicManager().clearCookie();
}
} catch (e) {
_showTopSnackBar('Error fetching user info: $e');
await prefs.remove('manual_cookie');
BujuanMusicManager().clearCookie();
}
} else {
_showTopSnackBar('No cookie received, please try manual input');
}
break;
default:
if (result.message != null) {
_showTopSnackBar(result.message!);
}
}
} catch (e) {
print('QR check error: $e');
}
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Dart Timer.periodic async callback wait for future completion before next tick

💡 Result:

Timer.periodic cannot “wait” for an async callback to finish before scheduling the next tick—the callback type is void Function(Timer) (not Future), so if you start async work inside it, ticks can overlap. (api.flutter.dev)

Option A (recommended): don’t use Timer.periodic; use an async loop

Future<void> poll(Duration period) async {
  while (true) {
    final sw = Stopwatch()..start();
    await doWork(); // waits for completion
    final remaining = period - sw.elapsed;
    if (remaining > Duration.zero) await Future.delayed(remaining);
  }
}

This guarantees no overlap because await pauses until the Future completes. (dart.dev)

Option B: self-rescheduling one-shot Timer (no overlap, Timer-based)

Timer? _t;
bool _running = true;

void start(Duration period) {
  void schedule() {
    _t = Timer(period, () async {
      await doWork();
      if (_running) schedule();
    });
  }
  schedule();
}

void stop() { _running = false; _t?.cancel(); }

Option C: keep Timer.periodic, but prevent re-entry (skips ticks while busy)

bool busy = false;
Timer.periodic(period, (_) async {
  if (busy) return;
  busy = true;
  try { await doWork(); } finally { busy = false; }
});

Citations:


Prevent overlapping QR poll requests.

The Timer.periodic callback is async; because Timer.periodic does not wait for async work to complete before the next tick, new ticks can start before the previous qrCodeLogin finishes, causing concurrent polls and duplicate state transitions.

💡 Add an in-flight guard for polling
+ bool _qrPollInFlight = false;
  ...
  _qrTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
+   if (_qrPollInFlight) return;
+   _qrPollInFlight = true;
    if (_qrKey == null) return;
    try {
      final result = await BujuanMusicManager().qrCodeLogin(key: _qrKey!);
      ...
    } catch (e) {
      print('QR check error: $e');
+   } finally {
+     _qrPollInFlight = false;
    }
  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_qrTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
if (_qrKey == null) return;
try {
final result = await BujuanMusicManager().qrCodeLogin(key: _qrKey!);
if (result == null) return;
switch (result.code) {
case 800:
_showTopSnackBar('QR code expired, please refresh');
timer.cancel();
setState(() {
_isQrMode = false;
_qrKey = null;
});
break;
case 801:
case 802:
// waiting
break;
case 803:
timer.cancel();
if (result.cookie != null && result.cookie!.isNotEmpty) {
// 保存到内存
BujuanMusicManager().setCookie(result.cookie!);
// 保存到 SharedPreferences
final prefs = await SharedPreferences.getInstance();
await prefs.setString('manual_cookie', result.cookie!);
try {
final userInfo = await BujuanMusicManager().userInfo();
if (userInfo?.profile != null) {
await setValue(AppConfig.userInfoKey, userInfo!.profile!.toJson());
if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst);
context.replace(AppRouter.home);
}
} else {
_showTopSnackBar('Failed to get user info with this cookie');
await prefs.remove('manual_cookie');
BujuanMusicManager().clearCookie();
}
} catch (e) {
_showTopSnackBar('Error fetching user info: $e');
await prefs.remove('manual_cookie');
BujuanMusicManager().clearCookie();
}
} else {
_showTopSnackBar('No cookie received, please try manual input');
}
break;
default:
if (result.message != null) {
_showTopSnackBar(result.message!);
}
}
} catch (e) {
print('QR check error: $e');
}
});
_qrTimer = Timer.periodic(const Duration(seconds: 2), (timer) async {
if (_qrPollInFlight) return;
_qrPollInFlight = true;
if (_qrKey == null) return;
try {
final result = await BujuanMusicManager().qrCodeLogin(key: _qrKey!);
if (result == null) return;
switch (result.code) {
case 800:
_showTopSnackBar('QR code expired, please refresh');
timer.cancel();
setState(() {
_isQrMode = false;
_qrKey = null;
});
break;
case 801:
case 802:
// waiting
break;
case 803:
timer.cancel();
if (result.cookie != null && result.cookie!.isNotEmpty) {
// 保存到内存
BujuanMusicManager().setCookie(result.cookie!);
// 保存到 SharedPreferences
final prefs = await SharedPreferences.getInstance();
await prefs.setString('manual_cookie', result.cookie!);
try {
final userInfo = await BujuanMusicManager().userInfo();
if (userInfo?.profile != null) {
await setValue(AppConfig.userInfoKey, userInfo!.profile!.toJson());
if (mounted) {
Navigator.popUntil(context, (route) => route.isFirst);
context.replace(AppRouter.home);
}
} else {
_showTopSnackBar('Failed to get user info with this cookie');
await prefs.remove('manual_cookie');
BujuanMusicManager().clearCookie();
}
} catch (e) {
_showTopSnackBar('Error fetching user info: $e');
await prefs.remove('manual_cookie');
BujuanMusicManager().clearCookie();
}
} else {
_showTopSnackBar('No cookie received, please try manual input');
}
break;
default:
if (result.message != null) {
_showTopSnackBar(result.message!);
}
}
} catch (e) {
print('QR check error: $e');
} finally {
_qrPollInFlight = false;
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/login/login_page.dart` around lines 232 - 289, The periodic QR
check can overlap because the Timer.periodic callback is async; add an in-flight
guard (e.g., a private bool _isQrChecking) and check it at the top of the
Timer.periodic callback used to poll in _qrTimer and return immediately if true,
then set _isQrChecking = true before calling BujuanMusicManager().qrCodeLogin
and ensure you clear it in a finally block (set to false) after the
await/processing so no concurrent qrCodeLogin calls happen; keep the existing
expiry handling (timer.cancel / state updates) but ensure the guard is cleared
on all paths including errors.

Comment on lines +257 to +258
final prefs = await SharedPreferences.getInstance();
await prefs.setString('manual_cookie', result.cookie!);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Is Flutter shared_preferences encrypted by default on Android/iOS recommended for storing auth tokens cookies security

💡 Result:

No. Flutter shared_preferences is not encrypted by default on either platform—it’s just a wrapper over Android SharedPreferences and iOS NSUserDefaults (meant for simple app settings), and the plugin explicitly says it “must not be used for storing critical data.” [1]

Should you store auth tokens / session cookies in shared_preferences?

Not recommended. Anything stored there can be extracted on compromised devices (e.g., rooted/jailbroken, backups, device-level forensics). OWASP explicitly treats storing sensitive material in SharedPreferences as a “not recommended” storage choice compared to KeyStore/Keychain-backed approaches. [2]

Recommended approach for tokens on Android/iOS (Flutter)

Use platform secure storage (Keychain/KeyStore). In Flutter, the common choice is flutter_secure_storage, which stores secrets using iOS Keychain and encrypted shared preferences backed by Android Keystore (via Tink). [3]

Rule of thumb

  • shared_preferences: UI settings, feature flags, non-sensitive prefs.
  • flutter_secure_storage (Keychain/KeyStore): access/refresh tokens, session secrets, sensitive identifiers. [1][3]

Sources: shared_preferences package docs [1], OWASP Mobile AppSec Testing Guide (storage/key handling) [2], flutter_secure_storage package docs [3].


🏁 Script executed:

# Find and check lines around 389-390 in the login_page.dart file
grep -n "remove\|setString\|manual_cookie" lib/pages/login/login_page.dart | head -20

Repository: 2697a/bujuan

Length of output: 405


Replace SharedPreferences with FlutterSecureStorage for storing authentication cookies.

shared_preferences stores data in plaintext (not encrypted by default on Android or iOS) and is explicitly unsuitable for storing sensitive credentials like auth cookies. Use flutter_secure_storage instead, which encrypts data via Android Keystore and iOS Keychain.

This applies to lines 258, 269, 274 (and 390, 400, 406 for the same manual_cookie key).

Replace all cookie persistence calls
-import 'package:shared_preferences/shared_preferences.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';

...
+static const _secureStorage = FlutterSecureStorage();

...
- final prefs = await SharedPreferences.getInstance();
- await prefs.setString('manual_cookie', result.cookie!);
+ await _secureStorage.write(key: 'manual_cookie', value: result.cookie!);

...
- await prefs.remove('manual_cookie');
+ await _secureStorage.delete(key: 'manual_cookie');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/login/login_page.dart` around lines 257 - 258, Replace plaintext
SharedPreferences usage for the 'manual_cookie' key with encrypted
FlutterSecureStorage: where you currently call SharedPreferences.getInstance()
and prefs.setString('manual_cookie', result.cookie!), instantiate a
FlutterSecureStorage (e.g., final storage = FlutterSecureStorage()) and use
storage.write(key: 'manual_cookie', value: result.cookie) and storage.read(key:
'manual_cookie') for reads; update all occurrences referenced (the spots using
SharedPreferences, prefs, setString and the 'manual_cookie' key at the other
locations) and add the import for flutter_secure_storage plus appropriate
async/error handling around read/write operations.

Comment on lines +476 to +477
: const Text('Get an SMS QR code'),
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix misleading button copy.

Line 476 says “Get an SMS QR code”, but this action sends an SMS verification code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/login/login_page.dart` around lines 476 - 477, Update the
misleading button label in the login_page widget: replace the Text('Get an SMS
QR code') string with a clear action like Text('Send SMS verification code') (or
'Send SMS code') in the button widget so the UI reflects that the action sends
an SMS verification code rather than providing a QR code.

Comment on lines 2 to 3
import 'package:riverpod_annotation/riverpod_annotation.dart';

import '../../widgets/lyric/mode/lyric_line.dart';

part 'provider.g.dart';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Dangling part directive will cause a compile-time error.

provider.g.dart was deleted as part of this PR, but the part 'provider.g.dart' directive still references it. Dart requires the part file to exist at compile time, so this will break the build. The riverpod_annotation import is also unused since no @riverpod annotations remain.

Remove both until the lyrics provider is restored:

🐛 Proposed fix
-import 'package:riverpod_annotation/riverpod_annotation.dart';
-part 'provider.g.dart';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../widgets/lyric/mode/lyric_line.dart';
part 'provider.g.dart';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/play/provider.dart` around lines 2 - 3, Remove the dangling part
directive and unused import: delete the line containing part 'provider.g.dart'
and the import 'package:riverpod_annotation/riverpod_annotation.dart' from
provider.dart (or re-add them only if you restore the `@riverpod-annotated` lyrics
provider and its generated file); this eliminates the compile-time reference to
the deleted provider.g.dart and removes the unused riverpod_annotation import.

Comment on lines +41 to +43
git:
url: https://github.com/Zhenghao-Wen/bujuan_music_api.git
ref: login_test
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Pin bujuan_music_api to a commit SHA, not a branch name.

ref: login_test tracks the tip of a mutable branch. Two builds at different times (or on different machines) can silently resolve to different commits, making builds non-reproducible and CI results unreliable. Once the companion bujuan_music_api PR is merged, replace the branch ref with the exact commit SHA (or a tag):

♻️ Proposed fix
   bujuan_music_api:
    git:
     url: https://github.com/Zhenghao-Wen/bujuan_music_api.git
-    ref: login_test
+    ref: <commit-sha-after-companion-pr-merge>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pubspec.yaml` around lines 41 - 43, The git dependency entry referencing
bujuan_music_api currently uses a mutable branch ref ('ref: login_test') which
makes builds non-reproducible; update the pubspec.yaml git dependency (the block
with "git: url: https://github.com/Zhenghao-Wen/bujuan_music_api.git" and "ref:
login_test") to use the exact commit SHA (or a released tag) instead of the
branch name, then run pub get to refresh lockfiles and commit the updated
lockfile so CI uses the pinned commit.

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