mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
3413b1686d | |||
c24670d5d8 | |||
a50c456390 | |||
915eb47ab6 | |||
c442a5d2e7 | |||
fbedf327ee | |||
45c684b774 | |||
b6015ae6ca |
@ -63,18 +63,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.4"
|
version: "10.0.5"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.5"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -95,18 +95,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.0"
|
version: "0.11.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.0"
|
version: "1.15.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -164,10 +164,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.0"
|
version: "0.7.2"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -180,10 +180,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.1"
|
version: "14.2.5"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.3.0 <4.0.0"
|
dart: ">=3.3.0 <4.0.0"
|
||||||
flutter: ">=3.18.0-18.0.pre.54"
|
flutter: ">=3.18.0-18.0.pre.54"
|
||||||
|
1
fastlane/metadata/android/en-US/changelogs/149.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/149.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
- Improved tablet mode, you can now resize submission panel.
|
@ -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',
|
||||||
);
|
);
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
<string>mailto</string>
|
<string>mailto</string>
|
||||||
</array>
|
</array>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>15.0</string>
|
<string>14.0</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
|
@ -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) {
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/screens/screens.dart';
|
import 'package:hacki/screens/screens.dart';
|
||||||
import 'package:hacki/services/services.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
|
|
||||||
part 'split_view_state.dart';
|
part 'split_view_state.dart';
|
||||||
|
|
||||||
class SplitViewCubit extends Cubit<SplitViewState> with Loggable {
|
class SplitViewCubit extends HydratedCubit<SplitViewState> with Loggable {
|
||||||
SplitViewCubit({
|
SplitViewCubit({
|
||||||
CommentCache? commentCache,
|
CommentCache? commentCache,
|
||||||
}) : _commentCache = commentCache ?? locator.get<CommentCache>(),
|
}) : _commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||||
@ -25,8 +26,36 @@ class SplitViewCubit extends Cubit<SplitViewState> with Loggable {
|
|||||||
|
|
||||||
void disableSplitView() => emit(state.copyWith(enabled: false));
|
void disableSplitView() => emit(state.copyWith(enabled: false));
|
||||||
|
|
||||||
void zoom() => emit(state.copyWith(expanded: !state.expanded));
|
void zoom() => emit(
|
||||||
|
state.copyWith(
|
||||||
|
expanded: !state.expanded,
|
||||||
|
resizingAnimationDuration: AppDurations.ms300,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
void updateSubmissionPanelWidth(double width) => emit(
|
||||||
|
state.copyWith(
|
||||||
|
submissionPanelWidth: width,
|
||||||
|
resizingAnimationDuration: Duration.zero,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get logIdentifier => '[SplitViewCubit]';
|
String get logIdentifier => '[SplitViewCubit]';
|
||||||
|
|
||||||
|
static const String _submissionPanelWidthKey = 'submissionPanelWidth';
|
||||||
|
|
||||||
|
@override
|
||||||
|
SplitViewState? fromJson(Map<String, dynamic> json) {
|
||||||
|
return state.copyWith(
|
||||||
|
submissionPanelWidth: json[_submissionPanelWidthKey] as double?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, dynamic>? toJson(SplitViewState state) {
|
||||||
|
return <String, dynamic>{
|
||||||
|
_submissionPanelWidthKey: state.submissionPanelWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,25 +5,36 @@ class SplitViewState extends Equatable {
|
|||||||
required this.itemScreenArgs,
|
required this.itemScreenArgs,
|
||||||
required this.expanded,
|
required this.expanded,
|
||||||
required this.enabled,
|
required this.enabled,
|
||||||
|
required this.resizingAnimationDuration,
|
||||||
|
this.submissionPanelWidth,
|
||||||
});
|
});
|
||||||
|
|
||||||
const SplitViewState.init()
|
const SplitViewState.init()
|
||||||
: enabled = false,
|
: enabled = false,
|
||||||
expanded = false,
|
expanded = false,
|
||||||
|
submissionPanelWidth = null,
|
||||||
|
resizingAnimationDuration = Duration.zero,
|
||||||
itemScreenArgs = null;
|
itemScreenArgs = null;
|
||||||
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final bool expanded;
|
final bool expanded;
|
||||||
|
final double? submissionPanelWidth;
|
||||||
|
final Duration resizingAnimationDuration;
|
||||||
final ItemScreenArgs? itemScreenArgs;
|
final ItemScreenArgs? itemScreenArgs;
|
||||||
|
|
||||||
SplitViewState copyWith({
|
SplitViewState copyWith({
|
||||||
bool? enabled,
|
bool? enabled,
|
||||||
bool? expanded,
|
bool? expanded,
|
||||||
|
double? submissionPanelWidth,
|
||||||
|
Duration? resizingAnimationDuration,
|
||||||
ItemScreenArgs? itemScreenArgs,
|
ItemScreenArgs? itemScreenArgs,
|
||||||
}) {
|
}) {
|
||||||
return SplitViewState(
|
return SplitViewState(
|
||||||
enabled: enabled ?? this.enabled,
|
enabled: enabled ?? this.enabled,
|
||||||
expanded: expanded ?? this.expanded,
|
expanded: expanded ?? this.expanded,
|
||||||
|
submissionPanelWidth: submissionPanelWidth ?? this.submissionPanelWidth,
|
||||||
|
resizingAnimationDuration:
|
||||||
|
resizingAnimationDuration ?? this.resizingAnimationDuration,
|
||||||
itemScreenArgs: itemScreenArgs ?? this.itemScreenArgs,
|
itemScreenArgs: itemScreenArgs ?? this.itemScreenArgs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -32,6 +43,8 @@ class SplitViewState extends Equatable {
|
|||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
enabled,
|
enabled,
|
||||||
expanded,
|
expanded,
|
||||||
|
submissionPanelWidth,
|
||||||
|
resizingAnimationDuration,
|
||||||
itemScreenArgs,
|
itemScreenArgs,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -110,11 +110,11 @@ final class SwipeGesturePreference extends BooleanPreference {
|
|||||||
String get key => 'swipeGestureMode';
|
String get key => 'swipeGestureMode';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get title => 'Swipe Gesture';
|
String get title => 'Swipe Gesture for Switching Tabs';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get subtitle =>
|
String get subtitle =>
|
||||||
'''enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu.''';
|
'''enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu and double tap to open the url (if complex tile is disabled).''';
|
||||||
}
|
}
|
||||||
|
|
||||||
final class NotificationModePreference extends BooleanPreference {
|
final class NotificationModePreference extends BooleanPreference {
|
||||||
|
@ -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';
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart' hide Badge;
|
import 'package:flutter/material.dart' hide Badge;
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/screens/screens.dart';
|
import 'package:hacki/screens/screens.dart';
|
||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
@ -14,6 +14,8 @@ class TabletHomeScreen extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final Widget homeScreen;
|
final Widget homeScreen;
|
||||||
|
static const double _dragPanelWidth = Dimens.pt2;
|
||||||
|
static const double _dragDotHeight = Dimens.pt30;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -28,16 +30,26 @@ class TabletHomeScreen extends StatelessWidget {
|
|||||||
|
|
||||||
return BlocBuilder<SplitViewCubit, SplitViewState>(
|
return BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||||
buildWhen: (SplitViewState previous, SplitViewState current) =>
|
buildWhen: (SplitViewState previous, SplitViewState current) =>
|
||||||
previous.expanded != current.expanded,
|
previous.expanded != current.expanded ||
|
||||||
|
previous.submissionPanelWidth != current.submissionPanelWidth,
|
||||||
builder: (BuildContext context, SplitViewState state) {
|
builder: (BuildContext context, SplitViewState state) {
|
||||||
|
double submissionPanelWidth =
|
||||||
|
state.submissionPanelWidth ?? homeScreenWidth;
|
||||||
|
|
||||||
|
/// Prevent overflow after orientation change.
|
||||||
|
if (submissionPanelWidth > MediaQuery.of(context).size.width) {
|
||||||
|
submissionPanelWidth =
|
||||||
|
MediaQuery.of(context).size.width - Dimens.pt64;
|
||||||
|
}
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
AnimatedPositioned(
|
AnimatedPositioned(
|
||||||
left: Dimens.zero,
|
left: Dimens.zero,
|
||||||
top: Dimens.zero,
|
top: Dimens.zero,
|
||||||
bottom: Dimens.zero,
|
bottom: Dimens.zero,
|
||||||
width: homeScreenWidth,
|
width: submissionPanelWidth,
|
||||||
duration: AppDurations.ms300,
|
duration: state.resizingAnimationDuration,
|
||||||
curve: Curves.elasticOut,
|
curve: Curves.elasticOut,
|
||||||
child: homeScreen,
|
child: homeScreen,
|
||||||
),
|
),
|
||||||
@ -46,7 +58,7 @@ class TabletHomeScreen extends StatelessWidget {
|
|||||||
left: Dimens.pt24,
|
left: Dimens.pt24,
|
||||||
bottom: Dimens.pt36,
|
bottom: Dimens.pt36,
|
||||||
height: Dimens.pt40,
|
height: Dimens.pt40,
|
||||||
width: homeScreenWidth - Dimens.pt24,
|
width: submissionPanelWidth - Dimens.pt48,
|
||||||
child: const CountdownReminder(),
|
child: const CountdownReminder(),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@ -54,18 +66,74 @@ class TabletHomeScreen extends StatelessWidget {
|
|||||||
left: Dimens.pt24,
|
left: Dimens.pt24,
|
||||||
bottom: Dimens.pt36,
|
bottom: Dimens.pt36,
|
||||||
height: Dimens.pt40,
|
height: Dimens.pt40,
|
||||||
width: homeScreenWidth - Dimens.pt24,
|
width: submissionPanelWidth - Dimens.pt48,
|
||||||
child: const DownloadProgressReminder(),
|
child: const DownloadProgressReminder(),
|
||||||
),
|
),
|
||||||
AnimatedPositioned(
|
AnimatedPositioned(
|
||||||
right: Dimens.zero,
|
right: Dimens.zero,
|
||||||
top: Dimens.zero,
|
top: Dimens.zero,
|
||||||
bottom: Dimens.zero,
|
bottom: Dimens.zero,
|
||||||
left: state.expanded ? Dimens.zero : homeScreenWidth,
|
left: state.expanded
|
||||||
duration: AppDurations.ms300,
|
? Dimens.zero
|
||||||
|
: submissionPanelWidth + _dragPanelWidth,
|
||||||
|
duration: state.resizingAnimationDuration,
|
||||||
curve: Curves.elasticOut,
|
curve: Curves.elasticOut,
|
||||||
child: const _TabletStoryView(),
|
child: const _TabletStoryView(),
|
||||||
),
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -82,6 +82,13 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
|||||||
FadeIn(
|
FadeIn(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => onTap(e),
|
onTap: () => onTap(e),
|
||||||
|
|
||||||
|
/// If swipe gesture is enabled on home screen, use
|
||||||
|
/// long press instead of slide action to trigger
|
||||||
|
/// the action menu.
|
||||||
|
onLongPress: swipeGestureEnabled
|
||||||
|
? () => onMoreTapped?.call(e, context.rect)
|
||||||
|
: null,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: Dimens.pt8,
|
top: Dimens.pt8,
|
||||||
|
@ -115,37 +115,74 @@ 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(
|
||||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
|
||||||
cacheKey: imageUri,
|
|
||||||
errorWidget: (_, __, ___) {
|
|
||||||
if (url.isEmpty) {
|
|
||||||
return FadeIn(
|
|
||||||
child: Center(
|
child: Center(
|
||||||
child: _HackerNewsImage(
|
child: _HackerNewsImage(
|
||||||
height: layoutHeight,
|
height: layoutHeight,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}
|
: () {
|
||||||
return Center(
|
if (imageUri?.isNotEmpty ?? false) {
|
||||||
child: CachedNetworkImage(
|
return CachedNetworkImage(
|
||||||
imageUrl: Constants.favicon(url),
|
imageUrl: imageUri!,
|
||||||
fit: BoxFit.scaleDown,
|
fit:
|
||||||
cacheKey: iconUri,
|
isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||||
errorWidget: (_, __, ___) {
|
cacheKey: imageUri,
|
||||||
return const FadeIn(
|
errorWidget: (_, __, ___) {
|
||||||
child: Icon(
|
if (url.isEmpty) {
|
||||||
Icons.public,
|
return FadeIn(
|
||||||
size: Dimens.pt20,
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (url.isNotEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: Constants.favicon(url),
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
cacheKey: iconUri,
|
||||||
|
errorWidget: (_, __, ___) {
|
||||||
|
return const FadeIn(
|
||||||
|
child: Icon(
|
||||||
|
Icons.public,
|
||||||
|
size: Dimens.pt20,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
} else {
|
||||||
),
|
return FadeIn(
|
||||||
);
|
child: Center(
|
||||||
},
|
child: _HackerNewsImage(
|
||||||
),
|
height: layoutHeight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -128,7 +128,7 @@ class StoryTile extends StatelessWidget {
|
|||||||
excludeSemantics: true,
|
excludeSemantics: true,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: () {
|
onDoubleTap: () {
|
||||||
if (story.url.isNotEmpty) {
|
if (story.url.isNotEmpty) {
|
||||||
LinkUtil.launch(
|
LinkUtil.launch(
|
||||||
story.url,
|
story.url,
|
||||||
|
@ -13,6 +13,7 @@ abstract class Dimens {
|
|||||||
static const double pt18 = 18;
|
static const double pt18 = 18;
|
||||||
static const double pt20 = 20;
|
static const double pt20 = 20;
|
||||||
static const double pt24 = 24;
|
static const double pt24 = 24;
|
||||||
|
static const double pt30 = 30;
|
||||||
static const double pt36 = 36;
|
static const double pt36 = 36;
|
||||||
static const double pt40 = 40;
|
static const double pt40 = 40;
|
||||||
static const double pt48 = 48;
|
static const double pt48 = 48;
|
||||||
|
14
pubspec.lock
14
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:
|
||||||
@ -1421,10 +1429,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
|
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.4"
|
version: "14.2.5"
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
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.0"
|
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.0+148
|
version: 2.9.3+151
|
||||||
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.0"
|
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: 80c2e84975...2663184aa7
Reference in New Issue
Block a user