Skip to content

Commit

Permalink
Fix/ Optimize and Add Loading Indicator for Auto-Scroller in Reading (#…
Browse files Browse the repository at this point in the history
…1475)

* refactor: Optimize Quran Reading Auto-Scroll and Add Loading Indicator

* fix: stopping the auto-scroll causes an unwanted page advancement

* fix: initializing the scrollController with the correct offset

* fix exit on scroll to correct page

* remove unecssary state rotation

* fix: the change in page caused by the rotation

* fix: enlarging the font size

* fix format

* fix: reinitialize the font or reset it again

* refactor: use text instead of loading

* fix: formatting

* fix: white screen for the box

* show logs in riverpod using print

* refactor: the `AutoScrollState` with equtable

* test another prod solution

* test

* try fix prod

* add more logs

* refactor: add more commit

* test: working on passing the controller from the widget screen

* fix(quran): resolve page initialization and loading states

- Fix white screen flash during initial load
- Add proper page selection initialization
- Optimize SVG loading performance
- Implement smooth page transitions

* fix(quran): optimize SVG loading performance & prevent UI freeze

- Add progressive page loading
- Implement page caching mechanism
- Fix initial page jump issues
- Optimize loading states

* code format

* remove print

* fix the formatting

---------

Co-authored-by: Yassin <yassinnouh21@gmail.com>
Co-authored-by: Yassin Nouh <70436855+YassinNouh21@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 9, 2025
1 parent 4eacd0b commit 24857a4
Show file tree
Hide file tree
Showing 11 changed files with 443 additions and 125 deletions.
4 changes: 2 additions & 2 deletions lib/l10n/intl_ar.arb
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,6 @@
},
"ishaAndFajrOnly": "فقط صلاتي الفجر و العشاء",
"minutesBeforeFajrPrayer": "دقائق قبل وقت صلاة الفجر",
"minutesAfterIshaPrayer": "دقائق بعد وقت صلاة العشاء"

