mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
f39408fbcc | |||
ca2f063297 | |||
1ad231adbb | |||
60b09fd81e | |||
fe162208ca | |||
58139ba7a3 | |||
33a31acbe2 | |||
0fcfcbb7e3 | |||
a98f52c90b | |||
8e8e48c44a | |||
603b7cc939 | |||
649fa33df3 | |||
81d4a0f2df | |||
24112a471e | |||
c7824eaef3 | |||
c2b66d29c3 | |||
e0a53e44b2 | |||
4cf8379db0 | |||
c1c26bf0e0 | |||
29e2f4163d | |||
c3de80015d | |||
436cd9ce8b | |||
efb326be68 | |||
047903fe24 |
18
.github/workflows/commit_check.yml
vendored
18
.github/workflows/commit_check.yml
vendored
@ -11,15 +11,13 @@ jobs:
|
||||
name: Check commit
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
FLUTTER_VERSION: "3.7.0"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: subosito/flutter-action@v2
|
||||
- name: checkout all the submodules
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
flutter-version: '3.7.0'
|
||||
channel: 'stable'
|
||||
- run: flutter pub get
|
||||
- run: flutter format --set-exit-if-changed .
|
||||
- run: flutter analyze
|
||||
- run: flutter test
|
||||
submodules: recursive
|
||||
- run: submodules/flutter/bin/flutter doctor
|
||||
- run: submodules/flutter/bin/flutter pub get
|
||||
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
|
||||
- run: submodules/flutter/bin/flutter analyze lib test integration_test
|
||||
- run: submodules/flutter/bin/flutter test
|
23
.github/workflows/publish_ios.yml
vendored
23
.github/workflows/publish_ios.yml
vendored
@ -20,21 +20,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out from git
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- run: submodules/flutter/bin/flutter doctor
|
||||
- run: submodules/flutter/bin/flutter pub get
|
||||
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
|
||||
- run: submodules/flutter/bin/flutter analyze lib test integration_test
|
||||
- run: submodules/flutter/bin/flutter test
|
||||
|
||||
# Configure ruby according to our .ruby-version
|
||||
- name: Setup ruby & Bundler
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true
|
||||
# Set up flutter (feel free to adjust the version below)
|
||||
- name: Setup flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
cache: true
|
||||
flutter-version: 3.7.0
|
||||
- run: flutter pub get
|
||||
- run: flutter format --set-exit-if-changed .
|
||||
- run: flutter analyze
|
||||
|
||||
# Start an ssh-agent that will provide the SSH key from the
|
||||
# SSH_PRIVATE_KEY secret to `fastlane match`
|
||||
- name: Setup SSH key
|
||||
@ -43,8 +43,7 @@ jobs:
|
||||
run: |
|
||||
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
|
||||
ssh-add - <<< "${{ secrets.SSH_PRIVATE_KEY }}"
|
||||
- name: Download dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build & Publish to TestFlight with Fastlane
|
||||
env:
|
||||
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
|
||||
|
3
fastlane/metadata/android/en-US/changelogs/84.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/84.txt
Normal file
@ -0,0 +1,3 @@
|
||||
- Customization of tab bar.
|
||||
- Option to enable swipe gesture for switching between tabs.
|
||||
- Access to action menu from home screen.
|
@ -137,7 +137,7 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
|
||||
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
|
||||
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f
|
||||
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6
|
||||
|
@ -49,7 +49,7 @@ latest_testflight_build_number
|
||||
|
||||
# Prep the xcodeproject from Flutter without building (`--config-only`)
|
||||
sh(
|
||||
"flutter", "build", "ios", "--config-only",
|
||||
"/Users/runner/work/Hacki/Hacki/submodules/flutter/bin/flutter", "build", "ios", "--config-only",
|
||||
"--release", "--no-pub", "--no-codesign",
|
||||
"--build-number", new_build_number.to_s
|
||||
)
|
||||
|
@ -41,8 +41,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
await _authRepository.loggedIn.then((bool loggedIn) async {
|
||||
if (loggedIn) {
|
||||
final String? username = await _authRepository.username;
|
||||
final User user =
|
||||
await _storiesRepository.fetchUserBy(userId: username!);
|
||||
final User user = await _storiesRepository.fetchUser(id: username!);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -84,8 +83,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
);
|
||||
|
||||
if (successful) {
|
||||
final User user =
|
||||
await _storiesRepository.fetchUserBy(userId: event.username);
|
||||
final User user = await _storiesRepository.fetchUser(id: event.username);
|
||||
emit(
|
||||
state.copyWith(
|
||||
user: user,
|
||||
|
@ -56,15 +56,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
static const int _tabletSmallPageSize = 15;
|
||||
static const int _tabletLargePageSize = 25;
|
||||
|
||||
/// Types of story to be shown in the tab bar.
|
||||
static const Set<StoryType> types = <StoryType>{
|
||||
StoryType.top,
|
||||
StoryType.best,
|
||||
StoryType.latest,
|
||||
StoryType.ask,
|
||||
StoryType.show,
|
||||
};
|
||||
|
||||
Future<void> onInitialize(
|
||||
StoriesInitialize event,
|
||||
Emitter<StoriesState> emit,
|
||||
@ -72,7 +63,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
_streamSubscription ??=
|
||||
_preferenceCubit.stream.listen((PreferenceState event) {
|
||||
final bool isComplexTile = event.complexStoryTileEnabled;
|
||||
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
|
||||
final int pageSize = getPageSize(isComplexTile: isComplexTile);
|
||||
|
||||
if (pageSize != state.currentPageSize) {
|
||||
add(StoriesPageSizeChanged(pageSize: pageSize));
|
||||
@ -80,54 +71,57 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
});
|
||||
final bool hasCachedStories = await _offlineRepository.hasCachedStories;
|
||||
final bool isComplexTile = _preferenceCubit.state.complexStoryTileEnabled;
|
||||
final int pageSize = _getPageSize(isComplexTile: isComplexTile);
|
||||
final int pageSize = getPageSize(isComplexTile: isComplexTile);
|
||||
emit(
|
||||
const StoriesState.init().copyWith(
|
||||
offlineReading: hasCachedStories,
|
||||
offlineReading: hasCachedStories &&
|
||||
// Only go into offline mode in the next session.
|
||||
state.downloadStatus == StoriesDownloadStatus.initial,
|
||||
currentPageSize: pageSize,
|
||||
downloadStatus: state.downloadStatus,
|
||||
storiesDownloaded: state.storiesDownloaded,
|
||||
storiesToBeDownloaded: state.storiesToBeDownloaded,
|
||||
),
|
||||
);
|
||||
for (final StoryType type in types) {
|
||||
await loadStories(of: type, emit: emit);
|
||||
for (final StoryType type in StoryType.values) {
|
||||
await loadStories(type: type, emit: emit);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadStories({
|
||||
required StoryType of,
|
||||
required StoryType type,
|
||||
required Emitter<StoriesState> emit,
|
||||
}) async {
|
||||
if (state.offlineReading) {
|
||||
final List<int> ids = await _offlineRepository.getCachedStoryIds(of: of);
|
||||
final List<int> ids =
|
||||
await _offlineRepository.getCachedStoryIds(type: type);
|
||||
emit(
|
||||
state
|
||||
.copyWithStoryIdsUpdated(of: of, to: ids)
|
||||
.copyWithCurrentPageUpdated(of: of, to: 0),
|
||||
.copyWithStoryIdsUpdated(type: type, to: ids)
|
||||
.copyWithCurrentPageUpdated(type: type, to: 0),
|
||||
);
|
||||
_offlineRepository
|
||||
.getCachedStoriesStream(
|
||||
ids: ids.sublist(0, min(ids.length, state.currentPageSize)),
|
||||
)
|
||||
.listen((Story story) {
|
||||
add(StoryLoaded(story: story, type: of));
|
||||
add(StoryLoaded(story: story, type: type));
|
||||
}).onDone(() {
|
||||
add(StoriesLoaded(type: of));
|
||||
add(StoriesLoaded(type: type));
|
||||
});
|
||||
} else {
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(of: of);
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
|
||||
emit(
|
||||
state
|
||||
.copyWithStoryIdsUpdated(of: of, to: ids)
|
||||
.copyWithCurrentPageUpdated(of: of, to: 0),
|
||||
.copyWithStoryIdsUpdated(type: type, to: ids)
|
||||
.copyWithCurrentPageUpdated(type: type, to: 0),
|
||||
);
|
||||
_storiesRepository
|
||||
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize))
|
||||
.listen((Story story) {
|
||||
add(StoryLoaded(story: story, type: of));
|
||||
add(StoryLoaded(story: story, type: type));
|
||||
}).onDone(() {
|
||||
add(StoriesLoaded(type: of));
|
||||
add(StoriesLoaded(type: type));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -138,7 +132,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
) async {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
of: event.type,
|
||||
type: event.type,
|
||||
to: StoriesStatus.loading,
|
||||
),
|
||||
);
|
||||
@ -146,27 +140,29 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
if (state.offlineReading) {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
of: event.type,
|
||||
type: event.type,
|
||||
to: StoriesStatus.loaded,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWithRefreshed(of: event.type));
|
||||
await loadStories(of: event.type, emit: emit);
|
||||
emit(state.copyWithRefreshed(type: event.type));
|
||||
await loadStories(type: event.type, emit: emit);
|
||||
}
|
||||
}
|
||||
|
||||
void onLoadMore(StoriesLoadMore event, Emitter<StoriesState> emit) {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
of: event.type,
|
||||
type: event.type,
|
||||
to: StoriesStatus.loading,
|
||||
),
|
||||
);
|
||||
|
||||
final int currentPage = state.currentPageByType[event.type]!;
|
||||
final int len = state.storyIdsByType[event.type]!.length;
|
||||
emit(state.copyWithCurrentPageUpdated(of: event.type, to: currentPage + 1));
|
||||
emit(
|
||||
state.copyWithCurrentPageUpdated(type: event.type, to: currentPage + 1),
|
||||
);
|
||||
final int currentPageSize = state.currentPageSize;
|
||||
final int lower = currentPageSize * (currentPage + 1);
|
||||
int upper = currentPageSize + lower;
|
||||
@ -216,7 +212,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
} else {
|
||||
emit(
|
||||
state.copyWithStatusUpdated(
|
||||
of: event.type,
|
||||
type: event.type,
|
||||
to: StoriesStatus.loaded,
|
||||
),
|
||||
);
|
||||
@ -230,7 +226,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
final bool hasRead = await _preferenceRepository.hasRead(event.story.id);
|
||||
emit(
|
||||
state.copyWithStoryAdded(
|
||||
of: event.type,
|
||||
type: event.type,
|
||||
story: event.story,
|
||||
hasRead: hasRead,
|
||||
),
|
||||
@ -238,7 +234,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
}
|
||||
|
||||
void onStoriesLoaded(StoriesLoaded event, Emitter<StoriesState> emit) {
|
||||
emit(state.copyWithStatusUpdated(of: event.type, to: StoriesStatus.loaded));
|
||||
emit(
|
||||
state.copyWithStatusUpdated(type: event.type, to: StoriesStatus.loaded),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onDownload(
|
||||
@ -256,12 +254,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _offlineRepository.deleteAllComments();
|
||||
|
||||
final Set<int> prioritizedIds = <int>{};
|
||||
final List<StoryType> prioritizedTypes = <StoryType>[...types]
|
||||
|
||||
/// Prioritizing all types of stories except StoryType.latest since
|
||||
/// new stories tend to have less or no comment at all.
|
||||
final List<StoryType> prioritizedTypes = <StoryType>[...StoryType.values]
|
||||
..remove(StoryType.latest);
|
||||
|
||||
for (final StoryType type in prioritizedTypes) {
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(of: type);
|
||||
await _offlineRepository.cacheStoryIds(of: type, ids: ids);
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(type: type);
|
||||
await _offlineRepository.cacheStoryIds(type: type, ids: ids);
|
||||
prioritizedIds.addAll(ids);
|
||||
}
|
||||
|
||||
@ -281,9 +282,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
|
||||
final Set<int> latestIds = <int>{};
|
||||
final List<int> ids = await _storiesRepository.fetchStoryIds(
|
||||
of: StoryType.latest,
|
||||
type: StoryType.latest,
|
||||
);
|
||||
await _offlineRepository.cacheStoryIds(of: StoryType.latest, ids: ids);
|
||||
await _offlineRepository.cacheStoryIds(type: StoryType.latest, ids: ids);
|
||||
latestIds.addAll(ids);
|
||||
|
||||
await fetchAndCacheStories(
|
||||
@ -309,10 +310,6 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
downloadStatus: StoriesDownloadStatus.canceled,
|
||||
),
|
||||
);
|
||||
|
||||
await _offlineRepository.deleteAllStoryIds();
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
}
|
||||
|
||||
Future<void> fetchAndCacheStories(
|
||||
@ -320,11 +317,25 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
required bool includingWebPage,
|
||||
required bool isPrioritized,
|
||||
}) async {
|
||||
final List<StreamSubscription<Comment>> downloadStreams =
|
||||
<StreamSubscription<Comment>>[];
|
||||
for (final int id in ids) {
|
||||
if (state.downloadStatus == StoriesDownloadStatus.canceled) break;
|
||||
if (state.downloadStatus == StoriesDownloadStatus.canceled) {
|
||||
_logger.d('aborting downloading');
|
||||
|
||||
for (final StreamSubscription<Comment> stream in downloadStreams) {
|
||||
await stream.cancel();
|
||||
}
|
||||
|
||||
_logger.d('deleting downloaded contents');
|
||||
await _offlineRepository.deleteAllStoryIds();
|
||||
await _offlineRepository.deleteAllStories();
|
||||
await _offlineRepository.deleteAllComments();
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.d('fetching story $id');
|
||||
final Story? story = await _storiesRepository.fetchStoryBy(id);
|
||||
final Story? story = await _storiesRepository.fetchStory(id: id);
|
||||
|
||||
if (story == null) {
|
||||
if (isPrioritized) {
|
||||
@ -347,17 +358,37 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
await _offlineRepository.cacheUrl(url: story.url);
|
||||
}
|
||||
|
||||
_storiesRepository
|
||||
/// Not awaiting the completion of comments stream because otherwise
|
||||
/// it's going to take forever to finish downloading all the stories
|
||||
/// since we need to make a single http call for each comment.
|
||||
///
|
||||
/// In other words, we are prioritizing the story itself instead of
|
||||
/// the comments in the story.
|
||||
late final StreamSubscription<Comment>? downloadStream;
|
||||
downloadStream = _storiesRepository
|
||||
.fetchAllChildrenComments(ids: story.kids)
|
||||
.whereType<Comment>()
|
||||
.listen(
|
||||
(Comment comment) {
|
||||
if (state.downloadStatus == StoriesDownloadStatus.canceled) {
|
||||
_logger.d('aborting downloading from comments stream');
|
||||
downloadStream?.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.d('fetched comment ${comment.id}');
|
||||
unawaited(
|
||||
_offlineRepository.cacheComment(comment: comment),
|
||||
);
|
||||
},
|
||||
).onDone(() => add(StoryDownloaded(skipped: false)));
|
||||
)..onDone(() {
|
||||
_logger.d(
|
||||
'''finished downloading story ${story.id} with ${story.descendants} comments''',
|
||||
);
|
||||
add(StoryDownloaded(skipped: false));
|
||||
});
|
||||
|
||||
downloadStreams.add(downloadStream);
|
||||
}
|
||||
}
|
||||
|
||||
@ -441,7 +472,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
|
||||
bool hasRead(Story story) => state.readStoriesIds.contains(story.id);
|
||||
|
||||
int _getPageSize({required bool isComplexTile}) {
|
||||
int getPageSize({required bool isComplexTile}) {
|
||||
int pageSize = isComplexTile ? _smallPageSize : _largePageSize;
|
||||
|
||||
if (deviceScreenType != DeviceScreenType.mobile) {
|
||||
|
@ -103,13 +103,13 @@ class StoriesState extends Equatable {
|
||||
}
|
||||
|
||||
StoriesState copyWithStoryAdded({
|
||||
required StoryType of,
|
||||
required StoryType type,
|
||||
required Story story,
|
||||
required bool hasRead,
|
||||
}) {
|
||||
final Map<StoryType, List<Story>> newMap =
|
||||
Map<StoryType, List<Story>>.from(storiesByType);
|
||||
newMap[of] = List<Story>.from(newMap[of]!)..add(story);
|
||||
newMap[type] = List<Story>.from(newMap[type]!)..add(story);
|
||||
return copyWith(
|
||||
storiesByType: newMap,
|
||||
readStoriesIds: <int>{
|
||||
@ -120,54 +120,54 @@ class StoriesState extends Equatable {
|
||||
}
|
||||
|
||||
StoriesState copyWithStoryIdsUpdated({
|
||||
required StoryType of,
|
||||
required StoryType type,
|
||||
required List<int> to,
|
||||
}) {
|
||||
final Map<StoryType, List<int>> newMap =
|
||||
Map<StoryType, List<int>>.from(storyIdsByType);
|
||||
newMap[of] = to;
|
||||
newMap[type] = to;
|
||||
return copyWith(
|
||||
storyIdsByType: newMap,
|
||||
);
|
||||
}
|
||||
|
||||
StoriesState copyWithStatusUpdated({
|
||||
required StoryType of,
|
||||
required StoryType type,
|
||||
required StoriesStatus to,
|
||||
}) {
|
||||
final Map<StoryType, StoriesStatus> newMap =
|
||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||
newMap[of] = to;
|
||||
newMap[type] = to;
|
||||
return copyWith(
|
||||
statusByType: newMap,
|
||||
);
|
||||
}
|
||||
|
||||
StoriesState copyWithCurrentPageUpdated({
|
||||
required StoryType of,
|
||||
required StoryType type,
|
||||
required int to,
|
||||
}) {
|
||||
final Map<StoryType, int> newMap =
|
||||
Map<StoryType, int>.from(currentPageByType);
|
||||
newMap[of] = to;
|
||||
newMap[type] = to;
|
||||
return copyWith(
|
||||
currentPageByType: newMap,
|
||||
);
|
||||
}
|
||||
|
||||
StoriesState copyWithRefreshed({required StoryType of}) {
|
||||
StoriesState copyWithRefreshed({required StoryType type}) {
|
||||
final Map<StoryType, List<Story>> newStoriesMap =
|
||||
Map<StoryType, List<Story>>.from(storiesByType);
|
||||
newStoriesMap[of] = <Story>[];
|
||||
newStoriesMap[type] = <Story>[];
|
||||
final Map<StoryType, List<int>> newStoryIdsMap =
|
||||
Map<StoryType, List<int>>.from(storyIdsByType);
|
||||
newStoryIdsMap[of] = <int>[];
|
||||
newStoryIdsMap[type] = <int>[];
|
||||
final Map<StoryType, StoriesStatus> newStatusMap =
|
||||
Map<StoryType, StoriesStatus>.from(statusByType);
|
||||
newStatusMap[of] = StoriesStatus.loading;
|
||||
newStatusMap[type] = StoriesStatus.loading;
|
||||
final Map<StoryType, int> newCurrentPageMap =
|
||||
Map<StoryType, int>.from(currentPageByType);
|
||||
newCurrentPageMap[of] = 0;
|
||||
newCurrentPageMap[type] = 0;
|
||||
return copyWith(
|
||||
storiesByType: newStoriesMap,
|
||||
storyIdsByType: newStoryIdsMap,
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
|
||||
abstract class Constants {
|
||||
static const String endUserAgreementLink =
|
||||
'https://www.termsfeed.com/live/c1417f5c-a48b-4bd7-93b2-9cd4577bfc45';
|
||||
@ -34,16 +36,16 @@ abstract class Constants {
|
||||
static const String featureLogIn = 'log_in';
|
||||
static const String featurePinToTop = 'pin_to_top';
|
||||
|
||||
static const List<String> happyFaces = <String>[
|
||||
static final String happyFace = <String>[
|
||||
'(๑•̀ㅂ•́)و✧',
|
||||
'( ͡• ͜ʖ ͡•)',
|
||||
'( ͡~ ͜ʖ ͡°)',
|
||||
'٩(˘◡˘)۶',
|
||||
'(─‿‿─)',
|
||||
'(¬‿¬)',
|
||||
];
|
||||
].pickRandomly()!;
|
||||
|
||||
static const List<String> sadFaces = <String>[
|
||||
static final String sadFace = <String>[
|
||||
'ಥ_ಥ',
|
||||
'(╯°□°)╯︵ ┻━┻',
|
||||
r'¯\_(ツ)_/¯',
|
||||
@ -53,10 +55,12 @@ abstract class Constants {
|
||||
'(ㆆ_ㆆ)',
|
||||
'ʕ•́ᴥ•̀ʔっ',
|
||||
'(ㆆ_ㆆ)',
|
||||
];
|
||||
].pickRandomly()!;
|
||||
|
||||
static final String errorMessage = 'Something went wrong...$sadFace';
|
||||
}
|
||||
|
||||
abstract class RegExpConstants {
|
||||
static const String linkSuffix = r'(\)|])(.)*$';
|
||||
static const String linkSuffix = r'(\)|]|,|\*)(.)*$';
|
||||
static const String number = '[0-9]+';
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
|
||||
/// Custom router.
|
||||
@ -39,8 +40,8 @@ class CustomRouter {
|
||||
appBar: AppBar(
|
||||
title: const Text('Error'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('Something went wrong!'),
|
||||
body: Center(
|
||||
child: Text(Constants.errorMessage),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -20,7 +20,7 @@ Future<void> setUpLocator() async {
|
||||
Logger(
|
||||
filter: CustomLogFilter(),
|
||||
printer: LogUtil.logPrinter,
|
||||
output: LogUtil.getLogOutput(logOutputFile),
|
||||
output: LogUtil.logOutput(logOutputFile),
|
||||
),
|
||||
)
|
||||
..registerSingleton<StoriesRepository>(StoriesRepository())
|
||||
|
@ -106,7 +106,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
final Item item = state.item;
|
||||
final Item updatedItem = state.offlineReading
|
||||
? item
|
||||
: await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
||||
: await _storiesRepository.fetchItem(id: item.id) ?? item;
|
||||
final List<int> kids = sortKids(updatedItem.kids);
|
||||
|
||||
emit(state.copyWith(item: updatedItem));
|
||||
@ -173,7 +173,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
|
||||
final Item item = state.item;
|
||||
final Item updatedItem =
|
||||
await _storiesRepository.fetchItemBy(id: item.id) ?? item;
|
||||
await _storiesRepository.fetchItem(id: item.id) ?? item;
|
||||
final List<int> kids = sortKids(updatedItem.kids);
|
||||
|
||||
if (state.fetchMode == FetchMode.lazy) {
|
||||
|
@ -73,7 +73,7 @@ class FavCubit extends Cubit<FavState> {
|
||||
),
|
||||
);
|
||||
|
||||
final Item? item = await _storiesRepository.fetchItemBy(id: id);
|
||||
final Item? item = await _storiesRepository.fetchItem(id: id);
|
||||
|
||||
if (item == null) return;
|
||||
|
||||
|
@ -28,7 +28,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
final String username = authState.username;
|
||||
|
||||
_storiesRepository
|
||||
.fetchSubmitted(of: username)
|
||||
.fetchSubmitted(userId: username)
|
||||
.then((List<int>? submittedIds) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -94,7 +94,7 @@ class HistoryCubit extends Cubit<HistoryState> {
|
||||
);
|
||||
|
||||
_storiesRepository
|
||||
.fetchSubmitted(of: username)
|
||||
.fetchSubmitted(userId: username)
|
||||
.then((List<int>? submittedIds) {
|
||||
emit(state.copyWith(submittedIds: submittedIds));
|
||||
if (submittedIds != null) {
|
||||
|
@ -81,7 +81,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
for (final int id in commentsToBeLoaded) {
|
||||
Comment? comment = await _sembastRepository.getComment(id: id);
|
||||
comment ??= await _storiesRepository.fetchCommentBy(id: id);
|
||||
comment ??= await _storiesRepository.fetchComment(id: id);
|
||||
if (comment != null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -159,7 +159,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
for (final int id in commentsToBeLoaded) {
|
||||
Comment? comment = await _sembastRepository.getComment(id: id);
|
||||
comment ??= await _storiesRepository.fetchCommentBy(id: id);
|
||||
comment ??= await _storiesRepository.fetchComment(id: id);
|
||||
if (comment != null) {
|
||||
emit(state.copyWith(comments: <Comment>[...state.comments, comment]));
|
||||
}
|
||||
@ -184,7 +184,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
|
||||
Future<void> _fetchReplies() {
|
||||
return _storiesRepository
|
||||
.fetchSubmitted(of: _authBloc.state.username)
|
||||
.fetchSubmitted(userId: _authBloc.state.username)
|
||||
.then((List<int>? submittedItems) async {
|
||||
if (submittedItems != null) {
|
||||
final List<int> subscribedItems = submittedItems.sublist(
|
||||
@ -193,7 +193,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
);
|
||||
|
||||
for (final int id in subscribedItems) {
|
||||
await _storiesRepository.fetchItemBy(id: id).then((Item? item) async {
|
||||
await _storiesRepository.fetchItem(id: id).then((Item? item) async {
|
||||
final List<int> kids = item?.kids ?? <int>[];
|
||||
final List<int> previousKids =
|
||||
(await _sembastRepository.kids(of: id)) ?? <int>[];
|
||||
@ -216,7 +216,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
]..sort((int lhs, int rhs) => rhs.compareTo(lhs)),
|
||||
);
|
||||
await _storiesRepository
|
||||
.fetchCommentBy(id: newCommentId)
|
||||
.fetchComment(id: newCommentId)
|
||||
.then((Comment? comment) {
|
||||
if (comment != null && !comment.dead && !comment.deleted) {
|
||||
_sembastRepository
|
||||
|
@ -33,7 +33,7 @@ class PollCubit extends Cubit<PollState> {
|
||||
|
||||
if (pollOptionsIds.isEmpty || refresh) {
|
||||
final Story? updatedStory =
|
||||
await _storiesRepository.fetchStoryBy(_story.id);
|
||||
await _storiesRepository.fetchStory(id: _story.id);
|
||||
|
||||
if (updatedStory != null) {
|
||||
pollOptionsIds = updatedStory.parts;
|
||||
|
@ -56,13 +56,6 @@ class PreferenceCubit extends Cubit<PreferenceState> {
|
||||
}
|
||||
}
|
||||
|
||||
void toggle(BooleanPreference preference) {
|
||||
final BooleanPreference updatedPreference =
|
||||
preference.copyWith(val: !preference.val) as BooleanPreference;
|
||||
emit(state.copyWithPreference(updatedPreference));
|
||||
_preferenceRepository.setBool(preference.key, !preference.val);
|
||||
}
|
||||
|
||||
void update<T>(Preference<T> preference, {required T to}) {
|
||||
final T value = to;
|
||||
final Preference<T> updatedPreference = preference.copyWith(val: value);
|
||||
|
@ -16,7 +16,7 @@ class UserCubit extends Cubit<UserState> {
|
||||
|
||||
void init({required String userId}) {
|
||||
emit(state.copyWith(status: UserStatus.loading));
|
||||
_storiesRepository.fetchUserBy(userId: userId).then((User user) {
|
||||
_storiesRepository.fetchUser(id: userId).then((User user) {
|
||||
emit(state.copyWith(user: user, status: UserStatus.loaded));
|
||||
}).onError((_, __) {
|
||||
emit(state.copyWith(status: UserStatus.failure));
|
||||
|
@ -2,6 +2,8 @@ import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
extension ContextExtension on BuildContext {
|
||||
T? tryRead<T>() {
|
||||
@ -12,6 +14,31 @@ extension ContextExtension on BuildContext {
|
||||
}
|
||||
}
|
||||
|
||||
void showSnackBar({
|
||||
required String content,
|
||||
VoidCallback? action,
|
||||
String? label,
|
||||
}) {
|
||||
ScaffoldMessenger.of(this).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Palette.deepOrange,
|
||||
content: Text(content),
|
||||
action: action != null && label != null
|
||||
? SnackBarAction(
|
||||
label: label,
|
||||
onPressed: action,
|
||||
textColor: Theme.of(this).textTheme.bodyLarge?.color,
|
||||
)
|
||||
: null,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void showErrorSnackBar() => showSnackBar(
|
||||
content: Constants.errorMessage,
|
||||
);
|
||||
|
||||
Rect? get rect {
|
||||
final RenderBox? box = findRenderObject() as RenderBox?;
|
||||
final Rect? rect =
|
||||
|
@ -21,22 +21,15 @@ extension StateExtension on State {
|
||||
VoidCallback? action,
|
||||
String? label,
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Palette.deepOrange,
|
||||
content: Text(content),
|
||||
action: action != null && label != null
|
||||
? SnackBarAction(
|
||||
label: label,
|
||||
onPressed: action,
|
||||
textColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
)
|
||||
: null,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
context.showSnackBar(
|
||||
content: content,
|
||||
action: action,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
void showErrorSnackBar() => context.showErrorSnackBar();
|
||||
|
||||
Future<void>? goToItemScreen({
|
||||
required ItemScreenArgs args,
|
||||
bool forceNewScreen = false,
|
||||
@ -70,7 +63,6 @@ extension StateExtension on State {
|
||||
return MorePopupMenu(
|
||||
item: item,
|
||||
isBlocked: isBlocked,
|
||||
showSnackBar: showSnackBar,
|
||||
onStoryLinkTapped: onStoryLinkTapped,
|
||||
onLoginTapped: onLoginTapped,
|
||||
);
|
||||
@ -103,7 +95,7 @@ extension StateExtension on State {
|
||||
if (id != null) {
|
||||
await locator
|
||||
.get<StoriesRepository>()
|
||||
.fetchItemBy(id: id)
|
||||
.fetchItem(id: id)
|
||||
.then((Item? item) {
|
||||
if (mounted) {
|
||||
if (item != null) {
|
||||
@ -119,11 +111,45 @@ extension StateExtension on State {
|
||||
}
|
||||
}
|
||||
|
||||
void onShareTapped(Item item, Rect? rect) {
|
||||
Share.share(
|
||||
'https://news.ycombinator.com/item?id=${item.id}',
|
||||
sharePositionOrigin: rect,
|
||||
);
|
||||
Future<void> onShareTapped(Item item, Rect? rect) async {
|
||||
late final String? linkToShare;
|
||||
if (item.url.isNotEmpty) {
|
||||
linkToShare = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
height: 140,
|
||||
color: Theme.of(context).canvasColor,
|
||||
child: Material(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
onTap: () => Navigator.pop(context, item.url),
|
||||
title: const Text('Link to article'),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => Navigator.pop(
|
||||
context,
|
||||
'https://news.ycombinator.com/item?id=${item.id}',
|
||||
),
|
||||
title: const Text('Link to HN'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
linkToShare = 'https://news.ycombinator.com/item?id=${item.id}';
|
||||
}
|
||||
|
||||
if (linkToShare != null) {
|
||||
await Share.share(
|
||||
linkToShare,
|
||||
sharePositionOrigin: rect,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onFlagTapped(Item item) {
|
||||
|
@ -2,6 +2,8 @@ import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:hacki/models/comment.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
|
||||
/// [BuildableComment] is a subtype of [Comment] which stores
|
||||
/// the corresponding [LinkifyElement] for faster widget building.
|
||||
class BuildableComment extends Comment {
|
||||
BuildableComment({
|
||||
required super.id,
|
||||
|
@ -20,23 +20,7 @@ class Comment extends Item {
|
||||
type: '',
|
||||
);
|
||||
|
||||
Comment.fromJson(Map<String, dynamic> json, {this.level = 0})
|
||||
: super(
|
||||
id: json['id'] as int? ?? 0,
|
||||
time: json['time'] as int? ?? 0,
|
||||
by: json['by'] as String? ?? '',
|
||||
text: json['text'] as String? ?? '',
|
||||
kids: (json['kids'] as List<dynamic>?)?.cast<int>() ?? <int>[],
|
||||
parent: json['parent'] as int? ?? 0,
|
||||
deleted: json['deleted'] as bool? ?? false,
|
||||
score: json['score'] as int? ?? 0,
|
||||
descendants: 0,
|
||||
dead: json['dead'] as bool? ?? false,
|
||||
parts: <int>[],
|
||||
title: '',
|
||||
url: '',
|
||||
type: '',
|
||||
);
|
||||
Comment.fromJson(super.json, {this.level = 0}) : super.fromJson();
|
||||
|
||||
final int level;
|
||||
|
||||
|
@ -1,6 +1,10 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/extensions/date_time_extension.dart';
|
||||
import 'package:hacki/models/comment.dart';
|
||||
import 'package:hacki/models/poll_option.dart';
|
||||
import 'package:hacki/models/story.dart';
|
||||
|
||||
/// [Item] is the base type of [Story], [Comment] and [PollOption].
|
||||
class Item extends Equatable {
|
||||
const Item({
|
||||
required this.id,
|
||||
@ -44,11 +48,11 @@ class Item extends Equatable {
|
||||
title = json['title'] as String? ?? '',
|
||||
text = json['text'] as String? ?? '',
|
||||
url = json['url'] as String? ?? '',
|
||||
kids = <int>[],
|
||||
kids = (json['kids'] as List<dynamic>?)?.cast<int>() ?? <int>[],
|
||||
dead = json['dead'] as bool? ?? false,
|
||||
deleted = json['deleted'] as bool? ?? false,
|
||||
parent = json['parent'] as int? ?? 0,
|
||||
parts = <int>[],
|
||||
parts = (json['parts'] as List<dynamic>?)?.cast<int>() ?? <int>[],
|
||||
type = json['type'] as String? ?? '';
|
||||
|
||||
final int id;
|
||||
|
@ -9,4 +9,5 @@ export 'post_data.dart';
|
||||
export 'preference.dart';
|
||||
export 'search_params.dart';
|
||||
export 'story.dart';
|
||||
export 'story_type.dart';
|
||||
export 'user.dart';
|
||||
|
@ -24,41 +24,11 @@ class PollOption extends Item {
|
||||
|
||||
PollOption.empty()
|
||||
: ratio = 0,
|
||||
super(
|
||||
id: 0,
|
||||
score: 0,
|
||||
descendants: 0,
|
||||
time: 0,
|
||||
by: '',
|
||||
title: '',
|
||||
url: '',
|
||||
kids: <int>[],
|
||||
dead: false,
|
||||
parts: <int>[],
|
||||
deleted: false,
|
||||
parent: 0,
|
||||
text: '',
|
||||
type: '',
|
||||
);
|
||||
super.empty();
|
||||
|
||||
PollOption.fromJson(Map<String, dynamic> json)
|
||||
PollOption.fromJson(super.json)
|
||||
: ratio = 0,
|
||||
super(
|
||||
descendants: 0,
|
||||
id: json['id'] as int? ?? 0,
|
||||
score: json['score'] as int? ?? 0,
|
||||
time: json['time'] as int? ?? 0,
|
||||
by: json['by'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
url: json['url'] as String? ?? '',
|
||||
kids: <int>[],
|
||||
text: json['text'] as String? ?? '',
|
||||
dead: json['dead'] as bool? ?? false,
|
||||
deleted: json['deleted'] as bool? ?? false,
|
||||
type: json['type'] as String? ?? '',
|
||||
parts: <int>[],
|
||||
parent: 0,
|
||||
);
|
||||
super.fromJson();
|
||||
|
||||
final double ratio;
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
@ -13,26 +14,29 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
||||
|
||||
Preference<T> copyWith({required T? val});
|
||||
|
||||
static List<Preference<dynamic>> allPreferences = <Preference<dynamic>>[
|
||||
// Order of these first three preferences does not matter.
|
||||
FetchModePreference(),
|
||||
CommentsOrderPreference(),
|
||||
FontSizePreference(),
|
||||
TabOrderPreference(),
|
||||
// Order of items below matters and
|
||||
// reflects the order on settings screen.
|
||||
const DisplayModePreference(),
|
||||
const MetadataModePreference(),
|
||||
const StoryUrlModePreference(),
|
||||
const NotificationModePreference(),
|
||||
const SwipeGesturePreference(),
|
||||
const CollapseModePreference(),
|
||||
NavigationModePreference(),
|
||||
const ReaderModePreference(),
|
||||
const MarkReadStoriesModePreference(),
|
||||
const EyeCandyModePreference(),
|
||||
const TrueDarkModePreference(),
|
||||
];
|
||||
static final List<Preference<dynamic>> allPreferences =
|
||||
UnmodifiableListView<Preference<dynamic>>(
|
||||
<Preference<dynamic>>[
|
||||
// Order of these first four preferences does not matter.
|
||||
FetchModePreference(),
|
||||
CommentsOrderPreference(),
|
||||
FontSizePreference(),
|
||||
TabOrderPreference(),
|
||||
// Order of items below matters and
|
||||
// reflects the order on settings screen.
|
||||
const DisplayModePreference(),
|
||||
const MetadataModePreference(),
|
||||
const StoryUrlModePreference(),
|
||||
const NotificationModePreference(),
|
||||
const SwipeGesturePreference(),
|
||||
const CollapseModePreference(),
|
||||
NavigationModePreference(),
|
||||
const ReaderModePreference(),
|
||||
const MarkReadStoriesModePreference(),
|
||||
const EyeCandyModePreference(),
|
||||
const TrueDarkModePreference(),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[key];
|
||||
@ -81,7 +85,7 @@ class SwipeGesturePreference extends BooleanPreference {
|
||||
|
||||
@override
|
||||
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.''';
|
||||
}
|
||||
|
||||
class NotificationModePreference extends BooleanPreference {
|
||||
@ -118,6 +122,10 @@ class CollapseModePreference extends BooleanPreference {
|
||||
|
||||
@override
|
||||
String get title => 'Tap Anywhere to Collapse';
|
||||
|
||||
@override
|
||||
String get subtitle =>
|
||||
'''if disabled, tap on the top of comment tile to collapse.''';
|
||||
}
|
||||
|
||||
/// The value deciding whether or not the story
|
||||
|
@ -1,41 +1,6 @@
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/models/item.dart';
|
||||
|
||||
enum StoryType {
|
||||
top('topstories'),
|
||||
best('beststories'),
|
||||
latest('newstories'),
|
||||
ask('askstories'),
|
||||
show('showstories');
|
||||
|
||||
const StoryType(this.path);
|
||||
|
||||
final String path;
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case StoryType.top:
|
||||
return 'TOP';
|
||||
case StoryType.best:
|
||||
return 'BEST';
|
||||
case StoryType.latest:
|
||||
return 'NEW';
|
||||
case StoryType.ask:
|
||||
return 'ASK';
|
||||
case StoryType.show:
|
||||
return 'SHOW';
|
||||
}
|
||||
}
|
||||
|
||||
static int convertToSettingsValue(List<StoryType> tabs) {
|
||||
return int.parse(
|
||||
tabs
|
||||
.map((StoryType e) => e.index.toString())
|
||||
.reduce((String value, String element) => '$value$element'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Story extends Item {
|
||||
const Story({
|
||||
required super.descendants,
|
||||
@ -55,23 +20,7 @@ class Story extends Item {
|
||||
parent: 0,
|
||||
);
|
||||
|
||||
Story.empty()
|
||||
: super(
|
||||
id: 0,
|
||||
score: 0,
|
||||
descendants: 0,
|
||||
time: 0,
|
||||
by: '',
|
||||
title: '',
|
||||
url: '',
|
||||
kids: <int>[],
|
||||
dead: false,
|
||||
parts: <int>[],
|
||||
deleted: false,
|
||||
parent: 0,
|
||||
text: '',
|
||||
type: '',
|
||||
);
|
||||
Story.empty() : super.empty();
|
||||
|
||||
Story.placeholder()
|
||||
: super(
|
||||
@ -91,23 +40,7 @@ class Story extends Item {
|
||||
type: '',
|
||||
);
|
||||
|
||||
Story.fromJson(Map<String, dynamic> json)
|
||||
: super(
|
||||
descendants: json['descendants'] as int? ?? 0,
|
||||
id: json['id'] as int? ?? 0,
|
||||
score: json['score'] as int? ?? 0,
|
||||
time: json['time'] as int? ?? 0,
|
||||
by: json['by'] as String? ?? '',
|
||||
title: json['title'] as String? ?? '',
|
||||
url: json['url'] as String? ?? '',
|
||||
kids: (json['kids'] as List<dynamic>?)?.cast<int>() ?? <int>[],
|
||||
text: json['text'] as String? ?? '',
|
||||
dead: json['dead'] as bool? ?? false,
|
||||
deleted: json['deleted'] as bool? ?? false,
|
||||
type: json['type'] as String? ?? '',
|
||||
parts: (json['parts'] as List<dynamic>?)?.cast<int>() ?? <int>[],
|
||||
parent: 0,
|
||||
);
|
||||
Story.fromJson(super.json) : super.fromJson();
|
||||
|
||||
String get metadata =>
|
||||
'''$score point${score > 1 ? 's' : ''} by $by $postedDate | $descendants comment${descendants > 1 ? 's' : ''}''';
|
||||
|
34
lib/models/story_type.dart
Normal file
34
lib/models/story_type.dart
Normal file
@ -0,0 +1,34 @@
|
||||
enum StoryType {
|
||||
top('topstories'),
|
||||
best('beststories'),
|
||||
latest('newstories'),
|
||||
ask('askstories'),
|
||||
show('showstories');
|
||||
|
||||
const StoryType(this.path);
|
||||
|
||||
final String path;
|
||||
|
||||
String get label {
|
||||
switch (this) {
|
||||
case StoryType.top:
|
||||
return 'TOP';
|
||||
case StoryType.best:
|
||||
return 'BEST';
|
||||
case StoryType.latest:
|
||||
return 'NEW';
|
||||
case StoryType.ask:
|
||||
return 'ASK';
|
||||
case StoryType.show:
|
||||
return 'SHOW';
|
||||
}
|
||||
}
|
||||
|
||||
static int convertToSettingsValue(List<StoryType> tabs) {
|
||||
return int.parse(
|
||||
tabs
|
||||
.map((StoryType e) => e.index.toString())
|
||||
.reduce((String value, String element) => '$value$element'),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,10 +2,16 @@ import 'dart:async';
|
||||
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/post_repository.dart';
|
||||
import 'package:hacki/repositories/postable_repository.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/repositories/preference_repository.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
/// [AuthRepository] if for logging user in/out and performing actions
|
||||
/// that require a logged in user such as [flag], [favorite], [upvote],
|
||||
/// and [downvote].
|
||||
///
|
||||
/// For posting actions such as posting a comment, see [PostRepository].
|
||||
class AuthRepository extends PostableRepository {
|
||||
AuthRepository({
|
||||
super.dio,
|
||||
@ -18,8 +24,6 @@ class AuthRepository extends PostableRepository {
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final Logger _logger;
|
||||
|
||||
static const String _authority = 'news.ycombinator.com';
|
||||
|
||||
Future<bool> get loggedIn async => _preferenceRepository.loggedIn;
|
||||
|
||||
Future<String?> get username async => _preferenceRepository.username;
|
||||
@ -30,7 +34,7 @@ class AuthRepository extends PostableRepository {
|
||||
required String username,
|
||||
required String password,
|
||||
}) async {
|
||||
final Uri uri = Uri.https(_authority, 'login');
|
||||
final Uri uri = Uri.https(authority, 'login');
|
||||
final PostDataMixin data = LoginPostData(
|
||||
acct: username,
|
||||
pw: password,
|
||||
@ -64,7 +68,7 @@ class AuthRepository extends PostableRepository {
|
||||
required int id,
|
||||
required bool flag,
|
||||
}) async {
|
||||
final Uri uri = Uri.https(_authority, 'flag');
|
||||
final Uri uri = Uri.https(authority, 'flag');
|
||||
final String? username = await _preferenceRepository.username;
|
||||
final String? password = await _preferenceRepository.password;
|
||||
final PostDataMixin data = FlagPostData(
|
||||
@ -81,7 +85,7 @@ class AuthRepository extends PostableRepository {
|
||||
required int id,
|
||||
required bool favorite,
|
||||
}) async {
|
||||
final Uri uri = Uri.https(_authority, 'fave');
|
||||
final Uri uri = Uri.https(authority, 'fave');
|
||||
final String? username = await _preferenceRepository.username;
|
||||
final String? password = await _preferenceRepository.password;
|
||||
final PostDataMixin data = FavoritePostData(
|
||||
@ -98,7 +102,7 @@ class AuthRepository extends PostableRepository {
|
||||
required int id,
|
||||
required bool upvote,
|
||||
}) async {
|
||||
final Uri uri = Uri.https(_authority, 'vote');
|
||||
final Uri uri = Uri.https(authority, 'vote');
|
||||
final String? username = await _preferenceRepository.username;
|
||||
final String? password = await _preferenceRepository.password;
|
||||
final PostDataMixin data = VotePostData(
|
||||
@ -115,7 +119,7 @@ class AuthRepository extends PostableRepository {
|
||||
required int id,
|
||||
required bool downvote,
|
||||
}) async {
|
||||
final Uri uri = Uri.https(_authority, 'vote');
|
||||
final Uri uri = Uri.https(authority, 'vote');
|
||||
final String? username = await _preferenceRepository.username;
|
||||
final String? password = await _preferenceRepository.password;
|
||||
final PostDataMixin data = VotePostData(
|
||||
|
@ -4,9 +4,14 @@ import 'package:hacki/models/models.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
/// [OfflineRepository] is for storing stories and comments for offline reading.
|
||||
/// It's using [Hive] as its database which is being stored in temp directory.
|
||||
/// [OfflineRepository] is for storing [Story] and [Comment] for
|
||||
/// offline reading.
|
||||
///
|
||||
/// [Hive] is used as its database and is being stored in the temporary
|
||||
/// directory assigned by host system which you can retrieve
|
||||
/// by calling [getTemporaryDirectory].
|
||||
class OfflineRepository {
|
||||
OfflineRepository({
|
||||
Future<Box<List<int>>>? storyIdBox,
|
||||
@ -36,7 +41,7 @@ class OfflineRepository {
|
||||
_storyBox.then((Box<Map<dynamic, dynamic>> box) => box.isNotEmpty);
|
||||
|
||||
Future<void> cacheStoryIds({
|
||||
required StoryType of,
|
||||
required StoryType type,
|
||||
required List<int> ids,
|
||||
}) async {
|
||||
late final Box<List<int>> box;
|
||||
@ -49,7 +54,7 @@ class OfflineRepository {
|
||||
box = await _storyIdBox;
|
||||
}
|
||||
|
||||
return box.put(of.name, ids);
|
||||
return box.put(type.name, ids);
|
||||
}
|
||||
|
||||
Future<void> cacheStory({required Story story}) async {
|
||||
@ -103,10 +108,10 @@ class OfflineRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<int>> getCachedStoryIds({required StoryType of}) async {
|
||||
Future<List<int>> getCachedStoryIds({required StoryType type}) async {
|
||||
try {
|
||||
final Box<List<int>> box = await _storyIdBox;
|
||||
final List<int>? ids = box.get(of.name);
|
||||
final List<int>? ids = box.get(type.name);
|
||||
return ids ?? <int>[];
|
||||
} catch (_) {
|
||||
_logger.e(_);
|
||||
|
@ -7,6 +7,7 @@ import 'package:hacki/repositories/postable_repository.dart';
|
||||
import 'package:hacki/repositories/preference_repository.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
|
||||
/// [PostRepository] is for posting contents to Hacker News.
|
||||
class PostRepository extends PostableRepository {
|
||||
PostRepository({super.dio, PreferenceRepository? storageRepository})
|
||||
: _preferenceRepository =
|
||||
@ -14,15 +15,13 @@ class PostRepository extends PostableRepository {
|
||||
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
|
||||
static const String _authority = 'news.ycombinator.com';
|
||||
|
||||
Future<bool> comment({
|
||||
required int parentId,
|
||||
required String text,
|
||||
}) async {
|
||||
final String? username = await _preferenceRepository.username;
|
||||
final String? password = await _preferenceRepository.password;
|
||||
final Uri uri = Uri.https(_authority, 'comment');
|
||||
final Uri uri = Uri.https(authority, 'comment');
|
||||
|
||||
if (username == null || password == null) {
|
||||
return false;
|
||||
@ -54,7 +53,7 @@ class PostRepository extends PostableRepository {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Response<List<int>> formResponse = await _getFormResponse(
|
||||
final Response<List<int>> formResponse = await getFormResponse(
|
||||
username: username,
|
||||
password: password,
|
||||
path: 'submitlink',
|
||||
@ -69,7 +68,7 @@ class PostRepository extends PostableRepository {
|
||||
final String? cookie =
|
||||
formResponse.headers.value(HttpHeaders.setCookieHeader);
|
||||
|
||||
final Uri uri = Uri.https(_authority, 'r');
|
||||
final Uri uri = Uri.https(authority, 'r');
|
||||
final PostDataMixin data = SubmitPostData(
|
||||
fnid: formValues['fnid']!,
|
||||
fnop: formValues['fnop']!,
|
||||
@ -97,7 +96,7 @@ class PostRepository extends PostableRepository {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Response<List<int>> formResponse = await _getFormResponse(
|
||||
final Response<List<int>> formResponse = await getFormResponse(
|
||||
username: username,
|
||||
password: password,
|
||||
id: id,
|
||||
@ -113,7 +112,7 @@ class PostRepository extends PostableRepository {
|
||||
final String? cookie =
|
||||
formResponse.headers.value(HttpHeaders.setCookieHeader);
|
||||
|
||||
final Uri uri = Uri.https(_authority, 'xedit');
|
||||
final Uri uri = Uri.https(authority, 'xedit');
|
||||
final PostDataMixin data = EditPostData(
|
||||
hmac: formValues['hmac']!,
|
||||
id: id,
|
||||
@ -126,28 +125,4 @@ class PostRepository extends PostableRepository {
|
||||
cookie: cookie,
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response<List<int>>> _getFormResponse({
|
||||
required String username,
|
||||
required String password,
|
||||
required String path,
|
||||
int? id,
|
||||
}) async {
|
||||
final Uri uri = Uri.https(
|
||||
_authority,
|
||||
path,
|
||||
<String, dynamic>{if (id != null) 'id': id.toString()},
|
||||
);
|
||||
final PostDataMixin data = FormPostData(
|
||||
acct: username,
|
||||
pw: password,
|
||||
id: id,
|
||||
);
|
||||
return performPost(
|
||||
uri,
|
||||
data,
|
||||
responseType: ResponseType.bytes,
|
||||
validateStatus: (int? status) => status == HttpStatus.ok,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,23 @@ import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/auth_repository.dart';
|
||||
import 'package:hacki/repositories/post_repository.dart';
|
||||
import 'package:hacki/utils/service_exception.dart';
|
||||
|
||||
/// [PostableRepository] is solely for hosting functionalities shared between
|
||||
/// [AuthRepository] and [PostRepository].
|
||||
class PostableRepository {
|
||||
PostableRepository({
|
||||
Dio? dio,
|
||||
this.authority = 'news.ycombinator.com',
|
||||
}) : _dio = dio ?? Dio();
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
@protected
|
||||
final String authority;
|
||||
|
||||
@protected
|
||||
Future<bool> performDefaultPost(
|
||||
Uri uri,
|
||||
@ -60,4 +68,29 @@ class PostableRepository {
|
||||
throw ServiceException(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
Future<Response<List<int>>> getFormResponse({
|
||||
required String username,
|
||||
required String password,
|
||||
required String path,
|
||||
int? id,
|
||||
}) async {
|
||||
final Uri uri = Uri.https(
|
||||
authority,
|
||||
path,
|
||||
<String, dynamic>{if (id != null) 'id': id.toString()},
|
||||
);
|
||||
final PostDataMixin data = FormPostData(
|
||||
acct: username,
|
||||
pw: password,
|
||||
id: id,
|
||||
);
|
||||
return performPost(
|
||||
uri,
|
||||
data,
|
||||
responseType: ResponseType.bytes,
|
||||
validateStatus: (int? status) => status == HttpStatus.ok,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import 'package:logger/logger.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:synced_shared_preferences/synced_shared_preferences.dart';
|
||||
|
||||
/// [PreferenceRepository] is for storing user preferences.
|
||||
class PreferenceRepository {
|
||||
PreferenceRepository({
|
||||
SyncedSharedPreferences? syncedPrefs,
|
||||
|
@ -3,6 +3,9 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
|
||||
/// [SearchRepository] is for searching contents on Hacker News.
|
||||
///
|
||||
/// You can learn about the search API at https://hn.algolia.com/api.
|
||||
class SearchRepository {
|
||||
SearchRepository({Dio? dio}) : _dio = dio ?? Dio();
|
||||
|
||||
|
@ -7,7 +7,10 @@ import 'package:sembast/sembast.dart';
|
||||
import 'package:sembast/sembast_io.dart';
|
||||
|
||||
/// [SembastRepository] is for storing stories and comments for faster loading.
|
||||
/// It's using Sembast as its database which is being stored in doc directory.
|
||||
///
|
||||
/// Sembast [Database] is used as its database and is being stored in the
|
||||
/// documents directory assigned by host system which you can retrieve
|
||||
/// by calling [getApplicationDocumentsDirectory].
|
||||
class SembastRepository {
|
||||
SembastRepository({Database? database}) {
|
||||
if (database == null) {
|
||||
|
@ -4,6 +4,11 @@ import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
/// [StoriesRepository] is for fetching
|
||||
/// [Item] such as [Story], [PollOption], [Comment] or [User].
|
||||
///
|
||||
/// You can learn more about the Hacker News API at
|
||||
/// https://github.com/HackerNews/API.
|
||||
class StoriesRepository {
|
||||
StoriesRepository({
|
||||
FirebaseClient? firebaseClient,
|
||||
@ -12,9 +17,11 @@ class StoriesRepository {
|
||||
final FirebaseClient _firebaseClient;
|
||||
static const String _baseUrl = 'https://hacker-news.firebaseio.com/v0/';
|
||||
|
||||
Future<User> fetchUserBy({required String userId}) async {
|
||||
/// Fetch a [User] by its [id].
|
||||
/// Hacker News uses user's username as [id].
|
||||
Future<User> fetchUser({required String id}) async {
|
||||
final User user = await _firebaseClient
|
||||
.get('${_baseUrl}user/$userId.json')
|
||||
.get('${_baseUrl}user/$id.json')
|
||||
.then((dynamic val) {
|
||||
final Map<String, dynamic> json = val as Map<String, dynamic>;
|
||||
final User user = User.fromJson(json);
|
||||
@ -24,9 +31,10 @@ class StoriesRepository {
|
||||
return user;
|
||||
}
|
||||
|
||||
Future<List<int>> fetchStoryIds({required StoryType of}) async {
|
||||
/// Fetch ids of stories of a certain [StoryType].
|
||||
Future<List<int>> fetchStoryIds({required StoryType type}) async {
|
||||
final List<int> ids = await _firebaseClient
|
||||
.get('$_baseUrl${of.path}.json')
|
||||
.get('$_baseUrl${type.path}.json')
|
||||
.then((dynamic val) {
|
||||
final List<int> ids = (val as List<dynamic>).cast<int>();
|
||||
return ids;
|
||||
@ -35,7 +43,8 @@ class StoriesRepository {
|
||||
return ids;
|
||||
}
|
||||
|
||||
Future<Story?> fetchStoryBy(int id) async {
|
||||
/// Fetch a [Story] based on its id.
|
||||
Future<Story?> fetchStory({required int id}) async {
|
||||
final Story? story = await _firebaseClient
|
||||
.get('${_baseUrl}item/$id.json')
|
||||
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
|
||||
@ -48,6 +57,8 @@ class StoriesRepository {
|
||||
return story;
|
||||
}
|
||||
|
||||
/// Fetch a list of [Comment] based on ids and return results
|
||||
/// using a stream.
|
||||
Stream<Comment> fetchCommentsStream({
|
||||
required List<int> ids,
|
||||
int level = 0,
|
||||
@ -73,6 +84,8 @@ class StoriesRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
/// Fetch a list of [Comment] based on ids recursively and
|
||||
/// return results using a stream.
|
||||
Stream<Comment> fetchAllCommentsRecursivelyStream({
|
||||
required List<int> ids,
|
||||
int level = 0,
|
||||
@ -104,6 +117,8 @@ class StoriesRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
/// Fetch a list of [Item] based on ids and return results
|
||||
/// using a stream.
|
||||
Stream<Item> fetchItemsStream({required List<int> ids}) async* {
|
||||
for (final int id in ids) {
|
||||
final Item? item = await _firebaseClient
|
||||
@ -129,6 +144,8 @@ class StoriesRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a list of [Story] based on ids and return results
|
||||
/// using a stream.
|
||||
Stream<Story> fetchStoriesStream({required List<int> ids}) async* {
|
||||
for (final int id in ids) {
|
||||
final Story? story = await _firebaseClient
|
||||
@ -146,6 +163,8 @@ class StoriesRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a list of [PollOption] based on ids and return results
|
||||
/// using a stream.
|
||||
Stream<PollOption> fetchPollOptionsStream({required List<int> ids}) async* {
|
||||
for (final int id in ids) {
|
||||
final PollOption? option = await _firebaseClient
|
||||
@ -163,7 +182,8 @@ class StoriesRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Comment?> fetchCommentBy({required int id}) async {
|
||||
/// Fetch a [Comment] based on its id.
|
||||
Future<Comment?> fetchComment({required int id}) async {
|
||||
final Comment? comment = await _firebaseClient
|
||||
.get('${_baseUrl}item/$id.json')
|
||||
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
|
||||
@ -177,7 +197,10 @@ class StoriesRepository {
|
||||
return comment;
|
||||
}
|
||||
|
||||
Future<Comment?> fetchRawCommentBy({required int id}) async {
|
||||
/// Fetch a raw [Comment] based on its id.
|
||||
/// The content of [Comment] will not be parsed, use this function only if
|
||||
/// the format of content doesn't matter, otherwise, use [fetchComment].
|
||||
Future<Comment?> fetchRawComment({required int id}) async {
|
||||
final Comment? comment = await _firebaseClient
|
||||
.get('${_baseUrl}item/$id.json')
|
||||
.then((dynamic val) async {
|
||||
@ -191,7 +214,8 @@ class StoriesRepository {
|
||||
return comment;
|
||||
}
|
||||
|
||||
Future<Item?> fetchItemBy({required int id}) async {
|
||||
/// Fetch a [Item] based on its id.
|
||||
Future<Item?> fetchItem({required int id}) async {
|
||||
final Item? item = await _firebaseClient
|
||||
.get('${_baseUrl}item/$id.json')
|
||||
.then((dynamic json) => _parseJson(json as Map<String, dynamic>?))
|
||||
@ -212,7 +236,10 @@ class StoriesRepository {
|
||||
return item;
|
||||
}
|
||||
|
||||
Future<Item?> fetchRawItemBy({required int id}) async {
|
||||
/// Fetch a raw [Item] based on its id.
|
||||
/// The content of [Item] will not be parsed, use this function only if
|
||||
/// the format of content doesn't matter, otherwise, use [fetchItem].
|
||||
Future<Item?> fetchRawItem({required int id}) async {
|
||||
final Item? item = await _firebaseClient
|
||||
.get('${_baseUrl}item/$id.json')
|
||||
.then((dynamic val) {
|
||||
@ -234,9 +261,10 @@ class StoriesRepository {
|
||||
return item;
|
||||
}
|
||||
|
||||
Future<List<int>?> fetchSubmitted({required String of}) async {
|
||||
/// Fetch a list of ids of [Story] or [Comment] submitted by the user.
|
||||
Future<List<int>?> fetchSubmitted({required String userId}) async {
|
||||
final List<int>? submitted = await _firebaseClient
|
||||
.get('${_baseUrl}user/$of.json')
|
||||
.get('${_baseUrl}user/$userId.json')
|
||||
.then((dynamic val) {
|
||||
if (val == null) {
|
||||
return null;
|
||||
@ -250,28 +278,34 @@ class StoriesRepository {
|
||||
return submitted;
|
||||
}
|
||||
|
||||
/// Fetch the parent [Story] of a [Comment].
|
||||
Future<Story?> fetchParentStory({required int id}) async {
|
||||
Item? item;
|
||||
|
||||
do {
|
||||
item = await fetchItemBy(id: item?.parent ?? id);
|
||||
item = await fetchItem(id: item?.parent ?? id);
|
||||
if (item == null) return null;
|
||||
} while (item is Comment);
|
||||
|
||||
return item as Story;
|
||||
}
|
||||
|
||||
/// Fetch the raw parent [Story] of a [Comment].
|
||||
/// The content of [Story] will not be parsed, use this function only if
|
||||
/// the format of content doesn't matter, otherwise, use [fetchParentStory].
|
||||
Future<Story?> fetchRawParentStory({required int id}) async {
|
||||
Item? item;
|
||||
|
||||
do {
|
||||
item = await fetchRawItemBy(id: item?.parent ?? id);
|
||||
item = await fetchRawItem(id: item?.parent ?? id);
|
||||
if (item == null) return null;
|
||||
} while (item is Comment);
|
||||
|
||||
return item as Story;
|
||||
}
|
||||
|
||||
/// Fetch the parent [Story] of a [Comment] as well as
|
||||
/// the list of [Comment] traversed in order to reach the parent.
|
||||
Future<Tuple2<Story, List<Comment>>?> fetchParentStoryWithComments({
|
||||
required int id,
|
||||
}) async {
|
||||
@ -279,7 +313,7 @@ class StoriesRepository {
|
||||
final List<Comment> parentComments = <Comment>[];
|
||||
|
||||
do {
|
||||
item = await fetchItemBy(id: item?.parent ?? id);
|
||||
item = await fetchItem(id: item?.parent ?? id);
|
||||
if (item is Comment) {
|
||||
parentComments.add(item);
|
||||
}
|
||||
@ -297,9 +331,10 @@ class StoriesRepository {
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch a list of [Comment] based on ids recursively.
|
||||
Stream<Comment?> fetchAllChildrenComments({required List<int> ids}) async* {
|
||||
for (final int id in ids) {
|
||||
final Comment? comment = await fetchCommentBy(id: id);
|
||||
final Comment? comment = await fetchComment(id: id);
|
||||
if (comment != null) {
|
||||
yield comment;
|
||||
yield* fetchAllChildrenComments(ids: comment.kids);
|
||||
@ -307,6 +342,7 @@ class StoriesRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the json of an [Item] by removing useless HTML tags.
|
||||
Future<Map<String, dynamic>?> _parseJson(Map<String, dynamic>? json) async {
|
||||
if (json == null) return null;
|
||||
final String text = json['text'] as String? ?? '';
|
||||
|
@ -5,11 +5,8 @@ import 'dart:io';
|
||||
import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/material.dart' hide Badge;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:flutter_siri_suggestions/flutter_siri_suggestions.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
@ -18,6 +15,7 @@ import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/screens/home/widgets/widgets.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
@ -145,57 +143,6 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
previous.metadataEnabled != current.metadataEnabled ||
|
||||
previous.swipeGestureEnabled != current.swipeGestureEnabled,
|
||||
builder: (BuildContext context, PreferenceState preferenceState) {
|
||||
final BlocBuilder<PinCubit, PinState> pinnedStories =
|
||||
BlocBuilder<PinCubit, PinState>(
|
||||
builder: (BuildContext context, PinState state) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
for (final Story story in state.pinnedStories)
|
||||
FadeIn(
|
||||
child: Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
context.read<PinCubit>().unpinStory(story);
|
||||
},
|
||||
backgroundColor: Palette.red,
|
||||
foregroundColor: Palette.white,
|
||||
icon: preferenceState.complexStoryTileEnabled
|
||||
? Icons.close
|
||||
: null,
|
||||
label: 'Unpin',
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ColoredBox(
|
||||
color: Palette.orangeAccent.withOpacity(0.2),
|
||||
child: StoryTile(
|
||||
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
|
||||
story: story,
|
||||
onTap: () => onStoryTapped(story, isPin: true),
|
||||
showWebPreview:
|
||||
preferenceState.complexStoryTileEnabled,
|
||||
showMetadata: preferenceState.metadataEnabled,
|
||||
showUrl: preferenceState.urlEnabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.pinnedStories.isNotEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: Dimens.pt12),
|
||||
child: Divider(
|
||||
color: Palette.orangeAccent,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return DefaultTabController(
|
||||
length: tabLength,
|
||||
child: Scaffold(
|
||||
@ -235,7 +182,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
StoriesListView(
|
||||
key: ValueKey<StoryType>(type),
|
||||
storyType: type,
|
||||
header: pinnedStories,
|
||||
header: PinnedStories(
|
||||
preferenceState: preferenceState,
|
||||
onStoryTapped: onStoryTapped,
|
||||
),
|
||||
onStoryTapped: onStoryTapped,
|
||||
),
|
||||
const ProfileScreen(),
|
||||
@ -251,11 +201,11 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
return ScreenTypeLayout.builder(
|
||||
mobile: (BuildContext context) {
|
||||
context.read<SplitViewCubit>().disableSplitView();
|
||||
return _MobileHomeScreen(
|
||||
return MobileHomeScreen(
|
||||
homeScreen: homeScreen,
|
||||
);
|
||||
},
|
||||
tablet: (BuildContext context) => _TabletHomeScreen(
|
||||
tablet: (BuildContext context) => TabletHomeScreen(
|
||||
homeScreen: homeScreen,
|
||||
),
|
||||
);
|
||||
@ -328,7 +278,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
final int? id = event.itemId;
|
||||
|
||||
if (id != null) {
|
||||
locator.get<StoriesRepository>().fetchItemBy(id: id).then((Item? item) {
|
||||
locator.get<StoriesRepository>().fetchItem(id: id).then((Item? item) {
|
||||
if (mounted) {
|
||||
if (item != null) {
|
||||
goToItemScreen(
|
||||
@ -348,10 +298,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
await locator
|
||||
.get<StoriesRepository>()
|
||||
.fetchStoryBy(storyId)
|
||||
.fetchStory(id: storyId)
|
||||
.then((Story? story) {
|
||||
if (story == null) {
|
||||
showSnackBar(content: 'Something went wrong...');
|
||||
showErrorSnackBar();
|
||||
return;
|
||||
}
|
||||
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
||||
@ -373,10 +323,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
await locator
|
||||
.get<StoriesRepository>()
|
||||
.fetchStoryBy(storyId)
|
||||
.fetchStory(id: storyId)
|
||||
.then((Story? story) {
|
||||
if (story == null) {
|
||||
showSnackBar(content: 'Something went wrong...');
|
||||
showErrorSnackBar();
|
||||
return;
|
||||
}
|
||||
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
||||
@ -385,112 +335,3 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _MobileHomeScreen extends StatelessWidget {
|
||||
const _MobileHomeScreen({
|
||||
required this.homeScreen,
|
||||
});
|
||||
|
||||
final Widget homeScreen;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(child: homeScreen),
|
||||
if (!context.read<ReminderCubit>().state.hasShown)
|
||||
const Positioned(
|
||||
left: Dimens.pt24,
|
||||
right: Dimens.pt24,
|
||||
bottom: Dimens.pt36,
|
||||
height: Dimens.pt40,
|
||||
child: CountdownReminder(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabletHomeScreen extends StatelessWidget {
|
||||
const _TabletHomeScreen({
|
||||
required this.homeScreen,
|
||||
});
|
||||
|
||||
final Widget homeScreen;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (BuildContext context, SizingInformation sizeInfo) {
|
||||
context.read<SplitViewCubit>().enableSplitView();
|
||||
double homeScreenWidth = 428;
|
||||
|
||||
if (sizeInfo.screenSize.width < homeScreenWidth * 2) {
|
||||
homeScreenWidth = 345;
|
||||
}
|
||||
|
||||
return BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||
buildWhen: (SplitViewState previous, SplitViewState current) =>
|
||||
previous.expanded != current.expanded,
|
||||
builder: (BuildContext context, SplitViewState state) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
AnimatedPositioned(
|
||||
left: Dimens.zero,
|
||||
top: Dimens.zero,
|
||||
bottom: Dimens.zero,
|
||||
width: homeScreenWidth,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.elasticOut,
|
||||
child: homeScreen,
|
||||
),
|
||||
Positioned(
|
||||
left: Dimens.pt24,
|
||||
bottom: Dimens.pt36,
|
||||
height: Dimens.pt40,
|
||||
width: homeScreenWidth - Dimens.pt24,
|
||||
child: const CountdownReminder(),
|
||||
),
|
||||
AnimatedPositioned(
|
||||
right: Dimens.zero,
|
||||
top: Dimens.zero,
|
||||
bottom: Dimens.zero,
|
||||
left: state.expanded ? Dimens.zero : homeScreenWidth,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.elasticOut,
|
||||
child: const _TabletStoryView(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabletStoryView extends StatelessWidget {
|
||||
const _TabletStoryView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||
buildWhen: (SplitViewState previous, SplitViewState current) =>
|
||||
previous.itemScreenArgs != current.itemScreenArgs,
|
||||
builder: (BuildContext context, SplitViewState state) {
|
||||
if (state.itemScreenArgs != null) {
|
||||
return ItemScreen.build(context, state.itemScreenArgs!);
|
||||
}
|
||||
|
||||
return Material(
|
||||
child: ColoredBox(
|
||||
color: Theme.of(context).canvasColor,
|
||||
child: const Center(
|
||||
child: Text('Tap on story tile to view comments.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
31
lib/screens/home/widgets/mobile_home_screen.dart
Normal file
31
lib/screens/home/widgets/mobile_home_screen.dart
Normal file
@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart' hide Badge;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class MobileHomeScreen extends StatelessWidget {
|
||||
const MobileHomeScreen({
|
||||
super.key,
|
||||
required this.homeScreen,
|
||||
});
|
||||
|
||||
final Widget homeScreen;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Positioned.fill(child: homeScreen),
|
||||
if (!context.read<ReminderCubit>().state.hasShown)
|
||||
const Positioned(
|
||||
left: Dimens.pt24,
|
||||
right: Dimens.pt24,
|
||||
bottom: Dimens.pt36,
|
||||
height: Dimens.pt40,
|
||||
child: CountdownReminder(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
72
lib/screens/home/widgets/pinned_stories.dart
Normal file
72
lib/screens/home/widgets/pinned_stories.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart' hide Badge;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class PinnedStories extends StatelessWidget {
|
||||
const PinnedStories({
|
||||
super.key,
|
||||
required this.preferenceState,
|
||||
required this.onStoryTapped,
|
||||
});
|
||||
|
||||
final PreferenceState preferenceState;
|
||||
final void Function(Story story, {bool isPin}) onStoryTapped;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<PinCubit, PinState>(
|
||||
builder: (BuildContext context, PinState state) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
for (final Story story in state.pinnedStories)
|
||||
FadeIn(
|
||||
child: Slidable(
|
||||
startActionPane: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) {
|
||||
HapticFeedback.lightImpact();
|
||||
context.read<PinCubit>().unpinStory(story);
|
||||
},
|
||||
backgroundColor: Palette.red,
|
||||
foregroundColor: Palette.white,
|
||||
icon: preferenceState.complexStoryTileEnabled
|
||||
? Icons.close
|
||||
: null,
|
||||
label: 'Unpin',
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ColoredBox(
|
||||
color: Palette.orangeAccent.withOpacity(0.2),
|
||||
child: StoryTile(
|
||||
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
|
||||
story: story,
|
||||
onTap: () => onStoryTapped(story, isPin: true),
|
||||
showWebPreview: preferenceState.complexStoryTileEnabled,
|
||||
showMetadata: preferenceState.metadataEnabled,
|
||||
showUrl: preferenceState.urlEnabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.pinnedStories.isNotEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: Dimens.pt12),
|
||||
child: Divider(
|
||||
color: Palette.orangeAccent,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
92
lib/screens/home/widgets/tablet_home_screen.dart
Normal file
92
lib/screens/home/widgets/tablet_home_screen.dart
Normal file
@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart' hide Badge;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:responsive_builder/responsive_builder.dart';
|
||||
|
||||
class TabletHomeScreen extends StatelessWidget {
|
||||
const TabletHomeScreen({
|
||||
super.key,
|
||||
required this.homeScreen,
|
||||
});
|
||||
|
||||
final Widget homeScreen;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ResponsiveBuilder(
|
||||
builder: (BuildContext context, SizingInformation sizeInfo) {
|
||||
context.read<SplitViewCubit>().enableSplitView();
|
||||
double homeScreenWidth = 428;
|
||||
|
||||
if (sizeInfo.screenSize.width < homeScreenWidth * 2) {
|
||||
homeScreenWidth = 345;
|
||||
}
|
||||
|
||||
return BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||
buildWhen: (SplitViewState previous, SplitViewState current) =>
|
||||
previous.expanded != current.expanded,
|
||||
builder: (BuildContext context, SplitViewState state) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
AnimatedPositioned(
|
||||
left: Dimens.zero,
|
||||
top: Dimens.zero,
|
||||
bottom: Dimens.zero,
|
||||
width: homeScreenWidth,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.elasticOut,
|
||||
child: homeScreen,
|
||||
),
|
||||
Positioned(
|
||||
left: Dimens.pt24,
|
||||
bottom: Dimens.pt36,
|
||||
height: Dimens.pt40,
|
||||
width: homeScreenWidth - Dimens.pt24,
|
||||
child: const CountdownReminder(),
|
||||
),
|
||||
AnimatedPositioned(
|
||||
right: Dimens.zero,
|
||||
top: Dimens.zero,
|
||||
bottom: Dimens.zero,
|
||||
left: state.expanded ? Dimens.zero : homeScreenWidth,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.elasticOut,
|
||||
child: const _TabletStoryView(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TabletStoryView extends StatelessWidget {
|
||||
const _TabletStoryView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SplitViewCubit, SplitViewState>(
|
||||
buildWhen: (SplitViewState previous, SplitViewState current) =>
|
||||
previous.itemScreenArgs != current.itemScreenArgs,
|
||||
builder: (BuildContext context, SplitViewState state) {
|
||||
if (state.itemScreenArgs != null) {
|
||||
return ItemScreen.build(context, state.itemScreenArgs!);
|
||||
}
|
||||
|
||||
return Material(
|
||||
child: ColoredBox(
|
||||
color: Theme.of(context).canvasColor,
|
||||
child: const Center(
|
||||
child: Text('Tap on story tile to view comments.'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
3
lib/screens/home/widgets/widgets.dart
Normal file
3
lib/screens/home/widgets/widgets.dart
Normal file
@ -0,0 +1,3 @@
|
||||
export 'mobile_home_screen.dart';
|
||||
export 'pinned_stories.dart';
|
||||
export 'tablet_home_screen.dart';
|
@ -1,5 +1,3 @@
|
||||
// ignore_for_file: comment_references
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -13,6 +11,7 @@ import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/main.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/stories_repository.dart';
|
||||
import 'package:hacki/screens/item/widgets/widgets.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
@ -33,7 +32,7 @@ class ItemScreenArgs extends Equatable {
|
||||
final List<Comment>? targetComments;
|
||||
|
||||
/// when a user is trying to view a sub-thread from a main thread, we don't
|
||||
/// need to fetch comments from [StoryRepository] since we have some, if not
|
||||
/// need to fetch comments from [StoriesRepository] since we have some, if not
|
||||
/// all, comments cached in [CommentCache].
|
||||
final bool useCommentCache;
|
||||
|
||||
@ -149,7 +148,6 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
initialRefreshStatus: RefreshStatus.refreshing,
|
||||
);
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final String happyFace = Constants.happyFaces.pickRandomly()!;
|
||||
final Throttle storyLinkTapThrottle = Throttle(
|
||||
delay: _storyLinkTapThrottleDelay,
|
||||
);
|
||||
@ -233,20 +231,14 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
||||
context.read<EditCubit>().state.replyingTo == null
|
||||
? 'updated'
|
||||
: 'submitted';
|
||||
final String msg =
|
||||
'Comment $verb! ${Constants.happyFaces.pickRandomly()}';
|
||||
final String msg = 'Comment $verb! ${Constants.happyFace}';
|
||||
focusNode.unfocus();
|
||||
HapticFeedback.lightImpact();
|
||||
showSnackBar(content: msg);
|
||||
context.read<EditCubit>().onReplySubmittedSuccessfully();
|
||||
context.read<PostCubit>().reset();
|
||||
} else if (postState.status == PostStatus.failure) {
|
||||
showSnackBar(
|
||||
content: 'Something went wrong...'
|
||||
'${Constants.sadFaces.pickRandomly()}',
|
||||
label: 'Okay',
|
||||
action: ScaffoldMessenger.of(context).hideCurrentSnackBar,
|
||||
);
|
||||
showErrorSnackBar();
|
||||
context.read<PostCubit>().reset();
|
||||
}
|
||||
},
|
||||
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/screens/widgets/widgets.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
@ -28,9 +27,10 @@ class LoginDialog extends StatelessWidget {
|
||||
return BlocConsumer<AuthBloc, AuthState>(
|
||||
listener: (BuildContext context, AuthState state) {
|
||||
if (state.isLoggedIn) {
|
||||
final String happyFace = Constants.happyFaces.pickRandomly()!;
|
||||
Navigator.pop(context);
|
||||
showSnackBar(content: 'Logged in successfully! $happyFace');
|
||||
showSnackBar(
|
||||
content: 'Logged in successfully! ${Constants.happyFace}',
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, AuthState state) {
|
||||
@ -87,13 +87,13 @@ class LoginDialog extends StatelessWidget {
|
||||
height: Dimens.pt16,
|
||||
),
|
||||
if (state.status == AuthStatus.failure)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt18,
|
||||
),
|
||||
child: Text(
|
||||
'Something went wrong...',
|
||||
style: TextStyle(
|
||||
Constants.errorMessage,
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
fontSize: TextDimens.pt12,
|
||||
),
|
||||
|
@ -135,7 +135,7 @@ class MainView extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: _trailingBoxHeight,
|
||||
child: Center(
|
||||
child: Text(Constants.happyFaces.pickRandomly()!),
|
||||
child: Text(Constants.happyFace),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@ -342,13 +342,13 @@ class _ParentItemSection extends StatelessWidget {
|
||||
),
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyLarge
|
||||
@ -359,10 +359,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
text: state.item.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
color: state.item.url.isNotEmpty
|
||||
? Palette.orange
|
||||
: null,
|
||||
@ -374,10 +371,8 @@ class _ParentItemSection extends StatelessWidget {
|
||||
''' (${(state.item as Story).readableUrl})''',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
(prefState.fontSize.fontSize - 4),
|
||||
fontSize:
|
||||
prefState.fontSize.fontSize - 4,
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
@ -391,36 +386,39 @@ class _ParentItemSection extends StatelessWidget {
|
||||
height: Dimens.pt6,
|
||||
),
|
||||
if (state.item.text.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
child: SelectableLinkify(
|
||||
text: state.item.text,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(context).textScaleFactor *
|
||||
context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt10,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context).textScaleFactor *
|
||||
context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
color: Palette.orange,
|
||||
child: SelectableLinkify(
|
||||
text: state.item.text,
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
style: TextStyle(
|
||||
fontSize: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: context
|
||||
.read<PreferenceCubit>()
|
||||
.state
|
||||
.fontSize
|
||||
.fontSize,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -15,18 +15,12 @@ class MorePopupMenu extends StatelessWidget {
|
||||
super.key,
|
||||
required this.item,
|
||||
required this.isBlocked,
|
||||
required this.showSnackBar,
|
||||
required this.onStoryLinkTapped,
|
||||
required this.onLoginTapped,
|
||||
});
|
||||
|
||||
final Item item;
|
||||
final bool isBlocked;
|
||||
final void Function({
|
||||
required String content,
|
||||
VoidCallback? action,
|
||||
String? label,
|
||||
}) showSnackBar;
|
||||
final ValueChanged<String> onStoryLinkTapped;
|
||||
final VoidCallback onLoginTapped;
|
||||
|
||||
@ -43,24 +37,26 @@ class MorePopupMenu extends StatelessWidget {
|
||||
},
|
||||
listener: (BuildContext context, VoteState voteState) {
|
||||
if (voteState.status == VoteStatus.submitted) {
|
||||
showSnackBar(content: 'Vote submitted successfully.');
|
||||
context.showSnackBar(content: 'Vote submitted successfully.');
|
||||
} else if (voteState.status == VoteStatus.canceled) {
|
||||
showSnackBar(content: 'Vote canceled.');
|
||||
context.showSnackBar(content: 'Vote canceled.');
|
||||
} else if (voteState.status == VoteStatus.failure) {
|
||||
showSnackBar(content: 'Something went wrong...');
|
||||
context.showErrorSnackBar();
|
||||
} else if (voteState.status ==
|
||||
VoteStatus.failureKarmaBelowThreshold) {
|
||||
showSnackBar(
|
||||
context.showSnackBar(
|
||||
content: "You can't downvote because you are karmaly broke.",
|
||||
);
|
||||
} else if (voteState.status == VoteStatus.failureNotLoggedIn) {
|
||||
showSnackBar(
|
||||
context.showSnackBar(
|
||||
content: 'Not logged in, no voting! (;`O´)o',
|
||||
action: onLoginTapped,
|
||||
label: 'Log in',
|
||||
);
|
||||
} else if (voteState.status == VoteStatus.failureBeHumble) {
|
||||
showSnackBar(content: 'No voting on your own post! (;`O´)o');
|
||||
context.showSnackBar(
|
||||
content: 'No voting on your own post! (;`O´)o',
|
||||
);
|
||||
}
|
||||
|
||||
Navigator.pop(
|
||||
|
@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||
import 'package:hacki/blocs/auth/auth_bloc.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/context_extension.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
@ -61,36 +62,29 @@ class PollView extends StatelessWidget {
|
||||
listener: (BuildContext context, VoteState voteState) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
if (voteState.status == VoteStatus.submitted) {
|
||||
showSnackBar(
|
||||
context,
|
||||
context.showSnackBar(
|
||||
content: 'Vote submitted successfully.',
|
||||
);
|
||||
} else if (voteState.status == VoteStatus.canceled) {
|
||||
showSnackBar(context, content: 'Vote canceled.');
|
||||
context.showSnackBar(content: 'Vote canceled.');
|
||||
} else if (voteState.status == VoteStatus.failure) {
|
||||
showSnackBar(
|
||||
context,
|
||||
content: 'Something went wrong...',
|
||||
);
|
||||
context.showErrorSnackBar();
|
||||
} else if (voteState.status ==
|
||||
VoteStatus.failureKarmaBelowThreshold) {
|
||||
showSnackBar(
|
||||
context,
|
||||
context.showSnackBar(
|
||||
content: "You can't downvote because"
|
||||
' you are karmaly broke.',
|
||||
);
|
||||
} else if (voteState.status ==
|
||||
VoteStatus.failureNotLoggedIn) {
|
||||
showSnackBar(
|
||||
context,
|
||||
context.showSnackBar(
|
||||
content: 'Not logged in, no voting! (;`O´)o',
|
||||
action: onLoginTapped,
|
||||
label: 'Log in',
|
||||
);
|
||||
} else if (voteState.status ==
|
||||
VoteStatus.failureBeHumble) {
|
||||
showSnackBar(
|
||||
context,
|
||||
context.showSnackBar(
|
||||
content: 'No voting on your own post! (;`O´)o',
|
||||
);
|
||||
}
|
||||
@ -153,26 +147,4 @@ class PollView extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showSnackBar(
|
||||
BuildContext context, {
|
||||
required String content,
|
||||
VoidCallback? action,
|
||||
String? label,
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Palette.deepOrange,
|
||||
content: Text(content),
|
||||
action: action != null && label != null
|
||||
? SnackBarAction(
|
||||
label: label,
|
||||
onPressed: action,
|
||||
textColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||||
)
|
||||
: null,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -336,9 +336,10 @@ class _ReplyBoxState extends State<ReplyBox> {
|
||||
child: SingleChildScrollView(
|
||||
child: SelectableLinkify(
|
||||
scrollPhysics: const NeverScrollableScrollPhysics(),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context).textScaleFactor *
|
||||
TextDimens.pt15,
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
linkStyle: const TextStyle(
|
||||
fontSize: TextDimens.pt15,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) =>
|
||||
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/blocs/blocs.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';
|
||||
@ -384,193 +383,6 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
});
|
||||
}
|
||||
|
||||
void onLoginTapped() {
|
||||
final TextEditingController usernameController = TextEditingController();
|
||||
final TextEditingController passwordController = TextEditingController();
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return BlocConsumer<AuthBloc, AuthState>(
|
||||
listener: (BuildContext context, AuthState state) {
|
||||
if (state.isLoggedIn) {
|
||||
Navigator.pop(context);
|
||||
showSnackBar(content: 'Logged in successfully!');
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, AuthState state) {
|
||||
return SimpleDialog(
|
||||
children: <Widget>[
|
||||
if (state.status == AuthStatus.loading)
|
||||
const SizedBox(
|
||||
height: Dimens.pt36,
|
||||
width: Dimens.pt36,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Palette.orange,
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (!state.isLoggedIn) ...<Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt18,
|
||||
),
|
||||
child: TextField(
|
||||
controller: usernameController,
|
||||
cursorColor: Palette.orange,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Username',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Palette.orange),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt16,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt18,
|
||||
),
|
||||
child: TextField(
|
||||
controller: passwordController,
|
||||
cursorColor: Palette.orange,
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Password',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: Palette.orange),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt16,
|
||||
),
|
||||
if (state.status == AuthStatus.failure)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: Dimens.pt18,
|
||||
),
|
||||
child: Text(
|
||||
'Something went wrong...',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
fontSize: TextDimens.pt12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
state.agreedToEULA
|
||||
? Icons.check_box
|
||||
: Icons.check_box_outline_blank,
|
||||
color: state.agreedToEULA
|
||||
? Palette.deepOrange
|
||||
: Palette.grey,
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<AuthBloc>()
|
||||
.add(AuthToggleAgreeToEULA()),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: <InlineSpan>[
|
||||
const TextSpan(
|
||||
text: 'I agree to ',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, 1),
|
||||
child: TapDownWrapper(
|
||||
onTap: () => LinkUtil.launch(
|
||||
Constants.endUserAgreementLink,
|
||||
),
|
||||
child: const Text(
|
||||
'End User Agreement',
|
||||
style: TextStyle(
|
||||
color: Palette.deepOrange,
|
||||
decoration: TextDecoration.underline,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: Dimens.pt12,
|
||||
),
|
||||
child: ButtonBar(
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
context.read<AuthBloc>().add(AuthInitialize());
|
||||
},
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (state.agreedToEULA) {
|
||||
final String username = usernameController.text;
|
||||
final String password = passwordController.text;
|
||||
if (username.isNotEmpty && password.isNotEmpty) {
|
||||
context.read<AuthBloc>().add(
|
||||
AuthLogin(
|
||||
username: username,
|
||||
password: password,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
state.agreedToEULA
|
||||
? Palette.deepOrange
|
||||
: Palette.grey,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Log in',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Palette.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
@ -131,6 +131,7 @@ class _SettingsState extends State<Settings> {
|
||||
.toList(),
|
||||
onChanged: (FetchMode? fetchMode) {
|
||||
if (fetchMode != null) {
|
||||
HapticFeedback.selectionClick();
|
||||
context.read<PreferenceCubit>().update(
|
||||
FetchModePreference(),
|
||||
to: fetchMode.index,
|
||||
@ -164,6 +165,7 @@ class _SettingsState extends State<Settings> {
|
||||
.toList(),
|
||||
onChanged: (CommentsOrder? order) {
|
||||
if (order != null) {
|
||||
HapticFeedback.selectionClick();
|
||||
context.read<PreferenceCubit>().update(
|
||||
CommentsOrderPreference(),
|
||||
to: order.index,
|
||||
|
@ -1,4 +1,4 @@
|
||||
export 'home_screen.dart';
|
||||
export 'home/home_screen.dart';
|
||||
export 'item/item_screen.dart';
|
||||
export 'profile/profile_screen.dart';
|
||||
export 'search/search_screen.dart';
|
||||
|
@ -50,9 +50,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
|
||||
content: 'Post submitted successfully.',
|
||||
);
|
||||
} else if (state.status == SubmitStatus.failure) {
|
||||
showSnackBar(
|
||||
content: 'Something went wrong...',
|
||||
);
|
||||
showErrorSnackBar();
|
||||
}
|
||||
},
|
||||
builder: (BuildContext context, SubmitState state) {
|
||||
|
48
lib/screens/widgets/centered_text.dart
Normal file
48
lib/screens/widgets/centered_text.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class CenteredText extends StatelessWidget {
|
||||
const CenteredText({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.color = Palette.grey,
|
||||
});
|
||||
|
||||
const CenteredText.deleted({Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
text: 'deleted',
|
||||
);
|
||||
|
||||
const CenteredText.dead({Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
text: 'dead',
|
||||
);
|
||||
|
||||
const CenteredText.blocked({Key? key})
|
||||
: this(
|
||||
key: key,
|
||||
text: 'blocked',
|
||||
);
|
||||
|
||||
final String text;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/bloc_builder_3.dart';
|
||||
import 'package:hacki/screens/widgets/centered_text.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:hacki/utils/utils.dart';
|
||||
@ -40,6 +41,8 @@ class CommentTile extends StatelessWidget {
|
||||
final void Function(String) onStoryLinkTapped;
|
||||
final FetchMode fetchMode;
|
||||
|
||||
static final Map<int, Color> _colors = <int, Color>{};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<CollapseCubit>(
|
||||
@ -157,136 +160,45 @@ class CommentTile extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (actionable && state.collapsed)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'collapsed '
|
||||
'(${state.collapsedCount + 1})',
|
||||
style: const TextStyle(
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (actionable && state.collapsed)
|
||||
CenteredText(
|
||||
text:
|
||||
'''collapsed (${state.collapsedCount + 1})''',
|
||||
color: Palette.orangeAccent,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (comment.deleted)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'deleted',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (comment.dead)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'dead',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (blocklistState.blocklist.contains(comment.by))
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: Text(
|
||||
'blocked',
|
||||
style: TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt8,
|
||||
right: Dimens.pt8,
|
||||
top: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: comment is BuildableComment
|
||||
? SelectableText.rich(
|
||||
key: ValueKey<int>(comment.id),
|
||||
buildTextSpan(
|
||||
(comment as BuildableComment).elements,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(
|
||||
context,
|
||||
).textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped.call(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
),
|
||||
onTap: () => onTextTapped(context),
|
||||
)
|
||||
: SelectableLinkify(
|
||||
key: ValueKey<int>(comment.id),
|
||||
text: comment.text,
|
||||
style: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: MediaQuery.of(context)
|
||||
.textScaleFactor *
|
||||
prefState.fontSize.fontSize,
|
||||
color: Palette.orange,
|
||||
),
|
||||
onOpen: (LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped.call(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
},
|
||||
onTap: () => onTextTapped(context),
|
||||
)
|
||||
else if (comment.deleted)
|
||||
const CenteredText.deleted()
|
||||
else if (comment.dead)
|
||||
const CenteredText.dead()
|
||||
else if (blocklistState.blocklist
|
||||
.contains(comment.by))
|
||||
const CenteredText.blocked()
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: Dimens.pt8,
|
||||
right: Dimens.pt8,
|
||||
top: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: _CommentText(
|
||||
key: ValueKey<int>(comment.id),
|
||||
comment: comment,
|
||||
onLinkTapped: _onLinkTapped,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!state.collapsed &&
|
||||
fetchMode == FetchMode.lazy &&
|
||||
comment.kids.isNotEmpty &&
|
||||
!context
|
||||
.read<CommentsCubit>()
|
||||
.state
|
||||
.commentIds
|
||||
.contains(comment.kids.first) &&
|
||||
!context
|
||||
.read<CommentsCubit>()
|
||||
.state
|
||||
.onlyShowTargetComment)
|
||||
),
|
||||
if (_shouldShowLoadButton(context))
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@ -376,8 +288,6 @@ class CommentTile extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
static final Map<int, Color> _colors = <int, Color>{};
|
||||
|
||||
Color _getColor(int level) {
|
||||
final int initialLevel = level;
|
||||
if (_colors[initialLevel] != null) return _colors[initialLevel]!;
|
||||
@ -406,6 +316,68 @@ class CommentTile extends StatelessWidget {
|
||||
return color;
|
||||
}
|
||||
|
||||
bool _shouldShowLoadButton(BuildContext context) {
|
||||
final CollapseState collapseState = context.read<CollapseCubit>().state;
|
||||
final CommentsState? commentsState =
|
||||
context.tryRead<CommentsCubit>()?.state;
|
||||
return fetchMode == FetchMode.lazy &&
|
||||
comment.kids.isNotEmpty &&
|
||||
collapseState.collapsed == false &&
|
||||
commentsState?.commentIds.contains(comment.kids.first) == false &&
|
||||
commentsState?.onlyShowTargetComment == false;
|
||||
}
|
||||
|
||||
void _onLinkTapped(LinkableElement link) {
|
||||
if (link.url.isStoryLink) {
|
||||
onStoryLinkTapped.call(link.url);
|
||||
} else {
|
||||
LinkUtil.launch(link.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _CommentText extends StatelessWidget {
|
||||
const _CommentText({
|
||||
super.key,
|
||||
required this.comment,
|
||||
required this.onLinkTapped,
|
||||
});
|
||||
|
||||
final Comment comment;
|
||||
final void Function(LinkableElement) onLinkTapped;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final PreferenceState prefState = context.read<PreferenceCubit>().state;
|
||||
final TextStyle style = TextStyle(
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
);
|
||||
final TextStyle linkStyle = TextStyle(
|
||||
fontSize: prefState.fontSize.fontSize,
|
||||
decoration: TextDecoration.underline,
|
||||
color: Palette.orange,
|
||||
);
|
||||
if (comment is BuildableComment) {
|
||||
return SelectableText.rich(
|
||||
buildTextSpan(
|
||||
(comment as BuildableComment).elements,
|
||||
style: style,
|
||||
linkStyle: linkStyle,
|
||||
onOpen: onLinkTapped,
|
||||
),
|
||||
onTap: () => onTextTapped(context),
|
||||
);
|
||||
} else {
|
||||
return SelectableLinkify(
|
||||
text: comment.text,
|
||||
style: style,
|
||||
linkStyle: linkStyle,
|
||||
onOpen: onLinkTapped,
|
||||
onTap: () => onTextTapped(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void onTextTapped(BuildContext context) {
|
||||
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
|
||||
HapticFeedback.selectionClick();
|
||||
|
@ -108,10 +108,10 @@ class _CountDownReminderState extends State<CountdownReminder>
|
||||
if (state.storyId != null) {
|
||||
locator
|
||||
.get<StoriesRepository>()
|
||||
.fetchStoryBy(state.storyId!)
|
||||
.fetchStory(id: state.storyId!)
|
||||
.then((Story? story) {
|
||||
if (story == null) {
|
||||
showSnackBar(content: 'Something went wrong...');
|
||||
showErrorSnackBar();
|
||||
return;
|
||||
}
|
||||
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
||||
|
@ -5,7 +5,7 @@ import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/screens/widgets/link_preview/link_view.dart';
|
||||
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
@ -119,7 +119,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_errorTitle = widget.errorTitle ?? 'Something went wrong!';
|
||||
_errorTitle = widget.errorTitle ?? Constants.errorMessage;
|
||||
_errorBody = widget.errorBody ??
|
||||
'Oops! Unable to parse the url. We have '
|
||||
'sent feedback to our developers & '
|
||||
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart';
|
||||
import 'package:hacki/services/services.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class OfflineBanner extends StatelessWidget {
|
||||
|
@ -1,4 +1,5 @@
|
||||
export 'bloc_builder_3.dart';
|
||||
export 'centered_text.dart';
|
||||
export 'circle_tab_indicator.dart';
|
||||
export 'comment_tile.dart';
|
||||
export 'countdown_reminder.dart';
|
||||
|
@ -4,7 +4,6 @@ import 'dart:math';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
import 'package:hacki/repositories/repositories.dart';
|
||||
import 'package:hacki/utils/html_util.dart';
|
||||
@ -43,7 +42,6 @@ abstract class Fetcher {
|
||||
final SembastRepository sembastRepository = SembastRepository();
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
final String happyFace = Constants.happyFaces.pickRandomly()!;
|
||||
final String? username = await authRepository.username;
|
||||
final List<int> unreadIds = await preferenceRepository.unreadCommentsIds;
|
||||
|
||||
@ -52,7 +50,7 @@ abstract class Fetcher {
|
||||
Comment? newReply;
|
||||
|
||||
await storiesRepository
|
||||
.fetchSubmitted(of: username)
|
||||
.fetchSubmitted(userId: username)
|
||||
.then((List<int>? submittedItems) async {
|
||||
if (submittedItems != null) {
|
||||
final List<int> subscribedItems = submittedItems.sublist(
|
||||
@ -61,9 +59,7 @@ abstract class Fetcher {
|
||||
);
|
||||
|
||||
for (final int id in subscribedItems) {
|
||||
await storiesRepository
|
||||
.fetchRawItemBy(id: id)
|
||||
.then((Item? item) async {
|
||||
await storiesRepository.fetchRawItem(id: id).then((Item? item) async {
|
||||
final List<int> kids = item?.kids ?? <int>[];
|
||||
final List<int> previousKids =
|
||||
(await sembastRepository.kids(of: id)) ?? <int>[];
|
||||
@ -78,7 +74,7 @@ abstract class Fetcher {
|
||||
if (unreadIds.contains(newCommentId)) continue;
|
||||
|
||||
await storiesRepository
|
||||
.fetchRawCommentBy(id: newCommentId)
|
||||
.fetchRawComment(id: newCommentId)
|
||||
.then((Comment? comment) async {
|
||||
final bool hasPushedBefore =
|
||||
await preferenceRepository.hasPushed(newReply!.id);
|
||||
@ -123,7 +119,7 @@ abstract class Fetcher {
|
||||
|
||||
await flutterLocalNotificationsPlugin.show(
|
||||
newReply?.id ?? 0,
|
||||
'You have a new reply! $happyFace',
|
||||
'You have a new reply! ${Constants.happyFace}',
|
||||
'${newReply?.by}: $text',
|
||||
const NotificationDetails(
|
||||
iOS: DarwinNotificationDetails(
|
||||
|
@ -2,14 +2,12 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:hacki/config/constants.dart';
|
||||
import 'package:hacki/extensions/extensions.dart';
|
||||
import 'package:hacki/models/models.dart';
|
||||
|
||||
class LocalNotification {
|
||||
Future<void> pushForNewReply(Comment newReply, int storyId) async {
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
final String happyFace = Constants.happyFaces.pickRandomly()!;
|
||||
|
||||
final Map<String, int> payloadJson = <String, int>{
|
||||
'commentId': newReply.id,
|
||||
@ -19,7 +17,7 @@ class LocalNotification {
|
||||
|
||||
return flutterLocalNotificationsPlugin.show(
|
||||
newReply.id,
|
||||
'You have a new reply! $happyFace',
|
||||
'You have a new reply! ${Constants.happyFace}',
|
||||
'${newReply.by}: ${newReply.text}',
|
||||
const NotificationDetails(
|
||||
iOS: DarwinNotificationDetails(
|
||||
|
@ -3,3 +3,4 @@ export 'custom_bloc_observer.dart';
|
||||
export 'fetcher.dart';
|
||||
export 'firebase_client.dart';
|
||||
export 'local_notification.dart';
|
||||
export 'web_analyzer.dart';
|
||||
|
@ -294,7 +294,7 @@ class WebAnalyzer {
|
||||
// Kids of stories from search results are always empty, so here we try
|
||||
// to fetch the story itself first and see if the kids are still empty.
|
||||
if (kids.isEmpty) {
|
||||
final Story? story = await storiesRepository.fetchStoryBy(storyId);
|
||||
final Story? story = await storiesRepository.fetchStory(id: storyId);
|
||||
|
||||
if (story == null) return null;
|
||||
|
||||
@ -304,7 +304,7 @@ class WebAnalyzer {
|
||||
}
|
||||
|
||||
final Comment? comment =
|
||||
await storiesRepository.fetchCommentBy(id: kids.first);
|
||||
await storiesRepository.fetchComment(id: kids.first);
|
||||
|
||||
return comment != null ? '${comment.by}: ${comment.text}' : null;
|
||||
}
|
@ -14,7 +14,7 @@ abstract class LogUtil {
|
||||
colors: false,
|
||||
);
|
||||
|
||||
static LogOutput getLogOutput(File outputFile) => MultiOutput(
|
||||
static LogOutput logOutput(File outputFile) => MultiOutput(
|
||||
<LogOutput>[
|
||||
ConsoleOutput(),
|
||||
CustomFileOutput(
|
||||
@ -43,7 +43,7 @@ abstract class LogUtil {
|
||||
|
||||
final Uint8List fileContent = await currentSessionLog.readAsBytes();
|
||||
await previousSessionLog.writeAsString(
|
||||
'Current session logs:',
|
||||
'Current session logs:\n',
|
||||
mode: FileMode.append,
|
||||
);
|
||||
return previousSessionLog.writeAsBytes(
|
||||
|
100
pubspec.lock
100
pubspec.lock
@ -37,10 +37,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
|
||||
sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -61,18 +61,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bloc
|
||||
sha256: bd4f8027bfa60d96c8046dec5ce74c463b2c918dce1b0d36593575995344534a
|
||||
sha256: "658a5ae59edcf1e58aac98b000a71c762ad8f46f1394c34a52050cafb3e11a80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.0"
|
||||
version: "8.1.1"
|
||||
bloc_test:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: bloc_test
|
||||
sha256: "622b97678bf8c06a94f4c26a89ee9ebf7319bf775383dee2233e86e1f94ee28d"
|
||||
sha256: ffbb60c17ee3d8e3784cb78071088e353199057233665541e8ac6cd438dca8ad
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.0"
|
||||
version: "9.1.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -141,18 +141,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: connectivity_plus
|
||||
sha256: "745ebcccb1ef73768386154428a55250bc8d44059c19fd27aecda2a6dc013a22"
|
||||
sha256: "8875e8ed511a49f030e313656154e4bbbcef18d68dfd32eb853fac10bce48e96"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
connectivity_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
sha256: b8795b9238bf83b64375f63492034cb3d8e222af4d9ce59dda085edf038fa06f
|
||||
sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
version: "1.2.4"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -165,18 +165,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "961c4aebd27917269b1896382c7cb1b1ba81629ba669ba09c27a7e5710ec9040"
|
||||
sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.2"
|
||||
version: "1.6.3"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: f71079978789bc2fe78d79227f1f8cfe195b31bbd8db2399b0d15a4b96fb843b
|
||||
sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.3+2"
|
||||
version: "0.3.3+4"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -275,10 +275,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bloc
|
||||
sha256: "890c51c8007f0182360e523518a0c732efb89876cb4669307af7efada5b55557"
|
||||
sha256: "434951eea948dbe87f737b674281465f610b8259c16c097b8163ce138749a775"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.1"
|
||||
version: "8.1.2"
|
||||
flutter_blurhash:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -368,26 +368,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: f2afec1f1762c040a349ea2a588e32f442da5d0db3494a52a929a97c9e550bc5
|
||||
sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
version: "8.0.0"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: "736436adaf91552433823f51ce22e098c2f0551db06b6596f58597a25b8ea797"
|
||||
sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
version: "1.1.3"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: ff0768a6700ea1d9620e03518e2e25eac86a8bd07ca3556e9617bfa5ace4bd00
|
||||
sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.0"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -408,10 +408,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: ca89c8059cf439985aa83c59619b3674c7ef6cc2e86943d169a7369d6a69cab5
|
||||
sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
version: "2.0.0"
|
||||
flutter_siri_suggestions:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -442,10 +442,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: font_awesome_flutter
|
||||
sha256: "875dbb9ec1ad30d68102019ceb682760d06c72747c1c5b7885781b95f88569cc"
|
||||
sha256: "959ef4add147753f990b4a7c6cccb746d5792dbdc81b1cde99e62e7edb31b206"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.3.0"
|
||||
version: "10.4.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -535,10 +535,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hydrated_bloc
|
||||
sha256: "5871204f14b24638dc9d18d5b94cf22a66fc4be40756925cafff3a7553c7d7b7"
|
||||
sha256: eb92d88061b6b911c48779b08a91c8a9f3a3aa8475f80d9380045375d9876536
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
version: "9.1.0"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -676,10 +676,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: f619162573096d428ccde2e33f92e05b5a179cd6f0e3120c1005f181bee8ed16
|
||||
sha256: "8df5ab0a481d7dc20c0e63809e90a588e496d276ba53358afc4c4443d0a00697"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -724,10 +724,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379
|
||||
sha256: "2e32f1640f07caef0d3cb993680f181c79e54a3827b997d5ee221490d131fbd9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
version: "2.1.8"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -829,10 +829,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: responsive_builder
|
||||
sha256: "0f082dff291f5ee4b4ef713d7d1e2a242b126204559024de07039aa7d9012aa5"
|
||||
sha256: "8eed603781a53fe1804a9ba50089ceb4882887f9c5b84ff139b03d8583a12fc9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0+1"
|
||||
version: "0.5.1"
|
||||
rxdart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -853,10 +853,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: e387077716f80609bb979cd199331033326033ecd1c8f200a90c5f57b1c9f55e
|
||||
sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.1"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -885,10 +885,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "1ffa239043ab8baf881ec3094a3c767af9d10399b2839020b9e4d44c0bb23951"
|
||||
sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.3"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1121,10 +1121,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809"
|
||||
sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.8"
|
||||
version: "6.1.9"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1137,10 +1137,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3
|
||||
sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.18"
|
||||
version: "6.1.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1201,10 +1201,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: very_good_analysis
|
||||
sha256: "4815adc7ded57657038d2bb2a7f332c50e3c8152f7d3c6acf8f6b7c0cc81e5e2"
|
||||
sha256: ebc48c51db35beeeec8c414e32f7bd78e612bd7f5992ccb0d46e19edaeb40b08
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "4.0.0+1"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1297,10 +1297,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: "9d97fa2bae0f1900553c48a2ef0aaa3864367fd7bb625d683c460754b691312c"
|
||||
sha256: "5f49a6e5fc59e21fcec5e1bbcd401afbee9792a24a4f3d9cef9b5bb0cd1e3767"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.2.4"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1313,10 +1313,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: "523aff9168af9bb2170e4809e0499d7dee065c3919799fd3341d3e616c137960"
|
||||
sha256: "92e7e7fa468f1df597fb9d37bcf1f303175cbe147c4dbdf06ecc323d950116eb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.5"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1358,5 +1358,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
sdks:
|
||||
dart: ">=2.18.0 <4.0.0"
|
||||
flutter: ">=3.7.0"
|
||||
dart: ">=2.19.0 <3.0.0"
|
||||
flutter: ">=3.7.3"
|
||||
|
48
pubspec.yaml
48
pubspec.yaml
@ -1,21 +1,21 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 1.0.4+82
|
||||
version: 1.0.11+89
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0 <3.0.0"
|
||||
flutter: "3.7.0"
|
||||
flutter: "3.7.3"
|
||||
|
||||
dependencies:
|
||||
adaptive_theme: ^3.0.0
|
||||
badges: ^3.0.2
|
||||
bloc: ^8.1.0
|
||||
cached_network_image: ^3.2.1
|
||||
bloc: ^8.1.1
|
||||
cached_network_image: ^3.2.3
|
||||
clipboard: ^0.1.3
|
||||
collection: ^1.17.0
|
||||
connectivity_plus: ^3.0.2
|
||||
dio: ^4.0.4
|
||||
dio: ^4.0.6
|
||||
equatable: ^2.0.5
|
||||
fast_gbk: ^1.0.0
|
||||
feature_discovery:
|
||||
@ -24,7 +24,7 @@ dependencies:
|
||||
ref: flutter3_compatibility
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.1.1
|
||||
flutter_bloc: ^8.1.2
|
||||
flutter_cache_manager: ^3.3.0
|
||||
flutter_email_sender: ^5.2.0
|
||||
flutter_fadein: ^2.0.0
|
||||
@ -32,45 +32,45 @@ dependencies:
|
||||
flutter_inappwebview: ^5.7.2+3
|
||||
flutter_linkify: ^5.0.2
|
||||
flutter_local_notifications: ^13.0.0
|
||||
flutter_secure_storage: ^7.0.1
|
||||
flutter_secure_storage: ^8.0.0
|
||||
flutter_siri_suggestions: ^2.1.0
|
||||
flutter_slidable: ^2.0.0
|
||||
font_awesome_flutter: ^10.3.0
|
||||
gbk_codec: ^0.4.0
|
||||
get_it: 7.2.0
|
||||
hive: ^2.0.6
|
||||
html: ^0.15.0
|
||||
get_it: ^7.2.0
|
||||
hive: ^2.2.3
|
||||
html: ^0.15.1
|
||||
html_unescape: ^2.0.0
|
||||
http: ^0.13.3
|
||||
hydrated_bloc: ^9.0.0-dev.3
|
||||
http: ^0.13.5
|
||||
hydrated_bloc: ^9.1.0
|
||||
intl: ^0.18.0
|
||||
logger: ^1.1.0
|
||||
package_info_plus: ^3.0.2
|
||||
package_info_plus: ^3.0.3
|
||||
path: ^1.8.2
|
||||
path_provider: ^2.0.8
|
||||
path_provider_android: ^2.0.8
|
||||
path_provider: ^2.0.12
|
||||
path_provider_android: ^2.0.22
|
||||
path_provider_foundation: ^2.1.1
|
||||
pull_to_refresh:
|
||||
git:
|
||||
url: https://github.com/livinglist/flutter_pulltorefresh
|
||||
ref: master
|
||||
receive_sharing_intent: ^1.4.5
|
||||
responsive_builder: ^0.5.0+1
|
||||
rxdart: ^0.27.3
|
||||
sembast: ^3.1.1+1
|
||||
share_plus: ^6.3.0
|
||||
responsive_builder: ^0.5.1
|
||||
rxdart: ^0.27.7
|
||||
sembast: ^3.4.0+6
|
||||
share_plus: ^6.3.1
|
||||
shared_preferences: ^2.0.17
|
||||
shared_preferences_android: ^2.0.15
|
||||
shared_preferences_foundation: ^2.1.2
|
||||
shared_preferences_foundation: ^2.1.3
|
||||
shimmer: ^2.0.0
|
||||
synced_shared_preferences:
|
||||
path: components/synced_shared_preferences
|
||||
tuple: ^2.0.0
|
||||
tuple: ^2.0.1
|
||||
universal_platform: ^1.0.0+1
|
||||
url_launcher: ^6.1.3
|
||||
url_launcher: ^6.1.9
|
||||
wakelock: ^0.6.1+2
|
||||
webview_flutter: ^4.0.2
|
||||
workmanager: ^0.5.0
|
||||
workmanager: ^0.5.1
|
||||
|
||||
dev_dependencies:
|
||||
bloc_test: ^9.1.0
|
||||
@ -81,7 +81,7 @@ dev_dependencies:
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
mocktail: ^0.3.0
|
||||
very_good_analysis: ^3.1.0
|
||||
very_good_analysis: ^4.0.0+1
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
Submodule submodules/flutter updated: b06b8b2710...9944297138
@ -67,7 +67,7 @@ void main() {
|
||||
.thenAnswer((_) => Future<String?>.value(username));
|
||||
when(() => mockAuthRepository.password)
|
||||
.thenAnswer((_) => Future<String>.value(password));
|
||||
when(() => mockStoriesRepository.fetchUserBy(userId: username))
|
||||
when(() => mockStoriesRepository.fetchUser(id: username))
|
||||
.thenAnswer((_) => Future<User>.value(tUser));
|
||||
when(() => mockAuthRepository.loggedIn)
|
||||
.thenAnswer((_) => Future<bool>.value(false));
|
||||
@ -91,7 +91,7 @@ void main() {
|
||||
verify: (_) {
|
||||
verify(() => mockAuthRepository.loggedIn).called(2);
|
||||
verifyNever(() => mockAuthRepository.username);
|
||||
verifyNever(() => mockStoriesRepository.fetchUserBy(userId: username));
|
||||
verifyNever(() => mockStoriesRepository.fetchUser(id: username));
|
||||
},
|
||||
);
|
||||
|
||||
@ -154,8 +154,7 @@ void main() {
|
||||
password: password,
|
||||
),
|
||||
).called(1);
|
||||
verify(() => mockStoriesRepository.fetchUserBy(userId: username))
|
||||
.called(1);
|
||||
verify(() => mockStoriesRepository.fetchUser(id: username)).called(1);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
Reference in New Issue
Block a user