Skip to content

Feat: linux app by AI #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
24 changes: 24 additions & 0 deletions assets/linux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Linux Notification Icons

This directory contains PNG versions of the ICO files used by the Windows version of the application.

For Linux compatibility, please convert the following ICO files to PNG format using ImageMagick or another tool:

- loading.ico → loading.png
- offline.ico → offline.png
- network_error.ico → network_error.png
- globe.ico → globe.png
- iran.ico → iran.png
- notification.ico → notification.png

You can convert them using ImageMagick with the following command:

```bash
convert assets/loading.ico assets/linux/loading.png
convert assets/offline.ico assets/linux/offline.png
convert assets/network_error.ico assets/linux/network_error.png
convert assets/globe.ico assets/linux/globe.png
convert assets/iran.ico assets/linux/iran.png
```

After conversion, place the PNG files in this directory.
49 changes: 42 additions & 7 deletions lib/bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import 'package:ir_net/data/leak_item.dart';
import 'package:ir_net/data/shared_preferences.dart';
import 'package:ir_net/utils/cmd.dart';
import 'package:ir_net/utils/http.dart';
import 'package:ir_net/utils/platform_icons.dart';
import 'package:ir_net/utils/system_tray.dart';
import 'package:latlng/latlng.dart';
import 'package:live_event/live_event.dart';
import 'package:rxdart/rxdart.dart';
import 'package:win_toast/win_toast.dart';

import 'utils/kerio.dart';

Expand Down Expand Up @@ -77,11 +77,11 @@ class AppBloc with AppSystemTray {

void onAddLeakItemClick() async {
if (_leakInput == null || _leakInput?.trim().isEmpty == true) {
WinToast.instance().showToast(type: ToastType.text01, title: 'No input entered!');
_showNotification('No input entered!');
return;
}
if ((await AppSharedPreferences.leakChecklist).contains(_leakInput)) {
WinToast.instance().showToast(type: ToastType.text01, title: 'Repetitive input not allowed!');
_showNotification('Repetitive input not allowed!');
return;
}
await AppSharedPreferences.addToLeakChecklist(_leakInput!);
Expand All @@ -90,6 +90,39 @@ class AppBloc with AppSystemTray {
_leakInput = null;
}

void _showNotification(String message) {
if (Platform.isWindows) {
// Use Windows toast notifications
_showWindowsNotification(message);
} else {
// Use system tray notification for Linux
updateSysTrayIcon('IRNet: $message',
Platform.isLinux ? 'assets/notification.png' : 'assets/notification.ico');

// Restore icon after a delay
Future.delayed(const Duration(seconds: 3), () {
if (_foundALeakedSite) {
setSystemTrayStatusToNetworkError();
} else {
_updateCountryTrayIcon();
}
});
}
}

void _showWindowsNotification(String message) async {
try {
// Use dynamic import to avoid issues on Linux
if (Platform.isWindows) {
final winToast = await import('package:win_toast/win_toast.dart');
winToast.WinToast.instance().showToast(
type: winToast.ToastType.text01, title: message);
}
} catch (e) {
debugPrint('Error showing Windows notification: $e');
}
}

Future<void> _updateLeakChecklist() async {
_leakChecklist.value =
(await AppSharedPreferences.leakChecklist).map((e) => LeakItem(e)).toList();
Expand Down Expand Up @@ -259,7 +292,7 @@ class AppBloc with AppSystemTray {
}

if (remaining < 1073741824 && lowBalanceToastCount < 2) {
WinToast.instance().showToast(type: ToastType.text01, title: 'Less than 1 GB is left in your kerio account!');
_showNotification('Less than 1 GB is left in your kerio account!');
await AppSharedPreferences.setKerioLowBalanceToastCount(lowBalanceToastCount + 1);
await AppSharedPreferences.setKerioLowBalanceToastDate(today.toIso8601String());
}
Expand Down Expand Up @@ -337,11 +370,13 @@ class AppBloc with AppSystemTray {
} else {
tooltip = 'IRNet: $country';
}
var globIcon = 'assets/globe.ico';
String globIcon = PlatformIcons.globeIcon;
if (_foundALeakedSite && (await AppSharedPreferences.showLeakInSysTray)) {
globIcon = 'assets/globe_leaked.ico';
globIcon = Platform.isLinux ? 'assets/linux/globe_leaked.png' : 'assets/globe_leaked.ico';
}
final iconPath = isIran ? 'assets/iran.ico' : globIcon;
final iconPath = isIran ?
(Platform.isLinux ? 'assets/linux/iran.png' : 'assets/iran.ico') :
globIcon;
updateSysTrayIcon(tooltip, iconPath);
debugPrint('Country => $country');
}
Expand Down
32 changes: 22 additions & 10 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import 'package:ir_net/data/leak_item.dart';
import 'package:ir_net/data/shared_preferences.dart';
import 'package:launch_at_startup/launch_at_startup.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:win_toast/win_toast.dart';
import 'package:window_manager/window_manager.dart';
import 'package:windows_single_instance/windows_single_instance.dart';

import 'app.dart';
import 'bloc.dart';
Expand All @@ -17,8 +15,13 @@ final bloc = AppBloc();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initWindowManager();
await initSingleInstance();
await initWinToast();

// Use platform-specific initialization
if (Platform.isWindows) {
await initSingleInstance();
await initWinToast();
}

await initLaunchAtStartup();
await initSharedPreferences();
bloc.initialize();
Expand All @@ -35,16 +38,25 @@ Future<void> initWindowManager() async {
},);
}