"minutesAfterIshaPrayer": "دقائق بعد وقت صلاة العشاء",
"initializingAutoReading": "جاري التهيئة..."
}
3 changes: 2 additions & 1 deletion lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -424,5 +424,6 @@
"selectReciter": "Select a Reciter",
"selectMoshaf": "Select a Mushaf",
"randomSurahSelection": "Random Surah Selection",
"selectSurah": "Select a Surah"
"selectSurah": "Select a Surah",
"initializingAutoReading": "Initializing in progress..."
}
8 changes: 4 additions & 4 deletions lib/src/helpers/AppDate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ class AppDateTime {
static final DateTime _initialRealTime = DateTime.now();
static final DateTime _initialDebugTime = DateTime(
_initialRealTime.year,
_initialRealTime.month,
_initialRealTime.day,
13,
48,
11,
1,
00,
-20,
00,
);

Expand Down
1 change: 0 additions & 1 deletion lib/src/pages/quran/page/quran_reading_screen.dart

This file was deleted.

1 change: 0 additions & 1 deletion lib/src/pages/quran/page/reciter_selection_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:mawaqit/const/resource.dart';
import 'package:mawaqit/src/helpers/RelativeSizes.dart';
import 'package:mawaqit/src/pages/quran/page/quran_reading_screen.dart';
import 'package:mawaqit/src/pages/quran/page/schedule_screen.dart';
import 'package:mawaqit/src/pages/quran/widget/recite_type_grid_view.dart';
import 'package:mawaqit/src/services/theme_manager.dart';
Expand Down
229 changes: 201 additions & 28 deletions lib/src/pages/quran/reading/quran_reading_screen.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import 'dart:developer';

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
Expand All @@ -25,6 +22,8 @@ import 'package:provider/provider.dart' as provider;

import 'package:mawaqit/src/pages/quran/widget/reading/quran_reading_page_selector.dart';
import 'package:mawaqit/src/routes/routes_constant.dart';
import 'dart:math' as math;
import 'package:flutter_svg/flutter_svg.dart';

abstract class QuranViewStrategy {
Widget buildView(QuranReadingState state, WidgetRef ref, BuildContext context);
Expand Down Expand Up @@ -63,6 +62,7 @@ class FocusNodes {
required this.switchScreenViewFocusNode,
required this.switchQuranModeNode,
});

void setupFocusTraversal({required bool isPortrait}) {
if (isPortrait) {
setupPortraitFocusTraversal();
Expand Down Expand Up @@ -254,38 +254,207 @@ class FocusNodes {
}
}

class AutoScrollViewStrategy implements QuranViewStrategy {
class AutoScrollReadingView extends ConsumerStatefulWidget {
final AutoScrollState autoScrollState;
final int initialPage;

AutoScrollViewStrategy(this.autoScrollState);
AutoScrollReadingView({
required this.autoScrollState,
this.initialPage = 1,
});

@override
Widget buildView(QuranReadingState state, WidgetRef ref, BuildContext context) {
final scalingFactor = autoScrollState.fontSize;
_AutoScrollReadingViewState createState() => _AutoScrollReadingViewState();
}

return ListView.builder(
physics: NeverScrollableScrollPhysics(),
controller: autoScrollState.scrollController,
itemCount: state.totalPages,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
final autoScrollNotifier = ref.read(autoScrollNotifierProvider.notifier);
if (autoScrollState.isPlaying) {
autoScrollNotifier.pauseAutoScroll();
} else {
autoScrollNotifier.resumeAutoScroll();
class _AutoScrollReadingViewState extends ConsumerState<AutoScrollReadingView> {
late ScrollController scrollController;
bool _isInitialized = false;
bool _isLoading = true;
double? _cachedItemHeight;
Map<int, bool> _loadedPages = {};

@override
void initState() {
super.initState();
scrollController = ScrollController();
_initializeScrollView();
}

Future<void> _initializeScrollView() async {
try {
if (!mounted) return;

setState(() {
_isLoading = true;
_isInitialized = false;
});

// Load initial pages in microtask to prevent UI freeze
await Future.microtask(() async {
final readingState = ref.read(quranReadingNotifierProvider);

await readingState.whenOrNull(
data: (data) async {
// Preload pages around initial page
final startIndex = math.max(0, widget.initialPage - 1);
final endIndex = math.min(data.totalPages, widget.initialPage + 1);

for (var i = startIndex; i < endIndex; i++) {
_loadedPages[i] = true;
}
},
child: SizedBox(
width: MediaQuery.of(context).size.width * scalingFactor,
height: MediaQuery.of(context).size.height * scalingFactor,
child: SvgPictureWidget(
svgPicture: state.svgs[index],
);
});

// Small delay to ensure layout is ready
await Future.delayed(Duration(milliseconds: 50));

if (!mounted) return;

await _jumpToInitialPage();
ref.read(autoScrollNotifierProvider.notifier).setScrollController(scrollController);

if (mounted) {
setState(() {
_isInitialized = true;
_isLoading = false;
});
}
} catch (e) {
print('Initialization error: $e');
if (mounted) {
setState(() => _isLoading = false);
}
}
}

Future<void> _jumpToInitialPage() async {
if (widget.initialPage <= 1) return;

try {
if (!mounted) return;

final size = MediaQuery.of(context).size;
final scalingFactor = widget.autoScrollState.fontSize;
final itemHeight = size.height * scalingFactor;
_cachedItemHeight = itemHeight;

final scrollPosition = (widget.initialPage - 1) * itemHeight;
scrollController.jumpTo(scrollPosition);
} catch (e) {
print('Error jumping to initial page: $e');
}
}

Widget _buildPage(int index, SvgPicture svgPicture, double scalingFactor) {
// Load page only when it becomes visible
if (!_loadedPages.containsKey(index)) {
Future.microtask(() {
if (mounted) {
setState(() => _loadedPages[index] = true);
}
});

return SizedBox(
width: MediaQuery.of(context).size.width * scalingFactor,
height: MediaQuery.of(context).size.height * scalingFactor,
child: Center(child: CircularProgressIndicator()),
);
}

return GestureDetector(
onTap: _handleTap,
child: SizedBox(
width: MediaQuery.of(context).size.width * scalingFactor,
height: MediaQuery.of(context).size.height * scalingFactor,
child: SvgPictureWidget(
key: ValueKey('page_$index'),
svgPicture: svgPicture,
),
),
);
}

Widget _buildLoadingIndicator() {
return Container(
color: Colors.black.withOpacity(0.9),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Colors.white),
SizedBox(height: 16),
Text(
S.of(context).initializingAutoReading,
style: Theme.of(context).textTheme.headlineMedium!.copyWith(color: Colors.white),
),
],
),
),
);
}

void _handleTap() {
final autoScrollNotifier = ref.read(autoScrollNotifierProvider.notifier);
if (widget.autoScrollState.isPlaying) {
autoScrollNotifier.pauseAutoScroll();
} else {
autoScrollNotifier.resumeAutoScroll();
}
}

@override
Widget build(BuildContext context) {
final scalingFactor = widget.autoScrollState.fontSize;
final readingState = ref.watch(quranReadingNotifierProvider);
final total = readingState.whenOrNull(data: (data) => data.totalPages) ?? 0;
final pages = readingState.whenOrNull(data: (data) => data.svgs) ?? [];

return Stack(
children: [
Container(color: Theme.of(context).scaffoldBackgroundColor),
if (pages.isNotEmpty)
ListView.builder(
physics: NeverScrollableScrollPhysics(),
controller: scrollController,
itemCount: total,
cacheExtent: MediaQuery.of(context).size.height * 2,
itemBuilder: (context, index) {
if (!_isInitialized) {
return SizedBox(
height: _cachedItemHeight ?? MediaQuery.of(context).size.height * scalingFactor,
);
}

return _buildPage(index, pages[index], scalingFactor);
},
),
);
},
if (_isLoading || widget.autoScrollState.isLoading) _buildLoadingIndicator(),
],
);
}

@override
void dispose() {
scrollController.dispose();
_loadedPages.clear();
super.dispose();
}
}

// Update AutoScrollViewStrategy to use AutoScrollReadingView
class AutoScrollViewStrategy implements QuranViewStrategy {
final AutoScrollState autoScrollState;
final int initialPage;

AutoScrollViewStrategy(this.autoScrollState, {this.initialPage = 1});

@override
Widget buildView(QuranReadingState state, WidgetRef ref, BuildContext context) {
return AutoScrollReadingView(
autoScrollState: autoScrollState,
initialPage: initialPage,
);
}

Expand Down Expand Up @@ -580,8 +749,12 @@ class _QuranReadingScreenState extends ConsumerState<QuranReadingScreen> {
error: (error, s) => _buildErrorIndicator(error),
data: (state) {
// Initialize the appropriate strategy
final viewStrategy =
autoScrollState.isSinglePageView ? AutoScrollViewStrategy(autoScrollState) : NormalViewStrategy(isPortrait);
final viewStrategy = autoScrollState.isSinglePageView
? AutoScrollViewStrategy(
autoScrollState,
initialPage: state.currentPage, // Or whatever page you want to start from
)
: NormalViewStrategy(isPortrait);

// Create focus nodes bundle
final focusNodes = FocusNodes(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ class _AutoScrollingReadingMode extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
_ExitButton(
quranReadingState: quranReadingState,
isPortrait: isPortrait,
),
SizedBox(height: 1.h),
Expand Down Expand Up @@ -215,10 +216,12 @@ class _FontSizeControls extends ConsumerWidget {
// Add new Exit button widget
class _ExitButton extends ConsumerStatefulWidget {
final bool isPortrait;
final QuranReadingState quranReadingState;

const _ExitButton({
super.key,
required this.isPortrait,
required this.quranReadingState,
});

@override
Expand All @@ -244,7 +247,9 @@ class __ExitButtonState extends ConsumerState<_ExitButton> {
isPortrait: widget.isPortrait,
icon: Icons.close,
onPressed: () {
ref.read(autoScrollNotifierProvider.notifier).stopAutoScroll();
ref
.read(autoScrollNotifierProvider.notifier)
.stopAutoScroll(isPortairt: widget.isPortrait, quranReadingState: widget.quranReadingState);
},
tooltip: 'Exit Auto-Scroll',
);
Expand Down Expand Up @@ -326,7 +331,6 @@ class _ActionButton extends StatelessWidget {

class _OrientationToggleButton extends ConsumerWidget {
final FocusNode switchScreenViewFocusNode;

const _OrientationToggleButton({
required this.switchScreenViewFocusNode,
});
Expand Down
Loading

0 comments on commit 24857a4

Please sign in to comment.