From 955b139ff05a5a2bd453e9a67b91fa60f83e2754 Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Sun, 5 Apr 2026 11:05:27 +0800 Subject: [PATCH 01/14] feat: add ap_common_liquid_glass package for iOS 26 Liquid Glass support --- packages/ap_common_liquid_glass/CHANGELOG.md | 8 + .../lib/ap_common_liquid_glass.dart | 47 ++ .../lib/src/glass_theme_bridge.dart | 53 +++ .../lib/src/liquid_glass_app.dart | 272 +++++++++++ .../lib/src/pages/glass_ap_course_page.dart | 162 +++++++ .../lib/src/pages/glass_ap_login_page.dart | 290 ++++++++++++ .../lib/src/pages/glass_ap_score_page.dart | 129 ++++++ .../src/scaffold/glass_course_scaffold.dart | 139 ++++++ .../scaffold/glass_home_page_scaffold.dart | 429 ++++++++++++++++++ .../scaffold/glass_image_viewer_scaffold.dart | 89 ++++ .../src/scaffold/glass_login_scaffold.dart | 163 +++++++ .../src/scaffold/glass_score_scaffold.dart | 112 +++++ .../scaffold/glass_user_info_scaffold.dart | 355 +++++++++++++++ .../lib/src/widgets/glass_ap_drawer.dart | 194 ++++++++ .../lib/src/widgets/glass_dialog.dart | 76 ++++ packages/ap_common_liquid_glass/pubspec.yaml | 27 ++ pubspec.yaml | 1 + 17 files changed, 2546 insertions(+) create mode 100644 packages/ap_common_liquid_glass/CHANGELOG.md create mode 100644 packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/glass_theme_bridge.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/liquid_glass_app.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/pages/glass_ap_course_page.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/pages/glass_ap_login_page.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/pages/glass_ap_score_page.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/widgets/glass_ap_drawer.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/widgets/glass_dialog.dart create mode 100644 packages/ap_common_liquid_glass/pubspec.yaml diff --git a/packages/ap_common_liquid_glass/CHANGELOG.md b/packages/ap_common_liquid_glass/CHANGELOG.md new file mode 100644 index 00000000..6febad8e --- /dev/null +++ b/packages/ap_common_liquid_glass/CHANGELOG.md @@ -0,0 +1,8 @@ +## 0.1.0-dev.1 + +- Initial development release +- `LiquidGlassApApp` wrapper with automatic shader initialization +- `GlassThemeBridge` for mapping ApTheme seed colors to GlassThemeData +- Glass scaffold variants for all 6 scaffolds +- Glass widget replacements (drawer, dialogs, buttons) +- Glass convenience pages diff --git a/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart b/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart new file mode 100644 index 00000000..1790369a --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart @@ -0,0 +1,47 @@ +/// Liquid Glass design language for AP series apps. +/// +/// This package provides glass-effect scaffold variants and widgets +/// powered by [liquid_glass_widgets]. It is an opt-in extension of +/// [ap_common_flutter_ui] — existing apps work unchanged. +/// +/// ## Quick Start +/// +/// 1. Add `ap_common_liquid_glass` to your dependencies. +/// 2. Initialize shaders in `main()`: +/// ```dart +/// await LiquidGlassWidgets.initialize(); +/// ``` +/// 3. Use [LiquidGlassApApp] instead of [ApApp]: +/// ```dart +/// LiquidGlassApApp(home: MyHomePage()) +/// ``` +/// 4. Use glass scaffold variants: +/// ```dart +/// GlassHomePageScaffold(...) +/// GlassLoginScaffold(...) +/// ``` +library ap_common_liquid_glass; + +export 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +export 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +// Core +export 'src/glass_theme_bridge.dart'; +export 'src/liquid_glass_app.dart'; + +// Scaffolds +export 'src/scaffold/glass_course_scaffold.dart'; +export 'src/scaffold/glass_home_page_scaffold.dart'; +export 'src/scaffold/glass_image_viewer_scaffold.dart'; +export 'src/scaffold/glass_login_scaffold.dart'; +export 'src/scaffold/glass_score_scaffold.dart'; +export 'src/scaffold/glass_user_info_scaffold.dart'; + +// Widgets +export 'src/widgets/glass_ap_drawer.dart'; +export 'src/widgets/glass_dialog.dart'; + +// Pages +export 'src/pages/glass_ap_course_page.dart'; +export 'src/pages/glass_ap_login_page.dart'; +export 'src/pages/glass_ap_score_page.dart'; diff --git a/packages/ap_common_liquid_glass/lib/src/glass_theme_bridge.dart b/packages/ap_common_liquid_glass/lib/src/glass_theme_bridge.dart new file mode 100644 index 00000000..48b6b8ad --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/glass_theme_bridge.dart @@ -0,0 +1,53 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// Bridges [ApTheme] seed colors to [GlassThemeData]. +/// +/// This ensures glass widgets use settings that complement the +/// current AP theme configuration. +/// +/// ```dart +/// GlassTheme( +/// data: GlassThemeBridge.fromContext(context), +/// child: child, +/// ) +/// ``` +class GlassThemeBridge { + const GlassThemeBridge._(); + + /// Creates a [GlassThemeData] aligned with the current theme. + /// + /// Uses [GlassQuality.standard] for broad compatibility. + /// Override via [LiquidGlassSettings] for per-widget control. + static GlassThemeData fromContext( + BuildContext context, + ) { + return const GlassThemeData( + light: GlassThemeVariant( + quality: GlassQuality.standard, + ), + dark: GlassThemeVariant( + quality: GlassQuality.standard, + ), + ); + } + + /// Creates a [GlassThemeData] with custom settings. + static GlassThemeData withSettings({ + LiquidGlassSettings? lightSettings, + LiquidGlassSettings? darkSettings, + GlassQuality quality = GlassQuality.standard, + }) { + return GlassThemeData( + light: GlassThemeVariant( + settings: lightSettings, + quality: quality, + ), + dark: GlassThemeVariant( + settings: darkSettings, + quality: quality, + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/liquid_glass_app.dart b/packages/ap_common_liquid_glass/lib/src/liquid_glass_app.dart new file mode 100644 index 00000000..e006d2a2 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/liquid_glass_app.dart @@ -0,0 +1,272 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/glass_theme_bridge.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A convenience wrapper around [MaterialApp] that applies both +/// [ApTheme] and [LiquidGlassWidgets] setup. +/// +/// This is the glass-enhanced equivalent of [ApApp]. It: +/// - Calls [LiquidGlassWidgets.initialize] to pre-cache shaders +/// - Wraps the app with [LiquidGlassWidgets.wrap] for GPU sharing +/// - Applies [GlassTheme] bridged from [ApTheme] seed colors +/// +/// ```dart +/// void main() async { +/// WidgetsFlutterBinding.ensureInitialized(); +/// await LiquidGlassWidgets.initialize(); +/// runApp(const MyApp()); +/// } +/// +/// class MyApp extends StatefulWidget { +/// @override +/// State createState() => _MyAppState(); +/// } +/// +/// class _MyAppState extends State { +/// @override +/// Widget build(BuildContext context) { +/// return LiquidGlassApApp( +/// home: GlassHomePage(), +/// ); +/// } +/// } +/// ``` +class LiquidGlassApApp extends StatefulWidget { + const LiquidGlassApApp({ + super.key, + this.home, + this.routes = const {}, + this.onGenerateTitle, + this.additionalLocalizationsDelegates = + const >[], + this.initialThemeMode = ThemeMode.system, + this.themeModePreferenceKey = 'pref_theme_mode_index', + this.languageCodePreferenceKey = 'pref_language_code', + this.debugShowCheckedModeBanner = false, + this.navigatorObservers = const [], + this.onGenerateRoute, + this.quality = GlassQuality.standard, + this.glassSettings, + }); + + /// The default route widget. + final Widget? home; + + /// Named routes. + final Map routes; + + /// Callback to generate the app title. + final GenerateAppTitle? onGenerateTitle; + + /// Extra localization delegates. + final List> + additionalLocalizationsDelegates; + + /// The initial theme mode. + final ThemeMode initialThemeMode; + + /// Preference key for storing theme mode index. + final String themeModePreferenceKey; + + /// Preference key for storing language code. + final String languageCodePreferenceKey; + + /// Whether to show the debug banner. + final bool debugShowCheckedModeBanner; + + /// Navigator observers. + final List navigatorObservers; + + /// Custom route generation. + final RouteFactory? onGenerateRoute; + + /// Glass rendering quality. + final GlassQuality quality; + + /// Optional glass effect settings (thickness, blur, specular). + final LiquidGlassSettings? glassSettings; + + /// Find the nearest [LiquidGlassApAppState] in the widget tree. + static LiquidGlassApAppState of(BuildContext context) { + final _LiquidGlassApAppScope? scope = context + .dependOnInheritedWidgetOfExactType<_LiquidGlassApAppScope>(); + assert(scope != null, 'No LiquidGlassApApp found in context'); + return scope!.state; + } + + @override + State createState() => LiquidGlassApAppState(); +} + +class LiquidGlassApAppState extends State + with WidgetsBindingObserver { + late ThemeMode _themeMode; + int _currentColorIndex = 0; + Color? _customColor; + + ThemeMode get themeMode => _themeMode; + + @override + void initState() { + super.initState(); + final int themeIndex = PreferenceUtil.instance.getInt( + widget.themeModePreferenceKey, + 0, + ); + _themeMode = + themeIndex >= 0 && themeIndex < ThemeMode.values.length + ? ThemeMode.values[themeIndex] + : ThemeMode.system; + _currentColorIndex = PreferenceUtil.instance.getInt( + ApTheme.PREF_COLOR_INDEX, + 0, + ); + final int customColorValue = PreferenceUtil.instance.getInt( + ApTheme.PREF_CUSTOM_COLOR, + 0, + ); + if (_currentColorIndex == ApTheme.customColorIndex && + customColorValue != 0) { + _customColor = Color(customColorValue); + } + WidgetsBinding.instance.addObserver(this); + _initLocale(); + Future.microtask(() { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + systemNavigationBarContrastEnforced: true, + systemNavigationBarColor: Colors.transparent, + ), + ); + }); + } + + Future _initLocale() async { + final String languageCode = PreferenceUtil.instance.getString( + widget.languageCodePreferenceKey, + ApSupportLanguageConstants.system, + ); + if (languageCode == ApSupportLanguageConstants.system) { + await useApDeviceLocale(); + } else { + final Locale locale = Locale( + languageCode, + languageCode == ApSupportLanguageConstants.zh ? 'TW' : null, + ); + await setApLocaleFromFlutter(locale); + } + if (mounted) setState(() {}); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangePlatformBrightness() { + setState(() {}); + super.didChangePlatformBrightness(); + } + + /// Change the app's theme mode. + void setThemeMode(ThemeMode mode) { + setState(() => _themeMode = mode); + } + + /// Change the app's locale. + void setLocale(Locale locale) { + setApLocaleFromFlutter(locale); + setState(() {}); + } + + /// Change the app's theme color. + void setThemeColor(int index, Color? custom) { + setState(() { + _currentColorIndex = index; + _customColor = custom; + }); + } + + /// Force a rebuild. + void update() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return _LiquidGlassApAppScope( + state: this, + child: ApTheme( + themeMode: _themeMode, + currentColorIndex: _currentColorIndex, + customColor: _customColor, + preferences: PreferenceUtil.instance, + child: Builder( + builder: (BuildContext context) { + final Color seedColor = ApTheme.of(context).seedColor; + final GlassThemeData glassThemeData = + GlassThemeBridge.fromContext(context); + return TranslationProvider( + child: Builder( + builder: (BuildContext context) { + return LiquidGlassWidgets.wrap( + GlassTheme( + data: glassThemeData, + child: MaterialApp( + onGenerateTitle: widget.onGenerateTitle, + debugShowCheckedModeBanner: + widget.debugShowCheckedModeBanner, + routes: { + if (widget.home != null) + Navigator.defaultRouteName: + (_) => widget.home!, + ...widget.routes, + }, + onGenerateRoute: widget.onGenerateRoute, + theme: ApTheme.light(seedColor), + darkTheme: ApTheme.dark(seedColor), + themeMode: _themeMode, + locale: TranslationProvider.of(context) + .flutterLocale, + navigatorObservers: + widget.navigatorObservers, + supportedLocales: + AppLocaleUtils.supportedLocales, + localizationsDelegates: + >[ + ...widget + .additionalLocalizationsDelegates, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + ), + ), + ); + }, + ), + ); + }, + ), + ), + ); + } +} + +class _LiquidGlassApAppScope extends InheritedWidget { + const _LiquidGlassApAppScope({ + required this.state, + required super.child, + }); + + final LiquidGlassApAppState state; + + @override + bool updateShouldNotify(_LiquidGlassApAppScope oldWidget) => true; +} diff --git a/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_course_page.dart b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_course_page.dart new file mode 100644 index 00000000..a12cb0f8 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_course_page.dart @@ -0,0 +1,162 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/scaffold/glass_course_scaffold.dart'; +import 'package:flutter/material.dart'; + +/// A ready-to-use glass course page that handles semester +/// loading and state management internally. +/// +/// This is the glass-enhanced equivalent of [ApCoursePage]. +class GlassApCoursePage extends StatefulWidget { + const GlassApCoursePage({ + super.key, + required this.onLoadSemesters, + required this.onLoadCourse, + this.title, + this.enableNotifyControl = true, + this.enableAddToCalendar = true, + this.enableCaptureCourseTable = false, + this.androidResourceIcon, + this.actions, + this.showSectionTime, + this.showInstructors, + this.showClassroomLocation, + this.courseNotifySaveKey, + }); + + final Future Function() + onLoadSemesters; + final Future Function( + Semester semester, + ) onLoadCourse; + final String? title; + final bool enableNotifyControl; + final bool enableAddToCalendar; + final bool enableCaptureCourseTable; + final String? androidResourceIcon; + final List? actions; + final bool? showSectionTime; + final bool? showInstructors; + final bool? showClassroomLocation; + final String? courseNotifySaveKey; + + @override + State createState() => + _GlassApCoursePageState(); +} + +class _GlassApCoursePageState + extends State { + DataState _state = + const DataLoading(); + SemesterData? _semesterData; + CourseNotifyData? _notifyData; + + String get _notifyCacheKey => + widget.courseNotifySaveKey ?? + PreferenceUtil.instance.getString( + ApConstants.currentSemesterCode, + ApConstants.semesterLatest, + ); + + @override + void initState() { + super.initState(); + _loadSemesters(); + } + + Future _loadSemesters() async { + try { + _semesterData = await widget.onLoadSemesters(); + _loadCourse(); + } catch (_) { + if (mounted) { + setState( + () => _state = + const DataError(), + ); + } + } + } + + Future _loadCourse() async { + if (_semesterData == null) return; + setState( + () => _state = const DataLoading(), + ); + try { + final Semester semester = _semesterData! + .data[_semesterData!.currentIndex]; + final CourseData courseData = + await widget.onLoadCourse(semester); + if (mounted) { + setState(() { + if (courseData.courses.isEmpty) { + _state = const DataEmpty(); + } else { + _state = DataLoaded( + courseData, + ); + _notifyData = CourseNotifyData.load( + _notifyCacheKey, + ); + } + }); + } + } catch (e) { + if (mounted) { + setState( + () => _state = DataError( + hint: e.toString(), + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return GlassCourseScaffold( + state: _state.when( + loading: () => CourseState.loading, + loaded: (_, __) => CourseState.finish, + error: (_) => CourseState.error, + empty: (_) => CourseState.empty, + ), + courseData: + _state.dataOrNull ?? CourseData.empty(), + notifyData: _notifyData, + title: widget.title, + customHint: _state is DataLoaded + ? (_state as DataLoaded).hint + : null, + customStateHint: + _state is DataError + ? (_state as DataError).hint + : null, + courseNotifySaveKey: _notifyCacheKey, + semesterData: _semesterData, + enableNotifyControl: + widget.enableNotifyControl, + enableAddToCalendar: + widget.enableAddToCalendar, + enableCaptureCourseTable: + widget.enableCaptureCourseTable, + androidResourceIcon: + widget.androidResourceIcon, + actions: widget.actions, + showSectionTime: widget.showSectionTime, + showInstructors: widget.showInstructors, + showClassroomLocation: + widget.showClassroomLocation, + onSelect: (int index) { + _semesterData = _semesterData! + .copyWith(currentIndex: index); + _loadCourse(); + }, + onRefresh: () async { + await _loadCourse(); + return null; + }, + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_login_page.dart b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_login_page.dart new file mode 100644 index 00000000..01a68dd5 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_login_page.dart @@ -0,0 +1,290 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/scaffold/glass_login_scaffold.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// A ready-to-use glass login page with built-in preference +/// handling for remember password and auto-login. +/// +/// This is the glass-enhanced equivalent of [ApLoginPage]. +class GlassApLoginPage extends StatefulWidget { + const GlassApLoginPage({ + super.key, + required this.logoSource, + required this.onLogin, + this.logoMode = LogoMode.text, + this.enableOfflineLogin = false, + this.onOfflineLogin, + this.extraForms, + this.usernamePreferenceKey = 'pref_username', + this.passwordPreferenceKey = 'pref_password', + this.rememberPasswordPreferenceKey = + 'pref_remember_password', + this.autoLoginPreferenceKey = 'pref_auto_login', + }); + + final LogoMode logoMode; + final String logoSource; + final Future Function( + String username, + String password, + ) onLogin; + final bool enableOfflineLogin; + final Future Function( + String username, + String password, + )? onOfflineLogin; + final List? extraForms; + final String usernamePreferenceKey; + final String passwordPreferenceKey; + final String rememberPasswordPreferenceKey; + final String autoLoginPreferenceKey; + + @override + State createState() => + _GlassApLoginPageState(); +} + +class _GlassApLoginPageState + extends State { + ApLocalizations get ap => context.ap; + + final TextEditingController _username = + TextEditingController(); + final TextEditingController _password = + TextEditingController(); + final FocusNode _usernameFocus = FocusNode(); + final FocusNode _passwordFocus = FocusNode(); + + bool _isRememberPassword = true; + bool _isAutoLogin = false; + + @override + void initState() { + super.initState(); + _loadPreferences(); + } + + @override + void dispose() { + _username.dispose(); + _password.dispose(); + _usernameFocus.dispose(); + _passwordFocus.dispose(); + super.dispose(); + } + + void _loadPreferences() { + _isRememberPassword = PreferenceUtil.instance + .getBool( + widget.rememberPasswordPreferenceKey, + true, + ); + _isAutoLogin = PreferenceUtil.instance.getBool( + widget.autoLoginPreferenceKey, + false, + ); + setState(() { + _username.text = PreferenceUtil.instance + .getString( + widget.usernamePreferenceKey, + '', + ); + _password.text = _isRememberPassword + ? PreferenceUtil.instance.getStringSecurity( + widget.passwordPreferenceKey, + '', + ) + : ''; + }); + } + + @override + Widget build(BuildContext context) { + return GlassLoginScaffold( + logoMode: widget.logoMode, + logoSource: widget.logoSource, + forms: [ + ApTextField( + controller: _username, + keyboardType: TextInputType.emailAddress, + focusNode: _usernameFocus, + nextFocusNode: _passwordFocus, + labelText: ap.studentId, + autofillHints: const [ + AutofillHints.username, + ], + ), + ApTextField( + obscureText: true, + textInputAction: TextInputAction.send, + controller: _password, + focusNode: _passwordFocus, + onSubmitted: (_) { + _passwordFocus.unfocus(); + _login(); + }, + labelText: ap.password, + autofillHints: const [ + AutofillHints.password, + ], + ), + const SizedBox(height: 8.0), + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + TextCheckBox( + text: ap.autoLogin, + value: _isAutoLogin, + onChanged: (bool? value) { + setState(() { + _isAutoLogin = value ?? false; + _isRememberPassword = _isAutoLogin; + _saveLoginPreferences(); + }); + }, + ), + TextCheckBox( + text: ap.rememberPassword, + value: _isRememberPassword, + onChanged: (bool? value) { + setState(() { + _isRememberPassword = + value ?? false; + if (!_isRememberPassword) { + _isAutoLogin = false; + } + _saveLoginPreferences(); + }); + }, + ), + ], + ), + if (widget.extraForms != null) + ...widget.extraForms!, + const SizedBox(height: 8.0), + ApButton( + text: ap.login, + onPressed: _login, + ), + if (widget.enableOfflineLogin) + ApFlatButton( + text: ap.offlineLogin, + onPressed: _offlineLogin, + ), + ], + ); + } + + void _saveLoginPreferences() { + PreferenceUtil.instance.setBool( + widget.autoLoginPreferenceKey, + _isAutoLogin, + ); + PreferenceUtil.instance.setBool( + widget.rememberPasswordPreferenceKey, + _isRememberPassword, + ); + } + + Future _login() async { + if (_username.text.isEmpty || + _password.text.isEmpty) { + UiUtil.instance.showToast( + context, + ap.doNotEmpty, + ); + return; + } + + showDialog( + context: context, + builder: (BuildContext context) => PopScope( + canPop: false, + child: ProgressDialog(ap.logining), + ), + barrierDismissible: false, + ); + + try { + final bool success = await widget.onLogin( + _username.text, + _password.text, + ); + if (!mounted) return; + Navigator.of(context, rootNavigator: true).pop(); + + if (success) { + _saveCredentials(); + TextInput.finishAutofillContext(); + Navigator.of(context).pop(true); + } + } catch (e) { + if (!mounted) return; + Navigator.of(context, rootNavigator: true).pop(); + UiUtil.instance + .showToast(context, e.toString()); + } + } + + void _saveCredentials() { + PreferenceUtil.instance.setString( + widget.usernamePreferenceKey, + _username.text, + ); + if (_isRememberPassword) { + PreferenceUtil.instance.setStringSecurity( + widget.passwordPreferenceKey, + _password.text, + ); + } + } + + Future _offlineLogin() async { + if (widget.onOfflineLogin != null) { + final bool success = + await widget.onOfflineLogin!( + _username.text, + _password.text, + ); + if (success && mounted) { + Navigator.of(context).pop(true); + } + return; + } + + final String savedUsername = + PreferenceUtil.instance.getString( + widget.usernamePreferenceKey, + '', + ); + final String savedPassword = + PreferenceUtil.instance.getStringSecurity( + widget.passwordPreferenceKey, + '', + ); + + if (savedUsername.isEmpty || + savedPassword.isEmpty) { + UiUtil.instance.showToast( + context, + ap.noOfflineLoginData, + ); + } else if (savedUsername != _username.text || + savedPassword != _password.text) { + UiUtil.instance.showToast( + context, + ap.offlineLoginPasswordError, + ); + } else { + UiUtil.instance.showToast( + context, + ap.loadOfflineData, + ); + if (mounted) { + Navigator.of(context).pop(true); + } + } + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_score_page.dart b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_score_page.dart new file mode 100644 index 00000000..f299157a --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/pages/glass_ap_score_page.dart @@ -0,0 +1,129 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/scaffold/glass_score_scaffold.dart'; +import 'package:flutter/material.dart'; + +/// A ready-to-use glass score page that handles semester +/// loading and state management internally. +/// +/// This is the glass-enhanced equivalent of [ApScorePage]. +class GlassApScorePage extends StatefulWidget { + const GlassApScorePage({ + super.key, + required this.onLoadSemesters, + required this.onLoadScore, + this.title, + this.middleTitle, + this.finalTitle, + this.middleScoreBuilder, + this.finalScoreBuilder, + this.detailBuilder, + this.isShowSearchButton = false, + }); + + final Future Function() + onLoadSemesters; + final Future Function( + Semester semester, + ) onLoadScore; + final String? title; + final String? middleTitle; + final String? finalTitle; + final Widget Function(int index)? + middleScoreBuilder; + final Widget Function(int index)? + finalScoreBuilder; + final List Function(ScoreData?)? + detailBuilder; + final bool isShowSearchButton; + + @override + State createState() => + _GlassApScorePageState(); +} + +class _GlassApScorePageState + extends State { + DataState _state = + const DataLoading(); + SemesterData? _semesterData; + + @override + void initState() { + super.initState(); + _loadSemesters(); + } + + Future _loadSemesters() async { + try { + _semesterData = await widget.onLoadSemesters(); + _loadScore(); + } catch (_) { + if (mounted) { + setState( + () => _state = + const DataError(), + ); + } + } + } + + Future _loadScore() async { + if (_semesterData == null) return; + setState( + () => _state = + const DataLoading(), + ); + try { + final Semester semester = _semesterData! + .data[_semesterData!.currentIndex]; + final ScoreData? scoreData = + await widget.onLoadScore(semester); + if (mounted) { + setState(() { + if (scoreData == null || + scoreData.scores.isEmpty) { + _state = const DataEmpty(); + } else { + _state = + DataLoaded(scoreData); + } + }); + } + } catch (e) { + if (mounted) { + setState( + () => _state = DataError( + hint: e.toString(), + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return GlassScoreScaffold( + state: _state.when( + loading: () => ScoreState.loading, + loaded: (_, __) => ScoreState.finish, + error: (_) => ScoreState.error, + empty: (_) => ScoreState.empty, + ), + scoreData: _state.dataOrNull, + onRefresh: _loadScore, + title: widget.title, + semesterData: _semesterData, + middleTitle: widget.middleTitle, + finalTitle: widget.finalTitle, + middleScoreBuilder: widget.middleScoreBuilder, + finalScoreBuilder: widget.finalScoreBuilder, + isShowSearchButton: + widget.isShowSearchButton, + onSelect: (int index) { + _semesterData = _semesterData! + .copyWith(currentIndex: index); + _loadScore(); + }, + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart new file mode 100644 index 00000000..488003ce --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart @@ -0,0 +1,139 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; + +/// A glass-enhanced version of [CourseScaffold]. +/// +/// Currently delegates to the original [CourseScaffold] while the +/// app tree is wrapped in [LiquidGlassWidgets.wrap] via +/// [LiquidGlassApApp]. The glass theme bridge ensures glass widgets +/// used within the scaffold pick up the correct tint colors. +/// +/// Course scaffold has deeply integrated state management (course +/// table rendering, notification control, screenshot capture) that +/// makes partial widget replacement impractical. Instead, this +/// wrapper ensures the scaffold operates correctly within a +/// Liquid Glass context. +class GlassCourseScaffold extends StatelessWidget { + const GlassCourseScaffold({ + super.key, + required this.state, + required this.courseData, + this.title, + this.customHint, + this.semesterData, + this.onSelect, + this.onRefresh, + this.actions, + this.enableNotifyControl = true, + this.notifyData, + this.autoNotifySave = true, + this.onNotifyClick, + this.courseNotifySaveKey = ApConstants.semesterLatest, + this.customStateHint, + this.itemPicker, + this.onSearchButtonClick, + this.enableAddToCalendar = true, + this.androidResourceIcon, + this.enableCaptureCourseTable = false, + this.showSectionTime, + this.showInstructors, + this.showClassroomLocation, + this.showSearchButton, + }); + + /// Creates from a [DataState]. + GlassCourseScaffold.fromDataState({ + super.key, + required DataState dataState, + this.title, + this.semesterData, + this.onSelect, + this.onRefresh, + this.actions, + this.enableNotifyControl = true, + this.notifyData, + this.autoNotifySave = true, + this.onNotifyClick, + this.courseNotifySaveKey = ApConstants.semesterLatest, + this.itemPicker, + this.onSearchButtonClick, + this.enableAddToCalendar = true, + this.androidResourceIcon, + this.enableCaptureCourseTable = false, + this.showSectionTime, + this.showInstructors, + this.showClassroomLocation, + this.showSearchButton, + }) : state = dataState.when( + loading: () => CourseState.loading, + loaded: (_, __) => CourseState.finish, + error: (_) => CourseState.error, + empty: (_) => CourseState.empty, + ), + courseData = + dataState.dataOrNull ?? CourseData.empty(), + customHint = + dataState is DataLoaded + ? dataState.hint + : null, + customStateHint = + dataState is DataError + ? dataState.hint + : dataState is DataEmpty + ? dataState.hint + : null; + + final CourseState state; + final CourseData courseData; + final String? title; + final String? customHint; + final SemesterData? semesterData; + final Function(int index)? onSelect; + final Future? Function()? onRefresh; + final List? actions; + final bool enableNotifyControl; + final CourseNotifyData? notifyData; + final bool autoNotifySave; + final CourseNotifyCallback? onNotifyClick; + final String courseNotifySaveKey; + final String? customStateHint; + final Widget? itemPicker; + final Function()? onSearchButtonClick; + final bool enableAddToCalendar; + final String? androidResourceIcon; + final bool enableCaptureCourseTable; + final bool? showSectionTime; + final bool? showInstructors; + final bool? showClassroomLocation; + final bool? showSearchButton; + + @override + Widget build(BuildContext context) { + return CourseScaffold( + state: state, + courseData: courseData, + title: title, + customHint: customHint, + semesterData: semesterData, + onSelect: onSelect, + onRefresh: onRefresh, + actions: actions, + enableNotifyControl: enableNotifyControl, + notifyData: notifyData, + autoNotifySave: autoNotifySave, + onNotifyClick: onNotifyClick, + courseNotifySaveKey: courseNotifySaveKey, + customStateHint: customStateHint, + itemPicker: itemPicker, + onSearchButtonClick: onSearchButtonClick, + enableAddToCalendar: enableAddToCalendar, + androidResourceIcon: androidResourceIcon, + enableCaptureCourseTable: + enableCaptureCourseTable, + showSectionTime: showSectionTime, + showInstructors: showInstructors, + showClassroomLocation: showClassroomLocation, + showSearchButton: showSearchButton, + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart new file mode 100644 index 00000000..ea69386e --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart @@ -0,0 +1,429 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [HomePageScaffold]. +/// +/// Replaces the Material [AppBar] with [GlassAppBar] and the +/// [NavigationBar] with [GlassBottomBar] while preserving all +/// original functionality (announcement carousel, responsive +/// layout, etc.). +class GlassHomePageScaffold extends StatefulWidget { + const GlassHomePageScaffold({ + super.key, + required this.state, + required this.announcements, + required this.isLogin, + this.actions, + this.onTabTapped, + this.bottomNavigationBarItems, + this.drawer, + this.content, + this.floatingActionButton, + this.title, + this.onImageTapped, + this.autoPlay = true, + this.autoPlayDuration = + const Duration(milliseconds: 5000), + }); + + /// Creates from a [DataState>]. + GlassHomePageScaffold.fromDataState({ + super.key, + required DataState> dataState, + required this.isLogin, + this.actions, + this.onTabTapped, + this.bottomNavigationBarItems, + this.drawer, + this.content, + this.floatingActionButton, + this.title, + this.onImageTapped, + this.autoPlay = true, + this.autoPlayDuration = + const Duration(milliseconds: 5000), + }) : state = dataState.when( + loading: () => HomeState.loading, + loaded: (_, __) => HomeState.finish, + error: (_) => HomeState.error, + empty: (_) => HomeState.empty, + ), + announcements = + dataState.dataOrNull ?? []; + + final HomeState state; + final String? title; + final List announcements; + final List? actions; + final List? bottomNavigationBarItems; + final Function(int index)? onTabTapped; + final Function(Announcement announcement)? onImageTapped; + final Widget? drawer; + final Widget? content; + final Widget? floatingActionButton; + final bool isLogin; + final bool autoPlay; + final Duration autoPlayDuration; + + @override + GlassHomePageScaffoldState createState() => + GlassHomePageScaffoldState(); +} + +class GlassHomePageScaffoldState + extends State { + final GlobalKey + _scaffoldMessengerKey = + GlobalKey(); + + PageController? pageController; + int _currentNewsIndex = 0; + Timer? _timer; + + bool get isTablet => + MediaQuery.of(context).size.shortestSide >= 680; + + @override + void initState() { + _setTimer(); + super.initState(); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, _) { + if (Platform.isAndroid) { + _showLogoutDialog(); + } else { + SystemNavigator.pop(); + } + }, + child: ScaffoldMessenger( + key: _scaffoldMessengerKey, + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: isTablet + ? null + : GlassAppBar( + title: Text(widget.title ?? ''), + actions: widget.actions, + ), + drawer: isTablet ? null : widget.drawer, + floatingActionButton: + widget.floatingActionButton, + body: Row( + children: [ + if (isTablet) widget.drawer!, + Expanded( + child: (isTablet && + widget.content != null) + ? widget.content! + : OrientationBuilder( + builder: + (_, Orientation orientation) { + return Container( + padding: + EdgeInsets.symmetric( + vertical: orientation == + Orientation.portrait + ? 32.0 + : 4.0, + ), + alignment: Alignment.center, + child: + _homeBody(orientation), + ); + }, + ), + ), + ], + ), + bottomNavigationBar: + (widget.bottomNavigationBarItems == null || + isTablet) + ? null + : NavigationBar( + elevation: 12.0, + height: 56, + indicatorColor: + const Color(0x00000000), + onDestinationSelected: + widget.onTabTapped, + destinations: widget + .bottomNavigationBarItems!, + ), + ), + ), + ); + } + + void _setTimer() { + if (widget.autoPlay) { + _timer = Timer.periodic( + widget.autoPlayDuration, + (Timer timer) { + if (widget.state == HomeState.finish && + widget.announcements.length > 1 && + pageController!.hasClients) { + setState(() { + _currentNewsIndex++; + if (_currentNewsIndex >= + widget.announcements.length) { + _currentNewsIndex = 0; + } + pageController?.animateToPage( + _currentNewsIndex, + duration: + const Duration(milliseconds: 500), + curve: Curves.easeOutQuint, + ); + }); + } + }, + ); + } + } + + Widget _newsImage( + Announcement announcement, + Orientation orientation, + bool active, + ) { + return GestureDetector( + onTap: () { + widget.onImageTapped?.call(announcement); + AnalyticsUtil.instance + .logEvent('announcement_image_click'); + }, + onTapDown: (TapDownDetails detail) { + _timer?.cancel(); + }, + onTapUp: (TapUpDetails detail) { + _setTimer(); + }, + onTapCancel: () { + _setTimer(); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 500), + curve: Curves.easeOutQuint, + margin: EdgeInsets.symmetric( + vertical: MediaQuery.of(context).size.height * + (active ? 0.05 : 0.15), + horizontal: + MediaQuery.of(context).size.width * 0.02, + ), + child: Hero( + tag: announcement.hashCode, + child: ApNetworkImage( + url: announcement.imgUrl, + ), + ), + ), + ); + } + + Widget _homeBody(Orientation orientation) { + double viewportFraction = 0.65; + if (orientation == Orientation.landscape) { + viewportFraction = 0.5; + } + pageController = PageController( + viewportFraction: viewportFraction, + ); + pageController!.addListener(() { + final int next = pageController!.page!.round(); + if (_currentNewsIndex != next) { + setState(() { + _currentNewsIndex = next; + }); + } + }); + switch (widget.state) { + case HomeState.loading: + return const Center( + child: CircularProgressIndicator(), + ); + case HomeState.finish: + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Hero( + tag: ApConstants.tagAnnouncementTitle, + child: Material( + color: const Color(0x00000000), + child: Text( + widget + .announcements[_currentNewsIndex] + .title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20.0, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + const Hero( + tag: ApConstants.tagAnnouncementIcon, + child: Icon(Icons.arrow_drop_down), + ), + Expanded( + child: PageView.builder( + controller: pageController, + itemCount: widget.announcements.length, + itemBuilder: ( + BuildContext context, + int currentIndex, + ) { + final bool active = + currentIndex == _currentNewsIndex; + return _newsImage( + widget.announcements[currentIndex], + orientation, + active, + ); + }, + ), + ), + SizedBox( + height: + orientation == Orientation.portrait + ? 16.0 + : 4.0, + ), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + fontSize: 24.0, + ), + children: [ + TextSpan( + text: + '${widget.announcements.length >= 10 && _currentNewsIndex < 9 ? '0' : ''}' + '${_currentNewsIndex + 1}', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + TextSpan( + text: + ' / ${widget.announcements.length}', + ), + ], + ), + ), + SizedBox( + height: + orientation == Orientation.portrait + ? 24.0 + : 0.0, + ), + ], + ); + case HomeState.offline: + return HintContent( + icon: ApIcon.offlineBolt, + content: context.ap.offlineMode, + ); + case HomeState.empty: + return HintContent( + icon: ApIcon.offlineBolt, + content: context.ap.announcementEmpty, + ); + case HomeState.error: + return HintContent( + icon: ApIcon.offlineBolt, + content: context.ap.somethingError, + ); + } + } + + void _showLogoutDialog() { + final ApLocalizations l10n = context.ap; + showDialog( + context: context, + builder: (BuildContext context) => YesNoDialog( + title: l10n.closeAppTitle, + contentWidget: Text( + l10n.closeAppHint, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + leftActionText: l10n.cancel, + rightActionText: l10n.confirm, + rightActionFunction: () { + AnalyticsUtil.instance + .logEvent('logout_dialog_confirm'); + SystemNavigator.pop(); + }, + ), + ); + AnalyticsUtil.instance + .logEvent('logout_dialog_open'); + } + + /// Hide the current snack bar. + void hideSnackBar() { + _scaffoldMessengerKey.currentState + ?.hideCurrentSnackBar(); + } + + /// Show a short hint snack bar. + void showBasicHint({required String text}) { + showSnackBar( + text: text, + duration: const Duration(seconds: 2), + ); + } + + /// Show a snack bar with optional action. + ScaffoldFeatureController? + showSnackBar({ + required String text, + String? actionText, + Function()? onSnackBarTapped, + Duration? duration, + }) { + return _scaffoldMessengerKey.currentState + ?.showSnackBar( + SnackBar( + content: Text(text), + duration: duration ?? const Duration(days: 1), + action: actionText == null + ? null + : SnackBarAction( + onPressed: onSnackBarTapped!, + label: actionText, + ), + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart new file mode 100644 index 00000000..ea371cc1 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart @@ -0,0 +1,89 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; +import 'package:photo_view/photo_view.dart'; + +/// A glass-enhanced version of [ImageViewerScaffold]. +/// +/// Replaces the [AppBar] with [GlassAppBar]. Uses the standard +/// [TabBar] for multi-image navigation since [GlassTabBar] +/// requires [GlassTab] items and index-based selection rather +/// than a [TabController]. +class GlassImageViewerScaffold extends StatefulWidget { + const GlassImageViewerScaffold({ + super.key, + this.title, + required this.imageViewers, + this.actions, + }); + + final String? title; + final List imageViewers; + final List? actions; + + @override + GlassImageViewerScaffoldState createState() => + GlassImageViewerScaffoldState(); +} + +class GlassImageViewerScaffoldState + extends State + with SingleTickerProviderStateMixin { + TabController? _tabController; + + @override + void initState() { + _tabController = TabController( + vsync: this, + length: widget.imageViewers.length, + ); + super.initState(); + } + + @override + void dispose() { + _tabController!.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBodyBehindAppBar: true, + appBar: GlassAppBar( + title: Text(widget.title ?? ''), + actions: widget.actions, + ), + body: Column( + children: [ + SizedBox( + height: MediaQuery.of(context).padding.top + + 44, + ), + if (widget.imageViewers.length > 1) + TabBar( + controller: _tabController, + tabs: [ + for (final ImageViewer image + in widget.imageViewers) + Tab(text: image.title), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + for (final ImageViewer image + in widget.imageViewers) + PhotoView( + imageProvider: + AssetImage(image.assetName), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart new file mode 100644 index 00000000..5e9f7fc6 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart @@ -0,0 +1,163 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [LoginScaffold]. +/// +/// Replaces the gradient background with a translucent glass +/// surface and the form card with [GlassCard]. +class GlassLoginScaffold extends StatefulWidget { + const GlassLoginScaffold({ + super.key, + required this.logoSource, + required this.forms, + this.logoMode = LogoMode.text, + }); + + final LogoMode logoMode; + final String logoSource; + final List forms; + + static const String routerName = '/login'; + + @override + GlassLoginScaffoldState createState() => + GlassLoginScaffoldState(); +} + +class GlassLoginScaffoldState + extends State { + bool get isTablet => + MediaQuery.of(context).size.shortestSide >= 600; + + @override + Widget build(BuildContext context) { + final Orientation orientation = + MediaQuery.of(context).orientation; + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final bool isDark = + Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + resizeToAvoidBottomInset: + orientation == Orientation.portrait, + body: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: isDark + ? [ + colorScheme.surface, + colorScheme.surfaceContainerLowest, + ] + : [ + colorScheme.primary, + colorScheme.primaryContainer, + ], + ), + ), + child: AutofillGroup( + child: KeyboardDismissOnTap( + child: SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 32.0, + ), + child: orientation == + Orientation.portrait + ? Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + mainAxisAlignment: + MainAxisAlignment.center, + children: _renderContent( + orientation, + colorScheme, + isDark, + ), + ) + : Row( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: _renderContent( + orientation, + colorScheme, + isDark, + ), + ), + ), + ), + ), + ), + ), + ), + ); + } + + Widget _logo(ColorScheme colorScheme, bool isDark) { + switch (widget.logoMode) { + case LogoMode.image: + return GlassCard( + child: SizedBox( + width: 100, + height: 100, + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: Image.asset( + widget.logoSource, + fit: BoxFit.contain, + ), + ), + ), + ); + case LogoMode.text: + return TextLogo( + text: widget.logoSource, + isDark: isDark, + colorScheme: colorScheme, + ); + } + } + + List _renderContent( + Orientation orientation, + ColorScheme colorScheme, + bool isDark, + ) { + final Widget logoWidget = + _logo(colorScheme, isDark); + final Widget formCard = GlassCard( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: widget.forms, + ), + ), + ), + ); + + if (orientation == Orientation.portrait) { + return [ + Center(child: logoWidget), + const SizedBox(height: 48), + formCard, + ]; + } else { + return [ + Expanded(child: Center(child: logoWidget)), + const SizedBox(width: 32), + Expanded(child: Center(child: formCard)), + ]; + } + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart new file mode 100644 index 00000000..c68c5058 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart @@ -0,0 +1,112 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; + +/// A glass-enhanced version of [ScoreScaffold]. +/// +/// Currently delegates to the original [ScoreScaffold] while the +/// app tree is wrapped in [LiquidGlassWidgets.wrap] via +/// [LiquidGlassApApp]. The glass theme bridge ensures glass widgets +/// used within the scaffold pick up the correct tint colors. +/// +/// Score scaffold has deeply integrated state management (analysis +/// views, scroll-aware FABs, responsive layouts) that makes partial +/// widget replacement impractical. Instead, this wrapper ensures the +/// scaffold operates correctly within a Liquid Glass context. +class GlassScoreScaffold extends StatelessWidget { + const GlassScoreScaffold({ + super.key, + required this.state, + required this.scoreData, + required this.onRefresh, + this.title, + this.itemPicker, + this.semesterData, + this.onSelect, + this.onSearchButtonClick, + this.middleTitle, + this.finalTitle, + this.onScoreSelect, + this.middleScoreBuilder, + this.finalScoreBuilder, + this.customHint, + this.isShowSearchButton = false, + this.bottom, + this.customStateHint, + }); + + /// Creates from a [DataState]. + GlassScoreScaffold.fromDataState({ + super.key, + required DataState dataState, + required this.onRefresh, + this.title, + this.itemPicker, + this.semesterData, + this.onSelect, + this.onSearchButtonClick, + this.middleTitle, + this.finalTitle, + this.onScoreSelect, + this.middleScoreBuilder, + this.finalScoreBuilder, + this.isShowSearchButton = false, + this.bottom, + }) : state = dataState.when( + loading: () => ScoreState.loading, + loaded: (_, __) => ScoreState.finish, + error: (_) => ScoreState.error, + empty: (_) => ScoreState.empty, + ), + scoreData = dataState.dataOrNull, + customHint = + dataState is DataLoaded + ? dataState.hint + : null, + customStateHint = + dataState is DataError + ? dataState.hint + : dataState is DataEmpty + ? dataState.hint + : null; + + final ScoreState state; + final String? customStateHint; + final String? title; + final ScoreData? scoreData; + final SemesterData? semesterData; + final Function(int index)? onSelect; + final Function()? onSearchButtonClick; + final Function()? onRefresh; + final Widget? itemPicker; + final String? middleTitle; + final String? finalTitle; + final Function(int index)? onScoreSelect; + final Widget Function(int index)? middleScoreBuilder; + final Widget Function(int index)? finalScoreBuilder; + final bool isShowSearchButton; + final String? customHint; + final Widget? bottom; + + @override + Widget build(BuildContext context) { + return ScoreScaffold( + state: state, + scoreData: scoreData, + onRefresh: onRefresh, + title: title, + itemPicker: itemPicker, + semesterData: semesterData, + onSelect: onSelect, + onSearchButtonClick: onSearchButtonClick, + middleTitle: middleTitle, + finalTitle: finalTitle, + onScoreSelect: onScoreSelect, + middleScoreBuilder: middleScoreBuilder, + finalScoreBuilder: finalScoreBuilder, + customHint: customHint, + isShowSearchButton: isShowSearchButton, + bottom: bottom, + customStateHint: customStateHint, + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart new file mode 100644 index 00000000..2504993b --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart @@ -0,0 +1,355 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:barcode_widget/barcode_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [UserInfoScaffold]. +/// +/// Replaces the [SliverAppBar] gradient header with a +/// [GlassPanel] overlay and wraps info cards with [GlassCard]. +class GlassUserInfoScaffold extends StatefulWidget { + const GlassUserInfoScaffold({ + super.key, + required this.userInfo, + this.heroTag, + this.actions, + this.onRefresh, + this.enableBarCode = false, + }); + + final UserInfo userInfo; + final String? heroTag; + final List? actions; + final Future Function()? onRefresh; + final bool enableBarCode; + + @override + GlassUserInfoScaffoldState createState() => + GlassUserInfoScaffoldState(); +} + +class GlassUserInfoScaffoldState + extends State { + BarCodeMode codeMode = BarCodeMode.qrCode; + bool _isRefreshing = false; + + String get iconName { + switch (codeMode) { + case BarCodeMode.code39: + return ApImageIcons.qrcode; + case BarCodeMode.qrCode: + return ApImageIcons.barcode; + } + } + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final bool isDark = + Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + extendBodyBehindAppBar: true, + appBar: GlassAppBar( + actions: [ + ...widget.actions ?? [], + if (widget.enableBarCode) + IconButton( + icon: Image.asset( + iconName, + height: 24.0, + width: 24.0, + ), + onPressed: () { + setState( + () => codeMode = BarCodeMode.values[ + (codeMode.index + 1) % + BarCodeMode.values.length], + ); + AnalyticsUtil.instance + .logEvent('user_info_barcode_switch'); + }, + ), + IconButton( + icon: _isRefreshing + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: isDark + ? colorScheme.onSurface + : colorScheme.onPrimary, + ), + ) + : const Icon(Icons.refresh_rounded), + onPressed: + _isRefreshing ? null : _handleRefresh, + ), + ], + ), + body: RefreshIndicator( + onRefresh: () async { + await _handleRefresh(); + AnalyticsUtil.instance + .logEvent('user_info_refresh'); + }, + child: SingleChildScrollView( + physics: + const AlwaysScrollableScrollPhysics(), + child: Column( + children: [ + _buildGlassHeader( + colorScheme, + isDark, + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildInfoCard(colorScheme), + if (widget.enableBarCode) ...[ + const SizedBox(height: 16), + _buildBarcodeCard(colorScheme), + ], + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildGlassHeader( + ColorScheme colorScheme, + bool isDark, + ) { + final bool hasImage = + widget.userInfo.pictureBytes != null && + widget.userInfo.pictureBytes!.isNotEmpty; + return GlassPanel( + child: Container( + width: double.infinity, + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 56, + bottom: 24, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: isDark + ? [ + colorScheme.primaryContainer + .withAlpha(128), + colorScheme.surface.withAlpha(128), + ] + : [ + colorScheme.primary.withAlpha(128), + colorScheme.primaryContainer + .withAlpha(128), + ], + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: isDark + ? colorScheme.primary + : colorScheme.onPrimary + .withAlpha(128), + width: 4, + ), + ), + child: CircleAvatar( + radius: 48, + backgroundColor: isDark + ? colorScheme.primaryContainer + : colorScheme.onPrimary + .withAlpha(51), + backgroundImage: hasImage + ? MemoryImage( + widget.userInfo.pictureBytes!, + ) + : null, + child: hasImage + ? null + : Icon( + Icons.person_rounded, + size: 56, + color: isDark + ? colorScheme.primary + : colorScheme.onPrimary, + ), + ), + ), + const SizedBox(height: 12), + Text( + widget.userInfo.name ?? '', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: isDark + ? colorScheme.onSurface + : colorScheme.onPrimary, + ), + ), + ], + ), + ), + ); + } + + Widget _buildInfoCard(ColorScheme colorScheme) { + return GlassCard( + child: Column( + children: [ + InfoRow( + icon: Icons.badge_outlined, + title: context.ap.studentId, + value: widget.userInfo.id, + ), + _buildDivider(colorScheme), + if (widget.userInfo.educationSystem != + null) ...[ + InfoRow( + icon: Icons.school_outlined, + title: context.ap.educationSystem, + value: + widget.userInfo.educationSystem!, + ), + _buildDivider(colorScheme), + ], + if (widget.userInfo.email != + null) ...[ + InfoRow( + icon: Icons.email_outlined, + title: context.ap.email, + value: widget.userInfo.email!, + ), + _buildDivider(colorScheme), + ], + InfoRow( + icon: Icons.domain_outlined, + title: context.ap.department, + value: widget.userInfo.department ?? '', + ), + _buildDivider(colorScheme), + InfoRow( + icon: Icons.class_outlined, + title: context.ap.studentClass, + value: widget.userInfo.className ?? '', + ), + ], + ), + ); + } + + Widget _buildBarcodeCard(ColorScheme colorScheme) { + return GlassCard( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.badge_outlined, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + '學號條碼', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ], + ), + const SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: 16, + ), + decoration: BoxDecoration( + color: colorScheme + .surfaceContainerHighest, + borderRadius: + BorderRadius.circular(12), + ), + child: Column( + children: [ + Text( + widget.userInfo.id, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + letterSpacing: 4, + fontFamily: 'monospace', + ), + ), + const SizedBox(height: 12), + BarcodeWidget( + barcode: + codeMode == BarCodeMode.code39 + ? Barcode.code39() + : Barcode.qrCode(), + data: widget.userInfo.id, + color: colorScheme.onSurface, + height: + codeMode == BarCodeMode.code39 + ? 80 + : 160, + width: + codeMode == BarCodeMode.code39 + ? double.infinity + : 160, + ), + const SizedBox(height: 12), + Text( + '可持本條碼於圖書館借書', + style: TextStyle( + fontSize: 12, + color: colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildDivider(ColorScheme colorScheme) { + return Divider( + height: 1, + indent: 72, + color: + colorScheme.outlineVariant.withAlpha(77), + ); + } + + Future _handleRefresh() async { + if (widget.onRefresh == null) return; + setState(() => _isRefreshing = true); + await widget.onRefresh!(); + if (mounted) { + setState(() => _isRefreshing = false); + } + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_ap_drawer.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_ap_drawer.dart new file mode 100644 index 00000000..4533c12c --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_ap_drawer.dart @@ -0,0 +1,194 @@ +import 'package:ap_common_flutter_core/ap_common_flutter_core.dart'; +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [ApDrawer]. +/// +/// Uses [GlassPanel] for the header section while preserving +/// the original menu item structure. Wrapped in a standard +/// [Drawer] since [GlassSideBar] expects navigation items +/// rather than arbitrary widget lists. +class GlassApDrawer extends StatelessWidget { + const GlassApDrawer({ + super.key, + required this.onTapHeader, + required this.widgets, + this.userInfo, + this.imageAsset, + this.imageHeroTag = ApConstants.tagStudentPicture, + this.displayPicture = false, + }); + + final UserInfo? userInfo; + final VoidCallback onTapHeader; + final String? imageAsset; + final List widgets; + final String imageHeroTag; + final bool displayPicture; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final bool isDark = + Theme.of(context).brightness == Brightness.dark; + + return Drawer( + backgroundColor: const Color(0x00000000), + elevation: 0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.horizontal( + right: Radius.circular(20), + ), + ), + child: Material( + color: colorScheme.surface, + borderRadius: const BorderRadius.horizontal( + right: Radius.circular(20), + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + _buildHeader( + context, + colorScheme, + isDark, + ), + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + children: widgets, + ), + ), + ], + ), + ), + ); + } + + Widget _buildHeader( + BuildContext context, + ColorScheme colorScheme, + bool isDark, + ) { + return GlassPanel( + padding: EdgeInsets.zero, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDark + ? [ + colorScheme.primaryContainer + .withAlpha(128), + colorScheme.surface.withAlpha(128), + ] + : [ + colorScheme.primary.withAlpha(128), + colorScheme.primaryContainer + .withAlpha(128), + ], + ), + ), + child: SafeArea( + bottom: false, + child: InkWell( + onTap: onTapHeader, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 24, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + _buildAvatar( + context, + colorScheme, + isDark, + ), + const SizedBox(height: 12), + Text( + userInfo?.name ?? '', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isDark + ? colorScheme.onSurface + : colorScheme.onPrimary, + ), + ), + if (userInfo?.id.isNotEmpty ?? + false) + Text( + userInfo!.id, + style: TextStyle( + fontSize: 14, + color: isDark + ? colorScheme + .onSurfaceVariant + : colorScheme.onPrimary + .withAlpha(204), + ), + ), + if (userInfo?.department != null) + Text( + userInfo!.department!, + style: TextStyle( + fontSize: 13, + color: isDark + ? colorScheme + .onSurfaceVariant + : colorScheme.onPrimary + .withAlpha(179), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildAvatar( + BuildContext context, + ColorScheme colorScheme, + bool isDark, + ) { + final bool hasImage = + displayPicture && + userInfo?.pictureBytes != null && + userInfo!.pictureBytes!.isNotEmpty; + + return Hero( + tag: imageHeroTag, + child: CircleAvatar( + radius: 36, + backgroundColor: isDark + ? colorScheme.primaryContainer + : colorScheme.onPrimary.withAlpha(51), + backgroundImage: hasImage + ? MemoryImage(userInfo!.pictureBytes!) + : null, + child: hasImage + ? null + : Icon( + Icons.person_rounded, + size: 40, + color: isDark + ? colorScheme.primary + : colorScheme.onPrimary, + ), + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_dialog.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_dialog.dart new file mode 100644 index 00000000..23fb6920 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_dialog.dart @@ -0,0 +1,76 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// Shows a [GlassDialog] with a single action button. +/// +/// This is the glass-enhanced equivalent of [DefaultDialog]. +Future showGlassDefaultDialog({ + required BuildContext context, + required String title, + required Widget contentWidget, + required String actionText, + required VoidCallback actionFunction, +}) { + return GlassDialog.show( + context: context, + title: title, + content: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: + MediaQuery.of(context).size.height * 0.5, + ), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 8.0, + ), + child: contentWidget, + ), + ), + actions: [ + GlassDialogAction( + label: actionText, + onPressed: actionFunction, + ), + ], + ); +} + +/// Shows a [GlassDialog] with two action buttons (yes/no). +/// +/// This is the glass-enhanced equivalent of [YesNoDialog]. +Future showGlassYesNoDialog({ + required BuildContext context, + String? title, + Widget? contentWidget, + String? leftActionText, + String? rightActionText, + VoidCallback? leftActionFunction, + VoidCallback? rightActionFunction, +}) { + final ApLocalizations ap = context.ap; + return GlassDialog.show( + context: context, + title: title, + content: contentWidget, + actions: [ + GlassDialogAction( + label: leftActionText ?? ap.confirm, + onPressed: () { + Navigator.of(context, rootNavigator: true) + .pop(); + leftActionFunction?.call(); + }, + ), + GlassDialogAction( + label: rightActionText ?? ap.cancel, + onPressed: () { + Navigator.of(context, rootNavigator: true) + .pop(); + rightActionFunction?.call(); + }, + ), + ], + ); +} diff --git a/packages/ap_common_liquid_glass/pubspec.yaml b/packages/ap_common_liquid_glass/pubspec.yaml new file mode 100644 index 00000000..a276c5d4 --- /dev/null +++ b/packages/ap_common_liquid_glass/pubspec.yaml @@ -0,0 +1,27 @@ +name: ap_common_liquid_glass +description: > + Liquid Glass design language for AP series apps. Provides glass-effect + scaffold variants and widgets powered by liquid_glass_widgets. +version: 0.1.0-dev.1 +homepage: https://github.com/abc873693/ap_common +repository: https://github.com/abc873693/ap_common/tree/master/packages/ap_common_liquid_glass + +environment: + sdk: ^3.9.0 + flutter: ">=3.24.0 <4.0.0" + +resolution: workspace + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + ap_common_flutter_ui: ^1.1.0-dev.6 + liquid_glass_widgets: ^0.7.0 + barcode_widget: ^2.0.1 + photo_view: ^0.15.0 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 399c50d2..d35f33e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ workspace: - packages/ap_common_announcement_ui - packages/ap_common_plugin - packages/ap_common_firebase + - packages/ap_common_liquid_glass - apps/example dev_dependencies: From 2c7b184789cc8e1ab8523d26f67d7686f1e29a40 Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Sun, 5 Apr 2026 11:05:28 +0800 Subject: [PATCH 02/14] docs(liquid_glass): add implementation spec for continuing development --- .../LIQUID_GLASS_SPEC.md | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 packages/ap_common_liquid_glass/LIQUID_GLASS_SPEC.md diff --git a/packages/ap_common_liquid_glass/LIQUID_GLASS_SPEC.md b/packages/ap_common_liquid_glass/LIQUID_GLASS_SPEC.md new file mode 100644 index 00000000..3908937f --- /dev/null +++ b/packages/ap_common_liquid_glass/LIQUID_GLASS_SPEC.md @@ -0,0 +1,156 @@ +# Liquid Glass Integration Spec + +> This file captures the design decisions and implementation status from the +> initial planning session. Read this before continuing development. + +## Architecture Decision + +- **New package**: `packages/ap_common_liquid_glass/` (opt-in, not integrated into `ap_common_flutter_ui`) +- **Dependency**: `liquid_glass_widgets: ^0.7.0` (pre-1.0, API may change) +- **Scope**: All 6 scaffolds + drawer + dialogs + 3 convenience pages + +## Dependency Chain + +``` +ap_common_flutter_ui (Material 3, no glass dependency) + └─ ap_common_liquid_glass (opt-in glass layer) + └─ liquid_glass_widgets ^0.7.0 +``` + +## Current Implementation Status + +### Done (initial scaffold, needs `pub get` + `analyze` to verify) + +| File | Status | Notes | +|------|--------|-------| +| `pubspec.yaml` | Done | Registered in root workspace | +| `lib/ap_common_liquid_glass.dart` | Done | Barrel export | +| `lib/src/liquid_glass_app.dart` | Done | `LiquidGlassApApp` wrapping `MaterialApp` + `LiquidGlassWidgets.wrap()` + `GlassTheme` | +| `lib/src/glass_theme_bridge.dart` | Done | `GlassThemeBridge.fromContext()` — currently returns default `GlassThemeData`, needs seed color mapping | +| `lib/src/scaffold/glass_home_page_scaffold.dart` | Done | `GlassAppBar`, keeps Material `NavigationBar` (TODO: swap to `GlassBottomBar`) | +| `lib/src/scaffold/glass_login_scaffold.dart` | Done | Gradient background kept, form card → `GlassCard`, logo image → `GlassCard` | +| `lib/src/scaffold/glass_course_scaffold.dart` | Done | Delegates to original `CourseScaffold` (complex internal state) | +| `lib/src/scaffold/glass_score_scaffold.dart` | Done | Delegates to original `ScoreScaffold` | +| `lib/src/scaffold/glass_user_info_scaffold.dart` | Done | Needs review — SliverAppBar has no direct glass equivalent | +| `lib/src/scaffold/glass_image_viewer_scaffold.dart` | Done | `GlassAppBar` + `GlassTabBar` | +| `lib/src/widgets/glass_ap_drawer.dart` | Done | Needs review | +| `lib/src/widgets/glass_dialog.dart` | Done | `GlassYesNoDialog`, `GlassDefaultDialog` | +| `lib/src/pages/glass_ap_login_page.dart` | Done | Wraps `GlassLoginScaffold` | +| `lib/src/pages/glass_ap_course_page.dart` | Done | Wraps `GlassCourseScaffold` | +| `lib/src/pages/glass_ap_score_page.dart` | Done | Wraps `GlassScoreScaffold` | + +### TODO (next steps) + +1. **`flutter pub get` + `dart analyze`** — fix all compile errors against real `liquid_glass_widgets` API +2. **GlassThemeBridge** — map `ApTheme.seedColor` to glass tint color (currently returns defaults) +3. **GlassBottomBar** — `GlassHomePageScaffold` still uses Material `NavigationBar`; need to map `List bottomNavigationBarItems` to `GlassBottomBar` tabs +4. **CourseScaffold / ScoreScaffold deep integration** — currently just delegates to original; ideally: + - Extract body builder from original scaffolds + - Replace AppBar, FAB, card surfaces with glass equivalents +5. **UserInfoScaffold** — `SliverAppBar` with gradient has no glass equivalent; design custom glass header +6. **Example App** — add glass demo page in `apps/example/` +7. **Widget tests** — at least smoke test each glass scaffold renders without errors +8. **`AboutUsPage` glass variant** — not yet created + +## Key Design Decisions + +### Composition over Duplication +- Glass scaffolds should **reuse** original scaffold logic, not copy-paste +- Replace only the "chrome" (AppBar, NavigationBar, FAB, Card surfaces) +- Keep all state management, DataState patterns, responsive layouts + +### API Compatibility +- Glass scaffolds accept the **same parameters** as originals +- Downstream apps swap `HomePageScaffold(...)` → `GlassHomePageScaffold(...)` with no other changes +- `LiquidGlassApApp` has the same API as `ApApp` + `quality` and `glassSettings` params + +### Initialization +```dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await LiquidGlassWidgets.initialize(); // pre-cache shaders + registerOneForAll(); + registerApCommonService(); + await (PreferenceUtil.instance as ApPreferenceUtil).init(key: key, iv: iv); + runApp(const MyApp()); +} + +// Use LiquidGlassApApp instead of ApApp +LiquidGlassApApp( + home: GlassHomePage(), + quality: GlassQuality.standard, // or .premium for Impeller +) +``` + +## Widget Mapping Reference + +| liquid_glass_widgets | Replaces in ap_common | Where Used | +|---------------------|----------------------|------------| +| `GlassAppBar` | `AppBar` | All scaffolds | +| `GlassBottomBar` | `NavigationBar` | HomePageScaffold | +| `GlassCard` | Card surfaces, form containers | LoginScaffold, settings | +| `GlassButton` | `FloatingActionButton`, `ApButton` | Course/Score FABs | +| `GlassTextField` | `ApTextField` | Login form | +| `GlassDialog` | `DefaultDialog`, `YesNoDialog` | Dialogs | +| `GlassSideBar` | `Drawer` in `ApDrawer` | Drawer | +| `GlassTabBar` | `TabBar` | ImageViewerScaffold | +| `GlassSearchBar` | Search in CourseScaffold | Course search | +| `GlassListTile` | Course/Score list items | List views | +| `GlassSheet` | Bottom sheets | Various | +| `GlassSnackBar` | SnackBar | Hints/notifications | +| `GlassTheme` + `GlassThemeData` | — | App-level via `GlassThemeBridge` | + +## Original Scaffold Structures (for reference) + +### HomePageScaffold +- `AppBar` (title + actions) → `GlassAppBar` +- `NavigationBar` (elevation 12, height 56) → `GlassBottomBar` +- Announcement carousel with auto-play `PageView` +- Responsive: tablet (680px) shows drawer inline +- State: `HomeState {loading, finish, error, empty, offline}` + +### LoginScaffold +- Gradient `DecoratedBox` background (primary→primaryContainer / surface→surfaceContainerLowest) +- Form card with 24dp radius, shadow +- Logo: text or image mode +- Responsive: portrait=column, landscape=row + +### CourseScaffold (~1700 lines) +- `AppBar` + `SemesterPicker` + settings gear +- List tab + Table tab (course color-coded) +- Screenshot capture with `RepaintBoundary` +- `CourseConfig` InheritedWidget for display settings +- 12 predefined course colors +- Complex state: `CourseState {loading, finish, error, empty, offlineEmpty, custom}` + +### ScoreScaffold (~925 lines) +- `AppBar` + `SemesterPicker` +- Score list + Analysis view toggle +- Score color by range (90+ green, 80+ light green, 70+ blue, 60+ orange, <60 red) +- Analysis: ScorePRCard, ScoreStatisticsCard, ScoreDistributionCard, ScoreCreditSummaryCard + +### UserInfoScaffold +- `SliverAppBar` (expandedHeight 200, gradient FlexibleSpaceBar) +- Avatar with Hero animation +- QR code / Code39 barcode toggle +- `CustomScrollView` with pinned app bar + +### ImageViewerScaffold +- `AppBar` + optional actions +- `TabBar` (if multiple images) +- `TabBarView` with `PhotoView` galleries + +## Performance Notes + +- `GlassQuality.standard` — default, works on all platforms +- `GlassQuality.premium` — Impeller only, for static surfaces +- `LiquidGlassWidgets.initialize()` pre-warms shaders (call in `main()`) +- Library respects system Reduce Motion / Reduce Transparency (WCAG) + +## Code Style Reminders + +- `always_specify_types: true` +- `prefer_single_quotes: true` +- 80 char line limit +- Constructors first in class +- Conventional Commits: `feat(liquid_glass):`, `fix(liquid_glass):`, etc. From be49240eb04ef54d771472c90da2cb5fa755dd0b Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Sun, 5 Apr 2026 11:35:01 +0800 Subject: [PATCH 03/14] feat(liquid_glass): add Liquid Glass example app and preview workflow Add apps/example_liquid_glass/ with GlassApLoginPage, GlassApCoursePage, and GlassApScorePage demos. Add liquid_glass_preview job to the example-pull-request-preview workflow for independent Firebase deploy. --- .../example-pull-request-preview.yml | 32 +++++- .../analysis_options.yaml | 1 + apps/example_liquid_glass/assets | 1 + apps/example_liquid_glass/firebase.json | 10 ++ apps/example_liquid_glass/lib/app.dart | 17 +++ apps/example_liquid_glass/lib/main.dart | 18 ++++ .../lib/pages/course_page.dart | 26 +++++ .../lib/pages/home_page.dart | 98 ++++++++++++++++++ .../lib/pages/login_page.dart | 21 ++++ .../lib/pages/score_page.dart | 33 ++++++ apps/example_liquid_glass/lib/res/assets.dart | 13 +++ apps/example_liquid_glass/pubspec.yaml | 35 +++++++ apps/example_liquid_glass/web/favicon.ico | Bin 0 -> 1150 bytes .../web/icons/Icon-192.png | Bin 0 -> 5916 bytes .../web/icons/Icon-512.png | Bin 0 -> 11648 bytes .../web/icons/Icon-maskable-192.png | Bin 0 -> 5916 bytes .../web/icons/Icon-maskable-512.png | Bin 0 -> 11648 bytes apps/example_liquid_glass/web/index.html | 47 +++++++++ apps/example_liquid_glass/web/manifest.json | 35 +++++++ pubspec.lock | 24 +++++ pubspec.yaml | 1 + 21 files changed, 410 insertions(+), 2 deletions(-) create mode 100644 apps/example_liquid_glass/analysis_options.yaml create mode 120000 apps/example_liquid_glass/assets create mode 100644 apps/example_liquid_glass/firebase.json create mode 100644 apps/example_liquid_glass/lib/app.dart create mode 100644 apps/example_liquid_glass/lib/main.dart create mode 100644 apps/example_liquid_glass/lib/pages/course_page.dart create mode 100644 apps/example_liquid_glass/lib/pages/home_page.dart create mode 100644 apps/example_liquid_glass/lib/pages/login_page.dart create mode 100644 apps/example_liquid_glass/lib/pages/score_page.dart create mode 100644 apps/example_liquid_glass/lib/res/assets.dart create mode 100644 apps/example_liquid_glass/pubspec.yaml create mode 100644 apps/example_liquid_glass/web/favicon.ico create mode 100644 apps/example_liquid_glass/web/icons/Icon-192.png create mode 100644 apps/example_liquid_glass/web/icons/Icon-512.png create mode 100644 apps/example_liquid_glass/web/icons/Icon-maskable-192.png create mode 100644 apps/example_liquid_glass/web/icons/Icon-maskable-512.png create mode 100644 apps/example_liquid_glass/web/index.html create mode 100644 apps/example_liquid_glass/web/manifest.json diff --git a/.github/workflows/example-pull-request-preview.yml b/.github/workflows/example-pull-request-preview.yml index 7d4bb222..6aa9e05c 100644 --- a/.github/workflows/example-pull-request-preview.yml +++ b/.github/workflows/example-pull-request-preview.yml @@ -3,6 +3,7 @@ on: pull_request: paths: - "apps/example/**" + - "apps/example_liquid_glass/**" - "packages/**" jobs: example_preview: @@ -17,16 +18,43 @@ jobs: channel: 'stable' cache: true cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' - - name: Run Build + - name: Build Example working-directory: ./apps/example run: | flutter pub get flutter build web --release - - uses: FirebaseExtended/action-hosting-deploy@v0 + - name: Deploy Example Preview + uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: '${{ secrets.GITHUB_TOKEN }}' firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_AP_COMMON }}' projectId: ap-common entryPoint: "apps/example" channelId: "example-preview" + expires: 7d + liquid_glass_preview: + if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.38.x' + channel: 'stable' + cache: true + cache-key: 'flutter-:os:-:channel:-:version:-:arch:-:hash:' + - name: Build Liquid Glass Example + working-directory: ./apps/example_liquid_glass + run: | + flutter pub get + flutter build web --release + - name: Deploy Liquid Glass Preview + uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_AP_COMMON }}' + projectId: ap-common + entryPoint: "apps/example_liquid_glass" + channelId: "liquid-glass-preview" expires: 7d \ No newline at end of file diff --git a/apps/example_liquid_glass/analysis_options.yaml b/apps/example_liquid_glass/analysis_options.yaml new file mode 100644 index 00000000..a3008fc9 --- /dev/null +++ b/apps/example_liquid_glass/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lint/strict.yaml diff --git a/apps/example_liquid_glass/assets b/apps/example_liquid_glass/assets new file mode 120000 index 00000000..6f2de55f --- /dev/null +++ b/apps/example_liquid_glass/assets @@ -0,0 +1 @@ +../example/assets \ No newline at end of file diff --git a/apps/example_liquid_glass/firebase.json b/apps/example_liquid_glass/firebase.json new file mode 100644 index 00000000..0d25a779 --- /dev/null +++ b/apps/example_liquid_glass/firebase.json @@ -0,0 +1,10 @@ +{ + "hosting": { + "public": "build/web", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ] + } +} diff --git a/apps/example_liquid_glass/lib/app.dart b/apps/example_liquid_glass/lib/app.dart new file mode 100644 index 00000000..9a9a6c6b --- /dev/null +++ b/apps/example_liquid_glass/lib/app.dart @@ -0,0 +1,17 @@ +import 'package:ap_common_example_liquid_glass/pages/home_page.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:flutter/material.dart'; + +class LiquidGlassExampleApp extends StatelessWidget { + const LiquidGlassExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return TranslationProvider( + child: LiquidGlassApApp( + home: const GlassHomePage(), + onGenerateTitle: (_) => 'Liquid Glass Example', + ), + ); + } +} diff --git a/apps/example_liquid_glass/lib/main.dart b/apps/example_liquid_glass/lib/main.dart new file mode 100644 index 00000000..a5b246aa --- /dev/null +++ b/apps/example_liquid_glass/lib/main.dart @@ -0,0 +1,18 @@ +import 'package:ap_common/ap_common.dart' + hide AppLocaleUtils, TranslationProvider; +import 'package:ap_common_example_liquid_glass/app.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; +import 'package:flutter/material.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await LiquidGlassWidgets.initialize(); + registerOneForAll(); + registerApCommonService(); + await (PreferenceUtil.instance as ApPreferenceUtil).init( + key: encrypt.Key.fromUtf8('l9r1W3wcsnJTayxCXwoFt62w1i4sQ5J9'), + iv: encrypt.IV.fromUtf8('auc9OV5r0nLwjCAH'), + ); + runApp(const LiquidGlassExampleApp()); +} diff --git a/apps/example_liquid_glass/lib/pages/course_page.dart b/apps/example_liquid_glass/lib/pages/course_page.dart new file mode 100644 index 00000000..aefe9396 --- /dev/null +++ b/apps/example_liquid_glass/lib/pages/course_page.dart @@ -0,0 +1,26 @@ +import 'package:ap_common_example_liquid_glass/res/assets.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Course page using [GlassApCoursePage]. +class GlassCoursePage extends StatelessWidget { + const GlassCoursePage({super.key}); + + @override + Widget build(BuildContext context) { + return GlassApCoursePage( + onLoadSemesters: () async { + final String rawString = + await rootBundle.loadString(FileAssets.semesters); + return SemesterData.fromRawJson(rawString); + }, + onLoadCourse: (Semester semester) async { + final String rawString = + await rootBundle.loadString(FileAssets.courses); + return CourseData.fromRawJson(rawString); + }, + enableCaptureCourseTable: true, + ); + } +} diff --git a/apps/example_liquid_glass/lib/pages/home_page.dart b/apps/example_liquid_glass/lib/pages/home_page.dart new file mode 100644 index 00000000..c78d8d75 --- /dev/null +++ b/apps/example_liquid_glass/lib/pages/home_page.dart @@ -0,0 +1,98 @@ +import 'package:ap_common_example_liquid_glass/pages/course_page.dart'; +import 'package:ap_common_example_liquid_glass/pages/login_page.dart'; +import 'package:ap_common_example_liquid_glass/pages/score_page.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:flutter/material.dart'; + +/// Home page for the Liquid Glass example. +class GlassHomePage extends StatelessWidget { + const GlassHomePage({super.key}); + + @override + Widget build(BuildContext context) { + final ApLocalizations ap = context.ap; + + return Scaffold( + appBar: GlassAppBar( + title: const Text('Liquid Glass Example'), + actions: [ + IconButton( + icon: const Icon(Icons.brightness_6), + onPressed: () { + final LiquidGlassApAppState state = + LiquidGlassApApp.of(context); + state.setThemeMode( + state.themeMode == ThemeMode.dark + ? ThemeMode.light + : ThemeMode.dark, + ); + }, + ), + ], + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildCard( + context, + icon: Icons.login, + title: ap.login, + subtitle: 'GlassApLoginPage', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const GlassLoginPage(), + ), + ), + ), + _buildCard( + context, + icon: Icons.calendar_today, + title: ap.course, + subtitle: 'GlassApCoursePage', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const GlassCoursePage(), + ), + ), + ), + _buildCard( + context, + icon: Icons.assignment, + title: ap.score, + subtitle: 'GlassApScorePage', + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const GlassScorePage(), + ), + ), + ), + ], + ), + ); + } + + Widget _buildCard( + BuildContext context, { + required IconData icon, + required String title, + required String subtitle, + required VoidCallback onTap, + }) { + return GlassCard( + margin: const EdgeInsets.only(bottom: 12), + child: ListTile( + leading: Icon(icon), + title: Text(title), + subtitle: Text( + subtitle, + style: const TextStyle(fontSize: 12), + ), + trailing: const Icon(Icons.chevron_right), + onTap: onTap, + ), + ); + } +} diff --git a/apps/example_liquid_glass/lib/pages/login_page.dart b/apps/example_liquid_glass/lib/pages/login_page.dart new file mode 100644 index 00000000..eb84a0c4 --- /dev/null +++ b/apps/example_liquid_glass/lib/pages/login_page.dart @@ -0,0 +1,21 @@ +import 'package:ap_common_example_liquid_glass/res/assets.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:flutter/material.dart'; + +/// Login page using [GlassApLoginPage]. +class GlassLoginPage extends StatelessWidget { + const GlassLoginPage({super.key}); + + @override + Widget build(BuildContext context) { + return GlassApLoginPage( + logoMode: LogoMode.image, + logoSource: ImageAssets.K, + onLogin: (String username, String password) async { + await Future.delayed(const Duration(seconds: 1)); + return true; + }, + enableOfflineLogin: true, + ); + } +} diff --git a/apps/example_liquid_glass/lib/pages/score_page.dart b/apps/example_liquid_glass/lib/pages/score_page.dart new file mode 100644 index 00000000..bbd52a69 --- /dev/null +++ b/apps/example_liquid_glass/lib/pages/score_page.dart @@ -0,0 +1,33 @@ +import 'package:ap_common_example_liquid_glass/res/assets.dart'; +import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Score page using [GlassApScorePage]. +class GlassScorePage extends StatelessWidget { + const GlassScorePage({super.key}); + + @override + Widget build(BuildContext context) { + final ApLocalizations ap = context.ap; + + return GlassApScorePage( + onLoadSemesters: () async { + final String rawString = + await rootBundle.loadString(FileAssets.semesters); + return SemesterData.fromRawJson(rawString); + }, + onLoadScore: (Semester semester) async { + final String rawString = + await rootBundle.loadString(FileAssets.scores); + return ScoreData.fromRawJson(rawString); + }, + detailBuilder: (ScoreData? scoreData) => [ + '${ap.conductScore}:${scoreData?.detail.conduct ?? ''}', + '${ap.average}:${scoreData?.detail.average ?? ''}', + '${ap.classRank}:${scoreData?.detail.classRank ?? ''}', + '${ap.departmentRank}:${scoreData?.detail.departmentRank ?? ''}', + ], + ); + } +} diff --git a/apps/example_liquid_glass/lib/res/assets.dart b/apps/example_liquid_glass/lib/res/assets.dart new file mode 100644 index 00000000..21d7a083 --- /dev/null +++ b/apps/example_liquid_glass/lib/res/assets.dart @@ -0,0 +1,13 @@ +class ImageAssets { + static const String basePath = 'assets/images'; + + static const String K = '$basePath/K.webp'; +} + +class FileAssets { + static const String basePath = 'assets'; + + static const String courses = '$basePath/coursetable.json'; + static const String scores = '$basePath/scores.json'; + static const String semesters = '$basePath/semesters.json'; +} diff --git a/apps/example_liquid_glass/pubspec.yaml b/apps/example_liquid_glass/pubspec.yaml new file mode 100644 index 00000000..f79c887f --- /dev/null +++ b/apps/example_liquid_glass/pubspec.yaml @@ -0,0 +1,35 @@ +name: ap_common_example_liquid_glass +description: Liquid Glass example for ap_common. + +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ^3.9.0 + +resolution: workspace + +dependencies: + ap_common: + path: ../../packages/ap_common + ap_common_liquid_glass: + path: ../../packages/ap_common_liquid_glass + encrypt: any + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + slang: ^4.14.0 + slang_flutter: ^4.14.0 + +dev_dependencies: + flutter_test: + sdk: flutter + lint: ^2.1.2 + +flutter: + uses-material-design: true + assets: + - assets/images/ + - assets/ diff --git a/apps/example_liquid_glass/web/favicon.ico b/apps/example_liquid_glass/web/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..58f3621a230616231dfa52b303e938d701debf3f GIT binary patch literal 1150 zcmbW1O-ox*5QdM?otw}_(R_IB(S`ZYLSsy@i%cvDLWbBCO{&pUJG%(+6;(a+-%^t8m>BO#s$ zA%bMIL@RoooDdXyT(8akPmkyA5^P;<67h&cg1lL)_8jX7H6hZ{NY;QhGMgoSC@xXR zN=&3AdY<0bV0O4$Vm{}`blQuqu-0In^RA3q_`aj?wWu%hl*91MzUq@W zDJd|!U%1Pf{k!qJHr6X~ylUC(!E6BW&~093Yw^iv5-)osz7=&%hjRfWTv#`rdo-2S z?9W{V%~G!``!#uuclM9>&-{nH&U>}3@IL25Na8%F`2wyhm)_KAJ)jxTz^^8bzE#(aB4(@ zK70QEHovU#f;E=)e4lSv81MBW8uZ`XGoE#Rr;u^KKdiz1$PY@)W-V}kxo)RDbbn2p NYh^vdn(x5>`ahZm!!ZB= literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/web/icons/Icon-192.png b/apps/example_liquid_glass/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..f3599c6a6f376d5fce00b8d592e9218930eae029 GIT binary patch literal 5916 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 w*31e1Ago-1$@e89!wsXdP&4o0U`w8WR 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 literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/web/icons/Icon-maskable-192.png b/apps/example_liquid_glass/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..f3599c6a6f376d5fce00b8d592e9218930eae029 GIT binary patch literal 5916 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 w*31e1Ago-1$@e89!wsXdP&4o0U`w8WR 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 literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/web/index.html b/apps/example_liquid_glass/web/index.html new file mode 100644 index 00000000..d074ceb7 --- /dev/null +++ b/apps/example_liquid_glass/web/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + 校務通 Liquid Glass + + + + + + + + + + \ No newline at end of file diff --git a/apps/example_liquid_glass/web/manifest.json b/apps/example_liquid_glass/web/manifest.json new file mode 100644 index 00000000..65b120f2 --- /dev/null +++ b/apps/example_liquid_glass/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "nkust_ap", + "short_name": "nkust_ap", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "史上最強高科大校務系統 APP", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/pubspec.lock b/pubspec.lock index d33de1cc..f3f786ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -393,6 +393,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" fake_async: dependency: transitive description: @@ -707,6 +715,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.34" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_test: dependency: transitive description: flutter @@ -1053,6 +1069,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + liquid_glass_widgets: + dependency: transitive + description: + name: liquid_glass_widgets + sha256: ac8ebf50943fb9dd4b71ea5e1945a3e77ea2015d87c27492703c45c7f8dc0cab + url: "https://pub.dev" + source: hosted + version: "0.7.0" logger: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d35f33e4..1e6b5c73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ workspace: - packages/ap_common_firebase - packages/ap_common_liquid_glass - apps/example + - apps/example_liquid_glass dev_dependencies: melos: ^7.5.0 From 0f33d7cdeb531dbc1ed10aed87ec2ac2a1addba9 Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Sun, 5 Apr 2026 12:08:47 +0800 Subject: [PATCH 04/14] fix(liquid_glass): fix duplicate GlobalKey crash in LiquidGlassApApp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move LiquidGlassWidgets.wrap() from build() to runApp() level, as BackdropGroup uses GlobalKey internally for GPU state - Remove external TranslationProvider wrapping — LiquidGlassApApp already includes one, and slang's singleton GlobalKeyHandler causes duplicate key crash when two providers of the same locale type exist in the widget tree - Add native platform projects (android, ios, macos, linux, windows) for example_liquid_glass --- apps/example_liquid_glass/.gitignore | 45 + apps/example_liquid_glass/.metadata | 42 + apps/example_liquid_glass/android/.gitignore | 14 + .../android/app/build.gradle.kts | 44 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 45 + .../MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + .../android/build.gradle.kts | 24 + .../android/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../android/settings.gradle.kts | 26 + apps/example_liquid_glass/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 26 + .../ios/Flutter/Debug.xcconfig | 2 + .../ios/Flutter/Release.xcconfig | 2 + .../ios/Runner.xcodeproj/project.pbxproj | 616 +++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 101 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + .../ios/Runner/Info.plist | 49 ++ .../ios/Runner/Runner-Bridging-Header.h | 1 + .../ios/RunnerTests/RunnerTests.swift | 12 + apps/example_liquid_glass/lib/app.dart | 8 +- apps/example_liquid_glass/lib/main.dart | 4 +- apps/example_liquid_glass/linux/.gitignore | 1 + .../example_liquid_glass/linux/CMakeLists.txt | 128 +++ .../linux/flutter/CMakeLists.txt | 88 ++ .../flutter/generated_plugin_registrant.cc | 27 + .../flutter/generated_plugin_registrant.h | 15 + .../linux/flutter/generated_plugins.cmake | 27 + .../linux/runner/CMakeLists.txt | 26 + .../example_liquid_glass/linux/runner/main.cc | 6 + .../linux/runner/my_application.cc | 148 ++++ .../linux/runner/my_application.h | 21 + apps/example_liquid_glass/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../macos/Runner.xcodeproj/project.pbxproj | 819 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 99 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 ++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + .../macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../macos/Runner/Release.entitlements | 8 + .../macos/RunnerTests/RunnerTests.swift | 12 + apps/example_liquid_glass/windows/.gitignore | 17 + .../windows/CMakeLists.txt | 108 +++ .../windows/flutter/CMakeLists.txt | 109 +++ .../flutter/generated_plugin_registrant.cc | 26 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 28 + .../windows/runner/CMakeLists.txt | 40 + .../windows/runner/Runner.rc | 121 +++ .../windows/runner/flutter_window.cpp | 71 ++ .../windows/runner/flutter_window.h | 33 + .../windows/runner/main.cpp | 43 + .../windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 14 + .../windows/runner/utils.cpp | 65 ++ .../windows/runner/utils.h | 19 + .../windows/runner/win32_window.cpp | 288 ++++++ .../windows/runner/win32_window.h | 102 +++ .../lib/src/liquid_glass_app.dart | 95 +- 118 files changed, 4590 insertions(+), 49 deletions(-) create mode 100644 apps/example_liquid_glass/.gitignore create mode 100644 apps/example_liquid_glass/.metadata create mode 100644 apps/example_liquid_glass/android/.gitignore create mode 100644 apps/example_liquid_glass/android/app/build.gradle.kts create mode 100644 apps/example_liquid_glass/android/app/src/debug/AndroidManifest.xml create mode 100644 apps/example_liquid_glass/android/app/src/main/AndroidManifest.xml create mode 100644 apps/example_liquid_glass/android/app/src/main/kotlin/com/ap/common/liquidglass/ap_common_example_liquid_glass/MainActivity.kt create mode 100644 apps/example_liquid_glass/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 apps/example_liquid_glass/android/app/src/main/res/drawable/launch_background.xml create mode 100644 apps/example_liquid_glass/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 apps/example_liquid_glass/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 apps/example_liquid_glass/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 apps/example_liquid_glass/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 apps/example_liquid_glass/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 apps/example_liquid_glass/android/app/src/main/res/values-night/styles.xml create mode 100644 apps/example_liquid_glass/android/app/src/main/res/values/styles.xml create mode 100644 apps/example_liquid_glass/android/app/src/profile/AndroidManifest.xml create mode 100644 apps/example_liquid_glass/android/build.gradle.kts create mode 100644 apps/example_liquid_glass/android/gradle.properties create mode 100644 apps/example_liquid_glass/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 apps/example_liquid_glass/android/settings.gradle.kts create mode 100644 apps/example_liquid_glass/ios/.gitignore create mode 100644 apps/example_liquid_glass/ios/Flutter/AppFrameworkInfo.plist create mode 100644 apps/example_liquid_glass/ios/Flutter/Debug.xcconfig create mode 100644 apps/example_liquid_glass/ios/Flutter/Release.xcconfig create mode 100644 apps/example_liquid_glass/ios/Runner.xcodeproj/project.pbxproj create mode 100644 apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 apps/example_liquid_glass/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 apps/example_liquid_glass/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 apps/example_liquid_glass/ios/Runner/AppDelegate.swift create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 apps/example_liquid_glass/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 apps/example_liquid_glass/ios/Runner/Base.lproj/Main.storyboard create mode 100644 apps/example_liquid_glass/ios/Runner/Info.plist create mode 100644 apps/example_liquid_glass/ios/Runner/Runner-Bridging-Header.h create mode 100644 apps/example_liquid_glass/ios/RunnerTests/RunnerTests.swift create mode 100644 apps/example_liquid_glass/linux/.gitignore create mode 100644 apps/example_liquid_glass/linux/CMakeLists.txt create mode 100644 apps/example_liquid_glass/linux/flutter/CMakeLists.txt create mode 100644 apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.cc create mode 100644 apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.h create mode 100644 apps/example_liquid_glass/linux/flutter/generated_plugins.cmake create mode 100644 apps/example_liquid_glass/linux/runner/CMakeLists.txt create mode 100644 apps/example_liquid_glass/linux/runner/main.cc create mode 100644 apps/example_liquid_glass/linux/runner/my_application.cc create mode 100644 apps/example_liquid_glass/linux/runner/my_application.h create mode 100644 apps/example_liquid_glass/macos/.gitignore create mode 100644 apps/example_liquid_glass/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 apps/example_liquid_glass/macos/Flutter/Flutter-Release.xcconfig create mode 100644 apps/example_liquid_glass/macos/Runner.xcodeproj/project.pbxproj create mode 100644 apps/example_liquid_glass/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 apps/example_liquid_glass/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 apps/example_liquid_glass/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 apps/example_liquid_glass/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 apps/example_liquid_glass/macos/Runner/AppDelegate.swift create mode 100644 apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 apps/example_liquid_glass/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 apps/example_liquid_glass/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 apps/example_liquid_glass/macos/Runner/Configs/Debug.xcconfig create mode 100644 apps/example_liquid_glass/macos/Runner/Configs/Release.xcconfig create mode 100644 apps/example_liquid_glass/macos/Runner/Configs/Warnings.xcconfig create mode 100644 apps/example_liquid_glass/macos/Runner/DebugProfile.entitlements create mode 100644 apps/example_liquid_glass/macos/Runner/Info.plist create mode 100644 apps/example_liquid_glass/macos/Runner/MainFlutterWindow.swift create mode 100644 apps/example_liquid_glass/macos/Runner/Release.entitlements create mode 100644 apps/example_liquid_glass/macos/RunnerTests/RunnerTests.swift create mode 100644 apps/example_liquid_glass/windows/.gitignore create mode 100644 apps/example_liquid_glass/windows/CMakeLists.txt create mode 100644 apps/example_liquid_glass/windows/flutter/CMakeLists.txt create mode 100644 apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.cc create mode 100644 apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.h create mode 100644 apps/example_liquid_glass/windows/flutter/generated_plugins.cmake create mode 100644 apps/example_liquid_glass/windows/runner/CMakeLists.txt create mode 100644 apps/example_liquid_glass/windows/runner/Runner.rc create mode 100644 apps/example_liquid_glass/windows/runner/flutter_window.cpp create mode 100644 apps/example_liquid_glass/windows/runner/flutter_window.h create mode 100644 apps/example_liquid_glass/windows/runner/main.cpp create mode 100644 apps/example_liquid_glass/windows/runner/resource.h create mode 100644 apps/example_liquid_glass/windows/runner/resources/app_icon.ico create mode 100644 apps/example_liquid_glass/windows/runner/runner.exe.manifest create mode 100644 apps/example_liquid_glass/windows/runner/utils.cpp create mode 100644 apps/example_liquid_glass/windows/runner/utils.h create mode 100644 apps/example_liquid_glass/windows/runner/win32_window.cpp create mode 100644 apps/example_liquid_glass/windows/runner/win32_window.h diff --git a/apps/example_liquid_glass/.gitignore b/apps/example_liquid_glass/.gitignore new file mode 100644 index 00000000..3820a95c --- /dev/null +++ b/apps/example_liquid_glass/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/apps/example_liquid_glass/.metadata b/apps/example_liquid_glass/.metadata new file mode 100644 index 00000000..84205a9c --- /dev/null +++ b/apps/example_liquid_glass/.metadata @@ -0,0 +1,42 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + - platform: android + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + - platform: ios + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + - platform: linux + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + - platform: macos + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + - platform: windows + create_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + base_revision: c6f67dede3d4aa1aa7a69dd56a3494a5cde6cc80 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/apps/example_liquid_glass/android/.gitignore b/apps/example_liquid_glass/android/.gitignore new file mode 100644 index 00000000..be3943c9 --- /dev/null +++ b/apps/example_liquid_glass/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/apps/example_liquid_glass/android/app/build.gradle.kts b/apps/example_liquid_glass/android/app/build.gradle.kts new file mode 100644 index 00000000..8888801d --- /dev/null +++ b/apps/example_liquid_glass/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.ap.common.liquidglass.ap_common_example_liquid_glass" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.ap.common.liquidglass.ap_common_example_liquid_glass" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/apps/example_liquid_glass/android/app/src/debug/AndroidManifest.xml b/apps/example_liquid_glass/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/apps/example_liquid_glass/android/app/src/main/AndroidManifest.xml b/apps/example_liquid_glass/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f44f7a4a --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/android/app/src/main/kotlin/com/ap/common/liquidglass/ap_common_example_liquid_glass/MainActivity.kt b/apps/example_liquid_glass/android/app/src/main/kotlin/com/ap/common/liquidglass/ap_common_example_liquid_glass/MainActivity.kt new file mode 100644 index 00000000..cbf01a61 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/kotlin/com/ap/common/liquidglass/ap_common_example_liquid_glass/MainActivity.kt @@ -0,0 +1,5 @@ +package com.ap.common.liquidglass.ap_common_example_liquid_glass + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/apps/example_liquid_glass/android/app/src/main/res/drawable-v21/launch_background.xml b/apps/example_liquid_glass/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/apps/example_liquid_glass/android/app/src/main/res/drawable/launch_background.xml b/apps/example_liquid_glass/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/apps/example_liquid_glass/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/example_liquid_glass/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/example_liquid_glass/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/example_liquid_glass/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/example_liquid_glass/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/android/app/src/main/res/values-night/styles.xml b/apps/example_liquid_glass/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/example_liquid_glass/android/app/src/main/res/values/styles.xml b/apps/example_liquid_glass/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/example_liquid_glass/android/app/src/profile/AndroidManifest.xml b/apps/example_liquid_glass/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/apps/example_liquid_glass/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/apps/example_liquid_glass/android/build.gradle.kts b/apps/example_liquid_glass/android/build.gradle.kts new file mode 100644 index 00000000..dbee657b --- /dev/null +++ b/apps/example_liquid_glass/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/apps/example_liquid_glass/android/gradle.properties b/apps/example_liquid_glass/android/gradle.properties new file mode 100644 index 00000000..fbee1d8c --- /dev/null +++ b/apps/example_liquid_glass/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/apps/example_liquid_glass/android/gradle/wrapper/gradle-wrapper.properties b/apps/example_liquid_glass/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..e4ef43fb --- /dev/null +++ b/apps/example_liquid_glass/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/apps/example_liquid_glass/android/settings.gradle.kts b/apps/example_liquid_glass/android/settings.gradle.kts new file mode 100644 index 00000000..ca7fe065 --- /dev/null +++ b/apps/example_liquid_glass/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/apps/example_liquid_glass/ios/.gitignore b/apps/example_liquid_glass/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/apps/example_liquid_glass/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/apps/example_liquid_glass/ios/Flutter/AppFrameworkInfo.plist b/apps/example_liquid_glass/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..1dc6cf76 --- /dev/null +++ b/apps/example_liquid_glass/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/apps/example_liquid_glass/ios/Flutter/Debug.xcconfig b/apps/example_liquid_glass/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/apps/example_liquid_glass/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/apps/example_liquid_glass/ios/Flutter/Release.xcconfig b/apps/example_liquid_glass/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/apps/example_liquid_glass/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/apps/example_liquid_glass/ios/Runner.xcodeproj/project.pbxproj b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..f5cdd4d2 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/example_liquid_glass/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..e3773d42 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcworkspace/contents.xcworkspacedata b/apps/example_liquid_glass/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..1d526a16 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/apps/example_liquid_glass/ios/Runner/AppDelegate.swift b/apps/example_liquid_glass/ios/Runner/AppDelegate.swift new file mode 100644 index 00000000..62666446 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/apps/example_liquid_glass/ios/Runner/Base.lproj/LaunchScreen.storyboard b/apps/example_liquid_glass/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/ios/Runner/Base.lproj/Main.storyboard b/apps/example_liquid_glass/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/ios/Runner/Info.plist b/apps/example_liquid_glass/ios/Runner/Info.plist new file mode 100644 index 00000000..4ac182e8 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Ap Common Example Liquid Glass + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ap_common_example_liquid_glass + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/apps/example_liquid_glass/ios/Runner/Runner-Bridging-Header.h b/apps/example_liquid_glass/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/apps/example_liquid_glass/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/apps/example_liquid_glass/ios/RunnerTests/RunnerTests.swift b/apps/example_liquid_glass/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/apps/example_liquid_glass/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/apps/example_liquid_glass/lib/app.dart b/apps/example_liquid_glass/lib/app.dart index 9a9a6c6b..20f596d1 100644 --- a/apps/example_liquid_glass/lib/app.dart +++ b/apps/example_liquid_glass/lib/app.dart @@ -7,11 +7,9 @@ class LiquidGlassExampleApp extends StatelessWidget { @override Widget build(BuildContext context) { - return TranslationProvider( - child: LiquidGlassApApp( - home: const GlassHomePage(), - onGenerateTitle: (_) => 'Liquid Glass Example', - ), + return LiquidGlassApApp( + home: const GlassHomePage(), + onGenerateTitle: (_) => 'Liquid Glass Example', ); } } diff --git a/apps/example_liquid_glass/lib/main.dart b/apps/example_liquid_glass/lib/main.dart index a5b246aa..4666e64e 100644 --- a/apps/example_liquid_glass/lib/main.dart +++ b/apps/example_liquid_glass/lib/main.dart @@ -14,5 +14,7 @@ Future main() async { key: encrypt.Key.fromUtf8('l9r1W3wcsnJTayxCXwoFt62w1i4sQ5J9'), iv: encrypt.IV.fromUtf8('auc9OV5r0nLwjCAH'), ); - runApp(const LiquidGlassExampleApp()); + runApp( + LiquidGlassWidgets.wrap(const LiquidGlassExampleApp()), + ); } diff --git a/apps/example_liquid_glass/linux/.gitignore b/apps/example_liquid_glass/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/apps/example_liquid_glass/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/apps/example_liquid_glass/linux/CMakeLists.txt b/apps/example_liquid_glass/linux/CMakeLists.txt new file mode 100644 index 00000000..2076e14d --- /dev/null +++ b/apps/example_liquid_glass/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "ap_common_example_liquid_glass") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.ap.common.liquidglass.ap_common_example_liquid_glass") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/apps/example_liquid_glass/linux/flutter/CMakeLists.txt b/apps/example_liquid_glass/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/apps/example_liquid_glass/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.cc b/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..1ef38c29 --- /dev/null +++ b/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,27 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_saver_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); + file_saver_plugin_register_with_registrar(file_saver_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) printing_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin"); + printing_plugin_register_with_registrar(printing_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.h b/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/apps/example_liquid_glass/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/apps/example_liquid_glass/linux/flutter/generated_plugins.cmake b/apps/example_liquid_glass/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..cba264c8 --- /dev/null +++ b/apps/example_liquid_glass/linux/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_saver + file_selector_linux + printing + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/apps/example_liquid_glass/linux/runner/CMakeLists.txt b/apps/example_liquid_glass/linux/runner/CMakeLists.txt new file mode 100644 index 00000000..e97dabc7 --- /dev/null +++ b/apps/example_liquid_glass/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/apps/example_liquid_glass/linux/runner/main.cc b/apps/example_liquid_glass/linux/runner/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/apps/example_liquid_glass/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/apps/example_liquid_glass/linux/runner/my_application.cc b/apps/example_liquid_glass/linux/runner/my_application.cc new file mode 100644 index 00000000..e947e4ad --- /dev/null +++ b/apps/example_liquid_glass/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "ap_common_example_liquid_glass"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "ap_common_example_liquid_glass"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/apps/example_liquid_glass/linux/runner/my_application.h b/apps/example_liquid_glass/linux/runner/my_application.h new file mode 100644 index 00000000..db16367a --- /dev/null +++ b/apps/example_liquid_glass/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/apps/example_liquid_glass/macos/.gitignore b/apps/example_liquid_glass/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/apps/example_liquid_glass/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/apps/example_liquid_glass/macos/Flutter/Flutter-Debug.xcconfig b/apps/example_liquid_glass/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/apps/example_liquid_glass/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/apps/example_liquid_glass/macos/Flutter/Flutter-Release.xcconfig b/apps/example_liquid_glass/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/apps/example_liquid_glass/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/apps/example_liquid_glass/macos/Runner.xcodeproj/project.pbxproj b/apps/example_liquid_glass/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..7ab697ec --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,819 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 877BE9C6444A2222CA96448D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9ED21E24E2592E459A0048DD /* Pods_RunnerTests.framework */; }; + 921885A9670B88D62FD64AF3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7323D157E85AA2DEF564E19F /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* ap_common_example_liquid_glass.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ap_common_example_liquid_glass.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7323D157E85AA2DEF564E19F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 947A67C3FDFE60B13F42508E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 9D26850C2DE263EBF4F94647 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9ED21E24E2592E459A0048DD /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9F96348047BCB77E822E7D59 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + BE26E5BBE785D3E9D616D90C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + DB1ACD1AC0DB22A77BFD2CE9 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + FCB57B8043F6B7849761FE7D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 877BE9C6444A2222CA96448D /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 921885A9670B88D62FD64AF3 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + BFC6AD02D4EB304802EC6B7B /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* ap_common_example_liquid_glass.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + BFC6AD02D4EB304802EC6B7B /* Pods */ = { + isa = PBXGroup; + children = ( + BE26E5BBE785D3E9D616D90C /* Pods-Runner.debug.xcconfig */, + 9F96348047BCB77E822E7D59 /* Pods-Runner.release.xcconfig */, + FCB57B8043F6B7849761FE7D /* Pods-Runner.profile.xcconfig */, + DB1ACD1AC0DB22A77BFD2CE9 /* Pods-RunnerTests.debug.xcconfig */, + 9D26850C2DE263EBF4F94647 /* Pods-RunnerTests.release.xcconfig */, + 947A67C3FDFE60B13F42508E /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7323D157E85AA2DEF564E19F /* Pods_Runner.framework */, + 9ED21E24E2592E459A0048DD /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + E6D94A79AC19F8A4E0414E0B /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 424ECA0403487CD85158166E /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 80D578BC7659B17A8D513EA2 /* [CP] Embed Pods Frameworks */, + B6D578792651946F4CB0D551 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* ap_common_example_liquid_glass.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 424ECA0403487CD85158166E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 80D578BC7659B17A8D513EA2 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B6D578792651946F4CB0D551 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E6D94A79AC19F8A4E0414E0B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DB1ACD1AC0DB22A77BFD2CE9 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ap_common_example_liquid_glass.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ap_common_example_liquid_glass"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9D26850C2DE263EBF4F94647 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ap_common_example_liquid_glass.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ap_common_example_liquid_glass"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 947A67C3FDFE60B13F42508E /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ap_common_example_liquid_glass.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ap_common_example_liquid_glass"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/apps/example_liquid_glass/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example_liquid_glass/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/example_liquid_glass/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/example_liquid_glass/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..edb543cb --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/macos/Runner.xcworkspace/contents.xcworkspacedata b/apps/example_liquid_glass/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/apps/example_liquid_glass/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/example_liquid_glass/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/example_liquid_glass/macos/Runner/AppDelegate.swift b/apps/example_liquid_glass/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..b3c17614 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/apps/example_liquid_glass/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/example_liquid_glass/macos/Runner/Configs/AppInfo.xcconfig b/apps/example_liquid_glass/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..b9965e6a --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = ap_common_example_liquid_glass + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.ap.common.liquidglass.apCommonExampleLiquidGlass + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.ap.common.liquidglass. All rights reserved. diff --git a/apps/example_liquid_glass/macos/Runner/Configs/Debug.xcconfig b/apps/example_liquid_glass/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/apps/example_liquid_glass/macos/Runner/Configs/Release.xcconfig b/apps/example_liquid_glass/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/apps/example_liquid_glass/macos/Runner/Configs/Warnings.xcconfig b/apps/example_liquid_glass/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/apps/example_liquid_glass/macos/Runner/DebugProfile.entitlements b/apps/example_liquid_glass/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..dddb8a30 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/apps/example_liquid_glass/macos/Runner/Info.plist b/apps/example_liquid_glass/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/apps/example_liquid_glass/macos/Runner/MainFlutterWindow.swift b/apps/example_liquid_glass/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/apps/example_liquid_glass/macos/Runner/Release.entitlements b/apps/example_liquid_glass/macos/Runner/Release.entitlements new file mode 100644 index 00000000..852fa1a4 --- /dev/null +++ b/apps/example_liquid_glass/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/apps/example_liquid_glass/macos/RunnerTests/RunnerTests.swift b/apps/example_liquid_glass/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/apps/example_liquid_glass/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/apps/example_liquid_glass/windows/.gitignore b/apps/example_liquid_glass/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/apps/example_liquid_glass/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/apps/example_liquid_glass/windows/CMakeLists.txt b/apps/example_liquid_glass/windows/CMakeLists.txt new file mode 100644 index 00000000..061c9312 --- /dev/null +++ b/apps/example_liquid_glass/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(ap_common_example_liquid_glass LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "ap_common_example_liquid_glass") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/apps/example_liquid_glass/windows/flutter/CMakeLists.txt b/apps/example_liquid_glass/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..903f4899 --- /dev/null +++ b/apps/example_liquid_glass/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.cc b/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..cd55fe34 --- /dev/null +++ b/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSaverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSaverPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + PrintingPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PrintingPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.h b/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/apps/example_liquid_glass/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/apps/example_liquid_glass/windows/flutter/generated_plugins.cmake b/apps/example_liquid_glass/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..802c4136 --- /dev/null +++ b/apps/example_liquid_glass/windows/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_saver + file_selector_windows + printing + share_plus + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/apps/example_liquid_glass/windows/runner/CMakeLists.txt b/apps/example_liquid_glass/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/apps/example_liquid_glass/windows/runner/Runner.rc b/apps/example_liquid_glass/windows/runner/Runner.rc new file mode 100644 index 00000000..5c4d33ba --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.ap.common.liquidglass" "\0" + VALUE "FileDescription", "ap_common_example_liquid_glass" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "ap_common_example_liquid_glass" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.ap.common.liquidglass. All rights reserved." "\0" + VALUE "OriginalFilename", "ap_common_example_liquid_glass.exe" "\0" + VALUE "ProductName", "ap_common_example_liquid_glass" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/apps/example_liquid_glass/windows/runner/flutter_window.cpp b/apps/example_liquid_glass/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..955ee303 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/apps/example_liquid_glass/windows/runner/flutter_window.h b/apps/example_liquid_glass/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/apps/example_liquid_glass/windows/runner/main.cpp b/apps/example_liquid_glass/windows/runner/main.cpp new file mode 100644 index 00000000..58273a5e --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"ap_common_example_liquid_glass", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/apps/example_liquid_glass/windows/runner/resource.h b/apps/example_liquid_glass/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/apps/example_liquid_glass/windows/runner/resources/app_icon.ico b/apps/example_liquid_glass/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/apps/example_liquid_glass/windows/runner/runner.exe.manifest b/apps/example_liquid_glass/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..153653e8 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/apps/example_liquid_glass/windows/runner/utils.cpp b/apps/example_liquid_glass/windows/runner/utils.cpp new file mode 100644 index 00000000..3a0b4651 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/apps/example_liquid_glass/windows/runner/utils.h b/apps/example_liquid_glass/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/apps/example_liquid_glass/windows/runner/win32_window.cpp b/apps/example_liquid_glass/windows/runner/win32_window.cpp new file mode 100644 index 00000000..60608d0f --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/apps/example_liquid_glass/windows/runner/win32_window.h b/apps/example_liquid_glass/windows/runner/win32_window.h new file mode 100644 index 00000000..e901dde6 --- /dev/null +++ b/apps/example_liquid_glass/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/ap_common_liquid_glass/lib/src/liquid_glass_app.dart b/packages/ap_common_liquid_glass/lib/src/liquid_glass_app.dart index e006d2a2..c9327b76 100644 --- a/packages/ap_common_liquid_glass/lib/src/liquid_glass_app.dart +++ b/packages/ap_common_liquid_glass/lib/src/liquid_glass_app.dart @@ -6,30 +6,34 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; /// A convenience wrapper around [MaterialApp] that applies both -/// [ApTheme] and [LiquidGlassWidgets] setup. +/// [ApTheme] and [GlassTheme] setup. /// /// This is the glass-enhanced equivalent of [ApApp]. It: -/// - Calls [LiquidGlassWidgets.initialize] to pre-cache shaders -/// - Wraps the app with [LiquidGlassWidgets.wrap] for GPU sharing /// - Applies [GlassTheme] bridged from [ApTheme] seed colors +/// - Manages theme mode, locale, and color preferences +/// +/// **Important:** +/// - Call [LiquidGlassWidgets.initialize] in `main()` and wrap +/// with [LiquidGlassWidgets.wrap] at the `runApp` level. +/// - Do NOT wrap with [TranslationProvider] externally — this +/// widget already includes one. Nesting two providers of the +/// same locale type causes a duplicate GlobalKey crash. /// /// ```dart /// void main() async { /// WidgetsFlutterBinding.ensureInitialized(); /// await LiquidGlassWidgets.initialize(); -/// runApp(const MyApp()); +/// runApp(LiquidGlassWidgets.wrap(const MyApp())); /// } /// -/// class MyApp extends StatefulWidget { -/// @override -/// State createState() => _MyAppState(); -/// } +/// class MyApp extends StatelessWidget { +/// const MyApp({super.key}); /// -/// class _MyAppState extends State { /// @override /// Widget build(BuildContext context) { +/// // No TranslationProvider needed here. /// return LiquidGlassApApp( -/// home: GlassHomePage(), +/// home: const GlassHomePage(), /// ); /// } /// } @@ -209,44 +213,49 @@ class LiquidGlassApAppState extends State preferences: PreferenceUtil.instance, child: Builder( builder: (BuildContext context) { - final Color seedColor = ApTheme.of(context).seedColor; + final Color seedColor = + ApTheme.of(context).seedColor; final GlassThemeData glassThemeData = GlassThemeBridge.fromContext(context); return TranslationProvider( child: Builder( builder: (BuildContext context) { - return LiquidGlassWidgets.wrap( - GlassTheme( - data: glassThemeData, - child: MaterialApp( - onGenerateTitle: widget.onGenerateTitle, - debugShowCheckedModeBanner: - widget.debugShowCheckedModeBanner, - routes: { - if (widget.home != null) - Navigator.defaultRouteName: - (_) => widget.home!, - ...widget.routes, - }, - onGenerateRoute: widget.onGenerateRoute, - theme: ApTheme.light(seedColor), - darkTheme: ApTheme.dark(seedColor), - themeMode: _themeMode, - locale: TranslationProvider.of(context) - .flutterLocale, - navigatorObservers: - widget.navigatorObservers, - supportedLocales: - AppLocaleUtils.supportedLocales, - localizationsDelegates: - >[ - ...widget - .additionalLocalizationsDelegates, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - ), + return GlassTheme( + data: glassThemeData, + child: MaterialApp( + home: widget.home, + onGenerateTitle: + widget.onGenerateTitle, + debugShowCheckedModeBanner: widget + .debugShowCheckedModeBanner, + routes: widget.routes, + onGenerateRoute: + widget.onGenerateRoute, + theme: ApTheme.light(seedColor), + darkTheme: + ApTheme.dark(seedColor), + themeMode: _themeMode, + locale: TranslationProvider.of( + context, + ).flutterLocale, + navigatorObservers: + widget.navigatorObservers, + supportedLocales: + AppLocaleUtils + .supportedLocales, + localizationsDelegates: + >[ + ...widget + .additionalLocalizationsDelegates, + GlobalMaterialLocalizations + .delegate, + GlobalWidgetsLocalizations + .delegate, + GlobalCupertinoLocalizations + .delegate, + ], ), ); }, From 9bf41a28a635b0fb0e9d99e29bcd826dbceb58ee Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Sun, 5 Apr 2026 15:53:55 +0800 Subject: [PATCH 05/14] refactor(ui): extract CourseTableView and ScoreListTab/ScoreAnalysisTab as public widgets Extract course table grid rendering into CourseTableView and score tab widgets into ScoreListTab/ScoreAnalysisTab for reuse by the Liquid Glass package. Original scaffolds delegate to the new widgets. --- .../lib/ap_common_flutter_ui.dart | 2 + .../lib/src/scaffold/course_scaffold.dart | 410 +------------- .../lib/src/scaffold/score_scaffold.dart | 362 +------------ .../lib/src/widgets/course_table_view.dart | 508 ++++++++++++++++++ .../lib/src/widgets/score_tab_widgets.dart | 402 ++++++++++++++ 5 files changed, 936 insertions(+), 748 deletions(-) create mode 100644 packages/ap_common_flutter_ui/lib/src/widgets/course_table_view.dart create mode 100644 packages/ap_common_flutter_ui/lib/src/widgets/score_tab_widgets.dart diff --git a/packages/ap_common_flutter_ui/lib/ap_common_flutter_ui.dart b/packages/ap_common_flutter_ui/lib/ap_common_flutter_ui.dart index 2e533c6a..b84ea968 100644 --- a/packages/ap_common_flutter_ui/lib/ap_common_flutter_ui.dart +++ b/packages/ap_common_flutter_ui/lib/ap_common_flutter_ui.dart @@ -26,6 +26,7 @@ export 'src/widgets/about_info_card.dart'; export 'src/widgets/ap_drawer.dart'; export 'src/widgets/ap_network_image.dart'; export 'src/widgets/color_picker_widgets.dart'; +export 'src/widgets/course_table_view.dart'; export 'src/widgets/default_dialog.dart'; export 'src/widgets/dialog_option.dart'; export 'src/widgets/hint_banner.dart'; @@ -37,6 +38,7 @@ export 'src/widgets/option_dialog.dart'; export 'src/widgets/phone_list_item.dart'; export 'src/widgets/progress_dialog.dart'; export 'src/widgets/score_analysis_widgets.dart'; +export 'src/widgets/score_tab_widgets.dart'; export 'src/widgets/semester_picker.dart'; export 'src/widgets/setting_page_widgets.dart'; export 'src/widgets/yes_no_dialog.dart'; diff --git a/packages/ap_common_flutter_ui/lib/src/scaffold/course_scaffold.dart b/packages/ap_common_flutter_ui/lib/src/scaffold/course_scaffold.dart index 5cd8e5a8..dc4bff03 100644 --- a/packages/ap_common_flutter_ui/lib/src/scaffold/course_scaffold.dart +++ b/packages/ap_common_flutter_ui/lib/src/scaffold/course_scaffold.dart @@ -15,10 +15,6 @@ enum CourseState { loading, finish, error, empty, offlineEmpty, custom } enum CourseNotifyState { schedule, cancel } -enum _ContentStyle { list, table } - -const double _courseHeight = 64.0; - const String _kCourseInvisibleKey = '${ApConstants.packageName}.course_invisible_'; @@ -171,7 +167,7 @@ class CourseScaffold extends StatefulWidget { class CourseScaffoldState extends State { final GlobalKey _repaintBoundaryGlobalKey = GlobalKey(); - _ContentStyle _contentStyle = _ContentStyle.table; + ContentStyle _contentStyle = ContentStyle.table; bool? showSectionTime; bool? showInstructors; @@ -185,8 +181,6 @@ class CourseScaffoldState extends State { List invisibleCourseCodes = []; - late Map> _courseLookup; - final Map _courseColorMap = {}; int _colorIndex = 0; @@ -204,7 +198,6 @@ class CourseScaffoldState extends State { @override void initState() { - _buildCourseLookup(); showSectionTime = widget.showSectionTime ?? PreferenceUtil.instance.getBool(ApConstants.showSectionTime, true); showInstructors = widget.showInstructors ?? @@ -239,9 +232,6 @@ class CourseScaffoldState extends State { @override void didUpdateWidget(covariant CourseScaffold oldWidget) { - if (widget.courseData != oldWidget.courseData) { - _buildCourseLookup(); - } fetchInvisibleCourseCodes(); super.didUpdateWidget(oldWidget); } @@ -416,13 +406,13 @@ class CourseScaffoldState extends State { onPressed: () { setState( () => _contentStyle = - (_contentStyle == _ContentStyle.table) - ? _ContentStyle.list - : _ContentStyle.table, + (_contentStyle == ContentStyle.table) + ? ContentStyle.list + : ContentStyle.table, ); }, child: Icon( - _contentStyle == _ContentStyle.table + _contentStyle == ContentStyle.table ? Icons.list_rounded : Icons.grid_view_rounded, ), @@ -556,31 +546,23 @@ class CourseScaffoldState extends State { Icons.warning_amber_rounded, ); default: - if (isLandscape || _contentStyle == _ContentStyle.table) { - final int weekdayCount = widget.courseData.hasHoliday ? 7 : 5; - return SingleChildScrollView( + if (isLandscape || _contentStyle == ContentStyle.table) { + return CourseTableView( + courseData: widget.courseData, + invisibleCourseCodes: invisibleCourseCodes, controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.only( - top: 8.0, - bottom: 80.0, - ), - child: RepaintBoundary( - key: _repaintBoundaryGlobalKey, - child: ColoredBox( - color: Theme.of(context).scaffoldBackgroundColor, - child: Column( - children: [ - _buildWeekdayHeader(colorScheme, weekdayCount), - _buildCourseGrid( - colorScheme, - weekdayCount, - widget.courseData.timeCodes, - ), - ], - ), - ), - ), + onCoursePressed: ( + Course course, + TimeCode timeCode, + int weekday, + ) => + _onPressed(weekday, timeCode, course), + courseColorResolver: _getCourseColor, + repaintBoundaryKey: _repaintBoundaryGlobalKey, + mergeCourse: mergeCourse, + showSectionTime: showSectionTime, + showInstructors: showInstructors, + showClassroomLocation: showClassroomLocation, ); } else { return CourseList( @@ -636,356 +618,6 @@ class CourseScaffoldState extends State { } } - Widget _buildWeekdayHeader(ColorScheme colorScheme, int weekdayCount) { - return Container( - decoration: BoxDecoration( - color: colorScheme.primaryContainer.withAlpha(77), - border: Border( - bottom: BorderSide( - color: colorScheme.outlineVariant.withAlpha(128), - ), - ), - ), - child: Row( - children: [ - _buildTimeSlotHeader(colorScheme), - for (int i = 0; i < weekdayCount; i++) - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text( - context.ap.weekdaysCourse[i], - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: colorScheme.primary, - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildTimeSlotHeader(ColorScheme colorScheme) { - return Container( - width: 48, - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text( - '', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: colorScheme.primary, - ), - ), - ); - } - - Widget _buildCourseGrid( - ColorScheme colorScheme, - int weekdayCount, - List timeCodes, - ) { - final int minIndex = widget.courseData.minTimeCodeIndex; - final int maxIndex = widget.courseData.maxTimeCodeIndex; - - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTimeColumn(colorScheme, timeCodes, minIndex, maxIndex), - for (int weekday = 1; weekday <= weekdayCount; weekday++) - Expanded( - child: _buildWeekdayColumn( - colorScheme, - weekday, - weekdayCount, - minIndex, - maxIndex, - ), - ), - ], - ); - } - - Widget _buildTimeColumn( - ColorScheme colorScheme, - List timeCodes, - int minIndex, - int maxIndex, - ) { - return Column( - children: [ - for (int i = minIndex; i <= maxIndex; i++) - _buildTimeSlot( - colorScheme, - i < timeCodes.length ? timeCodes[i] : null, - i, - i == maxIndex, - ), - ], - ); - } - - Widget _buildWeekdayColumn( - ColorScheme colorScheme, - int weekday, - int weekdayCount, - int minIndex, - int maxIndex, - ) { - final List children = []; - for (int i = minIndex; i <= maxIndex; i++) { - final Course? course = _getCourseAt(weekday, i); - final bool isInvisible = - course != null && invisibleCourseCodes.contains(course.code); - - if (course == null || isInvisible) { - children.add( - _buildEmptyCell( - colorScheme, - weekday < weekdayCount, - i == maxIndex, - ), - ); - } else { - int span = 1; - if (mergeCourse ?? true) { - while (i + span <= maxIndex && - _getCourseAt(weekday, i + span) == course) { - span++; - } - } - children.add( - _buildCourseCell( - colorScheme, - weekday, - i, - course, - span, - weekday < weekdayCount, - i + span - 1 == maxIndex, - ), - ); - i += span - 1; - } - } - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: children, - ); - } - - Widget _buildTimeSlot( - ColorScheme colorScheme, - TimeCode? timeCode, - int timeIndex, - bool isLast, - ) { - return Container( - width: 48, - height: _courseHeight, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest.withAlpha(77), - border: Border( - right: BorderSide( - color: colorScheme.outlineVariant.withAlpha(128), - ), - bottom: isLast - ? BorderSide.none - : BorderSide( - color: colorScheme.outlineVariant.withAlpha(77), - width: 0.5, - ), - ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - timeCode?.title ?? '${timeIndex + 1}', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: colorScheme.onSurfaceVariant, - ), - ), - if (timeCode != null && (showSectionTime ?? true)) ...[ - const SizedBox(height: 2), - Text( - timeCode.startTime, - style: TextStyle( - fontSize: 9, - color: colorScheme.onSurfaceVariant.withAlpha(179), - ), - ), - ], - ], - ), - ); - } - - Widget _buildEmptyCell( - ColorScheme colorScheme, - bool showRightBorder, - bool isLast, - ) { - return Container( - height: _courseHeight, - decoration: BoxDecoration( - border: Border( - right: showRightBorder - ? BorderSide( - color: colorScheme.outlineVariant.withAlpha(51), - width: 0.5, - ) - : BorderSide.none, - bottom: isLast - ? BorderSide.none - : BorderSide( - color: colorScheme.outlineVariant.withAlpha(77), - width: 0.5, - ), - ), - ), - ); - } - - Widget _buildCourseCell( - ColorScheme colorScheme, - int weekday, - int timeIndex, - Course course, - int span, - bool showRightBorder, - bool isLast, - ) { - return Container( - height: _courseHeight * span, - decoration: BoxDecoration( - border: Border( - right: showRightBorder - ? BorderSide( - color: colorScheme.outlineVariant.withAlpha(51), - width: 0.5, - ) - : BorderSide.none, - bottom: isLast - ? BorderSide.none - : BorderSide( - color: colorScheme.outlineVariant.withAlpha(77), - width: 0.5, - ), - ), - ), - child: _buildCourseCard(colorScheme, weekday, timeIndex, course, span), - ); - } - - void _buildCourseLookup() { - _courseLookup = >{}; - for (final Course course in widget.courseData.courses) { - for (final SectionTime time in course.times) { - _courseLookup - .putIfAbsent( - time.weekday, - () => {}, - )[time.index] = course; - } - } - } - - Course? _getCourseAt(int weekday, int timeIndex) { - return _courseLookup[weekday]?[timeIndex]; - } - - Widget _buildCourseCard( - ColorScheme colorScheme, - int weekday, - int timeIndex, - Course course, - int span, - ) { - final Color courseColor = _getCourseColor(course.code); - final String locationInfo = - (showClassroomLocation ?? true) && course.location != null - ? course.location.toString() - : ''; - final String instructorInfo = - (showInstructors ?? true) ? course.getInstructors() : ''; - - final Color onCourseColor = - ThemeData.estimateBrightnessForColor(courseColor) == Brightness.dark - ? Colors.white - : Colors.black; - - final String displayInfo = [ - if (instructorInfo.isNotEmpty) instructorInfo, - if (locationInfo.isNotEmpty) locationInfo, - ].join('\n'); - - return GestureDetector( - onTap: () { - final TimeCode timeCode = timeIndex < widget.courseData.timeCodes.length - ? widget.courseData.timeCodes[timeIndex] - : const TimeCode(title: '?', startTime: '?', endTime: '?'); - _onPressed(weekday, timeCode, course); - }, - child: Container( - width: double.infinity, - height: double.infinity, - margin: const EdgeInsets.all(2), - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), - decoration: BoxDecoration( - color: courseColor.withAlpha(230), - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: courseColor.withAlpha(77), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - course.title, - textAlign: TextAlign.center, - maxLines: span > 1 ? 4 : 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: span > 1 ? 14 : 9.5, - fontWeight: FontWeight.w600, - color: onCourseColor, - height: 1.1, - ), - ), - if (displayInfo.isNotEmpty) ...[ - SizedBox(height: span > 1 ? 4 : 2), - Text( - displayInfo, - textAlign: TextAlign.center, - maxLines: span > 1 ? 4 : 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: span > 1 ? 12 : 8.5, - color: onCourseColor.withAlpha(217), - height: 1.0, - ), - ), - ], - ], - ), - ), - ); - } - void _onPressed(int weekday, TimeCode timeCode, Course course) { showModalBottomSheet( context: context, diff --git a/packages/ap_common_flutter_ui/lib/src/scaffold/score_scaffold.dart b/packages/ap_common_flutter_ui/lib/src/scaffold/score_scaffold.dart index 8db8ad68..f44728f8 100644 --- a/packages/ap_common_flutter_ui/lib/src/scaffold/score_scaffold.dart +++ b/packages/ap_common_flutter_ui/lib/src/scaffold/score_scaffold.dart @@ -225,7 +225,7 @@ class ScoreScaffoldState extends State { elevation: 12.0, child: ColoredBox( color: Theme.of(context).colorScheme.surfaceContainerHighest, - child: _ScoreListTab( + child: ScoreListTab( scoreData: widget.scoreData!, onRefresh: widget.onRefresh, middleTitle: widget.middleTitle, @@ -428,13 +428,13 @@ class _ScoreContentState extends State { if (widget.scoreData == null) return const SizedBox.shrink(); if (widget.isAnalysisView) { - return _ScoreAnalysisTab( + return ScoreAnalysisTab( scoreData: widget.scoreData!, onRefresh: widget.onRefresh, controller: widget.scrollController, ); } else { - return _ScoreListTab( + return ScoreListTab( scoreData: widget.scoreData!, onRefresh: widget.onRefresh, middleTitle: widget.middleTitle, @@ -448,362 +448,6 @@ class _ScoreContentState extends State { } } -class _ScoreListTab extends StatelessWidget { - const _ScoreListTab({ - required this.scoreData, - this.onRefresh, - this.middleTitle, - this.finalTitle, - this.onScoreSelect, - this.middleScoreBuilder, - this.finalScoreBuilder, - this.controller, - }); - - final ScoreData scoreData; - final VoidCallback? onRefresh; - final String? middleTitle; - final String? finalTitle; - final Function(int index)? onScoreSelect; - final Widget Function(int index)? middleScoreBuilder; - final Widget Function(int index)? finalScoreBuilder; - final ScrollController? controller; - - @override - Widget build(BuildContext context) { - final ColorScheme colorScheme = Theme.of(context).colorScheme; - - return RefreshIndicator( - onRefresh: () async => onRefresh?.call(), - child: ListView.builder( - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(16), - itemCount: scoreData.scores.length, - itemBuilder: (BuildContext context, int index) { - return _buildScoreItem(colorScheme, scoreData.scores[index], index); - }, - ), - ); - } - - Widget _buildScoreItem(ColorScheme colorScheme, Score score, int index) { - final String scoreStr = score.semesterScore ?? ''; - final double? scoreValue = double.tryParse(scoreStr); - final bool isPassed = scoreValue != null && scoreValue >= 60; - final Color scoreColor = scoreValue == null - ? colorScheme.onSurfaceVariant - : isPassed - ? _getScoreColor(scoreValue) - : colorScheme.error; - - return GestureDetector( - onTap: onScoreSelect == null - ? null - : () { - onScoreSelect!(index); - AnalyticsUtil.instance.logEvent('score_title_click'); - }, - child: Container( - margin: const EdgeInsets.only(bottom: 8), - decoration: BoxDecoration( - color: colorScheme.surfaceContainerLowest, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: colorScheme.outlineVariant.withAlpha(77)), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Container( - width: 4, - height: 50, - decoration: BoxDecoration( - color: scoreColor, - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - score.title, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 4), - Row( - children: [ - _buildTag( - colorScheme, - score.required ?? '', - colorScheme.tertiary, - ), - const SizedBox(width: 8), - Text( - '${score.units} 學分', - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ], - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (finalScoreBuilder == null) - Text( - score.semesterScore ?? '-', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: scoreColor, - ), - ), - if (finalScoreBuilder != null) finalScoreBuilder!(index), - const SizedBox(height: 4), - if (middleScoreBuilder == null && - score.middleScore != null && - score.middleScore!.isNotEmpty) - Text( - '期中: ${score.middleScore}', - style: TextStyle( - fontSize: 11, - color: colorScheme.onSurfaceVariant, - ), - ), - if (middleScoreBuilder != null) middleScoreBuilder!(index), - ], - ), - ], - ), - ), - ), - ); - } - - Color _getScoreColor(double score) { - if (score >= 90) return const Color(0xFF4CAF50); - if (score >= 80) return const Color(0xFF8BC34A); - if (score >= 70) return const Color(0xFF2196F3); - if (score >= 60) return const Color(0xFFFF9800); - return const Color(0xFFF44336); - } - - Widget _buildTag(ColorScheme colorScheme, String text, Color color) { - if (text.isEmpty) return const SizedBox.shrink(); - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: color.withAlpha(26), - borderRadius: BorderRadius.circular(4), - ), - child: Text( - text.replaceAll('【', '').replaceAll('】', ''), - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: color, - ), - ), - ); - } -} - -class _ScoreAnalysisTab extends StatelessWidget { - const _ScoreAnalysisTab({ - required this.scoreData, - this.onRefresh, - this.controller, - }); - - final ScoreData scoreData; - final VoidCallback? onRefresh; - final ScrollController? controller; - - @override - Widget build(BuildContext context) { - final ColorScheme colorScheme = Theme.of(context).colorScheme; - final ScoreAnalysis analysis = ScoreAnalysis(scoreData); - - return RefreshIndicator( - onRefresh: () async => onRefresh?.call(), - child: SingleChildScrollView( - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(16), - child: Column( - children: [ - _buildMainSummaryCard(colorScheme, context.ap, analysis), - const SizedBox(height: 16), - ScorePRCard(analysis: analysis), - const SizedBox(height: 16), - ScoreStatisticsCard(analysis: analysis), - const SizedBox(height: 16), - ScoreDistributionCard(analysis: analysis), - const SizedBox(height: 16), - ScoreCreditSummaryCard(analysis: analysis), - const SizedBox(height: 32), - ], - ), - ), - ); - } - - Widget _buildMainSummaryCard( - ColorScheme colorScheme, - ApLocalizations ap, - ScoreAnalysis analysis, - ) { - final Detail detail = scoreData.detail; - - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - colorScheme.primaryContainer, - colorScheme.secondaryContainer, - ], - ), - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: colorScheme.primary.withAlpha(38), - blurRadius: 16, - offset: const Offset(0, 8), - ), - ], - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(20), - child: Row( - children: [ - Expanded( - child: _buildMainItem( - colorScheme, - Icons.star_rounded, - ap.average, - detail.average?.toStringAsFixed(2) ?? '-', - ), - ), - Container( - width: 1, - height: 60, - color: colorScheme.onPrimaryContainer.withAlpha(51), - ), - Expanded( - child: _buildMainItem( - colorScheme, - Icons.school_rounded, - ap.conductScore, - detail.conduct?.toStringAsFixed(0) ?? '-', - ), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - decoration: BoxDecoration( - color: colorScheme.surface.withAlpha(179), - borderRadius: const BorderRadius.vertical( - bottom: Radius.circular(20), - ), - ), - child: Row( - children: [ - Expanded( - child: _buildRankItem( - colorScheme, - ap.classRank, - detail.classRank ?? '-', - ), - ), - Container( - width: 1, - height: 40, - color: colorScheme.outlineVariant.withAlpha(128), - ), - Expanded( - child: _buildRankItem( - colorScheme, - ap.departmentRank, - detail.departmentRank ?? '-', - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildMainItem( - ColorScheme colorScheme, - IconData icon, - String label, - String value, - ) { - return Column( - children: [ - Icon(icon, size: 28, color: colorScheme.primary), - const SizedBox(height: 8), - Text( - value, - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: colorScheme.onPrimaryContainer, - ), - ), - const SizedBox(height: 4), - Text( - label, - style: TextStyle( - fontSize: 13, - color: colorScheme.onPrimaryContainer.withAlpha(179), - ), - ), - ], - ); - } - - Widget _buildRankItem(ColorScheme colorScheme, String label, String value) { - return Column( - children: [ - Text( - value, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, - ), - ), - const SizedBox(height: 2), - Text( - label, - style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant), - ), - ], - ); - } -} - class ScoreAnalysis { ScoreAnalysis(this.scoreData) { _scores = []; diff --git a/packages/ap_common_flutter_ui/lib/src/widgets/course_table_view.dart b/packages/ap_common_flutter_ui/lib/src/widgets/course_table_view.dart new file mode 100644 index 00000000..6f917f84 --- /dev/null +++ b/packages/ap_common_flutter_ui/lib/src/widgets/course_table_view.dart @@ -0,0 +1,508 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; + +/// The display style for course content. +enum ContentStyle { list, table } + +/// Height of each course time slot cell. +const double courseHeight = 64.0; + +/// Builds a lookup map from (weekday, timeIndex) to [Course]. +/// +/// Returns a nested map where the outer key is the weekday number +/// and the inner key is the time slot index. +Map> buildCourseLookup( + List courses, +) { + final Map> lookup = + >{}; + for (final Course course in courses) { + for (final SectionTime time in course.times) { + lookup + .putIfAbsent( + time.weekday, + () => {}, + )[time.index] = course; + } + } + return lookup; +} + +/// A widget that renders a course timetable grid. +/// +/// This widget encapsulates all the grid rendering logic for +/// displaying courses in a table layout, including weekday headers, +/// time slot columns, and course cards. +class CourseTableView extends StatelessWidget { + /// Creates a [CourseTableView]. + const CourseTableView({ + super.key, + required this.courseData, + required this.invisibleCourseCodes, + this.controller, + this.onCoursePressed, + required this.courseColorResolver, + this.repaintBoundaryKey, + this.mergeCourse, + this.showSectionTime, + this.showInstructors, + this.showClassroomLocation, + }); + + /// The course data to display. + final CourseData courseData; + + /// Course codes that should be hidden in the grid. + final List invisibleCourseCodes; + + /// Optional scroll controller for the grid. + final ScrollController? controller; + + /// Callback when a course cell is tapped. + final void Function( + Course course, + TimeCode timeCode, + int weekday, + )? onCoursePressed; + + /// Resolves a course code to its display color. + final Color Function(String courseCode) courseColorResolver; + + /// Key for the [RepaintBoundary] used for screenshot capture. + final GlobalKey? repaintBoundaryKey; + + /// Whether to merge adjacent identical course cells. + final bool? mergeCourse; + + /// Whether to show time information in time slot cells. + final bool? showSectionTime; + + /// Whether to show instructor names on course cards. + final bool? showInstructors; + + /// Whether to show classroom location on course cards. + final bool? showClassroomLocation; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final int weekdayCount = courseData.hasHoliday ? 7 : 5; + final Map> courseLookup = + buildCourseLookup(courseData.courses); + return SingleChildScrollView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only( + top: 8.0, + bottom: 80.0, + ), + child: RepaintBoundary( + key: repaintBoundaryKey, + child: ColoredBox( + color: Theme.of(context).scaffoldBackgroundColor, + child: Column( + children: [ + _buildWeekdayHeader( + context, + colorScheme, + weekdayCount, + ), + _buildCourseGrid( + context, + colorScheme, + weekdayCount, + courseData.timeCodes, + courseLookup, + ), + ], + ), + ), + ), + ); + } + + Widget _buildWeekdayHeader( + BuildContext context, + ColorScheme colorScheme, + int weekdayCount, + ) { + return Container( + decoration: BoxDecoration( + color: colorScheme.primaryContainer.withAlpha(77), + border: Border( + bottom: BorderSide( + color: + colorScheme.outlineVariant.withAlpha(128), + ), + ), + ), + child: Row( + children: [ + _buildTimeSlotHeader(colorScheme), + for (int i = 0; i < weekdayCount; i++) + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: Text( + context.ap.weekdaysCourse[i], + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colorScheme.primary, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildTimeSlotHeader(ColorScheme colorScheme) { + return Container( + width: 48, + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + '', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: colorScheme.primary, + ), + ), + ); + } + + Widget _buildCourseGrid( + BuildContext context, + ColorScheme colorScheme, + int weekdayCount, + List timeCodes, + Map> courseLookup, + ) { + final int minIndex = courseData.minTimeCodeIndex; + final int maxIndex = courseData.maxTimeCodeIndex; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTimeColumn( + colorScheme, + timeCodes, + minIndex, + maxIndex, + ), + for (int weekday = 1; + weekday <= weekdayCount; + weekday++) + Expanded( + child: _buildWeekdayColumn( + colorScheme, + weekday, + weekdayCount, + minIndex, + maxIndex, + courseLookup, + ), + ), + ], + ); + } + + Widget _buildTimeColumn( + ColorScheme colorScheme, + List timeCodes, + int minIndex, + int maxIndex, + ) { + return Column( + children: [ + for (int i = minIndex; i <= maxIndex; i++) + _buildTimeSlot( + colorScheme, + i < timeCodes.length ? timeCodes[i] : null, + i, + i == maxIndex, + ), + ], + ); + } + + Widget _buildWeekdayColumn( + ColorScheme colorScheme, + int weekday, + int weekdayCount, + int minIndex, + int maxIndex, + Map> courseLookup, + ) { + final List children = []; + for (int i = minIndex; i <= maxIndex; i++) { + final Course? course = + courseLookup[weekday]?[i]; + final bool isInvisible = course != null && + invisibleCourseCodes.contains(course.code); + + if (course == null || isInvisible) { + children.add( + _buildEmptyCell( + colorScheme, + weekday < weekdayCount, + i == maxIndex, + ), + ); + } else { + int span = 1; + if (mergeCourse ?? true) { + while (i + span <= maxIndex && + courseLookup[weekday]?[i + span] == + course) { + span++; + } + } + children.add( + _buildCourseCell( + colorScheme, + weekday, + i, + course, + span, + weekday < weekdayCount, + i + span - 1 == maxIndex, + ), + ); + i += span - 1; + } + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ); + } + + Widget _buildTimeSlot( + ColorScheme colorScheme, + TimeCode? timeCode, + int timeIndex, + bool isLast, + ) { + return Container( + width: 48, + height: courseHeight, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 4, + ), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest + .withAlpha(77), + border: Border( + right: BorderSide( + color: + colorScheme.outlineVariant.withAlpha(128), + ), + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + timeCode?.title ?? '${timeIndex + 1}', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: colorScheme.onSurfaceVariant, + ), + ), + if (timeCode != null && + (showSectionTime ?? true)) ...[ + const SizedBox(height: 2), + Text( + timeCode.startTime, + style: TextStyle( + fontSize: 9, + color: colorScheme.onSurfaceVariant + .withAlpha(179), + ), + ), + ], + ], + ), + ); + } + + Widget _buildEmptyCell( + ColorScheme colorScheme, + bool showRightBorder, + bool isLast, + ) { + return Container( + height: courseHeight, + decoration: BoxDecoration( + border: Border( + right: showRightBorder + ? BorderSide( + color: colorScheme.outlineVariant + .withAlpha(51), + width: 0.5, + ) + : BorderSide.none, + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + ); + } + + Widget _buildCourseCell( + ColorScheme colorScheme, + int weekday, + int timeIndex, + Course course, + int span, + bool showRightBorder, + bool isLast, + ) { + return Container( + height: courseHeight * span, + decoration: BoxDecoration( + border: Border( + right: showRightBorder + ? BorderSide( + color: colorScheme.outlineVariant + .withAlpha(51), + width: 0.5, + ) + : BorderSide.none, + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + child: _buildCourseCard( + colorScheme, + weekday, + timeIndex, + course, + span, + ), + ); + } + + Widget _buildCourseCard( + ColorScheme colorScheme, + int weekday, + int timeIndex, + Course course, + int span, + ) { + final Color courseColor = + courseColorResolver(course.code); + final String locationInfo = + (showClassroomLocation ?? true) && + course.location != null + ? course.location.toString() + : ''; + final String instructorInfo = + (showInstructors ?? true) + ? course.getInstructors() + : ''; + + final Color onCourseColor = + ThemeData.estimateBrightnessForColor(courseColor) == + Brightness.dark + ? Colors.white + : Colors.black; + + final String displayInfo = [ + if (instructorInfo.isNotEmpty) instructorInfo, + if (locationInfo.isNotEmpty) locationInfo, + ].join('\n'); + + return GestureDetector( + onTap: () { + final TimeCode timeCode = + timeIndex < courseData.timeCodes.length + ? courseData.timeCodes[timeIndex] + : const TimeCode( + title: '?', + startTime: '?', + endTime: '?', + ); + onCoursePressed?.call(course, timeCode, weekday); + }, + child: Container( + width: double.infinity, + height: double.infinity, + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 6, + ), + decoration: BoxDecoration( + color: courseColor.withAlpha(230), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: courseColor.withAlpha(77), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + course.title, + textAlign: TextAlign.center, + maxLines: span > 1 ? 4 : 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: span > 1 ? 14 : 9.5, + fontWeight: FontWeight.w600, + color: onCourseColor, + height: 1.1, + ), + ), + if (displayInfo.isNotEmpty) ...[ + SizedBox(height: span > 1 ? 4 : 2), + Text( + displayInfo, + textAlign: TextAlign.center, + maxLines: span > 1 ? 4 : 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: span > 1 ? 12 : 8.5, + color: onCourseColor.withAlpha(217), + height: 1.0, + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/packages/ap_common_flutter_ui/lib/src/widgets/score_tab_widgets.dart b/packages/ap_common_flutter_ui/lib/src/widgets/score_tab_widgets.dart new file mode 100644 index 00000000..fa5cd9b7 --- /dev/null +++ b/packages/ap_common_flutter_ui/lib/src/widgets/score_tab_widgets.dart @@ -0,0 +1,402 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; + +class ScoreListTab extends StatelessWidget { + const ScoreListTab({ + super.key, + required this.scoreData, + this.onRefresh, + this.middleTitle, + this.finalTitle, + this.onScoreSelect, + this.middleScoreBuilder, + this.finalScoreBuilder, + this.controller, + }); + + final ScoreData scoreData; + final VoidCallback? onRefresh; + final String? middleTitle; + final String? finalTitle; + final Function(int index)? onScoreSelect; + final Widget Function(int index)? middleScoreBuilder; + final Widget Function(int index)? finalScoreBuilder; + final ScrollController? controller; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = Theme.of(context).colorScheme; + + return RefreshIndicator( + onRefresh: () async => onRefresh?.call(), + child: ListView.builder( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: scoreData.scores.length, + itemBuilder: (BuildContext context, int index) { + return _buildScoreItem( + colorScheme, + scoreData.scores[index], + index, + ); + }, + ), + ); + } + + Widget _buildScoreItem( + ColorScheme colorScheme, + Score score, + int index, + ) { + final String scoreStr = score.semesterScore ?? ''; + final double? scoreValue = double.tryParse(scoreStr); + final bool isPassed = scoreValue != null && scoreValue >= 60; + final Color scoreColor = scoreValue == null + ? colorScheme.onSurfaceVariant + : isPassed + ? _getScoreColor(scoreValue) + : colorScheme.error; + + return GestureDetector( + onTap: onScoreSelect == null + ? null + : () { + onScoreSelect!(index); + AnalyticsUtil.instance + .logEvent('score_title_click'); + }, + child: Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerLowest, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: colorScheme.outlineVariant.withAlpha(77), + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + width: 4, + height: 50, + decoration: BoxDecoration( + color: scoreColor, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + score.title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + _buildTag( + colorScheme, + score.required ?? '', + colorScheme.tertiary, + ), + const SizedBox(width: 8), + Text( + '${score.units} 學分', + style: TextStyle( + fontSize: 12, + color: + colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (finalScoreBuilder == null) + Text( + score.semesterScore ?? '-', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: scoreColor, + ), + ), + if (finalScoreBuilder != null) + finalScoreBuilder!(index), + const SizedBox(height: 4), + if (middleScoreBuilder == null && + score.middleScore != null && + score.middleScore!.isNotEmpty) + Text( + '期中: ${score.middleScore}', + style: TextStyle( + fontSize: 11, + color: colorScheme.onSurfaceVariant, + ), + ), + if (middleScoreBuilder != null) + middleScoreBuilder!(index), + ], + ), + ], + ), + ), + ), + ); + } + + Color _getScoreColor(double score) { + if (score >= 90) return const Color(0xFF4CAF50); + if (score >= 80) return const Color(0xFF8BC34A); + if (score >= 70) return const Color(0xFF2196F3); + if (score >= 60) return const Color(0xFFFF9800); + return const Color(0xFFF44336); + } + + Widget _buildTag( + ColorScheme colorScheme, + String text, + Color color, + ) { + if (text.isEmpty) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: color.withAlpha(26), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + text.replaceAll('【', '').replaceAll('】', ''), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ); + } +} + +class ScoreAnalysisTab extends StatelessWidget { + const ScoreAnalysisTab({ + super.key, + required this.scoreData, + this.onRefresh, + this.controller, + }); + + final ScoreData scoreData; + final VoidCallback? onRefresh; + final ScrollController? controller; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final ScoreAnalysis analysis = ScoreAnalysis(scoreData); + + return RefreshIndicator( + onRefresh: () async => onRefresh?.call(), + child: SingleChildScrollView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildMainSummaryCard( + colorScheme, + context.ap, + analysis, + ), + const SizedBox(height: 16), + ScorePRCard(analysis: analysis), + const SizedBox(height: 16), + ScoreStatisticsCard(analysis: analysis), + const SizedBox(height: 16), + ScoreDistributionCard(analysis: analysis), + const SizedBox(height: 16), + ScoreCreditSummaryCard(analysis: analysis), + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildMainSummaryCard( + ColorScheme colorScheme, + ApLocalizations ap, + ScoreAnalysis analysis, + ) { + final Detail detail = scoreData.detail; + + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primaryContainer, + colorScheme.secondaryContainer, + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withAlpha(38), + blurRadius: 16, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Expanded( + child: _buildMainItem( + colorScheme, + Icons.star_rounded, + ap.average, + detail.average?.toStringAsFixed(2) ?? + '-', + ), + ), + Container( + width: 1, + height: 60, + color: colorScheme.onPrimaryContainer + .withAlpha(51), + ), + Expanded( + child: _buildMainItem( + colorScheme, + Icons.school_rounded, + ap.conductScore, + detail.conduct?.toStringAsFixed(0) ?? + '-', + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + decoration: BoxDecoration( + color: colorScheme.surface.withAlpha(179), + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(20), + ), + ), + child: Row( + children: [ + Expanded( + child: _buildRankItem( + colorScheme, + ap.classRank, + detail.classRank ?? '-', + ), + ), + Container( + width: 1, + height: 40, + color: colorScheme.outlineVariant + .withAlpha(128), + ), + Expanded( + child: _buildRankItem( + colorScheme, + ap.departmentRank, + detail.departmentRank ?? '-', + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildMainItem( + ColorScheme colorScheme, + IconData icon, + String label, + String value, + ) { + return Column( + children: [ + Icon(icon, size: 28, color: colorScheme.primary), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 13, + color: colorScheme.onPrimaryContainer + .withAlpha(179), + ), + ), + ], + ); + } + + Widget _buildRankItem( + ColorScheme colorScheme, + String label, + String value, + ) { + return Column( + children: [ + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ); + } +} From 977b349ef7bded5acfc1c9210602a33fcf4777d2 Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Sun, 5 Apr 2026 15:54:02 +0800 Subject: [PATCH 06/14] feat(liquid_glass): add glass body widgets for full Liquid Glass styling Create GlassCard-based versions of all body content widgets: - GlassScoreListTab, GlassScoreAnalysisTab, 4 glass analysis cards - GlassCourseTableView, GlassCourseList, GlassCourseContent Rewrite GlassCourseScaffold and GlassScoreScaffold to use glass body widgets instead of delegating to Material originals. --- .../lib/ap_common_liquid_glass.dart | 19 +- .../src/scaffold/glass_course_scaffold.dart | 637 +++++++++++++++++- .../src/scaffold/glass_score_scaffold.dart | 359 +++++++++- .../lib/src/widgets/glass_course_content.dart | 497 ++++++++++++++ .../lib/src/widgets/glass_course_list.dart | 224 ++++++ .../src/widgets/glass_course_table_view.dart | 497 ++++++++++++++ .../widgets/glass_score_analysis_cards.dart | 627 +++++++++++++++++ .../src/widgets/glass_score_analysis_tab.dart | 215 ++++++ .../lib/src/widgets/glass_score_list_tab.dart | 205 ++++++ 9 files changed, 3209 insertions(+), 71 deletions(-) create mode 100644 packages/ap_common_liquid_glass/lib/src/widgets/glass_course_content.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/widgets/glass_course_list.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/widgets/glass_course_table_view.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_cards.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_tab.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/widgets/glass_score_list_tab.dart diff --git a/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart b/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart index 1790369a..706dd7ae 100644 --- a/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart +++ b/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart @@ -25,23 +25,22 @@ library ap_common_liquid_glass; export 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; export 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; -// Core export 'src/glass_theme_bridge.dart'; export 'src/liquid_glass_app.dart'; - -// Scaffolds +export 'src/pages/glass_ap_course_page.dart'; +export 'src/pages/glass_ap_login_page.dart'; +export 'src/pages/glass_ap_score_page.dart'; export 'src/scaffold/glass_course_scaffold.dart'; export 'src/scaffold/glass_home_page_scaffold.dart'; export 'src/scaffold/glass_image_viewer_scaffold.dart'; export 'src/scaffold/glass_login_scaffold.dart'; export 'src/scaffold/glass_score_scaffold.dart'; export 'src/scaffold/glass_user_info_scaffold.dart'; - -// Widgets export 'src/widgets/glass_ap_drawer.dart'; +export 'src/widgets/glass_course_content.dart'; +export 'src/widgets/glass_course_list.dart'; +export 'src/widgets/glass_course_table_view.dart'; export 'src/widgets/glass_dialog.dart'; - -// Pages -export 'src/pages/glass_ap_course_page.dart'; -export 'src/pages/glass_ap_login_page.dart'; -export 'src/pages/glass_ap_score_page.dart'; +export 'src/widgets/glass_score_analysis_cards.dart'; +export 'src/widgets/glass_score_analysis_tab.dart'; +export 'src/widgets/glass_score_list_tab.dart'; diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart index 488003ce..2f6e2115 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart @@ -1,19 +1,24 @@ +import 'dart:ui' as ui; + import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_course_content.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_course_list.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_course_table_view.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +const String _kCourseInvisibleKey = + '${ApConstants.packageName}.course_invisible_'; /// A glass-enhanced version of [CourseScaffold]. /// -/// Currently delegates to the original [CourseScaffold] while the -/// app tree is wrapped in [LiquidGlassWidgets.wrap] via -/// [LiquidGlassApApp]. The glass theme bridge ensures glass widgets -/// used within the scaffold pick up the correct tint colors. -/// -/// Course scaffold has deeply integrated state management (course -/// table rendering, notification control, screenshot capture) that -/// makes partial widget replacement impractical. Instead, this -/// wrapper ensures the scaffold operates correctly within a -/// Liquid Glass context. -class GlassCourseScaffold extends StatelessWidget { +/// Replaces the Material [AppBar] with [GlassAppBar] and the +/// [FloatingActionButton]s with [GlassButton] widgets while +/// reusing the body content widgets ([CourseTableView], +/// [CourseList], etc.) from the original scaffold. +class GlassCourseScaffold extends StatefulWidget { const GlassCourseScaffold({ super.key, required this.state, @@ -107,33 +112,599 @@ class GlassCourseScaffold extends StatelessWidget { final bool? showClassroomLocation; final bool? showSearchButton; + @override + GlassCourseScaffoldState createState() => + GlassCourseScaffoldState(); +} + +class GlassCourseScaffoldState + extends State { + final GlobalKey _repaintBoundaryGlobalKey = GlobalKey(); + + ContentStyle _contentStyle = ContentStyle.table; + + bool? showSectionTime; + bool? showInstructors; + bool? showClassroomLocation; + bool? showSearchButton; + bool? mergeCourse; + + bool get isLandscape => + MediaQuery.of(context).orientation == + Orientation.landscape; + + List invisibleCourseCodes = []; + + final Map _courseColorMap = + {}; + int _colorIndex = 0; + + Color _getCourseColor(String courseCode) { + if (!_courseColorMap.containsKey(courseCode)) { + _courseColorMap[courseCode] = + courseColors[_colorIndex % courseColors.length]; + _colorIndex++; + } + return _courseColorMap[courseCode]!; + } + + late ScrollController _scrollController; + bool _showFab = true; + + @override + void initState() { + showSectionTime = widget.showSectionTime ?? + PreferenceUtil.instance.getBool( + ApConstants.showSectionTime, + true, + ); + showInstructors = widget.showInstructors ?? + PreferenceUtil.instance.getBool( + ApConstants.showInstructors, + true, + ); + showClassroomLocation = + widget.showClassroomLocation ?? + PreferenceUtil.instance.getBool( + ApConstants.showClassroomLocation, + true, + ); + showSearchButton = widget.showSearchButton ?? + PreferenceUtil.instance.getBool( + ApConstants.showCourseSearchButton, + true, + ); + mergeCourse = PreferenceUtil.instance.getBool( + '${ApConstants.packageName}.merge_course', + true, + ); + fetchInvisibleCourseCodes(); + _scrollController = ScrollController(); + _scrollController.addListener(() { + if (_scrollController.position.userScrollDirection == + ScrollDirection.reverse) { + if (_showFab) { + setState(() => _showFab = false); + } + } else if (_scrollController + .position.userScrollDirection == + ScrollDirection.forward) { + if (!_showFab) { + setState(() => _showFab = true); + } + } + }); + super.initState(); + } + + @override + void didUpdateWidget( + covariant GlassCourseScaffold oldWidget, + ) { + fetchInvisibleCourseCodes(); + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return CourseScaffold( - state: state, - courseData: courseData, - title: title, - customHint: customHint, - semesterData: semesterData, - onSelect: onSelect, - onRefresh: onRefresh, - actions: actions, - enableNotifyControl: enableNotifyControl, - notifyData: notifyData, - autoNotifySave: autoNotifySave, - onNotifyClick: onNotifyClick, - courseNotifySaveKey: courseNotifySaveKey, - customStateHint: customStateHint, - itemPicker: itemPicker, - onSearchButtonClick: onSearchButtonClick, - enableAddToCalendar: enableAddToCalendar, - androidResourceIcon: androidResourceIcon, - enableCaptureCourseTable: - enableCaptureCourseTable, + return AdaptiveLiquidGlassLayer( + child: CourseConfig( showSectionTime: showSectionTime, showInstructors: showInstructors, showClassroomLocation: showClassroomLocation, - showSearchButton: showSearchButton, + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: GlassAppBar( + title: Row( + children: [ + Flexible( + child: Text( + widget.title ?? context.ap.course, + overflow: TextOverflow.ellipsis, + ), + ), + if (widget.itemPicker != + null) ...[ + const SizedBox(width: 8), + widget.itemPicker!, + ], + if (widget.semesterData != null && + widget.itemPicker == + null) ...[ + const SizedBox(width: 8), + SemesterPicker( + semesterData: + widget.semesterData!, + currentIndex: widget + .semesterData!.currentIndex, + onSelect: ( + Semester semester, + int index, + ) { + widget.onSelect?.call(index); + }, + featureTag: 'course', + ), + ], + ], + ), + actions: [ + ...widget.actions ?? [], + if (widget.enableCaptureCourseTable) + IconButton( + icon: Icon(ApIcon.download), + onPressed: _captureCourseTable, + tooltip: + context.ap.exportCourseTable, + ), + IconButton( + icon: Icon(ApIcon.settings), + onPressed: _openSettings, + tooltip: + context.ap.courseScaffoldSetting, + ), + ], + ), + body: Row( + children: [ + Expanded( + flex: 3, + child: Flex( + direction: Axis.vertical, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + if (widget.customHint != null && + widget.customHint!.isNotEmpty) + HintBanner( + text: widget.customHint!, + ), + Expanded( + child: RefreshIndicator( + onRefresh: () async { + await widget.onRefresh!(); + AnalyticsUtil.instance + .logEvent( + 'course_refresh', + ); + return; + }, + child: _body(), + ), + ), + ], + ), + ), + if (widget.state == + CourseState.finish && + isLandscape) ...[ + const SizedBox(width: 16.0), + Expanded( + flex: 2, + child: GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: GlassCourseList( + courses: + widget.courseData.courses, + timeCodes: + widget.courseData.timeCodes, + invisibleCourseCodes: + invisibleCourseCodes, + onVisibilityChanged: ( + Course course, + bool visibility, + ) => + saveInvisibleCourseCodes( + course: course, + visibility: visibility, + ), + ), + ), + ), + ], + ], + ), + floatingActionButton: AnimatedScale( + scale: _showFab ? 1.0 : 0.0, + duration: + const Duration(milliseconds: 250), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + if (!isLandscape) + GlassButton( + key: const ValueKey( + 'switch_content_style_button', + ), + icon: Icon( + _contentStyle == + ContentStyle.table + ? Icons.list_rounded + : Icons.grid_view_rounded, + ), + onTap: () { + setState( + () => _contentStyle = + (_contentStyle == + ContentStyle.table) + ? ContentStyle.list + : ContentStyle.table, + ); + }, + useOwnLayer: true, + ), + if (showSearchButton ?? + true) ...[ + if (!isLandscape) + const SizedBox(height: 8), + GlassButton( + key: const ValueKey( + 'search_button', + ), + icon: const Icon(Icons.search), + onTap: () { + _pickSemester(); + AnalyticsUtil.instance.logEvent( + 'course_search_button_click', + ); + }, + useOwnLayer: true, + ), + ], + ], + ), + ), + ), + ), + ); + } + + Widget _buildErrorState( + ColorScheme colorScheme, + String message, + IconData icon, + ) { + return InkWell( + onTap: () { + if (widget.state == CourseState.empty) { + _pickSemester(); + } else { + widget.onRefresh?.call(); + } + }, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GlassCard( + useOwnLayer: true, + width: 80, + height: 80, + padding: EdgeInsets.zero, + shape: + const LiquidRoundedSuperellipse( + borderRadius: 20, + ), + child: Center( + child: Icon( + icon, + size: 40, + color: colorScheme.primary, + ), + ), + ), + const SizedBox(height: 16), + Text( + message, + style: TextStyle( + fontSize: 16, + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + context.ap.clickToRetry, + style: TextStyle( + fontSize: 14, + color: colorScheme.primary, + ), + ), + ], + ), + ), + ); + } + + Widget _body() { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + switch (widget.state) { + case CourseState.loading: + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 48, + height: 48, + child: CircularProgressIndicator( + strokeWidth: 3, + color: colorScheme.primary, + ), + ), + ], + ), + ); + case CourseState.error: + return _buildErrorState( + colorScheme, + context.ap.clickToRetry, + Icons.error_outline_rounded, + ); + case CourseState.empty: + return _buildErrorState( + colorScheme, + context.ap.courseEmpty, + Icons.event_busy_rounded, + ); + case CourseState.offlineEmpty: + return _buildErrorState( + colorScheme, + context.ap.noOfflineData, + Icons.cloud_off_rounded, + ); + case CourseState.custom: + return _buildErrorState( + colorScheme, + widget.customStateHint ?? + context.ap.unknownError, + Icons.warning_amber_rounded, + ); + default: + if (isLandscape || + _contentStyle == ContentStyle.table) { + return GlassCourseTableView( + courseData: widget.courseData, + invisibleCourseCodes: + invisibleCourseCodes, + controller: _scrollController, + onCoursePressed: ( + Course course, + TimeCode timeCode, + int weekday, + ) => + _onPressed(weekday, timeCode, course), + courseColorResolver: _getCourseColor, + repaintBoundaryKey: + _repaintBoundaryGlobalKey, + mergeCourse: mergeCourse, + showSectionTime: showSectionTime, + showInstructors: showInstructors, + showClassroomLocation: + showClassroomLocation, + ); + } else { + return GlassCourseList( + controller: _scrollController, + courses: widget.courseData.courses, + timeCodes: widget.courseData.timeCodes, + invisibleCourseCodes: + invisibleCourseCodes, + onVisibilityChanged: ( + Course course, + bool visibility, + ) => + saveInvisibleCourseCodes( + course: course, + visibility: visibility, + ), + ); + } + } + } + + Future _captureCourseTable() async { + final RenderRepaintBoundary? boundary = + _repaintBoundaryGlobalKey.currentContext! + .findRenderObject() + as RenderRepaintBoundary?; + if (boundary == null) { + UiUtil.instance.showToast( + context, + context.ap.unknownError, + ); + return; + } + final ui.Image image = + await boundary.toImage(pixelRatio: 3.0); + final ByteData? byteData = await image.toByteData( + format: ui.ImageByteFormat.png, + ); + final DateTime now = DateTime.now(); + final String formattedDate = + DateFormat('yyyyMMdd_hhmmss').format(now); + if (byteData != null) { + final SaveImageResult result = + await MediaUtil.instance.saveImage( + byteData: byteData, + fileName: 'course_table_$formattedDate', + ); + if (!mounted) return; + switch (result) { + case SaveImageSuccess(:final String filePath): + final String message = + '${context.ap.exportCourseTableSuccess}' + '\n$filePath'; + Toast.show(message, context); + AnalyticsUtil.instance.logEvent( + 'export_course_table_image_success', + ); + case SaveImageError(:final String message): + UiUtil.instance.showToast(context, message); + } + } else { + if (!mounted) return; + UiUtil.instance.showToast( + context, + context.ap.unknownError, + ); + } + } + + void _onPressed( + int weekday, + TimeCode timeCode, + Course course, + ) { + showModalBottomSheet( + context: context, + backgroundColor: const Color(0x00000000), + isScrollControlled: true, + builder: (BuildContext builder) { + return GlassCourseContent( + enableNotifyControl: + widget.enableNotifyControl, + course: course, + notifyData: widget.notifyData, + weekday: weekday, + courseNotifySaveKey: + widget.courseNotifySaveKey, + timeCode: timeCode, + courseColor: _getCourseColor(course.code), + invisibleCourseCodes: invisibleCourseCodes, + onVisibilityChanged: (bool visibility) => + saveInvisibleCourseCodes( + course: course, + visibility: visibility, + ), + ); + }, + ); + } + + void _pickSemester() { + if (widget.semesterData != null) { + SemesterPicker.show( + context: context, + semesterData: widget.semesterData!, + currentIndex: + widget.semesterData!.currentIndex, + onSelect: (Semester semester, int index) { + widget.onSelect?.call(index); + }, + ); + } + widget.onSearchButtonClick?.call(); + } + + void _openSettings() { + showDialog( + context: context, + builder: (_) => CourseScaffoldSettingDialog( + showSectionTime: showSectionTime, + showInstructors: showInstructors, + showClassroomLocation: showClassroomLocation, + showSearchButton: showSearchButton, + mergeCourse: mergeCourse, + showSectionTimeOnChanged: (bool? value) { + setState(() => showSectionTime = value); + PreferenceUtil.instance.setBool( + ApConstants.showSectionTime, + showSectionTime!, + ); + }, + showInstructorsOnChanged: (bool? value) { + setState(() => showInstructors = value); + PreferenceUtil.instance.setBool( + ApConstants.showInstructors, + showInstructors!, + ); + }, + showClassroomLocationOnChanged: + (bool? value) { + setState( + () => showClassroomLocation = value, + ); + PreferenceUtil.instance.setBool( + ApConstants.showClassroomLocation, + showClassroomLocation!, + ); + }, + showSearchButtonOnChanged: (bool? value) { + setState(() => showSearchButton = value); + PreferenceUtil.instance.setBool( + ApConstants.showCourseSearchButton, + showSearchButton!, + ); + }, + mergeCourseOnChanged: (bool? value) { + setState(() => mergeCourse = value); + PreferenceUtil.instance.setBool( + '${ApConstants.packageName}.merge_course', + mergeCourse!, + ); + }, + ), + ); + AnalyticsUtil.instance + .logEvent('course_setting_click'); + } + + void saveInvisibleCourseCodes({ + required Course course, + required bool visibility, + }) { + if (visibility) { + invisibleCourseCodes.remove(course.code); + } else { + invisibleCourseCodes.add(course.code); + } + PreferenceUtil.instance.setStringList( + '$_kCourseInvisibleKey' + '${widget.courseNotifySaveKey}', + invisibleCourseCodes, + ); + setState(() {}); + } + + void fetchInvisibleCourseCodes() { + invisibleCourseCodes = + PreferenceUtil.instance.getStringList( + '$_kCourseInvisibleKey' + '${widget.courseNotifySaveKey}', + [], ); + setState(() {}); } } diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart index c68c5058..2af49afd 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart @@ -1,18 +1,17 @@ import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_score_analysis_tab.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_score_list_tab.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; /// A glass-enhanced version of [ScoreScaffold]. /// -/// Currently delegates to the original [ScoreScaffold] while the -/// app tree is wrapped in [LiquidGlassWidgets.wrap] via -/// [LiquidGlassApApp]. The glass theme bridge ensures glass widgets -/// used within the scaffold pick up the correct tint colors. -/// -/// Score scaffold has deeply integrated state management (analysis -/// views, scroll-aware FABs, responsive layouts) that makes partial -/// widget replacement impractical. Instead, this wrapper ensures the -/// scaffold operates correctly within a Liquid Glass context. -class GlassScoreScaffold extends StatelessWidget { +/// Replaces the Material [AppBar] with [GlassAppBar] and the +/// [FloatingActionButton]s with [GlassButton] while preserving +/// all original functionality (analysis view toggle, scroll-aware +/// FAB visibility, responsive landscape layout, etc.). +class GlassScoreScaffold extends StatefulWidget { const GlassScoreScaffold({ super.key, required this.state, @@ -87,26 +86,330 @@ class GlassScoreScaffold extends StatelessWidget { final String? customHint; final Widget? bottom; + @override + GlassScoreScaffoldState createState() => + GlassScoreScaffoldState(); +} + +class GlassScoreScaffoldState + extends State { + bool get isLandscape => + MediaQuery.of(context).orientation == + Orientation.landscape; + + bool _isAnalysisView = false; + late ScrollController _scrollController; + bool _showFab = true; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _scrollController.addListener(() { + if (_scrollController.position.userScrollDirection == + ScrollDirection.reverse) { + if (_showFab) { + setState(() => _showFab = false); + } + } else if (_scrollController + .position + .userScrollDirection == + ScrollDirection.forward) { + if (!_showFab) { + setState(() => _showFab = true); + } + } + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return ScoreScaffold( - state: state, - scoreData: scoreData, - onRefresh: onRefresh, - title: title, - itemPicker: itemPicker, - semesterData: semesterData, - onSelect: onSelect, - onSearchButtonClick: onSearchButtonClick, - middleTitle: middleTitle, - finalTitle: finalTitle, - onScoreSelect: onScoreSelect, - middleScoreBuilder: middleScoreBuilder, - finalScoreBuilder: finalScoreBuilder, - customHint: customHint, - isShowSearchButton: isShowSearchButton, - bottom: bottom, - customStateHint: customStateHint, + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return AdaptiveLiquidGlassLayer( + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: GlassAppBar( + title: Row( + children: [ + Flexible( + child: Text( + widget.title ?? context.ap.score, + overflow: TextOverflow.ellipsis, + ), + ), + if (widget.itemPicker != null) ...[ + const SizedBox(width: 12), + widget.itemPicker!, + ], + if (widget.semesterData != null && + widget.itemPicker == + null) ...[ + const SizedBox(width: 12), + SemesterPicker( + semesterData: widget.semesterData!, + currentIndex: + widget.semesterData!.currentIndex, + onSelect: + (Semester semester, int index) { + widget.onSelect?.call(index); + }, + featureTag: 'score', + ), + ], + ], + ), + actions: const [], + ), + floatingActionButton: AnimatedScale( + scale: _showFab ? 1.0 : 0.0, + duration: const Duration(milliseconds: 250), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (widget.state == ScoreState.finish && + widget.scoreData != null && + widget.scoreData!.scores.isNotEmpty && + !isLandscape) + GlassButton( + key: const ValueKey( + 'switch_view_button', + ), + icon: Icon( + _isAnalysisView + ? Icons.list_alt_rounded + : Icons.analytics_outlined, + ), + onTap: () { + setState( + () => _isAnalysisView = + !_isAnalysisView, + ); + }, + ), + if (widget + .isShowSearchButton) ...[ + if (widget.state == ScoreState.finish && + widget.scoreData != null && + widget.scoreData!.scores.isNotEmpty && + !isLandscape) + const SizedBox(height: 8), + GlassButton( + key: const ValueKey( + 'search_button', + ), + icon: const Icon(Icons.search), + onTap: () { + _pickSemester(); + AnalyticsUtil.instance.logEvent( + 'score_search_button_click', + ); + }, + ), + ], + ], + ), + ), + body: Row( + children: [ + Expanded( + flex: 3, + child: Column( + children: [ + if (widget.customHint != null && + widget.customHint!.isNotEmpty) + HintBanner(text: widget.customHint!), + Expanded( + child: _buildContent( + context, + colorScheme, + ), + ), + ], + ), + ), + if (widget.state == ScoreState.finish && + isLandscape) ...[ + const SizedBox(width: 16.0), + Expanded( + flex: 2, + child: GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: GlassScoreListTab( + scoreData: widget.scoreData!, + onRefresh: widget.onRefresh, + middleTitle: widget.middleTitle, + finalTitle: widget.finalTitle, + onScoreSelect: + widget.onScoreSelect, + middleScoreBuilder: + widget.middleScoreBuilder, + finalScoreBuilder: + widget.finalScoreBuilder, + ), + ), + ), + ], + ], + ), + ), ); } + + Widget _buildContent( + BuildContext context, + ColorScheme colorScheme, + ) { + switch (widget.state) { + case ScoreState.loading: + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 48, + height: 48, + child: CircularProgressIndicator( + strokeWidth: 3, + color: colorScheme.primary, + ), + ), + const SizedBox(height: 16), + Text( + context.ap.loading, + style: TextStyle( + fontSize: 16, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ); + case ScoreState.error: + return _buildErrorState( + colorScheme, + context.ap.clickToRetry, + Icons.error_outline_rounded, + ); + case ScoreState.empty: + return _buildErrorState( + colorScheme, + context.ap.scoreEmpty, + Icons.assignment_outlined, + ); + case ScoreState.offlineEmpty: + return _buildErrorState( + colorScheme, + context.ap.noOfflineData, + Icons.cloud_off_rounded, + ); + case ScoreState.custom: + return _buildErrorState( + colorScheme, + widget.customStateHint ?? + context.ap.somethingError, + Icons.warning_amber_rounded, + ); + case ScoreState.finish: + if (widget.scoreData == null) { + return const SizedBox.shrink(); + } + if (isLandscape || _isAnalysisView) { + return GlassScoreAnalysisTab( + scoreData: widget.scoreData!, + onRefresh: widget.onRefresh, + controller: _scrollController, + ); + } + return GlassScoreListTab( + scoreData: widget.scoreData!, + onRefresh: widget.onRefresh, + middleTitle: widget.middleTitle, + finalTitle: widget.finalTitle, + onScoreSelect: widget.onScoreSelect, + middleScoreBuilder: + widget.middleScoreBuilder, + finalScoreBuilder: + widget.finalScoreBuilder, + controller: _scrollController, + ); + } + } + + Widget _buildErrorState( + ColorScheme colorScheme, + String message, + IconData icon, + ) { + return InkWell( + onTap: () { + if (widget.state == ScoreState.empty) { + _pickSemester(); + } else { + widget.onRefresh?.call(); + } + }, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GlassCard( + useOwnLayer: true, + width: 80, + height: 80, + child: Icon( + icon, + size: 40, + color: colorScheme.primary, + ), + ), + const SizedBox(height: 16), + Text( + message, + style: TextStyle( + fontSize: 16, + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + if (widget.state != + ScoreState.empty) ...[ + const SizedBox(height: 8), + Text( + context.ap.clickToRetry, + style: TextStyle( + fontSize: 14, + color: colorScheme.primary, + ), + ), + ], + ], + ), + ), + ); + } + + void _pickSemester() { + if (widget.semesterData != null) { + SemesterPicker.show( + context: context, + semesterData: widget.semesterData!, + currentIndex: + widget.semesterData!.currentIndex, + onSelect: (Semester semester, int index) { + widget.onSelect?.call(index); + }, + ); + } + widget.onSearchButtonClick?.call(); + } } diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_content.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_content.dart new file mode 100644 index 00000000..f83172e3 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_content.dart @@ -0,0 +1,497 @@ +import 'dart:io' show Platform; + +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [CourseContent]. +/// +/// Replaces the Material bottom sheet surface with +/// [GlassCard] / [GlassContainer] while keeping all +/// internal logic (notifications, calendar, visibility) +/// identical. +class GlassCourseContent extends StatefulWidget { + /// Creates a [GlassCourseContent]. + const GlassCourseContent({ + super.key, + required this.enableNotifyControl, + required this.course, + required this.timeCode, + required this.weekday, + this.notifyData, + this.autoNotifySave = true, + this.onNotifyClick, + this.courseNotifySaveKey = + ApConstants.semesterLatest, + this.enableAddToCalendar = true, + this.androidResourceIcon, + this.invisibleCourseCodes = const [], + this.onVisibilityChanged, + this.courseColor, + }); + + /// Whether to show notification controls. + final bool enableNotifyControl; + + /// The course to display details for. + final Course course; + + /// The time code for this course slot. + final TimeCode timeCode; + + /// The weekday number for this course slot. + final int weekday; + + /// Notification data for course reminders. + final CourseNotifyData? notifyData; + + /// Whether to auto-save notification changes. + final bool autoNotifySave; + + /// Callback when notification button is pressed. + final CourseNotifyCallback? onNotifyClick; + + /// Key used to persist notification data. + final String courseNotifySaveKey; + + /// Whether to show the add-to-calendar button. + final bool enableAddToCalendar; + + /// Android notification icon resource name. + final String? androidResourceIcon; + + /// Course codes that are currently hidden. + final List invisibleCourseCodes; + + /// Called when visibility is toggled. + final ValueChanged? onVisibilityChanged; + + /// Color for the course header. + final Color? courseColor; + + @override + State createState() => + _GlassCourseContentState(); +} + +class _GlassCourseContentState + extends State { + CourseNotifyData? _notifyData; + + @override + void initState() { + _notifyData = widget.notifyData; + super.initState(); + } + + @override + void didUpdateWidget( + covariant GlassCourseContent oldWidget, + ) { + if (widget.notifyData != oldWidget.notifyData) { + _notifyData = widget.notifyData; + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + CourseNotifyState? state; + if (widget.enableNotifyControl && + widget.notifyData != null) { + state = widget.notifyData!.getByCode( + widget.course.code, + widget.timeCode.startTime, + widget.weekday, + ) == + null + ? CourseNotifyState.cancel + : CourseNotifyState.schedule; + } + final bool visibility = !widget + .invisibleCourseCodes + .contains(widget.course.code); + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final Color courseColor = widget.courseColor ?? + courseColors[widget.course.code.hashCode % + courseColors.length]; + final Color onCourseColor = + ThemeData.estimateBrightnessForColor( + courseColor) == + Brightness.dark + ? Colors.white + : Colors.black; + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + shape: const LiquidRoundedSuperellipse( + borderRadius: 24, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.only( + left: 20, + right: 8, + top: 12, + bottom: 20, + ), + decoration: BoxDecoration( + color: courseColor, + borderRadius: + const BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + _buildActionRow( + context, + state, + visibility, + onCourseColor, + ), + Row( + children: [ + Expanded( + child: Text( + widget.course.title, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: onCourseColor, + ), + ), + ), + if (widget.course.required != + null && + widget.course.required! + .isNotEmpty) + Container( + padding: + const EdgeInsets + .symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: onCourseColor + .withAlpha(51), + borderRadius: + BorderRadius.circular( + 12, + ), + ), + child: Text( + widget.course.required!, + style: TextStyle( + fontSize: 12, + color: onCourseColor, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + widget.course.code, + style: TextStyle( + fontSize: 14, + color: onCourseColor + .withAlpha(204), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + _buildDetailRow( + Icons.person_outline_rounded, + context.ap.courseDialogProfessor, + widget.course.getInstructors(), + colorScheme, + ), + _buildDetailRow( + Icons.location_on_outlined, + context.ap.courseDialogLocation, + widget.course.location + ?.toString() ?? + '-', + colorScheme, + ), + _buildDetailRow( + Icons.school_outlined, + '學分數', + '${widget.course.units ?? "-"}' + ' ${context.ap.units}', + colorScheme, + ), + _buildDetailRow( + Icons.schedule_outlined, + context.ap.courseDialogTime, + '${widget.timeCode.startTime}' + '-${widget.timeCode.endTime}', + colorScheme, + ), + if (widget.course.className != + null && + widget.course.className! + .isNotEmpty) + _buildDetailRow( + Icons.class_outlined, + context.ap.studentClass, + widget.course.className!, + colorScheme, + ), + if (widget.course.hours != null && + widget.course.hours!.isNotEmpty) + _buildDetailRow( + Icons.timer_outlined, + context.ap.courseHours, + widget.course.hours!, + colorScheme, + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildActionRow( + BuildContext context, + CourseNotifyState? state, + bool visibility, + Color onCourseColor, + ) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if ((!kIsWeb && + (Platform.isAndroid || + Platform.isIOS)) && + widget.enableAddToCalendar) + IconButton( + tooltip: context.ap.addToCalendar, + icon: Image.asset( + ApImageIcons.calendarImport, + color: onCourseColor, + height: 24.0, + width: 24.0, + ), + onPressed: () { + final DateFormat format = + DateFormat('HH:mm'); + final DateTime startTime = + format.parse( + widget.timeCode.startTime, + ); + final DateTime endTime = + format.parse( + widget.timeCode.endTime, + ); + PlatformCalendarUtil.instance + .addToApp( + title: widget.course.title, + location: widget.course.location + ?.toString() ?? + '', + startDate: startTime + .weekTime(widget.weekday), + endDate: endTime + .weekTime(widget.weekday), + timeZone: 'GMT+8', + ); + AnalyticsUtil.instance.logEvent( + 'course_export_to_calendar', + ); + }, + ), + IconButton( + icon: Icon( + visibility + ? Icons.visibility_outlined + : Icons.visibility_off_outlined, + color: onCourseColor, + ), + onPressed: () { + setState(() { + widget.onVisibilityChanged + ?.call(!visibility); + }); + }, + ), + if (widget.enableNotifyControl && + _notifyData != null && + NotificationUtil.instance.isSupport) + IconButton( + icon: Icon( + state == CourseNotifyState.schedule + ? Icons.alarm_on + : Icons.alarm_off, + color: onCourseColor, + ), + onPressed: () async { + await _handleNotifyPressed( + context, + state, + ); + }, + ), + ], + ); + } + + Future _handleNotifyPressed( + BuildContext context, + CourseNotifyState? state, + ) async { + CourseNotify? courseNotify = + _notifyData!.getByCode( + widget.course.code, + widget.timeCode.startTime, + widget.weekday, + ); + if (widget.autoNotifySave) { + if (courseNotify == null) { + courseNotify = CourseNotify.fromCourse( + id: _notifyData!.lastId + 1, + course: widget.course, + weekday: widget.weekday, + timeCode: widget.timeCode, + ); + await NotificationUtil.instance + .scheduleCourseNotify( + context: context, + courseNotify: courseNotify, + weekday: widget.weekday, + androidResourceIcon: + widget.androidResourceIcon, + ); + _notifyData = _notifyData!.copyWith( + lastId: _notifyData!.lastId + 1, + data: [ + ..._notifyData!.data, + courseNotify, + ], + ); + _notifyData! + .save(widget.courseNotifySaveKey); + if (!context.mounted) return; + UiUtil.instance.showToast( + context, + context.ap.courseNotifyHint, + ); + AnalyticsUtil.instance.logEvent( + 'course_notify_schedule', + ); + } else { + await NotificationUtil.instance + .cancelNotify( + id: courseNotify.id, + ); + final List newData = + [ + ..._notifyData!.data, + ]; + newData.removeWhere( + (CourseNotify data) => + data.id == courseNotify!.id, + ); + _notifyData = _notifyData!.copyWith( + data: newData, + ); + _notifyData! + .save(widget.courseNotifySaveKey); + if (!context.mounted) return; + UiUtil.instance.showToast( + context, + context.ap.cancelNotifySuccess, + ); + } + setState(() {}); + AnalyticsUtil.instance.logEvent( + 'course_notify_cancel', + ); + } + if (widget.onNotifyClick != null) { + if (!mounted) return; + widget.onNotifyClick?.call( + courseNotify, + CourseNotifyState.values[ + (state!.index + 1) % + (CourseNotifyState.values.length)], + ); + } + } + + Widget _buildDetailRow( + IconData icon, + String label, + String value, + ColorScheme colorScheme, + ) { + if (value.isEmpty || value == '-') { + return const SizedBox(); + } + return Padding( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + width: 36, + height: 36, + child: Center( + child: Icon( + icon, + size: 18, + color: colorScheme.primary, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: colorScheme + .onSurfaceVariant, + ), + ), + const SizedBox(height: 2), + SelectableText( + value, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_list.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_list.dart new file mode 100644 index 00000000..f0170b21 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_list.dart @@ -0,0 +1,224 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_course_content.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [CourseList]. +/// +/// Replaces Material [Card] surfaces with [GlassCard] for +/// each course item while keeping the layout and +/// interactions identical. +class GlassCourseList extends StatelessWidget { + /// Creates a [GlassCourseList]. + const GlassCourseList({ + super.key, + required this.courses, + this.invisibleCourseCodes = const [], + this.onVisibilityChanged, + this.timeCodes, + this.controller, + }); + + /// The list of courses to display. + final List courses; + + /// Course codes that should be hidden in the grid. + final List invisibleCourseCodes; + + /// Called when a course's visibility is toggled. + final void Function(Course, bool)? + onVisibilityChanged; + + /// Time code definitions. + final List? timeCodes; + + /// Optional scroll controller. + final ScrollController? controller; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return ListView.builder( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only( + bottom: 80.0, + left: 16.0, + right: 16.0, + top: 16.0, + ), + itemCount: courses.length, + itemBuilder: (_, int index) { + final Course course = courses[index]; + final bool visibility = + !invisibleCourseCodes + .contains(course.code); + final Color courseColor = + courseColors[index % courseColors.length]; + final String instructors = + course.getInstructors(); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: InkWell( + onTap: () { + showModalBottomSheet( + context: context, + backgroundColor: + const Color(0x00000000), + isScrollControlled: true, + builder: + (BuildContext context) => + GlassCourseContent( + course: course, + invisibleCourseCodes: + invisibleCourseCodes, + onVisibilityChanged: + (bool visible) => + onVisibilityChanged + ?.call( + course, + visible, + ), + timeCode: timeCodes != null && + timeCodes!.isNotEmpty + ? timeCodes![0] + : const TimeCode( + title: '', + startTime: '', + endTime: '', + ), + weekday: course.times.isNotEmpty + ? course.times.first.weekday + : 1, + courseColor: courseColor, + enableNotifyControl: false, + ), + ); + }, + borderRadius: + BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + width: 4, + height: 60, + decoration: BoxDecoration( + color: courseColor, + borderRadius: + BorderRadius.circular(2), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + course.title, + style: TextStyle( + fontSize: 16, + fontWeight: + FontWeight.w600, + color: colorScheme + .onSurface, + ), + ), + const SizedBox(height: 4), + Text( + instructors.isNotEmpty + ? instructors + : context.ap.unknown, + style: TextStyle( + fontSize: 13, + color: colorScheme + .onSurfaceVariant, + ), + ), + Text( + course.location + ?.toString() ?? + '-', + style: TextStyle( + fontSize: 13, + color: colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + IconButton( + visualDensity: + VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: + const BoxConstraints(), + icon: Icon( + visibility + ? Icons + .visibility_outlined + : Icons + .visibility_off_outlined, + size: 20, + color: colorScheme + .onSurfaceVariant, + ), + onPressed: () => + onVisibilityChanged + ?.call( + course, + !visibility, + ), + ), + const SizedBox(height: 12), + Container( + padding: + const EdgeInsets + .symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: courseColor + .withAlpha(26), + borderRadius: + BorderRadius.circular( + 8, + ), + ), + child: Text( + '${course.units ?? "-"}' + ' ${context.ap.units}', + style: TextStyle( + fontSize: 12, + fontWeight: + FontWeight.w600, + color: courseColor, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_table_view.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_table_view.dart new file mode 100644 index 00000000..c5e8991d --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_table_view.dart @@ -0,0 +1,497 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [CourseTableView]. +/// +/// Replaces Material surfaces with translucent glass-style +/// backgrounds while keeping the dense grid layout readable. +class GlassCourseTableView extends StatelessWidget { + /// Creates a [GlassCourseTableView]. + const GlassCourseTableView({ + super.key, + required this.courseData, + required this.invisibleCourseCodes, + this.controller, + this.onCoursePressed, + required this.courseColorResolver, + this.repaintBoundaryKey, + this.mergeCourse, + this.showSectionTime, + this.showInstructors, + this.showClassroomLocation, + }); + + /// The course data to display. + final CourseData courseData; + + /// Course codes that should be hidden in the grid. + final List invisibleCourseCodes; + + /// Optional scroll controller for the grid. + final ScrollController? controller; + + /// Callback when a course cell is tapped. + final void Function( + Course course, + TimeCode timeCode, + int weekday, + )? onCoursePressed; + + /// Resolves a course code to its display color. + final Color Function(String courseCode) + courseColorResolver; + + /// Key for the [RepaintBoundary] used for screenshot + /// capture. + final GlobalKey? repaintBoundaryKey; + + /// Whether to merge adjacent identical course cells. + final bool? mergeCourse; + + /// Whether to show time information in time slot cells. + final bool? showSectionTime; + + /// Whether to show instructor names on course cards. + final bool? showInstructors; + + /// Whether to show classroom location on course cards. + final bool? showClassroomLocation; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final int weekdayCount = + courseData.hasHoliday ? 7 : 5; + final Map> courseLookup = + buildCourseLookup(courseData.courses); + return SingleChildScrollView( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only( + top: 8.0, + bottom: 80.0, + ), + child: RepaintBoundary( + key: repaintBoundaryKey, + child: Column( + children: [ + _buildWeekdayHeader( + context, + colorScheme, + weekdayCount, + ), + _buildCourseGrid( + context, + colorScheme, + weekdayCount, + courseData.timeCodes, + courseLookup, + ), + ], + ), + ), + ); + } + + Widget _buildWeekdayHeader( + BuildContext context, + ColorScheme colorScheme, + int weekdayCount, + ) { + return GlassContainer( + useOwnLayer: true, + padding: EdgeInsets.zero, + shape: const LiquidRoundedSuperellipse( + borderRadius: 0, + ), + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: colorScheme.outlineVariant + .withAlpha(128), + ), + ), + ), + child: Row( + children: [ + _buildTimeSlotHeader(colorScheme), + for (int i = 0; i < weekdayCount; i++) + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 12, + ), + child: Text( + context.ap.weekdaysCourse[i], + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: colorScheme.primary, + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTimeSlotHeader(ColorScheme colorScheme) { + return Container( + width: 48, + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + '', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: colorScheme.primary, + ), + ), + ); + } + + Widget _buildCourseGrid( + BuildContext context, + ColorScheme colorScheme, + int weekdayCount, + List timeCodes, + Map> courseLookup, + ) { + final int minIndex = courseData.minTimeCodeIndex; + final int maxIndex = courseData.maxTimeCodeIndex; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTimeColumn( + colorScheme, + timeCodes, + minIndex, + maxIndex, + ), + for (int weekday = 1; + weekday <= weekdayCount; + weekday++) + Expanded( + child: _buildWeekdayColumn( + colorScheme, + weekday, + weekdayCount, + minIndex, + maxIndex, + courseLookup, + ), + ), + ], + ); + } + + Widget _buildTimeColumn( + ColorScheme colorScheme, + List timeCodes, + int minIndex, + int maxIndex, + ) { + return Column( + children: [ + for (int i = minIndex; i <= maxIndex; i++) + _buildTimeSlot( + colorScheme, + i < timeCodes.length ? timeCodes[i] : null, + i, + i == maxIndex, + ), + ], + ); + } + + Widget _buildWeekdayColumn( + ColorScheme colorScheme, + int weekday, + int weekdayCount, + int minIndex, + int maxIndex, + Map> courseLookup, + ) { + final List children = []; + for (int i = minIndex; i <= maxIndex; i++) { + final Course? course = + courseLookup[weekday]?[i]; + final bool isInvisible = course != null && + invisibleCourseCodes.contains(course.code); + + if (course == null || isInvisible) { + children.add( + _buildEmptyCell( + colorScheme, + weekday < weekdayCount, + i == maxIndex, + ), + ); + } else { + int span = 1; + if (mergeCourse ?? true) { + while (i + span <= maxIndex && + courseLookup[weekday]?[i + span] == + course) { + span++; + } + } + children.add( + _buildCourseCell( + colorScheme, + weekday, + i, + course, + span, + weekday < weekdayCount, + i + span - 1 == maxIndex, + ), + ); + i += span - 1; + } + } + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ); + } + + Widget _buildTimeSlot( + ColorScheme colorScheme, + TimeCode? timeCode, + int timeIndex, + bool isLast, + ) { + return Container( + width: 48, + height: courseHeight, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 4, + ), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest + .withAlpha(51), + border: Border( + right: BorderSide( + color: colorScheme.outlineVariant + .withAlpha(128), + ), + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + timeCode?.title ?? '${timeIndex + 1}', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: colorScheme.onSurfaceVariant, + ), + ), + if (timeCode != null && + (showSectionTime ?? true)) ...[ + const SizedBox(height: 2), + Text( + timeCode.startTime, + style: TextStyle( + fontSize: 9, + color: colorScheme.onSurfaceVariant + .withAlpha(179), + ), + ), + ], + ], + ), + ); + } + + Widget _buildEmptyCell( + ColorScheme colorScheme, + bool showRightBorder, + bool isLast, + ) { + return Container( + height: courseHeight, + decoration: BoxDecoration( + border: Border( + right: showRightBorder + ? BorderSide( + color: colorScheme.outlineVariant + .withAlpha(51), + width: 0.5, + ) + : BorderSide.none, + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + ); + } + + Widget _buildCourseCell( + ColorScheme colorScheme, + int weekday, + int timeIndex, + Course course, + int span, + bool showRightBorder, + bool isLast, + ) { + return Container( + height: courseHeight * span, + decoration: BoxDecoration( + border: Border( + right: showRightBorder + ? BorderSide( + color: colorScheme.outlineVariant + .withAlpha(51), + width: 0.5, + ) + : BorderSide.none, + bottom: isLast + ? BorderSide.none + : BorderSide( + color: colorScheme.outlineVariant + .withAlpha(77), + width: 0.5, + ), + ), + ), + child: _buildCourseCard( + colorScheme, + weekday, + timeIndex, + course, + span, + ), + ); + } + + Widget _buildCourseCard( + ColorScheme colorScheme, + int weekday, + int timeIndex, + Course course, + int span, + ) { + final Color courseColor = + courseColorResolver(course.code); + final String locationInfo = + (showClassroomLocation ?? true) && + course.location != null + ? course.location.toString() + : ''; + final String instructorInfo = + (showInstructors ?? true) + ? course.getInstructors() + : ''; + + final Color onCourseColor = + ThemeData.estimateBrightnessForColor( + courseColor) == + Brightness.dark + ? Colors.white + : Colors.black; + + final String displayInfo = [ + if (instructorInfo.isNotEmpty) instructorInfo, + if (locationInfo.isNotEmpty) locationInfo, + ].join('\n'); + + return GestureDetector( + onTap: () { + final TimeCode timeCode = + timeIndex < courseData.timeCodes.length + ? courseData.timeCodes[timeIndex] + : const TimeCode( + title: '?', + startTime: '?', + endTime: '?', + ); + onCoursePressed?.call( + course, + timeCode, + weekday, + ); + }, + child: Container( + width: double.infinity, + height: double.infinity, + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 6, + ), + decoration: BoxDecoration( + color: courseColor.withAlpha(200), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.white.withAlpha(51), + width: 0.5, + ), + boxShadow: [ + BoxShadow( + color: courseColor.withAlpha(51), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + course.title, + textAlign: TextAlign.center, + maxLines: span > 1 ? 4 : 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: span > 1 ? 14 : 9.5, + fontWeight: FontWeight.w600, + color: onCourseColor, + height: 1.1, + ), + ), + if (displayInfo.isNotEmpty) ...[ + SizedBox(height: span > 1 ? 4 : 2), + Text( + displayInfo, + textAlign: TextAlign.center, + maxLines: span > 1 ? 4 : 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: span > 1 ? 12 : 8.5, + color: + onCourseColor.withAlpha(217), + height: 1.0, + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_cards.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_cards.dart new file mode 100644 index 00000000..ea1f05c1 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_cards.dart @@ -0,0 +1,627 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// Glass version of [ScorePRCard]. +class GlassScorePRCard extends StatelessWidget { + const GlassScorePRCard({ + super.key, + required this.analysis, + }); + + final ScoreAnalysis analysis; + + Color _getPRColor(ColorScheme colorScheme, int pr) { + if (pr >= 90) return const Color(0xFF4CAF50); + if (pr >= 75) return const Color(0xFF8BC34A); + if (pr >= 50) return colorScheme.primary; + if (pr >= 25) return const Color(0xFFFF9800); + return colorScheme.error; + } + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final int pr = analysis.estimatedPR; + final Color prColor = _getPRColor(colorScheme, pr); + + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: prColor.withAlpha(26), + borderRadius: + BorderRadius.circular(16), + ), + child: Icon( + Icons.trending_up_rounded, + size: 28, + color: prColor, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + '\u4f30\u8a08 PR \u503c', + style: TextStyle( + fontSize: 14, + color: colorScheme + .onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Text( + 'PR $pr', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: prColor, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: prColor.withAlpha(26), + borderRadius: + BorderRadius.circular(20), + ), + child: Text( + analysis.prLevel, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: prColor, + ), + ), + ), + ], + ), + ), + Container( + margin: + const EdgeInsets.fromLTRB(16, 0, 16, 16), + height: 8, + decoration: BoxDecoration( + color: + colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(4), + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: pr / 100, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + prColor.withAlpha(179), + prColor, + ], + ), + borderRadius: + BorderRadius.circular(4), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB( + 16, + 0, + 16, + 16, + ), + child: Text( + '※ PR 值為根據平均成績估算,僅供參考', + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant + .withAlpha(179), + ), + ), + ), + ], + ), + ); + } +} + +/// Glass version of [ScoreStatisticsCard]. +class GlassScoreStatisticsCard extends StatelessWidget { + const GlassScoreStatisticsCard({ + super.key, + required this.analysis, + }); + + final ScoreAnalysis analysis; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + Icons.bar_chart_rounded, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + '\u6210\u7e3e\u7d71\u8a08', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + Divider( + height: 1, + color: colorScheme.outlineVariant + .withAlpha(77), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: _GlassScoreStatItem( + label: '\u6700\u9ad8\u5206', + value: analysis.maxScore + .toStringAsFixed(0), + icon: + Icons.arrow_upward_rounded, + color: const Color(0xFF4CAF50), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _GlassScoreStatItem( + label: '\u6700\u4f4e\u5206', + value: analysis.minScore + .toStringAsFixed(0), + icon: Icons + .arrow_downward_rounded, + color: colorScheme.error, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _GlassScoreStatItem( + label: '\u6a19\u6e96\u5dee', + value: analysis.standardDeviation + .toStringAsFixed(2), + icon: + Icons.analytics_outlined, + color: colorScheme.tertiary, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _GlassScoreStatItem( + label: '\u79d1\u76ee\u6578', + value: analysis.totalSubjects + .toString(), + icon: + Icons.menu_book_rounded, + color: colorScheme.secondary, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} + +class _GlassScoreStatItem extends StatelessWidget { + const _GlassScoreStatItem({ + required this.label, + required this.value, + required this.icon, + required this.color, + }); + + final String label; + final String value; + final IconData icon; + final Color color; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: color.withAlpha(13), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: color.withAlpha(26), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, size: 18, color: color), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12, + color: + colorScheme.onSurfaceVariant, + ), + ), + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +/// Glass version of [ScoreDistributionCard]. +class GlassScoreDistributionCard + extends StatelessWidget { + const GlassScoreDistributionCard({ + super.key, + required this.analysis, + }); + + final ScoreAnalysis analysis; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + Icons.pie_chart_rounded, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + '\u6210\u7e3e\u5206\u4f48', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + Divider( + height: 1, + color: colorScheme.outlineVariant + .withAlpha(77), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _GlassDistributionBar( + label: + '90-100 (\u512a\u79c0)', + count: analysis + .distribution['90-100'] ?? + 0, + total: analysis.totalSubjects, + color: const Color(0xFF4CAF50), + ), + const SizedBox(height: 10), + _GlassDistributionBar( + label: + '80-89 (\u826f\u597d)', + count: analysis + .distribution['80-89'] ?? + 0, + total: analysis.totalSubjects, + color: const Color(0xFF8BC34A), + ), + const SizedBox(height: 10), + _GlassDistributionBar( + label: + '70-79 (\u666e\u901a)', + count: analysis + .distribution['70-79'] ?? + 0, + total: analysis.totalSubjects, + color: colorScheme.primary, + ), + const SizedBox(height: 10), + _GlassDistributionBar( + label: + '60-69 (\u53ca\u683c)', + count: analysis + .distribution['60-69'] ?? + 0, + total: analysis.totalSubjects, + color: const Color(0xFFFF9800), + ), + const SizedBox(height: 10), + _GlassDistributionBar( + label: '0-59 ' + '(\u4e0d\u53ca\u683c)', + count: analysis + .distribution['0-59'] ?? + 0, + total: analysis.totalSubjects, + color: colorScheme.error, + ), + ], + ), + ), + ], + ), + ); + } +} + +class _GlassDistributionBar extends StatelessWidget { + const _GlassDistributionBar({ + required this.label, + required this.count, + required this.total, + required this.color, + }); + + final String label; + final int count; + final int total; + final Color color; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final double percentage = + total > 0 ? count / total : 0; + + return Row( + children: [ + SizedBox( + width: 100, + child: Text( + label, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ), + Expanded( + child: Container( + height: 20, + decoration: BoxDecoration( + color: + colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10), + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: percentage, + child: DecoratedBox( + decoration: BoxDecoration( + color: color, + borderRadius: + BorderRadius.circular(10), + ), + ), + ), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 40, + child: Text( + '$count \u79d1', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: colorScheme.onSurface, + ), + textAlign: TextAlign.end, + ), + ), + ], + ); + } +} + +/// Glass version of [ScoreCreditSummaryCard]. +class GlassScoreCreditSummaryCard + extends StatelessWidget { + const GlassScoreCreditSummaryCard({ + super.key, + required this.analysis, + }); + + final ScoreAnalysis analysis; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon( + Icons.account_balance_rounded, + size: 20, + color: colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + '\u5b78\u5206\u7d71\u8a08', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ], + ), + ), + Divider( + height: 1, + color: colorScheme.outlineVariant + .withAlpha(77), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: _GlassCreditItem( + label: '\u4fee\u7fd2\u5b78\u5206', + value: analysis.totalCredits + .toStringAsFixed(1), + color: colorScheme.primary, + ), + ), + Expanded( + child: _GlassCreditItem( + label: '\u53ca\u683c\u5b78\u5206', + value: analysis.passedCredits + .toStringAsFixed(1), + color: const Color(0xFF4CAF50), + ), + ), + Expanded( + child: _GlassCreditItem( + label: + '\u4e0d\u53ca\u683c\u5b78' + '\u5206', + value: analysis.failedCredits + .toStringAsFixed(1), + color: colorScheme.error, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _GlassCreditItem extends StatelessWidget { + const _GlassCreditItem({ + required this.label, + required this.value, + required this.color, + }); + + final String label; + final String value; + final Color color; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return Column( + children: [ + Text( + value, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_tab.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_tab.dart new file mode 100644 index 00000000..0d336e64 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_tab.dart @@ -0,0 +1,215 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_score_analysis_cards.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// Glass version of [ScoreAnalysisTab]. +/// +/// Replaces the summary card's gradient [Container] with +/// [GlassCard] and uses glass analysis card variants. +class GlassScoreAnalysisTab extends StatelessWidget { + const GlassScoreAnalysisTab({ + super.key, + required this.scoreData, + this.onRefresh, + this.controller, + }); + + final ScoreData scoreData; + final VoidCallback? onRefresh; + final ScrollController? controller; + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final ScoreAnalysis analysis = + ScoreAnalysis(scoreData); + + return RefreshIndicator( + onRefresh: () async => onRefresh?.call(), + child: SingleChildScrollView( + controller: controller, + physics: + const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildMainSummaryCard( + colorScheme, + context.ap, + analysis, + ), + const SizedBox(height: 16), + GlassScorePRCard(analysis: analysis), + const SizedBox(height: 16), + GlassScoreStatisticsCard( + analysis: analysis, + ), + const SizedBox(height: 16), + GlassScoreDistributionCard( + analysis: analysis, + ), + const SizedBox(height: 16), + GlassScoreCreditSummaryCard( + analysis: analysis, + ), + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildMainSummaryCard( + ColorScheme colorScheme, + ApLocalizations ap, + ScoreAnalysis analysis, + ) { + final Detail detail = scoreData.detail; + + return GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + Expanded( + child: _buildMainItem( + colorScheme, + Icons.star_rounded, + ap.average, + detail.average + ?.toStringAsFixed(2) ?? + '-', + ), + ), + Container( + width: 1, + height: 60, + color: colorScheme + .onPrimaryContainer + .withAlpha(51), + ), + Expanded( + child: _buildMainItem( + colorScheme, + Icons.school_rounded, + ap.conductScore, + detail.conduct + ?.toStringAsFixed(0) ?? + '-', + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + decoration: BoxDecoration( + color: + colorScheme.surface.withAlpha(179), + borderRadius: + const BorderRadius.vertical( + bottom: Radius.circular(20), + ), + ), + child: Row( + children: [ + Expanded( + child: _buildRankItem( + colorScheme, + ap.classRank, + detail.classRank ?? '-', + ), + ), + Container( + width: 1, + height: 40, + color: colorScheme.outlineVariant + .withAlpha(128), + ), + Expanded( + child: _buildRankItem( + colorScheme, + ap.departmentRank, + detail.departmentRank ?? '-', + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildMainItem( + ColorScheme colorScheme, + IconData icon, + String label, + String value, + ) { + return Column( + children: [ + Icon( + icon, + size: 28, + color: colorScheme.primary, + ), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: + colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 13, + color: colorScheme.onPrimaryContainer + .withAlpha(179), + ), + ), + ], + ); + } + + Widget _buildRankItem( + ColorScheme colorScheme, + String label, + String value, + ) { + return Column( + children: [ + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_list_tab.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_list_tab.dart new file mode 100644 index 00000000..6e93379a --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_list_tab.dart @@ -0,0 +1,205 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// Glass version of [ScoreListTab]. +/// +/// Replaces each score item's [Container] with [GlassCard]. +class GlassScoreListTab extends StatelessWidget { + const GlassScoreListTab({ + super.key, + required this.scoreData, + this.onRefresh, + this.middleTitle, + this.finalTitle, + this.onScoreSelect, + this.middleScoreBuilder, + this.finalScoreBuilder, + this.controller, + }); + + final ScoreData scoreData; + final VoidCallback? onRefresh; + final String? middleTitle; + final String? finalTitle; + final Function(int index)? onScoreSelect; + final Widget Function(int index)? middleScoreBuilder; + final Widget Function(int index)? finalScoreBuilder; + final ScrollController? controller; + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: () async => onRefresh?.call(), + child: ListView.builder( + controller: controller, + physics: + const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16), + itemCount: scoreData.scores.length, + itemBuilder: + (BuildContext context, int index) { + return _buildScoreItem( + Theme.of(context).colorScheme, + scoreData.scores[index], + index, + ); + }, + ), + ); + } + + Widget _buildScoreItem( + ColorScheme colorScheme, + Score score, + int index, + ) { + final String scoreStr = + score.semesterScore ?? ''; + final double? scoreValue = + double.tryParse(scoreStr); + final bool isPassed = + scoreValue != null && scoreValue >= 60; + final Color scoreColor = scoreValue == null + ? colorScheme.onSurfaceVariant + : isPassed + ? _getScoreColor(scoreValue) + : colorScheme.error; + + return GestureDetector( + onTap: onScoreSelect == null + ? null + : () { + onScoreSelect!(index); + AnalyticsUtil.instance + .logEvent('score_title_click'); + }, + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: GlassCard( + useOwnLayer: true, + child: Row( + children: [ + Container( + width: 4, + height: 50, + decoration: BoxDecoration( + color: scoreColor, + borderRadius: + BorderRadius.circular(2), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + score.title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: + colorScheme.onSurface, + ), + ), + const SizedBox(height: 4), + Row( + children: [ + _buildTag( + colorScheme, + score.required ?? '', + colorScheme.tertiary, + ), + const SizedBox(width: 8), + Text( + '${score.units}' + ' \u5b78\u5206', + style: TextStyle( + fontSize: 12, + color: colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + ], + ), + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + if (finalScoreBuilder == null) + Text( + score.semesterScore ?? '-', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: scoreColor, + ), + ), + if (finalScoreBuilder != null) + finalScoreBuilder!(index), + const SizedBox(height: 4), + if (middleScoreBuilder == null && + score.middleScore != null && + score.middleScore!.isNotEmpty) + Text( + '\u671f\u4e2d: ' + '${score.middleScore}', + style: TextStyle( + fontSize: 11, + color: colorScheme + .onSurfaceVariant, + ), + ), + if (middleScoreBuilder != null) + middleScoreBuilder!(index), + ], + ), + ], + ), + ), + ), + ); + } + + Color _getScoreColor(double score) { + if (score >= 90) return const Color(0xFF4CAF50); + if (score >= 80) return const Color(0xFF8BC34A); + if (score >= 70) return const Color(0xFF2196F3); + if (score >= 60) return const Color(0xFFFF9800); + return const Color(0xFFF44336); + } + + Widget _buildTag( + ColorScheme colorScheme, + String text, + Color color, + ) { + if (text.isEmpty) return const SizedBox.shrink(); + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: color.withAlpha(26), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + text + .replaceAll('\u3010', '') + .replaceAll('\u3011', ''), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ); + } +} From eea24ba8f3e60a2c4268b42f086daf81759bbfc2 Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Sun, 5 Apr 2026 15:54:08 +0800 Subject: [PATCH 07/14] fix(liquid_glass): add AdaptiveLiquidGlassLayer to all glass scaffolds Wrap all 6 glass scaffolds and the example home page with AdaptiveLiquidGlassLayer to provide the required liquid glass renderer context. Without this, glass widgets crash with "No liquid glass renderer found in context". --- apps/example_liquid_glass/lib/pages/home_page.dart | 6 ++++-- .../lib/src/scaffold/glass_home_page_scaffold.dart | 4 +++- .../lib/src/scaffold/glass_image_viewer_scaffold.dart | 10 ++++++---- .../lib/src/scaffold/glass_login_scaffold.dart | 6 ++++-- .../lib/src/scaffold/glass_user_info_scaffold.dart | 10 ++++++---- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/apps/example_liquid_glass/lib/pages/home_page.dart b/apps/example_liquid_glass/lib/pages/home_page.dart index c78d8d75..7936a3c1 100644 --- a/apps/example_liquid_glass/lib/pages/home_page.dart +++ b/apps/example_liquid_glass/lib/pages/home_page.dart @@ -12,8 +12,9 @@ class GlassHomePage extends StatelessWidget { Widget build(BuildContext context) { final ApLocalizations ap = context.ap; - return Scaffold( - appBar: GlassAppBar( + return AdaptiveLiquidGlassLayer( + child: Scaffold( + appBar: GlassAppBar( title: const Text('Liquid Glass Example'), actions: [ IconButton( @@ -71,6 +72,7 @@ class GlassHomePage extends StatelessWidget { ), ], ), + ), ); } diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart index ea69386e..63508891 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart @@ -102,7 +102,8 @@ class GlassHomePageScaffoldState @override Widget build(BuildContext context) { - return PopScope( + return AdaptiveLiquidGlassLayer( + child: PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, _) { if (Platform.isAndroid) { @@ -167,6 +168,7 @@ class GlassHomePageScaffoldState ), ), ), + ), ); } diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart index ea371cc1..a6593fb2 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart @@ -48,10 +48,11 @@ class GlassImageViewerScaffoldState @override Widget build(BuildContext context) { - return Scaffold( - extendBodyBehindAppBar: true, - appBar: GlassAppBar( - title: Text(widget.title ?? ''), + return AdaptiveLiquidGlassLayer( + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: GlassAppBar( + title: Text(widget.title ?? ''), actions: widget.actions, ), body: Column( @@ -84,6 +85,7 @@ class GlassImageViewerScaffoldState ), ], ), + ), ); } } diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart index 5e9f7fc6..27b06c6f 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart @@ -39,8 +39,9 @@ class GlassLoginScaffoldState final bool isDark = Theme.of(context).brightness == Brightness.dark; - return Scaffold( - resizeToAvoidBottomInset: + return AdaptiveLiquidGlassLayer( + child: Scaffold( + resizeToAvoidBottomInset: orientation == Orientation.portrait, body: DecoratedBox( decoration: BoxDecoration( @@ -97,6 +98,7 @@ class GlassLoginScaffoldState ), ), ), + ), ); } diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart index 2504993b..a1ce2c95 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart @@ -49,10 +49,11 @@ class GlassUserInfoScaffoldState final bool isDark = Theme.of(context).brightness == Brightness.dark; - return Scaffold( - extendBodyBehindAppBar: true, - appBar: GlassAppBar( - actions: [ + return AdaptiveLiquidGlassLayer( + child: Scaffold( + extendBodyBehindAppBar: true, + appBar: GlassAppBar( + actions: [ ...widget.actions ?? [], if (widget.enableBarCode) IconButton( @@ -120,6 +121,7 @@ class GlassUserInfoScaffoldState ), ), ), + ), ); } From 959b0e051ac4b0d6fa00767c49f1891751a8559a Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Sun, 5 Apr 2026 15:54:14 +0800 Subject: [PATCH 08/14] feat(liquid_glass): implement GlassThemeBridge with Apple Liquid Glass color guidelines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map ApTheme ColorScheme to GlassGlowColors (primary, secondary, success, warning, danger, info) and configure LiquidGlassSettings per Apple guidelines: 135° light angle, light/dark-specific glass tint, thickness, blur, and refractive index values. --- .../lib/src/glass_theme_bridge.dart | 77 +++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/packages/ap_common_liquid_glass/lib/src/glass_theme_bridge.dart b/packages/ap_common_liquid_glass/lib/src/glass_theme_bridge.dart index 48b6b8ad..b04962b4 100644 --- a/packages/ap_common_liquid_glass/lib/src/glass_theme_bridge.dart +++ b/packages/ap_common_liquid_glass/lib/src/glass_theme_bridge.dart @@ -1,11 +1,18 @@ +import 'dart:math' as math; + import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; import 'package:flutter/material.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; +/// Apple standard light angle: 135° (upper-left light source). +const double _kLightAngle = 0.75 * math.pi; + /// Bridges [ApTheme] seed colors to [GlassThemeData]. /// -/// This ensures glass widgets use settings that complement the -/// current AP theme configuration. +/// Maps the current [ColorScheme] to [GlassGlowColors] and +/// configures [LiquidGlassSettings] following Apple's Liquid +/// Glass design guidelines (consistent light angle, appropriate +/// glass tint per brightness, etc.). /// /// ```dart /// GlassTheme( @@ -16,37 +23,97 @@ import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; class GlassThemeBridge { const GlassThemeBridge._(); - /// Creates a [GlassThemeData] aligned with the current theme. + /// Creates a [GlassThemeData] aligned with the current + /// [ApTheme] / [ColorScheme]. /// /// Uses [GlassQuality.standard] for broad compatibility. /// Override via [LiquidGlassSettings] for per-widget control. static GlassThemeData fromContext( BuildContext context, ) { - return const GlassThemeData( + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return GlassThemeData( light: GlassThemeVariant( quality: GlassQuality.standard, + settings: const LiquidGlassSettings( + glassColor: Color.fromRGBO( + 255, + 255, + 255, + 0.12, + ), + thickness: 30, + blur: 3, + lightAngle: _kLightAngle, + lightIntensity: 2.0, + refractiveIndex: 0.7, + ), + glowColors: GlassGlowColors( + primary: colorScheme.primary, + secondary: colorScheme.secondary, + success: const Color(0xFF34C759), + warning: const Color(0xFFFF9500), + danger: colorScheme.error, + info: const Color(0xFF5AC8FA), + ), ), dark: GlassThemeVariant( quality: GlassQuality.standard, + settings: const LiquidGlassSettings( + glassColor: Color.fromRGBO( + 255, + 255, + 255, + 0.08, + ), + thickness: 40, + lightAngle: _kLightAngle, + lightIntensity: 1.5, + refractiveIndex: 0.7, + ), + glowColors: GlassGlowColors( + primary: colorScheme.primary, + secondary: colorScheme.secondary, + success: const Color(0xFF30D158), + warning: const Color(0xFFFF9F0A), + danger: colorScheme.error, + info: const Color(0xFF64D2FF), + ), ), ); } - /// Creates a [GlassThemeData] with custom settings. + /// Creates a [GlassThemeData] with custom settings while + /// still bridging [GlassGlowColors] from the [ColorScheme]. static GlassThemeData withSettings({ + required BuildContext context, LiquidGlassSettings? lightSettings, LiquidGlassSettings? darkSettings, GlassQuality quality = GlassQuality.standard, }) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final GlassGlowColors glowColors = GlassGlowColors( + primary: colorScheme.primary, + secondary: colorScheme.secondary, + success: const Color(0xFF34C759), + warning: const Color(0xFFFF9500), + danger: colorScheme.error, + info: const Color(0xFF5AC8FA), + ); + return GlassThemeData( light: GlassThemeVariant( settings: lightSettings, quality: quality, + glowColors: glowColors, ), dark: GlassThemeVariant( settings: darkSettings, quality: quality, + glowColors: glowColors, ), ); } From 18ce4e7b1436f7cd4421e868fe368ab465be5844 Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Sun, 5 Apr 2026 15:54:20 +0800 Subject: [PATCH 09/14] chore(example_liquid_glass): align Android native config with original example Sync Gradle versions (AGP 8.9.1, Kotlin 1.9.10, Gradle 8.11.1), add desugaring + window dependencies, restore permissions, notification receivers, SplashScreenDrawable meta-data, and theme styles. --- .../android/app/build.gradle.kts | 20 ++++--- .../android/app/src/main/AndroidManifest.xml | 52 ++++++++++++------- .../app/src/main/res/values/styles.xml | 10 ++-- .../android/gradle.properties | 3 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../android/settings.gradle.kts | 4 +- 6 files changed, 54 insertions(+), 37 deletions(-) diff --git a/apps/example_liquid_glass/android/app/build.gradle.kts b/apps/example_liquid_glass/android/app/build.gradle.kts index 8888801d..86f09bd5 100644 --- a/apps/example_liquid_glass/android/app/build.gradle.kts +++ b/apps/example_liquid_glass/android/app/build.gradle.kts @@ -11,34 +11,38 @@ android { ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + // Flag to enable support for the new language APIs + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + jvmTarget = JavaVersion.VERSION_1_8.toString() } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.ap.common.liquidglass.ap_common_example_liquid_glass" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName + multiDexEnabled = true } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.getByName("debug") } } } +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.2.2") + implementation("androidx.window:window:1.0.0") + implementation("androidx.window:window-java:1.0.0") +} + flutter { source = "../.." } diff --git a/apps/example_liquid_glass/android/app/src/main/AndroidManifest.xml b/apps/example_liquid_glass/android/app/src/main/AndroidManifest.xml index f44f7a4a..1494afae 100644 --- a/apps/example_liquid_glass/android/app/src/main/AndroidManifest.xml +++ b/apps/example_liquid_glass/android/app/src/main/AndroidManifest.xml @@ -1,45 +1,57 @@ + + + + + + + + + + + + + + + + + android:label="Liquid Glass Example" + android:icon="@mipmap/ic_launcher" + android:enableOnBackInvokedCallback="true"> - + - + + + + + + + + + - - - - - - - diff --git a/apps/example_liquid_glass/android/app/src/main/res/values/styles.xml b/apps/example_liquid_glass/android/app/src/main/res/values/styles.xml index cb1ef880..322503e4 100644 --- a/apps/example_liquid_glass/android/app/src/main/res/values/styles.xml +++ b/apps/example_liquid_glass/android/app/src/main/res/values/styles.xml @@ -1,9 +1,9 @@ - - - diff --git a/apps/example_liquid_glass/android/gradle.properties b/apps/example_liquid_glass/android/gradle.properties index fbee1d8c..25971708 100644 --- a/apps/example_liquid_glass/android/gradle.properties +++ b/apps/example_liquid_glass/android/gradle.properties @@ -1,2 +1,3 @@ -org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true +android.enableJetifier=true diff --git a/apps/example_liquid_glass/android/gradle/wrapper/gradle-wrapper.properties b/apps/example_liquid_glass/android/gradle/wrapper/gradle-wrapper.properties index e4ef43fb..efdcc4ac 100644 --- a/apps/example_liquid_glass/android/gradle/wrapper/gradle-wrapper.properties +++ b/apps/example_liquid_glass/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/apps/example_liquid_glass/android/settings.gradle.kts b/apps/example_liquid_glass/android/settings.gradle.kts index ca7fe065..ec259148 100644 --- a/apps/example_liquid_glass/android/settings.gradle.kts +++ b/apps/example_liquid_glass/android/settings.gradle.kts @@ -19,8 +19,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.11.1" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "1.9.10" apply false } include(":app") From 012b90fbf23f702dcd1db2830a00e6d0edf0c4e0 Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Tue, 14 Apr 2026 23:40:33 +0800 Subject: [PATCH 10/14] feat(liquid_glass): add GlassFloatingToolbar and replace AppBar with floating capsules Replace Scaffold.appBar with iOS 26-style floating toolbar capsules across all glass scaffolds. Left capsule holds SemesterPicker/title, right capsule holds action IconButtons. Body content starts from top with manual padding for toolbar clearance. --- .../src/scaffold/glass_course_scaffold.dart | 611 ++++++++---------- .../scaffold/glass_home_page_scaffold.dart | 230 ++++--- .../scaffold/glass_image_viewer_scaffold.dart | 79 ++- .../src/scaffold/glass_login_scaffold.dart | 41 +- .../src/scaffold/glass_score_scaffold.dart | 295 ++++----- .../scaffold/glass_user_info_scaffold.dart | 120 ++-- .../src/widgets/glass_floating_toolbar.dart | 74 +++ 7 files changed, 750 insertions(+), 700 deletions(-) create mode 100644 packages/ap_common_liquid_glass/lib/src/widgets/glass_floating_toolbar.dart diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart index 2f6e2115..25dae235 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_course_scaffold.dart @@ -4,6 +4,9 @@ import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; import 'package:ap_common_liquid_glass/src/widgets/glass_course_content.dart'; import 'package:ap_common_liquid_glass/src/widgets/glass_course_list.dart'; import 'package:ap_common_liquid_glass/src/widgets/glass_course_table_view.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_dialog.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_semester_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -69,24 +72,19 @@ class GlassCourseScaffold extends StatefulWidget { this.showInstructors, this.showClassroomLocation, this.showSearchButton, - }) : state = dataState.when( - loading: () => CourseState.loading, - loaded: (_, __) => CourseState.finish, - error: (_) => CourseState.error, - empty: (_) => CourseState.empty, - ), - courseData = - dataState.dataOrNull ?? CourseData.empty(), - customHint = - dataState is DataLoaded - ? dataState.hint - : null, - customStateHint = - dataState is DataError - ? dataState.hint - : dataState is DataEmpty - ? dataState.hint - : null; + }) : state = dataState.when( + loading: () => CourseState.loading, + loaded: (_, __) => CourseState.finish, + error: (_) => CourseState.error, + empty: (_) => CourseState.empty, + ), + courseData = dataState.dataOrNull ?? CourseData.empty(), + customHint = dataState is DataLoaded ? dataState.hint : null, + customStateHint = dataState is DataError + ? dataState.hint + : dataState is DataEmpty + ? dataState.hint + : null; final CourseState state; final CourseData courseData; @@ -113,12 +111,10 @@ class GlassCourseScaffold extends StatefulWidget { final bool? showSearchButton; @override - GlassCourseScaffoldState createState() => - GlassCourseScaffoldState(); + GlassCourseScaffoldState createState() => GlassCourseScaffoldState(); } -class GlassCourseScaffoldState - extends State { +class GlassCourseScaffoldState extends State { final GlobalKey _repaintBoundaryGlobalKey = GlobalKey(); ContentStyle _contentStyle = ContentStyle.table; @@ -130,19 +126,17 @@ class GlassCourseScaffoldState bool? mergeCourse; bool get isLandscape => - MediaQuery.of(context).orientation == - Orientation.landscape; + MediaQuery.of(context).orientation == Orientation.landscape; List invisibleCourseCodes = []; - final Map _courseColorMap = - {}; + final Map _courseColorMap = {}; int _colorIndex = 0; Color _getCourseColor(String courseCode) { if (!_courseColorMap.containsKey(courseCode)) { _courseColorMap[courseCode] = - courseColors[_colorIndex % courseColors.length]; + glassCourseColors[_colorIndex % glassCourseColors.length]; _colorIndex++; } return _courseColorMap[courseCode]!; @@ -153,23 +147,20 @@ class GlassCourseScaffoldState @override void initState() { - showSectionTime = widget.showSectionTime ?? + showSectionTime = + widget.showSectionTime ?? + PreferenceUtil.instance.getBool(ApConstants.showSectionTime, true); + showInstructors = + widget.showInstructors ?? + PreferenceUtil.instance.getBool(ApConstants.showInstructors, true); + showClassroomLocation = + widget.showClassroomLocation ?? PreferenceUtil.instance.getBool( - ApConstants.showSectionTime, + ApConstants.showClassroomLocation, true, ); - showInstructors = widget.showInstructors ?? - PreferenceUtil.instance.getBool( - ApConstants.showInstructors, - true, - ); - showClassroomLocation = - widget.showClassroomLocation ?? - PreferenceUtil.instance.getBool( - ApConstants.showClassroomLocation, - true, - ); - showSearchButton = widget.showSearchButton ?? + showSearchButton = + widget.showSearchButton ?? PreferenceUtil.instance.getBool( ApConstants.showCourseSearchButton, true, @@ -186,8 +177,7 @@ class GlassCourseScaffoldState if (_showFab) { setState(() => _showFab = false); } - } else if (_scrollController - .position.userScrollDirection == + } else if (_scrollController.position.userScrollDirection == ScrollDirection.forward) { if (!_showFab) { setState(() => _showFab = true); @@ -198,9 +188,7 @@ class GlassCourseScaffoldState } @override - void didUpdateWidget( - covariant GlassCourseScaffold oldWidget, - ) { + void didUpdateWidget(covariant GlassCourseScaffold oldWidget) { fetchInvisibleCourseCodes(); super.didUpdateWidget(oldWidget); } @@ -215,177 +203,155 @@ class GlassCourseScaffoldState Widget build(BuildContext context) { return AdaptiveLiquidGlassLayer( child: CourseConfig( - showSectionTime: showSectionTime, - showInstructors: showInstructors, - showClassroomLocation: showClassroomLocation, - child: Scaffold( - extendBodyBehindAppBar: true, - appBar: GlassAppBar( - title: Row( + showSectionTime: showSectionTime, + showInstructors: showInstructors, + showClassroomLocation: showClassroomLocation, + child: Scaffold( + body: Stack( children: [ - Flexible( - child: Text( - widget.title ?? context.ap.course, - overflow: TextOverflow.ellipsis, + Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 50, ), - ), - if (widget.itemPicker != - null) ...[ - const SizedBox(width: 8), - widget.itemPicker!, - ], - if (widget.semesterData != null && - widget.itemPicker == - null) ...[ - const SizedBox(width: 8), - SemesterPicker( - semesterData: - widget.semesterData!, - currentIndex: widget - .semesterData!.currentIndex, - onSelect: ( - Semester semester, - int index, - ) { - widget.onSelect?.call(index); - }, - featureTag: 'course', + child: Row( + children: [ + Expanded( + flex: 3, + child: Flex( + direction: Axis.vertical, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (widget.customHint != null && + widget.customHint!.isNotEmpty) + HintBanner(text: widget.customHint!), + Expanded( + child: RefreshIndicator( + onRefresh: () async { + await widget.onRefresh!(); + AnalyticsUtil.instance.logEvent( + 'course_refresh', + ); + return; + }, + child: _body(), + ), + ), + ], + ), + ), + if (widget.state == CourseState.finish && + isLandscape) ...[ + const SizedBox(width: 16.0), + Expanded( + flex: 2, + child: GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: GlassCourseList( + courses: widget.courseData.courses, + timeCodes: widget.courseData.timeCodes, + invisibleCourseCodes: invisibleCourseCodes, + onVisibilityChanged: + (Course course, bool visibility) => + saveInvisibleCourseCodes( + course: course, + visibility: visibility, + ), + ), + ), + ), + ], + ], ), - ], - ], - ), - actions: [ - ...widget.actions ?? [], - if (widget.enableCaptureCourseTable) - IconButton( - icon: Icon(ApIcon.download), - onPressed: _captureCourseTable, - tooltip: - context.ap.exportCourseTable, ), - IconButton( - icon: Icon(ApIcon.settings), - onPressed: _openSettings, - tooltip: - context.ap.courseScaffoldSetting, - ), - ], - ), - body: Row( - children: [ - Expanded( - flex: 3, - child: Flex( - direction: Axis.vertical, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: - MainAxisAlignment.spaceEvenly, - children: [ - if (widget.customHint != null && - widget.customHint!.isNotEmpty) - HintBanner( - text: widget.customHint!, - ), - Expanded( - child: RefreshIndicator( - onRefresh: () async { - await widget.onRefresh!(); - AnalyticsUtil.instance - .logEvent( - 'course_refresh', - ); - return; + GlassFloatingToolbar( + leading: [ + if (widget.semesterData != null && widget.itemPicker == null) + GlassSemesterPicker( + semesterData: widget.semesterData!, + currentIndex: widget.semesterData!.currentIndex, + onSelect: (Semester semester, int index) { + widget.onSelect?.call(index); }, - child: _body(), - ), - ), + featureTag: 'course', + ) + else if (widget.itemPicker != null) + widget.itemPicker!, ], - ), - ), - if (widget.state == - CourseState.finish && - isLandscape) ...[ - const SizedBox(width: 16.0), - Expanded( - flex: 2, - child: GlassCard( - useOwnLayer: true, - padding: EdgeInsets.zero, - child: GlassCourseList( - courses: - widget.courseData.courses, - timeCodes: - widget.courseData.timeCodes, - invisibleCourseCodes: - invisibleCourseCodes, - onVisibilityChanged: ( - Course course, - bool visibility, - ) => - saveInvisibleCourseCodes( - course: course, - visibility: visibility, + trailing: [ + ...widget.actions ?? [], + if (widget.enableCaptureCourseTable) + IconButton( + icon: Icon(ApIcon.download), + onPressed: _captureCourseTable, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: + const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + IconButton( + icon: Icon(ApIcon.settings), + onPressed: _openSettings, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: + const BoxConstraints( + minWidth: 36, + minHeight: 36, ), ), - ), + ], ), ], - ], - ), - floatingActionButton: AnimatedScale( - scale: _showFab ? 1.0 : 0.0, - duration: - const Duration(milliseconds: 250), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.end, - children: [ - if (!isLandscape) - GlassButton( - key: const ValueKey( - 'switch_content_style_button', - ), - icon: Icon( - _contentStyle == - ContentStyle.table - ? Icons.list_rounded - : Icons.grid_view_rounded, - ), - onTap: () { - setState( - () => _contentStyle = - (_contentStyle == - ContentStyle.table) - ? ContentStyle.list - : ContentStyle.table, - ); - }, - useOwnLayer: true, - ), - if (showSearchButton ?? - true) ...[ + ), + floatingActionButton: AnimatedScale( + scale: _showFab ? 1.0 : 0.0, + duration: const Duration(milliseconds: 250), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ if (!isLandscape) - const SizedBox(height: 8), - GlassButton( - key: const ValueKey( - 'search_button', + GlassButton( + key: const ValueKey('switch_content_style_button'), + icon: Icon( + _contentStyle == ContentStyle.table + ? Icons.list_rounded + : Icons.grid_view_rounded, + ), + onTap: () { + setState( + () => _contentStyle = + (_contentStyle == ContentStyle.table) + ? ContentStyle.list + : ContentStyle.table, + ); + }, + useOwnLayer: true, ), - icon: const Icon(Icons.search), - onTap: () { - _pickSemester(); - AnalyticsUtil.instance.logEvent( - 'course_search_button_click', - ); - }, - useOwnLayer: true, - ), + if (showSearchButton ?? true) ...[ + if (!isLandscape) const SizedBox(height: 8), + GlassButton( + key: const ValueKey('search_button'), + icon: const Icon(Icons.search), + onTap: () { + _pickSemester(); + AnalyticsUtil.instance.logEvent( + 'course_search_button_click', + ); + }, + useOwnLayer: true, + ), + ], ], - ], + ), ), ), ), - ), ); } @@ -411,16 +377,9 @@ class GlassCourseScaffoldState width: 80, height: 80, padding: EdgeInsets.zero, - shape: - const LiquidRoundedSuperellipse( - borderRadius: 20, - ), + shape: const LiquidRoundedSuperellipse(borderRadius: 20), child: Center( - child: Icon( - icon, - size: 40, - color: colorScheme.primary, - ), + child: Icon(icon, size: 40, color: colorScheme.primary), ), ), const SizedBox(height: 16), @@ -435,10 +394,7 @@ class GlassCourseScaffoldState const SizedBox(height: 8), Text( context.ap.clickToRetry, - style: TextStyle( - fontSize: 14, - color: colorScheme.primary, - ), + style: TextStyle(fontSize: 14, color: colorScheme.primary), ), ], ), @@ -447,25 +403,10 @@ class GlassCourseScaffoldState } Widget _body() { - final ColorScheme colorScheme = - Theme.of(context).colorScheme; + final ColorScheme colorScheme = Theme.of(context).colorScheme; switch (widget.state) { case CourseState.loading: - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 48, - height: 48, - child: CircularProgressIndicator( - strokeWidth: 3, - color: colorScheme.primary, - ), - ), - ], - ), - ); + return const Center(child: GlassProgressIndicator.circular()); case CourseState.error: return _buildErrorState( colorScheme, @@ -487,48 +428,35 @@ class GlassCourseScaffoldState case CourseState.custom: return _buildErrorState( colorScheme, - widget.customStateHint ?? - context.ap.unknownError, + widget.customStateHint ?? context.ap.unknownError, Icons.warning_amber_rounded, ); default: - if (isLandscape || - _contentStyle == ContentStyle.table) { + if (isLandscape || _contentStyle == ContentStyle.table) { return GlassCourseTableView( courseData: widget.courseData, - invisibleCourseCodes: - invisibleCourseCodes, + invisibleCourseCodes: invisibleCourseCodes, controller: _scrollController, - onCoursePressed: ( - Course course, - TimeCode timeCode, - int weekday, - ) => + onCoursePressed: (Course course, TimeCode timeCode, int weekday) => _onPressed(weekday, timeCode, course), courseColorResolver: _getCourseColor, - repaintBoundaryKey: - _repaintBoundaryGlobalKey, + repaintBoundaryKey: _repaintBoundaryGlobalKey, mergeCourse: mergeCourse, showSectionTime: showSectionTime, showInstructors: showInstructors, - showClassroomLocation: - showClassroomLocation, + showClassroomLocation: showClassroomLocation, ); } else { return GlassCourseList( controller: _scrollController, courses: widget.courseData.courses, timeCodes: widget.courseData.timeCodes, - invisibleCourseCodes: - invisibleCourseCodes, - onVisibilityChanged: ( - Course course, - bool visibility, - ) => + invisibleCourseCodes: invisibleCourseCodes, + onVisibilityChanged: (Course course, bool visibility) => saveInvisibleCourseCodes( - course: course, - visibility: visibility, - ), + course: course, + visibility: visibility, + ), ); } } @@ -536,27 +464,20 @@ class GlassCourseScaffoldState Future _captureCourseTable() async { final RenderRepaintBoundary? boundary = - _repaintBoundaryGlobalKey.currentContext! - .findRenderObject() + _repaintBoundaryGlobalKey.currentContext!.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) { - UiUtil.instance.showToast( - context, - context.ap.unknownError, - ); + UiUtil.instance.showToast(context, context.ap.unknownError); return; } - final ui.Image image = - await boundary.toImage(pixelRatio: 3.0); + final ui.Image image = await boundary.toImage(pixelRatio: 3.0); final ByteData? byteData = await image.toByteData( format: ui.ImageByteFormat.png, ); final DateTime now = DateTime.now(); - final String formattedDate = - DateFormat('yyyyMMdd_hhmmss').format(now); + final String formattedDate = DateFormat('yyyyMMdd_hhmmss').format(now); if (byteData != null) { - final SaveImageResult result = - await MediaUtil.instance.saveImage( + final SaveImageResult result = await MediaUtil.instance.saveImage( byteData: byteData, fileName: 'course_table_$formattedDate', ); @@ -567,47 +488,32 @@ class GlassCourseScaffoldState '${context.ap.exportCourseTableSuccess}' '\n$filePath'; Toast.show(message, context); - AnalyticsUtil.instance.logEvent( - 'export_course_table_image_success', - ); + AnalyticsUtil.instance.logEvent('export_course_table_image_success'); case SaveImageError(:final String message): UiUtil.instance.showToast(context, message); } } else { if (!mounted) return; - UiUtil.instance.showToast( - context, - context.ap.unknownError, - ); + UiUtil.instance.showToast(context, context.ap.unknownError); } } - void _onPressed( - int weekday, - TimeCode timeCode, - Course course, - ) { - showModalBottomSheet( + void _onPressed(int weekday, TimeCode timeCode, Course course) { + GlassSheet.show( context: context, - backgroundColor: const Color(0x00000000), isScrollControlled: true, builder: (BuildContext builder) { return GlassCourseContent( - enableNotifyControl: - widget.enableNotifyControl, + enableNotifyControl: widget.enableNotifyControl, course: course, notifyData: widget.notifyData, weekday: weekday, - courseNotifySaveKey: - widget.courseNotifySaveKey, + courseNotifySaveKey: widget.courseNotifySaveKey, timeCode: timeCode, courseColor: _getCourseColor(course.code), invisibleCourseCodes: invisibleCourseCodes, onVisibilityChanged: (bool visibility) => - saveInvisibleCourseCodes( - course: course, - visibility: visibility, - ), + saveInvisibleCourseCodes(course: course, visibility: visibility), ); }, ); @@ -615,11 +521,10 @@ class GlassCourseScaffoldState void _pickSemester() { if (widget.semesterData != null) { - SemesterPicker.show( + GlassSemesterPicker.show( context: context, semesterData: widget.semesterData!, - currentIndex: - widget.semesterData!.currentIndex, + currentIndex: widget.semesterData!.currentIndex, onSelect: (Semester semester, int index) { widget.onSelect?.call(index); }, @@ -629,56 +534,81 @@ class GlassCourseScaffoldState } void _openSettings() { - showDialog( + showGlassDefaultDialog( context: context, - builder: (_) => CourseScaffoldSettingDialog( - showSectionTime: showSectionTime, - showInstructors: showInstructors, - showClassroomLocation: showClassroomLocation, - showSearchButton: showSearchButton, - mergeCourse: mergeCourse, - showSectionTimeOnChanged: (bool? value) { - setState(() => showSectionTime = value); - PreferenceUtil.instance.setBool( - ApConstants.showSectionTime, - showSectionTime!, - ); - }, - showInstructorsOnChanged: (bool? value) { - setState(() => showInstructors = value); - PreferenceUtil.instance.setBool( - ApConstants.showInstructors, - showInstructors!, - ); - }, - showClassroomLocationOnChanged: - (bool? value) { - setState( - () => showClassroomLocation = value, - ); - PreferenceUtil.instance.setBool( - ApConstants.showClassroomLocation, - showClassroomLocation!, - ); - }, - showSearchButtonOnChanged: (bool? value) { - setState(() => showSearchButton = value); - PreferenceUtil.instance.setBool( - ApConstants.showCourseSearchButton, - showSearchButton!, - ); - }, - mergeCourseOnChanged: (bool? value) { - setState(() => mergeCourse = value); - PreferenceUtil.instance.setBool( - '${ApConstants.packageName}.merge_course', - mergeCourse!, - ); - }, + title: context.ap.courseScaffoldSetting, + actionText: context.ap.confirm, + actionFunction: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + contentWidget: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CheckboxListTile( + title: Text(context.ap.showSectionTime), + secondary: Icon(ApIcon.accessTime), + value: showSectionTime, + onChanged: (bool? value) { + setState(() => showSectionTime = value); + PreferenceUtil.instance.setBool( + ApConstants.showSectionTime, + showSectionTime!, + ); + }, + ), + CheckboxListTile( + title: Text(context.ap.showInstructors), + secondary: Icon(ApIcon.person), + value: showInstructors, + onChanged: (bool? value) { + setState(() => showInstructors = value); + PreferenceUtil.instance.setBool( + ApConstants.showInstructors, + showInstructors!, + ); + }, + ), + CheckboxListTile( + title: Text(context.ap.showClassroomLocation), + secondary: Icon(ApIcon.locationOn), + value: showClassroomLocation, + onChanged: (bool? value) { + setState(() => showClassroomLocation = value); + PreferenceUtil.instance.setBool( + ApConstants.showClassroomLocation, + showClassroomLocation!, + ); + }, + ), + CheckboxListTile( + title: Text(context.ap.showSearchButton), + secondary: const Icon(Icons.search), + value: showSearchButton, + onChanged: (bool? value) { + setState(() => showSearchButton = value); + PreferenceUtil.instance.setBool( + ApConstants.showCourseSearchButton, + showSearchButton!, + ); + }, + ), + CheckboxListTile( + title: Text(context.ap.mergeCourse), + secondary: const Icon(Icons.merge_type_rounded), + value: mergeCourse, + onChanged: (bool? value) { + setState(() => mergeCourse = value); + PreferenceUtil.instance.setBool( + '${ApConstants.packageName}' + '.merge_course', + mergeCourse!, + ); + }, + ), + ], ), ); - AnalyticsUtil.instance - .logEvent('course_setting_click'); + AnalyticsUtil.instance.logEvent('course_setting_click'); } void saveInvisibleCourseCodes({ @@ -699,8 +629,7 @@ class GlassCourseScaffoldState } void fetchInvisibleCourseCodes() { - invisibleCourseCodes = - PreferenceUtil.instance.getStringList( + invisibleCourseCodes = PreferenceUtil.instance.getStringList( '$_kCourseInvisibleKey' '${widget.courseNotifySaveKey}', [], diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart index 63508891..cc2832b2 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_home_page_scaffold.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:io'; import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_dialog.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; @@ -21,6 +23,7 @@ class GlassHomePageScaffold extends StatefulWidget { this.actions, this.onTabTapped, this.bottomNavigationBarItems, + this.selectedIndex = 0, this.drawer, this.content, this.floatingActionButton, @@ -29,6 +32,7 @@ class GlassHomePageScaffold extends StatefulWidget { this.autoPlay = true, this.autoPlayDuration = const Duration(milliseconds: 5000), + this.bottomPadding = 0, }); /// Creates from a [DataState>]. @@ -39,6 +43,7 @@ class GlassHomePageScaffold extends StatefulWidget { this.actions, this.onTabTapped, this.bottomNavigationBarItems, + this.selectedIndex = 0, this.drawer, this.content, this.floatingActionButton, @@ -47,6 +52,7 @@ class GlassHomePageScaffold extends StatefulWidget { this.autoPlay = true, this.autoPlayDuration = const Duration(milliseconds: 5000), + this.bottomPadding = 0, }) : state = dataState.when( loading: () => HomeState.loading, loaded: (_, __) => HomeState.finish, @@ -60,7 +66,8 @@ class GlassHomePageScaffold extends StatefulWidget { final String? title; final List announcements; final List? actions; - final List? bottomNavigationBarItems; + final List? bottomNavigationBarItems; + final int selectedIndex; final Function(int index)? onTabTapped; final Function(Announcement announcement)? onImageTapped; final Widget? drawer; @@ -68,6 +75,11 @@ class GlassHomePageScaffold extends StatefulWidget { final Widget? floatingActionButton; final bool isLogin; final bool autoPlay; + + /// Extra bottom padding for the content area. + /// Use when the scaffold is embedded inside + /// another layout with a floating bottom bar. + final double bottomPadding; final Duration autoPlayDuration; @override @@ -115,57 +127,116 @@ class GlassHomePageScaffoldState child: ScaffoldMessenger( key: _scaffoldMessengerKey, child: Scaffold( - extendBodyBehindAppBar: true, - appBar: isTablet - ? null - : GlassAppBar( - title: Text(widget.title ?? ''), - actions: widget.actions, - ), drawer: isTablet ? null : widget.drawer, floatingActionButton: widget.floatingActionButton, - body: Row( + body: Stack( children: [ - if (isTablet) widget.drawer!, - Expanded( - child: (isTablet && - widget.content != null) - ? widget.content! - : OrientationBuilder( - builder: - (_, Orientation orientation) { - return Container( - padding: - EdgeInsets.symmetric( - vertical: orientation == - Orientation.portrait - ? 32.0 - : 4.0, - ), - alignment: Alignment.center, - child: - _homeBody(orientation), - ); - }, - ), + Row( + children: [ + if (isTablet) widget.drawer!, + Expanded( + child: (isTablet && + widget.content != null) + ? widget.content! + : OrientationBuilder( + builder: ( + _, + Orientation orientation, + ) { + return Container( + padding: + EdgeInsets.only( + top: MediaQuery.of( + context, + ).padding.top + + 60 + + (orientation == + Orientation + .portrait + ? 8.0 + : 4.0), + bottom: widget + .bottomNavigationBarItems != + null + ? 72.0 + : widget + .bottomPadding, + ), + alignment: + Alignment.center, + child: _homeBody( + orientation, + ), + ); + }, + ), + ), + ], ), + if (!isTablet) + GlassFloatingToolbar( + leading: [ + Text( + widget.title ?? '', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + trailing: widget.actions ?? + const [], + ), + if (widget.bottomNavigationBarItems != + null && + !isTablet) + Positioned( + left: 16, + right: MediaQuery.of(context) + .size + .width * + 0.2, + bottom: MediaQuery.of(context) + .padding + .bottom + + 8, + child: Builder( + builder: (BuildContext ctx) { + final bool isDark = + Theme.of(ctx).brightness == + Brightness.dark; + final Color iconColor = isDark + ? Colors.white + : Colors.black; + return GlassBottomBar( + tabs: widget + .bottomNavigationBarItems!, + selectedIndex: + widget.selectedIndex, + onTabSelected: (int index) { + widget.onTabTapped + ?.call(index); + }, + barHeight: 52, + barBorderRadius: 26, + horizontalPadding: 10, + verticalPadding: 10, + iconSize: 20, + selectedIconColor: iconColor, + unselectedIconColor: + iconColor.withAlpha(153), + textStyle: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: iconColor, + ), + ); + }, + ), + ), ], ), - bottomNavigationBar: - (widget.bottomNavigationBarItems == null || - isTablet) - ? null - : NavigationBar( - elevation: 12.0, - height: 56, - indicatorColor: - const Color(0x00000000), - onDestinationSelected: - widget.onTabTapped, - destinations: widget - .bottomNavigationBarItems!, - ), ), ), ), @@ -257,7 +328,7 @@ class GlassHomePageScaffoldState switch (widget.state) { case HomeState.loading: return const Center( - child: CircularProgressIndicator(), + child: GlassProgressIndicator.circular(), ); case HomeState.finish: return Column( @@ -265,20 +336,17 @@ class GlassHomePageScaffoldState children: [ Hero( tag: ApConstants.tagAnnouncementTitle, - child: Material( - color: const Color(0x00000000), - child: Text( - widget - .announcements[_currentNewsIndex] - .title, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20.0, - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - fontWeight: FontWeight.w500, - ), + child: Text( + widget + .announcements[_currentNewsIndex] + .title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20.0, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + fontWeight: FontWeight.w500, ), ), ), @@ -321,8 +389,12 @@ class GlassHomePageScaffoldState ), children: [ TextSpan( - text: - '${widget.announcements.length >= 10 && _currentNewsIndex < 9 ? '0' : ''}' + text: '${widget.announcements + .length >= + 10 && + _currentNewsIndex < 9 + ? '0' + : ''}' '${_currentNewsIndex + 1}', style: TextStyle( color: Theme.of(context) @@ -365,27 +437,25 @@ class GlassHomePageScaffoldState void _showLogoutDialog() { final ApLocalizations l10n = context.ap; - showDialog( + showGlassYesNoDialog( context: context, - builder: (BuildContext context) => YesNoDialog( - title: l10n.closeAppTitle, - contentWidget: Text( - l10n.closeAppHint, - textAlign: TextAlign.center, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), + title: l10n.closeAppTitle, + contentWidget: Text( + l10n.closeAppHint, + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, ), - leftActionText: l10n.cancel, - rightActionText: l10n.confirm, - rightActionFunction: () { - AnalyticsUtil.instance - .logEvent('logout_dialog_confirm'); - SystemNavigator.pop(); - }, ), + leftActionText: l10n.cancel, + rightActionText: l10n.confirm, + rightActionFunction: () { + AnalyticsUtil.instance + .logEvent('logout_dialog_confirm'); + SystemNavigator.pop(); + }, ); AnalyticsUtil.instance .logEvent('logout_dialog_open'); diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart index a6593fb2..9236f284 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_image_viewer_scaffold.dart @@ -1,4 +1,5 @@ import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; import 'package:flutter/material.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; import 'package:photo_view/photo_view.dart'; @@ -50,41 +51,57 @@ class GlassImageViewerScaffoldState Widget build(BuildContext context) { return AdaptiveLiquidGlassLayer( child: Scaffold( - extendBodyBehindAppBar: true, - appBar: GlassAppBar( - title: Text(widget.title ?? ''), - actions: widget.actions, - ), - body: Column( - children: [ - SizedBox( - height: MediaQuery.of(context).padding.top + - 44, - ), - if (widget.imageViewers.length > 1) - TabBar( - controller: _tabController, - tabs: [ - for (final ImageViewer image - in widget.imageViewers) - Tab(text: image.title), - ], + body: Stack( + children: [ + Padding( + padding: EdgeInsets.only( + top: + MediaQuery.of(context).padding.top + + 60, + ), + child: Column( + children: [ + if (widget.imageViewers.length > 1) + TabBar( + controller: _tabController, + tabs: [ + for (final ImageViewer image + in widget.imageViewers) + Tab(text: image.title), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + for (final ImageViewer image + in widget.imageViewers) + PhotoView( + imageProvider: AssetImage( + image.assetName, + ), + ), + ], + ), + ), + ], + ), ), - Expanded( - child: TabBarView( - controller: _tabController, - children: [ - for (final ImageViewer image - in widget.imageViewers) - PhotoView( - imageProvider: - AssetImage(image.assetName), + GlassFloatingToolbar( + leading: [ + Text( + widget.title ?? '', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, ), + ), ], + trailing: + widget.actions ?? const [], ), - ), - ], - ), + ], + ), ), ); } diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart index 27b06c6f..5becb9d3 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_login_scaffold.dart @@ -105,17 +105,13 @@ class GlassLoginScaffoldState Widget _logo(ColorScheme colorScheme, bool isDark) { switch (widget.logoMode) { case LogoMode.image: - return GlassCard( - child: SizedBox( - width: 100, - height: 100, - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: Image.asset( - widget.logoSource, - fit: BoxFit.contain, - ), - ), + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.asset( + widget.logoSource, + width: 80, + height: 80, + fit: BoxFit.contain, ), ); case LogoMode.text: @@ -134,16 +130,19 @@ class GlassLoginScaffoldState ) { final Widget logoWidget = _logo(colorScheme, isDark); - final Widget formCard = GlassCard( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: widget.forms, - ), + final Widget formCard = ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: 400), + child: GlassCard( + useOwnLayer: true, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 24, + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: widget.forms, ), ), ); diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart index 2af49afd..cc330e4f 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_score_scaffold.dart @@ -1,6 +1,8 @@ import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; import 'package:ap_common_liquid_glass/src/widgets/glass_score_analysis_tab.dart'; import 'package:ap_common_liquid_glass/src/widgets/glass_score_list_tab.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_semester_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; @@ -50,23 +52,19 @@ class GlassScoreScaffold extends StatefulWidget { this.finalScoreBuilder, this.isShowSearchButton = false, this.bottom, - }) : state = dataState.when( - loading: () => ScoreState.loading, - loaded: (_, __) => ScoreState.finish, - error: (_) => ScoreState.error, - empty: (_) => ScoreState.empty, - ), - scoreData = dataState.dataOrNull, - customHint = - dataState is DataLoaded - ? dataState.hint - : null, - customStateHint = - dataState is DataError - ? dataState.hint - : dataState is DataEmpty - ? dataState.hint - : null; + }) : state = dataState.when( + loading: () => ScoreState.loading, + loaded: (_, __) => ScoreState.finish, + error: (_) => ScoreState.error, + empty: (_) => ScoreState.empty, + ), + scoreData = dataState.dataOrNull, + customHint = dataState is DataLoaded ? dataState.hint : null, + customStateHint = dataState is DataError + ? dataState.hint + : dataState is DataEmpty + ? dataState.hint + : null; final ScoreState state; final String? customStateHint; @@ -87,17 +85,14 @@ class GlassScoreScaffold extends StatefulWidget { final Widget? bottom; @override - GlassScoreScaffoldState createState() => - GlassScoreScaffoldState(); + GlassScoreScaffoldState createState() => GlassScoreScaffoldState(); } -class GlassScoreScaffoldState - extends State { +class GlassScoreScaffoldState extends State { bool get isLandscape => - MediaQuery.of(context).orientation == - Orientation.landscape; + MediaQuery.of(context).orientation == Orientation.landscape; - bool _isAnalysisView = false; + bool _isAnalysisView = true; late ScrollController _scrollController; bool _showFab = true; @@ -111,9 +106,7 @@ class GlassScoreScaffoldState if (_showFab) { setState(() => _showFab = false); } - } else if (_scrollController - .position - .userScrollDirection == + } else if (_scrollController.position.userScrollDirection == ScrollDirection.forward) { if (!_showFab) { setState(() => _showFab = true); @@ -130,159 +123,123 @@ class GlassScoreScaffoldState @override Widget build(BuildContext context) { - final ColorScheme colorScheme = - Theme.of(context).colorScheme; + final ColorScheme colorScheme = Theme.of(context).colorScheme; return AdaptiveLiquidGlassLayer( child: Scaffold( - extendBodyBehindAppBar: true, - appBar: GlassAppBar( - title: Row( + floatingActionButton: AnimatedScale( + scale: _showFab ? 1.0 : 0.0, + duration: const Duration(milliseconds: 250), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - Flexible( - child: Text( - widget.title ?? context.ap.score, - overflow: TextOverflow.ellipsis, - ), - ), - if (widget.itemPicker != null) ...[ - const SizedBox(width: 12), - widget.itemPicker!, - ], - if (widget.semesterData != null && - widget.itemPicker == - null) ...[ - const SizedBox(width: 12), - SemesterPicker( - semesterData: widget.semesterData!, - currentIndex: - widget.semesterData!.currentIndex, - onSelect: - (Semester semester, int index) { - widget.onSelect?.call(index); - }, - featureTag: 'score', - ), - ], - ], - ), - actions: const [], - ), - floatingActionButton: AnimatedScale( - scale: _showFab ? 1.0 : 0.0, - duration: const Duration(milliseconds: 250), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (widget.state == ScoreState.finish && - widget.scoreData != null && - widget.scoreData!.scores.isNotEmpty && - !isLandscape) - GlassButton( - key: const ValueKey( - 'switch_view_button', - ), - icon: Icon( - _isAnalysisView - ? Icons.list_alt_rounded - : Icons.analytics_outlined, - ), - onTap: () { - setState( - () => _isAnalysisView = - !_isAnalysisView, - ); - }, - ), - if (widget - .isShowSearchButton) ...[ if (widget.state == ScoreState.finish && widget.scoreData != null && widget.scoreData!.scores.isNotEmpty && !isLandscape) - const SizedBox(height: 8), - GlassButton( - key: const ValueKey( - 'search_button', - ), - icon: const Icon(Icons.search), - onTap: () { - _pickSemester(); - AnalyticsUtil.instance.logEvent( - 'score_search_button_click', - ); - }, - ), - ], - ], - ), - ), - body: Row( - children: [ - Expanded( - flex: 3, - child: Column( - children: [ - if (widget.customHint != null && - widget.customHint!.isNotEmpty) - HintBanner(text: widget.customHint!), - Expanded( - child: _buildContent( - context, - colorScheme, + GlassButton( + key: const ValueKey('switch_view_button'), + icon: Icon( + _isAnalysisView + ? Icons.list_alt_rounded + : Icons.analytics_outlined, ), + onTap: () { + setState(() => _isAnalysisView = !_isAnalysisView); + }, + ), + if (widget.isShowSearchButton) ...[ + if (widget.state == ScoreState.finish && + widget.scoreData != null && + widget.scoreData!.scores.isNotEmpty && + !isLandscape) + const SizedBox(height: 8), + GlassButton( + key: const ValueKey('search_button'), + icon: const Icon(Icons.search), + onTap: () { + _pickSemester(); + AnalyticsUtil.instance.logEvent( + 'score_search_button_click', + ); + }, ), ], - ), + ], ), - if (widget.state == ScoreState.finish && - isLandscape) ...[ - const SizedBox(width: 16.0), - Expanded( - flex: 2, - child: GlassCard( - useOwnLayer: true, - padding: EdgeInsets.zero, - child: GlassScoreListTab( - scoreData: widget.scoreData!, - onRefresh: widget.onRefresh, - middleTitle: widget.middleTitle, - finalTitle: widget.finalTitle, - onScoreSelect: - widget.onScoreSelect, - middleScoreBuilder: - widget.middleScoreBuilder, - finalScoreBuilder: - widget.finalScoreBuilder, - ), + ), + body: Stack( + children: [ + Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 60, + ), + child: Row( + children: [ + Expanded( + flex: 3, + child: Column( + children: [ + if (widget.customHint != null && + widget.customHint!.isNotEmpty) + HintBanner(text: widget.customHint!), + Expanded(child: _buildContent(context, colorScheme)), + ], + ), + ), + if (widget.state == ScoreState.finish && + isLandscape) ...[ + const SizedBox(width: 16.0), + Expanded( + flex: 2, + child: GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: GlassScoreListTab( + scoreData: widget.scoreData!, + onRefresh: widget.onRefresh, + middleTitle: widget.middleTitle, + finalTitle: widget.finalTitle, + onScoreSelect: widget.onScoreSelect, + middleScoreBuilder: widget.middleScoreBuilder, + finalScoreBuilder: widget.finalScoreBuilder, + ), + ), + ), + ], + ], ), ), + GlassFloatingToolbar( + leading: [ + if (widget.semesterData != null && widget.itemPicker == null) + GlassSemesterPicker( + semesterData: widget.semesterData!, + currentIndex: widget.semesterData!.currentIndex, + onSelect: (Semester semester, int index) { + widget.onSelect?.call(index); + }, + featureTag: 'score', + ) + else if (widget.itemPicker != null) + widget.itemPicker!, + ], + ), ], - ], - ), + ), ), ); } - Widget _buildContent( - BuildContext context, - ColorScheme colorScheme, - ) { + Widget _buildContent(BuildContext context, ColorScheme colorScheme) { switch (widget.state) { case ScoreState.loading: return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - width: 48, - height: 48, - child: CircularProgressIndicator( - strokeWidth: 3, - color: colorScheme.primary, - ), - ), + const GlassProgressIndicator.circular(), const SizedBox(height: 16), Text( context.ap.loading, @@ -315,8 +272,7 @@ class GlassScoreScaffoldState case ScoreState.custom: return _buildErrorState( colorScheme, - widget.customStateHint ?? - context.ap.somethingError, + widget.customStateHint ?? context.ap.somethingError, Icons.warning_amber_rounded, ); case ScoreState.finish: @@ -336,10 +292,8 @@ class GlassScoreScaffoldState middleTitle: widget.middleTitle, finalTitle: widget.finalTitle, onScoreSelect: widget.onScoreSelect, - middleScoreBuilder: - widget.middleScoreBuilder, - finalScoreBuilder: - widget.finalScoreBuilder, + middleScoreBuilder: widget.middleScoreBuilder, + finalScoreBuilder: widget.finalScoreBuilder, controller: _scrollController, ); } @@ -366,11 +320,7 @@ class GlassScoreScaffoldState useOwnLayer: true, width: 80, height: 80, - child: Icon( - icon, - size: 40, - color: colorScheme.primary, - ), + child: Icon(icon, size: 40, color: colorScheme.primary), ), const SizedBox(height: 16), Text( @@ -381,15 +331,11 @@ class GlassScoreScaffoldState ), textAlign: TextAlign.center, ), - if (widget.state != - ScoreState.empty) ...[ + if (widget.state != ScoreState.empty) ...[ const SizedBox(height: 8), Text( context.ap.clickToRetry, - style: TextStyle( - fontSize: 14, - color: colorScheme.primary, - ), + style: TextStyle(fontSize: 14, color: colorScheme.primary), ), ], ], @@ -400,11 +346,10 @@ class GlassScoreScaffoldState void _pickSemester() { if (widget.semesterData != null) { - SemesterPicker.show( + GlassSemesterPicker.show( context: context, semesterData: widget.semesterData!, - currentIndex: - widget.semesterData!.currentIndex, + currentIndex: widget.semesterData!.currentIndex, onSelect: (Semester semester, int index) { widget.onSelect?.call(index); }, diff --git a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart index a1ce2c95..236538ee 100644 --- a/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart +++ b/packages/ap_common_liquid_glass/lib/src/scaffold/glass_user_info_scaffold.dart @@ -1,4 +1,5 @@ import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; import 'package:barcode_widget/barcode_widget.dart'; import 'package:flutter/material.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; @@ -51,46 +52,9 @@ class GlassUserInfoScaffoldState return AdaptiveLiquidGlassLayer( child: Scaffold( - extendBodyBehindAppBar: true, - appBar: GlassAppBar( - actions: [ - ...widget.actions ?? [], - if (widget.enableBarCode) - IconButton( - icon: Image.asset( - iconName, - height: 24.0, - width: 24.0, - ), - onPressed: () { - setState( - () => codeMode = BarCodeMode.values[ - (codeMode.index + 1) % - BarCodeMode.values.length], - ); - AnalyticsUtil.instance - .logEvent('user_info_barcode_switch'); - }, - ), - IconButton( - icon: _isRefreshing - ? SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: isDark - ? colorScheme.onSurface - : colorScheme.onPrimary, - ), - ) - : const Icon(Icons.refresh_rounded), - onPressed: - _isRefreshing ? null : _handleRefresh, - ), - ], - ), - body: RefreshIndicator( + body: Stack( + children: [ + RefreshIndicator( onRefresh: () async { await _handleRefresh(); AnalyticsUtil.instance @@ -120,7 +84,68 @@ class GlassUserInfoScaffoldState ], ), ), - ), + ), + GlassFloatingToolbar( + trailing: [ + ...widget.actions ?? [], + if (widget.enableBarCode) + IconButton( + icon: Image.asset( + iconName, + height: 20.0, + width: 20.0, + ), + onPressed: () { + setState( + () => codeMode = + BarCodeMode.values[ + (codeMode.index + + 1) % + BarCodeMode + .values + .length], + ); + AnalyticsUtil.instance + .logEvent( + 'user_info_barcode_switch', + ); + }, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: + const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + IconButton( + icon: _isRefreshing + ? const SizedBox( + width: 20, + height: 20, + child: + CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon( + Icons.refresh_rounded, + ), + onPressed: _isRefreshing + ? null + : _handleRefresh, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: + const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + ], + ), + ], + ), ), ); } @@ -278,18 +303,12 @@ class GlassUserInfoScaffoldState ], ), const SizedBox(height: 16), - Container( + GlassContainer( width: double.infinity, padding: const EdgeInsets.symmetric( vertical: 24, horizontal: 16, ), - decoration: BoxDecoration( - color: colorScheme - .surfaceContainerHighest, - borderRadius: - BorderRadius.circular(12), - ), child: Column( children: [ Text( @@ -338,11 +357,8 @@ class GlassUserInfoScaffoldState } Widget _buildDivider(ColorScheme colorScheme) { - return Divider( - height: 1, + return const GlassDivider( indent: 72, - color: - colorScheme.outlineVariant.withAlpha(77), ); } diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_floating_toolbar.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_floating_toolbar.dart new file mode 100644 index 00000000..8120c03a --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_floating_toolbar.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A floating toolbar inspired by Apple iOS 26 Liquid Glass. +/// +/// Renders two capsule-shaped glass groups (leading and +/// trailing) that float below the status bar, overlaying +/// content. Use inside a [Stack]. +/// +/// Widgets placed in [leading] and [trailing] should **not** +/// have their own glass layer — the toolbar capsule provides +/// the glass effect. Use plain [IconButton] or [Text]. +class GlassFloatingToolbar extends StatelessWidget { + const GlassFloatingToolbar({ + super.key, + this.leading = const [], + this.trailing = const [], + }); + + /// Widgets on the left capsule. + final List leading; + + /// Widgets on the right capsule. + final List trailing; + + @override + Widget build(BuildContext context) { + final double topPadding = + MediaQuery.of(context).padding.top; + + return Positioned( + top: topPadding + 6, + left: 16, + right: 16, + child: Row( + children: [ + if (leading.isNotEmpty) + GlassCard( + useOwnLayer: true, + height: 38, + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + shape: + const LiquidRoundedSuperellipse( + borderRadius: 19, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: leading, + ), + ), + const Spacer(), + if (trailing.isNotEmpty) + GlassCard( + useOwnLayer: true, + height: 38, + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + shape: + const LiquidRoundedSuperellipse( + borderRadius: 19, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: trailing, + ), + ), + ], + ), + ); + } +} From 3b4f4d23c741f0e05cdbfd44b01ded6711899300 Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Tue, 14 Apr 2026 23:40:43 +0800 Subject: [PATCH 11/14] feat(liquid_glass): add GlassSemesterPicker with glass-styled chip and sheet Create glass version of SemesterPicker with GlassCard chip button and GlassSheet.show for the semester selection bottom sheet. Semester items use GlassCard, GlassDivider, and GlassProgressIndicator. --- .../src/widgets/glass_semester_picker.dart | 910 ++++++++++++++++++ 1 file changed, 910 insertions(+) create mode 100644 packages/ap_common_liquid_glass/lib/src/widgets/glass_semester_picker.dart diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_semester_picker.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_semester_picker.dart new file mode 100644 index 00000000..8ad61ec6 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_semester_picker.dart @@ -0,0 +1,910 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [SemesterPicker]. +/// +/// Replaces Material widgets with Liquid Glass equivalents while +/// maintaining the same API surface. The inline chip button uses +/// [GlassCard] and [Icons], and the bottom sheet uses +/// [GlassSheet] with glass-styled semester items. +class GlassSemesterPicker extends StatefulWidget { + const GlassSemesterPicker({ + super.key, + required this.semesterData, + this.onSelect, + this.featureTag, + this.currentIndex = 0, + this.uiConfig, + this.controller, + }); + + final SemesterData semesterData; + final SemesterCallback? onSelect; + final String? featureTag; + final int currentIndex; + final SemesterUIConfig? uiConfig; + final SemesterPickerController? controller; + + @override + GlassSemesterPickerState createState() => + GlassSemesterPickerState(); + + /// Shows a glass-styled semester picker bottom sheet. + static void show({ + required BuildContext context, + required SemesterData semesterData, + int currentIndex = 0, + SemesterCallback? onSelect, + String? featureTag, + Set? loadingSemesters, + Set? emptySemesters, + SemesterUIConfig? uiConfig, + SemesterPickerController? controller, + }) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final List> sortedSemesters = + _getSortedSemesters(semesterData, uiConfig); + + final Map>> + groupedByYear = + >>{}; + for (final MapEntry entry + in sortedSemesters) { + final String year = entry.value.year; + groupedByYear.putIfAbsent( + year, + () => >[], + ); + groupedByYear[year]!.add(entry); + } + + // Prefer controller's sets; fall back to explicit + // parameters. + final Set? effectiveLoading = + controller?.loadingSemesters ?? loadingSemesters; + final Set? effectiveEmpty = + controller?.emptySemesters ?? emptySemesters; + + VoidCallback? onControllerChanged; + GlassSheet.show( + context: context, + isScrollControlled: true, + builder: (BuildContext sheetContext) { + if (controller != null) { + controller.attachSheetNavigator( + Navigator.of(sheetContext), + ); + } + return StatefulBuilder( + builder: ( + BuildContext context, + StateSetter setSheetState, + ) { + if (onControllerChanged == null && + controller != null) { + onControllerChanged = + () => setSheetState(() {}); + controller + .addListener(onControllerChanged!); + } + return SizedBox( + height: MediaQuery.of(context) + .size + .height * + 0.6, + child: Column( + children: [ + Padding( + padding: + const EdgeInsets.all(20), + child: Row( + children: [ + Icon( + Icons.calendar_month_rounded, + color: + colorScheme.primary, + ), + const SizedBox(width: 12), + Text( + context.ap.pickSemester, + style: TextStyle( + fontSize: 20, + fontWeight: + FontWeight.bold, + color: colorScheme + .onSurface, + ), + ), + ], + ), + ), + const GlassDivider(), + Expanded( + child: ListView.builder( + padding: + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + itemCount: + groupedByYear.length, + itemBuilder: ( + BuildContext context, + int groupIndex, + ) { + final String year = + groupedByYear.keys + .elementAt( + groupIndex, + ); + final List< + MapEntry> + semesters = + groupedByYear[year]!; + + return _buildYearGroup( + context: context, + year: year, + semesters: semesters, + colorScheme: + colorScheme, + currentIndex: + currentIndex, + semesterData: + semesterData, + onSelect: onSelect, + loadingSemesters: + effectiveLoading, + emptySemesters: + effectiveEmpty, + setSheetState: + setSheetState, + uiConfig: uiConfig, + controller: controller, + ); + }, + ), + ), + ], + ), + ); + }, + ); + }, + ).whenComplete(() { + if (onControllerChanged != null) { + controller + ?.removeListener(onControllerChanged!); + } + controller?.detachSheetNavigator(); + }); + } + + static Widget _buildYearGroup({ + required BuildContext context, + required String year, + required List> semesters, + required ColorScheme colorScheme, + required int currentIndex, + required SemesterData semesterData, + SemesterCallback? onSelect, + Set? loadingSemesters, + Set? emptySemesters, + required StateSetter setSheetState, + SemesterUIConfig? uiConfig, + SemesterPickerController? controller, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: + const EdgeInsets.fromLTRB(8, 16, 8, 8), + child: Row( + children: [ + GlassCard( + useOwnLayer: true, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + shape: + const LiquidRoundedSuperellipse( + borderRadius: 8, + ), + child: Text( + context.ap + .schoolYearFormat(arg1: year), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: + colorScheme.onPrimaryContainer, + ), + ), + ), + const Expanded(child: SizedBox()), + ], + ), + ), + ...semesters.map( + (MapEntry entry) => + _buildSemesterItem( + context: context, + originalIndex: entry.key, + semester: entry.value, + colorScheme: colorScheme, + currentIndex: currentIndex, + semesterData: semesterData, + onSelect: onSelect, + loadingSemesters: loadingSemesters, + emptySemesters: emptySemesters, + setSheetState: setSheetState, + uiConfig: uiConfig, + controller: controller, + ), + ), + ], + ); + } + + static Widget _buildSemesterItem({ + required BuildContext context, + required int originalIndex, + required Semester semester, + required ColorScheme colorScheme, + required int currentIndex, + required SemesterData semesterData, + SemesterCallback? onSelect, + Set? loadingSemesters, + Set? emptySemesters, + required StateSetter setSheetState, + SemesterUIConfig? uiConfig, + SemesterPickerController? controller, + }) { + final bool isSelected = + originalIndex == currentIndex; + final bool isDefault = + originalIndex == semesterData.defaultIndex; + final bool isEmpty = + emptySemesters?.contains(semester.code) ?? + false; + final bool isLoading = + loadingSemesters?.contains(semester.code) ?? + false; + final bool isDisabled = isEmpty || isLoading; + final String semesterName = + uiConfig?.getName?.call(semester.value) ?? + _getSemesterName( + semester.value, + context.ap, + ); + final String displayName = semesterName.isNotEmpty + ? semesterName + : semester.text; + + return Opacity( + opacity: isEmpty ? 0.5 : 1.0, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 4), + child: GlassCard( + useOwnLayer: true, + padding: EdgeInsets.zero, + child: GestureDetector( + onTap: isDisabled + ? null + : () { + if (onSelect != null) { + if (controller != null) { + controller + .markSemesterLoading( + semester, + ); + onSelect( + semester, + originalIndex, + ); + } else if (loadingSemesters != + null) { + loadingSemesters + .add(semester.code); + setSheetState(() {}); + onSelect( + semester, + originalIndex, + ); + } else { + Navigator.of(context).pop(); + onSelect( + semester, + originalIndex, + ); + } + } else { + Navigator.of(context).pop(); + } + }, + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: isEmpty + ? colorScheme.outlineVariant + .withAlpha(77) + : isLoading + ? colorScheme.primary + .withAlpha(77) + : isSelected + ? colorScheme + .primary + : (uiConfig + ?.getColor + ?.call( + semester + .value, + colorScheme, + ) ?? + _getSemesterColor( + semester + .value, + colorScheme, + )), + borderRadius: + BorderRadius.circular(10), + ), + child: Center( + child: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: + GlassProgressIndicator + .circular(), + ) + : Icon( + isEmpty + ? Icons + .block_rounded + : (uiConfig?.getIcon + ?.call( + semester + .value, + ) ?? + _getSemesterIcon( + semester + .value, + )), + size: 22, + color: isEmpty + ? colorScheme + .onSurfaceVariant + : isSelected + ? colorScheme + .onPrimary + : colorScheme + .onPrimaryContainer, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Flexible( + child: Text( + displayName, + style: TextStyle( + fontSize: 16, + fontWeight: + FontWeight.w600, + color: isEmpty + ? colorScheme + .onSurfaceVariant + : isLoading + ? colorScheme + .primary + : isSelected + ? colorScheme + .primary + : colorScheme + .onSurface, + ), + overflow: TextOverflow + .ellipsis, + ), + ), + if (isLoading) ...[ + const SizedBox( + width: 8, + ), + Container( + padding: + const EdgeInsets + .symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: + BoxDecoration( + color: colorScheme + .primary + .withAlpha(26), + borderRadius: + BorderRadius + .circular( + 4, + ), + ), + child: Text( + context.ap + .semesterLoading, + style: TextStyle( + fontSize: 10, + fontWeight: + FontWeight + .w500, + color: + colorScheme + .primary, + ), + ), + ), + ] else if ( + isEmpty) ...[ + const SizedBox( + width: 8, + ), + Container( + padding: + const EdgeInsets + .symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: + BoxDecoration( + color: colorScheme + .outlineVariant + .withAlpha(77), + borderRadius: + BorderRadius + .circular( + 4, + ), + ), + child: Text( + context.ap + .semesterNoData, + style: TextStyle( + fontSize: 10, + fontWeight: + FontWeight + .w500, + color: colorScheme + .onSurfaceVariant, + ), + ), + ), + ] else if ( + isDefault) ...[ + const SizedBox( + width: 8, + ), + Container( + padding: + const EdgeInsets + .symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: + BoxDecoration( + color: colorScheme + .tertiary, + borderRadius: + BorderRadius + .circular( + 4, + ), + ), + child: Text( + context.ap + .currentSemester, + style: TextStyle( + fontSize: 10, + fontWeight: + FontWeight + .w600, + color: colorScheme + .onTertiary, + ), + ), + ), + ], + ], + ), + const SizedBox(height: 2), + Text( + semester.text, + style: TextStyle( + fontSize: 12, + color: colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + ), + if (isLoading) + const SizedBox( + width: 24, + height: 24, + child: GlassProgressIndicator + .circular(), + ) + else if (isEmpty) + Icon( + Icons.lock_outline_rounded, + color: + colorScheme.outlineVariant, + size: 20, + ) + else if (isSelected) + Icon( + Icons + .check_circle_rounded, + color: colorScheme.primary, + ) + else + Icon( + Icons.circle_outlined, + color: + colorScheme.outlineVariant, + ), + ], + ), + ), + ), + ), + ), + ); + } + + // ---------------------------------------------------------- + // Duplicated helpers (private in SemesterPickerState) + // ---------------------------------------------------------- + + static int _getSemesterSortValue(String value) { + switch (value) { + case '4': + return 1; + case '6': + return 2; + case '7': + return 3; + case '2': + return 4; + case '3': + return 5; + case '1': + return 6; + case '5': + return 7; + default: + return 99; + } + } + + static String _getSemesterName( + String value, [ + ApLocalizations? ap, + ]) { + switch (value) { + case '1': + return ap?.firstSemester ?? '上學期'; + case '2': + return ap?.secondSemester ?? '下學期'; + case '3': + return ap?.winterSession ?? '寒修'; + case '4': + return ap?.summerSession ?? '暑修'; + case '5': + return ap?.preCourse ?? '先修'; + case '6': + return ap?.summerSessionFirst ?? '暑修(一)'; + case '7': + return ap?.summerSessionSpecial ?? '暑修(特)'; + default: + return ''; + } + } + + static IconData _getSemesterIcon(String value) { + switch (value) { + case '1': + return Icons.looks_one_rounded; + case '2': + return Icons.looks_one_rounded; + case '3': + return Icons.ac_unit_rounded; + case '4': + case '6': + case '7': + return Icons.brightness_6; + case '5': + return Icons.auto_awesome_rounded; + default: + return Icons.calendar_month_rounded; + } + } + + static Color _getSemesterColor( + String value, + ColorScheme colorScheme, + ) { + switch (value) { + case '1': + return colorScheme.primaryContainer + .withAlpha(179); + case '2': + return colorScheme.secondaryContainer + .withAlpha(179); + case '3': + return colorScheme.errorContainer + .withAlpha(128); + case '5': + return colorScheme.primaryContainer + .withAlpha(102); + case '4': + case '6': + case '7': + return colorScheme.tertiaryContainer + .withAlpha(179); + default: + return colorScheme.surfaceContainerHighest; + } + } + + static List> + _getSortedSemesters( + SemesterData semesterData, + SemesterUIConfig? uiConfig, + ) { + final List> indexed = + >[]; + for (int i = 0; + i < semesterData.data.length; + i++) { + indexed.add( + MapEntry( + i, + semesterData.data[i], + ), + ); + } + + indexed.sort( + ( + MapEntry a, + MapEntry b, + ) { + final int yearA = + int.tryParse(a.value.year) ?? 0; + final int yearB = + int.tryParse(b.value.year) ?? 0; + + if (yearA != yearB) { + return yearB.compareTo(yearA); + } + + final int semA = uiConfig?.getSortValue + ?.call(a.value.value) ?? + _getSemesterSortValue(a.value.value); + final int semB = uiConfig?.getSortValue + ?.call(b.value.value) ?? + _getSemesterSortValue(b.value.value); + return semA.compareTo(semB); + }, + ); + + return indexed; + } + + static String _getShortSemesterText( + Semester semester, + SemesterUIConfig? uiConfig, [ + ApLocalizations? ap, + ]) { + final String name = + uiConfig?.getName?.call(semester.value) ?? + _getSemesterName(semester.value, ap); + if (name.isNotEmpty) { + return '${semester.year} $name'; + } + return semester.text; + } +} + +/// State for [GlassSemesterPicker]. +class GlassSemesterPickerState + extends State { + late SemesterData semesterData; + Semester? selectSemester; + late int currentIndex; + + final Set _emptySemesters = {}; + final Set _loadingSemesters = {}; + BuildContext? _sheetContext; + StateSetter? _sheetSetState; + + @override + void initState() { + semesterData = widget.semesterData; + currentIndex = widget.currentIndex; + if (semesterData.data.isNotEmpty && + currentIndex < semesterData.data.length) { + selectSemester = + semesterData.data[currentIndex]; + } + super.initState(); + } + + @override + void didUpdateWidget( + covariant GlassSemesterPicker oldWidget, + ) { + if (oldWidget.semesterData != + widget.semesterData || + oldWidget.currentIndex != + widget.currentIndex) { + semesterData = widget.semesterData; + currentIndex = widget.currentIndex; + if (semesterData.data.isNotEmpty && + currentIndex < semesterData.data.length) { + selectSemester = + semesterData.data[currentIndex]; + } + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final String displayText = selectSemester != null + ? GlassSemesterPicker._getShortSemesterText( + selectSemester!, + widget.uiConfig, + context.ap, + ) + : ''; + + return GestureDetector( + onTap: () { + if (selectSemester != null) pickSemester(); + if (widget.featureTag != null) { + AnalyticsUtil.instance.logEvent( + '${widget.featureTag}_item_picker_click', + ); + } + }, + behavior: HitTestBehavior.opaque, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.calendar_month_rounded, + size: 16, + color: colorScheme.onSurface, + ), + const SizedBox(width: 6), + Text( + displayText, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 2), + Icon( + Icons.arrow_drop_down, + size: 14, + color: colorScheme.onSurface, + ), + ], + ), + ); + } + + /// Mark a semester as empty (no data). + void markSemesterEmpty(Semester semester) { + _loadingSemesters.remove(semester.code); + _emptySemesters.add(semester.code); + _sheetSetState?.call(() {}); + if (mounted) setState(() {}); + } + + /// Mark a semester as having data. Auto-closes + /// the bottom sheet. + void markSemesterHasData(Semester semester) { + _loadingSemesters.remove(semester.code); + _emptySemesters.remove(semester.code); + if (_sheetContext != null && + Navigator.of(_sheetContext!).canPop()) { + Navigator.of(_sheetContext!).pop(); + _sheetContext = null; + _sheetSetState = null; + } + if (mounted) setState(() {}); + } + + /// Mark a semester as loading. + void markSemesterLoading(Semester semester) { + _loadingSemesters.add(semester.code); + _sheetSetState?.call(() {}); + if (mounted) setState(() {}); + } + + /// Whether the given semester is marked empty. + bool isSemesterEmpty(Semester semester) { + return _emptySemesters.contains(semester.code); + } + + /// Whether the given semester is currently loading. + bool isSemesterLoading(Semester semester) { + return _loadingSemesters.contains(semester.code); + } + + /// Opens the semester picker bottom sheet. + void pickSemester() { + final SemesterPickerController? ctrl = + widget.controller; + GlassSemesterPicker.show( + context: context, + semesterData: semesterData, + currentIndex: currentIndex, + onSelect: (Semester semester, int index) { + HapticFeedback.selectionClick(); + if (ctrl != null) { + ctrl.markSemesterLoading(semester); + } else { + markSemesterLoading(semester); + } + currentIndex = index; + selectSemester = + semesterData.data[currentIndex]; + widget.onSelect?.call( + semesterData.data[currentIndex], + currentIndex, + ); + if (mounted) setState(() {}); + }, + featureTag: widget.featureTag, + loadingSemesters: + ctrl != null ? null : _loadingSemesters, + emptySemesters: + ctrl != null ? null : _emptySemesters, + uiConfig: widget.uiConfig, + controller: ctrl, + ); + } +} From c24ed89c6b3b7193cb2e80eea313c423acfad42c Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Tue, 14 Apr 2026 23:40:51 +0800 Subject: [PATCH 12/14] feat(liquid_glass): update score and course colors to iOS style and fix text contrast Replace Material color palette with iOS system colors for course cells and score indicators. Fix text contrast for light/dark mode by using colorScheme.onSurface instead of hardcoded colors. Remove GlassContainer from weekday header, use simple divider. --- .../lib/src/widgets/glass_course_content.dart | 7 +- .../lib/src/widgets/glass_course_list.dart | 15 +-- .../src/widgets/glass_course_table_view.dart | 101 ++++++++---------- .../widgets/glass_score_analysis_cards.dart | 42 +++----- .../src/widgets/glass_score_analysis_tab.dart | 20 ++-- .../lib/src/widgets/glass_score_list_tab.dart | 17 +-- 6 files changed, 89 insertions(+), 113 deletions(-) diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_content.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_content.dart index f83172e3..ef16de3c 100644 --- a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_content.dart +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_content.dart @@ -281,8 +281,7 @@ class _GlassCourseContentState (Platform.isAndroid || Platform.isIOS)) && widget.enableAddToCalendar) - IconButton( - tooltip: context.ap.addToCalendar, + GlassIconButton( icon: Image.asset( ApImageIcons.calendarImport, color: onCourseColor, @@ -317,7 +316,7 @@ class _GlassCourseContentState ); }, ), - IconButton( + GlassIconButton( icon: Icon( visibility ? Icons.visibility_outlined @@ -334,7 +333,7 @@ class _GlassCourseContentState if (widget.enableNotifyControl && _notifyData != null && NotificationUtil.instance.isSupport) - IconButton( + GlassIconButton( icon: Icon( state == CourseNotifyState.schedule ? Icons.alarm_on diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_list.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_list.dart index f0170b21..e42a0973 100644 --- a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_list.dart +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_list.dart @@ -1,5 +1,6 @@ import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; import 'package:ap_common_liquid_glass/src/widgets/glass_course_content.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_course_table_view.dart'; import 'package:flutter/material.dart'; import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; @@ -56,7 +57,7 @@ class GlassCourseList extends StatelessWidget { !invisibleCourseCodes .contains(course.code); final Color courseColor = - courseColors[index % courseColors.length]; + glassCourseColors[index % glassCourseColors.length]; final String instructors = course.getInstructors(); @@ -67,10 +68,8 @@ class GlassCourseList extends StatelessWidget { padding: EdgeInsets.zero, child: InkWell( onTap: () { - showModalBottomSheet( + GlassSheet.show( context: context, - backgroundColor: - const Color(0x00000000), isScrollControlled: true, builder: (BuildContext context) => @@ -160,12 +159,8 @@ class GlassCourseList extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ - IconButton( - visualDensity: - VisualDensity.compact, - padding: EdgeInsets.zero, - constraints: - const BoxConstraints(), + GlassIconButton( + size: 32, icon: Icon( visibility ? Icons diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_table_view.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_table_view.dart index c5e8991d..92f64212 100644 --- a/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_table_view.dart +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_course_table_view.dart @@ -1,11 +1,31 @@ import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; import 'package:flutter/material.dart'; -import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// iOS-style pastel colors for course cells. +/// +/// These are softer, semi-transparent tones inspired by +/// Apple Calendar and iOS 26 Liquid Glass aesthetics, +/// designed to let the glass backdrop show through. +const List glassCourseColors = [ + Color(0xCC007AFF), // iOS Blue + Color(0xCC34C759), // iOS Green + Color(0xCCFF9500), // iOS Orange + Color(0xCCAF52DE), // iOS Purple + Color(0xCCFF3B30), // iOS Red + Color(0xCC5AC8FA), // iOS Light Blue + Color(0xCCFF2D55), // iOS Pink + Color(0xCC30D158), // iOS Mint Green + Color(0xCCFFCC00), // iOS Yellow + Color(0xCC5856D6), // iOS Indigo + Color(0xCC00C7BE), // iOS Teal + Color(0xFFA2845E), // iOS Brown +]; /// A glass-enhanced version of [CourseTableView]. /// -/// Replaces Material surfaces with translucent glass-style -/// backgrounds while keeping the dense grid layout readable. +/// Uses iOS-style pastel colors with translucent glass +/// backgrounds, rounded corners, and subtle frosted +/// effects for a Liquid Glass aesthetic. class GlassCourseTableView extends StatelessWidget { /// Creates a [GlassCourseTableView]. const GlassCourseTableView({ @@ -70,7 +90,6 @@ class GlassCourseTableView extends StatelessWidget { controller: controller, physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.only( - top: 8.0, bottom: 80.0, ), child: RepaintBoundary( @@ -100,22 +119,16 @@ class GlassCourseTableView extends StatelessWidget { ColorScheme colorScheme, int weekdayCount, ) { - return GlassContainer( - useOwnLayer: true, - padding: EdgeInsets.zero, - shape: const LiquidRoundedSuperellipse( - borderRadius: 0, - ), - child: Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: colorScheme.outlineVariant - .withAlpha(128), - ), + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: colorScheme.outlineVariant + .withAlpha(128), ), ), - child: Row( + ), + child: Row( children: [ _buildTimeSlotHeader(colorScheme), for (int i = 0; i < weekdayCount; i++) @@ -130,30 +143,20 @@ class GlassCourseTableView extends StatelessWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: colorScheme.primary, + color: colorScheme.onSurface, ), ), ), ), ], ), - ), ); } Widget _buildTimeSlotHeader(ColorScheme colorScheme) { - return Container( + return SizedBox( width: 48, - padding: const EdgeInsets.symmetric(vertical: 12), - child: Text( - '', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: colorScheme.primary, - ), - ), + height: 40, ); } @@ -279,7 +282,7 @@ class GlassCourseTableView extends StatelessWidget { ), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest - .withAlpha(51), + .withAlpha(26), border: Border( right: BorderSide( color: colorScheme.outlineVariant @@ -398,6 +401,7 @@ class GlassCourseTableView extends StatelessWidget { ) { final Color courseColor = courseColorResolver(course.code); + const Color onCourseColor = Colors.white; final String locationInfo = (showClassroomLocation ?? true) && course.location != null @@ -408,13 +412,6 @@ class GlassCourseTableView extends StatelessWidget { ? course.getInstructors() : ''; - final Color onCourseColor = - ThemeData.estimateBrightnessForColor( - courseColor) == - Brightness.dark - ? Colors.white - : Colors.black; - final String displayInfo = [ if (instructorInfo.isNotEmpty) instructorInfo, if (locationInfo.isNotEmpty) locationInfo, @@ -439,25 +436,18 @@ class GlassCourseTableView extends StatelessWidget { child: Container( width: double.infinity, height: double.infinity, - margin: const EdgeInsets.all(2), + margin: const EdgeInsets.all(1.5), padding: const EdgeInsets.symmetric( horizontal: 4, - vertical: 6, + vertical: 4, ), decoration: BoxDecoration( - color: courseColor.withAlpha(200), - borderRadius: BorderRadius.circular(8), + color: courseColor.withAlpha(179), + borderRadius: BorderRadius.circular(10), border: Border.all( - color: Colors.white.withAlpha(51), + color: onCourseColor.withAlpha(51), width: 0.5, ), - boxShadow: [ - BoxShadow( - color: courseColor.withAlpha(51), - blurRadius: 6, - offset: const Offset(0, 2), - ), - ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -468,23 +458,22 @@ class GlassCourseTableView extends StatelessWidget { maxLines: span > 1 ? 4 : 2, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: span > 1 ? 14 : 9.5, + fontSize: span > 1 ? 13 : 9.5, fontWeight: FontWeight.w600, color: onCourseColor, height: 1.1, ), ), if (displayInfo.isNotEmpty) ...[ - SizedBox(height: span > 1 ? 4 : 2), + SizedBox(height: span > 1 ? 3 : 1), Text( displayInfo, textAlign: TextAlign.center, maxLines: span > 1 ? 4 : 2, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: span > 1 ? 12 : 8.5, - color: - onCourseColor.withAlpha(217), + fontSize: span > 1 ? 11 : 8, + color: onCourseColor.withAlpha(204), height: 1.0, ), ), diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_cards.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_cards.dart index ea1f05c1..fa8c8f80 100644 --- a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_cards.dart +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_cards.dart @@ -12,11 +12,11 @@ class GlassScorePRCard extends StatelessWidget { final ScoreAnalysis analysis; Color _getPRColor(ColorScheme colorScheme, int pr) { - if (pr >= 90) return const Color(0xFF4CAF50); - if (pr >= 75) return const Color(0xFF8BC34A); - if (pr >= 50) return colorScheme.primary; - if (pr >= 25) return const Color(0xFFFF9800); - return colorScheme.error; + if (pr >= 90) return const Color(0xFF34C759); + if (pr >= 75) return const Color(0xFF00C7BE); + if (pr >= 50) return const Color(0xFF007AFF); + if (pr >= 25) return const Color(0xFFFF9500); + return const Color(0xFFFF3B30); } @override @@ -186,11 +186,7 @@ class GlassScoreStatisticsCard extends StatelessWidget { ], ), ), - Divider( - height: 1, - color: colorScheme.outlineVariant - .withAlpha(77), - ), + const GlassDivider(), Padding( padding: const EdgeInsets.all(16), child: Column( @@ -204,7 +200,7 @@ class GlassScoreStatisticsCard extends StatelessWidget { .toStringAsFixed(0), icon: Icons.arrow_upward_rounded, - color: const Color(0xFF4CAF50), + color: const Color(0xFF34C759), ), ), const SizedBox(width: 12), @@ -363,11 +359,7 @@ class GlassScoreDistributionCard ], ), ), - Divider( - height: 1, - color: colorScheme.outlineVariant - .withAlpha(77), - ), + const GlassDivider(), Padding( padding: const EdgeInsets.all(16), child: Column( @@ -379,7 +371,7 @@ class GlassScoreDistributionCard .distribution['90-100'] ?? 0, total: analysis.totalSubjects, - color: const Color(0xFF4CAF50), + color: const Color(0xFF34C759), ), const SizedBox(height: 10), _GlassDistributionBar( @@ -389,7 +381,7 @@ class GlassScoreDistributionCard .distribution['80-89'] ?? 0, total: analysis.totalSubjects, - color: const Color(0xFF8BC34A), + color: const Color(0xFF00C7BE), ), const SizedBox(height: 10), _GlassDistributionBar( @@ -399,7 +391,7 @@ class GlassScoreDistributionCard .distribution['70-79'] ?? 0, total: analysis.totalSubjects, - color: colorScheme.primary, + color: const Color(0xFF007AFF), ), const SizedBox(height: 10), _GlassDistributionBar( @@ -409,7 +401,7 @@ class GlassScoreDistributionCard .distribution['60-69'] ?? 0, total: analysis.totalSubjects, - color: const Color(0xFFFF9800), + color: const Color(0xFFFF9500), ), const SizedBox(height: 10), _GlassDistributionBar( @@ -419,7 +411,7 @@ class GlassScoreDistributionCard .distribution['0-59'] ?? 0, total: analysis.totalSubjects, - color: colorScheme.error, + color: const Color(0xFFFF3B30), ), ], ), @@ -543,11 +535,7 @@ class GlassScoreCreditSummaryCard ], ), ), - Divider( - height: 1, - color: colorScheme.outlineVariant - .withAlpha(77), - ), + const GlassDivider(), Padding( padding: const EdgeInsets.all(16), child: Row( @@ -565,7 +553,7 @@ class GlassScoreCreditSummaryCard label: '\u53ca\u683c\u5b78\u5206', value: analysis.passedCredits .toStringAsFixed(1), - color: const Color(0xFF4CAF50), + color: const Color(0xFF34C759), ), ), Expanded( diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_tab.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_tab.dart index 0d336e64..2636d7be 100644 --- a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_tab.dart +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_analysis_tab.dart @@ -32,7 +32,12 @@ class GlassScoreAnalysisTab extends StatelessWidget { controller: controller, physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: 80, + ), child: Column( children: [ _buildMainSummaryCard( @@ -107,18 +112,13 @@ class GlassScoreAnalysisTab extends StatelessWidget { ], ), ), - Container( + GlassContainer( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 16, ), - decoration: BoxDecoration( - color: - colorScheme.surface.withAlpha(179), - borderRadius: - const BorderRadius.vertical( - bottom: Radius.circular(20), - ), + shape: const LiquidRoundedSuperellipse( + borderRadius: 20, ), child: Row( children: [ @@ -170,7 +170,7 @@ class GlassScoreAnalysisTab extends StatelessWidget { fontSize: 28, fontWeight: FontWeight.bold, color: - colorScheme.onPrimaryContainer, + colorScheme.onSurface, ), ), const SizedBox(height: 4), diff --git a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_list_tab.dart b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_list_tab.dart index 6e93379a..61200a9c 100644 --- a/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_list_tab.dart +++ b/packages/ap_common_liquid_glass/lib/src/widgets/glass_score_list_tab.dart @@ -35,7 +35,12 @@ class GlassScoreListTab extends StatelessWidget { controller: controller, physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: 80, + ), itemCount: scoreData.scores.length, itemBuilder: (BuildContext context, int index) { @@ -167,11 +172,11 @@ class GlassScoreListTab extends StatelessWidget { } Color _getScoreColor(double score) { - if (score >= 90) return const Color(0xFF4CAF50); - if (score >= 80) return const Color(0xFF8BC34A); - if (score >= 70) return const Color(0xFF2196F3); - if (score >= 60) return const Color(0xFFFF9800); - return const Color(0xFFF44336); + if (score >= 90) return const Color(0xFF34C759); + if (score >= 80) return const Color(0xFF00C7BE); + if (score >= 70) return const Color(0xFF007AFF); + if (score >= 60) return const Color(0xFFFF9500); + return const Color(0xFFFF3B30); } Widget _buildTag( From 3ee1ad0f9cd36b8ab0b45d5f16c471a58ac2cc48 Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Tue, 14 Apr 2026 23:41:00 +0800 Subject: [PATCH 13/14] feat(liquid_glass): add GlassAboutUsPage and GlassAnnouncementContentPage Add glass-styled About Us page with GlassCard info sections, GlassIconButton contact icons, and GlassFloatingToolbar navigation. Add glass-styled Announcement Content page with hero image, GlassCard content area, and GlassButton for external links. --- .../lib/ap_common_liquid_glass.dart | 4 + .../lib/src/pages/glass_about_us_page.dart | 398 ++++++++++++++++++ .../glass_announcement_content_page.dart | 289 +++++++++++++ packages/ap_common_liquid_glass/pubspec.yaml | 2 + 4 files changed, 693 insertions(+) create mode 100644 packages/ap_common_liquid_glass/lib/src/pages/glass_about_us_page.dart create mode 100644 packages/ap_common_liquid_glass/lib/src/pages/glass_announcement_content_page.dart diff --git a/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart b/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart index 706dd7ae..b6e4edf6 100644 --- a/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart +++ b/packages/ap_common_liquid_glass/lib/ap_common_liquid_glass.dart @@ -27,6 +27,8 @@ export 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; export 'src/glass_theme_bridge.dart'; export 'src/liquid_glass_app.dart'; +export 'src/pages/glass_about_us_page.dart'; +export 'src/pages/glass_announcement_content_page.dart'; export 'src/pages/glass_ap_course_page.dart'; export 'src/pages/glass_ap_login_page.dart'; export 'src/pages/glass_ap_score_page.dart'; @@ -41,6 +43,8 @@ export 'src/widgets/glass_course_content.dart'; export 'src/widgets/glass_course_list.dart'; export 'src/widgets/glass_course_table_view.dart'; export 'src/widgets/glass_dialog.dart'; +export 'src/widgets/glass_floating_toolbar.dart'; export 'src/widgets/glass_score_analysis_cards.dart'; export 'src/widgets/glass_score_analysis_tab.dart'; export 'src/widgets/glass_score_list_tab.dart'; +export 'src/widgets/glass_semester_picker.dart'; diff --git a/packages/ap_common_liquid_glass/lib/src/pages/glass_about_us_page.dart b/packages/ap_common_liquid_glass/lib/src/pages/glass_about_us_page.dart new file mode 100644 index 00000000..a151337a --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/pages/glass_about_us_page.dart @@ -0,0 +1,398 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; + +/// A glass-enhanced version of [AboutUsPage]. +/// +/// Replaces the [SliverAppBar] with [GlassFloatingToolbar], +/// [Card] surfaces with [GlassCard], and uses +/// [AdaptiveLiquidGlassLayer] for the glass effect. +class GlassAboutUsPage extends StatefulWidget { + const GlassAboutUsPage({ + super.key, + required this.assetImage, + required this.fbFanPageUrl, + required this.fbFanPageId, + this.instagramUsername, + required this.githubUrl, + required this.githubName, + required this.email, + required this.appLicense, + this.actions, + this.applicationName, + this.applicationVersion, + this.applicationIcon, + this.applicationLegalese, + }); + + final String assetImage; + final String fbFanPageUrl; + final String fbFanPageId; + final String? instagramUsername; + final String githubUrl; + final String githubName; + final String email; + final String appLicense; + final String? applicationName; + final String? applicationVersion; + final Widget? applicationIcon; + final String? applicationLegalese; + final List? actions; + + @override + GlassAboutUsPageState createState() => + GlassAboutUsPageState(); +} + +class GlassAboutUsPageState + extends State { + @override + void initState() { + AnalyticsUtil.instance.setCurrentScreen( + 'GlassAboutUsPage', + 'glass_about_us_page.dart', + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final ApLocalizations ap = context.ap; + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + final double topPadding = + MediaQuery.of(context).padding.top; + + return AdaptiveLiquidGlassLayer( + child: Scaffold( + body: Stack( + children: [ + ListView( + padding: EdgeInsets.only( + top: topPadding + 52, + bottom: 32, + ), + children: [ + _buildHeader(context), + const SizedBox(height: 16), + _buildInfoCard( + ap.aboutAuthorTitle, + ap.aboutAuthorContent, + ), + _buildInfoCard( + ap.about, + ap.aboutUsContent, + ), + _buildInfoCard( + ap.aboutRecruitTitle, + ap.aboutRecruitContent, + ), + _buildInfoCardWithLogo( + ap.aboutItcTitle, + ap.aboutItcContent, + ApImageAssets.nkutstItc, + ), + _buildInfoCardWithLogo( + ap.aboutNsysuCodeClubTitle, + ap.aboutNsysuCodeClubContent, + ApImageAssets.nsysuGdsc, + ), + _buildContactCard( + context, + ap, + colorScheme, + ), + _buildInfoCard( + ap.aboutOpenSourceTitle, + widget.appLicense, + ), + ], + ), + GlassFloatingToolbar( + leading: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: const Icon( + Icons.arrow_back_ios_new_rounded, + size: 18, + ), + ), + const SizedBox(width: 8), + Text( + ap.about, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ], + trailing: [ + ...widget.actions ?? [], + IconButton( + icon: Icon(ApIcon.codeIcon), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => LicensePage( + applicationName: + widget.applicationName, + applicationVersion: + widget + .applicationVersion, + applicationIcon: + widget.applicationIcon, + applicationLegalese: + widget + .applicationLegalese, + ), + ), + ); + AnalyticsUtil.instance + .logEvent('license_page_click'); + }, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.asset( + widget.assetImage, + height: 180, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + ); + } + + Widget _buildInfoCard(String title, String content) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 16, + ), + child: GlassCard( + useOwnLayer: true, + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + SelectableLinkify( + text: title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + SelectableLinkify( + text: content, + style: TextStyle( + fontSize: 14, + color: + colorScheme.onSurfaceVariant, + ), + options: const LinkifyOptions( + humanize: false, + ), + onOpen: (LinkableElement link) => + PlatformUtil.instance + .launchUrl(link.url), + ), + ], + ), + ), + ), + ); + } + + Widget _buildInfoCardWithLogo( + String title, + String content, + String logoAsset, + ) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 16, + ), + child: GlassCard( + useOwnLayer: true, + child: SizedBox( + width: double.infinity, + child: Stack( + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only( + right: 72, + ), + child: SelectableLinkify( + text: title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(height: 8), + SelectableLinkify( + text: content, + style: TextStyle( + fontSize: 14, + color: colorScheme + .onSurfaceVariant, + ), + options: const LinkifyOptions( + humanize: false, + ), + onOpen: (LinkableElement link) => + PlatformUtil.instance + .launchUrl(link.url), + ), + ], + ), + Positioned( + top: 0, + right: 0, + child: Image.asset( + logoAsset, + width: 56, + fit: BoxFit.cover, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildContactCard( + BuildContext context, + ApLocalizations ap, + ColorScheme colorScheme, + ) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 16, + ), + child: GlassCard( + useOwnLayer: true, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + ap.aboutContactUsTitle, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceEvenly, + children: [ + _buildContactIcon( + ApImageAssets.fb, + colorScheme.onSurface, + () { + PlatformUtil.instance.launchUrl( + 'https://m.me/' + '${widget.fbFanPageId}', + ); + AnalyticsUtil.instance + .logEvent('fb_click'); + }, + ), + if (widget.instagramUsername + case final String username?) + _buildContactIcon( + ApImageAssets.instagram, + null, + () { + PlatformUtil.instance.launchUrl( + 'https://ig.me/m/$username', + ); + AnalyticsUtil.instance + .logEvent('instagram_click'); + }, + ), + _buildContactIcon( + ApImageAssets.github, + colorScheme.onSurface, + () { + PlatformUtil.instance + .launchUrl(widget.githubUrl); + AnalyticsUtil.instance + .logEvent('github_click'); + }, + ), + _buildContactIcon( + ApImageAssets.email, + colorScheme.onSurface, + () { + PlatformUtil.instance.launchUrl( + 'mailto:${widget.email}', + ); + AnalyticsUtil.instance + .logEvent('email_click'); + }, + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildContactIcon( + String asset, + Color? color, + VoidCallback onTap, + ) { + return GlassIconButton( + icon: Image.asset( + asset, + color: color, + width: 28, + height: 28, + ), + onPressed: onTap, + ); + } +} diff --git a/packages/ap_common_liquid_glass/lib/src/pages/glass_announcement_content_page.dart b/packages/ap_common_liquid_glass/lib/src/pages/glass_announcement_content_page.dart new file mode 100644 index 00000000..db8cc438 --- /dev/null +++ b/packages/ap_common_liquid_glass/lib/src/pages/glass_announcement_content_page.dart @@ -0,0 +1,289 @@ +import 'package:ap_common_flutter_ui/ap_common_flutter_ui.dart'; +import 'package:ap_common_liquid_glass/src/widgets/glass_floating_toolbar.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:liquid_glass_widgets/liquid_glass_widgets.dart'; +import 'package:photo_view/photo_view.dart'; + +/// A glass-enhanced version of [AnnouncementContentPage]. +/// +/// Uses [GlassFloatingToolbar] for navigation, +/// [GlassCard] for content sections, and +/// [GlassButton] for links. +class GlassAnnouncementContentPage extends StatefulWidget { + const GlassAnnouncementContentPage({ + super.key, + required this.announcement, + }); + + final Announcement announcement; + + @override + GlassAnnouncementContentPageState createState() => + GlassAnnouncementContentPageState(); +} + +class GlassAnnouncementContentPageState + extends State { + @override + void initState() { + AnalyticsUtil.instance.setCurrentScreen( + 'GlassAnnouncementContentPage', + 'glass_announcement_content_page.dart', + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final double topPadding = + MediaQuery.of(context).padding.top; + + return AdaptiveLiquidGlassLayer( + child: Scaffold( + body: Stack( + children: [ + OrientationBuilder( + builder: ( + _, + Orientation orientation, + ) { + return orientation == + Orientation.portrait + ? SingleChildScrollView( + padding: EdgeInsets.only( + top: topPadding + 52, + bottom: 32, + ), + child: Column( + children: + _renderContent( + orientation, + ), + ), + ) + : Padding( + padding: EdgeInsets.only( + top: topPadding + 52, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment + .center, + children: + _renderContent( + orientation, + ), + ), + ); + }, + ), + GlassFloatingToolbar( + leading: [ + GestureDetector( + onTap: () => + Navigator.pop(context), + child: const Icon( + Icons + .arrow_back_ios_new_rounded, + size: 18, + ), + ), + const SizedBox(width: 8), + Flexible( + child: Text( + context.ap.announcements, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + overflow: + TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ), + ); + } + + List _renderContent( + Orientation orientation, + ) { + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + final Widget image = GestureDetector( + onTap: () { + ApUtils.pushCupertinoStyle( + context, + Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + title: Text( + widget.announcement.title, + ), + ), + body: PhotoView( + imageProvider: + (ApUtils.isSupportCacheNetworkImage + ? CachedNetworkImageProvider( + widget.announcement + .imgUrl, + ) + : NetworkImage( + widget.announcement + .imgUrl, + )) + as ImageProvider?, + ), + ), + ); + AnalyticsUtil.instance.logEvent( + 'announcement_content_image_click', + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: ClipRRect( + borderRadius: + BorderRadius.circular(16), + child: AspectRatio( + aspectRatio: + orientation == Orientation.portrait + ? 4 / 3 + : 9 / 16, + child: Hero( + tag: + widget.announcement.hashCode, + child: ApNetworkImage( + url: + widget.announcement.imgUrl, + ), + ), + ), + ), + ), + ); + + final Widget contentCard = Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: GlassCard( + useOwnLayer: true, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Hero( + tag: + ApConstants.tagAnnouncementTitle, + child: Material( + color: Colors.transparent, + child: SelectableText( + widget.announcement.title, + style: TextStyle( + fontSize: 20, + color: colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + if (widget + .announcement + .publishedTime != + null) ...[ + const SizedBox(height: 8), + Text( + widget.announcement + .publishedTime!, + style: TextStyle( + fontSize: 13, + color: colorScheme + .onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 12), + SelectableLinkify( + text: widget + .announcement.description, + style: TextStyle( + fontSize: 15, + color: + colorScheme.onSurfaceVariant, + height: 1.5, + ), + linkStyle: TextStyle( + color: colorScheme.primary, + ), + options: const LinkifyOptions( + humanize: false, + ), + onOpen: (LinkableElement link) => + PlatformUtil.instance + .launchUrl(link.url), + ), + if (widget.announcement.url != + null && + widget.announcement.url! + .isNotEmpty) ...[ + const SizedBox(height: 16), + Center( + child: GlassButton.custom( + onTap: () { + PlatformUtil.instance + .launchUrl( + widget.announcement.url!, + ); + AnalyticsUtil.instance + .logEvent( + 'announcement_link_click', + ); + }, + width: 56, + height: 56, + child: Icon( + ApIcon.exitToApp, + color: + colorScheme.onSurface, + ), + ), + ), + ], + ], + ), + ), + ); + + if (orientation == Orientation.portrait) { + return [ + const SizedBox(height: 8), + image, + const SizedBox(height: 16), + contentCard, + ]; + } else { + return [ + const SizedBox(width: 16), + Expanded(child: image), + const SizedBox(width: 16), + Expanded( + child: SingleChildScrollView( + child: contentCard, + ), + ), + const SizedBox(width: 16), + ]; + } + } +} diff --git a/packages/ap_common_liquid_glass/pubspec.yaml b/packages/ap_common_liquid_glass/pubspec.yaml index a276c5d4..dafb3b74 100644 --- a/packages/ap_common_liquid_glass/pubspec.yaml +++ b/packages/ap_common_liquid_glass/pubspec.yaml @@ -20,6 +20,8 @@ dependencies: ap_common_flutter_ui: ^1.1.0-dev.6 liquid_glass_widgets: ^0.7.0 barcode_widget: ^2.0.1 + cached_network_image: ^3.2.3 + flutter_linkify: ^6.0.0 photo_view: ^0.15.0 dev_dependencies: From 45586560980067684108d987245bf8b1dbc44c02 Mon Sep 17 00:00:00 2001 From: Rainvisitor Date: Tue, 14 Apr 2026 23:41:07 +0800 Subject: [PATCH 14/14] feat(example_liquid_glass): update example with login flow, tabs, about and announcement pages Restructure example app with login state management, tab-based navigation using GlassBottomBar, settings page with GlassAboutUsPage link, and announcement tap to GlassAnnouncementContentPage. Login page uses GlassCard with transparent TextField inputs. --- .../ios/Runner.xcodeproj/project.pbxproj | 132 ++++++ .../contents.xcworkspacedata | 3 + .../ios/Runner/AppDelegate.swift | 7 +- .../ios/Runner/Info.plist | 29 +- apps/example_liquid_glass/lib/app.dart | 27 +- .../lib/pages/home_page.dart | 387 ++++++++++++++---- .../lib/pages/login_page.dart | 200 ++++++++- .../macos/Runner.xcodeproj/project.pbxproj | 23 +- apps/example_liquid_glass/pubspec.yaml | 2 + 9 files changed, 720 insertions(+), 90 deletions(-) diff --git a/apps/example_liquid_glass/ios/Runner.xcodeproj/project.pbxproj b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.pbxproj index f5cdd4d2..fd726013 100644 --- a/apps/example_liquid_glass/ios/Runner.xcodeproj/project.pbxproj +++ b/apps/example_liquid_glass/ios/Runner.xcodeproj/project.pbxproj @@ -10,10 +10,12 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 7104B2AF98D7133B5FC8DE3B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D4579526FB03E66736E237A1 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + FECA9E8B24DD661D8FA73851 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF35E34B094B13C437FDC031 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -55,6 +57,14 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AE35F34F5A1F6FA0BD6A6F03 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + B0E23A5F722B353FB3A31582 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + C7D4A2DE5F9353A9DE721BB9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + D4579526FB03E66736E237A1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D95C3C6CB7B334AA2FCF8D36 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + DF35E34B094B13C437FDC031 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DF7A69D88620A4E3E4C6860C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + E5123AB7DD5F6D3BB99CF4DD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,6 +72,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7104B2AF98D7133B5FC8DE3B /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2B3BB2D39C038EF76746AF6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FECA9E8B24DD661D8FA73851 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +95,28 @@ path = RunnerTests; sourceTree = ""; }; + 5287E88233F1450AB8717CE9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D4579526FB03E66736E237A1 /* Pods_Runner.framework */, + DF35E34B094B13C437FDC031 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7F691E62A5A6E0D67909DD01 /* Pods */ = { + isa = PBXGroup; + children = ( + C7D4A2DE5F9353A9DE721BB9 /* Pods-Runner.debug.xcconfig */, + E5123AB7DD5F6D3BB99CF4DD /* Pods-Runner.release.xcconfig */, + D95C3C6CB7B334AA2FCF8D36 /* Pods-Runner.profile.xcconfig */, + DF7A69D88620A4E3E4C6860C /* Pods-RunnerTests.debug.xcconfig */, + AE35F34F5A1F6FA0BD6A6F03 /* Pods-RunnerTests.release.xcconfig */, + B0E23A5F722B353FB3A31582 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +135,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 7F691E62A5A6E0D67909DD01 /* Pods */, + 5287E88233F1450AB8717CE9 /* Frameworks */, ); sourceTree = ""; }; @@ -128,8 +171,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 7726C7D82DFB077AD6384DC6 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + D2B3BB2D39C038EF76746AF6 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +190,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 1F3508AD5B113DC27785DE47 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 4EED1E5ED4FD5878D315FC9A /* [CP] Embed Pods Frameworks */, + 9CA632B520412222511A6633 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -222,6 +270,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1F3508AD5B113DC27785DE47 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -238,6 +308,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 4EED1E5ED4FD5878D315FC9A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 7726C7D82DFB077AD6384DC6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +362,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + 9CA632B520412222511A6633 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -362,6 +488,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 5SP52F6F2C; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -378,6 +505,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = DF7A69D88620A4E3E4C6860C /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -395,6 +523,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = AE35F34F5A1F6FA0BD6A6F03 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -410,6 +539,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = B0E23A5F722B353FB3A31582 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -541,6 +671,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 5SP52F6F2C; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -563,6 +694,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 5SP52F6F2C; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/apps/example_liquid_glass/ios/Runner.xcworkspace/contents.xcworkspacedata b/apps/example_liquid_glass/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/apps/example_liquid_glass/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/apps/example_liquid_glass/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/apps/example_liquid_glass/ios/Runner/AppDelegate.swift b/apps/example_liquid_glass/ios/Runner/AppDelegate.swift index 62666446..c30b367e 100644 --- a/apps/example_liquid_glass/ios/Runner/AppDelegate.swift +++ b/apps/example_liquid_glass/ios/Runner/AppDelegate.swift @@ -2,12 +2,15 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/apps/example_liquid_glass/ios/Runner/Info.plist b/apps/example_liquid_glass/ios/Runner/Info.plist index 4ac182e8..a6d7c160 100644 --- a/apps/example_liquid_glass/ios/Runner/Info.plist +++ b/apps/example_liquid_glass/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,29 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +66,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/apps/example_liquid_glass/lib/app.dart b/apps/example_liquid_glass/lib/app.dart index 20f596d1..965c3031 100644 --- a/apps/example_liquid_glass/lib/app.dart +++ b/apps/example_liquid_glass/lib/app.dart @@ -1,4 +1,5 @@ import 'package:ap_common_example_liquid_glass/pages/home_page.dart'; +import 'package:ap_common_example_liquid_glass/pages/login_page.dart'; import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; import 'package:flutter/material.dart'; @@ -8,8 +9,32 @@ class LiquidGlassExampleApp extends StatelessWidget { @override Widget build(BuildContext context) { return LiquidGlassApApp( - home: const GlassHomePage(), onGenerateTitle: (_) => 'Liquid Glass Example', + home: const _RootPage(), + ); + } +} + +class _RootPage extends StatefulWidget { + const _RootPage(); + + @override + State<_RootPage> createState() => _RootPageState(); +} + +class _RootPageState extends State<_RootPage> { + bool _isLogin = false; + + @override + Widget build(BuildContext context) { + if (_isLogin) { + return GlassMainPage( + onLogout: () => setState(() => _isLogin = false), + ); + } + return GlassLoginPage( + onLoginSuccess: () => + setState(() => _isLogin = true), ); } } diff --git a/apps/example_liquid_glass/lib/pages/home_page.dart b/apps/example_liquid_glass/lib/pages/home_page.dart index 7936a3c1..357dcf4e 100644 --- a/apps/example_liquid_glass/lib/pages/home_page.dart +++ b/apps/example_liquid_glass/lib/pages/home_page.dart @@ -1,12 +1,56 @@ +import 'package:ap_common_announcement_ui/ap_common_announcement_ui.dart'; import 'package:ap_common_example_liquid_glass/pages/course_page.dart'; -import 'package:ap_common_example_liquid_glass/pages/login_page.dart'; import 'package:ap_common_example_liquid_glass/pages/score_page.dart'; +import 'package:ap_common_example_liquid_glass/res/assets.dart'; import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; import 'package:flutter/material.dart'; -/// Home page for the Liquid Glass example. -class GlassHomePage extends StatelessWidget { - const GlassHomePage({super.key}); +/// Main page after login with bottom tab navigation. +class GlassMainPage extends StatefulWidget { + const GlassMainPage({ + super.key, + required this.onLogout, + }); + + final VoidCallback onLogout; + + @override + State createState() => + _GlassMainPageState(); +} + +class _GlassMainPageState extends State { + int _selectedIndex = 0; + + HomeState _homeState = HomeState.loading; + List _announcements = []; + + @override + void initState() { + super.initState(); + _getAnnouncements(); + } + + Future _getAnnouncements() async { + final ApiResult> result = + await AnnouncementHelper.instance + .getAnnouncements(tags: []); + if (!mounted) return; + switch (result) { + case ApiSuccess>( + :final List data, + ): + setState(() { + _announcements = data; + _homeState = data.isEmpty + ? HomeState.empty + : HomeState.finish; + }); + case ApiFailure>(): + case ApiError>(): + setState(() => _homeState = HomeState.error); + } + } @override Widget build(BuildContext context) { @@ -14,86 +58,283 @@ class GlassHomePage extends StatelessWidget { return AdaptiveLiquidGlassLayer( child: Scaffold( - appBar: GlassAppBar( - title: const Text('Liquid Glass Example'), - actions: [ - IconButton( - icon: const Icon(Icons.brightness_6), - onPressed: () { - final LiquidGlassApAppState state = - LiquidGlassApApp.of(context); - state.setThemeMode( - state.themeMode == ThemeMode.dark - ? ThemeMode.light - : ThemeMode.dark, - ); - }, - ), - ], - ), - body: ListView( - padding: const EdgeInsets.all(16), - children: [ - _buildCard( - context, - icon: Icons.login, - title: ap.login, - subtitle: 'GlassApLoginPage', - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const GlassLoginPage(), - ), + drawer: _buildDrawer(context, ap), + body: Stack( + children: [ + IndexedStack( + index: _selectedIndex, + children: [ + _buildHomeTab(), + const GlassCoursePage(), + const GlassScorePage(), + _buildSettingsTab(), + ], ), - ), - _buildCard( - context, - icon: Icons.calendar_today, - title: ap.course, - subtitle: 'GlassApCoursePage', - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const GlassCoursePage(), + Positioned( + left: 16, + right: MediaQuery.of(context) + .size + .width * + 0.2, + bottom: + MediaQuery.of(context) + .padding + .bottom + + 8, + child: Builder( + builder: (BuildContext ctx) { + final bool isDark = + Theme.of(ctx).brightness == + Brightness.dark; + final Color iconColor = isDark + ? Colors.white + : Colors.black; + return GlassBottomBar( + tabs: [ + GlassBottomBarTab( + icon: Icon(ApIcon.home), + label: ap.home, + ), + GlassBottomBarTab( + icon: Icon(ApIcon.classIcon), + label: ap.course, + ), + GlassBottomBarTab( + icon: Icon(ApIcon.assignment), + label: ap.score, + ), + GlassBottomBarTab( + icon: Icon(ApIcon.person), + label: ap.settings, + ), + ], + selectedIndex: _selectedIndex, + onTabSelected: (int index) { + setState( + () => + _selectedIndex = index, + ); + }, + barHeight: 52, + barBorderRadius: 26, + horizontalPadding: 10, + verticalPadding: 10, + iconSize: 22, + selectedIconColor: iconColor, + unselectedIconColor: + iconColor.withAlpha(153), + textStyle: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: iconColor, + ), + ); + }, ), ), + ], + ), + ), + ); + } + + Widget _buildHomeTab() { + return GlassHomePageScaffold( + state: _homeState, + announcements: _announcements, + isLogin: true, + title: 'Liquid Glass', + bottomPadding: 80, + actions: [ + IconButton( + icon: const Icon(Icons.brightness_6), + onPressed: () { + final LiquidGlassApAppState state = + LiquidGlassApApp.of(context); + state.setThemeMode( + state.themeMode == ThemeMode.dark + ? ThemeMode.light + : ThemeMode.dark, + ); + }, + iconSize: 22, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 36, + minHeight: 36, ), - _buildCard( - context, - icon: Icons.assignment, - title: ap.score, - subtitle: 'GlassApScorePage', - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const GlassScorePage(), - ), + ), + ], + onImageTapped: (Announcement announcement) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + GlassAnnouncementContentPage( + announcement: announcement, ), ), - ], - ), + ); + }, + ); + } + + Widget _buildDrawer( + BuildContext context, + ApLocalizations ap, + ) { + return GlassApDrawer( + imageAsset: ImageAssets.K, + userInfo: const UserInfo( + id: 'C000000000', + name: 'AP Example', + className: '資工四甲', + department: '資訊工程系', ), + onTapHeader: () {}, + widgets: [ + DrawerItem( + icon: ApIcon.home, + title: ap.home, + onTap: () { + Navigator.pop(context); + setState(() => _selectedIndex = 0); + }, + ), + DrawerItem( + icon: ApIcon.classIcon, + title: ap.course, + onTap: () { + Navigator.pop(context); + setState(() => _selectedIndex = 1); + }, + ), + DrawerItem( + icon: ApIcon.assignment, + title: ap.score, + onTap: () { + Navigator.pop(context); + setState(() => _selectedIndex = 2); + }, + ), + DrawerItem( + icon: ApIcon.settings, + title: ap.settings, + onTap: () { + Navigator.pop(context); + setState(() => _selectedIndex = 3); + }, + ), + const GlassDivider(), + DrawerItem( + icon: ApIcon.powerSettingsNew, + title: ap.logout, + onTap: () { + Navigator.pop(context); + widget.onLogout(); + }, + ), + ], ); } - Widget _buildCard( - BuildContext context, { - required IconData icon, - required String title, - required String subtitle, - required VoidCallback onTap, - }) { - return GlassCard( - margin: const EdgeInsets.only(bottom: 12), - child: ListTile( - leading: Icon(icon), - title: Text(title), - subtitle: Text( - subtitle, - style: const TextStyle(fontSize: 12), + Widget _buildSettingsTab() { + final ApLocalizations ap = context.ap; + final LiquidGlassApAppState appState = + LiquidGlassApApp.of(context); + + return AdaptiveLiquidGlassLayer( + child: Scaffold( + body: Stack( + children: [ + ListView( + padding: EdgeInsets.only( + top: + MediaQuery.of(context).padding.top + + 60 + + 16, + left: 16, + right: 16, + bottom: 80, + ), + children: [ + GlassCard( + child: Column( + children: [ + GlassListTile( + leading: const Icon( + Icons.brightness_6, + ), + title: const Text('Dark Mode'), + trailing: GlassSwitch( + value: appState.themeMode == + ThemeMode.dark, + onChanged: (bool value) { + appState.setThemeMode( + value + ? ThemeMode.dark + : ThemeMode.light, + ); + }, + ), + ), + const GlassDivider(), + GlassListTile( + leading: const Icon( + Icons.info_outline, + ), + title: Text(ap.about), + trailing: GlassListTile.chevron, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + const GlassAboutUsPage( + assetImage: + ImageAssets.K, + githubName: 'NKUST-ITC', + email: + 'abc873693@gmail.com', + appLicense: + 'MIT License', + fbFanPageId: + '735951703168873', + fbFanPageUrl: + 'https://www.facebook.com/NKUST.ITC/', + githubUrl: + 'https://github.com/NKUST-ITC', + ), + ), + ); + }, + ), + const GlassDivider(), + GlassListTile( + leading: const Icon( + Icons.logout, + ), + title: Text(ap.logout), + onTap: widget.onLogout, + ), + ], + ), + ), + ], + ), + GlassFloatingToolbar( + leading: [ + Text( + ap.settings, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], ), - trailing: const Icon(Icons.chevron_right), - onTap: onTap, ), ); } diff --git a/apps/example_liquid_glass/lib/pages/login_page.dart b/apps/example_liquid_glass/lib/pages/login_page.dart index eb84a0c4..d560b45f 100644 --- a/apps/example_liquid_glass/lib/pages/login_page.dart +++ b/apps/example_liquid_glass/lib/pages/login_page.dart @@ -2,20 +2,202 @@ import 'package:ap_common_example_liquid_glass/res/assets.dart'; import 'package:ap_common_liquid_glass/ap_common_liquid_glass.dart'; import 'package:flutter/material.dart'; -/// Login page using [GlassApLoginPage]. -class GlassLoginPage extends StatelessWidget { - const GlassLoginPage({super.key}); +/// Login page using [GlassLoginScaffold] with glass widgets. +class GlassLoginPage extends StatefulWidget { + const GlassLoginPage({ + super.key, + required this.onLoginSuccess, + }); + + final VoidCallback onLoginSuccess; + + @override + State createState() => + _GlassLoginPageState(); +} + +class _GlassLoginPageState extends State { + final TextEditingController _username = + TextEditingController(); + final TextEditingController _password = + TextEditingController(); + final FocusNode _usernameFocus = FocusNode(); + final FocusNode _passwordFocus = FocusNode(); + + @override + void dispose() { + _username.dispose(); + _password.dispose(); + _usernameFocus.dispose(); + _passwordFocus.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - return GlassApLoginPage( + final ApLocalizations ap = context.ap; + final ColorScheme colorScheme = + Theme.of(context).colorScheme; + + return GlassLoginScaffold( logoMode: LogoMode.image, logoSource: ImageAssets.K, - onLogin: (String username, String password) async { - await Future.delayed(const Duration(seconds: 1)); - return true; - }, - enableOfflineLogin: true, + forms: [ + TextField( + controller: _username, + focusNode: _usernameFocus, + textInputAction: TextInputAction.next, + onSubmitted: (_) => + _passwordFocus.requestFocus(), + style: TextStyle( + color: colorScheme.onSurface, + ), + decoration: InputDecoration( + hintText: ap.studentId, + hintStyle: TextStyle( + color: colorScheme.onSurface + .withAlpha(128), + ), + filled: false, + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.onSurface + .withAlpha(77), + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.onSurface + .withAlpha(77), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.primary, + ), + ), + contentPadding: + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _password, + focusNode: _passwordFocus, + obscureText: true, + textInputAction: TextInputAction.send, + onSubmitted: (_) => _login(), + style: TextStyle( + color: colorScheme.onSurface, + ), + decoration: InputDecoration( + hintText: ap.password, + hintStyle: TextStyle( + color: colorScheme.onSurface + .withAlpha(128), + ), + filled: false, + border: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.onSurface + .withAlpha(77), + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.onSurface + .withAlpha(77), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: + BorderRadius.circular(10), + borderSide: BorderSide( + color: colorScheme.primary, + ), + ), + contentPadding: + const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + const SizedBox(height: 20), + GlassButton.custom( + onTap: _login, + width: double.infinity, + height: 48, + shape: const LiquidRoundedSuperellipse( + borderRadius: 12, + ), + child: Text( + ap.login, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + ), + ], ); } + + Future _login() async { + final ApLocalizations ap = context.ap; + + if (_username.text.isEmpty || + _password.text.isEmpty) { + UiUtil.instance.showToast( + context, + ap.doNotEmpty, + ); + return; + } + + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => PopScope( + canPop: false, + child: Center( + child: GlassCard( + useOwnLayer: true, + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const GlassProgressIndicator + .circular(), + const SizedBox(height: 16), + Text(ap.logining), + ], + ), + ), + ), + ), + ); + + await Future.delayed( + const Duration(seconds: 1), + ); + + if (!mounted) return; + Navigator.of(context, rootNavigator: true).pop(); + widget.onLoginSuccess(); + } } diff --git a/apps/example_liquid_glass/macos/Runner.xcodeproj/project.pbxproj b/apps/example_liquid_glass/macos/Runner.xcodeproj/project.pbxproj index 7ab697ec..90929011 100644 --- a/apps/example_liquid_glass/macos/Runner.xcodeproj/project.pbxproj +++ b/apps/example_liquid_glass/macos/Runner.xcodeproj/project.pbxproj @@ -195,7 +195,6 @@ 9D26850C2DE263EBF4F94647 /* Pods-RunnerTests.release.xcconfig */, 947A67C3FDFE60B13F42508E /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -592,6 +591,17 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -724,6 +734,17 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + ENABLE_APP_SANDBOX = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = NO; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/apps/example_liquid_glass/pubspec.yaml b/apps/example_liquid_glass/pubspec.yaml index f79c887f..d0d625df 100644 --- a/apps/example_liquid_glass/pubspec.yaml +++ b/apps/example_liquid_glass/pubspec.yaml @@ -13,6 +13,8 @@ resolution: workspace dependencies: ap_common: path: ../../packages/ap_common + ap_common_announcement_ui: + path: ../../packages/ap_common_announcement_ui ap_common_liquid_glass: path: ../../packages/ap_common_liquid_glass encrypt: any