mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-14 10:32:46 +08:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
0ca3e96d91 | |||
d1c8eed3de | |||
aa6a2c684c | |||
d4778d9530 | |||
c702e08481 | |||
2af10391bc |
@ -50,7 +50,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.jiaqifeng.hacki"
|
applicationId "com.jiaqifeng.hacki"
|
||||||
minSdkVersion 26
|
minSdkVersion 30
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
@ -64,12 +64,15 @@ android {
|
|||||||
storePassword keystoreProperties['storePassword']
|
storePassword keystoreProperties['storePassword']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
|
|
||||||
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
@ -37,15 +37,6 @@
|
|||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme"
|
||||||
/>
|
/>
|
||||||
<!-- Displays an Android View that continues showing the launch screen
|
|
||||||
Drawable until Flutter paints its first frame, then this splash
|
|
||||||
screen fades out. A splash screen is useful to avoid any visual
|
|
||||||
gap between the end of Android's launch screen and the painting of
|
|
||||||
Flutter's first frame. -->
|
|
||||||
<meta-data
|
|
||||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
|
||||||
android:resource="@drawable/launch_background"
|
|
||||||
/>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
@ -74,7 +74,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
final int pageSize = getPageSize(isComplexTile: isComplexTile);
|
final int pageSize = getPageSize(isComplexTile: isComplexTile);
|
||||||
emit(
|
emit(
|
||||||
const StoriesState.init().copyWith(
|
const StoriesState.init().copyWith(
|
||||||
offlineReading: hasCachedStories &&
|
isOfflineReading: hasCachedStories &&
|
||||||
// Only go into offline mode in the next session.
|
// Only go into offline mode in the next session.
|
||||||
state.downloadStatus == StoriesDownloadStatus.initial,
|
state.downloadStatus == StoriesDownloadStatus.initial,
|
||||||
currentPageSize: pageSize,
|
currentPageSize: pageSize,
|
||||||
@ -92,7 +92,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
required StoryType type,
|
required StoryType type,
|
||||||
required Emitter<StoriesState> emit,
|
required Emitter<StoriesState> emit,
|
||||||
}) async {
|
}) async {
|
||||||
if (state.offlineReading) {
|
if (state.isOfflineReading) {
|
||||||
final List<int> ids =
|
final List<int> ids =
|
||||||
await _offlineRepository.getCachedStoryIds(type: type);
|
await _offlineRepository.getCachedStoryIds(type: type);
|
||||||
emit(
|
emit(
|
||||||
@ -137,7 +137,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (state.offlineReading) {
|
if (state.isOfflineReading) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWithStatusUpdated(
|
state.copyWithStatusUpdated(
|
||||||
type: event.type,
|
type: event.type,
|
||||||
@ -172,7 +172,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
upper = len;
|
upper = len;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.offlineReading) {
|
if (state.isOfflineReading) {
|
||||||
_offlineRepository
|
_offlineRepository
|
||||||
.getCachedStoriesStream(
|
.getCachedStoriesStream(
|
||||||
ids: state.storyIdsByType[event.type]!.sublist(
|
ids: state.storyIdsByType[event.type]!.sublist(
|
||||||
@ -440,7 +440,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
|
|||||||
await _offlineRepository.deleteAllStories();
|
await _offlineRepository.deleteAllStories();
|
||||||
await _offlineRepository.deleteAllComments();
|
await _offlineRepository.deleteAllComments();
|
||||||
await _offlineRepository.deleteAllWebPages();
|
await _offlineRepository.deleteAllWebPages();
|
||||||
emit(state.copyWith(offlineReading: false));
|
emit(state.copyWith(isOfflineReading: false));
|
||||||
add(StoriesInitialize());
|
add(StoriesInitialize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ class StoriesState extends Equatable {
|
|||||||
required this.statusByType,
|
required this.statusByType,
|
||||||
required this.currentPageByType,
|
required this.currentPageByType,
|
||||||
required this.readStoriesIds,
|
required this.readStoriesIds,
|
||||||
required this.offlineReading,
|
required this.isOfflineReading,
|
||||||
required this.downloadStatus,
|
required this.downloadStatus,
|
||||||
required this.currentPageSize,
|
required this.currentPageSize,
|
||||||
required this.storiesDownloaded,
|
required this.storiesDownloaded,
|
||||||
@ -57,7 +57,7 @@ class StoriesState extends Equatable {
|
|||||||
StoryType.ask: 0,
|
StoryType.ask: 0,
|
||||||
StoryType.show: 0,
|
StoryType.show: 0,
|
||||||
},
|
},
|
||||||
}) : offlineReading = false,
|
}) : isOfflineReading = false,
|
||||||
downloadStatus = StoriesDownloadStatus.initial,
|
downloadStatus = StoriesDownloadStatus.initial,
|
||||||
currentPageSize = 0,
|
currentPageSize = 0,
|
||||||
readStoriesIds = const <int>{},
|
readStoriesIds = const <int>{},
|
||||||
@ -70,7 +70,7 @@ class StoriesState extends Equatable {
|
|||||||
final Map<StoryType, int> currentPageByType;
|
final Map<StoryType, int> currentPageByType;
|
||||||
final Set<int> readStoriesIds;
|
final Set<int> readStoriesIds;
|
||||||
final StoriesDownloadStatus downloadStatus;
|
final StoriesDownloadStatus downloadStatus;
|
||||||
final bool offlineReading;
|
final bool isOfflineReading;
|
||||||
final int currentPageSize;
|
final int currentPageSize;
|
||||||
final int storiesDownloaded;
|
final int storiesDownloaded;
|
||||||
final int storiesToBeDownloaded;
|
final int storiesToBeDownloaded;
|
||||||
@ -82,7 +82,7 @@ class StoriesState extends Equatable {
|
|||||||
Map<StoryType, int>? currentPageByType,
|
Map<StoryType, int>? currentPageByType,
|
||||||
Set<int>? readStoriesIds,
|
Set<int>? readStoriesIds,
|
||||||
StoriesDownloadStatus? downloadStatus,
|
StoriesDownloadStatus? downloadStatus,
|
||||||
bool? offlineReading,
|
bool? isOfflineReading,
|
||||||
int? currentPageSize,
|
int? currentPageSize,
|
||||||
int? storiesDownloaded,
|
int? storiesDownloaded,
|
||||||
int? storiesToBeDownloaded,
|
int? storiesToBeDownloaded,
|
||||||
@ -93,7 +93,7 @@ class StoriesState extends Equatable {
|
|||||||
statusByType: statusByType ?? this.statusByType,
|
statusByType: statusByType ?? this.statusByType,
|
||||||
currentPageByType: currentPageByType ?? this.currentPageByType,
|
currentPageByType: currentPageByType ?? this.currentPageByType,
|
||||||
readStoriesIds: readStoriesIds ?? this.readStoriesIds,
|
readStoriesIds: readStoriesIds ?? this.readStoriesIds,
|
||||||
offlineReading: offlineReading ?? this.offlineReading,
|
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
|
||||||
downloadStatus: downloadStatus ?? this.downloadStatus,
|
downloadStatus: downloadStatus ?? this.downloadStatus,
|
||||||
currentPageSize: currentPageSize ?? this.currentPageSize,
|
currentPageSize: currentPageSize ?? this.currentPageSize,
|
||||||
storiesDownloaded: storiesDownloaded ?? this.storiesDownloaded,
|
storiesDownloaded: storiesDownloaded ?? this.storiesDownloaded,
|
||||||
@ -183,7 +183,7 @@ class StoriesState extends Equatable {
|
|||||||
statusByType,
|
statusByType,
|
||||||
currentPageByType,
|
currentPageByType,
|
||||||
readStoriesIds,
|
readStoriesIds,
|
||||||
offlineReading,
|
isOfflineReading,
|
||||||
downloadStatus,
|
downloadStatus,
|
||||||
currentPageSize,
|
currentPageSize,
|
||||||
storiesDownloaded,
|
storiesDownloaded,
|
||||||
|
@ -26,7 +26,8 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
StoriesRepository? storiesRepository,
|
StoriesRepository? storiesRepository,
|
||||||
SembastRepository? sembastRepository,
|
SembastRepository? sembastRepository,
|
||||||
Logger? logger,
|
Logger? logger,
|
||||||
required bool offlineReading,
|
required bool isScreenReaderEnabled,
|
||||||
|
required bool isOfflineReading,
|
||||||
required Item item,
|
required Item item,
|
||||||
required FetchMode defaultFetchMode,
|
required FetchMode defaultFetchMode,
|
||||||
required CommentsOrder defaultCommentsOrder,
|
required CommentsOrder defaultCommentsOrder,
|
||||||
@ -39,9 +40,10 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
_sembastRepository =
|
_sembastRepository =
|
||||||
sembastRepository ?? locator.get<SembastRepository>(),
|
sembastRepository ?? locator.get<SembastRepository>(),
|
||||||
_logger = logger ?? locator.get<Logger>(),
|
_logger = logger ?? locator.get<Logger>(),
|
||||||
|
_isScreenReaderEnabled = isScreenReaderEnabled,
|
||||||
super(
|
super(
|
||||||
CommentsState.init(
|
CommentsState.init(
|
||||||
offlineReading: offlineReading,
|
isOfflineReading: isOfflineReading,
|
||||||
item: item,
|
item: item,
|
||||||
fetchMode: defaultFetchMode,
|
fetchMode: defaultFetchMode,
|
||||||
order: defaultCommentsOrder,
|
order: defaultCommentsOrder,
|
||||||
@ -54,6 +56,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
final StoriesRepository _storiesRepository;
|
final StoriesRepository _storiesRepository;
|
||||||
final SembastRepository _sembastRepository;
|
final SembastRepository _sembastRepository;
|
||||||
final Logger _logger;
|
final Logger _logger;
|
||||||
|
final bool _isScreenReaderEnabled;
|
||||||
|
|
||||||
/// The [StreamSubscription] for stream (both lazy or eager)
|
/// The [StreamSubscription] for stream (both lazy or eager)
|
||||||
/// fetching comments posted directly to the story.
|
/// fetching comments posted directly to the story.
|
||||||
@ -109,7 +112,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final Item item = state.item;
|
final Item item = state.item;
|
||||||
final Item updatedItem = state.offlineReading
|
final Item updatedItem = state.isOfflineReading
|
||||||
? item
|
? item
|
||||||
: await _storiesRepository.fetchItem(id: item.id).then(_toBuildable) ??
|
: await _storiesRepository.fetchItem(id: item.id).then(_toBuildable) ??
|
||||||
item;
|
item;
|
||||||
@ -119,7 +122,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
|
|
||||||
late final Stream<Comment> commentStream;
|
late final Stream<Comment> commentStream;
|
||||||
|
|
||||||
if (state.offlineReading) {
|
if (state.isOfflineReading) {
|
||||||
commentStream = _offlineRepository.getCachedCommentsStream(ids: kids);
|
commentStream = _offlineRepository.getCachedCommentsStream(ids: kids);
|
||||||
} else {
|
} else {
|
||||||
switch (state.fetchMode) {
|
switch (state.fetchMode) {
|
||||||
@ -152,7 +155,7 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (state.offlineReading) {
|
if (state.isOfflineReading) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CommentsStatus.allLoaded,
|
status: CommentsStatus.allLoaded,
|
||||||
@ -356,6 +359,9 @@ class CommentsCubit extends Cubit<CommentsState> {
|
|||||||
emit(state.copyWith(comments: updatedComments));
|
emit(state.copyWith(comments: updatedComments));
|
||||||
|
|
||||||
if (state.fetchMode == FetchMode.eager) {
|
if (state.fetchMode == FetchMode.eager) {
|
||||||
|
/// If screen reader is on, fetch all the comments without paging.
|
||||||
|
if (_isScreenReaderEnabled) return;
|
||||||
|
|
||||||
if (updatedComments.length >=
|
if (updatedComments.length >=
|
||||||
_pageSize + _pageSize * state.currentPage &&
|
_pageSize + _pageSize * state.currentPage &&
|
||||||
updatedComments.length <=
|
updatedComments.length <=
|
||||||
|
@ -17,12 +17,12 @@ class CommentsState extends Equatable {
|
|||||||
required this.order,
|
required this.order,
|
||||||
required this.fetchMode,
|
required this.fetchMode,
|
||||||
required this.onlyShowTargetComment,
|
required this.onlyShowTargetComment,
|
||||||
required this.offlineReading,
|
required this.isOfflineReading,
|
||||||
required this.currentPage,
|
required this.currentPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
CommentsState.init({
|
CommentsState.init({
|
||||||
required this.offlineReading,
|
required this.isOfflineReading,
|
||||||
required this.item,
|
required this.item,
|
||||||
required this.fetchMode,
|
required this.fetchMode,
|
||||||
required this.order,
|
required this.order,
|
||||||
@ -39,7 +39,7 @@ class CommentsState extends Equatable {
|
|||||||
final CommentsOrder order;
|
final CommentsOrder order;
|
||||||
final FetchMode fetchMode;
|
final FetchMode fetchMode;
|
||||||
final bool onlyShowTargetComment;
|
final bool onlyShowTargetComment;
|
||||||
final bool offlineReading;
|
final bool isOfflineReading;
|
||||||
final int currentPage;
|
final int currentPage;
|
||||||
|
|
||||||
CommentsState copyWith({
|
CommentsState copyWith({
|
||||||
@ -50,7 +50,7 @@ class CommentsState extends Equatable {
|
|||||||
CommentsOrder? order,
|
CommentsOrder? order,
|
||||||
FetchMode? fetchMode,
|
FetchMode? fetchMode,
|
||||||
bool? onlyShowTargetComment,
|
bool? onlyShowTargetComment,
|
||||||
bool? offlineReading,
|
bool? isOfflineReading,
|
||||||
int? currentPage,
|
int? currentPage,
|
||||||
}) {
|
}) {
|
||||||
return CommentsState(
|
return CommentsState(
|
||||||
@ -62,7 +62,7 @@ class CommentsState extends Equatable {
|
|||||||
fetchMode: fetchMode ?? this.fetchMode,
|
fetchMode: fetchMode ?? this.fetchMode,
|
||||||
onlyShowTargetComment:
|
onlyShowTargetComment:
|
||||||
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
onlyShowTargetComment ?? this.onlyShowTargetComment,
|
||||||
offlineReading: offlineReading ?? this.offlineReading,
|
isOfflineReading: isOfflineReading ?? this.isOfflineReading,
|
||||||
currentPage: currentPage ?? this.currentPage,
|
currentPage: currentPage ?? this.currentPage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -77,7 +77,7 @@ class CommentsState extends Equatable {
|
|||||||
order,
|
order,
|
||||||
fetchMode,
|
fetchMode,
|
||||||
onlyShowTargetComment,
|
onlyShowTargetComment,
|
||||||
offlineReading,
|
isOfflineReading,
|
||||||
currentPage,
|
currentPage,
|
||||||
comments,
|
comments,
|
||||||
];
|
];
|
||||||
|
@ -160,6 +160,13 @@ class FavCubit extends Cubit<FavState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void removeAll() {
|
||||||
|
_preferenceRepository
|
||||||
|
..clearAllFavs(username: '')
|
||||||
|
..clearAllFavs(username: _authBloc.state.username);
|
||||||
|
emit(FavState.init());
|
||||||
|
}
|
||||||
|
|
||||||
void _onItemLoaded(Item item) {
|
void _onItemLoaded(Item item) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
|
@ -6,6 +6,8 @@ import 'package:hacki/config/constants.dart';
|
|||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
|
|
||||||
extension ContextExtension on BuildContext {
|
extension ContextExtension on BuildContext {
|
||||||
|
bool get isScreenReaderEnabled => MediaQuery.of(this).accessibleNavigation;
|
||||||
|
|
||||||
T? tryRead<T>() {
|
T? tryRead<T>() {
|
||||||
try {
|
try {
|
||||||
return read<T>();
|
return read<T>();
|
||||||
@ -19,6 +21,8 @@ extension ContextExtension on BuildContext {
|
|||||||
VoidCallback? action,
|
VoidCallback? action,
|
||||||
String? label,
|
String? label,
|
||||||
}) {
|
}) {
|
||||||
|
if (isScreenReaderEnabled) return;
|
||||||
|
|
||||||
ScaffoldMessenger.of(this).showSnackBar(
|
ScaffoldMessenger.of(this).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
backgroundColor: Palette.deepOrange,
|
backgroundColor: Palette.deepOrange,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
extension DateTimeExtension on DateTime {
|
extension DateTimeExtension on DateTime {
|
||||||
String toReadableString() {
|
String toTimeAgoString() {
|
||||||
final DateTime now = DateTime.now();
|
final DateTime now = DateTime.now();
|
||||||
final Duration diff = now.difference(this);
|
final Duration diff = now.difference(this);
|
||||||
if (diff.inDays > 365) {
|
if (diff.inDays > 365) {
|
||||||
|
@ -2,7 +2,18 @@ import 'package:hacki/config/locator.dart';
|
|||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
extension ObjectExtension on Object {
|
extension ObjectExtension on Object {
|
||||||
void log({String identifier = ''}) {
|
void log([String identifier = '']) {
|
||||||
locator.get<Logger>().d('$identifier ${toString()}');
|
locator.get<Logger>().d('$identifier ${toString()}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void logInfo({String identifier = ''}) {
|
||||||
|
locator.get<Logger>().i('$identifier ${toString()}');
|
||||||
|
}
|
||||||
|
|
||||||
|
void logError({
|
||||||
|
String identifier = '',
|
||||||
|
StackTrace? stackTrace,
|
||||||
|
}) {
|
||||||
|
locator.get<Logger>().e(identifier, this, stackTrace ?? StackTrace.current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
import 'package:hacki/screens/widgets/custom_linkify/custom_linkify.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
|
|
||||||
extension WidgetModifier on Widget {
|
extension WidgetModifier on Widget {
|
||||||
|
@ -24,7 +24,7 @@ class Comment extends Item {
|
|||||||
|
|
||||||
final int level;
|
final int level;
|
||||||
|
|
||||||
String get metadata => '''by $by $postedDate''';
|
String get metadata => '''by $by $timeAgo''';
|
||||||
|
|
||||||
Comment copyWith({int? level}) {
|
Comment copyWith({int? level}) {
|
||||||
return Comment(
|
return Comment(
|
||||||
|
@ -82,8 +82,8 @@ class Item extends Equatable {
|
|||||||
final List<int> kids;
|
final List<int> kids;
|
||||||
final List<int> parts;
|
final List<int> parts;
|
||||||
|
|
||||||
String get postedDate =>
|
String get timeAgo =>
|
||||||
DateTime.fromMillisecondsSinceEpoch(time * 1000).toReadableString();
|
DateTime.fromMillisecondsSinceEpoch(time * 1000).toTimeAgoString();
|
||||||
|
|
||||||
bool get isPoll => type == 'poll';
|
bool get isPoll => type == 'poll';
|
||||||
|
|
||||||
|
@ -43,10 +43,13 @@ class Story extends Item {
|
|||||||
Story.fromJson(super.json) : super.fromJson();
|
Story.fromJson(super.json) : super.fromJson();
|
||||||
|
|
||||||
String get metadata =>
|
String get metadata =>
|
||||||
'''$score point${score > 1 ? 's' : ''} by $by $postedDate | $descendants comment${descendants > 1 ? 's' : ''}''';
|
'''$score point${score > 1 ? 's' : ''} by $by $timeAgo | $descendants comment${descendants > 1 ? 's' : ''}''';
|
||||||
|
|
||||||
|
String get screenReaderLabel =>
|
||||||
|
'''$title, at $readableUrl, by $by $timeAgo. This story has $score point${score > 1 ? 's' : ''} and $descendants comment${descendants > 1 ? 's' : ''}''';
|
||||||
|
|
||||||
String get simpleMetadata =>
|
String get simpleMetadata =>
|
||||||
'''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $postedDate''';
|
'''$score point${score > 1 ? 's' : ''} $descendants comment${descendants > 1 ? 's' : ''} $timeAgo''';
|
||||||
|
|
||||||
String get readableUrl {
|
String get readableUrl {
|
||||||
final Uri url = Uri.parse(this.url);
|
final Uri url = Uri.parse(this.url);
|
||||||
@ -55,10 +58,5 @@ class Story extends Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() => 'Story $id';
|
||||||
// final String prettyString =
|
|
||||||
// const JsonEncoder.withIndent(' ').convert(this);
|
|
||||||
// return 'Story $prettyString';
|
|
||||||
return 'Story $id';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
|||||||
const NotificationModePreference(),
|
const NotificationModePreference(),
|
||||||
const SwipeGesturePreference(),
|
const SwipeGesturePreference(),
|
||||||
const CollapseModePreference(),
|
const CollapseModePreference(),
|
||||||
NavigationModePreference(),
|
const NavigationModePreference(),
|
||||||
const ReaderModePreference(),
|
const ReaderModePreference(),
|
||||||
const MarkReadStoriesModePreference(),
|
const MarkReadStoriesModePreference(),
|
||||||
const EyeCandyModePreference(),
|
const EyeCandyModePreference(),
|
||||||
@ -54,8 +54,7 @@ abstract class IntPreference extends Preference<int> {
|
|||||||
const bool _notificationModeDefaultValue = true;
|
const bool _notificationModeDefaultValue = true;
|
||||||
const bool _swipeGestureModeDefaultValue = false;
|
const bool _swipeGestureModeDefaultValue = false;
|
||||||
const bool _displayModeDefaultValue = true;
|
const bool _displayModeDefaultValue = true;
|
||||||
const bool _navigationModeDefaultValueIOS = false;
|
const bool _navigationModeDefaultValue = false;
|
||||||
const bool _navigationModeDefaultValueAndroid = false;
|
|
||||||
const bool _eyeCandyModeDefaultValue = false;
|
const bool _eyeCandyModeDefaultValue = false;
|
||||||
const bool _trueDarkModeDefaultValue = false;
|
const bool _trueDarkModeDefaultValue = false;
|
||||||
const bool _readerModeDefaultValue = true;
|
const bool _readerModeDefaultValue = true;
|
||||||
@ -193,12 +192,9 @@ class StoryUrlModePreference extends BooleanPreference {
|
|||||||
/// The value deciding whether or not user should be
|
/// The value deciding whether or not user should be
|
||||||
/// navigated to web view first. Defaults to false.
|
/// navigated to web view first. Defaults to false.
|
||||||
class NavigationModePreference extends BooleanPreference {
|
class NavigationModePreference extends BooleanPreference {
|
||||||
NavigationModePreference({bool? val})
|
const NavigationModePreference({bool? val})
|
||||||
: super(
|
: super(
|
||||||
val: val ??
|
val: val ?? _navigationModeDefaultValue,
|
||||||
(Platform.isAndroid
|
|
||||||
? _navigationModeDefaultValueAndroid
|
|
||||||
: _navigationModeDefaultValueIOS),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -207,6 +207,23 @@ class PreferenceRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> clearAllFavs({required String username}) async {
|
||||||
|
final String key = _getFavKey(username);
|
||||||
|
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
await _syncedPrefs.setStringList(
|
||||||
|
key: key,
|
||||||
|
val: <String>[],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final SharedPreferences prefs = await _prefs;
|
||||||
|
await prefs.setStringList(
|
||||||
|
key,
|
||||||
|
<String>[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static String _getFavKey(String username) => 'fav_$username';
|
static String _getFavKey(String username) => 'fav_$username';
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
@ -92,12 +92,14 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
|
|
||||||
SchedulerBinding.instance
|
SchedulerBinding.instance
|
||||||
..addPostFrameCallback((_) {
|
..addPostFrameCallback((_) {
|
||||||
FeatureDiscovery.discoverFeatures(
|
if (context.isScreenReaderEnabled == false) {
|
||||||
context,
|
FeatureDiscovery.discoverFeatures(
|
||||||
<String>{
|
context,
|
||||||
Constants.featureLogIn,
|
<String>{
|
||||||
},
|
Constants.featureLogIn,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
..addPostFrameCallback((_) {
|
..addPostFrameCallback((_) {
|
||||||
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
||||||
@ -214,7 +216,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
context.read<PreferenceCubit>().state.webFirstEnabled;
|
context.read<PreferenceCubit>().state.webFirstEnabled;
|
||||||
final bool useReader = context.read<PreferenceCubit>().state.readerEnabled;
|
final bool useReader = context.read<PreferenceCubit>().state.readerEnabled;
|
||||||
final bool offlineReading =
|
final bool offlineReading =
|
||||||
context.read<StoriesBloc>().state.offlineReading;
|
context.read<StoriesBloc>().state.isOfflineReading;
|
||||||
final bool hasRead = isPin || context.read<StoriesBloc>().hasRead(story);
|
final bool hasRead = isPin || context.read<StoriesBloc>().hasRead(story);
|
||||||
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
||||||
|
|
||||||
@ -225,7 +227,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
if (isJobWithLink) {
|
if (isJobWithLink) {
|
||||||
context.read<ReminderCubit>().removeLastReadStoryId();
|
context.read<ReminderCubit>().removeLastReadStoryId();
|
||||||
} else {
|
} else {
|
||||||
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
final ItemScreenArgs args = ItemScreenArgs(
|
||||||
|
item: story,
|
||||||
|
isScreenReaderEnabled: context.isScreenReaderEnabled,
|
||||||
|
);
|
||||||
|
|
||||||
context.read<ReminderCubit>().updateLastReadStoryId(story.id);
|
context.read<ReminderCubit>().updateLastReadStoryId(story.id);
|
||||||
|
|
||||||
|
@ -23,11 +23,13 @@ class ItemScreenArgs extends Equatable {
|
|||||||
required this.item,
|
required this.item,
|
||||||
this.onlyShowTargetComment = false,
|
this.onlyShowTargetComment = false,
|
||||||
this.useCommentCache = false,
|
this.useCommentCache = false,
|
||||||
|
this.isScreenReaderEnabled = false,
|
||||||
this.targetComments,
|
this.targetComments,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Item item;
|
final Item item;
|
||||||
final bool onlyShowTargetComment;
|
final bool onlyShowTargetComment;
|
||||||
|
final bool isScreenReaderEnabled;
|
||||||
final List<Comment>? targetComments;
|
final List<Comment>? targetComments;
|
||||||
|
|
||||||
/// when a user is trying to view a sub-thread from a main thread, we don't
|
/// when a user is trying to view a sub-thread from a main thread, we don't
|
||||||
@ -39,6 +41,7 @@ class ItemScreenArgs extends Equatable {
|
|||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
item,
|
item,
|
||||||
onlyShowTargetComment,
|
onlyShowTargetComment,
|
||||||
|
isScreenReaderEnabled,
|
||||||
targetComments,
|
targetComments,
|
||||||
useCommentCache,
|
useCommentCache,
|
||||||
];
|
];
|
||||||
@ -58,20 +61,21 @@ class ItemScreen extends StatefulWidget {
|
|||||||
return MaterialPageRoute<ItemScreen>(
|
return MaterialPageRoute<ItemScreen>(
|
||||||
settings: const RouteSettings(name: routeName),
|
settings: const RouteSettings(name: routeName),
|
||||||
builder: (BuildContext context) => RepositoryProvider<CollapseCache>(
|
builder: (BuildContext context) => RepositoryProvider<CollapseCache>(
|
||||||
create: (BuildContext context) => CollapseCache(),
|
create: (_) => CollapseCache(),
|
||||||
lazy: false,
|
lazy: false,
|
||||||
child: MultiBlocProvider(
|
child: MultiBlocProvider(
|
||||||
providers: <BlocProvider<dynamic>>[
|
providers: <BlocProvider<dynamic>>[
|
||||||
BlocProvider<CommentsCubit>(
|
BlocProvider<CommentsCubit>(
|
||||||
create: (BuildContext context) => CommentsCubit(
|
create: (BuildContext context) => CommentsCubit(
|
||||||
offlineReading:
|
isOfflineReading:
|
||||||
context.read<StoriesBloc>().state.offlineReading,
|
context.read<StoriesBloc>().state.isOfflineReading,
|
||||||
item: args.item,
|
item: args.item,
|
||||||
collapseCache: context.read<CollapseCache>(),
|
collapseCache: context.read<CollapseCache>(),
|
||||||
defaultFetchMode:
|
defaultFetchMode:
|
||||||
context.read<PreferenceCubit>().state.fetchMode,
|
context.read<PreferenceCubit>().state.fetchMode,
|
||||||
defaultCommentsOrder:
|
defaultCommentsOrder:
|
||||||
context.read<PreferenceCubit>().state.order,
|
context.read<PreferenceCubit>().state.order,
|
||||||
|
isScreenReaderEnabled: args.isScreenReaderEnabled,
|
||||||
)..init(
|
)..init(
|
||||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||||
targetAncestors: args.targetComments,
|
targetAncestors: args.targetComments,
|
||||||
@ -99,21 +103,22 @@ class ItemScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: RepositoryProvider<CollapseCache>(
|
child: RepositoryProvider<CollapseCache>(
|
||||||
create: (BuildContext context) => CollapseCache(),
|
create: (_) => CollapseCache(),
|
||||||
lazy: false,
|
lazy: false,
|
||||||
child: MultiBlocProvider(
|
child: MultiBlocProvider(
|
||||||
key: ValueKey<ItemScreenArgs>(args),
|
key: ValueKey<ItemScreenArgs>(args),
|
||||||
providers: <BlocProvider<dynamic>>[
|
providers: <BlocProvider<dynamic>>[
|
||||||
BlocProvider<CommentsCubit>(
|
BlocProvider<CommentsCubit>(
|
||||||
create: (BuildContext context) => CommentsCubit(
|
create: (BuildContext context) => CommentsCubit(
|
||||||
offlineReading:
|
isOfflineReading:
|
||||||
context.read<StoriesBloc>().state.offlineReading,
|
context.read<StoriesBloc>().state.isOfflineReading,
|
||||||
item: args.item,
|
item: args.item,
|
||||||
collapseCache: context.read<CollapseCache>(),
|
collapseCache: context.read<CollapseCache>(),
|
||||||
defaultFetchMode:
|
defaultFetchMode:
|
||||||
context.read<PreferenceCubit>().state.fetchMode,
|
context.read<PreferenceCubit>().state.fetchMode,
|
||||||
defaultCommentsOrder:
|
defaultCommentsOrder:
|
||||||
context.read<PreferenceCubit>().state.order,
|
context.read<PreferenceCubit>().state.order,
|
||||||
|
isScreenReaderEnabled: args.isScreenReaderEnabled,
|
||||||
)..init(
|
)..init(
|
||||||
onlyShowTargetComment: args.onlyShowTargetComment,
|
onlyShowTargetComment: args.onlyShowTargetComment,
|
||||||
targetAncestors: args.targetComments,
|
targetAncestors: args.targetComments,
|
||||||
@ -173,14 +178,16 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
|
|
||||||
SchedulerBinding.instance
|
SchedulerBinding.instance
|
||||||
..addPostFrameCallback((_) {
|
..addPostFrameCallback((_) {
|
||||||
FeatureDiscovery.discoverFeatures(
|
if (context.isScreenReaderEnabled == false) {
|
||||||
context,
|
FeatureDiscovery.discoverFeatures(
|
||||||
<String>{
|
context,
|
||||||
Constants.featurePinToTop,
|
<String>{
|
||||||
Constants.featureAddStoryToFavList,
|
Constants.featurePinToTop,
|
||||||
Constants.featureOpenStoryInWebView,
|
Constants.featureAddStoryToFavList,
|
||||||
},
|
Constants.featureOpenStoryInWebView,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
..addPostFrameCallback((_) {
|
..addPostFrameCallback((_) {
|
||||||
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
final ModalRoute<dynamic>? route = ModalRoute.of(context);
|
||||||
@ -318,6 +325,8 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
context.read<SplitViewCubit>().zoom,
|
context.read<SplitViewCubit>().zoom,
|
||||||
onFontSizeTap: onFontSizeTapped,
|
onFontSizeTap: onFontSizeTapped,
|
||||||
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
||||||
|
isScreenReaderEnabled:
|
||||||
|
context.isScreenReaderEnabled,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -353,6 +362,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
onFontSizeTap: onFontSizeTapped,
|
onFontSizeTap: onFontSizeTapped,
|
||||||
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
fontSizeIconButtonKey: fontSizeIconButtonKey,
|
||||||
|
isScreenReaderEnabled: context.isScreenReaderEnabled,
|
||||||
),
|
),
|
||||||
body: MainView(
|
body: MainView(
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
|
@ -16,6 +16,7 @@ class CustomAppBar extends AppBar {
|
|||||||
bool splitViewEnabled = false,
|
bool splitViewEnabled = false,
|
||||||
VoidCallback? onZoomTap,
|
VoidCallback? onZoomTap,
|
||||||
bool? expanded,
|
bool? expanded,
|
||||||
|
bool isScreenReaderEnabled = false,
|
||||||
}) : super(
|
}) : super(
|
||||||
elevation: Dimens.zero,
|
elevation: Dimens.zero,
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
@ -37,19 +38,20 @@ class CustomAppBar extends AppBar {
|
|||||||
ScrollUpIconButton(
|
ScrollUpIconButton(
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
),
|
),
|
||||||
IconButton(
|
if (isScreenReaderEnabled == false)
|
||||||
key: fontSizeIconButtonKey,
|
IconButton(
|
||||||
icon: Text(
|
key: fontSizeIconButtonKey,
|
||||||
String.fromCharCode(FeatherIcons.type.codePoint),
|
icon: Text(
|
||||||
style: TextStyle(
|
String.fromCharCode(FeatherIcons.type.codePoint),
|
||||||
fontWeight: FontWeight.w800,
|
style: TextStyle(
|
||||||
fontSize: TextDimens.pt18,
|
fontWeight: FontWeight.w800,
|
||||||
fontFamily: FeatherIcons.type.fontFamily,
|
fontSize: TextDimens.pt18,
|
||||||
package: FeatherIcons.type.fontPackage,
|
fontFamily: FeatherIcons.type.fontFamily,
|
||||||
|
package: FeatherIcons.type.fontPackage,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
onPressed: onFontSizeTap,
|
||||||
),
|
),
|
||||||
onPressed: onFontSizeTap,
|
|
||||||
),
|
|
||||||
if (item is Story)
|
if (item is Story)
|
||||||
PinIconButton(
|
PinIconButton(
|
||||||
story: item,
|
story: item,
|
||||||
|
@ -24,9 +24,7 @@ class LinkIconButton extends StatelessWidget {
|
|||||||
featureId: Constants.featureOpenStoryInWebView,
|
featureId: Constants.featureOpenStoryInWebView,
|
||||||
title: Text('Open in Browser'),
|
title: Text('Open in Browser'),
|
||||||
description: Text(
|
description: Text(
|
||||||
'Want more than just reading and replying? '
|
'''You can tap here to open this story in browser.''',
|
||||||
'You can tap here to open this story in a '
|
|
||||||
'browser.',
|
|
||||||
style: TextStyle(fontSize: TextDimens.pt16),
|
style: TextStyle(fontSize: TextDimens.pt16),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
|
@ -87,7 +87,7 @@ class MainView extends StatelessWidget {
|
|||||||
onRefresh: () {
|
onRefresh: () {
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
|
|
||||||
if (context.read<StoriesBloc>().state.offlineReading) {
|
if (context.read<StoriesBloc>().state.isOfflineReading) {
|
||||||
refreshController.refreshCompleted();
|
refreshController.refreshCompleted();
|
||||||
} else {
|
} else {
|
||||||
context.read<CommentsCubit>().refresh();
|
context.read<CommentsCubit>().refresh();
|
||||||
@ -231,232 +231,257 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Semantics(
|
||||||
children: <Widget>[
|
label:
|
||||||
SizedBox(
|
'''Posted by ${state.item.by} ${state.item.timeAgo}, ${state.item.title}. ${state.item.text}''',
|
||||||
height: topPadding,
|
child: Column(
|
||||||
),
|
children: <Widget>[
|
||||||
if (!splitViewEnabled)
|
SizedBox(
|
||||||
const Padding(
|
height: topPadding,
|
||||||
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
|
||||||
child: OfflineBanner(),
|
|
||||||
),
|
),
|
||||||
Slidable(
|
if (!splitViewEnabled)
|
||||||
startActionPane: ActionPane(
|
const Padding(
|
||||||
motion: const BehindMotion(),
|
padding: EdgeInsets.only(bottom: Dimens.pt6),
|
||||||
children: <Widget>[
|
child: OfflineBanner(),
|
||||||
SlidableAction(
|
),
|
||||||
onPressed: (_) {
|
Slidable(
|
||||||
HapticFeedback.lightImpact();
|
startActionPane: ActionPane(
|
||||||
|
motion: const BehindMotion(),
|
||||||
|
children: <Widget>[
|
||||||
|
SlidableAction(
|
||||||
|
onPressed: (_) {
|
||||||
|
HapticFeedback.lightImpact();
|
||||||
|
|
||||||
if (state.item.id !=
|
if (state.item.id !=
|
||||||
context.read<EditCubit>().state.replyingTo?.id) {
|
context.read<EditCubit>().state.replyingTo?.id) {
|
||||||
commentEditingController.clear();
|
commentEditingController.clear();
|
||||||
}
|
}
|
||||||
context.read<EditCubit>().onReplyTapped(state.item);
|
context.read<EditCubit>().onReplyTapped(state.item);
|
||||||
focusNode.requestFocus();
|
focusNode.requestFocus();
|
||||||
},
|
},
|
||||||
backgroundColor: Palette.orange,
|
backgroundColor: Palette.orange,
|
||||||
foregroundColor: Palette.white,
|
foregroundColor: Palette.white,
|
||||||
icon: Icons.message,
|
icon: Icons.message,
|
||||||
),
|
|
||||||
SlidableAction(
|
|
||||||
onPressed: (BuildContext context) =>
|
|
||||||
onMoreTapped(state.item, context.rect),
|
|
||||||
backgroundColor: Palette.orange,
|
|
||||||
foregroundColor: Palette.white,
|
|
||||||
icon: Icons.more_horiz,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: Dimens.pt6,
|
|
||||||
right: Dimens.pt6,
|
|
||||||
),
|
),
|
||||||
child: Row(
|
SlidableAction(
|
||||||
children: <Widget>[
|
onPressed: (BuildContext context) =>
|
||||||
Text(
|
onMoreTapped(state.item, context.rect),
|
||||||
state.item.by,
|
backgroundColor: Palette.orange,
|
||||||
style: const TextStyle(
|
foregroundColor: Palette.white,
|
||||||
color: Palette.orange,
|
icon: Icons.more_horiz,
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Text(
|
|
||||||
state.item.postedDate,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Palette.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
BlocBuilder<PreferenceCubit, PreferenceState>(
|
),
|
||||||
buildWhen: (
|
child: Column(
|
||||||
PreferenceState previous,
|
children: <Widget>[
|
||||||
PreferenceState current,
|
Padding(
|
||||||
) =>
|
padding: const EdgeInsets.only(
|
||||||
previous.fontSize != current.fontSize,
|
left: Dimens.pt6,
|
||||||
builder: (
|
right: Dimens.pt6,
|
||||||
BuildContext context,
|
),
|
||||||
PreferenceState prefState,
|
child: Row(
|
||||||
) {
|
|
||||||
return Column(
|
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
if (state.item is Story)
|
Text(
|
||||||
InkWell(
|
state.item.by,
|
||||||
onTap: () => LinkUtil.launch(
|
style: const TextStyle(
|
||||||
state.item.url,
|
color: Palette.orange,
|
||||||
useReader: context
|
),
|
||||||
.read<PreferenceCubit>()
|
),
|
||||||
.state
|
const Spacer(),
|
||||||
.readerEnabled,
|
Text(
|
||||||
offlineReading: context
|
state.item.timeAgo,
|
||||||
.read<StoriesBloc>()
|
style: const TextStyle(
|
||||||
.state
|
color: Palette.grey,
|
||||||
.offlineReading,
|
),
|
||||||
),
|
),
|
||||||
child: Padding(
|
],
|
||||||
padding: const EdgeInsets.only(
|
),
|
||||||
left: Dimens.pt6,
|
),
|
||||||
right: Dimens.pt6,
|
BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||||
bottom: Dimens.pt12,
|
buildWhen: (
|
||||||
top: Dimens.pt12,
|
PreferenceState previous,
|
||||||
|
PreferenceState current,
|
||||||
|
) =>
|
||||||
|
previous.fontSize != current.fontSize,
|
||||||
|
builder: (
|
||||||
|
BuildContext context,
|
||||||
|
PreferenceState prefState,
|
||||||
|
) {
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
if (state.item is Story)
|
||||||
|
InkWell(
|
||||||
|
onTap: () => LinkUtil.launch(
|
||||||
|
state.item.url,
|
||||||
|
useReader: context
|
||||||
|
.read<PreferenceCubit>()
|
||||||
|
.state
|
||||||
|
.readerEnabled,
|
||||||
|
offlineReading: context
|
||||||
|
.read<StoriesBloc>()
|
||||||
|
.state
|
||||||
|
.isOfflineReading,
|
||||||
),
|
),
|
||||||
child: Text.rich(
|
child: Padding(
|
||||||
TextSpan(
|
padding: const EdgeInsets.only(
|
||||||
style: TextStyle(
|
left: Dimens.pt6,
|
||||||
fontWeight: FontWeight.bold,
|
right: Dimens.pt6,
|
||||||
fontSize: prefState.fontSize.fontSize,
|
bottom: Dimens.pt12,
|
||||||
color: Theme.of(context)
|
top: Dimens.pt12,
|
||||||
.textTheme
|
),
|
||||||
.bodyLarge
|
child: Text.rich(
|
||||||
?.color,
|
TextSpan(
|
||||||
),
|
style: TextStyle(
|
||||||
children: <TextSpan>[
|
fontWeight: FontWeight.bold,
|
||||||
TextSpan(
|
fontSize: prefState.fontSize.fontSize,
|
||||||
text: state.item.title,
|
color: Theme.of(context)
|
||||||
style: TextStyle(
|
.textTheme
|
||||||
fontWeight: FontWeight.bold,
|
.bodyLarge
|
||||||
fontSize: prefState.fontSize.fontSize,
|
?.color,
|
||||||
color: state.item.url.isNotEmpty
|
|
||||||
? Palette.orange
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (state.item.url.isNotEmpty)
|
children: <TextSpan>[
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text:
|
semanticsLabel: state.item.title,
|
||||||
''' (${(state.item as Story).readableUrl})''',
|
text: state.item.title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize:
|
fontSize: prefState.fontSize.fontSize,
|
||||||
prefState.fontSize.fontSize - 4,
|
color: state.item.url.isNotEmpty
|
||||||
color: Palette.orange,
|
? Palette.orange
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
if (state.item.url.isNotEmpty)
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
''' (${(state.item as Story).readableUrl})''',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize:
|
||||||
|
prefState.fontSize.fontSize - 4,
|
||||||
|
color: Palette.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
textScaleFactor: MediaQuery.of(
|
||||||
|
context,
|
||||||
|
).textScaleFactor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt6,
|
||||||
|
),
|
||||||
|
if (state.item.text.isNotEmpty)
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Dimens.pt10,
|
||||||
|
),
|
||||||
|
child: ItemText(
|
||||||
|
item: state.item,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
|
||||||
textScaleFactor: MediaQuery.of(
|
|
||||||
context,
|
|
||||||
).textScaleFactor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
else
|
);
|
||||||
const SizedBox(
|
},
|
||||||
height: Dimens.pt6,
|
|
||||||
),
|
|
||||||
if (state.item.text.isNotEmpty)
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: Dimens.pt10,
|
|
||||||
),
|
|
||||||
child: ItemText(
|
|
||||||
item: state.item,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (state.item.isPoll)
|
|
||||||
BlocProvider<PollCubit>(
|
|
||||||
create: (BuildContext context) =>
|
|
||||||
PollCubit(story: state.item as Story)..init(),
|
|
||||||
child: const PollView(),
|
|
||||||
),
|
),
|
||||||
],
|
if (state.item.isPoll)
|
||||||
),
|
BlocProvider<PollCubit>(
|
||||||
),
|
create: (BuildContext context) =>
|
||||||
if (state.item.text.isNotEmpty)
|
PollCubit(story: state.item as Story)..init(),
|
||||||
const SizedBox(
|
child: const PollView(),
|
||||||
height: Dimens.pt8,
|
),
|
||||||
),
|
],
|
||||||
const Divider(
|
|
||||||
height: Dimens.zero,
|
|
||||||
),
|
|
||||||
if (state.onlyShowTargetComment) ...<Widget>[
|
|
||||||
Center(
|
|
||||||
child: TextButton(
|
|
||||||
onPressed: () =>
|
|
||||||
context.read<CommentsCubit>().loadAll(state.item as Story),
|
|
||||||
child: const Text('View all comments'),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (state.item.text.isNotEmpty)
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt8,
|
||||||
|
),
|
||||||
const Divider(
|
const Divider(
|
||||||
height: Dimens.zero,
|
height: Dimens.zero,
|
||||||
),
|
),
|
||||||
] else ...<Widget>[
|
if (state.onlyShowTargetComment) ...<Widget>[
|
||||||
Row(
|
Center(
|
||||||
children: <Widget>[
|
child: TextButton(
|
||||||
if (state.item is Story) ...<Widget>[
|
onPressed: () =>
|
||||||
const SizedBox(
|
context.read<CommentsCubit>().loadAll(state.item as Story),
|
||||||
width: Dimens.pt12,
|
child: const Text('View all comments'),
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
const Divider(
|
||||||
style: const TextStyle(
|
height: Dimens.zero,
|
||||||
fontSize: TextDimens.pt13,
|
),
|
||||||
|
] else ...<Widget>[
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
if (state.item is Story) ...<Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
width: Dimens.pt12,
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
] else ...<Widget>[
|
'''${state.item.score} karma, ${state.item.descendants} comment${state.item.descendants > 1 ? 's' : ''}''',
|
||||||
const SizedBox(
|
style: const TextStyle(
|
||||||
width: Dimens.pt4,
|
fontSize: TextDimens.pt13,
|
||||||
),
|
),
|
||||||
TextButton(
|
),
|
||||||
onPressed: context.read<CommentsCubit>().loadParentThread,
|
] else ...<Widget>[
|
||||||
child: state.fetchParentStatus == CommentsStatus.loading
|
const SizedBox(
|
||||||
? const SizedBox(
|
width: Dimens.pt4,
|
||||||
height: Dimens.pt12,
|
),
|
||||||
width: Dimens.pt12,
|
TextButton(
|
||||||
child: CustomCircularProgressIndicator(
|
onPressed: context.read<CommentsCubit>().loadParentThread,
|
||||||
strokeWidth: Dimens.pt2,
|
child: state.fetchParentStatus == CommentsStatus.loading
|
||||||
|
? const SizedBox(
|
||||||
|
height: Dimens.pt12,
|
||||||
|
width: Dimens.pt12,
|
||||||
|
child: CustomCircularProgressIndicator(
|
||||||
|
strokeWidth: Dimens.pt2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'View parent thread',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TextDimens.pt13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
if (!state.isOfflineReading)
|
||||||
|
DropdownButton<FetchMode>(
|
||||||
|
value: state.fetchMode,
|
||||||
|
underline: const SizedBox.shrink(),
|
||||||
|
items: FetchMode.values
|
||||||
|
.map(
|
||||||
|
(FetchMode val) => DropdownMenuItem<FetchMode>(
|
||||||
|
value: val,
|
||||||
|
child: Text(
|
||||||
|
val.description,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: TextDimens.pt13,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Text(
|
.toList(),
|
||||||
'View parent thread',
|
onChanged: context.read<CommentsCubit>().onFetchModeChanged,
|
||||||
style: TextStyle(
|
),
|
||||||
fontSize: TextDimens.pt13,
|
const SizedBox(
|
||||||
),
|
width: Dimens.pt6,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
DropdownButton<CommentsOrder>(
|
||||||
const Spacer(),
|
value: state.order,
|
||||||
if (!state.offlineReading)
|
|
||||||
DropdownButton<FetchMode>(
|
|
||||||
value: state.fetchMode,
|
|
||||||
underline: const SizedBox.shrink(),
|
underline: const SizedBox.shrink(),
|
||||||
items: FetchMode.values
|
items: CommentsOrder.values
|
||||||
.map(
|
.map(
|
||||||
(FetchMode val) => DropdownMenuItem<FetchMode>(
|
(CommentsOrder val) => DropdownMenuItem<CommentsOrder>(
|
||||||
value: val,
|
value: val,
|
||||||
child: Text(
|
child: Text(
|
||||||
val.description,
|
val.description,
|
||||||
@ -467,51 +492,31 @@ class _ParentItemSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: context.read<CommentsCubit>().onFetchModeChanged,
|
onChanged: context.read<CommentsCubit>().onOrderChanged,
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: Dimens.pt6,
|
width: Dimens.pt4,
|
||||||
),
|
),
|
||||||
DropdownButton<CommentsOrder>(
|
],
|
||||||
value: state.order,
|
|
||||||
underline: const SizedBox.shrink(),
|
|
||||||
items: CommentsOrder.values
|
|
||||||
.map(
|
|
||||||
(CommentsOrder val) => DropdownMenuItem<CommentsOrder>(
|
|
||||||
value: val,
|
|
||||||
child: Text(
|
|
||||||
val.description,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: TextDimens.pt13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
onChanged: context.read<CommentsCubit>().onOrderChanged,
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: Dimens.pt4,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Divider(
|
|
||||||
height: Dimens.zero,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (state.comments.isEmpty &&
|
|
||||||
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
|
||||||
const SizedBox(
|
|
||||||
height: 240,
|
|
||||||
),
|
|
||||||
const Center(
|
|
||||||
child: Text(
|
|
||||||
'Nothing yet',
|
|
||||||
style: TextStyle(color: Palette.grey),
|
|
||||||
),
|
),
|
||||||
),
|
const Divider(
|
||||||
|
height: Dimens.zero,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (state.comments.isEmpty &&
|
||||||
|
state.status == CommentsStatus.allLoaded) ...<Widget>[
|
||||||
|
const SizedBox(
|
||||||
|
height: 240,
|
||||||
|
),
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'Nothing yet',
|
||||||
|
style: TextStyle(color: Palette.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
|
||||||
@ -25,16 +23,8 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
final bool isBlocked;
|
final bool isBlocked;
|
||||||
final VoidCallback onLoginTapped;
|
final VoidCallback onLoginTapped;
|
||||||
|
|
||||||
static double? _cachedStoryHeight;
|
static const double _storySheetHeight = 500;
|
||||||
static double? _cachedCommentHeight;
|
static const double _commentSheetHeight = 480;
|
||||||
|
|
||||||
static double get storyHeight {
|
|
||||||
return _cachedStoryHeight ??= Platform.isIOS ? 500 : 530;
|
|
||||||
}
|
|
||||||
|
|
||||||
static double get commentHeight {
|
|
||||||
return _cachedCommentHeight ??= Platform.isIOS ? 480 : 520;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -80,7 +70,7 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
final bool upvoted = voteState.vote == Vote.up;
|
final bool upvoted = voteState.vote == Vote.up;
|
||||||
final bool downvoted = voteState.vote == Vote.down;
|
final bool downvoted = voteState.vote == Vote.down;
|
||||||
return Container(
|
return Container(
|
||||||
height: item is Comment ? commentHeight : storyHeight,
|
height: item is Comment ? _commentSheetHeight : _storySheetHeight,
|
||||||
color: Theme.of(context).canvasColor,
|
color: Theme.of(context).canvasColor,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Palette.transparent,
|
color: Palette.transparent,
|
||||||
@ -91,63 +81,71 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
UserCubit()..init(userId: item.by),
|
UserCubit()..init(userId: item.by),
|
||||||
child: BlocBuilder<UserCubit, UserState>(
|
child: BlocBuilder<UserCubit, UserState>(
|
||||||
builder: (BuildContext context, UserState state) {
|
builder: (BuildContext context, UserState state) {
|
||||||
return ListTile(
|
return Semantics(
|
||||||
leading: const Icon(
|
excludeSemantics: state.status == UserStatus.loading,
|
||||||
Icons.account_circle,
|
child: ListTile(
|
||||||
),
|
leading: const Icon(
|
||||||
title: Text(item.by),
|
Icons.account_circle,
|
||||||
subtitle: Text(
|
),
|
||||||
state.user.description,
|
title: Text(item.by),
|
||||||
),
|
subtitle: Text(
|
||||||
onTap: () {
|
state.user.description,
|
||||||
Navigator.pop(context);
|
),
|
||||||
showDialog<void>(
|
onTap: () {
|
||||||
context: context,
|
Navigator.pop(context);
|
||||||
builder: (BuildContext context) => AlertDialog(
|
showDialog<void>(
|
||||||
title: Text('About ${state.user.id}'),
|
context: context,
|
||||||
content: state.user.about.isEmpty
|
builder: (BuildContext context) => AlertDialog(
|
||||||
? Row(
|
semanticLabel:
|
||||||
mainAxisAlignment:
|
'''About ${state.user.id}. ${state.user.about}''',
|
||||||
MainAxisAlignment.center,
|
title: Text(
|
||||||
children: const <Widget>[
|
'About ${state.user.id}',
|
||||||
Text(
|
),
|
||||||
'empty',
|
content: state.user.about.isEmpty
|
||||||
style: TextStyle(
|
? Row(
|
||||||
color: Palette.grey,
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: const <Widget>[
|
||||||
|
Text(
|
||||||
|
'empty',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Palette.grey,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: SelectableLinkify(
|
||||||
|
text: HtmlUtil.parseHtml(
|
||||||
|
state.user.about,
|
||||||
),
|
),
|
||||||
],
|
linkStyle: const TextStyle(
|
||||||
)
|
color: Palette.orange,
|
||||||
: SelectableLinkify(
|
),
|
||||||
text: HtmlUtil.parseHtml(
|
onOpen: (LinkableElement link) =>
|
||||||
state.user.about,
|
LinkUtil.launch(link.url),
|
||||||
|
semanticsLabel: state.user.about,
|
||||||
),
|
),
|
||||||
linkStyle: const TextStyle(
|
actions: <Widget>[
|
||||||
color: Palette.orange,
|
TextButton(
|
||||||
),
|
onPressed: () {
|
||||||
onOpen: (LinkableElement link) =>
|
Navigator.pop(context);
|
||||||
LinkUtil.launch(link.url),
|
onSearchUserTapped(context);
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'Search',
|
||||||
),
|
),
|
||||||
actions: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
onSearchUserTapped(context);
|
|
||||||
},
|
|
||||||
child: const Text(
|
|
||||||
'Search',
|
|
||||||
),
|
),
|
||||||
),
|
TextButton(
|
||||||
TextButton(
|
onPressed: () => Navigator.pop(context),
|
||||||
onPressed: () => Navigator.pop(context),
|
child: const Text(
|
||||||
child: const Text(
|
'Okay',
|
||||||
'Okay',
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -34,6 +34,7 @@ class _ScrollUpIconButtonState extends State<ScrollUpIconButton> {
|
|||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: opacity.clamp(0, 1),
|
opacity: opacity.clamp(0, 1),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
|
tooltip: 'Scroll to top',
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
FeatherIcons.chevronsUp,
|
FeatherIcons.chevronsUp,
|
||||||
color: Palette.orange,
|
color: Palette.orange,
|
||||||
|
@ -118,7 +118,7 @@ class InboxView extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
e.postedDate,
|
e.timeAgo,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Palette.grey,
|
color: Palette.grey,
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||||
|
import 'package:clipboard/clipboard.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -21,7 +22,6 @@ import 'package:hacki/screens/profile/widgets/tab_bar_settings.dart';
|
|||||||
import 'package:hacki/screens/widgets/widgets.dart';
|
import 'package:hacki/screens/widgets/widgets.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
import 'package:logger/logger.dart';
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
@ -192,7 +192,7 @@ class _SettingsState extends State<Settings> {
|
|||||||
.whereType<BooleanPreference>()
|
.whereType<BooleanPreference>()
|
||||||
.where(
|
.where(
|
||||||
(Preference<dynamic> e) => e.isDisplayable,
|
(Preference<dynamic> e) => e.isDisplayable,
|
||||||
))
|
)) ...<Widget>[
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(preference.title),
|
title: Text(preference.title),
|
||||||
subtitle: preference.subtitle.isNotEmpty
|
subtitle: preference.subtitle.isNotEmpty
|
||||||
@ -217,6 +217,8 @@ class _SettingsState extends State<Settings> {
|
|||||||
},
|
},
|
||||||
activeColor: Palette.orange,
|
activeColor: Palette.orange,
|
||||||
),
|
),
|
||||||
|
if (preference is StoryUrlModePreference) const Divider(),
|
||||||
|
],
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Font',
|
'Font',
|
||||||
@ -229,11 +231,24 @@ class _SettingsState extends State<Settings> {
|
|||||||
),
|
),
|
||||||
onTap: showThemeSettingDialog,
|
onTap: showThemeSettingDialog,
|
||||||
),
|
),
|
||||||
|
const Divider(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Clear Data',
|
'Export Favorites',
|
||||||
),
|
),
|
||||||
onTap: showClearDataDialog,
|
onTap: onExportFavoritesTapped,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text(
|
||||||
|
'Clear Favorites',
|
||||||
|
),
|
||||||
|
onTap: showClearFavoritesDialog,
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: const Text(
|
||||||
|
'Clear Cache',
|
||||||
|
),
|
||||||
|
onTap: showClearCacheDialog,
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('About'),
|
title: const Text('About'),
|
||||||
@ -376,12 +391,12 @@ class _SettingsState extends State<Settings> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showClearDataDialog() {
|
void showClearCacheDialog() {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Clear Data?'),
|
title: const Text('Clear Cache?'),
|
||||||
content: const Text(
|
content: const Text(
|
||||||
'Clear all cached images, stories and comments.',
|
'Clear all cached images, stories and comments.',
|
||||||
),
|
),
|
||||||
@ -411,7 +426,7 @@ class _SettingsState extends State<Settings> {
|
|||||||
DefaultCacheManager().emptyCache,
|
DefaultCacheManager().emptyCache,
|
||||||
)
|
)
|
||||||
.whenComplete(() {
|
.whenComplete(() {
|
||||||
showSnackBar(content: 'Data cleared!');
|
showSnackBar(content: 'Cache cleared!');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
@ -621,11 +636,64 @@ class _SettingsState extends State<Settings> {
|
|||||||
LinkUtil.launchInExternalBrowser(Constants.githubIssueLink);
|
LinkUtil.launchInExternalBrowser(Constants.githubIssueLink);
|
||||||
}
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
locator.get<Logger>().e(
|
error.logError(stackTrace: stackTrace);
|
||||||
'Error caught in onGithubTapped',
|
|
||||||
error,
|
|
||||||
stackTrace,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> onExportFavoritesTapped() async {
|
||||||
|
final List<int> allFavorites = context.read<FavCubit>().state.favIds;
|
||||||
|
|
||||||
|
if (allFavorites.isEmpty) {
|
||||||
|
showSnackBar(content: "You don't have any favorite item.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await FlutterClipboard.copy(
|
||||||
|
allFavorites.join('\n'),
|
||||||
|
).whenComplete(HapticFeedback.selectionClick);
|
||||||
|
showSnackBar(content: 'Ids of favorites have been copied to clipboard.');
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
error.logError(stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showClearFavoritesDialog() {
|
||||||
|
showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Remove all favorites?'),
|
||||||
|
content: const Text(
|
||||||
|
'''This will not effect favorites saved in your Hacker News account.''',
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text(
|
||||||
|
'Cancel',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
try {
|
||||||
|
context.read<FavCubit>().removeAll();
|
||||||
|
showSnackBar(content: 'All favorites have been removed.');
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
error.logError(stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'Confirm',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Palette.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Slidable(
|
Slidable(
|
||||||
|
enabled: context.isScreenReaderEnabled == false,
|
||||||
startActionPane: actionable
|
startActionPane: actionable
|
||||||
? ActionPane(
|
? ActionPane(
|
||||||
motion: const StretchMotion(),
|
motion: const StretchMotion(),
|
||||||
@ -117,6 +118,14 @@ class CommentTile extends StatelessWidget {
|
|||||||
: null,
|
: null,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (context.isScreenReaderEnabled) {
|
||||||
|
onMoreTapped?.call(
|
||||||
|
comment,
|
||||||
|
context.rect,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (actionable) {
|
if (actionable) {
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
context.read<CollapseCubit>().collapse();
|
context.read<CollapseCubit>().collapse();
|
||||||
@ -152,7 +161,7 @@ class CommentTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
comment.postedDate,
|
comment.timeAgo,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Palette.grey,
|
color: Palette.grey,
|
||||||
),
|
),
|
||||||
@ -188,16 +197,19 @@ class CommentTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ItemText(
|
child: Semantics(
|
||||||
key: ValueKey<int>(comment.id),
|
label: '''At level ${comment.level}.''',
|
||||||
item: comment,
|
child: ItemText(
|
||||||
onTap: () {
|
key: ValueKey<int>(comment.id),
|
||||||
if (onTap == null) {
|
item: comment,
|
||||||
_onTextTapped(context);
|
onTap: () {
|
||||||
} else {
|
if (onTap == null) {
|
||||||
onTap!.call();
|
_onTextTapped(context);
|
||||||
}
|
} else {
|
||||||
},
|
onTap!.call();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -5,6 +5,7 @@ import 'package:hacki/styles/palette.dart';
|
|||||||
import 'package:hacki/utils/utils.dart';
|
import 'package:hacki/utils/utils.dart';
|
||||||
import 'package:linkify/linkify.dart';
|
import 'package:linkify/linkify.dart';
|
||||||
|
|
||||||
|
export 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
||||||
export 'package:linkify/linkify.dart'
|
export 'package:linkify/linkify.dart'
|
||||||
show
|
show
|
||||||
LinkifyElement,
|
LinkifyElement,
|
||||||
@ -27,7 +28,7 @@ class Linkify extends StatelessWidget {
|
|||||||
required this.text,
|
required this.text,
|
||||||
this.linkifiers = defaultLinkifiers,
|
this.linkifiers = defaultLinkifiers,
|
||||||
this.onOpen,
|
this.onOpen,
|
||||||
this.options = const LinkifyOptions(),
|
this.options = LinkifierUtil.linkifyOptions,
|
||||||
// TextSpan
|
// TextSpan
|
||||||
this.style,
|
this.style,
|
||||||
this.linkStyle,
|
this.linkStyle,
|
||||||
@ -152,9 +153,10 @@ class SelectableLinkify extends StatelessWidget {
|
|||||||
const SelectableLinkify({
|
const SelectableLinkify({
|
||||||
super.key,
|
super.key,
|
||||||
required this.text,
|
required this.text,
|
||||||
|
this.semanticsLabel,
|
||||||
this.linkifiers = defaultLinkifiers,
|
this.linkifiers = defaultLinkifiers,
|
||||||
this.onOpen,
|
this.onOpen,
|
||||||
this.options = const LinkifyOptions(),
|
this.options = LinkifierUtil.linkifyOptions,
|
||||||
// TextSpan
|
// TextSpan
|
||||||
this.style,
|
this.style,
|
||||||
this.linkStyle,
|
this.linkStyle,
|
||||||
@ -187,6 +189,8 @@ class SelectableLinkify extends StatelessWidget {
|
|||||||
/// Text to be linkified
|
/// Text to be linkified
|
||||||
final String text;
|
final String text;
|
||||||
|
|
||||||
|
final String? semanticsLabel;
|
||||||
|
|
||||||
/// The number of font pixels for each logical pixel
|
/// The number of font pixels for each logical pixel
|
||||||
final double textScaleFactor;
|
final double textScaleFactor;
|
||||||
|
|
||||||
@ -316,6 +320,7 @@ class SelectableLinkify extends StatelessWidget {
|
|||||||
selectionControls: selectionControls,
|
selectionControls: selectionControls,
|
||||||
onSelectionChanged: onSelectionChanged,
|
onSelectionChanged: onSelectionChanged,
|
||||||
contextMenuBuilder: contextMenuBuilder,
|
contextMenuBuilder: contextMenuBuilder,
|
||||||
|
semanticsLabel: semanticsLabel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:linkify/linkify.dart';
|
import 'package:linkify/linkify.dart';
|
||||||
|
|
||||||
final RegExp _quoteRegex = RegExp(
|
final RegExp _quoteRegex = RegExp(
|
||||||
r'(?=^> )(.*?)(?=\n|$)',
|
r'(?=^>)(.*?)(?=\n|$)',
|
||||||
multiLine: true,
|
multiLine: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -38,15 +38,18 @@ class ItemText extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
||||||
contextMenuBuilder: (
|
contextMenuBuilder: context.isScreenReaderEnabled
|
||||||
BuildContext context,
|
? null
|
||||||
EditableTextState editableTextState,
|
: (
|
||||||
) =>
|
BuildContext context,
|
||||||
contextMenuBuilder(
|
EditableTextState editableTextState,
|
||||||
context,
|
) =>
|
||||||
editableTextState,
|
contextMenuBuilder(
|
||||||
item: item,
|
context,
|
||||||
),
|
editableTextState,
|
||||||
|
item: item,
|
||||||
|
),
|
||||||
|
semanticsLabel: item.text,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return SelectableLinkify(
|
return SelectableLinkify(
|
||||||
@ -56,15 +59,18 @@ class ItemText extends StatelessWidget {
|
|||||||
linkStyle: linkStyle,
|
linkStyle: linkStyle,
|
||||||
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
|
onOpen: (LinkableElement link) => LinkUtil.launch(link.url),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
contextMenuBuilder: (
|
contextMenuBuilder: context.isScreenReaderEnabled
|
||||||
BuildContext context,
|
? null
|
||||||
EditableTextState editableTextState,
|
: (
|
||||||
) =>
|
BuildContext context,
|
||||||
contextMenuBuilder(
|
EditableTextState editableTextState,
|
||||||
context,
|
) =>
|
||||||
editableTextState,
|
contextMenuBuilder(
|
||||||
item: item,
|
context,
|
||||||
),
|
editableTextState,
|
||||||
|
item: item,
|
||||||
|
),
|
||||||
|
semanticsLabel: item.text,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,7 +200,7 @@ class ItemsListView<T extends Item> extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
e.postedDate,
|
e.timeAgo,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Palette.grey,
|
color: Palette.grey,
|
||||||
),
|
),
|
||||||
|
@ -16,10 +16,9 @@ class LinkPreview extends StatefulWidget {
|
|||||||
required this.story,
|
required this.story,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
required this.showUrl,
|
required this.showUrl,
|
||||||
required this.offlineReading,
|
required this.isOfflineReading,
|
||||||
|
required this.titleStyle,
|
||||||
this.cache = const Duration(days: 30),
|
this.cache = const Duration(days: 30),
|
||||||
this.titleStyle,
|
|
||||||
this.bodyStyle,
|
|
||||||
this.showMultimedia = true,
|
this.showMultimedia = true,
|
||||||
this.backgroundColor = const Color.fromRGBO(235, 235, 235, 1),
|
this.backgroundColor = const Color.fromRGBO(235, 235, 235, 1),
|
||||||
this.bodyMaxLines = 3,
|
this.bodyMaxLines = 3,
|
||||||
@ -84,10 +83,7 @@ class LinkPreview extends StatefulWidget {
|
|||||||
final Duration cache;
|
final Duration cache;
|
||||||
|
|
||||||
/// Customize body `TextStyle`
|
/// Customize body `TextStyle`
|
||||||
final TextStyle? titleStyle;
|
final TextStyle titleStyle;
|
||||||
|
|
||||||
/// Customize body `TextStyle`
|
|
||||||
final TextStyle? bodyStyle;
|
|
||||||
|
|
||||||
/// Show or Hide image if available defaults to `true`
|
/// Show or Hide image if available defaults to `true`
|
||||||
final bool showMultimedia;
|
final bool showMultimedia;
|
||||||
@ -105,7 +101,7 @@ class LinkPreview extends StatefulWidget {
|
|||||||
|
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
final bool showUrl;
|
final bool showUrl;
|
||||||
final bool offlineReading;
|
final bool isOfflineReading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_LinkPreviewState createState() => _LinkPreviewState();
|
_LinkPreviewState createState() => _LinkPreviewState();
|
||||||
@ -135,7 +131,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
|||||||
_info = await WebAnalyzer.getInfo(
|
_info = await WebAnalyzer.getInfo(
|
||||||
story: widget.story,
|
story: widget.story,
|
||||||
cache: widget.cache,
|
cache: widget.cache,
|
||||||
offlineReading: widget.offlineReading,
|
offlineReading: widget.isOfflineReading,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -190,7 +186,6 @@ class _LinkPreviewState extends State<LinkPreview> {
|
|||||||
imagePath: Constants.hackerNewsLogoPath,
|
imagePath: Constants.hackerNewsLogoPath,
|
||||||
onTap: _launchURL,
|
onTap: _launchURL,
|
||||||
titleTextStyle: widget.titleStyle,
|
titleTextStyle: widget.titleStyle,
|
||||||
bodyTextStyle: widget.bodyStyle,
|
|
||||||
bodyTextOverflow: widget.bodyTextOverflow,
|
bodyTextOverflow: widget.bodyTextOverflow,
|
||||||
bodyMaxLines: widget.bodyMaxLines,
|
bodyMaxLines: widget.bodyMaxLines,
|
||||||
showMultiMedia: widget.showMultimedia,
|
showMultiMedia: widget.showMultimedia,
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
|
import 'package:hacki/models/models.dart';
|
||||||
|
import 'package:hacki/screens/widgets/link_preview/models/models.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
|
|
||||||
class LinkView extends StatelessWidget {
|
class LinkView extends StatelessWidget {
|
||||||
const LinkView({
|
LinkView({
|
||||||
super.key,
|
super.key,
|
||||||
required this.metadata,
|
required this.metadata,
|
||||||
required this.url,
|
required this.url,
|
||||||
@ -13,18 +17,18 @@ class LinkView extends StatelessWidget {
|
|||||||
required this.description,
|
required this.description,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
required this.showMetadata,
|
required this.showMetadata,
|
||||||
required this.showUrl,
|
required bool showUrl,
|
||||||
|
required this.bodyMaxLines,
|
||||||
|
required this.titleTextStyle,
|
||||||
this.imageUri,
|
this.imageUri,
|
||||||
this.imagePath,
|
this.imagePath,
|
||||||
this.titleTextStyle,
|
|
||||||
this.bodyTextStyle,
|
|
||||||
this.showMultiMedia = true,
|
this.showMultiMedia = true,
|
||||||
this.bodyTextOverflow,
|
this.bodyTextOverflow,
|
||||||
this.bodyMaxLines,
|
|
||||||
this.isIcon = false,
|
this.isIcon = false,
|
||||||
this.bgColor,
|
this.bgColor,
|
||||||
this.radius = 0,
|
this.radius = 0,
|
||||||
}) : assert(
|
}) : showUrl = showUrl && url.isNotEmpty,
|
||||||
|
assert(
|
||||||
!showMultiMedia ||
|
!showMultiMedia ||
|
||||||
(showMultiMedia && (imageUri != null || imagePath != null)),
|
(showMultiMedia && (imageUri != null || imagePath != null)),
|
||||||
'imageUri or imagePath cannot be null when showMultiMedia is true',
|
'imageUri or imagePath cannot be null when showMultiMedia is true',
|
||||||
@ -38,35 +42,101 @@ class LinkView extends StatelessWidget {
|
|||||||
final String? imageUri;
|
final String? imageUri;
|
||||||
final String? imagePath;
|
final String? imagePath;
|
||||||
final void Function(String) onTap;
|
final void Function(String) onTap;
|
||||||
final TextStyle? titleTextStyle;
|
final TextStyle titleTextStyle;
|
||||||
final TextStyle? bodyTextStyle;
|
|
||||||
final bool showMultiMedia;
|
final bool showMultiMedia;
|
||||||
final TextOverflow? bodyTextOverflow;
|
final TextOverflow? bodyTextOverflow;
|
||||||
final int? bodyMaxLines;
|
final int bodyMaxLines;
|
||||||
final bool isIcon;
|
final bool isIcon;
|
||||||
final double radius;
|
final double radius;
|
||||||
final Color? bgColor;
|
final Color? bgColor;
|
||||||
final bool showMetadata;
|
final bool showMetadata;
|
||||||
final bool showUrl;
|
final bool showUrl;
|
||||||
|
|
||||||
double computeTitleFontSize(double width) {
|
static const double _bottomPadding = 6;
|
||||||
double size = width * 0.13;
|
static late TextStyle _urlStyle;
|
||||||
if (size > 15) {
|
static late TextStyle _metadataStyle;
|
||||||
size = 15;
|
static late TextStyle _descriptionStyle;
|
||||||
}
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
int computeTitleLines(double layoutHeight) {
|
static final Map<MaxLineComputationParams, int> _computationCache =
|
||||||
return layoutHeight >= 100 ? 2 : 1;
|
<MaxLineComputationParams, int>{};
|
||||||
}
|
|
||||||
|
|
||||||
int computeBodyLines(double layoutHeight) {
|
static int getDescriptionMaxLines(
|
||||||
int lines = 1;
|
MaxLineComputationParams params,
|
||||||
if (layoutHeight > 40) {
|
TextStyle titleStyle,
|
||||||
lines += (layoutHeight - 40.0) ~/ 15.0;
|
) {
|
||||||
|
if (_computationCache.containsKey(params)) {
|
||||||
|
return _computationCache[params]!;
|
||||||
}
|
}
|
||||||
return lines;
|
|
||||||
|
_urlStyle = titleStyle.copyWith(
|
||||||
|
color: Palette.grey,
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontFamily: params.fontFamily,
|
||||||
|
);
|
||||||
|
_descriptionStyle = TextStyle(
|
||||||
|
color: Palette.grey,
|
||||||
|
fontFamily: params.fontFamily,
|
||||||
|
fontSize: TextDimens.pt14,
|
||||||
|
);
|
||||||
|
_metadataStyle = _descriptionStyle.copyWith(
|
||||||
|
fontSize: TextDimens.pt12,
|
||||||
|
fontFamily: params.fontFamily,
|
||||||
|
);
|
||||||
|
|
||||||
|
final double urlHeight = (TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: '(url)',
|
||||||
|
style: _urlStyle,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
textScaleFactor: params.textScaleFactor,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout())
|
||||||
|
.size
|
||||||
|
.height;
|
||||||
|
final double metadataHeight = (TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: '123metadata',
|
||||||
|
style: _metadataStyle,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
textScaleFactor: params.textScaleFactor,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout())
|
||||||
|
.size
|
||||||
|
.height;
|
||||||
|
final double descriptionHeight = (TextPainter(
|
||||||
|
text: TextSpan(
|
||||||
|
text: 'DESCRIPTION',
|
||||||
|
style: _descriptionStyle,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
textScaleFactor: params.textScaleFactor,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout())
|
||||||
|
.size
|
||||||
|
.height;
|
||||||
|
|
||||||
|
final double allPaddings =
|
||||||
|
params.fontFamily == Font.robotoSlab.name ? Dimens.pt2 : Dimens.pt4;
|
||||||
|
|
||||||
|
final double height = <double>[
|
||||||
|
params.titleHeight,
|
||||||
|
if (params.showUrl) urlHeight,
|
||||||
|
if (params.showMetadata) metadataHeight,
|
||||||
|
allPaddings,
|
||||||
|
_bottomPadding,
|
||||||
|
].reduce((double a, double b) => a + b);
|
||||||
|
|
||||||
|
final double descriptionAllowedHeight = params.layoutHeight - height;
|
||||||
|
|
||||||
|
final int maxLines =
|
||||||
|
max(1, (descriptionAllowedHeight / descriptionHeight).floor());
|
||||||
|
|
||||||
|
_computationCache[params] = maxLines;
|
||||||
|
|
||||||
|
return maxLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -75,19 +145,36 @@ class LinkView extends StatelessWidget {
|
|||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
final double layoutWidth = constraints.biggest.width;
|
final double layoutWidth = constraints.biggest.width;
|
||||||
final double layoutHeight = constraints.biggest.height;
|
final double layoutHeight = constraints.biggest.height;
|
||||||
|
final double bodyWidth = layoutWidth - layoutHeight - 8;
|
||||||
|
final String? fontFamily =
|
||||||
|
Theme.of(context).primaryTextTheme.bodyMedium?.fontFamily;
|
||||||
|
final double textScaleFactor = MediaQuery.of(context).textScaleFactor;
|
||||||
|
|
||||||
final TextStyle titleFontSize = titleTextStyle ??
|
final TextStyle titleStyle = titleTextStyle;
|
||||||
TextStyle(
|
final double titleHeight = (TextPainter(
|
||||||
fontSize: computeTitleFontSize(layoutWidth),
|
text: TextSpan(
|
||||||
color: Palette.black,
|
text: title,
|
||||||
fontWeight: FontWeight.bold,
|
style: titleStyle,
|
||||||
);
|
),
|
||||||
final TextStyle bodyFontSize = bodyTextStyle ??
|
maxLines: 2,
|
||||||
TextStyle(
|
textScaleFactor: textScaleFactor,
|
||||||
fontSize: computeTitleFontSize(layoutWidth) - 1,
|
textDirection: TextDirection.ltr,
|
||||||
color: Palette.grey,
|
)..layout(maxWidth: bodyWidth))
|
||||||
fontWeight: FontWeight.w400,
|
.size
|
||||||
);
|
.height;
|
||||||
|
|
||||||
|
final int descriptionMaxLines = getDescriptionMaxLines(
|
||||||
|
MaxLineComputationParams(
|
||||||
|
fontFamily ?? Font.roboto.name,
|
||||||
|
bodyWidth,
|
||||||
|
layoutHeight,
|
||||||
|
titleHeight,
|
||||||
|
textScaleFactor,
|
||||||
|
showUrl,
|
||||||
|
showMetadata,
|
||||||
|
),
|
||||||
|
titleStyle,
|
||||||
|
);
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () => onTap(url),
|
onTap: () => onTap(url),
|
||||||
@ -96,7 +183,7 @@ class LinkView extends StatelessWidget {
|
|||||||
if (showMultiMedia)
|
if (showMultiMedia)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
right: 5,
|
right: 8,
|
||||||
top: 5,
|
top: 5,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
),
|
),
|
||||||
@ -112,7 +199,7 @@ class LinkView extends StatelessWidget {
|
|||||||
imageUrl: imageUri!,
|
imageUrl: imageUri!,
|
||||||
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
fit: isIcon ? BoxFit.scaleDown : BoxFit.fitWidth,
|
||||||
memCacheHeight: layoutHeight.toInt() * 4,
|
memCacheHeight: layoutHeight.toInt() * 4,
|
||||||
errorWidget: (BuildContext context, _, dynamic __) {
|
errorWidget: (BuildContext context, _, __) {
|
||||||
return Image.asset(
|
return Image.asset(
|
||||||
Constants.hackerNewsLogoPath,
|
Constants.hackerNewsLogoPath,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
@ -122,24 +209,53 @@ class LinkView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: Dimens.pt5),
|
||||||
Expanded(
|
SizedBox(
|
||||||
flex: 4,
|
height: layoutHeight,
|
||||||
child: Padding(
|
width: layoutWidth - layoutHeight - 8,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: <Widget>[
|
||||||
children: <Widget>[
|
SizedBox(
|
||||||
_buildTitleContainer(
|
height:
|
||||||
titleFontSize,
|
Theme.of(context).textTheme.bodyMedium?.fontFamily ==
|
||||||
computeTitleLines(layoutHeight),
|
Font.robotoSlab.name
|
||||||
|
? Dimens.pt2
|
||||||
|
: Dimens.pt4,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: titleStyle,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 2,
|
||||||
|
),
|
||||||
|
if (showUrl)
|
||||||
|
Text(
|
||||||
|
'($readableUrl)',
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: _urlStyle,
|
||||||
|
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
_buildBodyContainer(
|
if (showMetadata)
|
||||||
bodyFontSize,
|
Text(
|
||||||
computeBodyLines(layoutHeight),
|
metadata,
|
||||||
)
|
textAlign: TextAlign.left,
|
||||||
],
|
style: _metadataStyle,
|
||||||
),
|
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
description,
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
style: _descriptionStyle,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: descriptionMaxLines,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: _bottomPadding,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -148,81 +264,4 @@ class LinkView extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTitleContainer(TextStyle titleTS, int maxLines) {
|
|
||||||
final bool showUrl = this.showUrl && url.isNotEmpty;
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(4, 2, 3, 0),
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: titleTS,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
maxLines: maxLines,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (showUrl)
|
|
||||||
Container(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: Text(
|
|
||||||
'($readableUrl)',
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: titleTS.copyWith(
|
|
||||||
color: Palette.grey,
|
|
||||||
fontSize:
|
|
||||||
titleTS.fontSize == null ? 12 : titleTS.fontSize! - 4,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
),
|
|
||||||
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBodyContainer(TextStyle bodyTS, int maxLines) {
|
|
||||||
return Expanded(
|
|
||||||
flex: 2,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(5, 2, 5, 0),
|
|
||||||
child: Column(
|
|
||||||
children: <Widget>[
|
|
||||||
if (showMetadata)
|
|
||||||
Container(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: Text(
|
|
||||||
metadata,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: bodyTS.copyWith(
|
|
||||||
fontSize:
|
|
||||||
bodyTS.fontSize == null ? 12 : bodyTS.fontSize! - 2,
|
|
||||||
),
|
|
||||||
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
|
||||||
maxLines: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
child: Text(
|
|
||||||
description,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
style: bodyTS,
|
|
||||||
overflow: bodyTextOverflow ?? TextOverflow.ellipsis,
|
|
||||||
maxLines: (bodyMaxLines ?? maxLines) -
|
|
||||||
(showMetadata ? 1 : 0) -
|
|
||||||
(showUrl && url.isNotEmpty ? 1 : 0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
class MaxLineComputationParams extends Equatable {
|
||||||
|
const MaxLineComputationParams(
|
||||||
|
this.fontFamily,
|
||||||
|
this.layoutWidth,
|
||||||
|
this.layoutHeight,
|
||||||
|
this.titleHeight,
|
||||||
|
this.textScaleFactor,
|
||||||
|
// ignore: avoid_positional_boolean_parameters
|
||||||
|
this.showUrl,
|
||||||
|
this.showMetadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
final String fontFamily;
|
||||||
|
final double layoutWidth;
|
||||||
|
final double layoutHeight;
|
||||||
|
final double titleHeight;
|
||||||
|
final double textScaleFactor;
|
||||||
|
final bool showUrl;
|
||||||
|
final bool showMetadata;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object>[
|
||||||
|
fontFamily,
|
||||||
|
layoutWidth,
|
||||||
|
layoutHeight,
|
||||||
|
titleHeight,
|
||||||
|
textScaleFactor,
|
||||||
|
showUrl,
|
||||||
|
showMetadata,
|
||||||
|
];
|
||||||
|
}
|
1
lib/screens/widgets/link_preview/models/models.dart
Normal file
1
lib/screens/widgets/link_preview/models/models.dart
Normal file
@ -0,0 +1 @@
|
|||||||
|
export 'max_line_computation_params.dart';
|
@ -17,9 +17,9 @@ class OfflineBanner extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<StoriesBloc, StoriesState>(
|
return BlocBuilder<StoriesBloc, StoriesState>(
|
||||||
buildWhen: (StoriesState previous, StoriesState current) =>
|
buildWhen: (StoriesState previous, StoriesState current) =>
|
||||||
previous.offlineReading != current.offlineReading,
|
previous.isOfflineReading != current.isOfflineReading,
|
||||||
builder: (BuildContext context, StoriesState state) {
|
builder: (BuildContext context, StoriesState state) {
|
||||||
if (state.offlineReading) {
|
if (state.isOfflineReading) {
|
||||||
return MaterialBanner(
|
return MaterialBanner(
|
||||||
content: Text(
|
content: Text(
|
||||||
'You are currently in offline mode. '
|
'You are currently in offline mode. '
|
||||||
|
@ -52,15 +52,18 @@ class _OnboardingViewState extends State<OnboardingView> {
|
|||||||
children: const <Widget>[
|
children: const <Widget>[
|
||||||
_PageViewChild(
|
_PageViewChild(
|
||||||
path: Constants.commentTileRightSlidePath,
|
path: Constants.commentTileRightSlidePath,
|
||||||
description: 'Swipe right to leave a comment or vote.',
|
description:
|
||||||
|
'''Swipe right to leave a comment, vote, and more.''',
|
||||||
),
|
),
|
||||||
_PageViewChild(
|
_PageViewChild(
|
||||||
path: Constants.commentTileLeftSlidePath,
|
path: Constants.commentTileLeftSlidePath,
|
||||||
description: 'Swipe left to view all the parent comments.',
|
description:
|
||||||
|
'''Swipe left to view all the ancestor comments.''',
|
||||||
),
|
),
|
||||||
_PageViewChild(
|
_PageViewChild(
|
||||||
path: Constants.commentTileTopTapPath,
|
path: Constants.commentTileTopTapPath,
|
||||||
description: 'Tap on the top of comment tile to collapse.',
|
description:
|
||||||
|
'''Tap on anywhere inside a comment tile to collapse.''',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -86,7 +86,7 @@ class _StoriesListViewState extends State<StoriesListView> {
|
|||||||
},
|
},
|
||||||
onTap: onStoryTapped,
|
onTap: onStoryTapped,
|
||||||
onPinned: context.read<PinCubit>().pinStory,
|
onPinned: context.read<PinCubit>().pinStory,
|
||||||
header: state.offlineReading ? null : header,
|
header: state.isOfflineReading ? null : header,
|
||||||
onMoreTapped: onMoreTapped,
|
onMoreTapped: onMoreTapped,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -33,101 +33,110 @@ class StoryTile extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (showWebPreview) {
|
if (showWebPreview) {
|
||||||
final double height = context.storyTileHeight;
|
final double height = context.storyTileHeight;
|
||||||
return TapDownWrapper(
|
return Semantics(
|
||||||
onTap: onTap,
|
label: story.screenReaderLabel,
|
||||||
child: Padding(
|
excludeSemantics: true,
|
||||||
padding: const EdgeInsets.symmetric(
|
child: TapDownWrapper(
|
||||||
horizontal: Dimens.pt12,
|
onTap: onTap,
|
||||||
),
|
child: Padding(
|
||||||
child: AbsorbPointer(
|
padding: const EdgeInsets.symmetric(
|
||||||
child: LinkPreview(
|
horizontal: Dimens.pt12,
|
||||||
story: story,
|
),
|
||||||
link: story.url,
|
child: AbsorbPointer(
|
||||||
offlineReading: context.read<StoriesBloc>().state.offlineReading,
|
child: LinkPreview(
|
||||||
placeholderWidget: _LinkPreviewPlaceholder(
|
story: story,
|
||||||
height: height,
|
link: story.url,
|
||||||
|
isOfflineReading:
|
||||||
|
context.read<StoriesBloc>().state.isOfflineReading,
|
||||||
|
placeholderWidget: _LinkPreviewPlaceholder(
|
||||||
|
height: height,
|
||||||
|
),
|
||||||
|
errorImage: Constants.hackerNewsLogoLink,
|
||||||
|
backgroundColor: Palette.transparent,
|
||||||
|
borderRadius: Dimens.zero,
|
||||||
|
removeElevation: true,
|
||||||
|
bodyMaxLines: context.storyTileMaxLines,
|
||||||
|
errorTitle: story.title,
|
||||||
|
titleStyle: TextStyle(
|
||||||
|
color: hasRead
|
||||||
|
? Palette.grey[500]
|
||||||
|
: Theme.of(context).textTheme.bodyLarge?.color,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
showMetadata: showMetadata,
|
||||||
|
showUrl: showUrl,
|
||||||
),
|
),
|
||||||
errorImage: Constants.hackerNewsLogoLink,
|
|
||||||
backgroundColor: Palette.transparent,
|
|
||||||
borderRadius: Dimens.zero,
|
|
||||||
removeElevation: true,
|
|
||||||
bodyMaxLines: context.storyTileMaxLines,
|
|
||||||
errorTitle: story.title,
|
|
||||||
titleStyle: TextStyle(
|
|
||||||
color: hasRead
|
|
||||||
? Palette.grey[500]
|
|
||||||
: Theme.of(context).textTheme.bodyLarge?.color,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
showMetadata: showMetadata,
|
|
||||||
showUrl: showUrl,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return InkWell(
|
return Semantics(
|
||||||
onTap: onTap,
|
label: story.screenReaderLabel,
|
||||||
child: Padding(
|
excludeSemantics: true,
|
||||||
padding: const EdgeInsets.only(left: Dimens.pt12),
|
child: InkWell(
|
||||||
child: Column(
|
onTap: onTap,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Padding(
|
||||||
children: <Widget>[
|
padding: const EdgeInsets.only(left: Dimens.pt12),
|
||||||
const SizedBox(
|
child: Column(
|
||||||
height: Dimens.pt8,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
children: <Widget>[
|
||||||
Row(
|
const SizedBox(
|
||||||
children: <Widget>[
|
height: Dimens.pt8,
|
||||||
Expanded(
|
),
|
||||||
child: Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
children: <TextSpan>[
|
|
||||||
TextSpan(
|
|
||||||
text: story.title,
|
|
||||||
style: TextStyle(
|
|
||||||
color: hasRead
|
|
||||||
? Palette.grey[500]
|
|
||||||
: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyLarge
|
|
||||||
?.color,
|
|
||||||
fontSize: simpleTileFontSize,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (showUrl && story.url.isNotEmpty)
|
|
||||||
TextSpan(
|
|
||||||
text: ' (${story.readableUrl})',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Palette.grey[500],
|
|
||||||
fontSize: simpleTileFontSize - 4,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (showMetadata)
|
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text.rich(
|
||||||
story.metadata,
|
TextSpan(
|
||||||
style: TextStyle(
|
children: <TextSpan>[
|
||||||
color: Palette.grey,
|
TextSpan(
|
||||||
fontSize: simpleTileFontSize - 2,
|
text: story.title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: hasRead
|
||||||
|
? Palette.grey[500]
|
||||||
|
: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodyLarge
|
||||||
|
?.color,
|
||||||
|
fontSize: simpleTileFontSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showUrl && story.url.isNotEmpty)
|
||||||
|
TextSpan(
|
||||||
|
text: ' (${story.readableUrl})',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Palette.grey[500],
|
||||||
|
fontSize: simpleTileFontSize - 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(
|
if (showMetadata)
|
||||||
height: Dimens.pt8,
|
Row(
|
||||||
),
|
children: <Widget>[
|
||||||
],
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
story.metadata,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Palette.grey,
|
||||||
|
fontSize: simpleTileFontSize - 2,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: Dimens.pt8,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -2,8 +2,9 @@ import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart'
|
|||||||
import 'package:linkify/linkify.dart';
|
import 'package:linkify/linkify.dart';
|
||||||
|
|
||||||
abstract class LinkifierUtil {
|
abstract class LinkifierUtil {
|
||||||
|
static const LinkifyOptions linkifyOptions = LinkifyOptions(humanize: false);
|
||||||
|
|
||||||
static List<LinkifyElement> linkify(String text) {
|
static List<LinkifyElement> linkify(String text) {
|
||||||
const LinkifyOptions options = LinkifyOptions();
|
|
||||||
const List<Linkifier> linkifiers = <Linkifier>[
|
const List<Linkifier> linkifiers = <Linkifier>[
|
||||||
UrlLinkifier(),
|
UrlLinkifier(),
|
||||||
EmailLinkifier(),
|
EmailLinkifier(),
|
||||||
@ -21,7 +22,7 @@ abstract class LinkifierUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (final Linkifier linkifier in linkifiers) {
|
for (final Linkifier linkifier in linkifiers) {
|
||||||
list = linkifier.parse(list, options);
|
list = linkifier.parse(list, linkifyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
|
10
pubspec.lock
10
pubspec.lock
@ -600,6 +600,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.0"
|
||||||
|
memoize:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: memoize
|
||||||
|
sha256: "51481d328c86cbdc59711369179bac88551ca0556569249be5317e66fc796cac"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1351,4 +1359,4 @@ packages:
|
|||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.19.0 <3.0.0"
|
dart: ">=2.19.0 <3.0.0"
|
||||||
flutter: ">=3.7.5"
|
flutter: ">=3.7.6"
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 1.2.4+97
|
version: 1.3.3+102
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
flutter: "3.7.5"
|
flutter: "3.7.6"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
adaptive_theme: ^3.0.0
|
adaptive_theme: ^3.0.0
|
||||||
@ -45,6 +45,7 @@ dependencies:
|
|||||||
intl: ^0.18.0
|
intl: ^0.18.0
|
||||||
linkify: ^4.1.0
|
linkify: ^4.1.0
|
||||||
logger: ^1.1.0
|
logger: ^1.1.0
|
||||||
|
memoize: ^3.0.0
|
||||||
package_info_plus: ^3.0.3
|
package_info_plus: ^3.0.3
|
||||||
path: ^1.8.2
|
path: ^1.8.2
|
||||||
path_provider: ^2.0.12
|
path_provider: ^2.0.12
|
||||||
|
Submodule submodules/flutter updated: c07f788888...12cb4eb7a0
Reference in New Issue
Block a user