feat(ui): add Mornye glass theme and adaptive music player

Add a selectable Mornye theme with shared glass controls, dialogs, sheets, notifications, and collection headers while retaining Material components.

Update player artwork, lyrics transitions and highlighting, synchronized rolling time labels, volume controls, and compact navigation. Unify metadata, Library, search, and extension surfaces; preserve restored artwork and report extension load failures.

Validation: full Flutter analyzer clean; 713 tests passed and 4 skipped; pinned Dart formatting and diff checks passed; signed ARM64 and ARM32 release APKs built and verified.
This commit is contained in:
zarzet committed 2026-09-19 12:10:10 +07:00
1 parent 1ebdac8645
commit 63e1a9b9c8
155 files changed
+15707 -2318

No files matched your search

+139 -4
View File
@@ -3,6 +3,76 @@ import 'package:flutter/widgets.dart';
enum ShellTab { home, library, repository, settings }
class ShellNavigationService {
static final homeSearchRequests = ValueNotifier<int>(0);
static final chromeBrightness = ValueNotifier<Brightness?>(null);
static final chromeSurface = ValueNotifier<Color?>(null);
static final _visiblePages = <GlobalKey<NavigatorState>, Route<dynamic>?>{};
static final _chromeScopes =
<
Object,
({ModalRoute<dynamic> route, Brightness brightness, Color? surface})
>{};
static bool _chromeUpdateScheduled = false;
static int _homeSearchGeneration = 0;
static void setChromeBrightness({
required Object owner,
required ModalRoute<dynamic> route,
required Brightness brightness,
Color? surface,
}) {
final scope = (route: route, brightness: brightness, surface: surface);
if (_chromeScopes[owner] == scope) return;
_chromeScopes[owner] = scope;
_scheduleChromeUpdate();
}
static void clearChromeBrightness(Object owner) {
_chromeScopes.remove(owner);
_scheduleChromeUpdate();
}
static void _scheduleChromeUpdate() {
if (_chromeUpdateScheduled) return;
_chromeUpdateScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_chromeUpdateScheduled = false;
final activePage = _visiblePages[_activeTabNavigatorKey()];
Brightness? brightness;
Color? surface;
for (final scope in _chromeScopes.values) {
if (identical(scope.route, activePage)) {
brightness = scope.brightness;
surface = scope.surface;
}
}
chromeBrightness.value = brightness;
chromeSurface.value = surface;
});
WidgetsBinding.instance.ensureVisualUpdate();
}
static void requestHomeSearch() {
if (!requestTab(ShellTab.home)) return;
final generation = ++_homeSearchGeneration;
final owner = _tabSelectionOwner;
// PageView creates Home lazily. Deliver the request after its navigator
// and focus listener mount, rather than losing the first tap on another tab.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (generation != _homeSearchGeneration ||
!identical(owner, _tabSelectionOwner) ||
_currentTabIndex != 0) {
return;
}
homeTabNavigatorKey.currentState?.popUntil((route) => route.isFirst);
homeSearchRequests.value++;
// Home scrolls to its search field and requests focus after layout.
// Scheduling that frame avoids waiting for another tap to wake it up.
WidgetsBinding.instance.ensureVisualUpdate();
});
WidgetsBinding.instance.ensureVisualUpdate();
}
static final GlobalKey<NavigatorState> homeTabNavigatorKey =
GlobalKey<NavigatorState>();
static final GlobalKey<NavigatorState> libraryTabNavigatorKey =
@@ -40,16 +110,81 @@ class ShellNavigationService {
required int currentTabIndex,
required bool showRepoTab,
}) {
if (_currentTabIndex == currentTabIndex && _showRepoTab == showRepoTab) {
return;
}
_currentTabIndex = currentTabIndex;
_showRepoTab = showRepoTab;
_scheduleChromeUpdate();
}
static NavigatorState? activeTabNavigator() {
if (_currentTabIndex == 0) return homeTabNavigatorKey.currentState;
if (_currentTabIndex == 1) return libraryTabNavigatorKey.currentState;
static GlobalKey<NavigatorState>? _activeTabNavigatorKey() {
if (_currentTabIndex == 0) return homeTabNavigatorKey;
if (_currentTabIndex == 1) return libraryTabNavigatorKey;
if (_showRepoTab && _currentTabIndex == 2) {
return repoTabNavigatorKey.currentState;
return repoTabNavigatorKey;
}
return null;
}
static NavigatorState? activeTabNavigator() =>
_activeTabNavigatorKey()?.currentState;
}
/// Track the top page in each tab. Dialogs keep the underlying page's contrast;
/// a pushed album, another tab, or a back gesture restores its own appearance.
class ShellChromeObserver extends NavigatorObserver {
ShellChromeObserver(this.navigatorKey);
final GlobalKey<NavigatorState> navigatorKey;
final _routes = <Route<dynamic>>[];
void _update() {
Route<dynamic>? page;
for (final route in _routes.reversed) {
if (route is PageRoute<dynamic>) {
page = route;
break;
}
}
ShellNavigationService._visiblePages[navigatorKey] = page;
ShellNavigationService._scheduleChromeUpdate();
}
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
_routes.add(route);
_update();
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
_routes.remove(route);
_update();
}
@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
_routes.remove(route);
_update();
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
final index = oldRoute == null ? -1 : _routes.indexOf(oldRoute);
if (index >= 0) {
if (newRoute == null) {
_routes.removeAt(index);
} else {
_routes[index] = newRoute;
}
}
_update();
}
void detach() {
_routes.clear();
ShellNavigationService._visiblePages.remove(navigatorKey);
ShellNavigationService._scheduleChromeUpdate();
}
}