444 lines
13 KiB
Dart
444 lines
13 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import '../../services/api_service.dart';
|
|
|
|
// ایمپورت صفحات دیگر برای ناوبری
|
|
import '../../channel_list/channel_list_screen.dart';
|
|
import '../../screens/notifications_screen.dart';
|
|
import 'create_group_page.dart';
|
|
import '../../screens/about.dart';
|
|
|
|
// مدل دادهای برای آیکونهای منو
|
|
class LauncherItem {
|
|
final String label;
|
|
final IconData? icon;
|
|
final String? imagePath;
|
|
final Color color;
|
|
final VoidCallback onTap;
|
|
|
|
LauncherItem({
|
|
required this.label,
|
|
this.icon,
|
|
this.imagePath,
|
|
required this.color,
|
|
required this.onTap,
|
|
});
|
|
}
|
|
|
|
class WatchLauncher extends StatefulWidget {
|
|
const WatchLauncher({super.key});
|
|
|
|
@override
|
|
State<WatchLauncher> createState() => _WatchLauncherState();
|
|
}
|
|
|
|
class _WatchLauncherState extends State<WatchLauncher> {
|
|
// کنترلر اسکرول
|
|
final ScrollController _scrollController = ScrollController();
|
|
|
|
// ارتفاع هر آیکون
|
|
final double _itemHeight = 100.0;
|
|
|
|
// کانال ارتباطی با کد نیتیو
|
|
static final _nativeChannel = const MethodChannel(
|
|
'com.example.watch/launcher',
|
|
);
|
|
|
|
late final ApiService _api;
|
|
|
|
// لیست آیکونهای برنامه
|
|
late final List<LauncherItem> _items;
|
|
|
|
// متغیرهای برای ساعت و تاریخ داینامیک
|
|
String _currentTime = '';
|
|
String _currentDate = '';
|
|
late StreamSubscription<DateTime> _timeSubscription;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_items = _generateItems();
|
|
|
|
// استفاده از Stream برای بهینهسازی ساعت
|
|
_timeSubscription =
|
|
Stream<DateTime>.periodic(
|
|
const Duration(seconds: 1),
|
|
(count) => DateTime.now(),
|
|
).listen((dateTime) {
|
|
if (!mounted) return;
|
|
final timeString =
|
|
'${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
|
final dateString =
|
|
'${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}';
|
|
|
|
setState(() {
|
|
_currentTime = timeString;
|
|
_currentDate = dateString;
|
|
});
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollController.dispose();
|
|
_timeSubscription.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
// تولید لیست آیکونها
|
|
List<LauncherItem> _generateItems() {
|
|
return [
|
|
LauncherItem(
|
|
label: 'اعلانها',
|
|
icon: Icons.notifications_outlined,
|
|
color: const Color(0xFFFF1744),
|
|
onTap: () => _navigateTo(const NotificationsScreen()),
|
|
),
|
|
LauncherItem(
|
|
label: 'بیسیم',
|
|
icon: Icons.radio,
|
|
color: const Color(0xFF00C853),
|
|
onTap: () => _navigateTo(const ChannelListScreen()),
|
|
),
|
|
LauncherItem(
|
|
label: 'گروه جدید',
|
|
icon: Icons.add_circle_outline,
|
|
color: const Color(0xFF00C853),
|
|
onTap: _showCreateGroupDialog,
|
|
),
|
|
LauncherItem(
|
|
label: 'تلفن',
|
|
icon: Icons.phone,
|
|
color: const Color(0xFF2979FF),
|
|
onTap: _openDialer,
|
|
),
|
|
LauncherItem(
|
|
label: 'اینترنت',
|
|
icon: Icons.cell_tower,
|
|
color: const Color(0xFF00BCD4),
|
|
onTap: _openInternetSettings,
|
|
),
|
|
LauncherItem(
|
|
label: 'بلوتوث',
|
|
icon: Icons.bluetooth,
|
|
color: const Color(0xFF304FFE),
|
|
onTap: _openBluetoothSettings,
|
|
),
|
|
LauncherItem(
|
|
label: 'درباره ما',
|
|
imagePath: 'assets/images/logo.png',
|
|
color: const Color(0xFF9C27B0),
|
|
onTap: () => _navigateTo(const AboutScreen()),
|
|
),
|
|
LauncherItem(
|
|
label: 'خروج',
|
|
icon: Icons.logout,
|
|
color: Colors.redAccent,
|
|
onTap: _logout,
|
|
),
|
|
];
|
|
}
|
|
|
|
// --- متدهای کمکی ---
|
|
|
|
void _navigateTo(Widget page) {
|
|
Navigator.push(context, MaterialPageRoute(builder: (_) => page));
|
|
}
|
|
|
|
void _showSnack(String msg) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
msg,
|
|
style: const TextStyle(fontSize: 10, color: Colors.white),
|
|
),
|
|
backgroundColor: const Color(0xFF2C2C2E),
|
|
duration: const Duration(seconds: 2),
|
|
behavior: SnackBarBehavior.floating,
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 40),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _openDialer() async {
|
|
try {
|
|
await _nativeChannel.invokeMethod('openDialer');
|
|
} catch (_) {
|
|
_showSnack('خطا در باز کردن شمارهگیر');
|
|
}
|
|
}
|
|
|
|
Future<void> _openInternetSettings() async {
|
|
try {
|
|
await _nativeChannel.invokeMethod('openInternetSettings');
|
|
} catch (_) {
|
|
_showSnack('تنظیمات اینترنت در دسترس نیست');
|
|
}
|
|
}
|
|
|
|
Future<void> _openBluetoothSettings() async {
|
|
try {
|
|
await _nativeChannel.invokeMethod('openBluetoothSettings');
|
|
} catch (_) {
|
|
_showSnack('خطا در باز کردن بلوتوث');
|
|
}
|
|
}
|
|
|
|
Future<void> _logout() async {
|
|
try {
|
|
if (!mounted) return;
|
|
Navigator.pushReplacementNamed(context, '/login');
|
|
} catch (e) {
|
|
_showSnack('خطا در خروج');
|
|
}
|
|
}
|
|
|
|
Future<void> _showCreateGroupDialog() async {
|
|
final groupName = await Navigator.push<String>(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (ctx) => const CreateGroupPage(),
|
|
fullscreenDialog: true,
|
|
),
|
|
);
|
|
|
|
if (groupName != null && groupName.trim().isNotEmpty && mounted) {
|
|
try {
|
|
final newChannel = await _api.createGroup(groupName.trim());
|
|
|
|
if (!mounted) return;
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
newChannel != null ? 'گروه ساخته شد' : 'خطا در ساخت گروه',
|
|
style: const TextStyle(fontSize: 11, color: Colors.white),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
backgroundColor: const Color(0xFF2C2C2E),
|
|
behavior: SnackBarBehavior.floating,
|
|
duration: const Duration(seconds: 2),
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 40),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('خطا در ارتباط با سرور'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final double screenHeight = MediaQuery.of(context).size.height;
|
|
final double verticalPadding = (screenHeight / 2) - (_itemHeight / 2);
|
|
|
|
return Scaffold(
|
|
backgroundColor: Colors.black,
|
|
body: Stack(
|
|
children: [
|
|
// لایه اسکرول آیکونها
|
|
Positioned.fill(
|
|
child: NotificationListener<ScrollNotification>(
|
|
onNotification: (scrollNotification) {
|
|
// فقط برای Snapping نیاز به setState نیست، AnimatedBuilder کار را انجام میدهد
|
|
if (scrollNotification is ScrollEndNotification) {
|
|
_snapToItem();
|
|
}
|
|
return false;
|
|
},
|
|
child: ListView.builder(
|
|
controller: _scrollController,
|
|
physics: const BouncingScrollPhysics(),
|
|
padding: EdgeInsets.symmetric(vertical: verticalPadding),
|
|
itemCount: _items.length,
|
|
itemBuilder: (context, index) {
|
|
return _buildAnimatedItem(index);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
// ساعت در سمت چپ
|
|
Positioned(
|
|
left: 8,
|
|
top: 0,
|
|
bottom: 0,
|
|
child: Center(
|
|
child: RotatedBox(
|
|
quarterTurns: 3,
|
|
child: Text(
|
|
_currentTime,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: 1,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// تاریخ در سمت راست
|
|
Positioned(
|
|
right: 8,
|
|
top: 0,
|
|
bottom: 0,
|
|
child: Center(
|
|
child: RotatedBox(
|
|
quarterTurns: 1,
|
|
child: Text(
|
|
_currentDate,
|
|
style: const TextStyle(color: Colors.white70, fontSize: 13),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// متد ساخت آیکون با انیمیشن بهینه
|
|
Widget _buildAnimatedItem(int index) {
|
|
return AnimatedBuilder(
|
|
animation: _scrollController,
|
|
builder: (context, child) {
|
|
double scrollOffset = _scrollController.hasClients
|
|
? _scrollController.offset
|
|
: 0.0;
|
|
|
|
double itemPosition = index * _itemHeight;
|
|
double screenHeight = MediaQuery.of(context).size.height;
|
|
double centerOffset = scrollOffset + (screenHeight / 2);
|
|
double distanceFromCenter = (itemPosition - centerOffset).abs();
|
|
|
|
// محاسبات انیمیشن
|
|
double scale = (1.6 - (distanceFromCenter / 180)).clamp(0.4, 1.6);
|
|
double opacity = (1.2 - (distanceFromCenter / 250)).clamp(0.1, 1.0);
|
|
|
|
// چرخش آیکونهای دورتر برای افکت سه بعدی
|
|
double rotation = (distanceFromCenter / 1000).clamp(-0.2, 0.2);
|
|
|
|
// شدت سایه بر اساس فاصله از مرکز
|
|
double blurRadius = (20 - (distanceFromCenter / 20)).clamp(0, 20);
|
|
double shadowOpacity = (0.6 - (distanceFromCenter / 400)).clamp(
|
|
0.0,
|
|
0.6,
|
|
);
|
|
|
|
return SizedBox(
|
|
height: _itemHeight,
|
|
child: Center(
|
|
child: Transform.scale(
|
|
scale: scale,
|
|
child: Transform.rotate(
|
|
angle: rotation,
|
|
child: Opacity(
|
|
opacity: opacity,
|
|
child: Container(
|
|
// حلقه درخشان دور آیکون وقتی وسط است
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: _items[index].color.withOpacity(shadowOpacity),
|
|
blurRadius: blurRadius,
|
|
spreadRadius: 2,
|
|
),
|
|
],
|
|
),
|
|
child: _buildIconButton(_items[index]),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// متد قفل و زنجیر (Snapping)
|
|
void _snapToItem() {
|
|
if (!_scrollController.hasClients) return;
|
|
|
|
double screenHeight = MediaQuery.of(context).size.height;
|
|
double currentScroll = _scrollController.offset;
|
|
double centerOffset = currentScroll + (screenHeight / 2);
|
|
int targetIndex = (centerOffset / _itemHeight).round();
|
|
|
|
if (targetIndex < 0) targetIndex = 0;
|
|
if (targetIndex >= _items.length) targetIndex = _items.length - 1;
|
|
|
|
double targetScroll =
|
|
(targetIndex * _itemHeight) - ((screenHeight / 2) - (_itemHeight / 2));
|
|
|
|
_scrollController.animateTo(
|
|
targetScroll,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeOutCubic,
|
|
);
|
|
}
|
|
|
|
// ویجت دایرهای آیکون
|
|
Widget _buildIconButton(LauncherItem item) {
|
|
return InkWell(
|
|
onTap: item.onTap,
|
|
borderRadius: BorderRadius.circular(50),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 60,
|
|
height: 60,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: item.color,
|
|
border: Border.all(color: item.color.withOpacity(0.5), width: 2),
|
|
),
|
|
child: item.imagePath != null && item.imagePath!.isNotEmpty
|
|
? Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Image.asset(
|
|
item.imagePath!,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Icon(
|
|
item.icon ?? Icons.error_outline,
|
|
color: Colors.white,
|
|
size: 30,
|
|
);
|
|
},
|
|
),
|
|
)
|
|
: Icon(
|
|
item.icon ?? Icons.error_outline,
|
|
color: Colors.white,
|
|
size: 30,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
item.label,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|