Compare commits

...

4 Commits

10 changed files with 142 additions and 84 deletions

View File

@ -60,7 +60,7 @@ void main() {
expect(firstStoryFinder, findsOneWidget);
await tester.tap(firstStoryFinder);
await tester.pump(const Duration(seconds: 4));
await tester.pump(const Duration(seconds: 5));
},
reportKey: 'scrolling_timeline',
);

View File

@ -156,6 +156,6 @@ SPEC CHECKSUMS:
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
PODFILE CHECKSUM: 4f065ae0c88ce0d29cd933be6950e8da0fa046fc
PODFILE CHECKSUM: f03c7c11cf2b623592c89c68c628682778bb78b4
COCOAPODS: 1.15.2

View File

@ -126,7 +126,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> with Loggable {
.copyWithStatusUpdated(type: type, to: Status.inProgress),
);
_offlineRepository
.getCachedStoriesStream(ids: ids.sublist(0, _pageSize))
.getCachedStoriesStream(
ids: ids.sublist(0, min(_pageSize, ids.length)),
)
.listen((Story story) => add(StoryLoaded(story: story, type: type)))
.onDone(() => add(StoryLoadingCompleted(type: type)));
} else if (event.useApi || state.dataSource == HackerNewsDataSource.api) {
@ -142,7 +144,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> with Loggable {
await _hackerNewsRepository
.fetchStoriesStream(
ids: ids.sublist(0, _pageSize),
ids: ids.sublist(0, min(_pageSize, ids.length)),
sequential: true,
)
.listen((Story story) {

View File

@ -11,12 +11,19 @@ class AppException implements Exception {
}
class RateLimitedException extends AppException {
RateLimitedException() : super(message: 'Rate limited...');
RateLimitedException(this.statusCode)
: super(message: 'Rate limited ($statusCode)...');
final int? statusCode;
}
class RateLimitedWithFallbackException extends AppException {
RateLimitedWithFallbackException()
: super(message: 'Rate limited, fetching from API instead...');
RateLimitedWithFallbackException(this.statusCode)
: super(
message: 'Rate limited ($statusCode), fetching from API instead...',
);
final int? statusCode;
}
class PossibleParsingException extends AppException {

View File

@ -5,10 +5,12 @@ import 'dart:math';
import 'package:collection/collection.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:flutter/foundation.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/hacker_news_repository.dart';
import 'package:hacki/utils/utils.dart';
@ -17,13 +19,18 @@ import 'package:html/parser.dart';
import 'package:html_unescape/html_unescape.dart';
/// For fetching anything that cannot be fetched through Hacker News API.
class HackerNewsWebRepository {
class HackerNewsWebRepository with Loggable {
HackerNewsWebRepository({
RemoteConfigCubit? remoteConfigCubit,
HackerNewsRepository? hackerNewsRepository,
Dio? dioWithCache,
Dio? dio,
}) : _dio = dio ?? Dio(),
}) : _dio = dio ?? Dio()
..interceptors.addAll(
<Interceptor>[
if (kDebugMode) LoggerInterceptor(),
],
),
_dioWithCache = dioWithCache ?? Dio()
..interceptors.addAll(
<Interceptor>[
@ -34,10 +41,18 @@ class HackerNewsWebRepository {
_remoteConfigCubit =
remoteConfigCubit ?? locator.get<RemoteConfigCubit>(),
_hackerNewsRepository =
hackerNewsRepository ?? locator.get<HackerNewsRepository>();
hackerNewsRepository ?? locator.get<HackerNewsRepository>() {
_dio.interceptors.add(RetryInterceptor(dio: _dio));
}
/// The client for fetching comments. We should be careful
/// while fetching comments because it will easily trigger
/// 503 from the server.
final Dio _dioWithCache;
/// The client for fetching stories.
final Dio _dio;
final RemoteConfigCubit _remoteConfigCubit;
final HackerNewsRepository _hackerNewsRepository;
@ -66,6 +81,10 @@ class HackerNewsWebRepository {
String get _moreLinkSelector => _remoteConfigCubit.state.moreLinkSelector;
static final Map<int, int> _next = <int, int>{};
static const List<int> _rateLimitedStatusCode = <int>[
HttpStatus.forbidden,
HttpStatus.serviceUnavailable,
];
Stream<Story> fetchStoriesStream(
StoryType storyType, {
@ -84,6 +103,7 @@ class HackerNewsWebRepository {
StoryType.latest =>
'$_storiesBaseUrl/${storyType.webPathParam}?next=${_next[page]}'
};
final Uri url = Uri.parse(urlStr);
final Options option = Options(
headers: _headers,
@ -125,8 +145,9 @@ class HackerNewsWebRepository {
(elements.elementAt(index), subtextElements.elementAt(index)),
);
} on DioException catch (e) {
if (e.response?.statusCode == HttpStatus.forbidden) {
throw RateLimitedWithFallbackException();
logError('error fetching stories on page $page: $e');
if (_rateLimitedStatusCode.contains(e.response?.statusCode)) {
throw RateLimitedWithFallbackException(e.response?.statusCode);
}
throw GenericException();
}
@ -260,8 +281,9 @@ class HackerNewsWebRepository {
elements.map((Element e) => int.tryParse(e.id)).whereNotNull();
return parsedIds;
} on DioException catch (e) {
if (e.response?.statusCode == HttpStatus.forbidden) {
throw RateLimitedException();
if (_rateLimitedStatusCode.contains(e.response?.statusCode)) {
logError('error fetching favorites on page $page: $e');
throw RateLimitedException(e.response?.statusCode);
}
throw GenericException();
}
@ -338,8 +360,9 @@ class HackerNewsWebRepository {
document.querySelectorAll(_athingComtrSelector);
return elements;
} on DioException catch (e) {
if (e.response?.statusCode == HttpStatus.forbidden) {
throw RateLimitedWithFallbackException();
if (_rateLimitedStatusCode.contains(e.response?.statusCode)) {
logError('error fetching comments on page $page: $e');
throw RateLimitedWithFallbackException(e.response?.statusCode);
}
throw GenericException();
}
@ -484,4 +507,7 @@ class HackerNewsWebRepository {
)
.trim();
}
@override
String get logIdentifier => 'HackerNewsWebRepository';
}

View File

@ -14,7 +14,7 @@ class TabletHomeScreen extends StatelessWidget {
});
final Widget homeScreen;
static const double _dragPanelWidth = Dimens.pt6;
static const double _dragPanelWidth = Dimens.pt2;
static const double _dragDotHeight = Dimens.pt30;
@override
@ -80,54 +80,60 @@ class TabletHomeScreen extends StatelessWidget {
curve: Curves.elasticOut,
child: const _TabletStoryView(),
),
Positioned(
left: submissionPanelWidth,
top: Dimens.zero,
bottom: Dimens.zero,
width: _dragPanelWidth,
child: GestureDetector(
onHorizontalDragUpdate: (DragUpdateDetails details) {
context.read<SplitViewCubit>().updateSubmissionPanelWidth(
details.globalPosition.dx,
);
},
child: ColoredBox(
color: Theme.of(context).colorScheme.tertiary,
child: const SizedBox.shrink(),
if (!state.expanded) ...<Widget>[
Positioned(
left: submissionPanelWidth,
top: Dimens.zero,
bottom: Dimens.zero,
width: _dragPanelWidth,
child: GestureDetector(
onHorizontalDragUpdate: (DragUpdateDetails details) {
context
.read<SplitViewCubit>()
.updateSubmissionPanelWidth(
details.globalPosition.dx,
);
},
child: ColoredBox(
color: Theme.of(context).colorScheme.tertiary,
child: const SizedBox.shrink(),
),
),
),
),
Positioned(
left: submissionPanelWidth +
_dragPanelWidth / 2 -
_dragDotHeight / 2,
top:
(MediaQuery.of(context).size.height - _dragDotHeight) / 2,
height: _dragDotHeight,
width: _dragDotHeight,
child: GestureDetector(
onHorizontalDragUpdate: (DragUpdateDetails details) {
context.read<SplitViewCubit>().updateSubmissionPanelWidth(
details.globalPosition.dx,
);
},
child: Container(
width: _dragDotHeight,
height: _dragDotHeight,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.tertiary,
shape: BoxShape.circle,
),
child: Center(
child: FaIcon(
FontAwesomeIcons.gripLinesVertical,
color: Theme.of(context).colorScheme.onTertiary,
size: TextDimens.pt16,
Positioned(
left: submissionPanelWidth +
_dragPanelWidth / 2 -
_dragDotHeight / 2,
top: (MediaQuery.of(context).size.height - _dragDotHeight) /
2,
height: _dragDotHeight,
width: _dragDotHeight,
child: GestureDetector(
onHorizontalDragUpdate: (DragUpdateDetails details) {
context
.read<SplitViewCubit>()
.updateSubmissionPanelWidth(
details.globalPosition.dx,
);
},
child: Container(
width: _dragDotHeight,
height: _dragDotHeight,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.tertiary,
shape: BoxShape.circle,
),
child: Center(
child: FaIcon(
FontAwesomeIcons.gripLinesVertical,
color: Theme.of(context).colorScheme.onTertiary,
size: TextDimens.pt16,
),
),
),
),
),
),
],
],
);
},

View File

@ -115,37 +115,45 @@ class LinkView extends StatelessWidget {
child: SizedBox(
height: layoutHeight,
width: layoutHeight,
child: CachedNetworkImage(
imageUrl: imageUri ?? '',
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
cacheKey: imageUri,
errorWidget: (_, __, ___) {
if (url.isEmpty) {
return FadeIn(
child: imageUri == null && url.isEmpty
? FadeIn(
child: Center(
child: _HackerNewsImage(
height: layoutHeight,
),
),
);
}
return Center(
child: CachedNetworkImage(
imageUrl: Constants.favicon(url),
fit: BoxFit.scaleDown,
cacheKey: iconUri,
)
: CachedNetworkImage(
imageUrl: imageUri ?? Constants.favicon(url),
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
cacheKey: imageUri,
errorWidget: (_, __, ___) {
return const FadeIn(
child: Icon(
Icons.public,
size: Dimens.pt20,
if (url.isEmpty) {
return FadeIn(
child: Center(
child: _HackerNewsImage(
height: layoutHeight,
),
),
);
}
return Center(
child: CachedNetworkImage(
imageUrl: Constants.favicon(url),
fit: BoxFit.scaleDown,
cacheKey: iconUri,
errorWidget: (_, __, ___) {
return const FadeIn(
child: Icon(
Icons.public,
size: Dimens.pt20,
),
);
},
),
);
},
),
);
},
),
),
),
)

View File

@ -254,6 +254,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.6.0"
dio_smart_retry:
dependency: "direct main"
description:
name: dio_smart_retry
sha256: "3d71450c19b4d91ef4c7d726a55a284bfc11eb3634f1f25006cdfab3f8595653"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
dio_web_adapter:
dependency: transitive
description:
@ -1571,4 +1579,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.4.0 <4.0.0"
flutter: ">=3.24.2"
flutter: ">=3.24.3"

View File

@ -1,11 +1,11 @@
name: hacki
description: A Hacker News reader.
version: 2.9.1+149
version: 2.9.2+150
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: "3.24.2"
flutter: "3.24.3"
dependencies:
adaptive_theme: ^3.2.0
@ -18,6 +18,7 @@ dependencies:
connectivity_plus: ^6.0.3
device_info_plus: ^10.1.0
dio: ^5.4.3+1
dio_smart_retry: ^6.0.0
equatable: ^2.0.5
fast_gbk: ^1.0.0
feature_discovery: