mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 18:24:42 +08:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
fe162208ca | |||
58139ba7a3 | |||
33a31acbe2 | |||
0fcfcbb7e3 | |||
a98f52c90b | |||
8e8e48c44a | |||
603b7cc939 | |||
649fa33df3 | |||
81d4a0f2df | |||
24112a471e | |||
c7824eaef3 | |||
c2b66d29c3 | |||
e0a53e44b2 | |||
4cf8379db0 | |||
c1c26bf0e0 |
18
.github/workflows/commit_check.yml
vendored
18
.github/workflows/commit_check.yml
vendored
@ -11,15 +11,13 @@ jobs:
|
|||||||
name: Check commit
|
name: Check commit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
env:
|
|
||||||
FLUTTER_VERSION: "3.7.0"
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: checkout all the submodules
|
||||||
- uses: subosito/flutter-action@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
flutter-version: '3.7.0'
|
submodules: recursive
|
||||||
channel: 'stable'
|
- run: submodules/flutter/bin/flutter doctor
|
||||||
- run: flutter pub get
|
- run: submodules/flutter/bin/flutter pub get
|
||||||
- run: flutter format --set-exit-if-changed .
|
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
|
||||||
- run: flutter analyze
|
- run: submodules/flutter/bin/flutter analyze lib test integration_test
|
||||||
- run: flutter test
|
- run: submodules/flutter/bin/flutter test
|
23
.github/workflows/publish_ios.yml
vendored
23
.github/workflows/publish_ios.yml
vendored
@ -20,21 +20,21 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out from git
|
- name: Check out from git
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- run: submodules/flutter/bin/flutter doctor
|
||||||
|
- run: submodules/flutter/bin/flutter pub get
|
||||||
|
- run: submodules/flutter/bin/dart format --set-exit-if-changed lib test integration_test
|
||||||
|
- run: submodules/flutter/bin/flutter analyze lib test integration_test
|
||||||
|
- run: submodules/flutter/bin/flutter test
|
||||||
|
|
||||||
# Configure ruby according to our .ruby-version
|
# Configure ruby according to our .ruby-version
|
||||||
- name: Setup ruby & Bundler
|
- name: Setup ruby & Bundler
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
# Set up flutter (feel free to adjust the version below)
|
|
||||||
- name: Setup flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
cache: true
|
|
||||||
flutter-version: 3.7.0
|
|
||||||
- run: flutter pub get
|
|
||||||
- run: flutter format --set-exit-if-changed .
|
|
||||||
- run: flutter analyze
|
|
||||||
# Start an ssh-agent that will provide the SSH key from the
|
# Start an ssh-agent that will provide the SSH key from the
|
||||||
# SSH_PRIVATE_KEY secret to `fastlane match`
|
# SSH_PRIVATE_KEY secret to `fastlane match`
|
||||||
- name: Setup SSH key
|
- name: Setup SSH key
|
||||||
@ -43,8 +43,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
|
ssh-agent -a $SSH_AUTH_SOCK > /dev/null
|
||||||
ssh-add - <<< "${{ secrets.SSH_PRIVATE_KEY }}"
|
ssh-add - <<< "${{ secrets.SSH_PRIVATE_KEY }}"
|
||||||
- name: Download dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
- name: Build & Publish to TestFlight with Fastlane
|
- name: Build & Publish to TestFlight with Fastlane
|
||||||
env:
|
env:
|
||||||
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
|
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
|
||||||
|
3
fastlane/metadata/android/en-US/changelogs/84.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/84.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
- Customization of tab bar.
|
||||||
|
- Option to enable swipe gesture for switching between tabs.
|
||||||
|
- Access to action menu from home screen.
|
@ -49,7 +49,7 @@ latest_testflight_build_number
|
|||||||
|
|
||||||
# Prep the xcodeproject from Flutter without building (`--config-only`)
|
# Prep the xcodeproject from Flutter without building (`--config-only`)
|
||||||
sh(
|
sh(
|
||||||
"flutter", "build", "ios", "--config-only",
|
"/Users/runner/work/Hacki/Hacki/submodules/flutter/bin/flutter", "build", "ios", "--config-only",
|
||||||
"--release", "--no-pub", "--no-codesign",
|
"--release", "--no-pub", "--no-codesign",
|
||||||
"--build-number", new_build_number.to_s
|
"--build-number", new_build_number.to_s
|
||||||
)
|
)
|
||||||
|
@ -56,6 +56,8 @@ abstract class Constants {
|
|||||||
'ʕ•́ᴥ•̀ʔっ',
|
'ʕ•́ᴥ•̀ʔっ',
|
||||||
'(ㆆ_ㆆ)',
|
'(ㆆ_ㆆ)',
|
||||||
].pickRandomly()!;
|
].pickRandomly()!;
|
||||||
|
|
||||||
|
static final String errorMessage = 'Something went wrong...$sadFace';
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class RegExpConstants {
|
abstract class RegExpConstants {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/screens/screens.dart';
|
import 'package:hacki/screens/screens.dart';
|
||||||
|
|
||||||
/// Custom router.
|
/// Custom router.
|
||||||
@ -39,8 +40,8 @@ class CustomRouter {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Error'),
|
title: const Text('Error'),
|
||||||
),
|
),
|
||||||
body: const Center(
|
body: Center(
|
||||||
child: Text('Something went wrong!'),
|
child: Text(Constants.errorMessage),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -20,7 +20,7 @@ Future<void> setUpLocator() async {
|
|||||||
Logger(
|
Logger(
|
||||||
filter: CustomLogFilter(),
|
filter: CustomLogFilter(),
|
||||||
printer: LogUtil.logPrinter,
|
printer: LogUtil.logPrinter,
|
||||||
output: LogUtil.getLogOutput(logOutputFile),
|
output: LogUtil.logOutput(logOutputFile),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
..registerSingleton<StoriesRepository>(StoriesRepository())
|
..registerSingleton<StoriesRepository>(StoriesRepository())
|
||||||
|
@ -2,6 +2,8 @@ import 'dart:math';
|
|||||||
|
|
||||||
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:hacki/config/constants.dart';
|
||||||
|
import 'package:hacki/styles/styles.dart';
|
||||||
|
|
||||||
extension ContextExtension on BuildContext {
|
extension ContextExtension on BuildContext {
|
||||||
T? tryRead<T>() {
|
T? tryRead<T>() {
|
||||||
@ -12,6 +14,31 @@ extension ContextExtension on BuildContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showSnackBar({
|
||||||
|
required String content,
|
||||||
|
VoidCallback? action,
|
||||||
|
String? label,
|
||||||
|
}) {
|
||||||
|
ScaffoldMessenger.of(this).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
backgroundColor: Palette.deepOrange,
|
||||||
|
content: Text(content),
|
||||||
|
action: action != null && label != null
|
||||||
|
? SnackBarAction(
|
||||||
|
label: label,
|
||||||
|
onPressed: action,
|
||||||
|
textColor: Theme.of(this).textTheme.bodyLarge?.color,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showErrorSnackBar() => showSnackBar(
|
||||||
|
content: Constants.errorMessage,
|
||||||
|
);
|
||||||
|
|
||||||
Rect? get rect {
|
Rect? get rect {
|
||||||
final RenderBox? box = findRenderObject() as RenderBox?;
|
final RenderBox? box = findRenderObject() as RenderBox?;
|
||||||
final Rect? rect =
|
final Rect? rect =
|
||||||
|
@ -21,22 +21,15 @@ extension StateExtension on State {
|
|||||||
VoidCallback? action,
|
VoidCallback? action,
|
||||||
String? label,
|
String? label,
|
||||||
}) {
|
}) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
context.showSnackBar(
|
||||||
SnackBar(
|
content: content,
|
||||||
backgroundColor: Palette.deepOrange,
|
action: action,
|
||||||
content: Text(content),
|
label: label,
|
||||||
action: action != null && label != null
|
|
||||||
? SnackBarAction(
|
|
||||||
label: label,
|
|
||||||
onPressed: action,
|
|
||||||
textColor: Theme.of(context).textTheme.bodyLarge?.color,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showErrorSnackBar() => context.showErrorSnackBar();
|
||||||
|
|
||||||
Future<void>? goToItemScreen({
|
Future<void>? goToItemScreen({
|
||||||
required ItemScreenArgs args,
|
required ItemScreenArgs args,
|
||||||
bool forceNewScreen = false,
|
bool forceNewScreen = false,
|
||||||
@ -70,7 +63,6 @@ extension StateExtension on State {
|
|||||||
return MorePopupMenu(
|
return MorePopupMenu(
|
||||||
item: item,
|
item: item,
|
||||||
isBlocked: isBlocked,
|
isBlocked: isBlocked,
|
||||||
showSnackBar: showSnackBar,
|
|
||||||
onStoryLinkTapped: onStoryLinkTapped,
|
onStoryLinkTapped: onStoryLinkTapped,
|
||||||
onLoginTapped: onLoginTapped,
|
onLoginTapped: onLoginTapped,
|
||||||
);
|
);
|
||||||
@ -119,11 +111,45 @@ extension StateExtension on State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onShareTapped(Item item, Rect? rect) {
|
Future<void> onShareTapped(Item item, Rect? rect) async {
|
||||||
Share.share(
|
late final String? linkToShare;
|
||||||
'https://news.ycombinator.com/item?id=${item.id}',
|
if (item.url.isNotEmpty) {
|
||||||
sharePositionOrigin: rect,
|
linkToShare = await showModalBottomSheet<String>(
|
||||||
);
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 140,
|
||||||
|
color: Theme.of(context).canvasColor,
|
||||||
|
child: Material(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
ListTile(
|
||||||
|
onTap: () => Navigator.pop(context, item.url),
|
||||||
|
title: const Text('Link to article'),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
onTap: () => Navigator.pop(
|
||||||
|
context,
|
||||||
|
'https://news.ycombinator.com/item?id=${item.id}',
|
||||||
|
),
|
||||||
|
title: const Text('Link to HN'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
linkToShare = 'https://news.ycombinator.com/item?id=${item.id}';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linkToShare != null) {
|
||||||
|
await Share.share(
|
||||||
|
linkToShare,
|
||||||
|
sharePositionOrigin: rect,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onFlagTapped(Item item) {
|
void onFlagTapped(Item item) {
|
||||||
|
@ -9,4 +9,5 @@ export 'post_data.dart';
|
|||||||
export 'preference.dart';
|
export 'preference.dart';
|
||||||
export 'search_params.dart';
|
export 'search_params.dart';
|
||||||
export 'story.dart';
|
export 'story.dart';
|
||||||
|
export 'story_type.dart';
|
||||||
export 'user.dart';
|
export 'user.dart';
|
||||||
|
@ -24,22 +24,7 @@ class PollOption extends Item {
|
|||||||
|
|
||||||
PollOption.empty()
|
PollOption.empty()
|
||||||
: ratio = 0,
|
: ratio = 0,
|
||||||
super(
|
super.empty();
|
||||||
id: 0,
|
|
||||||
score: 0,
|
|
||||||
descendants: 0,
|
|
||||||
time: 0,
|
|
||||||
by: '',
|
|
||||||
title: '',
|
|
||||||
url: '',
|
|
||||||
kids: <int>[],
|
|
||||||
dead: false,
|
|
||||||
parts: <int>[],
|
|
||||||
deleted: false,
|
|
||||||
parent: 0,
|
|
||||||
text: '',
|
|
||||||
type: '',
|
|
||||||
);
|
|
||||||
|
|
||||||
PollOption.fromJson(super.json)
|
PollOption.fromJson(super.json)
|
||||||
: ratio = 0,
|
: ratio = 0,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:collection';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
@ -13,26 +14,29 @@ abstract class Preference<T> extends Equatable with SettingsDisplayable {
|
|||||||
|
|
||||||
Preference<T> copyWith({required T? val});
|
Preference<T> copyWith({required T? val});
|
||||||
|
|
||||||
static List<Preference<dynamic>> allPreferences = <Preference<dynamic>>[
|
static final List<Preference<dynamic>> allPreferences =
|
||||||
// Order of these first three preferences does not matter.
|
UnmodifiableListView<Preference<dynamic>>(
|
||||||
FetchModePreference(),
|
<Preference<dynamic>>[
|
||||||
CommentsOrderPreference(),
|
// Order of these first four preferences does not matter.
|
||||||
FontSizePreference(),
|
FetchModePreference(),
|
||||||
TabOrderPreference(),
|
CommentsOrderPreference(),
|
||||||
// Order of items below matters and
|
FontSizePreference(),
|
||||||
// reflects the order on settings screen.
|
TabOrderPreference(),
|
||||||
const DisplayModePreference(),
|
// Order of items below matters and
|
||||||
const MetadataModePreference(),
|
// reflects the order on settings screen.
|
||||||
const StoryUrlModePreference(),
|
const DisplayModePreference(),
|
||||||
const NotificationModePreference(),
|
const MetadataModePreference(),
|
||||||
const SwipeGesturePreference(),
|
const StoryUrlModePreference(),
|
||||||
const CollapseModePreference(),
|
const NotificationModePreference(),
|
||||||
NavigationModePreference(),
|
const SwipeGesturePreference(),
|
||||||
const ReaderModePreference(),
|
const CollapseModePreference(),
|
||||||
const MarkReadStoriesModePreference(),
|
NavigationModePreference(),
|
||||||
const EyeCandyModePreference(),
|
const ReaderModePreference(),
|
||||||
const TrueDarkModePreference(),
|
const MarkReadStoriesModePreference(),
|
||||||
];
|
const EyeCandyModePreference(),
|
||||||
|
const TrueDarkModePreference(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => <Object?>[key];
|
List<Object?> get props => <Object?>[key];
|
||||||
@ -81,7 +85,7 @@ class SwipeGesturePreference extends BooleanPreference {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get subtitle =>
|
String get subtitle =>
|
||||||
'''Enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu.''';
|
'''enable swipe gesture for switching between tabs. If enabled, long press on Story tile to trigger the action menu.''';
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationModePreference extends BooleanPreference {
|
class NotificationModePreference extends BooleanPreference {
|
||||||
@ -118,6 +122,10 @@ class CollapseModePreference extends BooleanPreference {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get title => 'Tap Anywhere to Collapse';
|
String get title => 'Tap Anywhere to Collapse';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get subtitle =>
|
||||||
|
'''if disabled, tap on the top of comment tile to collapse.''';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The value deciding whether or not the story
|
/// The value deciding whether or not the story
|
||||||
|
@ -1,41 +1,6 @@
|
|||||||
import 'package:hacki/config/constants.dart';
|
import 'package:hacki/config/constants.dart';
|
||||||
import 'package:hacki/models/item.dart';
|
import 'package:hacki/models/item.dart';
|
||||||
|
|
||||||
enum StoryType {
|
|
||||||
top('topstories'),
|
|
||||||
best('beststories'),
|
|
||||||
latest('newstories'),
|
|
||||||
ask('askstories'),
|
|
||||||
show('showstories');
|
|
||||||
|
|
||||||
const StoryType(this.path);
|
|
||||||
|
|
||||||
final String path;
|
|
||||||
|
|
||||||
String get label {
|
|
||||||
switch (this) {
|
|
||||||
case StoryType.top:
|
|
||||||
return 'TOP';
|
|
||||||
case StoryType.best:
|
|
||||||
return 'BEST';
|
|
||||||
case StoryType.latest:
|
|
||||||
return 'NEW';
|
|
||||||
case StoryType.ask:
|
|
||||||
return 'ASK';
|
|
||||||
case StoryType.show:
|
|
||||||
return 'SHOW';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static int convertToSettingsValue(List<StoryType> tabs) {
|
|
||||||
return int.parse(
|
|
||||||
tabs
|
|
||||||
.map((StoryType e) => e.index.toString())
|
|
||||||
.reduce((String value, String element) => '$value$element'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Story extends Item {
|
class Story extends Item {
|
||||||
const Story({
|
const Story({
|
||||||
required super.descendants,
|
required super.descendants,
|
||||||
@ -55,23 +20,7 @@ class Story extends Item {
|
|||||||
parent: 0,
|
parent: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
Story.empty()
|
Story.empty() : super.empty();
|
||||||
: super(
|
|
||||||
id: 0,
|
|
||||||
score: 0,
|
|
||||||
descendants: 0,
|
|
||||||
time: 0,
|
|
||||||
by: '',
|
|
||||||
title: '',
|
|
||||||
url: '',
|
|
||||||
kids: <int>[],
|
|
||||||
dead: false,
|
|
||||||
parts: <int>[],
|
|
||||||
deleted: false,
|
|
||||||
parent: 0,
|
|
||||||
text: '',
|
|
||||||
type: '',
|
|
||||||
);
|
|
||||||
|
|
||||||
Story.placeholder()
|
Story.placeholder()
|
||||||
: super(
|
: super(
|
||||||
|
34
lib/models/story_type.dart
Normal file
34
lib/models/story_type.dart
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
enum StoryType {
|
||||||
|
top('topstories'),
|
||||||
|
best('beststories'),
|
||||||
|
latest('newstories'),
|
||||||
|
ask('askstories'),
|
||||||
|
show('showstories');
|
||||||
|
|
||||||
|
const StoryType(this.path);
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
|
||||||
|
String get label {
|
||||||
|
switch (this) {
|
||||||
|
case StoryType.top:
|
||||||
|
return 'TOP';
|
||||||
|
case StoryType.best:
|
||||||
|
return 'BEST';
|
||||||
|
case StoryType.latest:
|
||||||
|
return 'NEW';
|
||||||
|
case StoryType.ask:
|
||||||
|
return 'ASK';
|
||||||
|
case StoryType.show:
|
||||||
|
return 'SHOW';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int convertToSettingsValue(List<StoryType> tabs) {
|
||||||
|
return int.parse(
|
||||||
|
tabs
|
||||||
|
.map((StoryType e) => e.index.toString())
|
||||||
|
.reduce((String value, String element) => '$value$element'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:hacki/config/locator.dart';
|
import 'package:hacki/config/locator.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/repositories/postable_repository.dart';
|
import 'package:hacki/repositories/postable_repository.dart';
|
||||||
import 'package:hacki/repositories/repositories.dart';
|
import 'package:hacki/repositories/preference_repository.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
class AuthRepository extends PostableRepository {
|
class AuthRepository extends PostableRepository {
|
||||||
@ -18,8 +18,6 @@ class AuthRepository extends PostableRepository {
|
|||||||
final PreferenceRepository _preferenceRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
final Logger _logger;
|
final Logger _logger;
|
||||||
|
|
||||||
static const String _authority = 'news.ycombinator.com';
|
|
||||||
|
|
||||||
Future<bool> get loggedIn async => _preferenceRepository.loggedIn;
|
Future<bool> get loggedIn async => _preferenceRepository.loggedIn;
|
||||||
|
|
||||||
Future<String?> get username async => _preferenceRepository.username;
|
Future<String?> get username async => _preferenceRepository.username;
|
||||||
@ -30,7 +28,7 @@ class AuthRepository extends PostableRepository {
|
|||||||
required String username,
|
required String username,
|
||||||
required String password,
|
required String password,
|
||||||
}) async {
|
}) async {
|
||||||
final Uri uri = Uri.https(_authority, 'login');
|
final Uri uri = Uri.https(authority, 'login');
|
||||||
final PostDataMixin data = LoginPostData(
|
final PostDataMixin data = LoginPostData(
|
||||||
acct: username,
|
acct: username,
|
||||||
pw: password,
|
pw: password,
|
||||||
@ -64,7 +62,7 @@ class AuthRepository extends PostableRepository {
|
|||||||
required int id,
|
required int id,
|
||||||
required bool flag,
|
required bool flag,
|
||||||
}) async {
|
}) async {
|
||||||
final Uri uri = Uri.https(_authority, 'flag');
|
final Uri uri = Uri.https(authority, 'flag');
|
||||||
final String? username = await _preferenceRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _preferenceRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
final PostDataMixin data = FlagPostData(
|
final PostDataMixin data = FlagPostData(
|
||||||
@ -81,7 +79,7 @@ class AuthRepository extends PostableRepository {
|
|||||||
required int id,
|
required int id,
|
||||||
required bool favorite,
|
required bool favorite,
|
||||||
}) async {
|
}) async {
|
||||||
final Uri uri = Uri.https(_authority, 'fave');
|
final Uri uri = Uri.https(authority, 'fave');
|
||||||
final String? username = await _preferenceRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _preferenceRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
final PostDataMixin data = FavoritePostData(
|
final PostDataMixin data = FavoritePostData(
|
||||||
@ -98,7 +96,7 @@ class AuthRepository extends PostableRepository {
|
|||||||
required int id,
|
required int id,
|
||||||
required bool upvote,
|
required bool upvote,
|
||||||
}) async {
|
}) async {
|
||||||
final Uri uri = Uri.https(_authority, 'vote');
|
final Uri uri = Uri.https(authority, 'vote');
|
||||||
final String? username = await _preferenceRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _preferenceRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
final PostDataMixin data = VotePostData(
|
final PostDataMixin data = VotePostData(
|
||||||
@ -115,7 +113,7 @@ class AuthRepository extends PostableRepository {
|
|||||||
required int id,
|
required int id,
|
||||||
required bool downvote,
|
required bool downvote,
|
||||||
}) async {
|
}) async {
|
||||||
final Uri uri = Uri.https(_authority, 'vote');
|
final Uri uri = Uri.https(authority, 'vote');
|
||||||
final String? username = await _preferenceRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _preferenceRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
final PostDataMixin data = VotePostData(
|
final PostDataMixin data = VotePostData(
|
||||||
|
@ -14,15 +14,13 @@ class PostRepository extends PostableRepository {
|
|||||||
|
|
||||||
final PreferenceRepository _preferenceRepository;
|
final PreferenceRepository _preferenceRepository;
|
||||||
|
|
||||||
static const String _authority = 'news.ycombinator.com';
|
|
||||||
|
|
||||||
Future<bool> comment({
|
Future<bool> comment({
|
||||||
required int parentId,
|
required int parentId,
|
||||||
required String text,
|
required String text,
|
||||||
}) async {
|
}) async {
|
||||||
final String? username = await _preferenceRepository.username;
|
final String? username = await _preferenceRepository.username;
|
||||||
final String? password = await _preferenceRepository.password;
|
final String? password = await _preferenceRepository.password;
|
||||||
final Uri uri = Uri.https(_authority, 'comment');
|
final Uri uri = Uri.https(authority, 'comment');
|
||||||
|
|
||||||
if (username == null || password == null) {
|
if (username == null || password == null) {
|
||||||
return false;
|
return false;
|
||||||
@ -54,7 +52,7 @@ class PostRepository extends PostableRepository {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Response<List<int>> formResponse = await _getFormResponse(
|
final Response<List<int>> formResponse = await getFormResponse(
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
path: 'submitlink',
|
path: 'submitlink',
|
||||||
@ -69,7 +67,7 @@ class PostRepository extends PostableRepository {
|
|||||||
final String? cookie =
|
final String? cookie =
|
||||||
formResponse.headers.value(HttpHeaders.setCookieHeader);
|
formResponse.headers.value(HttpHeaders.setCookieHeader);
|
||||||
|
|
||||||
final Uri uri = Uri.https(_authority, 'r');
|
final Uri uri = Uri.https(authority, 'r');
|
||||||
final PostDataMixin data = SubmitPostData(
|
final PostDataMixin data = SubmitPostData(
|
||||||
fnid: formValues['fnid']!,
|
fnid: formValues['fnid']!,
|
||||||
fnop: formValues['fnop']!,
|
fnop: formValues['fnop']!,
|
||||||
@ -97,7 +95,7 @@ class PostRepository extends PostableRepository {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Response<List<int>> formResponse = await _getFormResponse(
|
final Response<List<int>> formResponse = await getFormResponse(
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
id: id,
|
id: id,
|
||||||
@ -113,7 +111,7 @@ class PostRepository extends PostableRepository {
|
|||||||
final String? cookie =
|
final String? cookie =
|
||||||
formResponse.headers.value(HttpHeaders.setCookieHeader);
|
formResponse.headers.value(HttpHeaders.setCookieHeader);
|
||||||
|
|
||||||
final Uri uri = Uri.https(_authority, 'xedit');
|
final Uri uri = Uri.https(authority, 'xedit');
|
||||||
final PostDataMixin data = EditPostData(
|
final PostDataMixin data = EditPostData(
|
||||||
hmac: formValues['hmac']!,
|
hmac: formValues['hmac']!,
|
||||||
id: id,
|
id: id,
|
||||||
@ -126,28 +124,4 @@ class PostRepository extends PostableRepository {
|
|||||||
cookie: cookie,
|
cookie: cookie,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response<List<int>>> _getFormResponse({
|
|
||||||
required String username,
|
|
||||||
required String password,
|
|
||||||
required String path,
|
|
||||||
int? id,
|
|
||||||
}) async {
|
|
||||||
final Uri uri = Uri.https(
|
|
||||||
_authority,
|
|
||||||
path,
|
|
||||||
<String, dynamic>{if (id != null) 'id': id.toString()},
|
|
||||||
);
|
|
||||||
final PostDataMixin data = FormPostData(
|
|
||||||
acct: username,
|
|
||||||
pw: password,
|
|
||||||
id: id,
|
|
||||||
);
|
|
||||||
return performPost(
|
|
||||||
uri,
|
|
||||||
data,
|
|
||||||
responseType: ResponseType.bytes,
|
|
||||||
validateStatus: (int? status) => status == HttpStatus.ok,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,14 @@ import 'package:hacki/utils/service_exception.dart';
|
|||||||
class PostableRepository {
|
class PostableRepository {
|
||||||
PostableRepository({
|
PostableRepository({
|
||||||
Dio? dio,
|
Dio? dio,
|
||||||
|
this.authority = 'news.ycombinator.com',
|
||||||
}) : _dio = dio ?? Dio();
|
}) : _dio = dio ?? Dio();
|
||||||
|
|
||||||
final Dio _dio;
|
final Dio _dio;
|
||||||
|
|
||||||
|
@protected
|
||||||
|
final String authority;
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
Future<bool> performDefaultPost(
|
Future<bool> performDefaultPost(
|
||||||
Uri uri,
|
Uri uri,
|
||||||
@ -60,4 +64,29 @@ class PostableRepository {
|
|||||||
throw ServiceException(e.message);
|
throw ServiceException(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
Future<Response<List<int>>> getFormResponse({
|
||||||
|
required String username,
|
||||||
|
required String password,
|
||||||
|
required String path,
|
||||||
|
int? id,
|
||||||
|
}) async {
|
||||||
|
final Uri uri = Uri.https(
|
||||||
|
authority,
|
||||||
|
path,
|
||||||
|
<String, dynamic>{if (id != null) 'id': id.toString()},
|
||||||
|
);
|
||||||
|
final PostDataMixin data = FormPostData(
|
||||||
|
acct: username,
|
||||||
|
pw: password,
|
||||||
|
id: id,
|
||||||
|
);
|
||||||
|
return performPost(
|
||||||
|
uri,
|
||||||
|
data,
|
||||||
|
responseType: ResponseType.bytes,
|
||||||
|
validateStatus: (int? status) => status == HttpStatus.ok,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,7 +301,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
.fetchStoryBy(storyId)
|
.fetchStoryBy(storyId)
|
||||||
.then((Story? story) {
|
.then((Story? story) {
|
||||||
if (story == null) {
|
if (story == null) {
|
||||||
showSnackBar(content: 'Something went wrong...');
|
showErrorSnackBar();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
||||||
@ -326,7 +326,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
.fetchStoryBy(storyId)
|
.fetchStoryBy(storyId)
|
||||||
.then((Story? story) {
|
.then((Story? story) {
|
||||||
if (story == null) {
|
if (story == null) {
|
||||||
showSnackBar(content: 'Something went wrong...');
|
showErrorSnackBar();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
||||||
|
@ -239,12 +239,7 @@ class _ItemScreenState extends State<ItemScreen> with RouteAware {
|
|||||||
context.read<EditCubit>().onReplySubmittedSuccessfully();
|
context.read<EditCubit>().onReplySubmittedSuccessfully();
|
||||||
context.read<PostCubit>().reset();
|
context.read<PostCubit>().reset();
|
||||||
} else if (postState.status == PostStatus.failure) {
|
} else if (postState.status == PostStatus.failure) {
|
||||||
showSnackBar(
|
showErrorSnackBar();
|
||||||
content: 'Something went wrong...'
|
|
||||||
'${Constants.sadFace}',
|
|
||||||
label: 'Okay',
|
|
||||||
action: ScaffoldMessenger.of(context).hideCurrentSnackBar,
|
|
||||||
);
|
|
||||||
context.read<PostCubit>().reset();
|
context.read<PostCubit>().reset();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -87,13 +87,13 @@ class LoginDialog extends StatelessWidget {
|
|||||||
height: Dimens.pt16,
|
height: Dimens.pt16,
|
||||||
),
|
),
|
||||||
if (state.status == AuthStatus.failure)
|
if (state.status == AuthStatus.failure)
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: Dimens.pt18,
|
left: Dimens.pt18,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Something went wrong...',
|
Constants.errorMessage,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: Palette.grey,
|
color: Palette.grey,
|
||||||
fontSize: TextDimens.pt12,
|
fontSize: TextDimens.pt12,
|
||||||
),
|
),
|
||||||
|
@ -15,18 +15,12 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.item,
|
required this.item,
|
||||||
required this.isBlocked,
|
required this.isBlocked,
|
||||||
required this.showSnackBar,
|
|
||||||
required this.onStoryLinkTapped,
|
required this.onStoryLinkTapped,
|
||||||
required this.onLoginTapped,
|
required this.onLoginTapped,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Item item;
|
final Item item;
|
||||||
final bool isBlocked;
|
final bool isBlocked;
|
||||||
final void Function({
|
|
||||||
required String content,
|
|
||||||
VoidCallback? action,
|
|
||||||
String? label,
|
|
||||||
}) showSnackBar;
|
|
||||||
final ValueChanged<String> onStoryLinkTapped;
|
final ValueChanged<String> onStoryLinkTapped;
|
||||||
final VoidCallback onLoginTapped;
|
final VoidCallback onLoginTapped;
|
||||||
|
|
||||||
@ -43,24 +37,26 @@ class MorePopupMenu extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
listener: (BuildContext context, VoteState voteState) {
|
listener: (BuildContext context, VoteState voteState) {
|
||||||
if (voteState.status == VoteStatus.submitted) {
|
if (voteState.status == VoteStatus.submitted) {
|
||||||
showSnackBar(content: 'Vote submitted successfully.');
|
context.showSnackBar(content: 'Vote submitted successfully.');
|
||||||
} else if (voteState.status == VoteStatus.canceled) {
|
} else if (voteState.status == VoteStatus.canceled) {
|
||||||
showSnackBar(content: 'Vote canceled.');
|
context.showSnackBar(content: 'Vote canceled.');
|
||||||
} else if (voteState.status == VoteStatus.failure) {
|
} else if (voteState.status == VoteStatus.failure) {
|
||||||
showSnackBar(content: 'Something went wrong...');
|
context.showErrorSnackBar();
|
||||||
} else if (voteState.status ==
|
} else if (voteState.status ==
|
||||||
VoteStatus.failureKarmaBelowThreshold) {
|
VoteStatus.failureKarmaBelowThreshold) {
|
||||||
showSnackBar(
|
context.showSnackBar(
|
||||||
content: "You can't downvote because you are karmaly broke.",
|
content: "You can't downvote because you are karmaly broke.",
|
||||||
);
|
);
|
||||||
} else if (voteState.status == VoteStatus.failureNotLoggedIn) {
|
} else if (voteState.status == VoteStatus.failureNotLoggedIn) {
|
||||||
showSnackBar(
|
context.showSnackBar(
|
||||||
content: 'Not logged in, no voting! (;`O´)o',
|
content: 'Not logged in, no voting! (;`O´)o',
|
||||||
action: onLoginTapped,
|
action: onLoginTapped,
|
||||||
label: 'Log in',
|
label: 'Log in',
|
||||||
);
|
);
|
||||||
} else if (voteState.status == VoteStatus.failureBeHumble) {
|
} else if (voteState.status == VoteStatus.failureBeHumble) {
|
||||||
showSnackBar(content: 'No voting on your own post! (;`O´)o');
|
context.showSnackBar(
|
||||||
|
content: 'No voting on your own post! (;`O´)o',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
|
@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_fadein/flutter_fadein.dart';
|
import 'package:flutter_fadein/flutter_fadein.dart';
|
||||||
import 'package:hacki/blocs/auth/auth_bloc.dart';
|
import 'package:hacki/blocs/auth/auth_bloc.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
|
import 'package:hacki/extensions/context_extension.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
|
|
||||||
@ -61,36 +62,29 @@ class PollView extends StatelessWidget {
|
|||||||
listener: (BuildContext context, VoteState voteState) {
|
listener: (BuildContext context, VoteState voteState) {
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
if (voteState.status == VoteStatus.submitted) {
|
if (voteState.status == VoteStatus.submitted) {
|
||||||
showSnackBar(
|
context.showSnackBar(
|
||||||
context,
|
|
||||||
content: 'Vote submitted successfully.',
|
content: 'Vote submitted successfully.',
|
||||||
);
|
);
|
||||||
} else if (voteState.status == VoteStatus.canceled) {
|
} else if (voteState.status == VoteStatus.canceled) {
|
||||||
showSnackBar(context, content: 'Vote canceled.');
|
context.showSnackBar(content: 'Vote canceled.');
|
||||||
} else if (voteState.status == VoteStatus.failure) {
|
} else if (voteState.status == VoteStatus.failure) {
|
||||||
showSnackBar(
|
context.showErrorSnackBar();
|
||||||
context,
|
|
||||||
content: 'Something went wrong...',
|
|
||||||
);
|
|
||||||
} else if (voteState.status ==
|
} else if (voteState.status ==
|
||||||
VoteStatus.failureKarmaBelowThreshold) {
|
VoteStatus.failureKarmaBelowThreshold) {
|
||||||
showSnackBar(
|
context.showSnackBar(
|
||||||
context,
|
|
||||||
content: "You can't downvote because"
|
content: "You can't downvote because"
|
||||||
' you are karmaly broke.',
|
' you are karmaly broke.',
|
||||||
);
|
);
|
||||||
} else if (voteState.status ==
|
} else if (voteState.status ==
|
||||||
VoteStatus.failureNotLoggedIn) {
|
VoteStatus.failureNotLoggedIn) {
|
||||||
showSnackBar(
|
context.showSnackBar(
|
||||||
context,
|
|
||||||
content: 'Not logged in, no voting! (;`O´)o',
|
content: 'Not logged in, no voting! (;`O´)o',
|
||||||
action: onLoginTapped,
|
action: onLoginTapped,
|
||||||
label: 'Log in',
|
label: 'Log in',
|
||||||
);
|
);
|
||||||
} else if (voteState.status ==
|
} else if (voteState.status ==
|
||||||
VoteStatus.failureBeHumble) {
|
VoteStatus.failureBeHumble) {
|
||||||
showSnackBar(
|
context.showSnackBar(
|
||||||
context,
|
|
||||||
content: 'No voting on your own post! (;`O´)o',
|
content: 'No voting on your own post! (;`O´)o',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -153,26 +147,4 @@ class PollView extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showSnackBar(
|
|
||||||
BuildContext context, {
|
|
||||||
required String content,
|
|
||||||
VoidCallback? action,
|
|
||||||
String? label,
|
|
||||||
}) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
backgroundColor: Palette.deepOrange,
|
|
||||||
content: Text(content),
|
|
||||||
action: action != null && label != null
|
|
||||||
? SnackBarAction(
|
|
||||||
label: label,
|
|
||||||
onPressed: action,
|
|
||||||
textColor: Theme.of(context).textTheme.bodyLarge?.color,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
behavior: SnackBarBehavior.floating,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -453,13 +453,13 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
height: Dimens.pt16,
|
height: Dimens.pt16,
|
||||||
),
|
),
|
||||||
if (state.status == AuthStatus.failure)
|
if (state.status == AuthStatus.failure)
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
left: Dimens.pt18,
|
left: Dimens.pt18,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Something went wrong...',
|
Constants.errorMessage,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: Palette.grey,
|
color: Palette.grey,
|
||||||
fontSize: TextDimens.pt12,
|
fontSize: TextDimens.pt12,
|
||||||
),
|
),
|
||||||
|
@ -50,9 +50,7 @@ class _SubmitScreenState extends State<SubmitScreen> {
|
|||||||
content: 'Post submitted successfully.',
|
content: 'Post submitted successfully.',
|
||||||
);
|
);
|
||||||
} else if (state.status == SubmitStatus.failure) {
|
} else if (state.status == SubmitStatus.failure) {
|
||||||
showSnackBar(
|
showErrorSnackBar();
|
||||||
content: 'Something went wrong...',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (BuildContext context, SubmitState state) {
|
builder: (BuildContext context, SubmitState state) {
|
||||||
|
48
lib/screens/widgets/centered_text.dart
Normal file
48
lib/screens/widgets/centered_text.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hacki/styles/styles.dart';
|
||||||
|
|
||||||
|
class CenteredText extends StatelessWidget {
|
||||||
|
const CenteredText({
|
||||||
|
super.key,
|
||||||
|
required this.text,
|
||||||
|
this.color = Palette.grey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const CenteredText.deleted({Key? key})
|
||||||
|
: this(
|
||||||
|
key: key,
|
||||||
|
text: 'deleted',
|
||||||
|
);
|
||||||
|
|
||||||
|
const CenteredText.dead({Key? key})
|
||||||
|
: this(
|
||||||
|
key: key,
|
||||||
|
text: 'dead',
|
||||||
|
);
|
||||||
|
|
||||||
|
const CenteredText.blocked({Key? key})
|
||||||
|
: this(
|
||||||
|
key: key,
|
||||||
|
text: 'blocked',
|
||||||
|
);
|
||||||
|
|
||||||
|
final String text;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
bottom: Dimens.pt12,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import 'package:hacki/cubits/cubits.dart';
|
|||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/screens/widgets/bloc_builder_3.dart';
|
import 'package:hacki/screens/widgets/bloc_builder_3.dart';
|
||||||
|
import 'package:hacki/screens/widgets/centered_text.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';
|
||||||
@ -40,6 +41,8 @@ class CommentTile extends StatelessWidget {
|
|||||||
final void Function(String) onStoryLinkTapped;
|
final void Function(String) onStoryLinkTapped;
|
||||||
final FetchMode fetchMode;
|
final FetchMode fetchMode;
|
||||||
|
|
||||||
|
static final Map<int, Color> _colors = <int, Color>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider<CollapseCubit>(
|
return BlocProvider<CollapseCubit>(
|
||||||
@ -157,136 +160,92 @@ class CommentTile extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (actionable && state.collapsed)
|
AnimatedSize(
|
||||||
Center(
|
duration: const Duration(milliseconds: 200),
|
||||||
child: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.only(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
bottom: Dimens.pt12,
|
children: <Widget>[
|
||||||
),
|
if (actionable && state.collapsed)
|
||||||
child: Text(
|
CenteredText(
|
||||||
'collapsed '
|
text:
|
||||||
'(${state.collapsedCount + 1})',
|
'''collapsed (${state.collapsedCount + 1})''',
|
||||||
style: const TextStyle(
|
|
||||||
color: Palette.orangeAccent,
|
color: Palette.orangeAccent,
|
||||||
),
|
)
|
||||||
),
|
else if (comment.deleted)
|
||||||
),
|
const CenteredText.deleted()
|
||||||
)
|
else if (comment.dead)
|
||||||
else if (comment.deleted)
|
const CenteredText.dead()
|
||||||
const Center(
|
else if (blocklistState.blocklist
|
||||||
child: Padding(
|
.contains(comment.by))
|
||||||
padding: EdgeInsets.only(
|
const CenteredText.blocked()
|
||||||
bottom: Dimens.pt12,
|
else
|
||||||
),
|
Padding(
|
||||||
child: Text(
|
padding: const EdgeInsets.only(
|
||||||
'deleted',
|
left: Dimens.pt8,
|
||||||
style: TextStyle(
|
right: Dimens.pt8,
|
||||||
color: Palette.grey,
|
top: Dimens.pt6,
|
||||||
),
|
bottom: Dimens.pt12,
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else if (comment.dead)
|
|
||||||
const Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: Dimens.pt12,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'dead',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Palette.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else if (blocklistState.blocklist.contains(comment.by))
|
|
||||||
const Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: Dimens.pt12,
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'blocked',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Palette.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
left: Dimens.pt8,
|
|
||||||
right: Dimens.pt8,
|
|
||||||
top: Dimens.pt6,
|
|
||||||
bottom: Dimens.pt12,
|
|
||||||
),
|
|
||||||
child: comment is BuildableComment
|
|
||||||
? SelectableText.rich(
|
|
||||||
key: ValueKey<int>(comment.id),
|
|
||||||
buildTextSpan(
|
|
||||||
(comment as BuildableComment).elements,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: MediaQuery.of(
|
|
||||||
context,
|
|
||||||
).textScaleFactor *
|
|
||||||
prefState.fontSize.fontSize,
|
|
||||||
),
|
|
||||||
linkStyle: TextStyle(
|
|
||||||
fontSize: MediaQuery.of(
|
|
||||||
context,
|
|
||||||
).textScaleFactor *
|
|
||||||
prefState.fontSize.fontSize,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
color: Palette.orange,
|
|
||||||
),
|
|
||||||
onOpen: (LinkableElement link) {
|
|
||||||
if (link.url.isStoryLink) {
|
|
||||||
onStoryLinkTapped.call(link.url);
|
|
||||||
} else {
|
|
||||||
LinkUtil.launch(link.url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
onTap: () => onTextTapped(context),
|
|
||||||
)
|
|
||||||
: SelectableLinkify(
|
|
||||||
key: ValueKey<int>(comment.id),
|
|
||||||
text: comment.text,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: MediaQuery.of(context)
|
|
||||||
.textScaleFactor *
|
|
||||||
prefState.fontSize.fontSize,
|
|
||||||
),
|
|
||||||
linkStyle: TextStyle(
|
|
||||||
fontSize: MediaQuery.of(context)
|
|
||||||
.textScaleFactor *
|
|
||||||
prefState.fontSize.fontSize,
|
|
||||||
color: Palette.orange,
|
|
||||||
),
|
|
||||||
onOpen: (LinkableElement link) {
|
|
||||||
if (link.url.isStoryLink) {
|
|
||||||
onStoryLinkTapped.call(link.url);
|
|
||||||
} else {
|
|
||||||
LinkUtil.launch(link.url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap: () => onTextTapped(context),
|
|
||||||
),
|
),
|
||||||
|
child: comment is BuildableComment
|
||||||
|
? SelectableText.rich(
|
||||||
|
key: ValueKey<int>(comment.id),
|
||||||
|
buildTextSpan(
|
||||||
|
(comment as BuildableComment)
|
||||||
|
.elements,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: MediaQuery.of(
|
||||||
|
context,
|
||||||
|
).textScaleFactor *
|
||||||
|
prefState.fontSize.fontSize,
|
||||||
|
),
|
||||||
|
linkStyle: TextStyle(
|
||||||
|
fontSize: MediaQuery.of(
|
||||||
|
context,
|
||||||
|
).textScaleFactor *
|
||||||
|
prefState.fontSize.fontSize,
|
||||||
|
decoration:
|
||||||
|
TextDecoration.underline,
|
||||||
|
color: Palette.orange,
|
||||||
|
),
|
||||||
|
onOpen: (LinkableElement link) {
|
||||||
|
if (link.url.isStoryLink) {
|
||||||
|
onStoryLinkTapped
|
||||||
|
.call(link.url);
|
||||||
|
} else {
|
||||||
|
LinkUtil.launch(link.url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onTap: () => onTextTapped(context),
|
||||||
|
)
|
||||||
|
: SelectableLinkify(
|
||||||
|
key: ValueKey<int>(comment.id),
|
||||||
|
text: comment.text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: MediaQuery.of(context)
|
||||||
|
.textScaleFactor *
|
||||||
|
prefState.fontSize.fontSize,
|
||||||
|
),
|
||||||
|
linkStyle: TextStyle(
|
||||||
|
fontSize: MediaQuery.of(context)
|
||||||
|
.textScaleFactor *
|
||||||
|
prefState.fontSize.fontSize,
|
||||||
|
color: Palette.orange,
|
||||||
|
),
|
||||||
|
onOpen: (LinkableElement link) {
|
||||||
|
if (link.url.isStoryLink) {
|
||||||
|
onStoryLinkTapped.call(link.url);
|
||||||
|
} else {
|
||||||
|
LinkUtil.launch(link.url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap: () => onTextTapped(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (!state.collapsed &&
|
),
|
||||||
fetchMode == FetchMode.lazy &&
|
if (_shouldShowLoadButton(context))
|
||||||
comment.kids.isNotEmpty &&
|
|
||||||
!context
|
|
||||||
.read<CommentsCubit>()
|
|
||||||
.state
|
|
||||||
.commentIds
|
|
||||||
.contains(comment.kids.first) &&
|
|
||||||
!context
|
|
||||||
.read<CommentsCubit>()
|
|
||||||
.state
|
|
||||||
.onlyShowTargetComment)
|
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@ -376,7 +335,12 @@ class CommentTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static final Map<int, Color> _colors = <int, Color>{};
|
void onTextTapped(BuildContext context) {
|
||||||
|
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
context.read<CollapseCubit>().collapse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Color _getColor(int level) {
|
Color _getColor(int level) {
|
||||||
final int initialLevel = level;
|
final int initialLevel = level;
|
||||||
@ -406,10 +370,13 @@ class CommentTile extends StatelessWidget {
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onTextTapped(BuildContext context) {
|
bool _shouldShowLoadButton(BuildContext context) {
|
||||||
if (context.read<PreferenceCubit>().state.tapAnywhereToCollapseEnabled) {
|
final CollapseState collapseState = context.read<CollapseCubit>().state;
|
||||||
HapticFeedback.selectionClick();
|
final CommentsState commentsState = context.read<CommentsCubit>().state;
|
||||||
context.read<CollapseCubit>().collapse();
|
return fetchMode == FetchMode.lazy &&
|
||||||
}
|
comment.kids.isNotEmpty &&
|
||||||
|
collapseState.collapsed == false &&
|
||||||
|
commentsState.commentIds.contains(comment.kids.first) == false &&
|
||||||
|
commentsState.onlyShowTargetComment == false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ class _CountDownReminderState extends State<CountdownReminder>
|
|||||||
.fetchStoryBy(state.storyId!)
|
.fetchStoryBy(state.storyId!)
|
||||||
.then((Story? story) {
|
.then((Story? story) {
|
||||||
if (story == null) {
|
if (story == null) {
|
||||||
showSnackBar(content: 'Something went wrong...');
|
showErrorSnackBar();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
final ItemScreenArgs args = ItemScreenArgs(item: story);
|
||||||
|
@ -5,7 +5,7 @@ import 'package:hacki/config/constants.dart';
|
|||||||
import 'package:hacki/extensions/extensions.dart';
|
import 'package:hacki/extensions/extensions.dart';
|
||||||
import 'package:hacki/models/models.dart';
|
import 'package:hacki/models/models.dart';
|
||||||
import 'package:hacki/screens/widgets/link_preview/link_view.dart';
|
import 'package:hacki/screens/widgets/link_preview/link_view.dart';
|
||||||
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ class _LinkPreviewState extends State<LinkPreview> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_errorTitle = widget.errorTitle ?? 'Something went wrong!';
|
_errorTitle = widget.errorTitle ?? Constants.errorMessage;
|
||||||
_errorBody = widget.errorBody ??
|
_errorBody = widget.errorBody ??
|
||||||
'Oops! Unable to parse the url. We have '
|
'Oops! Unable to parse the url. We have '
|
||||||
'sent feedback to our developers & '
|
'sent feedback to our developers & '
|
||||||
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hacki/blocs/blocs.dart';
|
import 'package:hacki/blocs/blocs.dart';
|
||||||
import 'package:hacki/cubits/cubits.dart';
|
import 'package:hacki/cubits/cubits.dart';
|
||||||
import 'package:hacki/screens/widgets/link_preview/web_analyzer.dart';
|
import 'package:hacki/services/services.dart';
|
||||||
import 'package:hacki/styles/styles.dart';
|
import 'package:hacki/styles/styles.dart';
|
||||||
|
|
||||||
class OfflineBanner extends StatelessWidget {
|
class OfflineBanner extends StatelessWidget {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export 'bloc_builder_3.dart';
|
export 'bloc_builder_3.dart';
|
||||||
|
export 'centered_text.dart';
|
||||||
export 'circle_tab_indicator.dart';
|
export 'circle_tab_indicator.dart';
|
||||||
export 'comment_tile.dart';
|
export 'comment_tile.dart';
|
||||||
export 'countdown_reminder.dart';
|
export 'countdown_reminder.dart';
|
||||||
|
@ -3,3 +3,4 @@ export 'custom_bloc_observer.dart';
|
|||||||
export 'fetcher.dart';
|
export 'fetcher.dart';
|
||||||
export 'firebase_client.dart';
|
export 'firebase_client.dart';
|
||||||
export 'local_notification.dart';
|
export 'local_notification.dart';
|
||||||
|
export 'web_analyzer.dart';
|
||||||
|
@ -14,7 +14,7 @@ abstract class LogUtil {
|
|||||||
colors: false,
|
colors: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
static LogOutput getLogOutput(File outputFile) => MultiOutput(
|
static LogOutput logOutput(File outputFile) => MultiOutput(
|
||||||
<LogOutput>[
|
<LogOutput>[
|
||||||
ConsoleOutput(),
|
ConsoleOutput(),
|
||||||
CustomFileOutput(
|
CustomFileOutput(
|
||||||
@ -43,7 +43,7 @@ abstract class LogUtil {
|
|||||||
|
|
||||||
final Uint8List fileContent = await currentSessionLog.readAsBytes();
|
final Uint8List fileContent = await currentSessionLog.readAsBytes();
|
||||||
await previousSessionLog.writeAsString(
|
await previousSessionLog.writeAsString(
|
||||||
'Current session logs:',
|
'Current session logs:\n',
|
||||||
mode: FileMode.append,
|
mode: FileMode.append,
|
||||||
);
|
);
|
||||||
return previousSessionLog.writeAsBytes(
|
return previousSessionLog.writeAsBytes(
|
||||||
|
@ -1358,5 +1358,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.18.0 <4.0.0"
|
dart: ">=2.18.0 <3.0.0"
|
||||||
flutter: ">=3.7.0"
|
flutter: ">=3.7.3"
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
name: hacki
|
name: hacki
|
||||||
description: A Hacker News reader.
|
description: A Hacker News reader.
|
||||||
version: 1.0.5+83
|
version: 1.0.10+88
|
||||||
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.0"
|
flutter: "3.7.3"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
adaptive_theme: ^3.0.0
|
adaptive_theme: ^3.0.0
|
||||||
|
Submodule submodules/flutter updated: b06b8b2710...9944297138
Reference in New Issue
Block a user