diff --git a/README.md b/README.md index 2135b6be..05787a51 100644 --- a/README.md +++ b/README.md @@ -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

diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart index d839e590..de963c9f 100644 --- a/lib/screens/home/home_screen.dart +++ b/lib/screens/home/home_screen.dart @@ -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'; @@ -36,8 +37,7 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State with SingleTickerProviderStateMixin, RouteAware, ItemActionMixin, Loggable { late final TabController tabController; - late final StreamSubscription> - intentDataStreamSubscription; + StreamSubscription>? intentDataStreamSubscription; late final StreamSubscription notificationStreamSubscription; late final StreamSubscription siriSuggestionStreamSubscription; late final StreamSubscription @@ -68,13 +68,15 @@ class _HomeScreenState extends State } }); - 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( @@ -107,7 +109,7 @@ class _HomeScreenState extends State @override void dispose() { tabController.dispose(); - intentDataStreamSubscription.cancel(); + intentDataStreamSubscription?.cancel(); notificationStreamSubscription.cancel(); siriSuggestionStreamSubscription.cancel(); downloadStreamSubscription.cancel(); diff --git a/lib/screens/item/item_screen.dart b/lib/screens/item/item_screen.dart index ef3e697b..1eef2cf2 100644 --- a/lib/screens/item/item_screen.dart +++ b/lib/screens/item/item_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; @@ -230,7 +231,10 @@ class _ItemScreenState extends State .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( diff --git a/lib/screens/profile/qr_code_scanner_screen.dart b/lib/screens/profile/qr_code_scanner_screen.dart index ce3a9d7f..3a468d59 100644 --- a/lib/screens/profile/qr_code_scanner_screen.dart +++ b/lib/screens/profile/qr_code_scanner_screen.dart @@ -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'; @@ -40,13 +42,15 @@ class _QrCodeScannerScreenState extends State { ], ), extendBodyBehindAppBar: true, - body: Column( - children: [ - Expanded( - child: QRView(key: qrKey, onQRViewCreated: onQRViewCreated), - ), - ], - ), + body: (Platform.isAndroid || Platform.isIOS) + ? Column( + children: [ + Expanded( + child: QRView(key: qrKey, onQRViewCreated: onQRViewCreated), + ), + ], + ) + : const Center(child: Text('QR scanning is not supported on this platform.')), ); } diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index d4578446..9ef2f61a 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -811,16 +811,17 @@ class _SettingsViewState extends State return AlertDialog( actionsPadding: const EdgeInsets.all(Dimens.pt16), actions: [ - ElevatedButton( - onPressed: onSendEmailTapped, - child: const Row( - children: [ - Icon(Icons.email), - SizedBox(width: Dimens.pt12), - Text('Email'), - ], + if (Platform.isAndroid || Platform.isIOS) + ElevatedButton( + onPressed: onSendEmailTapped, + child: const Row( + children: [ + Icon(Icons.email), + SizedBox(width: Dimens.pt12), + Text('Email'), + ], + ), ), - ), ElevatedButton( onPressed: () => onGithubTapped(context.rect), child: const Row( @@ -991,11 +992,12 @@ class _SettingsViewState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - 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'), diff --git a/lib/screens/settings/widgets/offline_list_tile.dart b/lib/screens/settings/widgets/offline_list_tile.dart index f87f9e44..fa37bf28 100644 --- a/lib/screens/settings/widgets/offline_list_tile.dart +++ b/lib/screens/settings/widgets/offline_list_tile.dart @@ -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'; @@ -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) => @@ -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)); } diff --git a/lib/services/dialog_proxy.dart b/lib/services/dialog_proxy.dart index e6500332..c228ff1f 100644 --- a/lib/services/dialog_proxy.dart +++ b/lib/services/dialog_proxy.dart @@ -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'; @@ -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().add(StoriesCancelDownload()); diff --git a/lib/utils/link_utils.dart b/lib/utils/link_utils.dart index 5740896c..b62a483e 100644 --- a/lib/utils/link_utils.dart +++ b/lib/utils/link_utils.dart @@ -29,13 +29,15 @@ abstract final class LinkUtils { bool shouldUseHackiForHnLink = true, }) { if (isOfflineReading) { - locator.get().hasCachedWebPage(url: link).then(( - bool cached, - ) { - if (cached) { - router.push(Paths.webView.landing, extra: link); - } - }); + if (!Platform.isLinux) { + locator.get().hasCachedWebPage(url: link).then(( + bool cached, + ) { + if (cached) { + router.push(Paths.webView.landing, extra: link); + } + }); + } return; } @@ -63,6 +65,8 @@ abstract final class LinkUtils { context.read().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