Compare commits

..

9 Commits

Author SHA1 Message Date
7dc3618afe update color. (#339) 2023-12-02 23:31:45 -08:00
eef4691814 update Info.plist (#338) 2023-12-02 20:58:39 -08:00
9f71701845 update story tile. (#336) 2023-12-02 04:46:06 -08:00
d27203b041 update Info.plist (#335) 2023-12-02 04:21:58 -08:00
4f280ec4c9 add ability to sync favorites from Hacker News. (#334) 2023-12-01 21:53:48 -08:00
72cb2737ca fix story tile. (#333) 2023-12-01 12:09:14 -08:00
215203bd16 remove error placeholder. (#332) 2023-12-01 11:27:16 -08:00
3e320faece update story title. (#331) 2023-12-01 09:56:19 -08:00
1049568246 bump Flutter version to 3.16.2 (#330) 2023-12-01 01:11:30 -08:00
121 changed files with 1006 additions and 911 deletions

View File

@ -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>

View File

@ -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"

Binary file not shown.

Binary file not shown.

BIN
assets/hacki-github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 548 KiB

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 KiB

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
assets/tablet-hacki.xcf Normal file

Binary file not shown.

View 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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 835 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -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));
}
}

View File

@ -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?>[];

View File

@ -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);

View File

@ -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())

View File

@ -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),

View File

@ -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;

View File

@ -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;
}

View File

@ -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,

View File

@ -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);

View File

@ -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 =>

View File

@ -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(

View File

@ -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,
),

View File

@ -6,4 +6,7 @@ enum CommentsOrder {
const CommentsOrder(this.description);
final String description;
@override
String toString() => description;
}

View File

@ -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',

View File

@ -5,4 +5,7 @@ enum FetchMode {
const FetchMode(this.description);
final String description;
@override
String toString() => description;
}

View File

@ -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;
}
}
}

View File

@ -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);

View File

@ -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,

View 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;
}
}

View File

@ -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);

View File

@ -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,
);
}
}

View File

@ -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';

View File

@ -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,
),
],
),

View File

@ -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),
),
),
],

View File

@ -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(),
),

View File

@ -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,
),
),

View File

@ -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,
),

View File

@ -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>[

View File

@ -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: () {

View File

@ -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,
),
),
),

View File

@ -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,
),
),
),

View File

@ -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,

View File

@ -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',

View File

@ -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: () {

View File

@ -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,
),
],
),

View File

@ -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,
),
),
),

View File

@ -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,

View File

@ -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

View File

@ -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,
);
}();

View File

@ -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,
),
),
),

View File

@ -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>[

View File

@ -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,
),
),
),

View File

@ -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(

View File

@ -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',

View File

@ -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(
@ -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,
);
},
);

View File

@ -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(

View File

@ -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,
);
}
}

View File

@ -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,
),
);
}
}

View File

@ -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(

View 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();
}
},
);
},
);
}
}

View File

@ -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,

View File

@ -41,19 +41,22 @@ class _CustomTabBarState extends State<CustomTabBar> {
return BlocBuilder<TabCubit, TabState>(
builder: (BuildContext context, TabState state) {
return TabBar(
isScrollable: true,
controller: widget.tabController,
indicatorColor: Theme.of(context).primaryColor,
dividerHeight: 0,
isScrollable: true,
indicatorColor: Theme.of(context).colorScheme.primary,
indicator: CircleTabIndicator(
color: Theme.of(context).primaryColor,
color: Theme.of(context).colorScheme.primary,
radius: Dimens.pt2,
),
splashFactory: NoSplash.splashFactory,
indicatorPadding: const EdgeInsets.only(
bottom: Dimens.pt8,
),
onTap: (_) {
HapticFeedbackUtil.selection();
},
tabAlignment: TabAlignment.center,
tabs: <Widget>[
for (int i = 0; i < state.tabs.length; i++)
Tab(
@ -62,13 +65,14 @@ class _CustomTabBarState extends State<CustomTabBar> {
),
child: AnimatedDefaultTextStyle(
style: TextStyle(
fontFamily: context.read<PreferenceCubit>().state.font.name,
fontSize:
currentIndex == i ? TextDimens.pt14 : TextDimens.pt10,
currentIndex == i ? TextDimens.pt12 : TextDimens.pt10,
color: currentIndex == i
? Theme.of(context).primaryColor
? Theme.of(context).colorScheme.primary
: Palette.grey,
),
duration: Durations.ms200,
duration: AppDurations.ms200,
child: Text(
state.tabs.elementAt(i).label,
key: ValueKey<String>(
@ -113,7 +117,7 @@ class _CustomTabBarState extends State<CustomTabBar> {
? TextDimens.pt16
: TextDimens.pt12,
color: currentIndex == 5
? Theme.of(context).primaryColor
? Theme.of(context).colorScheme.primary
: Palette.grey,
),
);

Some files were not shown because too many files have changed in this diff Show More