Compare commits

..

20 Commits

Author SHA1 Message Date
3fbf5d4eea improve shortcut button. (#284) 2023-10-22 20:34:09 -07:00
332ffbb773 bump version. (#282) 2023-10-22 00:14:12 -07:00
346a6c709e fix inconsistent font size. (#281) 2023-10-21 23:50:06 -07:00
d4fe042245 fix border color of comment tile. (#280) 2023-10-21 21:25:29 -07:00
b82c4a1777 update changelogs. (#279) 2023-10-21 20:45:24 -07:00
7e0d1f0f1d add ability to use custom tabs. (#278) 2023-10-21 20:25:45 -07:00
f405a10c2e fix color of quote element. (#277) 2023-10-21 19:43:06 -07:00
edbad79cd3 add ability to customize text scale factor and improve keyword filter. (#276) 2023-10-21 18:50:51 -07:00
c9d8b2950a add ability to change app's primary color. (#275) 2023-10-21 01:02:44 -07:00
f2bc48f980 update project.pbxproj file. (#271) 2023-09-29 19:30:56 -07:00
d56697c57c add ability to render code block inside comment text. (#266) 2023-09-29 18:50:40 -07:00
320ec41aae update url linkifier. (#270) 2023-09-29 16:21:41 -07:00
d85b3535d5 update url linkifier. 2023-09-29 16:15:06 -07:00
f8cd1cbba0 update url_linkifier.dart (#269) 2023-09-29 14:56:11 -07:00
817ec208d6 fix url parser. (#268) 2023-09-29 12:34:20 -07:00
554a165789 fix selectable text. (#267) 2023-09-28 23:46:24 -07:00
0c680370ef add ability to long press on story title to copy link. (#265) 2023-09-28 14:10:08 -07:00
59541d2fcc update Fastfile (#264) 2023-09-28 01:48:20 -07:00
32083c3564 update fastlane. (#263) 2023-09-28 00:05:16 -07:00
258dbc4b8b fix url parsing. (#262) 2023-09-27 23:17:31 -07:00
64 changed files with 1329 additions and 954 deletions

View File

@ -50,7 +50,7 @@ android {
defaultConfig {
applicationId "com.jiaqifeng.hacki"
minSdkVersion 26
minSdkVersion 25
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName

View File

@ -13,6 +13,9 @@
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
<application

View File

@ -0,0 +1,2 @@
- Ability to customize text scale factor.
- Ability to customize app's accent color.

View File

@ -137,11 +137,11 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/workmanager/ios"
SPEC CHECKSUMS:
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
@ -155,7 +155,7 @@ SPEC CHECKSUMS:
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
synced_shared_preferences: f722742b06d65c7315b8e9f56b794c9fbd5597f7
@ -166,4 +166,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d28e9a1c7bee335d05ddd795703aad5bf05bb937
COCOAPODS: 1.11.3
COCOAPODS: 1.13.0

View File

@ -80,7 +80,5 @@
<string>This app needs camera access to scan QR codes</string>
<key>io.flutter.embedded_views_preview</key>
<true/>
<key>FLTEnableWideGamut</key>
<false/>
</dict>
</plist>

View File

@ -228,16 +228,18 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
StoryLoaded event,
Emitter<StoriesState> emit,
) async {
final bool hasRead = await _preferenceRepository.hasRead(event.story.id);
final bool hidden = _filterCubit.state.keywords.any(
(String keyword) =>
event.story.title.toLowerCase().contains(keyword) ||
event.story.text.toLowerCase().contains(keyword),
);
final Story story = event.story;
final bool hasRead = await _preferenceRepository.hasRead(story.id);
final bool hidden = _filterCubit.state.keywords.any((String keyword) {
// Match word only.
final RegExp regExp = RegExp('\\b($keyword)\\b');
return regExp.hasMatch(story.title.toLowerCase()) ||
regExp.hasMatch(story.text.toLowerCase());
});
emit(
state.copyWithStoryAdded(
type: event.type,
story: event.story.copyWith(hidden: hidden),
story: story.copyWith(hidden: hidden),
hasRead: hasRead,
),
);

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/services/services.dart';
@ -28,11 +29,12 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: _collapseCache.totalHidden(_commentId),
collapsed: _collapseCache.isCollapsed(_commentId),
hidden: _collapseCache.isHidden(_commentId),
locked: _collapseCache.lockedId == _commentId,
),
);
}
void collapse() {
void collapse({required VoidCallback onStateChanged}) {
if (state.collapsed) {
_collapseCache.uncollapse(_commentId);
@ -42,7 +44,14 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: 0,
),
);
onStateChanged();
} else {
if (state.locked) {
emit(state.copyWith(locked: false));
return;
}
final Set<int> collapsedCommentIds = _collapseCache.collapse(_commentId);
emit(
@ -51,6 +60,8 @@ class CollapseCubit extends Cubit<CollapseState> {
collapsedCount: state.collapsed ? 0 : collapsedCommentIds.length,
),
);
onStateChanged();
}
}
@ -85,6 +96,13 @@ class CollapseCubit extends Cubit<CollapseState> {
}
}
/// Prevent the item to be able to collapse, used when the comment
/// text is selected.
void lock() {
_collapseCache.lockedId = _commentId;
emit(state.copyWith(locked: true));
}
@override
Future<void> close() async {
await _streamSubscription.cancel();

View File

@ -4,26 +4,39 @@ class CollapseState extends Equatable {
const CollapseState({
required this.collapsed,
required this.hidden,
required this.locked,
required this.collapsedCount,
});
const CollapseState.init()
: collapsed = false,
hidden = false,
locked = false,
collapsedCount = 0;
final bool collapsed;
/// The value determining whether or not the comment should show up in the
/// screen, this is true when the comment's parent is collapsed.
final bool hidden;
/// The value determining whether or not the comment is collapsable.
/// If [locked] is true then the comment is not collapsable and vice versa.
final bool locked;
/// The number of children under this collapsed comment.
final int collapsedCount;
CollapseState copyWith({
bool? collapsed,
bool? hidden,
bool? locked,
int? collapsedCount,
}) {
return CollapseState(
collapsed: collapsed ?? this.collapsed,
hidden: hidden ?? this.hidden,
locked: locked ?? this.locked,
collapsedCount: collapsedCount ?? this.collapsedCount,
);
}
@ -32,6 +45,7 @@ class CollapseState extends Equatable {
List<Object?> get props => <Object?>[
collapsed,
hidden,
locked,
collapsedCount,
];
}

View File

@ -322,7 +322,7 @@ class CommentsCubit extends Cubit<CommentsState> {
}
}
void onOrderChanged(CommentsOrder? order) {
void updateOrder(CommentsOrder? order) {
if (order == null) return;
if (state.order == order) return;
HapticFeedbackUtil.selection();
@ -335,7 +335,7 @@ class CommentsCubit extends Cubit<CommentsState> {
init(useCommentCache: true);
}
void onFetchModeChanged(FetchMode? fetchMode) {
void updateFetchMode(FetchMode? fetchMode) {
if (fetchMode == null) return;
if (state.fetchMode == fetchMode) return;
_collapseCache.resetCollapsedComments();
@ -359,7 +359,7 @@ class CommentsCubit extends Cubit<CommentsState> {
.itemPositions.value
// The header is also a part of the list view,
// thus ignoring it here.
.where((ItemPosition e) => e.index >= 1 && e.itemLeadingEdge < 0.7)
.where((ItemPosition e) => e.index >= 1 && e.itemLeadingEdge > 0.1)
.sorted((ItemPosition a, ItemPosition b) => a.index.compareTo(b.index))
.map(
(ItemPosition e) => e.index <= state.comments.length
@ -369,9 +369,19 @@ class CommentsCubit extends Cubit<CommentsState> {
.whereNotNull()
.toList();
/// The index of last comment visible on screen.
final int lastVisibleIndex = state.comments.indexOf(onScreenComments.last);
final int startIndex = min(lastVisibleIndex + 1, totalComments);
if (onScreenComments.isEmpty && state.comments.isNotEmpty) {
itemScrollController.scrollTo(
index: 1,
alignment: 0.15,
duration: Durations.ms400,
);
return;
}
/// The index of first root level comment visible on screen.
final int firstVisibleRootIndex = state.comments
.indexOf(onScreenComments.firstWhere((Comment e) => e.isRoot));
final int startIndex = min(firstVisibleRootIndex + 1, totalComments);
for (int i = startIndex; i < totalComments; i++) {
final Comment cmt = state.comments.elementAt(i);

View File

@ -1,5 +1,7 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
@ -41,6 +43,16 @@ class PreferenceCubit extends Cubit<PreferenceState> {
return null;
});
}
for (final DoublePreference p
in Preference.allPreferences.whereType<DoublePreference>()) {
initPreference<double>(p).then<double?>((double? value) {
final Preference<dynamic> updatedPreference = p.copyWith(val: value);
emit(state.copyWithPreference(updatedPreference));
return null;
});
}
}
Future<T?> initPreference<T>(Preference<T> preference) async {
@ -48,6 +60,10 @@ class PreferenceCubit extends Cubit<PreferenceState> {
case int:
final int? value = await _preferenceRepository.getInt(preference.key);
return value as T?;
case double:
final double? value =
await _preferenceRepository.getDouble(preference.key);
return value as T?;
case bool:
final bool? value = await _preferenceRepository.getBool(preference.key);
return value as T?;
@ -56,19 +72,27 @@ class PreferenceCubit extends Cubit<PreferenceState> {
}
}
void update<T>(Preference<T> preference, {required T to}) {
final T value = to;
final Preference<T> updatedPreference = preference.copyWith(val: value);
void update<T>(Preference<T> preference) {
_logger.i('updating $preference to ${preference.val}');
_logger.i('updating $preference to $value');
emit(state.copyWithPreference(updatedPreference));
emit(state.copyWithPreference(preference));
switch (T) {
case int:
_preferenceRepository.setInt(preference.key, value as int);
_preferenceRepository.setInt(
preference.key,
preference.val as int,
);
case double:
_preferenceRepository.setDouble(
preference.key,
preference.val as double,
);
case bool:
_preferenceRepository.setBool(preference.key, value as bool);
_preferenceRepository.setBool(
preference.key,
preference.val as bool,
);
default:
throw UnimplementedError();
}

View File

@ -54,8 +54,6 @@ class PreferenceState extends Equatable {
bool get eyeCandyEnabled => _isOn<EyeCandyModePreference>();
bool get trueDarkEnabled => _isOn<TrueDarkModePreference>();
bool get readerEnabled => _isOn<ReaderModePreference>();
bool get markReadStoriesEnabled => _isOn<MarkReadStoriesModePreference>();
@ -70,6 +68,17 @@ class PreferenceState extends Equatable {
bool get autoScrollEnabled => _isOn<AutoScrollModePreference>();
bool get customTabEnabled => _isOn<CustomTabPreference>();
double get textScaleFactor =>
preferences.singleWhereType<TextScaleFactorPreference>().val;
MaterialColor get appColor {
return materialColors.elementAt(
preferences.singleWhereType<AppColorPreference>().val,
) as MaterialColor;
}
List<StoryType> get tabs {
final String result =
preferences.singleWhereType<TabOrderPreference>().val.toString();

View File

@ -38,8 +38,7 @@ class TabCubit extends Cubit<TabState> {
// Check to make sure there's no duplicate.
if (updatedTabs.toSet().length == StoryType.values.length) {
_preferenceCubit.update<int>(
TabOrderPreference(),
to: StoryType.convertToSettingsValue(updatedTabs),
TabOrderPreference(val: StoryType.convertToSettingsValue(updatedTabs)),
);
}
}

View File

@ -3,7 +3,6 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/styles/styles.dart';
extension ContextExtension on BuildContext {
T? tryRead<T>() {
@ -21,13 +20,18 @@ extension ContextExtension on BuildContext {
}) {
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
backgroundColor: Palette.deepOrange,
content: Text(content),
backgroundColor: Theme.of(this).primaryColor,
content: Text(
content,
style: TextStyle(
color: Theme.of(this).colorScheme.onPrimary,
),
),
action: action != null && label != null
? SnackBarAction(
label: label,
onPressed: action,
textColor: Theme.of(this).textTheme.bodyLarge?.color,
textColor: Theme.of(this).colorScheme.onPrimary,
)
: null,
),

View File

@ -1,9 +1,9 @@
export 'context_extension.dart';
export 'date_time_extension.dart';
export 'int_extension.dart';
export 'item_action_mixin.dart';
export 'list_extension.dart';
export 'object_extension.dart';
export 'set_extension.dart';
export 'state_extension.dart';
export 'string_extension.dart';
export 'widget_extension.dart';

View File

@ -12,7 +12,8 @@ import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
import 'package:share_plus/share_plus.dart';
extension StateExtension on State {
@optionalTypeArgs
mixin ItemActionMixin<T extends StatefulWidget> on State<T> {
void showSnackBar({
required String content,
VoidCallback? action,

View File

@ -2,21 +2,19 @@ import 'package:flutter/material.dart';
import 'package:hacki/config/constants.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/custom_linkify/custom_linkify.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
extension WidgetModifier on Widget {
Widget padded([EdgeInsetsGeometry value = const EdgeInsets.all(12)]) {
return Padding(
padding: value,
child: this,
);
}
extension ContextMenuBuilder on Widget {
Widget contextMenuBuilder(
BuildContext context,
EditableTextState editableTextState, {
required Item item,
}) {
if (item is! Buildable) {
return const SizedBox.shrink();
}
final int start = editableTextState.textEditingValue.selection.base.offset;
final int end = editableTextState.textEditingValue.selection.end;
@ -25,39 +23,24 @@ extension WidgetModifier on Widget {
];
if (start != -1 && end != -1) {
String selectedText = item.text.substring(start, end);
if (item is Buildable) {
final Iterable<EmphasisElement> emphasisElements =
(item as Buildable).elements.whereType<EmphasisElement>();
int count = 1;
while (selectedText.contains(' ') && count <= emphasisElements.length) {
final int s = (start + count * 2).clamp(0, item.text.length);
final int e = (end + count * 2).clamp(0, item.text.length);
selectedText = item.text.substring(s, e);
count++;
}
count = 1;
while (selectedText.contains(' ') && count <= emphasisElements.length) {
final int s = (start - count * 2).clamp(0, item.text.length);
final int e = (end - count * 2).clamp(0, item.text.length);
selectedText = item.text.substring(s, e);
count++;
}
}
final String text = (item as Buildable)
.elements
.map((LinkifyElement e) => e.text)
.reduce((String value, String e) => '$value$e');
final String selectedText = text.substring(start, end);
items.addAll(<ContextMenuButtonItem>[
ContextMenuButtonItem(
onPressed: () => LinkUtil.launch(
'''${Constants.wikipediaLink}$selectedText''',
context,
),
label: 'Wikipedia',
),
ContextMenuButtonItem(
onPressed: () => LinkUtil.launch(
'''${Constants.wiktionaryLink}$selectedText''',
context,
),
label: 'Wiktionary',
),
@ -70,3 +53,14 @@ extension WidgetModifier on Widget {
);
}
}
extension WidgetModifier on Widget {
Widget padded([
EdgeInsetsGeometry value = const EdgeInsets.all(Dimens.pt12),
]) {
return Padding(
padding: value,
child: this,
);
}
}

View File

@ -17,7 +17,6 @@ import 'package:hacki/config/constants.dart';
import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/services/fetcher.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/theme_util.dart';
@ -26,7 +25,6 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:visibility_detector/visibility_detector.dart';
import 'package:workmanager/workmanager.dart';
@ -133,12 +131,6 @@ Future<void> main({bool testing = false}) async {
}
final AdaptiveThemeMode? savedThemeMode = await AdaptiveTheme.getThemeMode();
final SharedPreferences prefs = await SharedPreferences.getInstance();
final bool trueDarkMode =
prefs.getBool(const TrueDarkModePreference().key) ?? false;
final Font font = Font.values.elementAt(
prefs.getInt(FontPreference().key) ?? Font.roboto.index,
);
// Uncomment this line to log events from bloc/cubit.
// Bloc.observer = CustomBlocObserver();
@ -150,23 +142,17 @@ Future<void> main({bool testing = false}) async {
runApp(
HackiApp(
savedThemeMode: savedThemeMode,
trueDarkMode: trueDarkMode,
font: font,
),
);
}
class HackiApp extends StatelessWidget {
const HackiApp({
required this.trueDarkMode,
required this.font,
super.key,
this.savedThemeMode,
});
final AdaptiveThemeMode? savedThemeMode;
final Font font;
final bool trueDarkMode;
@override
Widget build(BuildContext context) {
@ -243,25 +229,33 @@ class HackiApp extends StatelessWidget {
)..init(),
),
],
child: AdaptiveTheme(
child: BlocBuilder<PreferenceCubit, PreferenceState>(
buildWhen: (PreferenceState previous, PreferenceState current) =>
previous.appColor != current.appColor ||
previous.font != current.font ||
previous.textScaleFactor != current.textScaleFactor,
builder: (BuildContext context, PreferenceState state) {
return AdaptiveTheme(
key: ValueKey<String>('${state.appColor}${state.font}'),
light: ThemeData(
primarySwatch: Palette.orange,
fontFamily: font.name,
primaryColor: state.appColor,
colorScheme: ColorScheme.fromSwatch(
primarySwatch: state.appColor,
),
fontFamily: state.font.name,
),
dark: ThemeData(
brightness: Brightness.dark,
primarySwatch: Palette.orange,
canvasColor: trueDarkMode ? Palette.black : null,
fontFamily: font.name,
primaryColor: state.appColor,
colorScheme: ColorScheme.fromSwatch(
primarySwatch: state.appColor,
brightness: Brightness.dark,
),
canvasColor: Palette.black,
fontFamily: state.font.name,
),
initial: savedThemeMode ?? AdaptiveThemeMode.system,
builder: (ThemeData theme, ThemeData darkTheme) {
final ThemeData trueDarkTheme = ThemeData(
brightness: Brightness.dark,
primarySwatch: Palette.orange,
canvasColor: Palette.black,
fontFamily: font.name,
);
return FutureBuilder<AdaptiveThemeMode?>(
future: AdaptiveTheme.getThemeMode(),
builder: (
@ -270,30 +264,34 @@ class HackiApp extends StatelessWidget {
) {
final AdaptiveThemeMode? mode = snapshot.data;
ThemeUtil.updateStatusBarSetting(
SchedulerBinding.instance.platformDispatcher.platformBrightness,
SchedulerBinding
.instance.platformDispatcher.platformBrightness,
mode,
);
return BlocBuilder<PreferenceCubit, PreferenceState>(
buildWhen:
(PreferenceState previous, PreferenceState current) =>
previous.trueDarkEnabled != current.trueDarkEnabled,
builder: (BuildContext context, PreferenceState prefState) {
final bool useTrueDark = prefState.trueDarkEnabled &&
(mode == AdaptiveThemeMode.dark ||
final bool isDarkModeEnabled =
mode == AdaptiveThemeMode.dark ||
(mode == AdaptiveThemeMode.system &&
View.of(context)
.platformDispatcher
.platformBrightness ==
Brightness.dark));
Brightness.dark);
return FeatureDiscovery(
child: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaleFactor: state.textScaleFactor == 1
? null
: state.textScaleFactor,
),
child: MaterialApp.router(
key: Key(state.appColor.hashCode.toString()),
title: 'Hacki',
debugShowCheckedModeBanner: false,
theme: (useTrueDark ? trueDarkTheme : theme).copyWith(
theme: (isDarkModeEnabled ? darkTheme : theme).copyWith(
useMaterial3: false,
),
routerConfig: router,
),
),
);
},
);

View File

@ -2,8 +2,10 @@ import 'dart:collection';
import 'dart:io';
import 'package:equatable/equatable.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:hacki/models/displayable.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/styles/palette.dart';
abstract class Preference<T> extends Equatable with SettingsDisplayable {
const Preference({required this.val});
@ -24,19 +26,23 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
FontSizePreference(),
TabOrderPreference(),
StoryMarkingModePreference(),
AppColorPreference(),
const TextScaleFactorPreference(),
// Order of items below matters and
// reflects the order on settings screen.
const DisplayModePreference(),
const MetadataModePreference(),
const StoryUrlModePreference(),
// Divider.
const MarkReadStoriesModePreference(),
// Divider.
const NotificationModePreference(),
const SwipeGesturePreference(),
const AutoScrollModePreference(),
const CollapseModePreference(),
const ReaderModePreference(),
const CustomTabPreference(),
const EyeCandyModePreference(),
const TrueDarkModePreference(),
],
);
@ -52,20 +58,26 @@ abstract class IntPreference extends Preference<int> {
const IntPreference({required super.val});
}
abstract class DoublePreference extends Preference<double> {
const DoublePreference({required super.val});
}
const bool _notificationModeDefaultValue = true;
const bool _swipeGestureModeDefaultValue = false;
const bool _displayModeDefaultValue = true;
const bool _eyeCandyModeDefaultValue = false;
const bool _trueDarkModeDefaultValue = true;
const bool _readerModeDefaultValue = true;
const bool _markReadStoriesModeDefaultValue = true;
const bool _metadataModeDefaultValue = true;
const bool _storyUrlModeDefaultValue = true;
const bool _collapseModeDefaultValue = true;
const bool _autoScrollModeDefaultValue = true;
const bool _autoScrollModeDefaultValue = false;
const bool _customTabModeDefaultValue = 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 _tabOrderDefaultValue =
StoryType.convertToSettingsValue(StoryType.values);
@ -273,23 +285,31 @@ class EyeCandyModePreference extends BooleanPreference {
String get subtitle => 'some sort of magic.';
}
class TrueDarkModePreference extends BooleanPreference {
const TrueDarkModePreference({bool? val})
: super(val: val ?? _trueDarkModeDefaultValue);
/// Whether or not to use Custom Tabs for launching URLs.
/// If false, default browser will be used.
///
/// https://developer.chrome.com/docs/android/custom-tabs/
class CustomTabPreference extends BooleanPreference {
const CustomTabPreference({bool? val})
: super(val: val ?? _customTabModeDefaultValue);
@override
TrueDarkModePreference copyWith({required bool? val}) {
return TrueDarkModePreference(val: val);
CustomTabPreference copyWith({required bool? val}) {
return CustomTabPreference(val: val);
}
@override
String get key => 'trueDarkMode';
String get key => 'customTabPreference';
@override
String get title => 'True Dark Mode';
String get title => 'Use Custom Tabs';
@override
String get subtitle => 'you might need to restart the app.';
String get subtitle =>
'''use Custom tabs for URLs. If disabled, default browser is used instead.''';
@override
bool get isDisplayable => Platform.isAndroid;
}
class FetchModePreference extends IntPreference {
@ -383,3 +403,34 @@ class StoryMarkingModePreference extends IntPreference {
@override
String get title => 'Mark a Story as Read on';
}
class AppColorPreference extends IntPreference {
AppColorPreference({int? val}) : super(val: val ?? _appColorDefaultValue);
@override
AppColorPreference copyWith({required int? val}) {
return AppColorPreference(val: val);
}
@override
String get key => 'appColor';
@override
String get title => 'Accent Color';
}
class TextScaleFactorPreference extends DoublePreference {
const TextScaleFactorPreference({double? val})
: super(val: val ?? _textScaleFactorDefaultValue);
@override
TextScaleFactorPreference copyWith({required double? val}) {
return TextScaleFactorPreference(val: val);
}
@override
String get key => 'appTextScaleFactor';
@override
String get title => 'Default text scale factor';
}

View File

@ -46,6 +46,10 @@ class PreferenceRepository {
(SharedPreferences prefs) => prefs.getInt(key),
);
Future<double?> getDouble(String key) => _prefs.then(
(SharedPreferences prefs) => prefs.getDouble(key),
);
//ignore: avoid_positional_boolean_parameters
void setBool(String key, bool val) => _prefs.then(
(SharedPreferences prefs) => prefs.setBool(key, val),
@ -55,6 +59,10 @@ class PreferenceRepository {
(SharedPreferences prefs) => prefs.setInt(key, val),
);
void setDouble(String key, double val) => _prefs.then(
(SharedPreferences prefs) => prefs.setDouble(key, val),
);
Future<bool> hasPushed(int commentId) async =>
_prefs.then((SharedPreferences prefs) {
final bool? val = prefs.getBool(_getPushNotificationKey(commentId));

View File

@ -36,7 +36,7 @@ class HomeScreen extends StatefulWidget {
}
class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin, RouteAware {
with SingleTickerProviderStateMixin, RouteAware, ItemActionMixin {
late final TabController tabController;
late final StreamSubscription<String> intentDataStreamSubscription;
late final StreamSubscription<String?> notificationStreamSubscription;
@ -224,6 +224,7 @@ class _HomeScreenState extends State<HomeScreen>
if (story.url.isNotEmpty && isJobWithLink) {
LinkUtil.launch(
story.url,
context,
useReader: useReader,
offlineReading: offlineReading,
);

View File

@ -45,7 +45,7 @@ class PinnedStories extends StatelessWidget {
],
),
child: ColoredBox(
color: Palette.orangeAccent.withOpacity(0.2),
color: Theme.of(context).primaryColor.withOpacity(0.2),
child: StoryTile(
key: ValueKey<String>('${story.id}-PinnedStoryTile'),
story: story,
@ -58,10 +58,10 @@ class PinnedStories extends StatelessWidget {
),
),
if (state.pinnedStories.isNotEmpty)
const Padding(
padding: EdgeInsets.symmetric(horizontal: Dimens.pt12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: Dimens.pt12),
child: Divider(
color: Palette.orangeAccent,
color: Theme.of(context).primaryColor.withOpacity(0.8),
),
),
],

View File

@ -137,7 +137,8 @@ class ItemScreen extends StatefulWidget {
_ItemScreenState createState() => _ItemScreenState();
}
class _ItemScreenState extends State<ItemScreen> with RouteAware {
class _ItemScreenState extends State<ItemScreen>
with RouteAware, ItemActionMixin {
final TextEditingController commentEditingController =
TextEditingController();
final FocusNode focusNode = FocusNode();
@ -300,6 +301,8 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
backgroundColor: Theme.of(context)
.canvasColor
.withOpacity(0.6),
foregroundColor:
Theme.of(context).iconTheme.color,
item: widget.item,
splitViewEnabled: state.enabled,
expanded: state.expanded,
@ -339,6 +342,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
appBar: CustomAppBar(
backgroundColor:
Theme.of(context).canvasColor.withOpacity(0.6),
foregroundColor: Theme.of(context).iconTheme.color,
item: widget.item,
onFontSizeTap: onFontSizeTapped,
fontSizeIconButtonKey: fontSizeIconButtonKey,
@ -411,17 +415,16 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
fontSize: fontSize.fontSize,
color:
context.read<PreferenceCubit>().state.fontSize == fontSize
? Palette.deepOrange
? Theme.of(context).primaryColor
: null,
),
),
onTap: () {
HapticFeedbackUtil.light();
locator.get<AppReviewService>().requestReview();
context.read<PreferenceCubit>().update(
FontSizePreference(),
to: fontSize.index,
);
context
.read<PreferenceCubit>()
.update(FontSizePreference(val: fontSize.index));
},
),
],

View File

@ -9,6 +9,7 @@ class CustomAppBar extends AppBar {
CustomAppBar({
required Item item,
required super.backgroundColor,
required super.foregroundColor,
required VoidCallback onFontSizeTap,
required GlobalKey fontSizeIconButtonKey,
super.key,

View File

@ -29,7 +29,9 @@ class FavIconButton extends StatelessWidget {
feature: DiscoverableFeature.addStoryToFavList,
child: Icon(
isFav ? Icons.favorite : Icons.favorite_border,
color: isFav ? Palette.orange : Theme.of(context).iconTheme.color,
color: isFav
? Theme.of(context).primaryColor
: Theme.of(context).iconTheme.color,
),
),
onPressed: () {

View File

@ -28,6 +28,7 @@ class LinkIconButton extends StatelessWidget {
),
onPressed: () => LinkUtil.launch(
'https://news.ycombinator.com/item?id=$storyId',
context,
useHackiForHnLink: false,
),
);

View File

@ -16,7 +16,7 @@ class LoginDialog extends StatefulWidget {
State<LoginDialog> createState() => _LoginDialogState();
}
class _LoginDialogState extends State<LoginDialog> {
class _LoginDialogState extends State<LoginDialog> with ItemActionMixin {
final TextEditingController usernameController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
@ -35,12 +35,12 @@ class _LoginDialogState extends State<LoginDialog> {
return SimpleDialog(
children: <Widget>[
if (state.status.isLoading)
const SizedBox(
SizedBox(
height: Dimens.pt36,
width: Dimens.pt36,
child: Center(
child: CircularProgressIndicator(
color: Palette.orange,
color: Theme.of(context).primaryColor,
),
),
)
@ -51,12 +51,13 @@ class _LoginDialogState extends State<LoginDialog> {
),
child: TextField(
controller: usernameController,
cursorColor: Palette.orange,
cursorColor: Theme.of(context).primaryColor,
autocorrect: false,
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: 'Username',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Palette.orange),
borderSide:
BorderSide(color: Theme.of(context).primaryColor),
),
),
),
@ -70,13 +71,14 @@ class _LoginDialogState extends State<LoginDialog> {
),
child: TextField(
controller: passwordController,
cursorColor: Palette.orange,
cursorColor: Theme.of(context).primaryColor,
obscureText: true,
autocorrect: false,
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: 'Password',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Palette.orange),
borderSide:
BorderSide(color: Theme.of(context).primaryColor),
),
),
),
@ -108,7 +110,7 @@ class _LoginDialogState extends State<LoginDialog> {
? Icons.check_box
: Icons.check_box_outline_blank,
color: state.agreedToEULA
? Palette.deepOrange
? Theme.of(context).primaryColor
: Palette.grey,
),
onPressed: () =>
@ -129,11 +131,12 @@ class _LoginDialogState extends State<LoginDialog> {
child: TapDownWrapper(
onTap: () => LinkUtil.launch(
Constants.endUserAgreementLink,
context,
),
child: const Text(
child: Text(
'End User Agreement',
style: TextStyle(
color: Palette.deepOrange,
color: Theme.of(context).primaryColor,
decoration: TextDecoration.underline,
fontWeight: FontWeight.w600,
),
@ -179,7 +182,7 @@ class _LoginDialogState extends State<LoginDialog> {
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
state.agreedToEULA
? Palette.deepOrange
? Theme.of(context).primaryColor
: Palette.grey,
),
),

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:clipboard/clipboard.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart';
@ -188,9 +189,10 @@ class _ParentItemSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final Item item = state.item;
return Semantics(
label:
'''Posted by ${state.item.by} ${state.item.timeAgo}, ${state.item.title}. ${state.item.text}''',
'''Posted by ${item.by} ${item.timeAgo}, ${item.title}. ${item.text}''',
child: Column(
children: <Widget>[
if (!splitViewEnabled)
@ -207,21 +209,21 @@ class _ParentItemSection extends StatelessWidget {
onPressed: (_) {
HapticFeedbackUtil.light();
if (state.item.id !=
if (item.id !=
context.read<EditCubit>().state.replyingTo?.id) {
commentEditingController.clear();
}
context.read<EditCubit>().onReplyTapped(state.item);
context.read<EditCubit>().onReplyTapped(item);
},
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
icon: Icons.message,
),
SlidableAction(
onPressed: (BuildContext context) =>
onMoreTapped(state.item, context.rect),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
onMoreTapped(item, context.rect),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
icon: Icons.more_horiz,
),
],
@ -236,16 +238,16 @@ class _ParentItemSection extends StatelessWidget {
child: Row(
children: <Widget>[
Text(
state.item.by,
style: const TextStyle(
color: Palette.orange,
item.by,
style: TextStyle(
color: Theme.of(context).primaryColor,
),
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
),
const Spacer(),
Text(
state.item.timeAgo,
item.timeAgo,
style: const TextStyle(
color: Palette.grey,
),
@ -265,12 +267,14 @@ class _ParentItemSection extends StatelessWidget {
BuildContext context,
PreferenceState prefState,
) {
final double fontSize = prefState.fontSize.fontSize;
return Column(
children: <Widget>[
if (state.item is Story)
if (item is Story)
InkWell(
onTap: () => LinkUtil.launch(
state.item.url,
item.url,
context,
useReader: context
.read<PreferenceCubit>()
.state
@ -280,6 +284,17 @@ class _ParentItemSection extends StatelessWidget {
.state
.isOfflineReading,
),
onLongPress: () {
if (item.url.isNotEmpty) {
FlutterClipboard.copy(item.url)
.whenComplete(() {
HapticFeedbackUtil.selection();
context.showSnackBar(
content: 'Link copied.',
);
});
}
},
child: Padding(
padding: const EdgeInsets.only(
left: Dimens.pt6,
@ -289,35 +304,26 @@ class _ParentItemSection extends StatelessWidget {
),
child: Text.rich(
TextSpan(
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: prefState.fontSize.fontSize,
color: Theme.of(context)
.textTheme
.bodyLarge
?.color,
),
children: <TextSpan>[
TextSpan(
semanticsLabel: state.item.title,
text: state.item.title,
semanticsLabel: item.title,
text: item.title,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: prefState.fontSize.fontSize,
color: state.item.url.isNotEmpty
? Palette.orange
fontSize: fontSize,
color: item.url.isNotEmpty
? Theme.of(context).primaryColor
: null,
),
),
if (state.item.url.isNotEmpty)
if (item.url.isNotEmpty)
TextSpan(
text:
''' (${(state.item as Story).readableUrl})''',
text: ''' (${item.readableUrl})''',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize:
prefState.fontSize.fontSize - 4,
color: Palette.orange,
fontSize: fontSize - 4,
color:
Theme.of(context).primaryColor,
),
),
],
@ -332,7 +338,7 @@ class _ParentItemSection extends StatelessWidget {
const SizedBox(
height: Dimens.pt6,
),
if (state.item.text.isNotEmpty)
if (item.text.isNotEmpty)
FadeIn(
child: SizedBox(
width: double.infinity,
@ -341,9 +347,10 @@ class _ParentItemSection extends StatelessWidget {
left: Dimens.pt8,
),
child: ItemText(
item: state.item,
item: item,
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
selectable: true,
),
),
),
@ -352,28 +359,27 @@ class _ParentItemSection extends StatelessWidget {
);
},
),
if (state.item.isPoll)
if (item is Story && item.isPoll)
BlocProvider<PollCubit>(
create: (BuildContext context) =>
PollCubit(story: state.item as Story)..init(),
PollCubit(story: item)..init(),
child: const PollView(),
),
],
),
),
),
if (state.item.text.isNotEmpty)
if (item.text.isNotEmpty)
const SizedBox(
height: Dimens.pt8,
),
const Divider(
height: Dimens.zero,
),
if (state.onlyShowTargetComment) ...<Widget>[
if (state.onlyShowTargetComment && item is Story) ...<Widget>[
Center(
child: TextButton(
onPressed: () =>
context.read<CommentsCubit>().loadAll(state.item as Story),
onPressed: () => context.read<CommentsCubit>().loadAll(item),
child: const Text('View all comments'),
),
),
@ -383,12 +389,12 @@ class _ParentItemSection extends StatelessWidget {
] else ...<Widget>[
Row(
children: <Widget>[
if (state.item is Story) ...<Widget>[
if (item is Story) ...<Widget>[
const SizedBox(
width: Dimens.pt12,
),
Text(
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
'''${item.score} karma, ${item.descendants} comment${item.descendants > 1 ? 's' : ''}''',
style: const TextStyle(
fontSize: TextDimens.pt13,
),
@ -461,7 +467,7 @@ class _ParentItemSection extends StatelessWidget {
),
)
.toList(),
onChanged: context.read<CommentsCubit>().onFetchModeChanged,
onChanged: context.read<CommentsCubit>().updateFetchMode,
),
const SizedBox(
width: Dimens.pt6,
@ -483,7 +489,7 @@ class _ParentItemSection extends StatelessWidget {
),
)
.toList(),
onChanged: context.read<CommentsCubit>().onOrderChanged,
onChanged: context.read<CommentsCubit>().updateOrder,
),
const SizedBox(
width: Dimens.pt4,

View File

@ -115,11 +115,15 @@ class MorePopupMenu extends StatelessWidget {
text: HtmlUtil.parseHtml(
state.user.about,
),
linkStyle: const TextStyle(
color: Palette.orange,
linkStyle: TextStyle(
color:
Theme.of(context).primaryColor,
),
onOpen: (LinkableElement link) =>
LinkUtil.launch(link.url),
LinkUtil.launch(
link.url,
context,
),
semanticsLabel: state.user.about,
),
actions: <Widget>[
@ -158,12 +162,12 @@ class MorePopupMenu extends StatelessWidget {
ListTile(
leading: Icon(
FeatherIcons.chevronUp,
color: upvoted ? Palette.orange : null,
color: upvoted ? Theme.of(context).primaryColor : null,
),
title: Text(
upvoted ? 'Upvoted' : 'Upvote',
style: upvoted
? const TextStyle(color: Palette.orange)
? TextStyle(color: Theme.of(context).primaryColor)
: null,
),
subtitle:
@ -173,12 +177,12 @@ class MorePopupMenu extends StatelessWidget {
ListTile(
leading: Icon(
FeatherIcons.chevronDown,
color: downvoted ? Palette.orange : null,
color: downvoted ? Theme.of(context).primaryColor : null,
),
title: Text(
downvoted ? 'Downvoted' : 'Downvote',
style: downvoted
? const TextStyle(color: Palette.orange)
? TextStyle(color: Theme.of(context).primaryColor)
: null,
),
onTap: context.read<VoteCubit>().downvote,
@ -189,7 +193,7 @@ class MorePopupMenu extends StatelessWidget {
return ListTile(
leading: Icon(
isFav ? Icons.favorite : Icons.favorite_border,
color: isFav ? Palette.orange : null,
color: isFav ? Theme.of(context).primaryColor : null,
),
title: Text(
isFav ? 'Unfavorite' : 'Favorite',

View File

@ -36,7 +36,7 @@ class PinIconButton extends StatelessWidget {
child: Icon(
pinned ? Icons.push_pin : Icons.push_pin_outlined,
color: pinned
? Palette.orange
? Theme.of(context).primaryColor
: Theme.of(context).iconTheme.color,
),
),

View File

@ -15,7 +15,7 @@ class PollView extends StatefulWidget {
State<PollView> createState() => _PollViewState();
}
class _PollViewState extends State<PollView> {
class _PollViewState extends State<PollView> with ItemActionMixin {
@override
Widget build(BuildContext context) {
return BlocBuilder<PollCubit, PollState>(
@ -106,7 +106,7 @@ class _PollViewState extends State<PollView> {
icon: Icon(
Icons.arrow_drop_up,
color: voteState.vote == Vote.up
? Palette.orange
? Theme.of(context).primaryColor
: Palette.grey,
size: TextDimens.pt36,
),
@ -130,7 +130,7 @@ class _PollViewState extends State<PollView> {
),
LinearProgressIndicator(
value: option.ratio,
color: Palette.deepOrange,
color: Theme.of(context).primaryColor,
),
],
),

View File

@ -32,7 +32,7 @@ class ReplyBox extends StatefulWidget {
_ReplyBoxState createState() => _ReplyBoxState();
}
class _ReplyBoxState extends State<ReplyBox> {
class _ReplyBoxState extends State<ReplyBox> with ItemActionMixin {
bool expanded = false;
double? expandedHeight;
@ -124,9 +124,9 @@ class _ReplyBoxState extends State<ReplyBox> {
duration: Durations.ms300,
child: IconButton(
key: const Key('quote'),
icon: const Icon(
icon: Icon(
FeatherIcons.code,
color: Palette.orange,
color: Theme.of(context).primaryColor,
size: TextDimens.pt18,
),
onPressed: expanded ? showTextPopup : null,
@ -138,7 +138,7 @@ class _ReplyBoxState extends State<ReplyBox> {
expanded
? FeatherIcons.minimize2
: FeatherIcons.maximize2,
color: Palette.orange,
color: Theme.of(context).primaryColor,
size: TextDimens.pt18,
),
onPressed: () {
@ -150,9 +150,9 @@ class _ReplyBoxState extends State<ReplyBox> {
],
IconButton(
key: const Key('close'),
icon: const Icon(
icon: Icon(
Icons.close,
color: Palette.orange,
color: Theme.of(context).primaryColor,
),
onPressed: () {
setState(() {
@ -196,8 +196,8 @@ class _ReplyBoxState extends State<ReplyBox> {
),
],
if (isLoading)
const Padding(
padding: EdgeInsets.symmetric(
Padding(
padding: const EdgeInsets.symmetric(
vertical: Dimens.pt12,
horizontal: Dimens.pt16,
),
@ -205,7 +205,7 @@ class _ReplyBoxState extends State<ReplyBox> {
height: Dimens.pt24,
width: Dimens.pt24,
child: CircularProgressIndicator(
color: Palette.orange,
color: Theme.of(context).primaryColor,
strokeWidth: Dimens.pt2,
),
),
@ -213,9 +213,9 @@ class _ReplyBoxState extends State<ReplyBox> {
else
IconButton(
key: const Key('send'),
icon: const Icon(
icon: Icon(
Icons.send,
color: Palette.orange,
color: Theme.of(context).primaryColor,
),
onPressed: () {
widget.onSendTapped();
@ -343,9 +343,9 @@ class _ReplyBoxState extends State<ReplyBox> {
).then((_) => HapticFeedbackUtil.selection()),
),
IconButton(
icon: const Icon(
icon: Icon(
Icons.close,
color: Palette.orange,
color: Theme.of(context).primaryColor,
size: TextDimens.pt18,
),
onPressed: () => context.pop(),
@ -365,6 +365,7 @@ class _ReplyBoxState extends State<ReplyBox> {
child: SingleChildScrollView(
child: ItemText(
item: replyingTo,
selectable: true,
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
),

View File

@ -70,6 +70,7 @@ class TimeMachineDialog extends StatelessWidget {
CommentTile(
comment: c,
actionable: false,
collapsable: false,
fetchMode: FetchMode.eager,
),
const Divider(

View File

@ -26,7 +26,7 @@ class ProfileScreen extends StatefulWidget {
}
class _ProfileScreenState extends State<ProfileScreen>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, ItemActionMixin {
final RefreshController refreshControllerHistory = RefreshController();
final RefreshController refreshControllerFav = RefreshController();
final RefreshController refreshControllerNotification = RefreshController();
@ -99,7 +99,8 @@ class _ProfileScreenState extends State<ProfileScreen>
showWebPreviewOnStoryTile: false,
showMetadataOnStoryTile: false,
showUrl: false,
useConsistentFontSize: true,
showAuthor: false,
useSimpleTileForStory: true,
refreshController: refreshControllerHistory,
items: historyState.submittedItems
.where((Item e) => !e.dead && !e.deleted)
@ -167,7 +168,7 @@ class _ProfileScreenState extends State<ProfileScreen>
showMetadataOnStoryTile:
prefState.metadataEnabled,
showUrl: prefState.urlEnabled,
useCommentTile: true,
useSimpleTileForStory: true,
refreshController: refreshControllerFav,
items: favState.favItems,
onRefresh: () {
@ -289,9 +290,8 @@ class _ProfileScreenState extends State<ProfileScreen>
width: Dimens.pt12,
),
CustomChip(
label: 'Inbox : '
// ignore: lines_longer_than_80_chars
'${notificationState.unreadCommentsIds.length}',
label:
'''Inbox : ${notificationState.unreadCommentsIds.length}''',
selected: pageType == PageType.notification,
onSelected: (bool val) {
if (val) {

View File

@ -41,8 +41,8 @@ class InboxView extends StatelessWidget {
Expanded(
child: SmartRefresher(
enablePullUp: true,
header: const WaterDropMaterialHeader(
backgroundColor: Palette.orange,
header: WaterDropMaterialHeader(
backgroundColor: Theme.of(context).primaryColor,
),
footer: CustomFooter(
loadStyle: LoadStyle.ShowWhenLoading,
@ -79,46 +79,18 @@ class InboxView extends StatelessWidget {
),
child: InkWell(
onTap: () => onCommentTapped(e),
child: Padding(
padding: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Flex(
direction: Axis.horizontal,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: Dimens.pt8,
horizontal: Dimens.pt6,
),
child: Linkify(
text: '${e.by} : ${e.text}',
style: TextStyle(
color:
unreadCommentsIds.contains(e.id)
? textColor
: Palette.grey,
),
linkStyle: TextStyle(
color:
unreadCommentsIds.contains(e.id)
? Palette.orange
: Palette.orange
.withOpacity(0.6),
),
maxLines: 4,
onOpen: (LinkableElement link) =>
LinkUtil.launch(link.url),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Text(
e.timeAgo,
'''${e.timeAgo} from ${e.by}:''',
style: const TextStyle(
color: Palette.grey,
),
@ -128,10 +100,26 @@ class InboxView extends StatelessWidget {
),
],
),
],
Linkify(
text: e.text,
style: TextStyle(
color: unreadCommentsIds.contains(e.id)
? textColor
: Palette.grey,
fontSize: TextDimens.pt16,
),
const Divider(
height: Dimens.zero,
linkStyle: TextStyle(
color: Theme.of(context)
.primaryColor
.withOpacity(
unreadCommentsIds.contains(e.id)
? 1
: 0.6,
),
),
maxLines: 4,
onOpen: (LinkableElement link) =>
LinkUtil.launch(link.url, context),
),
],
),

View File

@ -39,9 +39,15 @@ class OfflineListTile extends StatelessWidget {
child: CustomCircularProgressIndicator(),
);
} else if (downloaded) {
return const Icon(Icons.check_circle);
return Icon(
Icons.check_circle,
color: Theme.of(context).primaryColor,
);
}
return const Icon(Icons.download);
return Icon(
Icons.download,
color: Theme.of(context).primaryColor,
);
}();
return ListTile(

View File

@ -9,6 +9,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:flutter_material_color_picker/flutter_material_color_picker.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/blocs/blocs.dart';
@ -24,6 +25,7 @@ import 'package:hacki/screens/profile/qr_code_scanner_screen.dart';
import 'package:hacki/screens/profile/qr_code_view_screen.dart';
import 'package:hacki/screens/profile/widgets/offline_list_tile.dart';
import 'package:hacki/screens/profile/widgets/tab_bar_settings.dart';
import 'package:hacki/screens/profile/widgets/text_scale_factor_settings.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
@ -47,7 +49,7 @@ class Settings extends StatefulWidget {
State<Settings> createState() => _SettingsState();
}
class _SettingsState extends State<Settings> {
class _SettingsState extends State<Settings> with ItemActionMixin {
@override
Widget build(BuildContext context) {
return BlocBuilder<PreferenceCubit, PreferenceState>(
@ -136,8 +138,9 @@ class _SettingsState extends State<Settings> {
if (fetchMode != null) {
HapticFeedbackUtil.selection();
context.read<PreferenceCubit>().update(
FetchModePreference(),
to: fetchMode.index,
FetchModePreference(
val: fetchMode.index,
),
);
}
},
@ -170,8 +173,9 @@ class _SettingsState extends State<Settings> {
if (order != null) {
HapticFeedbackUtil.selection();
context.read<PreferenceCubit>().update(
CommentsOrderPreference(),
to: order.index,
CommentsOrderPreference(
val: order.index,
),
);
}
},
@ -183,13 +187,17 @@ class _SettingsState extends State<Settings> {
],
),
const TabBarSettings(),
const TextScaleFactorSettings(),
const Divider(),
StoryTile(
showWebPreview: preferenceState.complexStoryTileEnabled,
showMetadata: preferenceState.metadataEnabled,
showUrl: preferenceState.urlEnabled,
story: Story.placeholder(),
onTap: () => LinkUtil.launch(Constants.guidelineLink),
onTap: () => LinkUtil.launch(
Constants.guidelineLink,
context,
),
),
const Divider(),
for (final Preference<dynamic> preference in preferenceState
@ -211,7 +219,7 @@ class _SettingsState extends State<Settings> {
context
.read<PreferenceCubit>()
.update(preference, to: val);
.update(preference.copyWith(val: val));
if (preference is MarkReadStoriesModePreference &&
val == false) {
@ -220,7 +228,7 @@ class _SettingsState extends State<Settings> {
.add(ClearAllReadStories());
}
},
activeColor: Palette.orange,
activeColor: Theme.of(context).primaryColor,
),
if (preference
is MarkReadStoriesModePreference) ...<Widget>[
@ -258,8 +266,9 @@ class _SettingsState extends State<Settings> {
if (storyMarkingMode != null) {
HapticFeedbackUtil.selection();
context.read<PreferenceCubit>().update(
StoryMarkingModePreference(),
to: storyMarkingMode.index,
StoryMarkingModePreference(
val: storyMarkingMode.index,
),
);
}
},
@ -281,6 +290,12 @@ class _SettingsState extends State<Settings> {
),
onTap: showThemeSettingDialog,
),
ListTile(
title: const Text(
'Accent Color',
),
onTap: showColorPicker,
),
const Divider(),
ListTile(
title: const Text(
@ -385,10 +400,9 @@ class _SettingsState extends State<Settings> {
groupValue: state.font,
onChanged: (Font? val) {
if (val != null) {
context.read<PreferenceCubit>().update(
FontPreference(),
to: val.index,
);
context
.read<PreferenceCubit>()
.update(FontPreference(val: val.index));
}
},
title: Text(
@ -396,18 +410,6 @@ class _SettingsState extends State<Settings> {
style: TextStyle(fontFamily: font.name),
),
),
const Row(
children: <Widget>[
Text(
'*Restart required',
style: TextStyle(
fontSize: TextDimens.pt12,
color: Palette.grey,
),
),
Spacer(),
],
),
],
),
);
@ -467,6 +469,32 @@ class _SettingsState extends State<Settings> {
ThemeUtil.updateStatusBarSetting(brightness, val);
}
void showColorPicker() {
showDialog<void>(
context: context,
builder: (_) {
return AlertDialog(
contentPadding: const EdgeInsets.all(Dimens.pt18),
title: Text(AppColorPreference().title),
content: MaterialColorPicker(
colors: materialColors,
selectedColor: context.read<PreferenceCubit>().state.appColor,
onMainColorChange: (ColorSwatch<dynamic>? color) {
CommentTile.levelToBorderColors.clear();
context.read<PreferenceCubit>().update(
AppColorPreference(
val: materialColors.indexOf(color ?? Palette.deepOrange),
),
);
context.pop();
},
onBack: context.pop,
),
);
},
);
}
void showClearCacheDialog() {
showDialog<void>(
context: context,
@ -479,10 +507,10 @@ class _SettingsState extends State<Settings> {
actions: <Widget>[
TextButton(
onPressed: () => context.pop(),
child: const Text(
child: Text(
'Cancel',
style: TextStyle(
color: Palette.orange,
color: Theme.of(context).primaryColor,
),
),
),
@ -540,6 +568,7 @@ class _SettingsState extends State<Settings> {
ElevatedButton(
onPressed: () => LinkUtil.launch(
Constants.portfolioLink,
context,
),
child: const Row(
children: <Widget>[
@ -556,6 +585,7 @@ class _SettingsState extends State<Settings> {
ElevatedButton(
onPressed: () => LinkUtil.launch(
Constants.privacyPolicyLink,
context,
),
child: const Row(
children: <Widget>[
@ -586,6 +616,7 @@ class _SettingsState extends State<Settings> {
ElevatedButton(
onPressed: () => LinkUtil.launch(
Constants.githubLink,
context,
),
child: const Row(
children: <Widget>[
@ -604,6 +635,7 @@ class _SettingsState extends State<Settings> {
Platform.isIOS
? Constants.appStoreLink
: Constants.googlePlayLink,
context,
),
child: const Row(
children: <Widget>[
@ -620,6 +652,7 @@ class _SettingsState extends State<Settings> {
ElevatedButton(
onPressed: () => LinkUtil.launch(
Constants.sponsorLink,
context,
),
child: const Row(
children: <Widget>[

View File

@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/styles/styles.dart';
class TextScaleFactorSettings extends StatelessWidget {
const TextScaleFactorSettings({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<PreferenceCubit, PreferenceState>(
buildWhen: (PreferenceState previous, PreferenceState current) =>
previous.textScaleFactor != current.textScaleFactor,
builder: (BuildContext context, PreferenceState state) {
final String label = state.textScaleFactor == 1
? '''system default ${MediaQuery.of(context).textScaleFactor.toStringAsPrecision(2)}'''
: state.textScaleFactor.toString();
return Column(
children: <Widget>[
Row(
children: <Widget>[
const SizedBox(
width: Dimens.pt16,
),
Text('Text scale factor: $label'),
const Spacer(),
],
),
Slider(
value: state.textScaleFactor,
min: 0.8,
max: 1.5,
divisions: 7,
label: label,
onChanged: (double value) => context
.read<PreferenceCubit>()
.update(TextScaleFactorPreference(val: value)),
),
],
);
},
);
}
}

View File

@ -3,3 +3,4 @@ export 'inbox_view.dart';
export 'offline_list_tile.dart';
export 'settings.dart';
export 'tab_bar_settings.dart';
export 'text_scale_factor_settings.dart';

View File

@ -27,7 +27,7 @@ class SearchScreen extends StatefulWidget {
_SearchScreenState createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
class _SearchScreenState extends State<SearchScreen> with ItemActionMixin {
final RefreshController refreshController = RefreshController();
final ScrollController scrollController = ScrollController();
final Debouncer debouncer = Debouncer(delay: Durations.oneSecond);
@ -57,18 +57,24 @@ class _SearchScreenState extends State<SearchScreen> {
body: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ColoredBox(
color: Theme.of(context).canvasColor,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.pt12,
),
child: TextField(
cursorColor: Palette.orange,
cursorColor: Theme.of(context).primaryColor,
autocorrect: false,
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: 'Search Hacker News',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Palette.orange),
borderSide: BorderSide(
color: Theme.of(context).primaryColor,
),
),
),
onChanged: (String val) {
@ -185,8 +191,9 @@ class _SearchScreenState extends State<SearchScreen> {
),
PostedByFilterChip(
filter: state.params.get<PostedByFilter>(),
onChanged:
context.read<SearchCubit>().onPostedByChanged,
onChanged: context
.read<SearchCubit>()
.onPostedByChanged,
),
const SizedBox(
width: Dimens.pt8,
@ -225,8 +232,9 @@ class _SearchScreenState extends State<SearchScreen> {
width: Dimens.pt8,
),
CustomChip(
onSelected: (_) =>
context.read<SearchCubit>().onToggled(filter),
onSelected: (_) => context
.read<SearchCubit>()
.onToggled(filter),
selected: context
.read<SearchCubit>()
.state
@ -239,6 +247,9 @@ class _SearchScreenState extends State<SearchScreen> {
],
),
),
],
),
),
if (state.status == SearchStatus.loading &&
state.results.isEmpty) ...<Widget>[
const SizedBox(
@ -266,8 +277,8 @@ class _SearchScreenState extends State<SearchScreen> {
child: SmartRefresher(
enablePullDown: false,
enablePullUp: true,
header: const WaterDropMaterialHeader(
backgroundColor: Palette.orange,
header: WaterDropMaterialHeader(
backgroundColor: Theme.of(context).primaryColor,
),
footer: CustomFooter(
loadStyle: LoadStyle.ShowWhenLoading,
@ -323,6 +334,8 @@ class _SearchScreenState extends State<SearchScreen> {
FadeIn(
child: CommentTile(
actionable: false,
collapsable: false,
selectable: false,
comment: e,
fetchMode: FetchMode.eager,
onTap: () => goToItemScreen(

View File

@ -48,12 +48,13 @@ class PostedByFilterChip extends StatelessWidget {
),
child: TextField(
controller: usernameController,
cursorColor: Palette.orange,
cursorColor: Theme.of(context).primaryColor,
autocorrect: false,
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: 'Username',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Palette.orange),
borderSide:
BorderSide(color: Theme.of(context).primaryColor),
),
),
),
@ -85,8 +86,9 @@ class PostedByFilterChip extends StatelessWidget {
context.pop(text.isEmpty ? null : text);
},
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(Palette.deepOrange),
backgroundColor: MaterialStateProperty.all(
Theme.of(context).primaryColor,
),
),
child: const Text(
'Confirm',

View File

@ -16,7 +16,7 @@ class SubmitScreen extends StatefulWidget {
_SubmitScreenState createState() => _SubmitScreenState();
}
class _SubmitScreenState extends State<SubmitScreen> {
class _SubmitScreenState extends State<SubmitScreen> with ItemActionMixin {
final TextEditingController titleEditingController = TextEditingController();
final TextEditingController urlEditingController = TextEditingController();
final TextEditingController textEditingController = TextEditingController();
@ -51,7 +51,10 @@ class _SubmitScreenState extends State<SubmitScreen> {
backgroundColor: Theme.of(context).canvasColor,
elevation: Dimens.zero,
leading: IconButton(
icon: const Icon(Icons.close),
icon: Icon(
Icons.close,
color: Theme.of(context).colorScheme.onSurface,
),
onPressed: () {
showDialog<bool>(
context: context,
@ -83,13 +86,16 @@ class _SubmitScreenState extends State<SubmitScreen> {
});
},
),
title: const Text(
title: Text(
'Submit',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
),
),
actions: <Widget>[
if (state.status == Status.inProgress)
const Padding(
padding: EdgeInsets.symmetric(
Padding(
padding: const EdgeInsets.symmetric(
vertical: Dimens.pt18,
horizontal: Dimens.pt16,
),
@ -97,16 +103,16 @@ class _SubmitScreenState extends State<SubmitScreen> {
height: Dimens.pt20,
width: Dimens.pt20,
child: CircularProgressIndicator(
color: Palette.orange,
color: Theme.of(context).primaryColor,
strokeWidth: 2,
),
),
)
else if (canSubmit())
IconButton(
icon: const Icon(
icon: Icon(
Icons.send,
color: Palette.orange,
color: Theme.of(context).primaryColor,
),
onPressed: context.read<SubmitCubit>().onSubmitTapped,
)
@ -128,13 +134,14 @@ class _SubmitScreenState extends State<SubmitScreen> {
),
child: TextField(
controller: titleEditingController,
cursorColor: Palette.orange,
cursorColor: Theme.of(context).primaryColor,
autocorrect: false,
maxLength: 80,
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: 'Title',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Palette.orange),
borderSide:
BorderSide(color: Theme.of(context).primaryColor),
),
),
onChanged: context.read<SubmitCubit>().onTitleChanged,
@ -147,12 +154,13 @@ class _SubmitScreenState extends State<SubmitScreen> {
child: TextField(
enabled: textEditingController.text.isEmpty,
controller: urlEditingController,
cursorColor: Palette.orange,
cursorColor: Theme.of(context).primaryColor,
autocorrect: false,
decoration: const InputDecoration(
decoration: InputDecoration(
hintText: 'Url',
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Palette.orange),
borderSide:
BorderSide(color: Theme.of(context).primaryColor),
),
),
onChanged: context.read<SubmitCubit>().onUrlChanged,
@ -174,7 +182,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
child: TextField(
enabled: urlEditingController.text.isEmpty,
controller: textEditingController,
cursorColor: Palette.orange,
cursorColor: Theme.of(context).primaryColor,
maxLines: 200,
decoration: const InputDecoration(
hintText: 'Text',

View File

@ -23,6 +23,8 @@ class CommentTile extends StatelessWidget {
this.onRightMoreTapped,
this.opUsername,
this.actionable = true,
this.collapsable = true,
this.selectable = true,
this.level = 0,
this.onTap,
this.itemScrollController,
@ -32,6 +34,8 @@ class CommentTile extends StatelessWidget {
final Comment comment;
final int level;
final bool actionable;
final bool collapsable;
final bool selectable;
final FetchMode fetchMode;
final ItemScrollController? itemScrollController;
@ -43,7 +47,7 @@ class CommentTile extends StatelessWidget {
/// Override for search screen.
final VoidCallback? onTap;
static final Map<int, Color> _colors = <int, Color>{};
static final Map<int, Color> levelToBorderColors = <int, Color>{};
@override
Widget build(BuildContext context) {
@ -64,8 +68,12 @@ class CommentTile extends StatelessWidget {
) {
if (actionable && state.hidden) return const SizedBox.shrink();
const Color orange = Color.fromRGBO(255, 152, 0, 1);
final Color color = _getColor(level);
final MaterialColor primaryColor =
context.read<PreferenceCubit>().state.appColor;
final Color color = _getColor(
level,
primaryColor: primaryColor,
);
final Widget child = DeviceGestureWrapper(
child: Column(
@ -78,16 +86,18 @@ class CommentTile extends StatelessWidget {
children: <Widget>[
SlidableAction(
onPressed: (_) => onReplyTapped?.call(comment),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
icon: Icons.message,
),
if (context.read<AuthBloc>().state.user.id ==
comment.by)
SlidableAction(
onPressed: (_) => onEditTapped?.call(comment),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
icon: Icons.edit,
),
SlidableAction(
@ -96,8 +106,9 @@ class CommentTile extends StatelessWidget {
comment,
context.rect,
),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
icon: Icons.more_horiz,
),
],
@ -110,8 +121,9 @@ class CommentTile extends StatelessWidget {
SlidableAction(
onPressed: (_) =>
onRightMoreTapped?.call(comment),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
icon: Icons.av_timer,
),
],
@ -119,7 +131,7 @@ class CommentTile extends StatelessWidget {
: null,
child: InkWell(
onTap: () {
if (actionable) {
if (collapsable) {
_collapse(context);
} else {
onTap?.call();
@ -139,18 +151,16 @@ class CommentTile extends StatelessWidget {
Text(
comment.by,
style: TextStyle(
color: prefState.eyeCandyEnabled
? orange
: color,
color: primaryColor,
),
textScaleFactor:
MediaQuery.of(context).textScaleFactor,
),
if (comment.by == opUsername)
const Text(
Text(
' - OP',
style: TextStyle(
color: orange,
color: primaryColor,
),
),
const Spacer(),
@ -174,7 +184,9 @@ class CommentTile extends StatelessWidget {
CenteredText(
text:
'''collapsed (${state.collapsedCount + 1})''',
color: Palette.orangeAccent,
color: Theme.of(context)
.primaryColor
.withOpacity(0.8),
)
else if (comment.hidden)
const CenteredText.hidden()
@ -200,6 +212,7 @@ class CommentTile extends StatelessWidget {
child: ItemText(
key: ValueKey<int>(comment.id),
item: comment,
selectable: selectable,
textScaleFactor: MediaQuery.of(context)
.textScaleFactor,
onTap: () {
@ -271,14 +284,17 @@ class CommentTile extends StatelessWidget {
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Palette.orange.withOpacity(0.2),
color: Theme.of(context).primaryColor.withOpacity(0.2),
),
child: wrapper,
);
}
for (final int i in level.to(0, inclusive: false)) {
final Color wrapperBorderColor = _getColor(i);
final Color wrapperBorderColor = _getColor(
i,
primaryColor: primaryColor,
);
final bool shouldHighlight = isMyComment && i == level;
wrapper = Container(
clipBehavior: Clip.hardEdge,
@ -294,7 +310,7 @@ class CommentTile extends StatelessWidget {
)
: null,
color: shouldHighlight
? Palette.orange.withOpacity(0.2)
? primaryColor.withOpacity(0.2)
: commentColor,
),
child: wrapper,
@ -307,31 +323,27 @@ class CommentTile extends StatelessWidget {
);
}
Color _getColor(int level) {
Color _getColor(
int level, {
required MaterialColor primaryColor,
}) {
final int initialLevel = level;
if (_colors[initialLevel] != null) return _colors[initialLevel]!;
if (levelToBorderColors[initialLevel] != null) {
return levelToBorderColors[initialLevel]!;
} else if (level == 0) {
levelToBorderColors[initialLevel] = primaryColor;
return primaryColor;
}
while (level >= 10) {
level = level - 10;
}
const int r = 255;
int g = level * 40 < 255 ? 152 : (level * 20).clamp(0, 255);
int b = (level * 40).clamp(0, 255);
final double opacity = ((10 - level) / 10).clamp(0.3, 1);
final Color color = primaryColor.withOpacity(opacity);
if (g == 255 && b == 255) {
g = (level * 30 - 255).clamp(0, 255);
b = (level * 40 - 255).clamp(0, 255);
}
final Color color = Color.fromRGBO(
r,
g,
b,
1,
);
_colors[initialLevel] = color;
levelToBorderColors[initialLevel] = color;
return color;
}
@ -353,17 +365,20 @@ class CommentTile extends StatelessWidget {
}
void _collapse(BuildContext context) {
HapticFeedbackUtil.selection();
context.read<CollapseCubit>().collapse();
if (context.read<CollapseCubit>().state.collapsed &&
context.read<PreferenceCubit>().state.autoScrollEnabled) {
final PreferenceCubit preferenceCubit = context.read<PreferenceCubit>();
final CollapseCubit collapseCubit = context.read<CollapseCubit>()
..collapse(onStateChanged: HapticFeedbackUtil.selection);
if (collapseCubit.state.collapsed &&
preferenceCubit.state.autoScrollEnabled) {
final List<Comment> comments =
context.read<CommentsCubit>().state.comments;
final int indexOfNextComment = comments.indexOf(comment) + 1;
if (indexOfNextComment < comments.length) {
Future<void>.delayed(
Durations.ms300,
() {
itemScrollController?.scrollTo(
index:
context.read<CommentsCubit>().state.comments.indexOf(comment) +
1,
index: indexOfNextComment,
alignment: 0.1,
duration: Durations.ms300,
);
@ -371,4 +386,5 @@ class CommentTile extends StatelessWidget {
);
}
}
}
}

View File

@ -17,7 +17,7 @@ class CountdownReminder extends StatefulWidget {
}
class _CountDownReminderState extends State<CountdownReminder>
with SingleTickerProviderStateMixin {
with SingleTickerProviderStateMixin, ItemActionMixin {
late final AnimationController animationController;
late final Animation<double> progressAnimation;
late final Animation<double> opacityAnimation;
@ -96,7 +96,7 @@ class _CountDownReminderState extends State<CountdownReminder>
animation: animationController,
child: FadeIn(
child: Material(
color: Palette.deepOrange,
color: Theme.of(context).primaryColor,
clipBehavior: Clip.hardEdge,
borderRadius: const BorderRadius.all(
Radius.circular(
@ -125,8 +125,8 @@ class _CountDownReminderState extends State<CountdownReminder>
},
child: Column(
children: <Widget>[
const Padding(
padding: EdgeInsets.only(
Padding(
padding: const EdgeInsets.only(
left: Dimens.pt12,
top: Dimens.pt10,
right: Dimens.pt10,
@ -136,15 +136,15 @@ class _CountDownReminderState extends State<CountdownReminder>
Text(
'Pick up where you left off',
style: TextStyle(
color: Palette.white,
color: Theme.of(context).colorScheme.onPrimary,
fontSize: TextDimens.pt12,
),
),
Spacer(),
const Spacer(),
Icon(
Icons.arrow_forward_ios,
size: TextDimens.pt12,
color: Palette.white,
color: Theme.of(context).colorScheme.onPrimary,
),
],
),

View File

@ -19,13 +19,17 @@ class CustomChip extends StatelessWidget {
shadowColor: Palette.transparent,
selectedShadowColor: Palette.transparent,
backgroundColor: Palette.transparent,
shape: const StadiumBorder(
side: BorderSide(color: Palette.orange),
shape: StadiumBorder(
side: BorderSide(color: Theme.of(context).primaryColor),
),
label: Text(label),
labelStyle: TextStyle(
color: selected ? Theme.of(context).colorScheme.onPrimary : null,
),
checkmarkColor: selected ? Theme.of(context).colorScheme.onPrimary : null,
selected: selected,
onSelected: onSelected,
selectedColor: Palette.orange,
selectedColor: Theme.of(context).primaryColor,
);
}
}

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:hacki/styles/styles.dart';
/// Circular progress indicator with color.
/// Changing `colorScheme`'s `primary` color doesn't work because it changes
@ -18,7 +17,7 @@ class CustomCircularProgressIndicator extends StatelessWidget {
Widget build(BuildContext context) {
return CircularProgressIndicator(
strokeWidth: strokeWidth,
valueColor: const AlwaysStoppedAnimation<Color>(Palette.orange),
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
);
}
}

View File

@ -1,7 +1,9 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
import 'package:hacki/styles/palette.dart';
import 'package:hacki/utils/utils.dart';
import 'package:linkify/linkify.dart' hide UrlLinkifier;
@ -35,7 +37,7 @@ class Linkify extends StatelessWidget {
this.textDirection,
this.maxLines,
this.overflow = TextOverflow.clip,
this.textScaleFactor = 1.0,
this.textScaleFactor,
this.softWrap = true,
this.strutStyle,
this.locale,
@ -78,7 +80,7 @@ class Linkify extends StatelessWidget {
final TextOverflow overflow;
/// The number of font pixels for each logical pixel
final double textScaleFactor;
final double? textScaleFactor;
/// Whether the text should break at soft line breaks.
final bool softWrap;
@ -108,6 +110,7 @@ class Linkify extends StatelessWidget {
return Text.rich(
buildTextSpan(
elements,
primaryColor: context.read<PreferenceCubit>().state.appColor,
style: Theme.of(context).textTheme.bodyMedium?.merge(style),
onOpen: onOpen,
useMouseRegion: true,
@ -139,7 +142,9 @@ const UrlLinkifier _urlLinkifier = UrlLinkifier();
const EmailLinkifier _emailLinkifier = EmailLinkifier();
const QuoteLinkifier _quoteLinkifier = QuoteLinkifier();
const EmphasisLinkifier _emphasisLinkifier = EmphasisLinkifier();
const CodeLinkifier _codeLinkifier = CodeLinkifier();
const List<Linkifier> defaultLinkifiers = <Linkifier>[
_codeLinkifier,
_urlLinkifier,
_emailLinkifier,
_quoteLinkifier,
@ -165,7 +170,7 @@ class SelectableLinkify extends StatelessWidget {
this.maxLines,
// SelectableText
this.focusNode,
this.textScaleFactor = 1.0,
this.textScaleFactor,
this.strutStyle,
this.showCursor = false,
this.autofocus = false,
@ -190,7 +195,7 @@ class SelectableLinkify extends StatelessWidget {
final String? semanticsLabel;
/// The number of font pixels for each logical pixel
final double textScaleFactor;
final double? textScaleFactor;
/// Linkifiers to be used for linkify
final List<Linkifier> linkifiers;
@ -284,6 +289,7 @@ class SelectableLinkify extends StatelessWidget {
return SelectableText.rich(
buildTextSpan(
elements,
primaryColor: context.read<PreferenceCubit>().state.appColor,
style: Theme.of(context).textTheme.bodyMedium?.merge(style),
onOpen: onOpen,
linkStyle: Theme.of(context)
@ -349,6 +355,7 @@ class LinkableSpan extends WidgetSpan {
/// Raw TextSpan builder for more control on the RichText
TextSpan buildTextSpan(
List<LinkifyElement> elements, {
required MaterialColor primaryColor,
TextStyle? style,
TextStyle? linkStyle,
LinkCallback? onOpen,
@ -383,7 +390,7 @@ TextSpan buildTextSpan(
return TextSpan(
text: element.text,
style: style?.copyWith(
backgroundColor: Palette.orangeAccent.withOpacity(0.3),
backgroundColor: primaryColor.withOpacity(0.3),
),
);
} else if (element is EmphasisElement) {
@ -391,6 +398,14 @@ TextSpan buildTextSpan(
text: element.text,
style: style?.copyWith(
fontStyle: FontStyle.italic,
fontWeight: FontWeight.bold,
),
);
} else if (element is CodeElement) {
return TextSpan(
text: element.text,
style: style?.copyWith(
fontFamily: Font.ubuntuMono.name,
),
);
}

View File

@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:linkify/linkify.dart';
final RegExp _codeRegex =
RegExp(r'\<pre\>\<code\>(.*?)\<\/code\>\<\/pre\>', dotAll: true);
class CodeLinkifier extends Linkifier {
const CodeLinkifier();
static const String _openTag = '<pre><code>';
static const String _closeTag = '</code></pre>';
@override
List<LinkifyElement> parse(
List<LinkifyElement> elements,
LinkifyOptions options,
) {
final List<LinkifyElement> list = <LinkifyElement>[];
for (final LinkifyElement element in elements) {
if (element is TextElement) {
final RegExpMatch? match = _codeRegex.firstMatch(
element.text.trimLeft(),
);
if (match == null || match.group(0) == null || match.group(1) == null) {
list.add(element);
} else {
final String matchedText = match.group(0)!;
final num pos = element.text.indexOf(matchedText);
final List<String> splitTexts = element.text.split(matchedText);
int curPos = 0;
bool added = false;
for (final String text in splitTexts) {
list.addAll(parse(<LinkifyElement>[TextElement(text)], options));
curPos += text.length;
if (!added && curPos >= pos) {
added = true;
final String trimmedText = matchedText
.replaceFirst(_openTag, '')
.replaceFirst(_closeTag, '');
list.add(CodeElement(trimmedText));
}
}
}
} else {
list.add(element);
}
}
return list;
}
}
/// Represents an element that is wrapped by <code> tag.
@immutable
class CodeElement extends LinkifyElement {
CodeElement(super.text);
@override
String toString() {
return "CodeElement: '$text'";
}
@override
bool operator ==(Object other) => equals(other);
@override
bool equals(dynamic other) => other is CodeElement && super.equals(other);
@override
int get hashCode => text.hashCode;
}

View File

@ -1,3 +1,4 @@
export 'code_linkifier.dart';
export 'emphasis_linkifier.dart';
export 'quote_linkifier.dart';
export 'url_linkifier.dart';

View File

@ -1,8 +1,10 @@
import 'package:flutter/foundation.dart';
import 'dart:math';
import 'package:flutter/widgets.dart' show StringCharacters, immutable;
import 'package:linkify/linkify.dart';
final RegExp _urlRegex = RegExp(
r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[\/\\\%:\?=&#@;A-Za-z0-9_.~-]*)',
r'^(.*?)((?:https?:\/\/|www\.)[^\s/$.?#].[\/\\\%:\?=&#@;A-Za-z0-9()+_.,~-]*)',
caseSensitive: false,
dotAll: true,
);
@ -62,6 +64,29 @@ class UrlLinkifier extends Linkifier {
originalUrl;
}
if (url.contains(')')) {
int openCount = 0;
int closeCount = 0;
for (final String c in url.characters) {
if (c == '(') {
openCount++;
} else if (c == ')') {
closeCount++;
}
}
if (openCount != closeCount) {
final int index = max(0, url.lastIndexOf(')'));
url = url.substring(0, index);
end = originalUrl.substring(index);
}
}
if (url.endsWith(',')) {
url = url.substring(0, max(0, url.length - 1));
end = '$end,';
}
if ((options.humanize) || (options.removeWww)) {
if (options.humanize) {
url = url.replaceFirst(RegExp('https?://'), '');
@ -70,15 +95,9 @@ class UrlLinkifier extends Linkifier {
url = url.replaceFirst(RegExp(r'www\.'), '');
}
list.add(
UrlElement(
originalUrl,
url,
originText,
),
);
list.add(UrlElement(originalUrl, url, originText));
} else {
list.add(UrlElement(originalUrl, null, originText));
list.add(UrlElement(url, url, originText));
}
if (end != null) {

View File

@ -43,9 +43,9 @@ class _CustomTabBarState extends State<CustomTabBar> {
return TabBar(
isScrollable: true,
controller: widget.tabController,
indicatorColor: Palette.orange,
indicatorColor: Theme.of(context).primaryColor,
indicator: CircleTabIndicator(
color: Palette.orange,
color: Theme.of(context).primaryColor,
radius: Dimens.pt2,
),
indicatorPadding: const EdgeInsets.only(
@ -64,7 +64,9 @@ class _CustomTabBarState extends State<CustomTabBar> {
style: TextStyle(
fontSize:
currentIndex == i ? TextDimens.pt14 : TextDimens.pt10,
color: currentIndex == i ? Palette.orange : Palette.grey,
color: currentIndex == i
? Theme.of(context).primaryColor
: Palette.grey,
),
duration: Durations.ms200,
child: Text(
@ -110,8 +112,9 @@ class _CustomTabBarState extends State<CustomTabBar> {
size: currentIndex == 5
? TextDimens.pt16
: TextDimens.pt12,
color:
currentIndex == 5 ? Palette.orange : Palette.grey,
color: currentIndex == 5
? Theme.of(context).primaryColor
: Palette.grey,
),
);
},

View File

@ -4,20 +4,24 @@ import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
class ItemText extends StatelessWidget {
const ItemText({
required this.item,
required this.textScaleFactor,
required this.selectable,
super.key,
this.onTap,
});
final Item item;
final VoidCallback? onTap;
final double textScaleFactor;
final bool selectable;
/// Reserved for collapsing a comment tile when
/// [CollapseModePreference] is enabled;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
@ -28,18 +32,34 @@ class ItemText extends StatelessWidget {
final TextStyle linkStyle = TextStyle(
fontSize: prefState.fontSize.fontSize,
decoration: TextDecoration.underline,
color: Palette.orange,
color: Theme.of(context).primaryColor,
);
if (item is Buildable) {
void onSelectionChanged(
TextSelection selection,
SelectionChangedCause? cause,
) {
if (cause == SelectionChangedCause.longPress &&
selection.baseOffset != selection.extentOffset) {
context.tryRead<CollapseCubit>()?.lock();
}
}
if (selectable && item is Buildable) {
return SelectableText.rich(
buildTextSpan(
(item as Buildable).elements,
primaryColor: context.read<PreferenceCubit>().state.appColor,
style: style,
linkStyle: linkStyle,
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
onOpen: (LinkableElement link) => LinkUtil.launch(
link.url,
context,
),
),
onTap: onTap,
textScaleFactor: textScaleFactor,
onSelectionChanged: onSelectionChanged,
contextMenuBuilder: (
BuildContext context,
EditableTextState editableTextState,
@ -52,24 +72,37 @@ class ItemText extends StatelessWidget {
semanticsLabel: item.text,
);
} else {
return SelectableLinkify(
if (item is Buildable) {
return InkWell(
child: Text.rich(
buildTextSpan(
(item as Buildable).elements,
primaryColor: context.read<PreferenceCubit>().state.appColor,
style: style,
linkStyle: linkStyle,
onOpen: (LinkableElement link) => LinkUtil.launch(
link.url,
context,
),
),
textScaleFactor: textScaleFactor,
semanticsLabel: item.text,
),
);
} else {
return InkWell(
child: Linkify(
text: item.text,
textScaleFactor: textScaleFactor,
style: style,
linkStyle: linkStyle,
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
onTap: onTap,
contextMenuBuilder: (
BuildContext context,
EditableTextState editableTextState,
) =>
contextMenuBuilder(
onOpen: (LinkableElement link) => LinkUtil.launch(
link.url,
context,
editableTextState,
item: item,
),
semanticsLabel: item.text,
),
);
}
}
}
}

View File

@ -20,11 +20,10 @@ class ItemsListView<T extends Item> extends StatelessWidget {
required this.onTap,
required this.refreshController,
super.key,
this.useCommentTile = false,
this.showCommentBy = false,
this.showAuthor = true,
this.useSimpleTileForStory = false,
this.enablePullDown = true,
this.markReadStories = false,
this.useConsistentFontSize = false,
this.showOfflineBanner = false,
this.onRefresh,
this.onLoadMore,
@ -35,8 +34,8 @@ class ItemsListView<T extends Item> extends StatelessWidget {
this.itemBuilder,
});
final bool useCommentTile;
final bool showCommentBy;
final bool showAuthor;
final bool useSimpleTileForStory;
final bool showWebPreviewOnStoryTile;
final bool showMetadataOnStoryTile;
final bool showUrl;
@ -44,9 +43,6 @@ class ItemsListView<T extends Item> extends StatelessWidget {
final bool markReadStories;
final bool showOfflineBanner;
/// Whether to use same font size for comment and story tiles.
final bool useConsistentFontSize;
final List<T> items;
final Widget? header;
final RefreshController refreshController;
@ -76,6 +72,56 @@ class ItemsListView<T extends Item> extends StatelessWidget {
final bool swipeGestureEnabled =
context.read<PreferenceCubit>().state.swipeGestureEnabled;
return <Widget>[
if (useSimpleTileForStory)
FadeIn(
child: InkWell(
onTap: () => onTap(e),
child: Padding(
padding: const EdgeInsets.only(
top: Dimens.pt8,
bottom: Dimens.pt8,
left: Dimens.pt12,
right: Dimens.pt6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
children: <Widget>[
Text(
showAuthor
? '''${e.timeAgo} by ${e.by}'''
: e.timeAgo,
style: const TextStyle(
color: Palette.grey,
),
),
const SizedBox(
width: Dimens.pt12,
),
],
),
Linkify(
text: e.title,
maxLines: 4,
style: const TextStyle(
fontSize: TextDimens.pt16,
),
linkStyle: TextStyle(
color: Theme.of(context).primaryColor,
),
onOpen: (LinkableElement link) => LinkUtil.launch(
link.url,
context,
),
),
],
),
),
),
)
else
GestureDetector(
/// If swipe gesture is enabled on home screen, use long press
/// instead of slide action to trigger the action menu.
@ -91,44 +137,26 @@ class ItemsListView<T extends Item> extends StatelessWidget {
showMetadata: showMetadataOnStoryTile,
showUrl: showUrl,
hasRead: markReadStories && hasRead,
simpleTileFontSize: useConsistentFontSize
? TextDimens.pt14
: TextDimens.pt16,
),
),
),
if (!showWebPreviewOnStoryTile)
if (useSimpleTileForStory || !showWebPreviewOnStoryTile)
const Divider(
height: Dimens.zero,
),
];
} else if (e is Comment) {
if (useCommentTile) {
return <Widget>[
if (showWebPreviewOnStoryTile)
const Divider(
height: Dimens.zero,
),
_CommentTile(
comment: e,
onTap: () => onTap(e),
fontSize: showWebPreviewOnStoryTile
? TextDimens.pt14
: TextDimens.pt16,
),
const Divider(
height: Dimens.zero,
),
];
}
return <Widget>[
FadeIn(
child: Padding(
padding: const EdgeInsets.only(left: Dimens.pt6),
child: InkWell(
onTap: () => onTap(e),
child: Padding(
padding: EdgeInsets.zero,
padding: const EdgeInsets.only(
top: Dimens.pt8,
bottom: Dimens.pt8,
left: Dimens.pt12,
right: Dimens.pt6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
@ -144,32 +172,16 @@ class ItemsListView<T extends Item> extends StatelessWidget {
),
),
),
Flex(
direction: Axis.horizontal,
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: Dimens.pt8,
horizontal: Dimens.pt6,
),
child: Linkify(
text:
'''${showCommentBy ? '${e.by}: ' : ''}${e.text}''',
maxLines: 4,
linkStyle: const TextStyle(
color: Palette.orange,
),
onOpen: (LinkableElement link) =>
LinkUtil.launch(link.url),
),
),
),
Row(
children: <Widget>[
Text(
e.timeAgo,
showAuthor
? '''${e.timeAgo} by ${e.by}'''
: e.timeAgo,
style: const TextStyle(
color: Palette.grey,
),
@ -179,13 +191,23 @@ class ItemsListView<T extends Item> extends StatelessWidget {
),
],
),
],
Linkify(
text: e.text,
maxLines: 4,
style: const TextStyle(
fontSize: TextDimens.pt16,
),
linkStyle: TextStyle(
color: Theme.of(context).primaryColor,
),
onOpen: (LinkableElement link) => LinkUtil.launch(
link.url,
context,
),
const Divider(
height: Dimens.zero,
),
],
),
],
),
),
),
@ -211,8 +233,8 @@ class ItemsListView<T extends Item> extends StatelessWidget {
return SmartRefresher(
enablePullUp: true,
enablePullDown: enablePullDown,
header: const WaterDropMaterialHeader(
backgroundColor: Palette.orange,
header: WaterDropMaterialHeader(
backgroundColor: Theme.of(context).primaryColor,
),
footer: CustomFooter(
loadStyle: LoadStyle.ShowWhenLoading,
@ -242,66 +264,3 @@ class ItemsListView<T extends Item> extends StatelessWidget {
);
}
}
class _CommentTile extends StatelessWidget {
const _CommentTile({
required this.comment,
required this.onTap,
this.fontSize = 16,
});
final Comment comment;
final VoidCallback onTap;
final double fontSize;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.only(
left: Dimens.pt12,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(
height: Dimens.pt8,
),
Row(
children: <Widget>[
Expanded(
child: Text(
comment.text,
style: TextStyle(
fontSize: fontSize,
),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
],
),
Row(
children: <Widget>[
Expanded(
child: Text(
comment.metadata,
style: TextStyle(
color: Palette.grey,
fontSize: fontSize - 2,
),
maxLines: 1,
),
),
],
),
const SizedBox(
height: Dimens.pt8,
),
],
),
),
);
}
}

View File

@ -196,6 +196,7 @@ class LinkView extends StatelessWidget {
if (url.isNotEmpty) {
LinkUtil.launch(
url,
context,
useHackiForHnLink: false,
offlineReading:
context.read<StoriesBloc>().state.isOfflineReading,

View File

@ -27,7 +27,7 @@ class OfflineBanner extends StatelessWidget {
'${showExitButton ? 'Exit to fetch latest stories.' : ''}',
textAlign: showExitButton ? TextAlign.left : TextAlign.center,
),
backgroundColor: Palette.orangeAccent.withOpacity(0.3),
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.3),
actions: <Widget>[
if (showExitButton)
TextButton(

View File

@ -24,7 +24,7 @@ class _OnboardingViewState extends State<OnboardingView> {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).brightness == Brightness.light
? Palette.orange
? Theme.of(context).primaryColor
: Theme.of(context).canvasColor,
elevation: Dimens.zero,
leading: IconButton(
@ -36,7 +36,7 @@ class _OnboardingViewState extends State<OnboardingView> {
),
),
backgroundColor: Theme.of(context).brightness == Brightness.light
? Palette.orange
? Theme.of(context).primaryColor
: null,
body: Stack(
children: <Widget>[
@ -89,7 +89,7 @@ class _OnboardingViewState extends State<OnboardingView> {
},
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
backgroundColor: Palette.orange,
backgroundColor: Theme.of(context).primaryColor,
padding: const EdgeInsets.all(
Dimens.pt18,
),

View File

@ -7,7 +7,6 @@ import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/styles/styles.dart';
import 'package:hacki/utils/utils.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:visibility_detector/visibility_detector.dart';
@ -28,7 +27,8 @@ class StoriesListView extends StatefulWidget {
State<StoriesListView> createState() => _StoriesListViewState();
}
class _StoriesListViewState extends State<StoriesListView> {
class _StoriesListViewState extends State<StoriesListView>
with ItemActionMixin {
final RefreshController refreshController = RefreshController();
final ScrollController scrollController = ScrollController();
@ -106,8 +106,9 @@ class _StoriesListViewState extends State<StoriesListView> {
HapticFeedbackUtil.light();
context.read<PinCubit>().pinStory(story);
},
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
icon: preferenceState.complexStoryTileEnabled
? Icons.push_pin_outlined
: null,
@ -117,8 +118,9 @@ class _StoriesListViewState extends State<StoriesListView> {
),
SlidableAction(
onPressed: (_) => onMoreTapped(story, context.rect),
backgroundColor: Palette.orange,
foregroundColor: Palette.white,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
icon: preferenceState.complexStoryTileEnabled
? Icons.more_horiz
: null,

View File

@ -96,6 +96,7 @@ class StoryTile extends StatelessWidget {
.textTheme
.bodyLarge
?.color,
fontWeight: hasRead ? null : FontWeight.w500,
fontSize: simpleTileFontSize,
),
),
@ -154,8 +155,8 @@ class _LinkPreviewPlaceholder extends StatelessWidget {
child: SizedBox(
height: height,
child: Shimmer.fromColors(
baseColor: Palette.orange,
highlightColor: Palette.orangeAccent,
baseColor: Theme.of(context).primaryColor,
highlightColor: Theme.of(context).primaryColor.withOpacity(0.8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[

View File

@ -6,6 +6,7 @@ class CollapseCache {
final Map<int, Set<int>> _hidden = <int, Set<int>>{};
final PublishSubject<Map<int, Set<int>>> _hiddenCommentsSubject =
PublishSubject<Map<int, Set<int>>>();
int? lockedId;
Stream<Map<int, Set<int>>> get hiddenComments =>
_hiddenCommentsSubject.stream;

View File

@ -39,10 +39,6 @@ abstract class HtmlUtil {
RegExp(r'\<i\>(.*?)\<\/i\>'),
(Match match) => '*${match[1]}*',
)
.replaceAllMapped(
RegExp(r'\<pre\>\<code\>(.*?)\<\/code\>\<\/pre\>', dotAll: true),
(Match match) => match[1]?.trimRight() ?? '',
)
.replaceAllMapped(
RegExp(r'\<a href=\"(.*?)\".*?\>.*?\<\/a\>'),
(Match match) => match[1] ?? '',

View File

@ -1,14 +1,16 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hacki/config/custom_router.dart';
import 'package:hacki/config/locator.dart';
import 'package:hacki/cubits/cubits.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/models/models.dart';
import 'package:hacki/repositories/repositories.dart';
import 'package:hacki/screens/screens.dart'
show ItemScreen, ItemScreenArgs, WebViewScreen;
import 'package:hacki/styles/styles.dart';
import 'package:url_launcher/url_launcher.dart';
abstract class LinkUtil {
@ -25,7 +27,8 @@ abstract class LinkUtil {
}
static void launch(
String link, {
String link,
BuildContext context, {
bool useReader = false,
bool offlineReading = false,
bool useHackiForHnLink = true,
@ -47,24 +50,40 @@ abstract class LinkUtil {
}
if (useHackiForHnLink && link.isStoryLink) {
_onStoryLinkTapped(link);
final int? id = link.itemId;
if (id != null) {
locator.get<StoriesRepository>().fetchItem(id: id).then((Item? item) {
if (item != null) {
router.push(
'/${ItemScreen.routeName}',
extra: ItemScreenArgs(item: item),
);
}
});
return;
}
}
final Uri uri = Uri.parse(link);
canLaunchUrl(uri).then((bool val) {
if (val) {
if (link.contains('http')) {
if (Platform.isAndroid) {
if (Platform.isAndroid &&
context.read<PreferenceCubit>().state.customTabEnabled == false) {
launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
final Color primaryColor = Theme.of(context).primaryColor;
_browser
.open(
url: uri,
options: ChromeSafariBrowserClassOptions(
ios: IOSSafariOptions(
entersReaderIfAvailable: useReader,
preferredControlTintColor: Palette.orange,
preferredControlTintColor: primaryColor,
),
android: AndroidChromeCustomTabsOptions(
toolbarBackgroundColor: primaryColor,
),
),
)
@ -76,23 +95,4 @@ abstract class LinkUtil {
}
});
}
static Future<void> _onStoryLinkTapped(String link) async {
final int? id = link.itemId;
if (id != null) {
await locator
.get<StoriesRepository>()
.fetchItem(id: id)
.then((Item? item) {
if (item != null) {
router.push(
'/${ItemScreen.routeName}',
extra: ItemScreenArgs(item: item),
);
}
});
} else {
launch(link, useHackiForHnLink: false);
}
}
}

View File

@ -1,27 +1,20 @@
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
import 'package:linkify/linkify.dart' hide UrlLinkifier;
import 'package:hacki/screens/widgets/custom_linkify/custom_linkify.dart';
abstract class LinkifierUtil {
static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false);
static List<LinkifyElement> linkify(String text) {
const List<Linkifier> linkifiers = <Linkifier>[
UrlLinkifier(),
EmailLinkifier(),
QuoteLinkifier(),
EmphasisLinkifier(),
];
List<LinkifyElement> list = <LinkifyElement>[TextElement(text)];
if (text.isEmpty) {
return <LinkifyElement>[];
}
if (linkifiers.isEmpty) {
if (defaultLinkifiers.isEmpty) {
return list;
}
for (final Linkifier linkifier in linkifiers) {
for (final Linkifier linkifier in defaultLinkifiers) {
list = linkifier.parse(list, linkifyOptions);
}

View File

@ -13,10 +13,10 @@ packages:
dependency: "direct main"
description:
name: adaptive_theme
sha256: "2d9bfee4240cdfad1b169cb43ac38fb49487e7fe1cc845e2973d4cef1780c0f6"
sha256: "28df95a6b86993b38a51ee97d33a9f1d845fd1c7320c21c5d5e2183b5605e152"
url: "https://pub.dev"
source: hosted
version: "3.3.0"
version: "3.4.0"
analyzer:
dependency: transitive
description:
@ -45,10 +45,10 @@ packages:
dependency: "direct main"
description:
name: badges
sha256: "6e7f3ec561ec08f47f912cfe349d4a1707afdc8dda271e17b046aa6d42c89e77"
sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.2"
bloc:
dependency: "direct main"
description:
@ -77,26 +77,26 @@ packages:
dependency: "direct main"
description:
name: cached_network_image
sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15
sha256: f98972704692ba679db144261172a8e20feb145636c617af0eb4022132a6797f
url: "https://pub.dev"
source: hosted
version: "3.2.3"
version: "3.3.0"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7
sha256: "56aa42a7a01e3c9db8456d9f3f999931f1e05535b5a424271e9a38cabf066613"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "3.0.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0
sha256: "759b9a9f8f6ccbb66c185df805fac107f05730b1dab9c64626d1008cca532257"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
version: "1.1.0"
characters:
dependency: transitive
description:
@ -133,10 +133,10 @@ packages:
dependency: "direct main"
description:
name: connectivity_plus
sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b"
sha256: b502a681ba415272ecc41400bd04fe543ed1a62632137dc84d25a91e7746f55f
url: "https://pub.dev"
source: hosted
version: "4.0.2"
version: "5.0.1"
connectivity_plus_platform_interface:
dependency: transitive
description:
@ -165,10 +165,10 @@ packages:
dependency: transitive
description:
name: cross_file
sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9"
sha256: fd832b5384d0d6da4f6df60b854d33accaaeb63aa9e10e736a87381f08dee2cb
url: "https://pub.dev"
source: hosted
version: "0.3.3+4"
version: "0.3.3+5"
crypto:
dependency: transitive
description:
@ -197,10 +197,10 @@ packages:
dependency: "direct main"
description:
name: device_info_plus
sha256: "86add5ef97215562d2e090535b0a16f197902b10c369c558a100e74ea06e8659"
sha256: "7035152271ff67b072a211152846e9f1259cf1be41e34cd3e0b5463d2d6b8419"
url: "https://pub.dev"
source: hosted
version: "9.0.3"
version: "9.1.0"
device_info_plus_platform_interface:
dependency: transitive
description:
@ -221,10 +221,10 @@ packages:
dependency: "direct main"
description:
name: dio
sha256: ce75a1b40947fea0a0e16ce73337122a86762e38b982e1ccb909daa3b9bc4197
sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
url: "https://pub.dev"
source: hosted
version: "5.3.2"
version: "5.3.3"
equatable:
dependency: "direct main"
description:
@ -287,14 +287,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.1.3"
flutter_blurhash:
dependency: transitive
description:
name: flutter_blurhash
sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
flutter_cache_manager:
dependency: "direct main"
description:
@ -312,10 +304,10 @@ packages:
dependency: "direct main"
description:
name: flutter_email_sender
sha256: "52b713a67a966be4d9e6f68a323fc0a5bc2da71c567eb451af1aa90d30adbc3a"
sha256: "5001e9158f91a8799140fb30a11ad89cd587244f30b4f848d87085985c49b60f"
url: "https://pub.dev"
source: hosted
version: "6.0.1"
version: "6.0.2"
flutter_fadein:
dependency: "direct main"
description:
@ -336,18 +328,18 @@ packages:
dependency: "direct main"
description:
name: flutter_inappwebview
sha256: f73505c792cf083d5566e1a94002311be497d984b5607f25be36d685cf6361cf
sha256: d198297060d116b94048301ee6749cd2e7d03c1f2689783f52d210a6b7aba350
url: "https://pub.dev"
source: hosted
version: "5.7.2+3"
version: "5.8.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "3cc40fe8c50ab8383f3e053a499f00f975636622ecdc8e20a77418ece3b1e975"
sha256: "6d11ea777496061e583623aaf31923f93a9409ef8fcaeeefdd6cd78bf4fe5bb3"
url: "https://pub.dev"
source: hosted
version: "15.1.0+1"
version: "16.1.0"
flutter_local_notifications_linux:
dependency: transitive
description:
@ -364,54 +356,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0+1"
flutter_material_color_picker:
dependency: "direct main"
description:
name: flutter_material_color_picker
sha256: ca1e7749d228c9155ea24bce98e647cdbffa350e6f334f6c001f841cd3d9c987
url: "https://pub.dev"
source: hosted
version: "1.2.0"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5"
sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685
url: "https://pub.dev"
source: hosted
version: "8.0.0"
version: "9.0.0"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3"
sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e"
url: "https://pub.dev"
source: hosted
version: "1.1.3"
version: "1.2.0"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50"
sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.0.1"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b
sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "1.0.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe"
sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee
sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
version: "3.0.0"
flutter_siri_suggestions:
dependency: "direct main"
description:
@ -471,10 +471,10 @@ packages:
dependency: "direct main"
description:
name: get_it
sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468"
sha256: f79870884de16d689cf9a7d15eedf31ed61d750e813c538a6efb92660fea83c3
url: "https://pub.dev"
source: hosted
version: "7.6.0"
version: "7.6.4"
glob:
dependency: transitive
description:
@ -487,10 +487,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "5668e6d3dbcb2d0dfa25f7567554b88c57e1e3f3c440b672b24d4a9477017d5b"
sha256: a206cc4621a644531a2e05e7774616ab4d9d85eab1f3b0e255f3102937fccab1
url: "https://pub.dev"
source: hosted
version: "10.1.2"
version: "12.0.0"
hive:
dependency: "direct main"
description:
@ -603,10 +603,10 @@ packages:
dependency: "direct main"
description:
name: logger
sha256: "66cb048220ca51cf9011da69fa581e4ee2bed4be6e82870d9e9baae75739da49"
sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
version: "2.0.2+1"
logging:
dependency: transitive
description:
@ -691,10 +691,10 @@ packages:
dependency: transitive
description:
name: octo_image
sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143"
sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
version: "2.0.0"
package_config:
dependency: transitive
description:
@ -731,50 +731,50 @@ packages:
dependency: "direct main"
description:
name: path_provider
sha256: "909b84830485dbcd0308edf6f7368bc8fd76afa26a270420f34cabea2a6467a0"
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
path_provider_android:
dependency: "direct main"
description:
name: path_provider_android
sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8"
sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.2.0"
path_provider_foundation:
dependency: "direct main"
description:
name: path_provider_foundation
sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5"
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.3.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.1"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.1"
petitparser:
dependency: transitive
description:
@ -795,10 +795,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd"
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
url: "https://pub.dev"
source: hosted
version: "2.1.5"
version: "2.1.6"
pool:
dependency: transitive
description:
@ -908,74 +908,74 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: "6cec740fa0943a826951223e76218df002804adb588235a8910dc3d6b0654e11"
sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd
url: "https://pub.dev"
source: hosted
version: "7.1.0"
version: "7.2.1"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
sha256: "357412af4178d8e11d14f41723f80f12caea54cf0d5cd29af9dcdab85d58aea7"
sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956
url: "https://pub.dev"
source: hosted
version: "3.3.0"
version: "3.3.1"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1"
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.2"
shared_preferences_android:
dependency: "direct main"
description:
name: shared_preferences_android
sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.1"
shared_preferences_foundation:
dependency: "direct main"
description:
name: shared_preferences_foundation
sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef
sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
version: "2.3.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1"
sha256: c2eb5bf57a2fe9ad6988121609e47d3e07bb3bdca5b6f8444e4cf302428a128a
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.3.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1"
sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.3.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a"
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.1"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d
sha256: f763a101313bd3be87edffe0560037500967de9c394a714cd598d945517f694f
url: "https://pub.dev"
source: hosted
version: "2.3.0"
version: "2.3.1"
shelf:
dependency: transitive
description:
@ -1168,66 +1168,66 @@ packages:
dependency: "direct main"
description:
name: url_launcher
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e"
sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
url: "https://pub.dev"
source: hosted
version: "6.1.12"
version: "6.1.14"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "3dd2388cc0c42912eee04434531a26a82512b9cb1827e0214430c9bcbddfe025"
sha256: b04af59516ab45762b2ca6da40fa830d72d0f6045cd97744450b73493fa76330
url: "https://pub.dev"
source: hosted
version: "6.0.38"
version: "6.1.0"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
sha256: "7c65021d5dee51813d652357bc65b8dd4a6177082a9966bc8ba6ee477baa795f"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
version: "6.1.5"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5"
sha256: b651aad005e0cb06a01dbd84b428a301916dc75f0e7ea6165f80057fee2d8e8e
url: "https://pub.dev"
source: hosted
version: "3.0.5"
version: "3.0.6"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1"
sha256: b55486791f666e62e0e8ff825e58a023fd6b1f71c49926483f1128d3bbd8fe88
url: "https://pub.dev"
source: hosted
version: "3.0.6"
version: "3.0.7"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea
sha256: "95465b39f83bfe95fcb9d174829d6476216f2d548b79c38ab2506e0458787618"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
version: "2.1.5"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4
sha256: "2942294a500b4fa0b918685aff406773ba0a4cd34b7f42198742a94083020ce5"
url: "https://pub.dev"
source: hosted
version: "2.0.18"
version: "2.0.20"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422"
sha256: "95fef3129dc7cfaba2bc3d5ba2e16063bb561fc6d78e63eee16162bc70029069"
url: "https://pub.dev"
source: hosted
version: "3.0.7"
version: "3.0.8"
uuid:
dependency: transitive
description:
@ -1248,10 +1248,10 @@ packages:
dependency: "direct dev"
description:
name: very_good_analysis
sha256: "5e4ea72d2a9188630f0dd8f120a541de730090ef8863243fedca8267a84508b8"
sha256: "9ae7f3a3bd5764fb021b335ca28a34f040cd0ab6eec00a1b213b445dae58a4b8"
url: "https://pub.dev"
source: hosted
version: "5.0.0+1"
version: "5.1.0"
visibility_detector:
dependency: "direct main"
description:
@ -1344,58 +1344,58 @@ packages:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d"
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.2.1"
webview_flutter:
dependency: "direct main"
description:
name: webview_flutter
sha256: "789d52bd789373cc1e100fb634af2127e86c99cf9abde09499743270c5de8d00"
sha256: c1ab9b81090705c6069197d9fdc1625e587b52b8d70cdde2339d177ad0dbb98e
url: "https://pub.dev"
source: hosted
version: "4.2.2"
version: "4.4.1"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: bca797abba472868655b5f1a6029c1132385685ee9db4713cb0e7f33076210c6
sha256: b0cd33dd7d3dd8e5f664e11a19e17ba12c352647269921a3b568406b001f1dff
url: "https://pub.dev"
source: hosted
version: "3.9.3"
version: "3.12.0"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: "0ca3cfcc6781a7de701d580917af4a9efc4e3e129f8ead95a80587f0a749480a"
sha256: "6d9213c65f1060116757a7c473247c60f3f7f332cac33dc417c9e362a9a13e4f"
url: "https://pub.dev"
source: hosted
version: "2.5.0"
version: "2.6.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: ed749f94ac9e814d04a258a9255cf69cfa4cc6006ff59542aea7fb4590144972
sha256: "30b9af6bdd457b44c08748b9190d23208b5165357cc2eb57914fee1366c42974"
url: "https://pub.dev"
source: hosted
version: "3.7.3"
version: "3.9.1"
win32:
dependency: "direct overridden"
description:
name: win32
sha256: f2add6fa510d3ae152903412227bda57d0d5a8da61d2c39c1fb022c9429a41c0
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
url: "https://pub.dev"
source: hosted
version: "5.0.6"
version: "5.0.9"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9
sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
workmanager:
dependency: "direct main"
description:
@ -1408,10 +1408,10 @@ packages:
dependency: transitive
description:
name: xdg_directories
sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247
sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
version: "1.0.3"
xml:
dependency: transitive
description:
@ -1429,5 +1429,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=3.13.4"
dart: ">=3.1.0 <4.0.0"
flutter: ">=3.13.8"

View File

@ -1,11 +1,11 @@
name: hacki
description: A Hacker News reader.
version: 1.9.2+123
version: 2.0.1+126
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: "3.13.4"
flutter: "3.13.8"
dependencies:
adaptive_theme: ^3.2.0
@ -14,8 +14,8 @@ dependencies:
cached_network_image: ^3.2.3
clipboard: ^0.1.3
collection: ^1.17.1
connectivity_plus: ^4.0.0
device_info_plus: ^9.0.0
connectivity_plus: ^5.0.1
device_info_plus: ^9.1.0
dio: ^5.0.3
equatable: ^2.0.5
fast_gbk: ^1.0.0
@ -30,15 +30,16 @@ dependencies:
flutter_email_sender: ^6.0.1
flutter_fadein: ^2.0.0
flutter_feather_icons: 2.0.0+1
flutter_inappwebview: ^5.7.2+3
flutter_local_notifications: ^15.1.0+1
flutter_secure_storage: ^8.0.0
flutter_inappwebview: ^5.8.0
flutter_local_notifications: ^16.1.0
flutter_material_color_picker: ^1.2.0
flutter_secure_storage: ^9.0.0
flutter_siri_suggestions: ^2.1.0
flutter_slidable: ^3.0.0
font_awesome_flutter: ^10.3.0
gbk_codec: ^0.4.0
get_it: ^7.2.0
go_router: ^10.1.2
go_router: ^12.0.0
hive: ^2.2.3
html: ^0.15.1
html_unescape: ^2.0.0
@ -66,10 +67,10 @@ dependencies:
rxdart: ^0.27.7
scrollable_positioned_list: ^0.3.5
sembast: ^3.4.0+6
share_plus: ^7.0.0
shared_preferences: ^2.0.17
shared_preferences_android: ^2.0.15
shared_preferences_foundation: ^2.1.3
share_plus: ^7.2.1
shared_preferences: ^2.2.2
shared_preferences_android: ^2.2.1
shared_preferences_foundation: ^2.3.4
shimmer: ^3.0.0
synced_shared_preferences:
path: components/synced_shared_preferences
@ -77,7 +78,7 @@ dependencies:
url_launcher: ^6.1.9
visibility_detector: ^0.4.0+2
wakelock: ^0.6.2
webview_flutter: ^4.0.2
webview_flutter: ^4.4.1
workmanager: ^0.5.1
dependency_overrides: