diff --git a/assets/linux/README.md b/assets/linux/README.md new file mode 100644 index 0000000..93fcbfb --- /dev/null +++ b/assets/linux/README.md @@ -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. diff --git a/lib/bloc.dart b/lib/bloc.dart index 79ae0b8..f1f0fa0 100644 --- a/lib/bloc.dart +++ b/lib/bloc.dart @@ -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'; @@ -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!); @@ -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 _updateLeakChecklist() async { _leakChecklist.value = (await AppSharedPreferences.leakChecklist).map((e) => LeakItem(e)).toList(); @@ -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()); } @@ -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'); } diff --git a/lib/main.dart b/lib/main.dart index 30e4a5c..96f11ee 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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'; @@ -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(); @@ -35,16 +38,25 @@ Future initWindowManager() async { },); } +// Only include Windows-specific imports when needed Future 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 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 initSharedPreferences() async { diff --git a/lib/utils/cmd.dart b/lib/utils/cmd.dart index 97f96f0..d13eada 100644 --- a/lib/utils/cmd.dart +++ b/lib/utils/cmd.dart @@ -2,6 +2,15 @@ import 'dart:io'; class AppCmd { static Future getProxySettings() async { + if (Platform.isWindows) { + return _getWindowsProxySettings(); + } else if (Platform.isLinux) { + return _getLinuxProxySettings(); + } + return WinRegProxyResult(false, null); + } + + static Future _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(); @@ -24,8 +33,52 @@ class AppCmd { return WinRegProxyResult(proxyIsEnabled, proxyServer); } + static Future _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 getLocalNetworkInfo() async { - final arpAddresses = await _getArpAddresses(); + if (Platform.isWindows) { + return _getWindowsNetworkInfo(); + } else if (Platform.isLinux) { + return _getLinuxNetworkInfo(); + } + return LocalNetworksResult([], []); + } + + static Future _getWindowsNetworkInfo() async { + final arpAddresses = await _getWindowsArpAddresses(); var command = await Process.run(r'ipconfig', [r'/all']); final stdout = command.stdout.toString(); if (stdout.isEmpty) { @@ -69,7 +122,57 @@ class AppCmd { return LocalNetworksResult(interfaces, [dns1, dns2]); } - static Future> _getArpAddresses() async { + static Future _getLinuxNetworkInfo() async { + final interfaces = []; + final dnsServers = []; + + // 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> _getWindowsArpAddresses() async { final result = []; var command = await Process.run(r'arp', [r'-a']); final stdout = command.stdout.toString(); diff --git a/lib/utils/platform_icons.dart b/lib/utils/platform_icons.dart new file mode 100644 index 0000000..7fe2de2 --- /dev/null +++ b/lib/utils/platform_icons.dart @@ -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'); +} diff --git a/lib/utils/system_tray.dart b/lib/utils/system_tray.dart index 047bd27..b4c5018 100644 --- a/lib/utils/system_tray.dart +++ b/lib/utils/system_tray.dart @@ -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(); @@ -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'); } @@ -31,7 +32,7 @@ mixin AppSystemTray { Future initSystemTray() async { await _systemTray.initSystemTray( title: "system tray", - iconPath: 'assets/loading.ico', + iconPath: PlatformIcons.loadingIcon, ); final Menu menu = Menu(); await menu.buildFrom([ diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 6fa77f4..6883ce5 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -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. diff --git a/linux/README_LINUX.md b/linux/README_LINUX.md new file mode 100644 index 0000000..ecfd54f --- /dev/null +++ b/linux/README_LINUX.md @@ -0,0 +1,71 @@ +# IRNet Linux Version + +This document explains how to build and run the Linux version of IRNet. + +## Prerequisites + +- Flutter SDK 3.29.0 or newer +- Linux development tools (build-essential, CMake, Ninja, etc.) +- GTK3 development libraries +- ImageMagick (for icon conversion) + +## Building from source + +1. Install the required dependencies: + +```bash +sudo apt-get update +sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev imagemagick +``` + +2. Convert ICO files to PNG format for Linux: + +```bash +mkdir -p assets/linux +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 +``` + +3. Build the application: + +```bash +./linux/build_linux.sh +``` + +The built application will be available in `build/linux/x64/release/bundle/`. + +## Creating a Debian package + +To create a Debian package for easy installation: + +```bash +./linux/create_deb_package.sh +``` + +The package will be created at `build/linux/irnet_1.2.2_amd64.deb`. + +## Running the application + +After building, you can run the application directly: + +```bash +./build/linux/x64/release/bundle/ir_net +``` + +Or install the Debian package: + +```bash +sudo dpkg -i build/linux/irnet_1.2.2_amd64.deb +``` + +## System Tray + +The Linux version uses the same system tray functionality as the Windows version but with PNG images instead of ICO files. The application will minimize to the system tray and can be controlled from there. + +## Known Issues + +- Launch at startup functionality might require additional configuration on some Linux distributions +- System tray icon appearance may vary depending on the desktop environment diff --git a/linux/build_linux.sh b/linux/build_linux.sh new file mode 100644 index 0000000..1bee90b --- /dev/null +++ b/linux/build_linux.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Build script for IRNet Linux version + +set -e + +echo "Building IRNet for Linux..." + +# Ensure we're in the project root +cd "$(dirname "$0")/.." + +# Install dependencies +echo "Installing dependencies..." +flutter pub get + +# Convert ICO files to PNG for Linux system tray +echo "Preparing Linux assets..." +mkdir -p assets/linux + +# You'll need to manually convert ICO files to PNG format for Linux +# For example, using ImageMagick: +# 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 + +# Build the Linux application +echo "Building Linux application..." +flutter build linux --release + +echo "Build completed successfully!" +echo "The application can be found in: build/linux/x64/release/bundle/" diff --git a/linux/create_deb_package.sh b/linux/create_deb_package.sh new file mode 100644 index 0000000..5f12a7f --- /dev/null +++ b/linux/create_deb_package.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# Deployment script for IRNet Linux version + +set -e + +echo "Preparing IRNet Linux deployment..." + +# Ensure we're in the project root +cd "$(dirname "$0")/.." + +# Build the application if not already built +if [ ! -d "build/linux/x64/release/bundle" ]; then + echo "Build not found, building now..." + ./linux/build_linux.sh +fi + +# Create a Debian package structure +echo "Creating Debian package structure..." +PACKAGE_DIR="build/linux/debian_package" +mkdir -p $PACKAGE_DIR/DEBIAN +mkdir -p $PACKAGE_DIR/usr/bin +mkdir -p $PACKAGE_DIR/usr/share/applications +mkdir -p $PACKAGE_DIR/usr/share/icons/hicolor/128x128/apps + +# Copy the application files +echo "Copying application files..." +cp -r build/linux/x64/release/bundle/* $PACKAGE_DIR/usr/bin/ + +# Create a symlink for the binary +ln -sf /usr/bin/ir_net $PACKAGE_DIR/usr/bin/irnet + +# Create desktop file +echo "Creating desktop entry..." +cat > $PACKAGE_DIR/usr/share/applications/irnet.desktop << EOF +[Desktop Entry] +Name=IRNet +Comment=Check if connected to Iran internet or VPN +Exec=/usr/bin/irnet +Icon=irnet +Terminal=false +Type=Application +Categories=Network;Utility; +EOF + +# Copy icon +echo "Copying application icon..." +cp assets/app_icon.png $PACKAGE_DIR/usr/share/icons/hicolor/128x128/apps/irnet.png + +# Create control file +echo "Creating package control file..." +cat > $PACKAGE_DIR/DEBIAN/control << EOF +Package: irnet +Version: 1.2.2 +Section: net +Priority: optional +Architecture: amd64 +Depends: libgtk-3-0, libblkid1, liblzma5 +Maintainer: BuildToApp +Description: IRNet + A tool to show if user is connected to Iran internet or a VPN +EOF + +# Create postinst script +echo "Creating post-installation script..." +cat > $PACKAGE_DIR/DEBIAN/postinst << EOF +#!/bin/bash +chmod +x /usr/bin/ir_net +update-desktop-database +EOF +chmod +x $PACKAGE_DIR/DEBIAN/postinst + +# Build the Debian package +echo "Building Debian package..." +dpkg-deb --build $PACKAGE_DIR build/linux/irnet_1.2.2_amd64.deb + +echo "Debian package created: build/linux/irnet_1.2.2_amd64.deb" diff --git a/linux/my_application.cc b/linux/my_application.cc index d44c25b..959b76f 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -40,14 +40,14 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "ir_net"); + gtk_header_bar_set_title(header_bar, "IRNet: freedom does not have a price"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "ir_net"); + gtk_window_set_title(window, "IRNet: freedom does not have a price"); } - gtk_window_set_default_size(window, 1280, 720); + gtk_window_set_default_size(window, 1000, 780); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); diff --git a/pubspec.yaml b/pubspec.yaml index 6724587..5f24731 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: ir_net -description: Windows tool to show if user is connected to Iran internet or a VPN +description: Tool for Windows and Linux to show if user is connected to Iran internet or a VPN publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.2.2+5 environment: