mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-14 10:32:46 +08:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
c24670d5d8 | |||
a50c456390 | |||
915eb47ab6 | |||
c442a5d2e7 |
@ -60,7 +60,7 @@ void main() {
|
|||||||
expect(firstStoryFinder, findsOneWidget);
|
expect(firstStoryFinder, findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(firstStoryFinder);
|
await tester.tap(firstStoryFinder);
|
||||||
await tester.pump(const Duration(seconds: 4));
|
await tester.pump(const Duration(seconds: 5));
|
||||||
},
|
},
|
||||||
reportKey: 'scrolling_timeline',
|
reportKey: 'scrolling_timeline',
|
||||||
);
|
);
|
||||||
|
@ -156,6 +156,6 @@ SPEC CHECKSUMS:
|
|||||||
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
|
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
|
||||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||||
|
|
||||||
PODFILE CHECKSUM: 4f065ae0c88ce0d29cd933be6950e8da0fa046fc
|
PODFILE CHECKSUM: f03c7c11cf2b623592c89c68c628682778bb78b4
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.15.2
|
||||||
|
@ -126,7 +126,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> with Loggable {
|
|||||||
.copyWithStatusUpdated(type: type, to: Status.inProgress),
|
.copyWithStatusUpdated(type: type, to: Status.inProgress),
|
||||||
);
|
);
|
||||||
_offlineRepository
|
_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)))
|
.listen((Story story) => add(StoryLoaded(story: story, type: type)))
|
||||||
.onDone(() => add(StoryLoadingCompleted(type: type)));
|
.onDone(() => add(StoryLoadingCompleted(type: type)));
|
||||||
} else if (event.useApi || state.dataSource == HackerNewsDataSource.api) {
|
} else if (event.useApi || state.dataSource == HackerNewsDataSource.api) {
|
||||||
@ -142,7 +144,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> with Loggable {
|
|||||||
|
|
||||||
await _hackerNewsRepository
|
await _hackerNewsRepository
|
||||||
.fetchStoriesStream(
|
.fetchStoriesStream(
|
||||||
ids: ids.sublist(0, _pageSize),
|
ids: ids.sublist(0, min(_pageSize, ids.length)),
|
||||||
sequential: true,
|
sequential: true,
|
||||||
)
|
)
|
||||||
.listen((Story story) {
|
.listen((Story story) {
|
||||||
|
@ -11,12 +11,19 @@ class AppException implements Exception {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RateLimitedException extends AppException {
|
class RateLimitedException extends AppException {
|
||||||
RateLimitedException() : super(message: 'Rate limited...');
|
RateLimitedException(this.statusCode)
|
||||||
|
: super(message: 'Rate limited ($statusCode)...');
|
||||||
|
|
||||||
|
final int? statusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RateLimitedWithFallbackException extends AppException {
|
class RateLimitedWithFallbackException extends AppException {
|
||||||
RateLimitedWithFallbackException()
|
RateLimitedWithFallbackException(this.statusCode)
|
||||||
: super(message: 'Rate limited, fetching from API instead...');
|
: super(
|
||||||
|
message: 'Rate limited ($statusCode), fetching from API instead...',
|
||||||
|
);
|
||||||
|
|
||||||
|
final int? statusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PossibleParsingException extends AppException {
|
class PossibleParsingException extends AppException {
|
||||||
|
@ -5,10 +5,12 @@ import 'dart:math';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:dio_smart_retry/dio_smart_retry.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/repositories/hacker_news_repository.dart';
|
import 'package:hacki/repositories/hacker_news_repository.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
@ -17,13 +19,18 @@ import 'package:html/parser.dart';
|
|||||||
import 'package:html_unescape/html_unescape.dart';
|
import 'package:html_unescape/html_unescape.dart';
|
||||||
|
|
||||||
/// For fetching anything that cannot be fetched through Hacker News API.
|
/// For fetching anything that cannot be fetched through Hacker News API.
|
||||||
class HackerNewsWebRepository {
|
class HackerNewsWebRepository with Loggable {
|
||||||
HackerNewsWebRepository({
|
HackerNewsWebRepository({
|
||||||
RemoteConfigCubit? remoteConfigCubit,
|
RemoteConfigCubit? remoteConfigCubit,
|
||||||
HackerNewsRepository? hackerNewsRepository,
|
HackerNewsRepository? hackerNewsRepository,
|
||||||
Dio? dioWithCache,
|
Dio? dioWithCache,
|
||||||
Dio? dio,
|
Dio? dio,
|
||||||
}) : _dio = dio ?? Dio(),
|
}) : _dio = dio ?? Dio()
|
||||||
|
..interceptors.addAll(
|
||||||
|
<Interceptor>[
|
||||||
|
if (kDebugMode) LoggerInterceptor(),
|
||||||
|
],
|
||||||
|
),
|
||||||
_dioWithCache = dioWithCache ?? Dio()
|
_dioWithCache = dioWithCache ?? Dio()
|
||||||
..interceptors.addAll(
|
..interceptors.addAll(
|
||||||
<Interceptor>[
|
<Interceptor>[
|
||||||
@ -34,10 +41,18 @@ class HackerNewsWebRepository {
|
|||||||
_remoteConfigCubit =
|
_remoteConfigCubit =
|
||||||
remoteConfigCubit ?? locator.get<RemoteConfigCubit>(),
|
remoteConfigCubit ?? locator.get<RemoteConfigCubit>(),
|
||||||
_hackerNewsRepository =
|
_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;
|
final Dio _dioWithCache;
|
||||||
|
|
||||||
|
/// The client for fetching stories.
|
||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
|
|
||||||
final RemoteConfigCubit _remoteConfigCubit;
|
final RemoteConfigCubit _remoteConfigCubit;
|
||||||
final HackerNewsRepository _hackerNewsRepository;
|
final HackerNewsRepository _hackerNewsRepository;
|
||||||
|
|
||||||
@ -66,6 +81,10 @@ class HackerNewsWebRepository {
|
|||||||
String get _moreLinkSelector => _remoteConfigCubit.state.moreLinkSelector;
|
String get _moreLinkSelector => _remoteConfigCubit.state.moreLinkSelector;
|
||||||
|
|
||||||
static final Map<int, int> _next = <int, int>{};
|
static final Map<int, int> _next = <int, int>{};
|
||||||
|
static const List<int> _rateLimitedStatusCode = <int>[
|
||||||
|
HttpStatus.forbidden,
|
||||||
|
HttpStatus.serviceUnavailable,
|
||||||
|
];
|
||||||
|
|
||||||
Stream<Story> fetchStoriesStream(
|
Stream<Story> fetchStoriesStream(
|
||||||
StoryType storyType, {
|
StoryType storyType, {
|
||||||
@ -84,6 +103,7 @@ class HackerNewsWebRepository {
|
|||||||
StoryType.latest =>
|
StoryType.latest =>
|
||||||
'$_storiesBaseUrl/${storyType.webPathParam}?next=${_next[page]}'
|
'$_storiesBaseUrl/${storyType.webPathParam}?next=${_next[page]}'
|
||||||
};
|
};
|
||||||
|
|
||||||
final Uri url = Uri.parse(urlStr);
|
final Uri url = Uri.parse(urlStr);
|
||||||
final Options option = Options(
|
final Options option = Options(
|
||||||
headers: _headers,
|
headers: _headers,
|
||||||
@ -125,8 +145,9 @@ class HackerNewsWebRepository {
|
|||||||
(elements.elementAt(index), subtextElements.elementAt(index)),
|
(elements.elementAt(index), subtextElements.elementAt(index)),
|
||||||
);
|
);
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.response?.statusCode == HttpStatus.forbidden) {
|
logError('error fetching stories on page $page: $e');
|
||||||
throw RateLimitedWithFallbackException();
|
if (_rateLimitedStatusCode.contains(e.response?.statusCode)) {
|
||||||
|
throw RateLimitedWithFallbackException(e.response?.statusCode);
|
||||||
}
|
}
|
||||||
throw GenericException();
|
throw GenericException();
|
||||||
}
|
}
|
||||||
@ -260,8 +281,9 @@ class HackerNewsWebRepository {
|
|||||||
elements.map((Element e) => int.tryParse(e.id)).whereNotNull();
|
elements.map((Element e) => int.tryParse(e.id)).whereNotNull();
|
||||||
return parsedIds;
|
return parsedIds;
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.response?.statusCode == HttpStatus.forbidden) {
|
if (_rateLimitedStatusCode.contains(e.response?.statusCode)) {
|
||||||
throw RateLimitedException();
|
logError('error fetching favorites on page $page: $e');
|
||||||
|
throw RateLimitedException(e.response?.statusCode);
|
||||||
}
|
}
|
||||||
throw GenericException();
|
throw GenericException();
|
||||||
}
|
}
|
||||||
@ -338,8 +360,9 @@ class HackerNewsWebRepository {
|
|||||||
document.querySelectorAll(_athingComtrSelector);
|
document.querySelectorAll(_athingComtrSelector);
|
||||||
return elements;
|
return elements;
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.response?.statusCode == HttpStatus.forbidden) {
|
if (_rateLimitedStatusCode.contains(e.response?.statusCode)) {
|
||||||
throw RateLimitedWithFallbackException();
|
logError('error fetching comments on page $page: $e');
|
||||||
|
throw RateLimitedWithFallbackException(e.response?.statusCode);
|
||||||
}
|
}
|
||||||
throw GenericException();
|
throw GenericException();
|
||||||
}
|
}
|
||||||
@ -484,4 +507,7 @@ class HackerNewsWebRepository {
|
|||||||
)
|
)
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get logIdentifier => 'HackerNewsWebRepository';
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ class TabletHomeScreen extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final Widget homeScreen;
|
final Widget homeScreen;
|
||||||
static const double _dragPanelWidth = Dimens.pt6;
|
static const double _dragPanelWidth = Dimens.pt2;
|
||||||
static const double _dragDotHeight = Dimens.pt30;
|
static const double _dragDotHeight = Dimens.pt30;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -80,6 +80,7 @@ class TabletHomeScreen extends StatelessWidget {
|
|||||||
curve: Curves.elasticOut,
|
curve: Curves.elasticOut,
|
||||||
child: const _TabletStoryView(),
|
child: const _TabletStoryView(),
|
||||||
),
|
),
|
||||||
|
if (!state.expanded) ...<Widget>[
|
||||||
Positioned(
|
Positioned(
|
||||||
left: submissionPanelWidth,
|
left: submissionPanelWidth,
|
||||||
top: Dimens.zero,
|
top: Dimens.zero,
|
||||||
@ -87,7 +88,9 @@ class TabletHomeScreen extends StatelessWidget {
|
|||||||
width: _dragPanelWidth,
|
width: _dragPanelWidth,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
||||||
context.read<SplitViewCubit>().updateSubmissionPanelWidth(
|
context
|
||||||
|
.read<SplitViewCubit>()
|
||||||
|
.updateSubmissionPanelWidth(
|
||||||
details.globalPosition.dx,
|
details.globalPosition.dx,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -101,13 +104,15 @@ class TabletHomeScreen extends StatelessWidget {
|
|||||||
left: submissionPanelWidth +
|
left: submissionPanelWidth +
|
||||||
_dragPanelWidth / 2 -
|
_dragPanelWidth / 2 -
|
||||||
_dragDotHeight / 2,
|
_dragDotHeight / 2,
|
||||||
top:
|
top: (MediaQuery.of(context).size.height - _dragDotHeight) /
|
||||||
(MediaQuery.of(context).size.height - _dragDotHeight) / 2,
|
2,
|
||||||
height: _dragDotHeight,
|
height: _dragDotHeight,
|
||||||
width: _dragDotHeight,
|
width: _dragDotHeight,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
||||||
context.read<SplitViewCubit>().updateSubmissionPanelWidth(
|
context
|
||||||
|
.read<SplitViewCubit>()
|
||||||
|
.updateSubmissionPanelWidth(
|
||||||
details.globalPosition.dx,
|
details.globalPosition.dx,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -129,6 +134,7 @@ class TabletHomeScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -115,8 +115,16 @@ class LinkView extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: layoutHeight,
|
height: layoutHeight,
|
||||||
width: layoutHeight,
|
width: layoutHeight,
|
||||||
child: CachedNetworkImage(
|
child: imageUri == null && url.isEmpty
|
||||||
imageUrl: imageUri ?? '',
|
? FadeIn(
|
||||||
|
child: Center(
|
||||||
|
child: _HackerNewsImage(
|
||||||
|
height: layoutHeight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: CachedNetworkImage(
|
||||||
|
imageUrl: imageUri ?? Constants.favicon(url),
|
||||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||||
cacheKey: imageUri,
|
cacheKey: imageUri,
|
||||||
errorWidget: (_, __, ___) {
|
errorWidget: (_, __, ___) {
|
||||||
|
10
pubspec.lock
10
pubspec.lock
@ -254,6 +254,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.6.0"
|
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:
|
dio_web_adapter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1571,4 +1579,4 @@ packages:
|
|||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.4.0 <4.0.0"
|
dart: ">=3.4.0 <4.0.0"
|
||||||
flutter: ">=3.24.2"
|
flutter: ">=3.24.3"
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 2.9.1+149
|
version: 2.9.2+150
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
flutter: "3.24.2"
|
flutter: "3.24.3"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
adaptive_theme: ^3.2.0
|
adaptive_theme: ^3.2.0
|
||||||
@ -18,6 +18,7 @@ dependencies:
|
|||||||
connectivity_plus: ^6.0.3
|
connectivity_plus: ^6.0.3
|
||||||
device_info_plus: ^10.1.0
|
device_info_plus: ^10.1.0
|
||||||
dio: ^5.4.3+1
|
dio: ^5.4.3+1
|
||||||
|
dio_smart_retry: ^6.0.0
|
||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
fast_gbk: ^1.0.0
|
fast_gbk: ^1.0.0
|
||||||
feature_discovery:
|
feature_discovery:
|
||||||
|
Submodule submodules/flutter updated: 4cf269e36d...2663184aa7
Reference in New Issue
Block a user