Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"IDX.aI.enableInlineCompletion": true,
"IDX.aI.enableCodebaseIndexing": true
}
58 changes: 12 additions & 46 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:vpn_client/l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:vpn_client/pages/apps/apps_page.dart';
import 'package:vpn_client/pages/main/main_page.dart';
import 'package:vpn_client/pages/servers/servers_page.dart';
import 'package:vpn_client/pages/settings/settings_page.dart';
import 'package:vpn_client/pages/speed/speed_page.dart';
import 'package:vpn_client/providers/vpn_provider.dart';
import 'package:vpn_client/theme_provider.dart';

import 'design/colors.dart';
import 'nav_bar.dart';

void main() {
runApp(
ChangeNotifierProvider(create: (_) => ThemeProvider(), child: const App()),
);
runApp(MultiProvider(providers: [ChangeNotifierProvider(create: (_) => ThemeProvider()), ChangeNotifierProvider(create: (_) => VPNProvider())], child: const App()));
}

class App extends StatelessWidget {
Expand All @@ -22,89 +21,56 @@ class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeProvider = Provider.of<ThemeProvider>(context);
final Locale? manualLocale = null;

return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'VPN Client',
theme: lightTheme,
darkTheme: darkTheme,
locale: manualLocale,
localeResolutionCallback: (locale, supportedLocales) {
if (locale == null) return const Locale('en');
for (var supportedLocale in supportedLocales) {
if (supportedLocale.languageCode == locale.languageCode &&
(supportedLocale.countryCode == null ||
supportedLocale.countryCode == locale.countryCode)) {
return supportedLocale;
}
}
if (locale.languageCode == 'zh') {
return supportedLocales.contains(const Locale('zh'))
? const Locale('zh')
: const Locale('en');
}
return const Locale('en');
},
themeMode: themeProvider.themeMode,
home: const MainScreen(),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('ru'),
Locale('th'),
Locale('zh'),
],
);
}
}

class MainScreen extends StatefulWidget {
const MainScreen({super.key});

@override
State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
int _currentIndex = 2;
late List<Widget> _pages;

@override
void initState() {
super.initState();
_pages = [
const AppsPage(),
ServersPage(onNavBarTap: _handleNavBarTap),
const MainPage(),
const PlaceholderPage(text: 'Speed Page'),
const PlaceholderPage(text: 'Settings Page'),
const SpeedPage(),
const SettingsPage(),
];
}

void _handleNavBarTap(int index) {
setState(() {
_currentIndex = index;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_currentIndex],
bottomNavigationBar: NavBar(
initialIndex: _currentIndex,
onItemTapped: _handleNavBarTap,
selectedColor: Theme.of(context).colorScheme.primary,
),
);
}
}

class PlaceholderPage extends StatelessWidget {
final String text;
const PlaceholderPage({super.key, required this.text});
@override
Widget build(BuildContext context) {
return Center(child: Text(text));
}
}
8 changes: 8 additions & 0 deletions lib/models/nav_item.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:flutter/material.dart';

class NavItem {
final Widget inactiveIcon;
final Widget activeIcon;

NavItem({required this.inactiveIcon, required this.activeIcon});
}
84 changes: 34 additions & 50 deletions lib/nav_bar.dart
Original file line number Diff line number Diff line change
@@ -1,70 +1,54 @@
import 'package:flutter/material.dart';
import 'design/images.dart';
import 'package:vpn_client/models/nav_item.dart';

class NavBar extends StatefulWidget {
class NavBar extends StatelessWidget {
final int initialIndex;
final Function(int) onItemTapped;
final Color selectedColor;

const NavBar({super.key, this.initialIndex = 2, required this.onItemTapped});

@override
State<NavBar> createState() => NavBarState();
}

class NavBarState extends State<NavBar> {
late int _selectedIndex;

final List<Widget> _inactiveIcons = [
appIcon,
serverIcon,
homeIcon,
speedIcon,
settingsIcon,
];

final List<Widget> _activeIcons = [
activeAppIcon,
activeServerIcon,
activeHomeIcon,
speedIcon,
settingsIcon,
];

@override
void initState() {
super.initState();
_selectedIndex = widget.initialIndex;
}

void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
widget.onItemTapped(index);
}
const NavBar({
super.key,
this.initialIndex = 2,
required this.onItemTapped,
required this.selectedColor,
});

@override
Widget build(BuildContext context) {
final List<NavItem> navItems = [
NavItem(inactiveIcon: appIcon, activeIcon: activeAppIcon),
NavItem(inactiveIcon: serverIcon, activeIcon: activeServerIcon),
NavItem(inactiveIcon: homeIcon, activeIcon: activeHomeIcon),
NavItem(inactiveIcon: speedIcon, activeIcon: speedIcon),
NavItem(inactiveIcon: settingsIcon, activeIcon: settingsIcon),
];

return Container(
alignment: Alignment.center,
width: MediaQuery.of(context).size.width,
height: 60,
margin: const EdgeInsets.only(bottom: 30),
padding: const EdgeInsets.symmetric(horizontal: 30),
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface),
decoration:
BoxDecoration(color: Theme.of(context).colorScheme.surface),
child: Row(
children: List.generate(_inactiveIcons.length, (index) {
bool isActive = _selectedIndex == index;
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(navItems.length, (index) {
bool isActive = initialIndex == index;
return GestureDetector(
onTap: () => _onItemTapped(index),
child: SizedBox(
width: (MediaQuery.of(context).size.width - 60) / 5,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
padding: const EdgeInsets.all(8),
child: isActive ? _activeIcons[index] : _inactiveIcons[index],
),
onTap: () => onItemTapped(index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
padding: const EdgeInsets.all(8),
child: isActive
? ColorFiltered(
colorFilter: ColorFilter.mode(
selectedColor, BlendMode.srcIn),
child: navItems[index].activeIcon,
)
: navItems[index].inactiveIcon,
),
);
}),
Expand Down
61 changes: 54 additions & 7 deletions lib/pages/main/location_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,67 @@ import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:vpn_client/l10n/app_localizations.dart';


class LocationWidget extends StatelessWidget {
final String title;
final Map<String, dynamic>? selectedServer;
final VoidCallback? onTap;

const LocationWidget({super.key, this.selectedServer});
const LocationWidget({
super.key,
required this.title,
this.selectedServer,
this.onTap,
});

@override
Widget build(BuildContext context) {
final String locationName = selectedServer?['text'] ?? '...';
final String iconPath =
selectedServer?['icon'] ?? 'assets/images/flags/auto.svg';
final String locationName = selectedServer?['text'] ?? '...'; final String iconPath = selectedServer?['icon'] ?? 'assets/images/flags/auto.svg';

return Container(
margin: const EdgeInsets.all(30),
padding: const EdgeInsets.only(left: 14),
return GestureDetector( onTap: onTap,
child: Container(
padding: const EdgeInsets.only(left: 14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurface,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.secondary,
),
),
Text(
locationName,
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
const Spacer(),
Column(
children: [
const SizedBox(height: 20),
SvgPicture.asset(iconPath, width: 48, height: 48),
],
),
],
),
),
);
}
}
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onSurface,
borderRadius: BorderRadius.circular(12),
Expand Down
Loading
Loading