Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ A [Hacker News](https://news.ycombinator.com/) client built with Flutter.
- [x] [Accent color](#thread) and [font customization](#accent-color-and-font-customization)
- [x] And more...

## Desktop Support (Linux, macOS, Windows)

Hacki can be built and run on desktop platforms. The primary motivation is Linux (including Arch Linux via the [AUR package](https://aur.archlinux.org/packages/hacki)), though macOS and Windows also benefit from the same changes.

Desktop builds are left to the reader/packager — no official distribution is provided for desktop at this time.

**Limitations on desktop — the following features are unavailable:**

| Feature | Limitation |
|---------|-----------|
| Offline reading (WebView) | Not supported on Linux; links in offline mode are silently skipped |
| Favorites import via QR code | Camera scanning unavailable; use the **From File** option instead |
| Share extension | System share intent is mobile-only |
| In-app bug report email | Use the **GitHub** option in the bug report dialog instead |
| Custom haptics | No-op on desktop |

## Home page and story tile customization
<p align="center">
<img width="200" src="assets/new_screenshots/hacki_01.png">
Expand Down
20 changes: 11 additions & 9 deletions lib/screens/home/home_screen.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:app_links/app_links.dart';
import 'package:feature_discovery/feature_discovery.dart';
Expand Down Expand Up @@ -36,8 +37,7 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin, RouteAware, ItemActionMixin, Loggable {
late final TabController tabController;
late final StreamSubscription<List<SharedMediaFile>>
intentDataStreamSubscription;
StreamSubscription<List<SharedMediaFile>>? intentDataStreamSubscription;
late final StreamSubscription<String?> notificationStreamSubscription;
late final StreamSubscription<String?> siriSuggestionStreamSubscription;
late final StreamSubscription<StoriesDownloadStatus>
Expand Down Expand Up @@ -68,13 +68,15 @@ class _HomeScreenState extends State<HomeScreen>
}
});

ReceiveSharingIntent.instance.getInitialMedia().then(
onShareExtensionTapped,
);
if (Platform.isAndroid || Platform.isIOS) {
ReceiveSharingIntent.instance.getInitialMedia().then(
onShareExtensionTapped,
);

intentDataStreamSubscription = ReceiveSharingIntent.instance
.getMediaStream()
.listen(onShareExtensionTapped);
intentDataStreamSubscription = ReceiveSharingIntent.instance
.getMediaStream()
.listen(onShareExtensionTapped);
}

if (!selectNotificationSubject.hasListener) {
notificationStreamSubscription = selectNotificationSubject.stream.listen(
Expand Down Expand Up @@ -107,7 +109,7 @@ class _HomeScreenState extends State<HomeScreen>
@override
void dispose() {
tabController.dispose();
intentDataStreamSubscription.cancel();
intentDataStreamSubscription?.cancel();
notificationStreamSubscription.cancel();
siriSuggestionStreamSubscription.cancel();
downloadStreamSubscription.cancel();
Expand Down
6 changes: 5 additions & 1 deletion lib/screens/item/item_screen.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';

import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -230,7 +231,10 @@ class _ItemScreenState extends State<ItemScreen>
.state
.isOfflineReading;
final bool shouldShowWebViewBottomSheet =
!isOfflineReading && widget.item is Story && widget.item.url.isNotEmpty;
!isOfflineReading &&
widget.item is Story &&
widget.item.url.isNotEmpty &&
!Platform.isLinux;
return MultiBlocListener(
listeners: <BlocListener<dynamic, dynamic>>[
BlocListener<PostCubit, PostState>(
Expand Down
18 changes: 11 additions & 7 deletions lib/screens/profile/qr_code_scanner_screen.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hacki/styles/styles.dart';
Expand Down Expand Up @@ -40,13 +42,15 @@ class _QrCodeScannerScreenState extends State<QrCodeScannerScreen> {
],
),
extendBodyBehindAppBar: true,
body: Column(
children: <Widget>[
Expanded(
child: QRView(key: qrKey, onQRViewCreated: onQRViewCreated),
),
],
),
body: (Platform.isAndroid || Platform.isIOS)
? Column(
children: <Widget>[
Expanded(
child: QRView(key: qrKey, onQRViewCreated: onQRViewCreated),
),
],
)
: const Center(child: Text('QR scanning is not supported on this platform.')),
);
}

Expand Down
30 changes: 16 additions & 14 deletions lib/screens/settings/settings_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -811,16 +811,17 @@ class _SettingsViewState extends State<SettingsView>
return AlertDialog(
actionsPadding: const EdgeInsets.all(Dimens.pt16),
actions: <Widget>[
ElevatedButton(
onPressed: onSendEmailTapped,
child: const Row(
children: <Widget>[
Icon(Icons.email),
SizedBox(width: Dimens.pt12),
Text('Email'),
],
if (Platform.isAndroid || Platform.isIOS)
ElevatedButton(
onPressed: onSendEmailTapped,
child: const Row(
children: <Widget>[
Icon(Icons.email),
SizedBox(width: Dimens.pt12),
Text('Email'),
],
),
),
),
ElevatedButton(
onPressed: () => onGithubTapped(context.rect),
child: const Row(
Expand Down Expand Up @@ -991,11 +992,12 @@ class _SettingsViewState extends State<SettingsView>
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: const Icon(Icons.qr_code_scanner),
title: const Text('QR Code'),
onTap: () => context.pop(ImportSource.qrCode),
),
if (Platform.isAndroid || Platform.isIOS)
ListTile(
leading: const Icon(Icons.qr_code_scanner),
title: const Text('QR Code'),
onTap: () => context.pop(ImportSource.qrCode),
),
ListTile(
leading: const Icon(Icons.file_open_outlined),
title: const Text('From File'),
Expand Down
6 changes: 4 additions & 2 deletions lib/screens/settings/widgets/offline_list_tile.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:io';

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Expand All @@ -21,7 +23,7 @@ class OfflineListTile extends StatelessWidget {
listener: (BuildContext context, StoriesState state) {
if (state.downloadStatus == StoriesDownloadStatus.failure ||
state.downloadStatus == StoriesDownloadStatus.finished) {
WakelockPlus.disable();
if (!Platform.isLinux) WakelockPlus.disable();
}
},
buildWhen: (StoriesState previous, StoriesState current) =>
Expand Down Expand Up @@ -161,7 +163,7 @@ class OfflineListTile extends StatelessWidget {
),
).then((bool? includeWebPage) {
if (includeWebPage != null) {
WakelockPlus.enable();
if (!Platform.isLinux) WakelockPlus.enable();

storiesBloc.add(StoriesDownload(includingWebPage: includeWebPage));
}
Expand Down
4 changes: 3 additions & 1 deletion lib/services/dialog_proxy.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
Expand Down Expand Up @@ -109,7 +111,7 @@ abstract final class DialogProxy {
),
).then((bool? abortDownloading) {
if (abortDownloading ?? false) {
WakelockPlus.enable();
if (!Platform.isLinux) WakelockPlus.enable();

if (context != null && context.mounted) {
context.read<StoriesBloc>().add(StoriesCancelDownload());
Expand Down
18 changes: 11 additions & 7 deletions lib/utils/link_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ abstract final class LinkUtils {
bool shouldUseHackiForHnLink = true,
}) {
if (isOfflineReading) {
locator.get<OfflineRepository>().hasCachedWebPage(url: link).then((
bool cached,
) {
if (cached) {
router.push(Paths.webView.landing, extra: link);
}
});
if (!Platform.isLinux) {
locator.get<OfflineRepository>().hasCachedWebPage(url: link).then((
bool cached,
) {
if (cached) {
router.push(Paths.webView.landing, extra: link);
}
});
}

return;
}
Expand Down Expand Up @@ -63,6 +65,8 @@ abstract final class LinkUtils {
context.read<PreferenceCubit>().state.isCustomTabEnabled ==
false) {
launchUrl(uri, mode: LaunchMode.externalApplication);
} else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
final Color primaryColor = Theme.of(context).colorScheme.primary;
_browser
Expand Down