Compare commits

...

3 Commits

Author SHA1 Message Date
c685f33f99 fix download progress bar. (#439) 2024-07-28 21:51:57 -07:00
518608893d auto scroll improvements. (#438) 2024-07-28 16:39:56 -07:00
856efa7c14 bump flutter version to 3.22.3. (#434) 2024-07-19 21:11:40 -07:00
47 changed files with 491 additions and 188 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list> </layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" /> <item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
<!-- You can insert your own image assets here --> </item>
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list> </layer-list>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when <!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame --> Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style> </style>
<!-- Theme applied to the Android Window as soon as the process has started. <!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your This theme determines the color of the Android Window while your

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when <!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame --> Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item> <item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style> </style>
<!-- Theme applied to the Android Window as soon as the process has started. <!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your This theme determines the color of the Android Window while your

View File

@ -0,0 +1,7 @@
{
"athingComtrSelector": "#hnmain > tbody > tr > td > table > tbody > .athing.comtr",
"commentTextSelector": "td > table > tbody > tr > td.default > div.comment > div.commtext",
"commentHeadSelector": "td > table > tbody > tr > td.default > div > span > a",
"commentAgeSelector": "td > table > tbody > tr > td.default > div > span > span.age",
"commentIndentSelector": "td > table > tbody > tr > td.ind"
}

View File

@ -16,6 +16,8 @@ PODS:
- OrderedSet (~> 5.0) - OrderedSet (~> 5.0)
- flutter_local_notifications (0.0.1): - flutter_local_notifications (0.0.1):
- Flutter - Flutter
- flutter_native_splash (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0): - flutter_secure_storage (6.0.0):
- Flutter - Flutter
- flutter_siri_suggestions (0.0.1): - flutter_siri_suggestions (0.0.1):
@ -62,6 +64,7 @@ DEPENDENCIES:
- flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`) - flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`) - flutter_siri_suggestions (from `.symlinks/plugins/flutter_siri_suggestions/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`)
@ -97,6 +100,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios" :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_local_notifications: flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios" :path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage: flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios" :path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_siri_suggestions: flutter_siri_suggestions:
@ -137,6 +142,7 @@ SPEC CHECKSUMS:
flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40 flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f flutter_siri_suggestions: 226fb7ef33d25d3fe0d4aa2a8bcf4b72730c466f
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"filename" : "background.png",
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"filename" : "darkbackground.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@ -1,23 +1,23 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal",
"filename" : "LaunchImage.png", "filename" : "LaunchImage.png",
"idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@2x.png", "filename" : "LaunchImage@2x.png",
"idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"idiom" : "universal",
"filename" : "LaunchImage@3x.png", "filename" : "LaunchImage@3x.png",
"idiom" : "universal",
"scale" : "3x" "scale" : "3x"
} }
], ],
"info" : { "info" : {
"version" : 1, "author" : "xcode",
"author" : "xcode" "version" : 1
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 69 B

View File

@ -16,13 +16,19 @@
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"> <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
</imageView> <imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
</subviews> </subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints> <constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/> <constraint firstItem="YRO-k0-Ey4" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="3T2-ad-Qdv"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/> <constraint firstItem="tWc-Dq-wcI" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="RPx-PI-7Xg"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="SdS-ul-q2q"/>
<constraint firstAttribute="trailing" secondItem="tWc-Dq-wcI" secondAttribute="trailing" id="Swv-Gf-Rwn"/>
<constraint firstAttribute="trailing" secondItem="YRO-k0-Ey4" secondAttribute="trailing" id="TQA-XW-tRk"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="bottom" secondItem="Ze5-6b-2t3" secondAttribute="bottom" id="duK-uY-Gun"/>
<constraint firstItem="tWc-Dq-wcI" firstAttribute="leading" secondItem="Ze5-6b-2t3" secondAttribute="leading" id="kV7-tw-vXt"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="top" secondItem="Ze5-6b-2t3" secondAttribute="top" id="xPn-NY-SIU"/>
</constraints> </constraints>
</view> </view>
</viewController> </viewController>
@ -33,5 +39,6 @@
</scenes> </scenes>
<resources> <resources>
<image name="LaunchImage" width="168" height="185"/> <image name="LaunchImage" width="168" height="185"/>
<image name="LaunchBackground" width="1" height="1"/>
</resources> </resources>
</document> </document>

View File

@ -1,84 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>workmanager.background.task</string> <string>workmanager.background.task</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Hacki</string> <string>Hacki</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>hacki</string> <string>hacki</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string> <string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
<key>CFBundleTypeRole</key> <key>CFBundleTypeRole</key>
<string>Editor</string> <string>Editor</string>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>ShareMedia</string> <string>ShareMedia</string>
</array> </array>
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>https</string> <string>https</string>
<string>http</string> <string>http</string>
<string>mailto</string> <string>mailto</string>
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>fetch</string> <string>fetch</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>com.apple.developer.associated-domains</key> <key>com.apple.developer.associated-domains</key>
<array> <array>
<string>applinks:example.com</string> <string>applinks:example.com</string>
</array> </array>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes</string> <string>This app needs camera access to scan QR codes</string>
<key>io.flutter.embedded_views_preview</key> <key>io.flutter.embedded_views_preview</key>
<true/> <true/>
</dict> <key>UIStatusBarHidden</key>
<false/>
</dict>
</plist> </plist>

View File

@ -69,6 +69,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
static const int _largePageSize = 20; static const int _largePageSize = 20;
static const int _tabletSmallPageSize = 15; static const int _tabletSmallPageSize = 15;
static const int _tabletLargePageSize = 25; static const int _tabletLargePageSize = 25;
static const String _logPrefix = '[StoriesBloc]';
Future<void> onInitialize( Future<void> onInitialize(
StoriesInitialize event, StoriesInitialize event,
@ -245,7 +246,9 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
final Story story = event.story; final Story story = event.story;
if (state.storiesByType[event.type]?.contains(story) ?? false) { if (state.storiesByType[event.type]?.contains(story) ?? false) {
_logger.d('story already exists.'); _logger.d(
'$_logPrefix story ${story.id} for ${event.type} already exists.',
);
return; return;
} }
final bool hasRead = await _preferenceRepository.hasRead(story.id); final bool hasRead = await _preferenceRepository.hasRead(story.id);
@ -349,20 +352,20 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
<StreamSubscription<Comment>>[]; <StreamSubscription<Comment>>[];
for (final int id in ids) { for (final int id in ids) {
if (state.downloadStatus == StoriesDownloadStatus.canceled) { if (state.downloadStatus == StoriesDownloadStatus.canceled) {
_logger.d('aborting downloading'); _logger.d('$_logPrefix aborting downloading');
for (final StreamSubscription<Comment> stream in downloadStreams) { for (final StreamSubscription<Comment> stream in downloadStreams) {
await stream.cancel(); await stream.cancel();
} }
_logger.d('deleting downloaded contents'); _logger.d('$_logPrefix deleting downloaded contents');
await _offlineRepository.deleteAllStoryIds(); await _offlineRepository.deleteAllStoryIds();
await _offlineRepository.deleteAllStories(); await _offlineRepository.deleteAllStories();
await _offlineRepository.deleteAllComments(); await _offlineRepository.deleteAllComments();
break; break;
} }
_logger.d('fetching story $id'); _logger.d('$_logPrefix fetching story $id');
final Story? story = await _hackerNewsRepository.fetchStory(id: id); final Story? story = await _hackerNewsRepository.fetchStory(id: id);
if (story == null) { if (story == null) {
@ -382,7 +385,7 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
await _offlineRepository.cacheStory(story: story); await _offlineRepository.cacheStory(story: story);
if (story.url.isNotEmpty && includingWebPage) { if (story.url.isNotEmpty && includingWebPage) {
_logger.i('downloading ${story.url}'); _logger.i('$_logPrefix downloading ${story.url}');
await _offlineRepository.cacheUrl(url: story.url); await _offlineRepository.cacheUrl(url: story.url);
} }
@ -399,19 +402,19 @@ class StoriesBloc extends Bloc<StoriesEvent, StoriesState> {
.listen( .listen(
(Comment comment) { (Comment comment) {
if (state.downloadStatus == StoriesDownloadStatus.canceled) { if (state.downloadStatus == StoriesDownloadStatus.canceled) {
_logger.d('aborting downloading from comments stream'); _logger.d('$_logPrefix aborting downloading from comments stream');
downloadStream?.cancel(); downloadStream?.cancel();
return; return;
} }
_logger.d('fetched comment ${comment.id}'); _logger.d('$_logPrefix fetched comment ${comment.id}');
unawaited( unawaited(
_offlineRepository.cacheComment(comment: comment), _offlineRepository.cacheComment(comment: comment),
); );
}, },
)..onDone(() { )..onDone(() {
_logger.d( _logger.d(
'''finished downloading story ${story.id} with ${story.descendants} comments''', '''$_logPrefix finished downloading story ${story.id} with ${story.descendants} comments''',
); );
add(StoryDownloaded(skipped: false)); add(StoryDownloaded(skipped: false));
}); });

View File

@ -84,6 +84,7 @@ class CommentsCubit extends Cubit<CommentsState> {
<int, StreamSubscription<Comment>>{}; <int, StreamSubscription<Comment>>{};
static const int _webFetchingCmtCountLowerLimit = 5; static const int _webFetchingCmtCountLowerLimit = 5;
static const String _logPrefix = '[CommentsCubit]';
Future<bool> get _shouldFetchFromWeb async { Future<bool> get _shouldFetchFromWeb async {
final bool isOnWifi = await _isOnWifi; final bool isOnWifi = await _isOnWifi;
@ -182,13 +183,14 @@ class CommentsCubit extends Cubit<CommentsState> {
case CommentsOrder.natural: case CommentsOrder.natural:
final bool shouldFetchFromWeb = await _shouldFetchFromWeb; final bool shouldFetchFromWeb = await _shouldFetchFromWeb;
if (fetchFromWeb && shouldFetchFromWeb) { if (fetchFromWeb && shouldFetchFromWeb) {
_logger.d('fetching from web.'); _logger
.d('$_logPrefix fetching comments of ${item.id} from web.');
commentStream = _hackerNewsWebRepository commentStream = _hackerNewsWebRepository
.fetchCommentsStream(state.item) .fetchCommentsStream(state.item)
.handleError((dynamic e) { .handleError((dynamic e) {
_streamSubscription?.cancel(); _streamSubscription?.cancel();
_logger.e(e); _logger.e('$_logPrefix $e');
switch (e.runtimeType) { switch (e.runtimeType) {
case RateLimitedException: case RateLimitedException:
@ -205,7 +207,8 @@ class CommentsCubit extends Cubit<CommentsState> {
} }
}); });
} else { } else {
_logger.d('fetching from API.'); _logger
.d('$_logPrefix fetching comments of ${item.id} from API.');
commentStream = commentStream =
_hackerNewsRepository.fetchAllCommentsRecursivelyStream( _hackerNewsRepository.fetchAllCommentsRecursivelyStream(
ids: kids, ids: kids,
@ -280,11 +283,13 @@ class CommentsCubit extends Cubit<CommentsState> {
case CommentsOrder.natural: case CommentsOrder.natural:
final bool shouldFetchFromWeb = await _shouldFetchFromWeb; final bool shouldFetchFromWeb = await _shouldFetchFromWeb;
if (fetchFromWeb && shouldFetchFromWeb) { if (fetchFromWeb && shouldFetchFromWeb) {
_logger.d('fetching from web.'); _logger.d(
'$_logPrefix fetching comments of ${item.id} from web.',
);
commentStream = _hackerNewsWebRepository commentStream = _hackerNewsWebRepository
.fetchCommentsStream(state.item) .fetchCommentsStream(state.item)
.handleError((dynamic e) { .handleError((dynamic e) {
_logger.e(e); _logger.e('$_logPrefix $e');
switch (e.runtimeType) { switch (e.runtimeType) {
case RateLimitedException: case RateLimitedException:
@ -301,7 +306,8 @@ class CommentsCubit extends Cubit<CommentsState> {
} }
}); });
} else { } else {
_logger.d('fetching from API.'); _logger
.d('$_logPrefix fetching comments of ${item.id} from API.');
commentStream = _hackerNewsRepository commentStream = _hackerNewsRepository
.fetchAllCommentsRecursivelyStream(ids: kids); .fetchAllCommentsRecursivelyStream(ids: kids);
} }

View File

@ -67,6 +67,7 @@ class NotificationCubit extends Cubit<NotificationState> {
static const Duration _refreshInterval = Duration(minutes: 5); static const Duration _refreshInterval = Duration(minutes: 5);
static const int _subscriptionUpperLimit = 15; static const int _subscriptionUpperLimit = 15;
static const int _pageSize = 20; static const int _pageSize = 20;
static const String _logPrefix = '[NotificationCubit]';
Future<void> init() async { Future<void> init() async {
emit(NotificationState.init()); emit(NotificationState.init());
@ -78,7 +79,7 @@ class NotificationCubit extends Cubit<NotificationState> {
}); });
await _preferenceRepository.unreadCommentsIds.then((List<int> unreadIds) { await _preferenceRepository.unreadCommentsIds.then((List<int> unreadIds) {
_logger.i('NotificationCubit: ${unreadIds.length} unread items.'); _logger.i('$_logPrefix ${unreadIds.length} unread items.');
emit(state.copyWith(unreadCommentsIds: unreadIds)); emit(state.copyWith(unreadCommentsIds: unreadIds));
}); });
@ -104,31 +105,17 @@ class NotificationCubit extends Cubit<NotificationState> {
} }
void markAsRead(int id) { void markAsRead(int id) {
Future.doWhile(() { if (state.unreadCommentsIds.contains(id)) {
if (state.status != Status.inProgress) { final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds]
if (state.unreadCommentsIds.contains(id)) { ..remove(id);
final List<int> updatedUnreadIds = <int>[...state.unreadCommentsIds] _preferenceRepository.updateUnreadCommentsIds(updatedUnreadIds);
..remove(id); emit(state.copyWith(unreadCommentsIds: updatedUnreadIds));
_preferenceRepository.updateUnreadCommentsIds(updatedUnreadIds); }
emit(state.copyWith(unreadCommentsIds: updatedUnreadIds));
}
return false;
}
return true;
});
} }
void markAllAsRead() { void markAllAsRead() {
Future.doWhile(() { emit(state.copyWith(unreadCommentsIds: <int>[]));
if (state.status != Status.inProgress) { _preferenceRepository.updateUnreadCommentsIds(<int>[]);
emit(state.copyWith(unreadCommentsIds: <int>[]));
_preferenceRepository.updateUnreadCommentsIds(<int>[]);
return false;
}
return true;
});
} }
Future<void> refresh() async { Future<void> refresh() async {

View File

@ -23,6 +23,7 @@ class PreferenceCubit extends Cubit<PreferenceState> {
final PreferenceRepository _preferenceRepository; final PreferenceRepository _preferenceRepository;
final Logger _logger; final Logger _logger;
static const String _logPrefix = '[PreferenceCubit]';
void init() { void init() {
for (final BooleanPreference p for (final BooleanPreference p
@ -73,7 +74,7 @@ class PreferenceCubit extends Cubit<PreferenceState> {
} }
void update<T>(Preference<T> preference) { void update<T>(Preference<T> preference) {
_logger.i('updating $preference to ${preference.val}'); _logger.i('$_logPrefix updating $preference to ${preference.val}');
emit(state.copyWithPreference(preference)); emit(state.copyWithPreference(preference));

View File

@ -3,25 +3,34 @@ import 'package:flutter/cupertino.dart';
import 'package:hacki/config/locator.dart'; import 'package:hacki/config/locator.dart';
import 'package:hacki/repositories/remote_config_repository.dart'; import 'package:hacki/repositories/remote_config_repository.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:logger/logger.dart';
part 'remote_config_state.dart'; part 'remote_config_state.dart';
class RemoteConfigCubit extends HydratedCubit<RemoteConfigState> { class RemoteConfigCubit extends HydratedCubit<RemoteConfigState> {
RemoteConfigCubit({RemoteConfigRepository? remoteConfigRepository}) RemoteConfigCubit({
: _remoteConfigRepository = RemoteConfigRepository? remoteConfigRepository,
Logger? logger,
}) : _remoteConfigRepository =
remoteConfigRepository ?? locator.get<RemoteConfigRepository>(), remoteConfigRepository ?? locator.get<RemoteConfigRepository>(),
_logger = logger ?? locator.get<Logger>(),
super(RemoteConfigState.init()) { super(RemoteConfigState.init()) {
init(); init();
} }
final RemoteConfigRepository _remoteConfigRepository; final RemoteConfigRepository _remoteConfigRepository;
final Logger _logger;
static const String _logPrefix = '';
void init() { void init() {
_remoteConfigRepository _remoteConfigRepository
.fetchRemoteConfig() .fetchRemoteConfig()
.then((Map<String, dynamic> data) { .then((Map<String, dynamic> data) {
if (data.isNotEmpty) { if (data.isNotEmpty) {
_logger.i('$_logPrefix remote config fetched: $data');
emit(state.copyWith(data: data)); emit(state.copyWith(data: data));
} else {
_logger.i('$_logPrefix empty remote config.');
} }
}); });
} }

View File

@ -17,9 +17,10 @@ class SplitViewCubit extends Cubit<SplitViewState> {
final Logger _logger; final Logger _logger;
final CommentCache _commentCache; final CommentCache _commentCache;
static const String _logPrefix = '[SplitViewCubit]';
void updateItemScreenArgs(ItemScreenArgs args) { void updateItemScreenArgs(ItemScreenArgs args) {
_logger.i('resetting comments in CommentCache'); _logger.i('$_logPrefix resetting comments in CommentCache');
_commentCache.resetComments(); _commentCache.resetComments();
emit(state.copyWith(itemScreenArgs: args)); emit(state.copyWith(itemScreenArgs: args));
} }

View File

@ -19,17 +19,20 @@ class TabCubit extends Cubit<TabState> {
final PreferenceCubit _preferenceCubit; final PreferenceCubit _preferenceCubit;
final Logger _logger; final Logger _logger;
static const String _logPrefix = '[TabCubit]';
void init() { void init() {
final List<StoryType> tabs = _preferenceCubit.state.tabs; final List<StoryType> tabs = _preferenceCubit.state.tabs;
_logger.i('updating tabs to $tabs'); _logger.i('$_logPrefix updating tabs to $tabs');
emit(state.copyWith(tabs: tabs)); emit(state.copyWith(tabs: tabs));
} }
void update(int startIndex, int endIndex) { void update(int startIndex, int endIndex) {
_logger.d('updating ${state.tabs} by moving $startIndex to $endIndex'); _logger.d(
'$_logPrefix updating ${state.tabs} by moving $startIndex to $endIndex',
);
final StoryType tab = state.tabs.elementAt(startIndex); final StoryType tab = state.tabs.elementAt(startIndex);
final List<StoryType> updatedTabs = List<StoryType>.from(state.tabs) final List<StoryType> updatedTabs = List<StoryType>.from(state.tabs)
..insert(endIndex, tab) ..insert(endIndex, tab)

View File

@ -164,7 +164,7 @@ final class AutoScrollModePreference extends BooleanPreference {
const AutoScrollModePreference({bool? val}) const AutoScrollModePreference({bool? val})
: super(val: val ?? _autoScrollModeDefaultValue); : super(val: val ?? _autoScrollModeDefaultValue);
static const bool _autoScrollModeDefaultValue = false; static const bool _autoScrollModeDefaultValue = true;
@override @override
AutoScrollModePreference copyWith({required bool? val}) { AutoScrollModePreference copyWith({required bool? val}) {

View File

@ -1,15 +1,20 @@
import 'dart:convert'; import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
class RemoteConfigRepository { class RemoteConfigRepository {
RemoteConfigRepository({Dio? dio}) : _dio = dio ?? Dio(); RemoteConfigRepository({Dio? dio}) : _dio = dio ?? Dio();
final Dio _dio; final Dio _dio;
static const String _path =
'https://raw.githubusercontent.com/Livinglist/Hacki/master/assets/';
Future<Map<String, dynamic>> fetchRemoteConfig() async { Future<Map<String, dynamic>> fetchRemoteConfig() async {
const String fileName =
kReleaseMode ? 'remote-config.json' : 'remote-config-dev.json';
final Response<dynamic> response = await _dio.get( final Response<dynamic> response = await _dio.get(
'https://raw.githubusercontent.com/Livinglist/Hacki/master/assets/remote-config.json', '$_path$fileName',
); );
final String data = response.data as String? ?? ''; final String data = response.data as String? ?? '';
final Map<String, dynamic> json = jsonDecode(data) as Map<String, dynamic>; final Map<String, dynamic> json = jsonDecode(data) as Map<String, dynamic>;

View File

@ -1,8 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:hacki/config/locator.dart';
import 'package:hacki/models/models.dart'; import 'package:hacki/models/models.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.dart';
import 'package:logger/logger.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:sembast/sembast.dart'; import 'package:sembast/sembast.dart';
@ -17,7 +19,8 @@ class SembastRepository {
SembastRepository({ SembastRepository({
Database? database, Database? database,
Database? cache, Database? cache,
}) { Logger? logger,
}) : _logger = logger ?? locator.get<Logger>() {
if (database == null) { if (database == null) {
initializeDatabase(); initializeDatabase();
} else { } else {
@ -31,6 +34,9 @@ class SembastRepository {
} }
} }
final Logger _logger;
static const String _logPrefix = '[SembastRepository]';
Database? _database; Database? _database;
Database? _cache; Database? _cache;
List<int>? _idsOfCommentsRepliedToMe; List<int>? _idsOfCommentsRepliedToMe;
@ -44,6 +50,9 @@ class SembastRepository {
final Directory dir = await getApplicationCacheDirectory(); final Directory dir = await getApplicationCacheDirectory();
await dir.create(recursive: true); await dir.create(recursive: true);
final String dbPath = join(dir.path, 'hacki.db'); final String dbPath = join(dir.path, 'hacki.db');
final File file = File(dbPath);
final FileStat stat = file.statSync();
_logger.i('$_logPrefix hacki.db file size: ${stat.size / 1000000}MB');
final DatabaseFactory dbFactory = databaseFactoryIo; final DatabaseFactory dbFactory = databaseFactoryIo;
final Database db = await dbFactory.openDatabase(dbPath); final Database db = await dbFactory.openDatabase(dbPath);
_database = db; _database = db;
@ -54,6 +63,9 @@ class SembastRepository {
final Directory tempDir = await getTemporaryDirectory(); final Directory tempDir = await getTemporaryDirectory();
await tempDir.create(recursive: true); await tempDir.create(recursive: true);
final String dbPath = join(tempDir.path, 'hacki_cache.db'); final String dbPath = join(tempDir.path, 'hacki_cache.db');
final File file = File(dbPath);
final FileStat stat = file.statSync();
_logger.i('$_logPrefix hacki_cache.db file size: ${stat.size / 1000000}MB');
final DatabaseFactory dbFactory = databaseFactoryIo; final DatabaseFactory dbFactory = databaseFactoryIo;
final Database db = await dbFactory.openDatabase(dbPath); final Database db = await dbFactory.openDatabase(dbPath);
_cache = db; _cache = db;

View File

@ -43,13 +43,14 @@ class _HomeScreenState extends State<HomeScreen>
late final StreamSubscription<String?> siriSuggestionStreamSubscription; late final StreamSubscription<String?> siriSuggestionStreamSubscription;
static final int tabLength = StoryType.values.length + 1; static final int tabLength = StoryType.values.length + 1;
static const String logPrefix = '[HomeScreen]';
@override @override
void didPopNext() { void didPopNext() {
super.didPopNext(); super.didPopNext();
if (context.read<StoriesBloc>().deviceScreenType == if (context.read<StoriesBloc>().deviceScreenType ==
DeviceScreenType.mobile) { DeviceScreenType.mobile) {
locator.get<Logger>().i('resetting comments in CommentCache'); locator.get<Logger>().i('$logPrefix resetting comments in CommentCache');
Future<void>.delayed( Future<void>.delayed(
AppDurations.ms500, AppDurations.ms500,
locator.get<CommentCache>().resetComments, locator.get<CommentCache>().resetComments,

View File

@ -24,6 +24,14 @@ class MobileHomeScreen extends StatelessWidget {
bottom: Dimens.pt36, bottom: Dimens.pt36,
height: Dimens.pt40, height: Dimens.pt40,
child: CountdownReminder(), child: CountdownReminder(),
)
else
const Positioned(
left: Dimens.pt24,
right: Dimens.pt24,
bottom: Dimens.pt36,
height: Dimens.pt40,
child: DownloadProgressReminder(),
), ),
], ],
); );

View File

@ -41,13 +41,22 @@ class TabletHomeScreen extends StatelessWidget {
curve: Curves.elasticOut, curve: Curves.elasticOut,
child: homeScreen, child: homeScreen,
), ),
Positioned( if (!context.read<ReminderCubit>().state.hasShown)
left: Dimens.pt24, Positioned(
bottom: Dimens.pt36, left: Dimens.pt24,
height: Dimens.pt40, bottom: Dimens.pt36,
width: homeScreenWidth - Dimens.pt24, height: Dimens.pt40,
child: const CountdownReminder(), width: homeScreenWidth - Dimens.pt24,
), child: const CountdownReminder(),
)
else
Positioned(
left: Dimens.pt24,
bottom: Dimens.pt36,
height: Dimens.pt40,
width: homeScreenWidth - Dimens.pt24,
child: const DownloadProgressReminder(),
),
AnimatedPositioned( AnimatedPositioned(
right: Dimens.zero, right: Dimens.zero,
top: Dimens.zero, top: Dimens.zero,

View File

@ -32,7 +32,9 @@ class InboxView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: <Widget>[ children: <Widget>[
if (unreadCommentsIds.isNotEmpty) if (context.read<NotificationCubit>().state.status !=
Status.inProgress &&
unreadCommentsIds.isNotEmpty)
TextButton( TextButton(
onPressed: onMarkAllAsReadTapped, onPressed: onMarkAllAsReadTapped,
child: const Text('Mark all as read'), child: const Text('Mark all as read'),

View File

@ -341,6 +341,13 @@ class _SettingsState extends State<Settings> with ItemActionMixin {
), ),
onTap: showClearCacheDialog, onTap: showClearCacheDialog,
), ),
if (preferenceState.isDevModeEnabled)
ListTile(
title: const Text(
'Logs',
),
onTap: () {},
),
ListTile( ListTile(
title: const Text('About'), title: const Text('About'),
subtitle: const Text('nothing interesting here.'), subtitle: const Text('nothing interesting here.'),

View File

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
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_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
@ -10,6 +11,7 @@ import 'package:hacki/screens/widgets/widgets.dart';
import 'package:hacki/services/services.dart'; import 'package:hacki/services/services.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:scrollable_positioned_list/scrollable_positioned_list.dart';
class CommentTile extends StatelessWidget { class CommentTile extends StatelessWidget {
const CommentTile({ const CommentTile({
@ -417,18 +419,28 @@ class CommentTile extends StatelessWidget {
preferenceCubit.state.isAutoScrollEnabled) { preferenceCubit.state.isAutoScrollEnabled) {
final CommentsCubit commentsCubit = context.read<CommentsCubit>(); final CommentsCubit commentsCubit = context.read<CommentsCubit>();
final List<Comment> comments = commentsCubit.state.comments; final List<Comment> comments = commentsCubit.state.comments;
final int indexOfNextComment = comments.indexOf(comment) + 1; final int indexOfComment = comments.indexOf(comment);
if (indexOfNextComment < comments.length) { if (indexOfComment < comments.length) {
Future<void>.delayed( final double? leadingEdge =
AppDurations.ms300, commentsCubit.itemPositionsListener.itemPositions.value
() { .singleWhereOrNull(
commentsCubit.itemScrollController.scrollTo( (ItemPosition e) => e.index - 1 == indexOfComment,
index: indexOfNextComment, )
alignment: 0.1, ?.itemLeadingEdge;
duration: AppDurations.ms300, final bool willBeOutsideOfScreen =
); leadingEdge != null && leadingEdge < 0.1;
}, if (willBeOutsideOfScreen) {
); Future<void>.delayed(
AppDurations.ms200,
() {
commentsCubit.itemScrollController.scrollTo(
index: indexOfComment + 1,
alignment: 0.15,
duration: AppDurations.ms300,
);
},
);
}
} }
} }
} }

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_fadein/flutter_fadein.dart';
import 'package:hacki/blocs/blocs.dart';
import 'package:hacki/extensions/extensions.dart';
import 'package:hacki/styles/styles.dart';
class DownloadProgressReminder extends StatefulWidget {
const DownloadProgressReminder({super.key});
@override
State<DownloadProgressReminder> createState() =>
_DownloadProgressReminderState();
}
class _DownloadProgressReminderState extends State<DownloadProgressReminder>
with SingleTickerProviderStateMixin, ItemActionMixin {
@override
Widget build(BuildContext context) {
return BlocSelector<StoriesBloc, StoriesState,
(int, int, StoriesDownloadStatus)>(
selector: (StoriesState state) {
return (
state.storiesDownloaded,
state.storiesToBeDownloaded,
state.downloadStatus
);
},
builder: (BuildContext context, (int, int, StoriesDownloadStatus) state) {
final int storiesDownloaded = state.$1;
final int storiesToBeDownloaded = state.$2;
final StoriesDownloadStatus status = state.$3;
final double progress = storiesToBeDownloaded == 0
? 0
: storiesDownloaded / storiesToBeDownloaded;
final bool isVisible = status == StoriesDownloadStatus.downloading;
return Visibility(
visible: isVisible,
child: FadeIn(
child: Material(
color: Theme.of(context).colorScheme.primary,
clipBehavior: Clip.hardEdge,
borderRadius: const BorderRadius.all(
Radius.circular(
Dimens.pt4,
),
),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(
left: Dimens.pt12,
top: Dimens.pt10,
right: Dimens.pt10,
),
child: Row(
children: <Widget>[
Text(
'Downloading all stories ($storiesDownloaded/$storiesToBeDownloaded)',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontSize: TextDimens.pt12,
),
),
const Spacer(),
],
),
),
const Spacer(),
LinearProgressIndicator(
value: progress,
color:
Theme.of(context).colorScheme.primary.withOpacity(0.5),
),
],
),
),
),
);
},
);
}
}

View File

@ -10,6 +10,7 @@ export 'custom_dropdown_menu.dart';
export 'custom_linkify/custom_linkify.dart'; export 'custom_linkify/custom_linkify.dart';
export 'custom_tab_bar.dart'; export 'custom_tab_bar.dart';
export 'device_gesture_wrapper.dart'; export 'device_gesture_wrapper.dart';
export 'download_progress_reminder.dart';
export 'item_text.dart'; export 'item_text.dart';
export 'items_list_view.dart'; export 'items_list_view.dart';
export 'link_preview/link_preview.dart'; export 'link_preview/link_preview.dart';

View File

@ -38,15 +38,15 @@ abstract class Fetcher {
final Logger logger = Logger(); final Logger logger = Logger();
final PreferenceRepository preferenceRepository = final PreferenceRepository preferenceRepository =
PreferenceRepository(logger: logger); PreferenceRepository(logger: logger);
final AuthRepository authRepository = AuthRepository( final AuthRepository authRepository = AuthRepository(
preferenceRepository: preferenceRepository, preferenceRepository: preferenceRepository,
logger: logger, logger: logger,
); );
final HackerNewsRepository hackerNewsRepository = HackerNewsRepository();
final SembastRepository sembastRepository = SembastRepository(); final SembastRepository sembastRepository = SembastRepository();
final HackerNewsRepository hackerNewsRepository = HackerNewsRepository(
sembastRepository: sembastRepository,
logger: logger,
);
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin(); FlutterLocalNotificationsPlugin();

View File

@ -85,6 +85,7 @@ class WebAnalyzer {
RegExp('(title|icon|description|image)', caseSensitive: false); RegExp('(title|icon|description|image)', caseSensitive: false);
static final RegExp _lineReg = RegExp(r'[\n\r]|&nbsp;|&gt;'); static final RegExp _lineReg = RegExp(r'[\n\r]|&nbsp;|&gt;');
static final RegExp _spaceReg = RegExp(r'\s+'); static final RegExp _spaceReg = RegExp(r'\s+');
static const String _logPrefix = '[WebAnalyzer]';
static bool isEmpty(String? str) { static bool isEmpty(String? str) {
return !isNotEmpty(str); return !isNotEmpty(str);
@ -120,7 +121,7 @@ class WebAnalyzer {
if (info != null) { if (info != null) {
locator.get<Logger>().d(''' locator.get<Logger>().d('''
fetched mem cached metadata using key $key for $story: $_logPrefix fetched mem cached metadata using key $key for $story:
${info.toJson()} ${info.toJson()}
'''); ''');
return info; return info;
@ -168,7 +169,7 @@ ${info.toJson()}
/// [5] If there is file cache, move it to mem cache for later retrieval. /// [5] If there is file cache, move it to mem cache for later retrieval.
if (info != null) { if (info != null) {
locator.get<Logger>().d(''' locator.get<Logger>().d('''
fetched file cached metadata using key $key for $story: $_logPrefix fetched file cached metadata using key $key for $story:
${info.toJson()} ${info.toJson()}
'''); ''');
cacheMap[key] = info; cacheMap[key] = info;
@ -189,7 +190,7 @@ ${info.toJson()}
if (info is WebInfo) { if (info is WebInfo) {
locator locator
.get<Logger>() .get<Logger>()
.d('caching metadata using key $key for $story.'); .d('$_logPrefix caching metadata using key $key for $story.');
unawaited( unawaited(
locator.get<SembastRepository>().cacheMetadata( locator.get<SembastRepository>().cacheMetadata(
key: key, key: key,

View File

@ -8,10 +8,12 @@ import 'package:path_provider/path_provider.dart';
abstract class LogUtil { abstract class LogUtil {
static LogPrinter get logPrinter => kReleaseMode static LogPrinter get logPrinter => kReleaseMode
? SimplePrinter(colors: false) ? SimplePrinter(
: PrettyPrinter(
methodCount: 0,
colors: false, colors: false,
printTime: true,
)
: PrettyPrinter(
printTime: true,
); );
static LogOutput logOutput(File outputFile) => MultiOutput( static LogOutput logOutput(File outputFile) => MultiOutput(

View File

@ -33,6 +33,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.11" version: "2.0.11"
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
archive:
dependency: transitive
description:
name: archive
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
url: "https://pub.dev"
source: hosted
version: "3.6.1"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -436,6 +452,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.0"
flutter_native_splash:
dependency: "direct main"
description:
name: flutter_native_splash
sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840
url: "https://pub.dev"
source: hosted
version: "2.4.1"
flutter_secure_storage: flutter_secure_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -612,6 +636,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.1.5" version: "9.1.5"
image:
dependency: transitive
description:
name: image
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
in_app_review: in_app_review:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1269,6 +1301,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.2"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
universal_platform: universal_platform:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1543,4 +1583,4 @@ packages:
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.4.0 <4.0.0" dart: ">=3.4.0 <4.0.0"
flutter: ">=3.22.2" flutter: ">=3.22.3"

View File

@ -1,11 +1,11 @@
name: hacki name: hacki
description: A Hacker News reader. description: A Hacker News reader.
version: 2.8.1+146 version: 2.8.2+147
publish_to: none publish_to: none
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
flutter: "3.22.2" flutter: "3.22.3"
dependencies: dependencies:
adaptive_theme: ^3.2.0 adaptive_theme: ^3.2.0
@ -35,6 +35,7 @@ dependencies:
flutter_inappwebview: ^6.0.0 flutter_inappwebview: ^6.0.0
flutter_local_notifications: ^17.1.2 flutter_local_notifications: ^17.1.2
flutter_material_color_picker: ^1.2.0 flutter_material_color_picker: ^1.2.0
flutter_native_splash: ^2.4.1
flutter_secure_storage: ^9.2.2 flutter_secure_storage: ^9.2.2
flutter_siri_suggestions: flutter_siri_suggestions:
git: git:
@ -139,4 +140,23 @@ flutter:
- asset: assets/fonts/atkinson_hyperlegible/AtkinsonHyperlegible-Bold.ttf - asset: assets/fonts/atkinson_hyperlegible/AtkinsonHyperlegible-Bold.ttf
weight: 700 weight: 700
flutter_native_splash:
# This package generates native code to customize Flutter's default white native splash screen
# with background color and splash image.
# Customize the parameters below, and run the following command in the terminal:
# dart run flutter_native_splash:create
# To restore Flutter's default white splash screen, run the following command in the terminal:
# dart run flutter_native_splash:remove
# IMPORTANT NOTE: These parameter do not affect the configuration of Android 12 and later, which
# handle splash screens differently that prior versions of Android. Android 12 and later must be
# configured specifically in the android_12 section below.
# color or background_image is the only required parameter. Use color to set the background
# of your splash screen to a solid color. Use background_image to set the background of your
# splash screen to a png image. This is useful for gradients. The image will be stretch to the
# size of the app. Only one parameter can be used, color and background_image cannot both be set.
color: "#ffffff"
color_dark: "#000000"