Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
7dc3618afe | |||
eef4691814 | |||
9f71701845 | |||
d27203b041 | |||
4f280ec4c9 | |||
72cb2737ca | |||
215203bd16 | |||
3e320faece | |||
1049568246 | |||
71aa42118d | |||
4f21d3e6bd | |||
96d0fe9e5e |
30
README.md
@ -35,22 +35,20 @@ Features:
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img width="200" alt="01" src="assets/screenshots/01.png">
|
||||
<img width="200" alt="02" src="assets/screenshots/02.png">
|
||||
<img width="200" alt="03" src="assets/screenshots/03.png">
|
||||
<img width="200" alt="04" src="assets/screenshots/04.png">
|
||||
<img width="200" alt="05" src="assets/screenshots/05.png">
|
||||
<img width="200" alt="06" src="assets/screenshots/06.png">
|
||||
<img width="200" alt="07" src="assets/screenshots/07.png">
|
||||
<img width="200" alt="08" src="assets/screenshots/08.png">
|
||||
<img width="200" alt="09" src="assets/screenshots/09.png">
|
||||
<img width="200" alt="10" src="assets/screenshots/10.png">
|
||||
<img width="200" alt="11" src="assets/screenshots/11.png">
|
||||
<img width="200" alt="12" src="assets/screenshots/12.png">
|
||||
<img width="400" alt="01" src="assets/screenshots/light-1.png">
|
||||
<img width="400" alt="06" src="assets/screenshots/dark-1.png">
|
||||
<img width="400" alt="02" src="assets/screenshots/light-2.png">
|
||||
<img width="400" alt="07" src="assets/screenshots/dark-2.png">
|
||||
<img width="400" alt="03" src="assets/screenshots/light-3.png">
|
||||
<img width="400" alt="08" src="assets/screenshots/dark-3.png">
|
||||
<img width="400" alt="04" src="assets/screenshots/light-4.png">
|
||||
<img width="400" alt="09" src="assets/screenshots/dark-4.png">
|
||||
<img width="400" alt="05" src="assets/screenshots/light-5.png">
|
||||
<img width="400" alt="10" src="assets/screenshots/dark-5.png">
|
||||
|
||||
<img width="400" alt="ipad-01" src="assets/screenshots/ipad-01.png">
|
||||
<img width="400" alt="ipad-02" src="assets/screenshots/ipad-02.png">
|
||||
<img width="400" alt="ipad-03" src="assets/screenshots/ipad-03.png">
|
||||
<img width="400" alt="ipad-04" src="assets/screenshots/ipad-04.png">
|
||||
<img width="400" alt="ipad-01" src="assets/screenshots/tablet-light-1.png">
|
||||
<img width="400" alt="ipad-02" src="assets/screenshots/tablet-dark-1.png">
|
||||
<img width="400" alt="ipad-03" src="assets/screenshots/tablet-light-2.png">
|
||||
<img width="400" alt="ipad-04" src="assets/screenshots/tablet-dark-2.png">
|
||||
</p>
|
||||
|
||||
|
@ -23,7 +23,8 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:usesCleartextTraffic="true"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
|
BIN
assets/fonts/exo_2/Exo2-Bold.ttf
Normal file
BIN
assets/fonts/exo_2/Exo2-Regular.ttf
Normal file
BIN
assets/hacki-github.png
Normal file
After Width: | Height: | Size: 419 KiB |
BIN
assets/hacki.xcf
Before Width: | Height: | Size: 548 KiB After Width: | Height: | Size: 333 KiB |
Before Width: | Height: | Size: 571 KiB After Width: | Height: | Size: 341 KiB |
Before Width: | Height: | Size: 592 KiB After Width: | Height: | Size: 359 KiB |
BIN
assets/screenshots/dark-1.png
Normal file
After Width: | Height: | Size: 1003 KiB |
BIN
assets/screenshots/dark-2.png
Normal file
After Width: | Height: | Size: 912 KiB |
BIN
assets/screenshots/dark-3.png
Normal file
After Width: | Height: | Size: 252 KiB |
BIN
assets/screenshots/dark-4.png
Normal file
After Width: | Height: | Size: 734 KiB |
BIN
assets/screenshots/dark-5.png
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/screenshots/light-1.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
assets/screenshots/light-2.png
Normal file
After Width: | Height: | Size: 893 KiB |
BIN
assets/screenshots/light-3.png
Normal file
After Width: | Height: | Size: 460 KiB |
BIN
assets/screenshots/light-4.png
Normal file
After Width: | Height: | Size: 712 KiB |
BIN
assets/screenshots/light-5.png
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
assets/screenshots/tablet-dark-1.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/screenshots/tablet-dark-2.png
Normal file
After Width: | Height: | Size: 1.5 MiB |
BIN
assets/screenshots/tablet-light-1.png
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/screenshots/tablet-light-2.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
assets/tablet-hacki.xcf
Normal file
4
fastlane/metadata/android/en-US/changelogs/132.txt
Normal file
@ -0,0 +1,4 @@
|
||||
- New comment indicator.
|
||||
- Ability to mark stories as read from home page.
|
||||
- Text rendering improvements.
|
||||
- Performance improvements.
|
4
fastlane/metadata/android/en-US/changelogs/134.txt
Normal file
@ -0,0 +1,4 @@
|
||||
- RobotoSlab as default font.
|
||||
- Material 3 design.
|
||||
- Ability to sync favorites from your Hacker News account.
|
||||
- Support for predictive back gesture.
|
Before Width: | Height: | Size: 522 KiB |
Before Width: | Height: | Size: 835 KiB |
Before Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 298 KiB |
Before Width: | Height: | Size: 820 KiB |
Before Width: | Height: | Size: 868 KiB |
Before Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 375 KiB |
Before Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 414 KiB |
Before Width: | Height: | Size: 530 KiB |
Before Width: | Height: | Size: 406 KiB |
After Width: | Height: | Size: 1003 KiB |
After Width: | Height: | Size: 912 KiB |
After Width: | Height: | Size: 252 KiB |
After Width: | Height: | Size: 734 KiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 1.3 MiB |
After Width: | Height: | Size: 893 KiB |
After Width: | Height: | Size: 460 KiB |
After Width: | Height: | Size: 712 KiB |
After Width: | Height: | Size: 1.0 MiB |
@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
@ -32,10 +33,17 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
super(const StoriesState.init()) {
|
||||
on<LoadStories>(
|
||||
onLoadStories,
|
||||
transformer: sequential(),
|
||||
);
|
||||
on<StoriesInitialize>(onInitialize);
|
||||
on<StoriesRefresh>(onRefresh);
|
||||
on<StoriesLoadMore>(onLoadMore);
|
||||
on<StoryLoaded>(onStoryLoaded);
|
||||
on<StoryLoaded>(
|
||||
onStoryLoaded,
|
||||
transformer: sequential(),
|
||||
);
|
||||
on<StoryRead>(onStoryRead);
|
||||
on<StoryUnread>(onStoryUnread);
|
||||
on<StoriesLoaded>(onStoriesLoaded);
|
||||
@ -88,14 +96,15 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
),
|
||||
);
|
||||
for (final StoryType type in StoryType.values) {
|
||||
await loadStories(type: type, emit: emit);
|
||||
add(LoadStories(type: type));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadStories({
|
||||
required StoryType type,
|
||||
required Emitter<StoriesState> emit,
|
||||
}) async {
|
||||
Future<void> onLoadStories(
|
||||
LoadStories event,
|
||||
Emitter<StoriesState> emit,
|
||||
) async {
|
||||
final StoryType type = event.type;
|
||||
if (state.isOfflineReading) {
|
||||
final List<int> ids =
|
||||
await _offlineRepository.getCachedStoryIds(type: type);
|
||||
@ -121,13 +130,12 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
.copyWithStoryIdsUpdated(type: type, to: ids)
|
||||
.copyWithCurrentPageUpdated(type: type, to: 0),
|
||||
);
|
||||
_hackerNewsRepository
|
||||
await _hackerNewsRepository
|
||||
.fetchStoriesStream(ids: ids.sublist(0, state.currentPageSize))
|
||||
.listen((Story story) {
|
||||
add(StoryLoaded(story: story, type: type));
|
||||
}).onDone(() {
|
||||
add(StoriesLoaded(type: type));
|
||||
});
|
||||
}).asFuture<void>();
|
||||
add(StoriesLoaded(type: type));
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,7 +161,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWithRefreshed(type: event.type));
|
||||
await loadStories(type: event.type, emit: emit);
|
||||
add(LoadStories(type: event.type));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,15 @@ abstract class StoriesEvent extends Equatable {
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
class LoadStories extends StoriesEvent {
|
||||
LoadStories({required this.type});
|
||||
|
||||
final StoryType type;
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[type];
|
||||
}
|
||||
|
||||
class StoriesInitialize extends StoriesEvent {
|
||||
@override
|
||||
List<Object?> get props => <Object?>[];
|
||||
|
@ -73,7 +73,7 @@ abstract class RegExpConstants {
|
||||
static const String number = '[0-9]+';
|
||||
}
|
||||
|
||||
abstract class Durations {
|
||||
abstract class AppDurations {
|
||||
static const Duration ms100 = Duration(milliseconds: 100);
|
||||
static const Duration ms200 = Duration(milliseconds: 200);
|
||||
static const Duration ms300 = Duration(milliseconds: 300);
|
||||
|
@ -25,6 +25,7 @@ Future<void> setUpLocator() async {
|
||||
)
|
||||
..registerSingleton<SembastRepository>(SembastRepository())
|
||||
..registerSingleton<HackerNewsRepository>(HackerNewsRepository())
|
||||
..registerSingleton<HackerNewsWebRepository>(HackerNewsWebRepository())
|
||||
..registerSingleton<PreferenceRepository>(PreferenceRepository())
|
||||
..registerSingleton<SearchRepository>(SearchRepository())
|
||||
..registerSingleton<AuthRepository>(AuthRepository())
|
||||
|
@ -32,6 +32,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
required CommentsOrder defaultCommentsOrder,
|
||||
CommentCache? commentCache,
|
||||
OfflineRepository? offlineRepository,
|
||||
SembastRepository? sembastRepository,
|
||||
HackerNewsRepository? hackerNewsRepository,
|
||||
Logger? logger,
|
||||
}) : _filterCubit = filterCubit,
|
||||
@ -39,6 +40,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
_commentCache = commentCache ?? locator.get<CommentCache>(),
|
||||
_offlineRepository =
|
||||
offlineRepository ?? locator.get<OfflineRepository>(),
|
||||
_sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
_hackerNewsRepository =
|
||||
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||
_logger = logger ?? locator.get<Logger>(),
|
||||
@ -55,6 +58,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
final CollapseCache _collapseCache;
|
||||
final CommentCache _commentCache;
|
||||
final OfflineRepository _offlineRepository;
|
||||
final SembastRepository _sembastRepository;
|
||||
final HackerNewsRepository _hackerNewsRepository;
|
||||
final Logger _logger;
|
||||
|
||||
@ -369,7 +373,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
itemScrollController.scrollTo(
|
||||
index: index,
|
||||
alignment: alignment,
|
||||
duration: Durations.ms400,
|
||||
duration: AppDurations.ms400,
|
||||
);
|
||||
}
|
||||
|
||||
@ -394,7 +398,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
itemScrollController.scrollTo(
|
||||
index: 1,
|
||||
alignment: 0.15,
|
||||
duration: Durations.ms400,
|
||||
duration: AppDurations.ms400,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -421,7 +425,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
itemScrollController.scrollTo(
|
||||
index: i + 1,
|
||||
alignment: 0.15,
|
||||
duration: Durations.ms400,
|
||||
duration: AppDurations.ms400,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -461,7 +465,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
itemScrollController.scrollTo(
|
||||
index: i + 1,
|
||||
alignment: 0.15,
|
||||
duration: Durations.ms400,
|
||||
duration: AppDurations.ms400,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -538,6 +542,10 @@ class CommentsCubit extends Cubit<CommentsState> {
|
||||
_collapseCache.addKid(comment.id, to: comment.parent);
|
||||
_commentCache.cacheComment(comment);
|
||||
|
||||
if (state.isOfflineReading) {
|
||||
_sembastRepository.cacheComment(comment);
|
||||
}
|
||||
|
||||
// Hide comment that matches any of the filter keywords.
|
||||
final bool hidden = _filterCubit.state.keywords.any(
|
||||
(String keyword) => comment.text.toLowerCase().contains(keyword),
|
||||
|
@ -12,7 +12,7 @@ part 'edit_state.dart';
|
||||
class EditCubit extends HydratedCubit<EditState> {
|
||||
EditCubit({DraftCache? draftCache})
|
||||
: _draftCache = draftCache ?? locator.get<DraftCache>(),
|
||||
_debouncer = Debouncer(delay: Durations.oneSecond),
|
||||
_debouncer = Debouncer(delay: AppDurations.oneSecond),
|
||||
super(const EditState.init());
|
||||
|
||||
final DraftCache _draftCache;
|
||||
|
@ -1,3 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
@ -13,12 +16,15 @@ class FavCubit extends Cubit<FavState> {
|
||||
AuthRepository? authRepository,
|
||||
PreferenceRepository? preferenceRepository,
|
||||
HackerNewsRepository? hackerNewsRepository,
|
||||
HackerNewsWebRepository? hackerNewsWebRepository,
|
||||
}) : _authBloc = authBloc,
|
||||
_authRepository = authRepository ?? locator.get<AuthRepository>(),
|
||||
_preferenceRepository =
|
||||
preferenceRepository ?? locator.get<PreferenceRepository>(),
|
||||
_hackerNewsRepository =
|
||||
hackerNewsRepository ?? locator.get<HackerNewsRepository>(),
|
||||
_hackerNewsWebRepository =
|
||||
hackerNewsWebRepository ?? locator.get<HackerNewsWebRepository>(),
|
||||
super(FavState.init()) {
|
||||
init();
|
||||
}
|
||||
@ -27,43 +33,41 @@ class FavCubit extends Cubit<FavState> {
|
||||
final AuthRepository _authRepository;
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final HackerNewsRepository _hackerNewsRepository;
|
||||
final HackerNewsWebRepository _hackerNewsWebRepository;
|
||||
late final StreamSubscription<String>? _usernameSubscription;
|
||||
static const int _pageSize = 20;
|
||||
String? _username;
|
||||
|
||||
Future<void> init() async {
|
||||
_authBloc.stream.listen((AuthState authState) {
|
||||
if (authState.username != _username) {
|
||||
_preferenceRepository
|
||||
.favList(of: authState.username)
|
||||
.then((List<int> favIds) {
|
||||
_usernameSubscription = _authBloc.stream
|
||||
.map((AuthState event) => event.username)
|
||||
.distinct()
|
||||
.listen((String username) {
|
||||
_preferenceRepository.favList(of: username).then((List<int> favIds) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
favIds: favIds,
|
||||
favItems: <Item>[],
|
||||
currentPage: 0,
|
||||
),
|
||||
);
|
||||
_hackerNewsRepository
|
||||
.fetchItemsStream(
|
||||
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(
|
||||
state.copyWith(
|
||||
favIds: favIds,
|
||||
favItems: <Item>[],
|
||||
currentPage: 0,
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
_hackerNewsRepository
|
||||
.fetchItemsStream(
|
||||
ids: favIds.sublist(0, _pageSize.clamp(0, favIds.length)),
|
||||
)
|
||||
.listen(_onItemLoaded)
|
||||
.onDone(() {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: Status.success,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
_username = authState.username;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addFav(int id) async {
|
||||
final String username = _authBloc.state.username;
|
||||
if (state.favIds.contains(id)) return;
|
||||
|
||||
await _preferenceRepository.addFav(username: username, id: id);
|
||||
|
||||
@ -89,8 +93,6 @@ class FavCubit extends Cubit<FavState> {
|
||||
}
|
||||
|
||||
void removeFav(int id) {
|
||||
final String username = _authBloc.state.username;
|
||||
|
||||
_preferenceRepository.removeFav(username: username, id: id);
|
||||
|
||||
emit(
|
||||
@ -136,8 +138,6 @@ class FavCubit extends Cubit<FavState> {
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
final String username = _authBloc.state.username;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: Status.inProgress,
|
||||
@ -167,6 +167,23 @@ class FavCubit extends Cubit<FavState> {
|
||||
emit(FavState.init());
|
||||
}
|
||||
|
||||
Future<void> merge() async {
|
||||
if (_authBloc.state.isLoggedIn) {
|
||||
emit(state.copyWith(mergeStatus: Status.inProgress));
|
||||
final Iterable<int> ids = await _hackerNewsWebRepository.fetchFavorites(
|
||||
of: _authBloc.state.username,
|
||||
);
|
||||
final List<int> combinedIds = <int>[...ids, ...state.favIds];
|
||||
final LinkedHashSet<int> mergedIds = LinkedHashSet<int>.from(combinedIds);
|
||||
await _preferenceRepository.overwriteFav(
|
||||
username: username,
|
||||
ids: mergedIds,
|
||||
);
|
||||
emit(state.copyWith(mergeStatus: Status.success));
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void _onItemLoaded(Item item) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@ -174,4 +191,14 @@ class FavCubit extends Cubit<FavState> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_usernameSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
extension on FavCubit {
|
||||
String get username => _authBloc.state.username;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ class FavState extends Equatable {
|
||||
required this.favIds,
|
||||
required this.favItems,
|
||||
required this.status,
|
||||
required this.mergeStatus,
|
||||
required this.currentPage,
|
||||
});
|
||||
|
||||
@ -12,23 +13,27 @@ class FavState extends Equatable {
|
||||
: favIds = <int>[],
|
||||
favItems = <Item>[],
|
||||
status = Status.idle,
|
||||
mergeStatus = Status.idle,
|
||||
currentPage = 0;
|
||||
|
||||
final List<int> favIds;
|
||||
final List<Item> favItems;
|
||||
final Status status;
|
||||
final Status mergeStatus;
|
||||
final int currentPage;
|
||||
|
||||
FavState copyWith({
|
||||
List<int>? favIds,
|
||||
List<Item>? favItems,
|
||||
Status? status,
|
||||
Status? mergeStatus,
|
||||
int? currentPage,
|
||||
}) {
|
||||
return FavState(
|
||||
favIds: favIds ?? this.favIds,
|
||||
favItems: favItems ?? this.favItems,
|
||||
status: status ?? this.status,
|
||||
mergeStatus: mergeStatus ?? this.mergeStatus,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
);
|
||||
}
|
||||
@ -36,6 +41,7 @@ class FavState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
status,
|
||||
mergeStatus,
|
||||
currentPage,
|
||||
favIds,
|
||||
favItems,
|
||||
|
@ -28,11 +28,14 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
_sembastRepository =
|
||||
sembastRepository ?? locator.get<SembastRepository>(),
|
||||
super(NotificationState.init()) {
|
||||
_authBloc.stream.listen((AuthState authState) {
|
||||
if (authState.isLoggedIn && authState.username != _username) {
|
||||
_authBloc.stream
|
||||
.map((AuthState event) => event.username)
|
||||
.distinct()
|
||||
.listen((String username) {
|
||||
if (username.isNotEmpty) {
|
||||
// Get the user setting.
|
||||
if (_preferenceCubit.state.notificationEnabled) {
|
||||
Future<void>.delayed(Durations.twoSeconds, init);
|
||||
Future<void>.delayed(AppDurations.twoSeconds, init);
|
||||
}
|
||||
|
||||
// Listen for setting changes in the future.
|
||||
@ -44,9 +47,7 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
_timer?.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
_username = authState.username;
|
||||
} else if (!authState.isLoggedIn) {
|
||||
} else {
|
||||
emit(NotificationState.init());
|
||||
}
|
||||
});
|
||||
@ -57,7 +58,6 @@ class NotificationCubit extends Cubit<NotificationState> {
|
||||
final HackerNewsRepository _hackerNewsRepository;
|
||||
final PreferenceRepository _preferenceRepository;
|
||||
final SembastRepository _sembastRepository;
|
||||
String? _username;
|
||||
Timer? _timer;
|
||||
|
||||
static const Duration _refreshInterval = Duration(minutes: 5);
|
||||
|
@ -70,12 +70,8 @@ class PreferenceState extends Equatable {
|
||||
|
||||
bool get customTabEnabled => _isOn<CustomTabPreference>();
|
||||
|
||||
bool get material3Enabled => _isOn<Material3Preference>();
|
||||
|
||||
bool get manualPaginationEnabled => _isOn<ManualPaginationPreference>();
|
||||
|
||||
bool get trueDarkModeEnabled => _isOn<TrueDarkModePreference>();
|
||||
|
||||
bool get hapticFeedbackEnabled => _isOn<HapticFeedbackPreference>();
|
||||
|
||||
double get textScaleFactor =>
|
||||
|
@ -20,7 +20,7 @@ extension ContextExtension on BuildContext {
|
||||
}) {
|
||||
ScaffoldMessenger.of(this).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Theme.of(this).primaryColor,
|
||||
backgroundColor: Theme.of(this).colorScheme.primary,
|
||||
content: Text(
|
||||
content,
|
||||
style: TextStyle(
|
||||
|
133
lib/main.dart
@ -138,7 +138,7 @@ Future<void> main({bool testing = false}) async {
|
||||
|
||||
HydratedBloc.storage = storage;
|
||||
|
||||
VisibilityDetectorController.instance.updateInterval = Durations.ms200;
|
||||
VisibilityDetectorController.instance.updateInterval = AppDurations.ms200;
|
||||
|
||||
runApp(
|
||||
HackiApp(
|
||||
@ -239,13 +239,11 @@ class HackiApp extends StatelessWidget {
|
||||
buildWhen: (PreferenceState previous, PreferenceState current) =>
|
||||
previous.appColor != current.appColor ||
|
||||
previous.font != current.font ||
|
||||
previous.textScaleFactor != current.textScaleFactor ||
|
||||
previous.material3Enabled != current.material3Enabled ||
|
||||
previous.trueDarkModeEnabled != current.trueDarkModeEnabled,
|
||||
previous.textScaleFactor != current.textScaleFactor,
|
||||
builder: (BuildContext context, PreferenceState state) {
|
||||
return AdaptiveTheme(
|
||||
key: ValueKey<String>(
|
||||
'''${state.appColor}${state.font}${state.material3Enabled}${state.trueDarkModeEnabled}''',
|
||||
'''${state.appColor}${state.font}''',
|
||||
),
|
||||
light: ThemeData(
|
||||
primaryColor: state.appColor,
|
||||
@ -261,7 +259,6 @@ class HackiApp extends StatelessWidget {
|
||||
primarySwatch: state.appColor,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
canvasColor: state.trueDarkModeEnabled ? Palette.black : null,
|
||||
fontFamily: state.font.name,
|
||||
),
|
||||
initial: savedThemeMode ?? AdaptiveThemeMode.system,
|
||||
@ -285,73 +282,75 @@ class HackiApp extends StatelessWidget {
|
||||
.platformDispatcher
|
||||
.platformBrightness ==
|
||||
Brightness.dark);
|
||||
final ColorScheme colorScheme = ColorScheme.fromSeed(
|
||||
brightness:
|
||||
isDarkModeEnabled ? Brightness.dark : Brightness.light,
|
||||
seedColor: state.appColor,
|
||||
);
|
||||
return FeatureDiscovery(
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaleFactor: state.textScaleFactor == 1
|
||||
? null
|
||||
: state.textScaleFactor,
|
||||
),
|
||||
data: state.textScaleFactor == 1
|
||||
? MediaQuery.of(context)
|
||||
: MediaQuery.of(context).copyWith(
|
||||
textScaler: TextScaler.linear(
|
||||
state.textScaleFactor,
|
||||
),
|
||||
),
|
||||
child: MaterialApp.router(
|
||||
key: Key(state.appColor.hashCode.toString()),
|
||||
title: 'Hacki',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: (isDarkModeEnabled ? darkTheme : theme).copyWith(
|
||||
useMaterial3: state.material3Enabled,
|
||||
dividerTheme: state.material3Enabled
|
||||
? DividerThemeData(
|
||||
color: Palette.grey.withOpacity(0.2),
|
||||
)
|
||||
: null,
|
||||
switchTheme: state.material3Enabled
|
||||
? SwitchThemeData(
|
||||
trackColor: MaterialStateProperty.resolveWith(
|
||||
(Set<MaterialState> states) {
|
||||
if (states
|
||||
.contains(MaterialState.selected)) {
|
||||
return null;
|
||||
} else {
|
||||
return Palette.grey.withOpacity(0.2);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
: null,
|
||||
bottomSheetTheme: state.material3Enabled
|
||||
? const BottomSheetThemeData(
|
||||
modalElevation: 8,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shadowColor: Palette.black,
|
||||
)
|
||||
: null,
|
||||
inputDecorationTheme: state.material3Enabled
|
||||
? InputDecorationTheme(
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: isDarkModeEnabled
|
||||
? Palette.white
|
||||
: Palette.black,
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
sliderTheme: state.material3Enabled
|
||||
? SliderThemeData(
|
||||
inactiveTrackColor:
|
||||
state.appColor.shade200.withOpacity(0.5),
|
||||
)
|
||||
: null,
|
||||
outlinedButtonTheme: state.material3Enabled
|
||||
? OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
side: MaterialStateBorderSide.resolveWith(
|
||||
(_) => const BorderSide(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
brightness: isDarkModeEnabled
|
||||
? Brightness.dark
|
||||
: Brightness.light,
|
||||
seedColor: state.appColor,
|
||||
),
|
||||
fontFamily: state.font.name,
|
||||
dividerTheme: DividerThemeData(
|
||||
color: Palette.grey.withOpacity(0.2),
|
||||
),
|
||||
switchTheme: SwitchThemeData(
|
||||
trackColor: MaterialStateProperty.resolveWith(
|
||||
(Set<MaterialState> states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return colorScheme.primary.withOpacity(0.6);
|
||||
} else {
|
||||
return Palette.grey.withOpacity(0.2);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
bottomSheetTheme: const BottomSheetThemeData(
|
||||
modalElevation: 8,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shadowColor: Palette.black,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: isDarkModeEnabled
|
||||
? Palette.white
|
||||
: Palette.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
sliderTheme: SliderThemeData(
|
||||
inactiveTrackColor:
|
||||
colorScheme.primary.withOpacity(0.5),
|
||||
activeTrackColor: colorScheme.primary,
|
||||
thumbColor: colorScheme.primary,
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: ButtonStyle(
|
||||
side: MaterialStateBorderSide.resolveWith(
|
||||
(_) => const BorderSide(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
routerConfig: router,
|
||||
),
|
||||
|
@ -6,4 +6,7 @@ enum CommentsOrder {
|
||||
const CommentsOrder(this.description);
|
||||
|
||||
final String description;
|
||||
|
||||
@override
|
||||
String toString() => description;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ enum DiscoverableFeature {
|
||||
addStoryToFavList(
|
||||
featureId: 'add_story_to_fav_list',
|
||||
title: 'Fav a Story',
|
||||
description: '''Add it to your favorites''',
|
||||
description: '''Add it to your favorites.''',
|
||||
),
|
||||
openStoryInWebView(
|
||||
featureId: 'open_story_in_web_view',
|
||||
|
@ -5,4 +5,7 @@ enum FetchMode {
|
||||
const FetchMode(this.description);
|
||||
|
||||
final String description;
|
||||
|
||||
@override
|
||||
String toString() => description;
|
||||
}
|
||||
|
@ -3,26 +3,11 @@ enum Font {
|
||||
robotoSlab('Roboto Slab', isSerif: true),
|
||||
ubuntu('Ubuntu'),
|
||||
ubuntuMono('Ubuntu Mono'),
|
||||
notoSerif('Noto Serif', isSerif: true);
|
||||
notoSerif('Noto Serif', isSerif: true),
|
||||
exo2('Exo 2');
|
||||
|
||||
const Font(this.uiLabel, {this.isSerif = false});
|
||||
|
||||
final String uiLabel;
|
||||
final bool isSerif;
|
||||
|
||||
static Font fromString(String? val) {
|
||||
switch (val) {
|
||||
case 'robotoSlab':
|
||||
return Font.robotoSlab;
|
||||
case 'ubuntu':
|
||||
return Font.ubuntu;
|
||||
case 'ubuntuMono':
|
||||
return Font.ubuntuMono;
|
||||
case 'notoSerif':
|
||||
return Font.notoSerif;
|
||||
case 'roboto':
|
||||
default:
|
||||
return Font.roboto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,8 +45,6 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
||||
const SwipeGesturePreference(),
|
||||
const HapticFeedbackPreference(),
|
||||
const EyeCandyModePreference(),
|
||||
const TrueDarkModePreference(),
|
||||
const Material3Preference(),
|
||||
],
|
||||
);
|
||||
|
||||
@ -70,7 +68,6 @@ const bool _notificationModeDefaultValue = true;
|
||||
const bool _swipeGestureModeDefaultValue = false;
|
||||
const bool _displayModeDefaultValue = true;
|
||||
const bool _eyeCandyModeDefaultValue = false;
|
||||
const bool _trueDarkModeDefaultValue = false;
|
||||
const bool _hapticFeedbackModeDefaultValue = true;
|
||||
const bool _readerModeDefaultValue = true;
|
||||
const bool _markReadStoriesModeDefaultValue = true;
|
||||
@ -79,14 +76,13 @@ const bool _storyUrlModeDefaultValue = true;
|
||||
const bool _collapseModeDefaultValue = true;
|
||||
const bool _autoScrollModeDefaultValue = false;
|
||||
const bool _customTabModeDefaultValue = false;
|
||||
const bool _material3ModeDefaultValue = false;
|
||||
const bool _paginationModeDefaultValue = false;
|
||||
const double _textScaleFactorDefaultValue = 1;
|
||||
final int _fetchModeDefaultValue = FetchMode.eager.index;
|
||||
final int _commentsOrderDefaultValue = CommentsOrder.natural.index;
|
||||
final int _fontSizeDefaultValue = FontSize.regular.index;
|
||||
final int _appColorDefaultValue = materialColors.indexOf(Palette.deepOrange);
|
||||
final int _fontDefaultValue = Font.roboto.index;
|
||||
final int _fontDefaultValue = Font.robotoSlab.index;
|
||||
final int _tabOrderDefaultValue =
|
||||
StoryType.convertToSettingsValue(StoryType.values);
|
||||
final int _markStoriesAsReadWhenPreferenceDefaultValue =
|
||||
@ -312,26 +308,6 @@ class ManualPaginationPreference extends BooleanPreference {
|
||||
String get subtitle => '''so you can get stuff done.''';
|
||||
}
|
||||
|
||||
class Material3Preference extends BooleanPreference {
|
||||
const Material3Preference({bool? val})
|
||||
: super(val: val ?? _material3ModeDefaultValue);
|
||||
|
||||
@override
|
||||
Material3Preference copyWith({required bool? val}) {
|
||||
return Material3Preference(val: val);
|
||||
}
|
||||
|
||||
@override
|
||||
String get key => 'material3Mode';
|
||||
|
||||
@override
|
||||
String get title => 'Material 3';
|
||||
|
||||
@override
|
||||
String get subtitle =>
|
||||
'''experimental feature. Please open an issue on GitHub if you notice anything weird.''';
|
||||
}
|
||||
|
||||
/// Whether or not to use Custom Tabs for launching URLs.
|
||||
/// If false, default browser will be used.
|
||||
///
|
||||
@ -359,25 +335,6 @@ class CustomTabPreference extends BooleanPreference {
|
||||
bool get isDisplayable => Platform.isAndroid;
|
||||
}
|
||||
|
||||
class TrueDarkModePreference extends BooleanPreference {
|
||||
const TrueDarkModePreference({bool? val})
|
||||
: super(val: val ?? _trueDarkModeDefaultValue);
|
||||
|
||||
@override
|
||||
TrueDarkModePreference copyWith({required bool? val}) {
|
||||
return TrueDarkModePreference(val: val);
|
||||
}
|
||||
|
||||
@override
|
||||
String get key => 'trueDarkMode';
|
||||
|
||||
@override
|
||||
String get title => 'True Dark Mode';
|
||||
|
||||
@override
|
||||
String get subtitle => 'real dark.';
|
||||
}
|
||||
|
||||
class HapticFeedbackPreference extends BooleanPreference {
|
||||
const HapticFeedbackPreference({bool? val})
|
||||
: super(val: val ?? _hapticFeedbackModeDefaultValue);
|
||||
|
@ -223,6 +223,9 @@ class HackerNewsRepository {
|
||||
|
||||
/// Fetch a list of [Comment] based on ids and return results
|
||||
/// using a stream.
|
||||
///
|
||||
/// this function caches every comment fetched to [SembastRepository] so that
|
||||
/// we don't need to parse the text again later.
|
||||
Stream<Comment> fetchCommentsStream({
|
||||
required List<int> ids,
|
||||
int level = 0,
|
||||
@ -258,6 +261,9 @@ class HackerNewsRepository {
|
||||
|
||||
/// Fetch a list of [Comment] based on ids recursively and
|
||||
/// return results using a stream.
|
||||
///
|
||||
/// this function caches every comment fetched to [SembastRepository] so that
|
||||
/// we don't need to parse the text again later.
|
||||
Stream<Comment> fetchAllCommentsRecursivelyStream({
|
||||
required List<int> ids,
|
||||
int level = 0,
|
||||
|
45
lib/repositories/hacker_news_web_repository.dart
Normal file
@ -0,0 +1,45 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:http/http.dart';
|
||||
|
||||
/// For fetching anything that cannot be fetched through Hacker News API.
|
||||
class HackerNewsWebRepository {
|
||||
HackerNewsWebRepository();
|
||||
|
||||
static const String _favoritesBaseUrl =
|
||||
'https://news.ycombinator.com/favorites?id=';
|
||||
static const String _aThingSelector =
|
||||
'#hnmain > tbody > tr:nth-child(3) > td > table > tbody > .athing';
|
||||
|
||||
Future<Iterable<int>> fetchFavorites({required String of}) async {
|
||||
final String username = of;
|
||||
final List<int> allIds = <int>[];
|
||||
int page = 0;
|
||||
|
||||
Future<Iterable<int>> fetchIds(int page) async {
|
||||
final Uri url = Uri.parse('$_favoritesBaseUrl$username&p=$page');
|
||||
final Response response = await get(url);
|
||||
final Document document = parse(response.body);
|
||||
final List<Element> elements = document.querySelectorAll(_aThingSelector);
|
||||
final Iterable<int> parsedIds = elements
|
||||
.map(
|
||||
(Element e) => int.tryParse(e.id),
|
||||
)
|
||||
.whereNotNull();
|
||||
return parsedIds;
|
||||
}
|
||||
|
||||
Iterable<int> ids;
|
||||
while (true) {
|
||||
ids = await fetchIds(page);
|
||||
if (ids.isEmpty) {
|
||||
break;
|
||||
}
|
||||
allIds.addAll(ids);
|
||||
page++;
|
||||
}
|
||||
|
||||
return allIds;
|
||||
}
|
||||
}
|
@ -185,7 +185,9 @@ class OfflineRepository {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
final Comment comment = Comment.fromJson(json.cast<String, dynamic>());
|
||||
final Map<String, dynamic> typedJson = json.cast<String, dynamic>();
|
||||
typedJson['fromCache'] = true;
|
||||
final Comment comment = Comment.fromJson(typedJson);
|
||||
return comment;
|
||||
} catch (_) {
|
||||
_logger.e(_);
|
||||
@ -204,8 +206,9 @@ class OfflineRepository {
|
||||
final Map<dynamic, dynamic>? json = await box.get(id.toString());
|
||||
|
||||
if (json != null) {
|
||||
final Comment comment =
|
||||
Comment.fromJson(json.cast<String, dynamic>(), level: level);
|
||||
final Map<String, dynamic> typedJson = json.cast<String, dynamic>();
|
||||
typedJson['fromCache'] = true;
|
||||
final Comment comment = Comment.fromJson(typedJson, level: level);
|
||||
|
||||
yield comment;
|
||||
yield* getCachedCommentsStream(ids: comment.kids, level: level + 1);
|
||||
|
@ -157,7 +157,6 @@ class PreferenceRepository {
|
||||
((prefs.getStringList(_getFavKey('')) ?? <String>[])
|
||||
..addAll(prefs.getStringList(_getFavKey(of)) ?? <String>[]))
|
||||
.map(int.parse)
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
return favList;
|
||||
@ -175,7 +174,7 @@ class PreferenceRepository {
|
||||
|
||||
await _syncedPrefs.setStringList(
|
||||
key: key,
|
||||
val: favList.map((int e) => e.toString()).toSet().toList(),
|
||||
val: favList.map((int e) => e.toString()).toList(),
|
||||
);
|
||||
} else {
|
||||
final SharedPreferences prefs = await _prefs;
|
||||
@ -186,7 +185,30 @@ class PreferenceRepository {
|
||||
|
||||
await prefs.setStringList(
|
||||
key,
|
||||
favList.map((int e) => e.toString()).toSet().toList(),
|
||||
favList.map((int e) => e.toString()).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> overwriteFav({
|
||||
required String username,
|
||||
required Iterable<int> ids,
|
||||
}) async {
|
||||
final String key = _getFavKey(username);
|
||||
final List<String> favList =
|
||||
ids.map((int e) => e.toString()).toList(growable: false);
|
||||
|
||||
if (Platform.isIOS) {
|
||||
await _syncedPrefs.setStringList(
|
||||
key: key,
|
||||
val: favList,
|
||||
);
|
||||
} else {
|
||||
final SharedPreferences prefs = await _prefs;
|
||||
|
||||
await prefs.setStringList(
|
||||
key,
|
||||
favList,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
export 'auth_repository.dart';
|
||||
export 'hacker_news_repository.dart';
|
||||
export 'hacker_news_web_repository.dart';
|
||||
export 'offline_repository.dart';
|
||||
export 'post_repository.dart';
|
||||
export 'preference_repository.dart';
|
||||
|
@ -51,7 +51,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
DeviceScreenType.mobile) {
|
||||
locator.get<Logger>().i('Resetting comments in CommentCache');
|
||||
Future<void>.delayed(
|
||||
Durations.ms500,
|
||||
AppDurations.ms500,
|
||||
locator.get<CommentCache>().resetComments,
|
||||
);
|
||||
}
|
||||
@ -141,15 +141,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).padding.top - Dimens.pt8,
|
||||
),
|
||||
Theme(
|
||||
data: ThemeData(
|
||||
highlightColor: Palette.transparent,
|
||||
splashColor: Palette.transparent,
|
||||
primaryColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: CustomTabBar(
|
||||
tabController: tabController,
|
||||
),
|
||||
CustomTabBar(
|
||||
tabController: tabController,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -45,7 +45,8 @@ class PinnedStories extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
child: ColoredBox(
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.2),
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.2),
|
||||
child: StoryTile(
|
||||
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
|
||||
story: story,
|
||||
@ -61,7 +62,7 @@ class PinnedStories extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Dimens.pt12),
|
||||
child: Divider(
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.8),
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -37,7 +37,7 @@ class TabletHomeScreen extends StatelessWidget {
|
||||
top: Dimens.zero,
|
||||
bottom: Dimens.zero,
|
||||
width: homeScreenWidth,
|
||||
duration: Durations.ms300,
|
||||
duration: AppDurations.ms300,
|
||||
curve: Curves.elasticOut,
|
||||
child: homeScreen,
|
||||
),
|
||||
@ -53,7 +53,7 @@ class TabletHomeScreen extends StatelessWidget {
|
||||
top: Dimens.zero,
|
||||
bottom: Dimens.zero,
|
||||
left: state.expanded ? Dimens.zero : homeScreenWidth,
|
||||
duration: Durations.ms300,
|
||||
duration: AppDurations.ms300,
|
||||
curve: Curves.elasticOut,
|
||||
child: const _TabletStoryView(),
|
||||
),
|
||||
|
@ -92,15 +92,15 @@ class ItemScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
static Widget tablet(BuildContext context, ItemScreenArgs args) {
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
return PopScope(
|
||||
canPop: () {
|
||||
if (context.read<SplitViewCubit>().state.expanded) {
|
||||
context.read<SplitViewCubit>().zoom();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
}(),
|
||||
child: RepositoryProvider<CollapseCache>(
|
||||
create: (_) => CollapseCache(),
|
||||
lazy: false,
|
||||
@ -160,9 +160,9 @@ class _ItemScreenState extends State<ItemScreen>
|
||||
final GlobalKey fontSizeIconButtonKey = GlobalKey();
|
||||
StreamSubscription<double>? scrollOffsetSubscription;
|
||||
|
||||
static const Duration _storyLinkTapThrottleDelay = Durations.twoSeconds;
|
||||
static const Duration _storyLinkTapThrottleDelay = AppDurations.twoSeconds;
|
||||
static const Duration _featureDiscoveryDismissThrottleDelay =
|
||||
Durations.oneSecond;
|
||||
AppDurations.oneSecond;
|
||||
|
||||
@override
|
||||
void didPop() {
|
||||
@ -300,6 +300,7 @@ class _ItemScreenState extends State<ItemScreen>
|
||||
left: Dimens.zero,
|
||||
right: Dimens.zero,
|
||||
child: CustomAppBar(
|
||||
context: context,
|
||||
backgroundColor: Theme.of(context)
|
||||
.canvasColor
|
||||
.withOpacity(0.6),
|
||||
@ -342,6 +343,7 @@ class _ItemScreenState extends State<ItemScreen>
|
||||
extendBodyBehindAppBar: true,
|
||||
resizeToAvoidBottomInset: true,
|
||||
appBar: CustomAppBar(
|
||||
context: context,
|
||||
backgroundColor:
|
||||
Theme.of(context).canvasColor.withOpacity(0.6),
|
||||
foregroundColor: Theme.of(context).iconTheme.color,
|
||||
@ -413,7 +415,7 @@ class _ItemScreenState extends State<ItemScreen>
|
||||
fontSize: fontSize.fontSize,
|
||||
color:
|
||||
context.read<PreferenceCubit>().state.fontSize == fontSize
|
||||
? Theme.of(context).primaryColor
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
|
@ -7,6 +7,7 @@ import 'package:hacki/utils/utils.dart';
|
||||
|
||||
class CustomAppBar extends AppBar {
|
||||
CustomAppBar({
|
||||
required BuildContext context,
|
||||
required Item item,
|
||||
required super.backgroundColor,
|
||||
required super.foregroundColor,
|
||||
@ -44,8 +45,9 @@ class CustomAppBar extends AppBar {
|
||||
fontSize: TextDimens.pt18,
|
||||
fontFamily: FeatherIcons.type.fontFamily,
|
||||
package: FeatherIcons.type.fontPackage,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
textScaler: TextScaler.noScaling,
|
||||
),
|
||||
onPressed: onFontSizeTap,
|
||||
),
|
||||
|
@ -26,7 +26,7 @@ class CustomFloatingActionButton extends StatelessWidget {
|
||||
bottom: Dimens.replyBoxCollapsedHeight,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
duration: Durations.ms200,
|
||||
duration: AppDurations.ms200,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
|
@ -30,8 +30,8 @@ class FavIconButton extends StatelessWidget {
|
||||
child: Icon(
|
||||
isFav ? Icons.favorite : Icons.favorite_border,
|
||||
color: isFav
|
||||
? Theme.of(context).primaryColor
|
||||
: Theme.of(context).iconTheme.color,
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
|
@ -110,14 +110,14 @@ class _InThreadSearchViewState extends State<_InThreadSearchView> {
|
||||
child: TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
cursorColor: Theme.of(context).primaryColor,
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search in this thread',
|
||||
suffixText: '${state.matchedComments.length} results',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -40,7 +40,7 @@ class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
|
||||
width: Dimens.pt36,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -51,13 +51,14 @@ class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
|
||||
),
|
||||
child: TextField(
|
||||
controller: usernameController,
|
||||
cursorColor: Theme.of(context).primaryColor,
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Username',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).primaryColor),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -71,14 +72,15 @@ class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
|
||||
),
|
||||
child: TextField(
|
||||
controller: passwordController,
|
||||
cursorColor: Theme.of(context).primaryColor,
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
obscureText: true,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Password',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).primaryColor),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -110,7 +112,7 @@ class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
|
||||
? Icons.check_box
|
||||
: Icons.check_box_outline_blank,
|
||||
color: state.agreedToEULA
|
||||
? Theme.of(context).primaryColor
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Palette.grey,
|
||||
),
|
||||
onPressed: () =>
|
||||
@ -136,7 +138,7 @@ class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
|
||||
child: Text(
|
||||
'End User Agreement',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@ -182,15 +184,15 @@ class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
state.agreedToEULA
|
||||
? Theme.of(context).primaryColor
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Palette.grey,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Log in',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Palette.white,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -190,9 +190,6 @@ class _ParentItemSection extends StatelessWidget {
|
||||
final void Function(Item item, Rect? rect) onMoreTapped;
|
||||
final ValueChanged<Comment> onRightMoreTapped;
|
||||
|
||||
static const double _viewParentButtonWidth = 100;
|
||||
static const double _viewRootButtonWidth = 85;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Item item = state.item;
|
||||
@ -221,14 +218,14 @@ class _ParentItemSection extends StatelessWidget {
|
||||
}
|
||||
context.read<EditCubit>().onReplyTapped(item);
|
||||
},
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
icon: Icons.message,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: (BuildContext context) =>
|
||||
onMoreTapped(item, context.rect),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
icon: Icons.more_horiz,
|
||||
),
|
||||
@ -246,10 +243,9 @@ class _ParentItemSection extends StatelessWidget {
|
||||
Text(
|
||||
item.by,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
textScaler: MediaQuery.of(context).textScaler,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
@ -257,8 +253,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
textScaler: MediaQuery.of(context).textScaler,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -306,7 +301,7 @@ class _ParentItemSection extends StatelessWidget {
|
||||
left: Dimens.pt6,
|
||||
right: Dimens.pt6,
|
||||
bottom: Dimens.pt12,
|
||||
top: Dimens.pt12,
|
||||
top: Dimens.pt6,
|
||||
),
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
@ -318,7 +313,9 @@ class _ParentItemSection extends StatelessWidget {
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: fontSize,
|
||||
color: item.url.isNotEmpty
|
||||
? Theme.of(context).primaryColor
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
@ -328,15 +325,15 @@ class _ParentItemSection extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: fontSize - 4,
|
||||
color:
|
||||
Theme.of(context).primaryColor,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
textScaler: MediaQuery.of(context).textScaler,
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -354,8 +351,8 @@ class _ParentItemSection extends StatelessWidget {
|
||||
),
|
||||
child: ItemText(
|
||||
item: item,
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
textScaler:
|
||||
MediaQuery.of(context).textScaler,
|
||||
selectable: true,
|
||||
),
|
||||
),
|
||||
@ -393,29 +390,31 @@ class _ParentItemSection extends StatelessWidget {
|
||||
height: Dimens.zero,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (item is Story) ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
Text(
|
||||
'''${item.score} karma, ${item.descendants} comment${item.descendants > 1 ? 's' : ''}''',
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
if (item is Story) ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt12,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
SizedBox(
|
||||
width: _viewParentButtonWidth,
|
||||
child: TextButton(
|
||||
onPressed: context.read<CommentsCubit>().loadParentThread,
|
||||
child:
|
||||
state.fetchParentStatus == CommentsStatus.inProgress
|
||||
Text(
|
||||
'''${item.score} karma, ${item.descendants} cmt${item.descendants > 1 ? 's' : ''}''',
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
textScaler: MediaQuery.of(context).clampedTextScaler,
|
||||
),
|
||||
] else ...<Widget>[
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
BlocSelector<CommentsCubit, CommentsState, CommentsStatus>(
|
||||
selector: (CommentsState state) =>
|
||||
state.fetchParentStatus,
|
||||
builder: (BuildContext context, CommentsStatus status) {
|
||||
return TextButton(
|
||||
onPressed:
|
||||
context.read<CommentsCubit>().loadParentThread,
|
||||
child: status == CommentsStatus.inProgress
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
@ -423,84 +422,73 @@ class _ParentItemSection extends StatelessWidget {
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'View parent',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
: Text(
|
||||
'View Parent',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
textScaler:
|
||||
MediaQuery.of(context).clampedTextScaler,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
BlocSelector<CommentsCubit, CommentsState, CommentsStatus>(
|
||||
selector: (CommentsState state) => state.fetchRootStatus,
|
||||
builder: (BuildContext context, CommentsStatus status) {
|
||||
return TextButton(
|
||||
onPressed:
|
||||
context.read<CommentsCubit>().loadRootThread,
|
||||
child: status == CommentsStatus.inProgress
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'View Root',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.labelLarge
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
textScaler:
|
||||
MediaQuery.of(context).clampedTextScaler,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
if (!state.isOfflineReading)
|
||||
CustomDropdownMenu<FetchMode>(
|
||||
menuChildren: FetchMode.values,
|
||||
onSelected: context.read<CommentsCubit>().updateFetchMode,
|
||||
selected: state.fetchMode,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt6,
|
||||
),
|
||||
SizedBox(
|
||||
width: _viewRootButtonWidth,
|
||||
child: TextButton(
|
||||
onPressed: context.read<CommentsCubit>().loadRootThread,
|
||||
child: state.fetchRootStatus == CommentsStatus.inProgress
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child: CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'View root',
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
),
|
||||
CustomDropdownMenu<CommentsOrder>(
|
||||
menuChildren: CommentsOrder.values,
|
||||
onSelected: context.read<CommentsCubit>().updateOrder,
|
||||
selected: state.order,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
if (!state.isOfflineReading)
|
||||
DropdownButton<FetchMode>(
|
||||
value: state.fetchMode,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: FetchMode.values
|
||||
.map(
|
||||
(FetchMode val) => DropdownMenuItem<FetchMode>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().updateFetchMode,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt6,
|
||||
),
|
||||
DropdownButton<CommentsOrder>(
|
||||
value: state.order,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: CommentsOrder.values
|
||||
.map(
|
||||
(CommentsOrder val) => DropdownMenuItem<CommentsOrder>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt13,
|
||||
),
|
||||
textScaleFactor: 1,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: context.read<CommentsCubit>().updateOrder,
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: Dimens.zero,
|
||||
|
@ -83,7 +83,7 @@ class MorePopupMenu extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
AnimatedCrossFade(
|
||||
alignment: Alignment.center,
|
||||
duration: Durations.ms300,
|
||||
duration: AppDurations.ms300,
|
||||
crossFadeState: state.status.isLoading
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
@ -137,7 +137,9 @@ class MorePopupMenu extends StatelessWidget {
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: fontSize,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
onOpen: (LinkableElement link) =>
|
||||
LinkUtil.launch(
|
||||
@ -182,12 +184,12 @@ class MorePopupMenu extends StatelessWidget {
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
FeatherIcons.chevronUp,
|
||||
color: upvoted ? Theme.of(context).primaryColor : null,
|
||||
color: upvoted ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
title: Text(
|
||||
upvoted ? 'Upvoted' : 'Upvote',
|
||||
style: upvoted
|
||||
? TextStyle(color: Theme.of(context).primaryColor)
|
||||
? TextStyle(color: Theme.of(context).colorScheme.primary)
|
||||
: null,
|
||||
),
|
||||
subtitle: item is Story ? Text(item.score.toString()) : null,
|
||||
@ -196,12 +198,13 @@ class MorePopupMenu extends StatelessWidget {
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
FeatherIcons.chevronDown,
|
||||
color: downvoted ? Theme.of(context).primaryColor : null,
|
||||
color:
|
||||
downvoted ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
title: Text(
|
||||
downvoted ? 'Downvoted' : 'Downvote',
|
||||
style: downvoted
|
||||
? TextStyle(color: Theme.of(context).primaryColor)
|
||||
? TextStyle(color: Theme.of(context).colorScheme.primary)
|
||||
: null,
|
||||
),
|
||||
onTap: context.read<VoteCubit>().downvote,
|
||||
@ -212,7 +215,8 @@ class MorePopupMenu extends StatelessWidget {
|
||||
return ListTile(
|
||||
leading: Icon(
|
||||
isFav ? Icons.favorite : Icons.favorite_border,
|
||||
color: isFav ? Theme.of(context).primaryColor : null,
|
||||
color:
|
||||
isFav ? Theme.of(context).colorScheme.primary : null,
|
||||
),
|
||||
title: Text(
|
||||
isFav ? 'Unfavorite' : 'Favorite',
|
||||
|
@ -37,8 +37,8 @@ class PinIconButton extends StatelessWidget {
|
||||
child: Icon(
|
||||
pinned ? Icons.push_pin : Icons.push_pin_outlined,
|
||||
color: pinned
|
||||
? Theme.of(context).primaryColor
|
||||
: Theme.of(context).iconTheme.color,
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
|
@ -106,7 +106,7 @@ class _PollViewState extends State<PollView> with ItemActionMixin {
|
||||
icon: Icon(
|
||||
Icons.arrow_drop_up,
|
||||
color: voteState.vote == Vote.up
|
||||
? Theme.of(context).primaryColor
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Palette.grey,
|
||||
size: TextDimens.pt36,
|
||||
),
|
||||
@ -130,7 +130,8 @@ class _PollViewState extends State<PollView> with ItemActionMixin {
|
||||
),
|
||||
LinearProgressIndicator(
|
||||
value: option.ratio,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -73,7 +73,7 @@ class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
|
||||
height: editState.showReplyBox
|
||||
? (expanded ? expandedHeight : collapsedHeight)
|
||||
: Dimens.zero,
|
||||
duration: Durations.ms200,
|
||||
duration: AppDurations.ms200,
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: <BoxShadow>[
|
||||
if (!context.read<SplitViewCubit>().state.enabled &&
|
||||
@ -97,7 +97,7 @@ class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
|
||||
),
|
||||
AnimatedContainer(
|
||||
height: expanded ? Dimens.pt40 : Dimens.zero,
|
||||
duration: Durations.ms300,
|
||||
duration: AppDurations.ms300,
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
@ -125,12 +125,13 @@ class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
|
||||
AnimatedOpacity(
|
||||
opacity:
|
||||
expanded ? NumSwitch.on : NumSwitch.off,
|
||||
duration: Durations.ms300,
|
||||
duration: AppDurations.ms300,
|
||||
child: IconButton(
|
||||
key: const Key('quote'),
|
||||
icon: Icon(
|
||||
FeatherIcons.code,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
size: TextDimens.pt18,
|
||||
),
|
||||
onPressed: expanded ? showTextPopup : null,
|
||||
@ -142,7 +143,7 @@ class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
|
||||
expanded
|
||||
? FeatherIcons.minimize2
|
||||
: FeatherIcons.maximize2,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: TextDimens.pt18,
|
||||
),
|
||||
onPressed: () {
|
||||
@ -156,7 +157,7 @@ class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
|
||||
key: const Key('close'),
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@ -209,7 +210,7 @@ class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
|
||||
height: Dimens.pt24,
|
||||
width: Dimens.pt24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
),
|
||||
@ -219,7 +220,7 @@ class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
|
||||
key: const Key('send'),
|
||||
icon: Icon(
|
||||
Icons.send,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
widget.onSendTapped();
|
||||
@ -349,7 +350,7 @@ class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.close,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: TextDimens.pt18,
|
||||
),
|
||||
onPressed: () => context.pop(),
|
||||
@ -370,8 +371,7 @@ class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
|
||||
child: ItemText(
|
||||
item: replyingTo,
|
||||
selectable: true,
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
textScaler: MediaQuery.of(context).textScaler,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -31,7 +31,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
final RefreshController refreshControllerFav = RefreshController();
|
||||
final RefreshController refreshControllerNotification = RefreshController();
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final Throttle throttle = Throttle(delay: Durations.twoSeconds);
|
||||
final Throttle throttle = Throttle(delay: AppDurations.twoSeconds);
|
||||
|
||||
PageType? pageType;
|
||||
|
||||
@ -138,13 +138,46 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
..loadComplete();
|
||||
}
|
||||
},
|
||||
buildWhen: (FavState previous, FavState current) =>
|
||||
previous.favItems.length != current.favItems.length,
|
||||
builder: (BuildContext context, FavState favState) {
|
||||
Widget? header() => authState.isLoggedIn
|
||||
? BlocSelector<FavCubit, FavState, Status>(
|
||||
selector: (FavState state) => state.mergeStatus,
|
||||
builder: (
|
||||
BuildContext context,
|
||||
Status status,
|
||||
) =>
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<FavCubit>().merge();
|
||||
},
|
||||
child: status == Status.inProgress
|
||||
? const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
width: Dimens.pt12,
|
||||
child:
|
||||
CustomCircularProgressIndicator(
|
||||
strokeWidth: Dimens.pt2,
|
||||
),
|
||||
)
|
||||
: const Text('Sync from Hacker News'),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
if (favState.favItems.isEmpty &&
|
||||
favState.status != Status.inProgress) {
|
||||
return const CenteredMessageView(
|
||||
content: 'Your favorite stories will show up here.'
|
||||
'\nThey will be synced to your Hacker '
|
||||
'News account if you are logged in.',
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
header() ?? const SizedBox.shrink(),
|
||||
const CenteredMessageView(
|
||||
content:
|
||||
'Your favorite stories will show up here.'
|
||||
'\nThey will be synced to your Hacker '
|
||||
'News account if you are logged in.',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -181,6 +214,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
onTap: (Item item) => goToItemScreen(
|
||||
args: ItemScreenArgs(item: item),
|
||||
),
|
||||
header: header(),
|
||||
itemBuilder: (Widget child, Item item) {
|
||||
return Slidable(
|
||||
dragStartBehavior: DragStartBehavior.start,
|
||||
|
@ -28,9 +28,6 @@ class InboxView extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color textColor = Theme.of(context).brightness == Brightness.dark
|
||||
? Palette.white
|
||||
: Palette.black;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
if (unreadCommentsIds.isNotEmpty)
|
||||
@ -42,7 +39,8 @@ class InboxView extends StatelessWidget {
|
||||
child: SmartRefresher(
|
||||
enablePullUp: true,
|
||||
header: WaterDropMaterialHeader(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
footer: CustomFooter(
|
||||
loadStyle: LoadStyle.ShowWhenLoading,
|
||||
@ -104,13 +102,16 @@ class InboxView extends StatelessWidget {
|
||||
text: e.text,
|
||||
style: TextStyle(
|
||||
color: unreadCommentsIds.contains(e.id)
|
||||
? textColor
|
||||
: Palette.grey,
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
: Theme.of(context).readGrey,
|
||||
fontSize: TextDimens.pt16,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.primaryColor
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(
|
||||
unreadCommentsIds.contains(e.id)
|
||||
? 1
|
||||
|
@ -41,12 +41,12 @@ class OfflineListTile extends StatelessWidget {
|
||||
} else if (downloaded) {
|
||||
return Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
}
|
||||
return Icon(
|
||||
Icons.download,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
}();
|
||||
|
||||
|
@ -82,34 +82,10 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
|
||||
const SizedBox(
|
||||
height: Dimens.pt8,
|
||||
),
|
||||
const Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
width: Dimens.pt16,
|
||||
),
|
||||
Text('Default fetch mode'),
|
||||
Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text('Default comments order'),
|
||||
Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Row(
|
||||
@ -117,75 +93,73 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
|
||||
const SizedBox(
|
||||
width: Dimens.pt16,
|
||||
),
|
||||
DropdownButton<FetchMode>(
|
||||
value: preferenceState.fetchMode,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: FetchMode.values
|
||||
.map(
|
||||
(FetchMode val) =>
|
||||
DropdownMenuItem<FetchMode>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt16,
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text('Default fetch mode'),
|
||||
DropdownMenu<FetchMode>(
|
||||
initialSelection: preferenceState.fetchMode,
|
||||
dropdownMenuEntries: FetchMode.values
|
||||
.map(
|
||||
(FetchMode val) =>
|
||||
DropdownMenuEntry<FetchMode>(
|
||||
value: val,
|
||||
label: val.description,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (FetchMode? fetchMode) {
|
||||
if (fetchMode != null) {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<PreferenceCubit>().update(
|
||||
FetchModePreference(
|
||||
val: fetchMode.index,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
onSelected: (FetchMode? fetchMode) {
|
||||
if (fetchMode != null) {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<PreferenceCubit>().update(
|
||||
FetchModePreference(
|
||||
val: fetchMode.index,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
DropdownButton<CommentsOrder>(
|
||||
value: preferenceState.order,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: CommentsOrder.values
|
||||
.map(
|
||||
(CommentsOrder val) =>
|
||||
DropdownMenuItem<CommentsOrder>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.description,
|
||||
style: const TextStyle(
|
||||
fontSize: TextDimens.pt16,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Text('Default comments order'),
|
||||
DropdownMenu<CommentsOrder>(
|
||||
initialSelection: preferenceState.order,
|
||||
dropdownMenuEntries: CommentsOrder.values
|
||||
.map(
|
||||
(CommentsOrder val) =>
|
||||
DropdownMenuEntry<CommentsOrder>(
|
||||
value: val,
|
||||
label: val.description,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onSelected: (CommentsOrder? order) {
|
||||
if (order != null) {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<PreferenceCubit>().update(
|
||||
CommentsOrderPreference(
|
||||
val: order.index,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (CommentsOrder? order) {
|
||||
if (order != null) {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<PreferenceCubit>().update(
|
||||
CommentsOrderPreference(
|
||||
val: order.index,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
width: Dimens.pt16,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: Dimens.pt12,
|
||||
),
|
||||
const TabBarSettings(),
|
||||
const TextScaleFactorSettings(),
|
||||
const Divider(),
|
||||
@ -228,41 +202,19 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
|
||||
.add(ClearAllReadStories());
|
||||
}
|
||||
},
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
if (preference
|
||||
is MarkReadStoriesModePreference) ...<Widget>[
|
||||
ListTile(
|
||||
title: Text(
|
||||
StoryMarkingModePreference().title,
|
||||
style: TextStyle(
|
||||
color: !preferenceState.markReadStoriesEnabled
|
||||
? Palette.grey
|
||||
: null,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Dimens.pt16,
|
||||
),
|
||||
trailing: DropdownButton<StoryMarkingMode>(
|
||||
value: preferenceState.storyMarkingMode,
|
||||
underline: const SizedBox.shrink(),
|
||||
items: StoryMarkingMode.values
|
||||
.map(
|
||||
(StoryMarkingMode val) =>
|
||||
DropdownMenuItem<StoryMarkingMode>(
|
||||
value: val,
|
||||
child: Text(
|
||||
val.label,
|
||||
style: TextStyle(
|
||||
fontSize: TextDimens.pt16,
|
||||
color: !preferenceState
|
||||
.markReadStoriesEnabled
|
||||
? Palette.grey
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (StoryMarkingMode? storyMarkingMode) {
|
||||
child: DropdownMenu<StoryMarkingMode>(
|
||||
enabled: preferenceState.markReadStoriesEnabled,
|
||||
label: Text(StoryMarkingModePreference().title),
|
||||
initialSelection: preferenceState.storyMarkingMode,
|
||||
onSelected: (StoryMarkingMode? storyMarkingMode) {
|
||||
if (storyMarkingMode != null) {
|
||||
HapticFeedbackUtil.selection();
|
||||
context.read<PreferenceCubit>().update(
|
||||
@ -272,6 +224,23 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
|
||||
);
|
||||
}
|
||||
},
|
||||
dropdownMenuEntries: StoryMarkingMode.values
|
||||
.map(
|
||||
(StoryMarkingMode val) =>
|
||||
DropdownMenuEntry<StoryMarkingMode>(
|
||||
value: val,
|
||||
label: val.label,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
disabledBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
expandedInsets: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
@ -510,7 +479,7 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -14,7 +14,7 @@ class TextScaleFactorSettings extends StatelessWidget {
|
||||
previous.textScaleFactor != current.textScaleFactor,
|
||||
builder: (BuildContext context, PreferenceState state) {
|
||||
final String label = state.textScaleFactor == 1
|
||||
? '''system default ${MediaQuery.of(context).textScaleFactor.toStringAsPrecision(2)}'''
|
||||
? '''system default'''
|
||||
: state.textScaleFactor.toString();
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
|
@ -32,9 +32,9 @@ class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final TextEditingController textEditingController = TextEditingController();
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final Debouncer debouncer = Debouncer(delay: Durations.oneSecond);
|
||||
final Debouncer debouncer = Debouncer(delay: AppDurations.oneSecond);
|
||||
|
||||
static const Duration chipsAnimationDuration = Durations.ms300;
|
||||
static const Duration chipsAnimationDuration = AppDurations.ms300;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -76,7 +76,8 @@ class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
|
||||
enablePullDown: false,
|
||||
enablePullUp: true,
|
||||
header: WaterDropMaterialHeader(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
footer: CustomFooter(
|
||||
loadStyle: LoadStyle.ShowWhenLoading,
|
||||
@ -120,13 +121,16 @@ class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
|
||||
child: TextField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
cursorColor: Theme.of(context).primaryColor,
|
||||
cursorColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search Hacker News',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -48,13 +48,14 @@ class PostedByFilterChip extends StatelessWidget {
|
||||
),
|
||||
child: TextField(
|
||||
controller: usernameController,
|
||||
cursorColor: Theme.of(context).primaryColor,
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Username',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).primaryColor),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -87,7 +88,7 @@ class PostedByFilterChip extends StatelessWidget {
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context).primaryColor,
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
|
@ -103,7 +103,7 @@ class _SubmitScreenState extends State<SubmitScreen> with ItemActionMixin {
|
||||
height: Dimens.pt20,
|
||||
width: Dimens.pt20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
@ -112,7 +112,7 @@ class _SubmitScreenState extends State<SubmitScreen> with ItemActionMixin {
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.send,
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: context.read<SubmitCubit>().onSubmitTapped,
|
||||
)
|
||||
@ -134,14 +134,15 @@ class _SubmitScreenState extends State<SubmitScreen> with ItemActionMixin {
|
||||
),
|
||||
child: TextField(
|
||||
controller: titleEditingController,
|
||||
cursorColor: Theme.of(context).primaryColor,
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
autocorrect: false,
|
||||
maxLength: 80,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Title',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).primaryColor),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: context.read<SubmitCubit>().onTitleChanged,
|
||||
@ -154,13 +155,14 @@ class _SubmitScreenState extends State<SubmitScreen> with ItemActionMixin {
|
||||
child: TextField(
|
||||
enabled: textEditingController.text.isEmpty,
|
||||
controller: urlEditingController,
|
||||
cursorColor: Theme.of(context).primaryColor,
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
autocorrect: false,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Url',
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Theme.of(context).primaryColor),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: context.read<SubmitCubit>().onUrlChanged,
|
||||
@ -182,7 +184,7 @@ class _SubmitScreenState extends State<SubmitScreen> with ItemActionMixin {
|
||||
child: TextField(
|
||||
enabled: urlEditingController.text.isEmpty,
|
||||
controller: textEditingController,
|
||||
cursorColor: Theme.of(context).primaryColor,
|
||||
cursorColor: Theme.of(context).colorScheme.primary,
|
||||
maxLines: 200,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Text',
|
||||
|
@ -71,11 +71,12 @@ class CommentTile extends StatelessWidget {
|
||||
) {
|
||||
if (actionable && state.hidden) return const SizedBox.shrink();
|
||||
|
||||
final MaterialColor primaryColor =
|
||||
context.read<PreferenceCubit>().state.appColor;
|
||||
final Color primaryColor = Theme.of(context).colorScheme.primary;
|
||||
final Brightness brightness = Theme.of(context).brightness;
|
||||
final Color color = _getColor(
|
||||
level,
|
||||
primaryColor: primaryColor,
|
||||
brightness: brightness,
|
||||
);
|
||||
|
||||
final Widget child = DeviceGestureWrapper(
|
||||
@ -89,7 +90,8 @@ class CommentTile extends StatelessWidget {
|
||||
children: <Widget>[
|
||||
SlidableAction(
|
||||
onPressed: (_) => onReplyTapped?.call(comment),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
icon: Icons.message,
|
||||
@ -98,7 +100,8 @@ class CommentTile extends StatelessWidget {
|
||||
comment.by)
|
||||
SlidableAction(
|
||||
onPressed: (_) => onEditTapped?.call(comment),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
icon: Icons.edit,
|
||||
@ -109,7 +112,8 @@ class CommentTile extends StatelessWidget {
|
||||
comment,
|
||||
context.rect,
|
||||
),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
icon: Icons.more_horiz,
|
||||
@ -124,7 +128,8 @@ class CommentTile extends StatelessWidget {
|
||||
SlidableAction(
|
||||
onPressed: (_) =>
|
||||
onRightMoreTapped?.call(comment),
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
icon: Icons.av_timer,
|
||||
@ -133,6 +138,7 @@ class CommentTile extends StatelessWidget {
|
||||
)
|
||||
: null,
|
||||
child: InkWell(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
onTap: () {
|
||||
if (collapsable) {
|
||||
_collapse(context);
|
||||
@ -156,8 +162,7 @@ class CommentTile extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
color: primaryColor,
|
||||
),
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
textScaler: MediaQuery.of(context).textScaler,
|
||||
),
|
||||
if (comment.by == opUsername)
|
||||
Text(
|
||||
@ -172,8 +177,7 @@ class CommentTile extends StatelessWidget {
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
textScaler: MediaQuery.of(context).textScaler,
|
||||
),
|
||||
if (isResponse)
|
||||
const Padding(
|
||||
@ -184,7 +188,7 @@ class CommentTile extends StatelessWidget {
|
||||
color: Palette.grey,
|
||||
),
|
||||
),
|
||||
if (isNew)
|
||||
if (!comment.dead && isNew)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 4),
|
||||
child: Icon(
|
||||
@ -199,14 +203,13 @@ class CommentTile extends StatelessWidget {
|
||||
style: const TextStyle(
|
||||
color: Palette.grey,
|
||||
),
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor,
|
||||
textScaler: MediaQuery.of(context).textScaler,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedSize(
|
||||
duration: Durations.ms200,
|
||||
duration: AppDurations.ms200,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
@ -215,7 +218,8 @@ class CommentTile extends StatelessWidget {
|
||||
text:
|
||||
'''collapsed (${state.collapsedCount + 1})''',
|
||||
color: Theme.of(context)
|
||||
.primaryColor
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.8),
|
||||
)
|
||||
else if (comment.hidden)
|
||||
@ -243,8 +247,8 @@ class CommentTile extends StatelessWidget {
|
||||
key: ValueKey<int>(comment.id),
|
||||
item: comment,
|
||||
selectable: selectable,
|
||||
textScaleFactor: MediaQuery.of(context)
|
||||
.textScaleFactor,
|
||||
textScaler:
|
||||
MediaQuery.of(context).textScaler,
|
||||
onTap: () {
|
||||
if (onTap == null) {
|
||||
_onTextTapped(context);
|
||||
@ -314,7 +318,7 @@ class CommentTile extends StatelessWidget {
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.2),
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.2),
|
||||
),
|
||||
child: wrapper,
|
||||
);
|
||||
@ -324,6 +328,7 @@ class CommentTile extends StatelessWidget {
|
||||
final Color wrapperBorderColor = _getColor(
|
||||
i,
|
||||
primaryColor: primaryColor,
|
||||
brightness: brightness,
|
||||
);
|
||||
final bool shouldHighlight = isMyComment && i == level;
|
||||
wrapper = Container(
|
||||
@ -355,14 +360,21 @@ class CommentTile extends StatelessWidget {
|
||||
|
||||
Color _getColor(
|
||||
int level, {
|
||||
required MaterialColor primaryColor,
|
||||
required Color primaryColor,
|
||||
required Brightness brightness,
|
||||
}) {
|
||||
final int initialLevel = level;
|
||||
|
||||
if (levelToBorderColors[initialLevel] != null) {
|
||||
return levelToBorderColors[initialLevel]!;
|
||||
int convertKeyBasedOnBrightness(int original) {
|
||||
return brightness == Brightness.light ? original : original * 100;
|
||||
}
|
||||
|
||||
final int cacheKey = convertKeyBasedOnBrightness(initialLevel);
|
||||
|
||||
if (levelToBorderColors[cacheKey] != null) {
|
||||
return levelToBorderColors[cacheKey]!;
|
||||
} else if (level == 0) {
|
||||
levelToBorderColors[initialLevel] = primaryColor;
|
||||
levelToBorderColors[cacheKey] = primaryColor;
|
||||
return primaryColor;
|
||||
}
|
||||
|
||||
@ -373,7 +385,7 @@ class CommentTile extends StatelessWidget {
|
||||
final double opacity = ((10 - level) / 10).clamp(0.3, 1);
|
||||
final Color color = primaryColor.withOpacity(opacity);
|
||||
|
||||
levelToBorderColors[initialLevel] = color;
|
||||
levelToBorderColors[cacheKey] = color;
|
||||
return color;
|
||||
}
|
||||
|
||||
@ -406,12 +418,12 @@ class CommentTile extends StatelessWidget {
|
||||
final int indexOfNextComment = comments.indexOf(comment) + 1;
|
||||
if (indexOfNextComment < comments.length) {
|
||||
Future<void>.delayed(
|
||||
Durations.ms300,
|
||||
AppDurations.ms300,
|
||||
() {
|
||||
commentsCubit.itemScrollController.scrollTo(
|
||||
index: indexOfNextComment,
|
||||
alignment: 0.1,
|
||||
duration: Durations.ms300,
|
||||
duration: AppDurations.ms300,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -96,7 +96,7 @@ class _CountDownReminderState extends State<CountdownReminder>
|
||||
animation: animationController,
|
||||
child: FadeIn(
|
||||
child: Material(
|
||||
color: Theme.of(context).primaryColor,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(
|
||||
|
@ -15,19 +15,13 @@ class CustomChip extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool useMaterial3 = Theme.of(context).useMaterial3;
|
||||
return FilterChip(
|
||||
shadowColor: Palette.transparent,
|
||||
selectedShadowColor: Palette.transparent,
|
||||
backgroundColor: Palette.transparent,
|
||||
side: useMaterial3 && !selected
|
||||
? BorderSide(color: Theme.of(context).colorScheme.onSurface)
|
||||
: null,
|
||||
shape: Theme.of(context).useMaterial3
|
||||
? null
|
||||
: StadiumBorder(
|
||||
side: BorderSide(color: Theme.of(context).primaryColor),
|
||||
),
|
||||
side: selected
|
||||
? BorderSide(color: Theme.of(context).colorScheme.primary)
|
||||
: BorderSide(color: Theme.of(context).colorScheme.onSurface),
|
||||
label: Text(label),
|
||||
labelStyle: TextStyle(
|
||||
color: selected ? Theme.of(context).colorScheme.onPrimary : null,
|
||||
@ -35,7 +29,7 @@ class CustomChip extends StatelessWidget {
|
||||
checkmarkColor: selected ? Theme.of(context).colorScheme.onPrimary : null,
|
||||
selected: selected,
|
||||
onSelected: onSelected,
|
||||
selectedColor: Theme.of(context).primaryColor,
|
||||
selectedColor: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,9 @@ class CustomCircularProgressIndicator extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return CircularProgressIndicator(
|
||||
strokeWidth: strokeWidth,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ class CustomDescribedFeatureOverlay extends StatelessWidget {
|
||||
return DescribedFeatureOverlay(
|
||||
featureId: feature.featureId,
|
||||
overflowMode: OverflowMode.extendBackground,
|
||||
targetColor: Theme.of(context).primaryColor,
|
||||
targetColor: Theme.of(context).colorScheme.primary,
|
||||
tapTarget: tapTarget,
|
||||
title: Text(feature.title),
|
||||
description: Text(
|
||||
|
65
lib/screens/widgets/custom_dropdown_menu.dart
Normal file
@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hacki/styles/styles.dart';
|
||||
|
||||
class CustomDropdownMenu<T> extends StatelessWidget {
|
||||
const CustomDropdownMenu({
|
||||
required this.menuChildren,
|
||||
required this.onSelected,
|
||||
required this.selected,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final List<T> menuChildren;
|
||||
final void Function(T) onSelected;
|
||||
final T selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MenuAnchor(
|
||||
menuChildren: menuChildren
|
||||
.map(
|
||||
(T val) => MenuItemButton(
|
||||
onPressed: () => onSelected(val),
|
||||
child: Text(
|
||||
val.toString(),
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
textScaler: MediaQuery.of(context).clampedTextScaler,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
builder: (BuildContext context, MenuController controller, _) {
|
||||
return InkWell(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: Dimens.pt8,
|
||||
horizontal: Dimens.pt4,
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
selected.toString(),
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
textScaler: MediaQuery.of(context).clampedTextScaler,
|
||||
),
|
||||
Icon(
|
||||
controller.isOpen
|
||||
? Icons.arrow_drop_up
|
||||
: Icons.arrow_drop_down,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -37,7 +37,7 @@ class Linkify extends StatelessWidget {
|
||||
this.textDirection,
|
||||
this.maxLines,
|
||||
this.overflow = TextOverflow.clip,
|
||||
this.textScaleFactor,
|
||||
this.textScaler,
|
||||
this.softWrap = true,
|
||||
this.strutStyle,
|
||||
this.locale,
|
||||
@ -80,7 +80,7 @@ class Linkify extends StatelessWidget {
|
||||
final TextOverflow overflow;
|
||||
|
||||
/// The number of font pixels for each logical pixel
|
||||
final double? textScaleFactor;
|
||||
final TextScaler? textScaler;
|
||||
|
||||
/// Whether the text should break at soft line breaks.
|
||||
final bool softWrap;
|
||||
@ -128,7 +128,7 @@ class Linkify extends StatelessWidget {
|
||||
textDirection: textDirection,
|
||||
maxLines: maxLines,
|
||||
overflow: overflow,
|
||||
textScaleFactor: textScaleFactor,
|
||||
textScaler: textScaler,
|
||||
softWrap: softWrap,
|
||||
strutStyle: strutStyle,
|
||||
locale: locale,
|
||||
@ -170,7 +170,7 @@ class SelectableLinkify extends StatelessWidget {
|
||||
this.maxLines,
|
||||
// SelectableText
|
||||
this.focusNode,
|
||||
this.textScaleFactor,
|
||||
this.textScaler,
|
||||
this.strutStyle,
|
||||
this.showCursor = false,
|
||||
this.autofocus = false,
|
||||
@ -195,7 +195,7 @@ class SelectableLinkify extends StatelessWidget {
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// The number of font pixels for each logical pixel
|
||||
final double? textScaleFactor;
|
||||
final TextScaler? textScaler;
|
||||
|
||||
/// Linkifiers to be used for linkify
|
||||
final List<Linkifier> linkifiers;
|
||||
@ -309,7 +309,7 @@ class SelectableLinkify extends StatelessWidget {
|
||||
focusNode: focusNode,
|
||||
strutStyle: strutStyle,
|
||||
showCursor: showCursor,
|
||||
textScaleFactor: textScaleFactor,
|
||||
textScaler: textScaler,
|
||||
autofocus: autofocus,
|
||||
cursorWidth: cursorWidth,
|
||||
cursorRadius: cursorRadius,
|
||||
|