// Only include Windows-specific imports when needed
Future<void> initSingleInstance() async {
await WindowsSingleInstance.ensureSingleInstance([], "pipeMain");
if (Platform.isWindows) {
// Import only when needed to avoid errors on Linux
await (await import('package:windows_single_instance/windows_single_instance.dart'))
.WindowsSingleInstance.ensureSingleInstance([], "pipeMain");
}
}

Future<void> initWinToast() async {
await WinToast.instance().initialize(
appName: 'IRNet',
productName: 'IRNet',
companyName: 'BuildToApp',
);
if (Platform.isWindows) {
// Import only when needed to avoid errors on Linux
await (await import('package:win_toast/win_toast.dart'))
.WinToast.instance().initialize(
appName: 'IRNet',
productName: 'IRNet',
companyName: 'BuildToApp',
);
}
}

Future<void> initSharedPreferences() async {
Expand Down
107 changes: 105 additions & 2 deletions lib/utils/cmd.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import 'dart:io';

class AppCmd {
static Future<WinRegProxyResult> getProxySettings() async {
if (Platform.isWindows) {
return _getWindowsProxySettings();
} else if (Platform.isLinux) {
return _getLinuxProxySettings();
}
return WinRegProxyResult(false, null);
}

static Future<WinRegProxyResult> _getWindowsProxySettings() async {
var executable = r'reg query "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings"';
var result = await Process.run(executable, []);
final stdout = result.stdout.toString();
Expand All @@ -24,8 +33,52 @@ class AppCmd {
return WinRegProxyResult(proxyIsEnabled, proxyServer);
}

static Future<WinRegProxyResult> _getLinuxProxySettings() async {
bool proxyEnabled = false;
String? proxyServer;

// Check gsettings for GNOME-based systems
try {
var gsettingsResult = await Process.run('gsettings', ['get', 'org.gnome.system.proxy', 'mode']);
proxyEnabled = gsettingsResult.stdout.toString().trim() == "'manual'";

if (proxyEnabled) {
var httpProxy = await Process.run('gsettings', ['get', 'org.gnome.system.proxy.http', 'host']);
var httpPort = await Process.run('gsettings', ['get', 'org.gnome.system.proxy.http', 'port']);
var host = httpProxy.stdout.toString().trim().replaceAll("'", "");
var port = httpPort.stdout.toString().trim();

if (host.isNotEmpty && port.isNotEmpty) {
proxyServer = "$host:$port";
}
}
} catch (e) {
// gsettings not available or failed
}

// Check environment variables as fallback
if (!proxyEnabled) {
var envVars = Platform.environment;
if (envVars.containsKey('http_proxy') || envVars.containsKey('HTTP_PROXY')) {
proxyEnabled = true;
proxyServer = envVars['http_proxy'] ?? envVars['HTTP_PROXY'];
}
}

return WinRegProxyResult(proxyEnabled, proxyServer);
}

static Future<LocalNetworksResult> getLocalNetworkInfo() async {
final arpAddresses = await _getArpAddresses();
if (Platform.isWindows) {
return _getWindowsNetworkInfo();
} else if (Platform.isLinux) {
return _getLinuxNetworkInfo();
}
return LocalNetworksResult([], []);
}

static Future<LocalNetworksResult> _getWindowsNetworkInfo() async {
final arpAddresses = await _getWindowsArpAddresses();
var command = await Process.run(r'ipconfig', [r'/all']);
final stdout = command.stdout.toString();
if (stdout.isEmpty) {
Expand Down Expand Up @@ -69,7 +122,57 @@ class AppCmd {
return LocalNetworksResult(interfaces, [dns1, dns2]);
}

static Future<List<String>> _getArpAddresses() async {
static Future<LocalNetworksResult> _getLinuxNetworkInfo() async {
final interfaces = <NetworkInterface>[];
final dnsServers = <String>[];

// Get network interfaces with IP addresses
try {
var ipResult = await Process.run('ip', ['addr']);
var currentInterface = '';

for (var line in ipResult.stdout.toString().split('\n')) {
if (line.startsWith(' ') && line.contains('inet ') && !line.contains('127.0.0.1')) {
// Extract IPv4 address
var parts = line.trim().split(' ');
var ipIndex = parts.indexOf('inet');
if (ipIndex >= 0 && ipIndex + 1 < parts.length) {
var ip = parts[ipIndex + 1].split('/')[0];
if (currentInterface.isNotEmpty) {
interfaces.add(NetworkInterface(currentInterface, ip));
}
}
} else if (!line.startsWith(' ') && line.contains(':')) {
// Extract interface name
var interfaceName = line.split(':')[1].trim();
if (interfaceName.isNotEmpty && !interfaceName.startsWith('lo')) {
currentInterface = interfaceName;
}
}
}
} catch (e) {
// Command failed
}

// Get DNS information from resolv.conf
try {
var dnsResult = await Process.run('cat', ['/etc/resolv.conf']);
for (var line in dnsResult.stdout.toString().split('\n')) {
if (line.startsWith('nameserver')) {
var dns = line.split('nameserver')[1].trim();
if (dns.isNotEmpty) {
dnsServers.add(dns);
}
}
}
} catch (e) {
// Command failed
}

return LocalNetworksResult(interfaces, dnsServers);
}

static Future<List<String>> _getWindowsArpAddresses() async {
final result = <String>[];
var command = await Process.run(r'arp', [r'-a']);
final stdout = command.stdout.toString();
Expand Down
16 changes: 16 additions & 0 deletions lib/utils/platform_icons.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'dart:io';

class PlatformIcons {
static String getIconPath(String baseName) {
return Platform.isLinux
? 'assets/linux/$baseName.png'
: 'assets/$baseName.ico';
}

static String get loadingIcon => getIconPath('loading');
static String get offlineIcon => getIconPath('offline');
static String get networkErrorIcon => getIconPath('network_error');
static String get globeIcon => getIconPath('globe');
static String get iranIcon => getIconPath('iran');
static String get notificationIcon => getIconPath('notification');
}
7 changes: 4 additions & 3 deletions lib/utils/system_tray.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:io';

import 'package:flutter/material.dart';
import 'package:system_tray/system_tray.dart';
import 'package:ir_net/utils/platform_icons.dart';

mixin AppSystemTray {
final SystemTray _systemTray = SystemTray();
Expand All @@ -15,12 +16,12 @@ mixin AppSystemTray {
}

void setSystemTrayStatusToOffline() {
_systemTray.setImage('assets/offline.ico');
_systemTray.setImage(PlatformIcons.offlineIcon);
_systemTray.setToolTip('IRNet: OFFLINE');
}

void setSystemTrayStatusToNetworkError() {
_systemTray.setImage('assets/network_error.ico');
_systemTray.setImage(PlatformIcons.networkErrorIcon);
_systemTray.setToolTip('IRNet: Network error');
}

Expand All @@ -31,7 +32,7 @@ mixin AppSystemTray {
Future<void> initSystemTray() async {
await _systemTray.initSystemTray(
title: "system tray",
iconPath: 'assets/loading.ico',
iconPath: PlatformIcons.loadingIcon,
);
final Menu menu = Menu();
await menu.buildFrom([
Expand Down
4 changes: 3 additions & 1 deletion linux/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ project(runner LANGUAGES CXX)
set(BINARY_NAME "ir_net")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.ir_net")
set(APPLICATION_ID "com.buildtoapp.irnet")
# Description of the application
set(APP_DESCRIPTION "Tool to show if user is connected to Iran internet or a VPN")

# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
Expand Down
Loading