mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
6a8022e177 | |||
4626b6f45b | |||
674c67a317 |
@ -1,7 +1,7 @@
|
||||
|
||||
# <img width="64" src="https://user-images.githubusercontent.com/7277662/167775086-0b234f28-dee4-44f6-aae4-14a28ed4bbb6.png"> Hacki for Hacker News
|
||||
|
||||
A simple noiseless Hacker News reader made with Flutter that is just enough.
|
||||
A simple noiseless [Hacker News](https://news.ycombinator.com/) reader made with Flutter that is just enough.
|
||||
|
||||
[](https://apps.apple.com/us/app/hacki/id1602043763)
|
||||
[](https://play.google.com/store/apps/details?id=com.jiaqifeng.hacki&hl=en_US&gl=US)
|
||||
|
4
fastlane/metadata/android/en-US/changelogs/40.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/40.txt
Normal file
@ -0,0 +1,4 @@
|
||||
- You can now participate in polls.
|
||||
- Pick up where you left off.
|
||||
- Swipe left on comment tile to view its parents without scrolling all the way up.
|
||||
- Huge performance boost.
|
@ -367,7 +367,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -376,7 +376,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.5;
|
||||
MARKETING_VERSION = 0.2.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -503,7 +503,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -512,7 +512,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.5;
|
||||
MARKETING_VERSION = 0.2.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -533,7 +533,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 11;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = QMWX3X2NF7;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -542,7 +542,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.2.5;
|
||||
MARKETING_VERSION = 0.2.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jiaqi.hacki;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -4,17 +4,20 @@ import 'dart:io';
|
||||
import 'package:adaptive_theme/adaptive_theme.dart';
|
||||
import 'package:feature_discovery/feature_discovery.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:hacki/blocs/blocs.dart';
|
||||
import 'package:hacki/config/custom_router.dart';
|
||||
import 'package:hacki/config/locator.dart';
|
||||
import 'package:hacki/cubits/cubits.dart';
|
||||
import 'package:hacki/repositories/repositories.dart' show PreferenceRepository;
|
||||
import 'package:hacki/screens/screens.dart';
|
||||
import 'package:hacki/services/fetcher.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:rxdart/rxdart.dart' show BehaviorSubject;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
|
||||
// For receiving payload event from local notifications.
|
||||
@ -63,6 +66,9 @@ Future<void> main() async {
|
||||
await setUpLocator();
|
||||
|
||||
final AdaptiveThemeMode? savedThemeMode = await AdaptiveTheme.getThemeMode();
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final bool trueDarkMode =
|
||||
prefs.getBool(PreferenceRepository.trueDarkModeKey) ?? false;
|
||||
|
||||
// Uncomment code below for running with logging.
|
||||
// BlocOverrides.runZoned(
|
||||
@ -79,6 +85,7 @@ Future<void> main() async {
|
||||
runApp(
|
||||
HackiApp(
|
||||
savedThemeMode: savedThemeMode,
|
||||
trueDarkMode: trueDarkMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -87,9 +94,11 @@ class HackiApp extends StatelessWidget {
|
||||
const HackiApp({
|
||||
super.key,
|
||||
this.savedThemeMode,
|
||||
required this.trueDarkMode,
|
||||
});
|
||||
|
||||
final AdaptiveThemeMode? savedThemeMode;
|
||||
final bool trueDarkMode;
|
||||
|
||||
static final GlobalKey<NavigatorState> navigatorKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
@ -166,6 +175,7 @@ class HackiApp extends StatelessWidget {
|
||||
dark: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.orange,
|
||||
canvasColor: trueDarkMode ? Colors.black : null,
|
||||
),
|
||||
initial: savedThemeMode ?? AdaptiveThemeMode.system,
|
||||
builder: (ThemeData theme, ThemeData darkTheme) {
|
||||
@ -174,20 +184,35 @@ class HackiApp extends StatelessWidget {
|
||||
primarySwatch: Colors.orange,
|
||||
canvasColor: Colors.black,
|
||||
);
|
||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen: (PreferenceState previous, PreferenceState current) =>
|
||||
previous.useTrueDark != current.useTrueDark,
|
||||
builder: (BuildContext context, PreferenceState prefState) {
|
||||
return FeatureDiscovery(
|
||||
child: MaterialApp(
|
||||
title: 'Hacki',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: prefState.useTrueDark ? trueDarkTheme : theme,
|
||||
darkTheme: prefState.useTrueDark ? trueDarkTheme : darkTheme,
|
||||
navigatorKey: navigatorKey,
|
||||
onGenerateRoute: CustomRouter.onGenerateRoute,
|
||||
initialRoute: HomeScreen.routeName,
|
||||
),
|
||||
return FutureBuilder<AdaptiveThemeMode?>(
|
||||
future: AdaptiveTheme.getThemeMode(),
|
||||
builder: (
|
||||
BuildContext context,
|
||||
AsyncSnapshot<AdaptiveThemeMode?> snapshot,
|
||||
) {
|
||||
final AdaptiveThemeMode? mode = snapshot.data;
|
||||
return BlocBuilder<PreferenceCubit, PreferenceState>(
|
||||
buildWhen:
|
||||
(PreferenceState previous, PreferenceState current) =>
|
||||
previous.useTrueDark != current.useTrueDark,
|
||||
builder: (BuildContext context, PreferenceState prefState) {
|
||||
final bool useTrueDark = prefState.useTrueDark &&
|
||||
(mode == AdaptiveThemeMode.dark ||
|
||||
(mode == AdaptiveThemeMode.system &&
|
||||
SchedulerBinding
|
||||
.instance.window.platformBrightness ==
|
||||
Brightness.dark));
|
||||
return FeatureDiscovery(
|
||||
child: MaterialApp(
|
||||
title: 'Hacki',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: useTrueDark ? trueDarkTheme : theme,
|
||||
navigatorKey: navigatorKey,
|
||||
onGenerateRoute: CustomRouter.onGenerateRoute,
|
||||
initialRoute: HomeScreen.routeName,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -22,9 +22,11 @@ class PreferenceRepository {
|
||||
static const String _lastReadStoryIdKey = 'lastReadStoryId';
|
||||
|
||||
static const String _notificationModeKey = 'notificationMode';
|
||||
static const String _trueDarkModeKey = 'trueDarkMode';
|
||||
static const String _readerModeKey = 'readerMode';
|
||||
|
||||
/// Exposing this val for main func.
|
||||
static const String trueDarkModeKey = 'trueDarkMode';
|
||||
|
||||
/// The key of a boolean value deciding whether or not the story
|
||||
/// tile should display link preview. Defaults to true.
|
||||
static const String _displayModeKey = 'displayMode';
|
||||
@ -99,7 +101,7 @@ class PreferenceRepository {
|
||||
|
||||
Future<bool> get trueDarkMode async => _prefs.then(
|
||||
(SharedPreferences prefs) =>
|
||||
prefs.getBool(_trueDarkModeKey) ?? _trueDarkModeDefaultValue,
|
||||
prefs.getBool(trueDarkModeKey) ?? _trueDarkModeDefaultValue,
|
||||
);
|
||||
|
||||
Future<bool> get readerMode async => _prefs.then(
|
||||
@ -247,8 +249,8 @@ class PreferenceRepository {
|
||||
Future<void> toggleTrueDarkMode() async {
|
||||
final SharedPreferences prefs = await _prefs;
|
||||
final bool currentMode =
|
||||
prefs.getBool(_trueDarkModeKey) ?? _trueDarkModeDefaultValue;
|
||||
await prefs.setBool(_trueDarkModeKey, !currentMode);
|
||||
prefs.getBool(trueDarkModeKey) ?? _trueDarkModeDefaultValue;
|
||||
await prefs.setBool(trueDarkModeKey, !currentMode);
|
||||
}
|
||||
|
||||
Future<void> toggleReaderMode() async {
|
||||
|
@ -372,6 +372,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
}
|
||||
|
||||
Future<bool> onFeatureDiscoveryDismissed() {
|
||||
HapticFeedback.lightImpact();
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
showSnackBar(content: 'Tap on icon to continue');
|
||||
return Future<bool>.value(false);
|
||||
@ -383,7 +384,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
final bool useReader = context.read<PreferenceCubit>().state.useReader;
|
||||
final bool offlineReading =
|
||||
context.read<StoriesBloc>().state.offlineReading;
|
||||
final bool firstTimeReading = cacheService.isFirstTimeReading(story.id);
|
||||
final bool hasRead = context.read<StoriesBloc>().hasRead(story);
|
||||
final bool splitViewEnabled = context.read<SplitViewCubit>().state.enabled;
|
||||
|
||||
// If a story is a job story and it has a link to the job posting,
|
||||
@ -411,8 +412,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
}
|
||||
}
|
||||
|
||||
if (!offlineReading &&
|
||||
(isJobWithLink || (showWebFirst && firstTimeReading))) {
|
||||
if (!offlineReading && (isJobWithLink || (showWebFirst && !hasRead))) {
|
||||
LinkUtil.launchUrl(story.url, useReader: useReader);
|
||||
cacheService.store(story.id);
|
||||
}
|
||||
|
@ -357,7 +357,9 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('True Dark Mode'),
|
||||
subtitle: const Text('real dark.'),
|
||||
subtitle: const Text(
|
||||
'you might need to restart the app.',
|
||||
),
|
||||
value: preferenceState.useTrueDark,
|
||||
onChanged: (bool val) {
|
||||
HapticFeedback.lightImpact();
|
||||
@ -368,17 +370,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
activeColor: Colors.orange,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(
|
||||
title: const Text(
|
||||
'Theme',
|
||||
style: TextStyle(
|
||||
decoration: preferenceState.useTrueDark
|
||||
? TextDecoration.lineThrough
|
||||
: null,
|
||||
),
|
||||
),
|
||||
onTap: () => showThemeSettingDialog(
|
||||
useTrueDarkMode: preferenceState.useTrueDark,
|
||||
),
|
||||
onTap: showThemeSettingDialog,
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('About'),
|
||||
@ -388,7 +383,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationName: 'Hacki',
|
||||
applicationVersion: 'v0.2.5',
|
||||
applicationVersion: 'v0.2.6',
|
||||
applicationIcon: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(12),
|
||||
@ -577,13 +572,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
||||
);
|
||||
}
|
||||
|
||||
void showThemeSettingDialog({bool useTrueDarkMode = false}) {
|
||||
if (useTrueDarkMode) {
|
||||
showSnackBar(
|
||||
content: "Can't choose theme when using true dark mode.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
void showThemeSettingDialog() {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
|
@ -559,6 +559,7 @@ class _StoryScreenState extends State<StoryScreen> {
|
||||
}
|
||||
|
||||
Future<bool> onFeatureDiscoveryDismissed() {
|
||||
HapticFeedback.lightImpact();
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
showSnackBar(content: 'Tap on icon to continue');
|
||||
return Future<bool>.value(false);
|
||||
|
@ -181,7 +181,7 @@ class CommentTile extends StatelessWidget {
|
||||
else if (state.collapsed)
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
padding: EdgeInsets.only(bottom: 8),
|
||||
child: Text(
|
||||
'collapsed',
|
||||
style:
|
||||
|
@ -1231,7 +1231,7 @@ packages:
|
||||
name: workmanager
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
version: "0.5.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1,6 +1,6 @@
|
||||
name: hacki
|
||||
description: A Hacker News reader.
|
||||
version: 0.2.5+39
|
||||
version: 0.2.6+40
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@ -56,7 +56,7 @@ dependencies:
|
||||
universal_platform: ^1.0.0+1
|
||||
url_launcher: ^6.0.10
|
||||
wakelock: ^0.6.1+2
|
||||
workmanager: ^0.4.1
|
||||
workmanager: ^0.5.0
|
||||
|
||||
dev_dependencies:
|
||||
bloc_test: ^9.0.3
|
||||
|
@ -9,6 +9,7 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
const HackiApp(
|
||||
savedThemeMode: AdaptiveThemeMode.light,
|
||||
trueDarkMode: false,
|
||||
),
|
||||
);
|
||||
|
||||
|
Reference in New Issue
Block a user