521 lines
22 KiB
Dart
521 lines
22 KiB
Dart
// ignore_for_file: deprecated_member_use
|
|
|
|
import 'package:didvan/config/design_config.dart';
|
|
import 'package:didvan/config/theme_data.dart';
|
|
import 'package:didvan/constants/assets.dart';
|
|
import 'package:didvan/models/day_time.dart';
|
|
import 'package:didvan/models/enums.dart';
|
|
import 'package:didvan/routes/routes.dart';
|
|
import 'package:didvan/views/customize_category/customize_category_state.dart';
|
|
import 'package:didvan/views/notification_time/notification_time_state.dart';
|
|
import 'package:didvan/views/widgets/didvan/button.dart';
|
|
import 'package:didvan/views/widgets/didvan/divider.dart';
|
|
import 'package:didvan/views/widgets/didvan/scaffold.dart';
|
|
import 'package:didvan/views/widgets/didvan/time_sky_animation.dart';
|
|
import 'package:didvan/views/widgets/didvan/time_slider_picker.dart';
|
|
import 'package:didvan/views/widgets/shimmer_placeholder.dart';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_svg/svg.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
class NotificationSettings extends StatefulWidget {
|
|
final Map<String, dynamic> pageData;
|
|
|
|
const NotificationSettings({super.key, required this.pageData});
|
|
|
|
@override
|
|
State<NotificationSettings> createState() => _NotificationSettingsState();
|
|
}
|
|
|
|
class _NotificationSettingsState extends State<NotificationSettings>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _animationController;
|
|
late NotificationTimeState notificationTimeState;
|
|
late CustomizeCategoryState customizeState;
|
|
bool _hasAnimated = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1200),
|
|
);
|
|
|
|
notificationTimeState = context.read<NotificationTimeState>();
|
|
customizeState = context.read<CustomizeCategoryState>();
|
|
|
|
Future.delayed(Duration.zero, () {
|
|
notificationTimeState.getTime();
|
|
customizeState.getFavourites();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _startAnimationIfNeeded() {
|
|
// Previously required BOTH providers idle. CustomizeCategoryState still
|
|
// pulls from a legacy endpoint that can hang, which starved the animation
|
|
// and left the notification section stuck at opacity 0. Now we start as
|
|
// soon as EITHER provider is ready (customize block is mostly commented
|
|
// out anyway) so the settings UI always appears.
|
|
if (!_hasAnimated &&
|
|
(customizeState.appState == AppState.idle ||
|
|
notificationTimeState.appState == AppState.idle)) {
|
|
_hasAnimated = true;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) {
|
|
_animationController.forward();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
Widget _buildAnimatedSection({required Widget child, required int index}) {
|
|
final double start = (index * 0.15).clamp(0.0, 1.0);
|
|
final double end = (start + 0.4).clamp(0.0, 1.0);
|
|
|
|
final animation = CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: Interval(start, end, curve: Curves.easeOutQuint),
|
|
);
|
|
|
|
return FadeTransition(
|
|
opacity: animation,
|
|
child: SlideTransition(
|
|
position: Tween<Offset>(
|
|
begin: const Offset(0, 0.1),
|
|
end: Offset.zero,
|
|
).animate(animation),
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
child: DidvanScaffold(
|
|
appBarData: null,
|
|
showSliversFirst: true,
|
|
slivers: [
|
|
SliverAppBar(
|
|
pinned: true,
|
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
|
automaticallyImplyLeading: false,
|
|
leadingWidth: 200,
|
|
leading: Padding(
|
|
padding: const EdgeInsetsDirectional.only(start: 0.0),
|
|
child: SvgPicture.asset(
|
|
Assets.horizontalLogoWithText,
|
|
fit: BoxFit.contain,
|
|
height: 90,
|
|
),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pushNamed(Routes.bookmarks);
|
|
},
|
|
icon: SvgPicture.asset(
|
|
'lib/assets/icons/hugeicons_telescope-01.svg',
|
|
color: DesignConfig.isDark
|
|
? Theme.of(context).colorScheme.caption
|
|
: null)),
|
|
IconButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
icon: SvgPicture.asset(
|
|
'lib/assets/icons/arrow-left.svg',
|
|
color: Theme.of(context).colorScheme.caption,
|
|
)),
|
|
const SizedBox(width: 8),
|
|
],
|
|
),
|
|
],
|
|
physics: const BouncingScrollPhysics(),
|
|
hidePlayer: true,
|
|
padding: const EdgeInsets.all(0).copyWith(bottom: 92),
|
|
backgroundColor: Theme.of(context).colorScheme.background,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 24),
|
|
child: Consumer<CustomizeCategoryState>(
|
|
builder: (context, state, child) {
|
|
if (state.appState == AppState.idle) {
|
|
_startAnimationIfNeeded();
|
|
|
|
final mediaKeywords = [
|
|
'پادکست',
|
|
'ویدئوکست',
|
|
'اینفوگرافی',
|
|
'آمار و داده'
|
|
];
|
|
|
|
final mediaFaves = state.faves.where((fav) {
|
|
return mediaKeywords.any((keyword) =>
|
|
fav.name != null && fav.name!.contains(keyword));
|
|
}).toList();
|
|
|
|
final exploreFaves = state.faves.where((fav) {
|
|
return !mediaKeywords.any((keyword) =>
|
|
fav.name != null && fav.name!.contains(keyword));
|
|
}).toList();
|
|
|
|
return _buildAnimatedSection(
|
|
index: 0,
|
|
child: const Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Padding(
|
|
// padding: const EdgeInsets.all(18.0),
|
|
// child: Column(
|
|
// mainAxisAlignment: MainAxisAlignment.start,
|
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
// children: [
|
|
// const SizedBox(height: 10),
|
|
// Text(
|
|
// 'دستهبندی اعلانها',
|
|
// style: TextStyle(
|
|
// fontSize: 16,
|
|
// fontWeight: FontWeight.bold,
|
|
// color: DesignConfig.isDark
|
|
// ? Colors.white
|
|
// : Colors.black),
|
|
// ),
|
|
// const SizedBox(height: 10),
|
|
// const Text(
|
|
// 'در این بخش میتوانید تعیین کنید از کدام دسته از محتواهای منتشرشده در اپلیکیشن دیدوان، اعلان دریافت کنید.',
|
|
// style: TextStyle(
|
|
// fontSize: 14,
|
|
// color: Color.fromARGB(255, 128, 128, 128),
|
|
// )),
|
|
// ],
|
|
// ),
|
|
// ),
|
|
// if (mediaFaves.isNotEmpty)
|
|
// _CategoryExpansionGroup(
|
|
// title: "رسانه",
|
|
// items: mediaFaves,
|
|
// state: state,
|
|
// ),
|
|
// if (exploreFaves.isNotEmpty)
|
|
// _CategoryExpansionGroup(
|
|
// title: "کاوش",
|
|
// items: exploreFaves,
|
|
// state: state,
|
|
// ),
|
|
],
|
|
),
|
|
);
|
|
} else {
|
|
return Column(
|
|
children: List.generate(
|
|
3,
|
|
(index) => Padding(
|
|
padding: const EdgeInsets.only(
|
|
bottom: 16, left: 18, right: 18, top: 16),
|
|
child: ShimmerPlaceholder(
|
|
height: 48,
|
|
width: MediaQuery.sizeOf(context).width,
|
|
borderRadius: DesignConfig.mediumBorderRadius,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 24),
|
|
child: Consumer<NotificationTimeState>(
|
|
builder: (context, state, child) {
|
|
if (state.appState == AppState.idle) {
|
|
_startAnimationIfNeeded();
|
|
}
|
|
return state.appState == AppState.idle
|
|
? _buildAnimatedSection(
|
|
index: 1,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(18.0),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
'زمانبندی دریافت اعلانها',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: DesignConfig.isDark
|
|
? Colors.white
|
|
: Colors.black),
|
|
),
|
|
const SizedBox(height: 10),
|
|
const Text(
|
|
'وقتی روشن باشد، اعلانها در ساعتی که خودتان انتخاب میکنید به شما میرسند. اگر خاموش باشد، اعلانها در همان زمانی که مدیر تعیین کرده ارسال میشوند.',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Color.fromARGB(255, 128, 128, 128),
|
|
)),
|
|
const SizedBox(height: 15),
|
|
Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'زمانبندی سفارشی',
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
color: DesignConfig.isDark
|
|
? Colors.white
|
|
: Colors.black,
|
|
),
|
|
),
|
|
),
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CupertinoSwitch(
|
|
value: state.enabled,
|
|
activeColor: Theme.of(context)
|
|
.colorScheme
|
|
.primary,
|
|
onChanged: (value) {
|
|
state.enabled = value;
|
|
state.update();
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Builder(
|
|
builder: (context) {
|
|
int hour24 =
|
|
int.tryParse(state.selectedTime.hour) ?? 12;
|
|
if (state.selectedTime.meridiem == Meridiem.PM &&
|
|
hour24 != 12) {
|
|
hour24 += 12;
|
|
}
|
|
if (state.selectedTime.meridiem == Meridiem.AM &&
|
|
hour24 == 12) {
|
|
hour24 = 0;
|
|
}
|
|
return Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 24),
|
|
child: Opacity(
|
|
opacity: state.enabled ? 1.0 : 0.4,
|
|
child: TimeSkyAnimation(hour: hour24),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
TimeSliderPicker(
|
|
selectedTime: state.selectedTime,
|
|
isDisabled: !state.enabled,
|
|
onTimeChanged: (newTime) {
|
|
if (!state.enabled) return;
|
|
state.selectedTime = newTime;
|
|
state.update();
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
Padding(
|
|
padding: const EdgeInsets.all(15.0),
|
|
child: DidvanButton(
|
|
onPressed: () {
|
|
// Note: previously also called
|
|
// CustomizeCategoryState.putFavourites, but
|
|
// the categories UI on this screen is
|
|
// commented out and the favourites endpoint
|
|
// isn't wired to the new backend yet — the
|
|
// call just triggered a "coming soon" alert
|
|
// and confused users. Save only saves the
|
|
// notification time preference now.
|
|
context
|
|
.read<NotificationTimeState>()
|
|
.putTime(context);
|
|
},
|
|
title: 'ذخیره تغییرات',
|
|
imagepath: 'lib/assets/icons/verify.svg',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: const SizedBox();
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CategoryExpansionGroup extends StatefulWidget {
|
|
final String title;
|
|
final List<dynamic> items;
|
|
final CustomizeCategoryState state;
|
|
|
|
const _CategoryExpansionGroup({
|
|
required this.title,
|
|
required this.items,
|
|
required this.state,
|
|
});
|
|
|
|
@override
|
|
State<_CategoryExpansionGroup> createState() =>
|
|
_CategoryExpansionGroupState();
|
|
}
|
|
|
|
class _CategoryExpansionGroupState extends State<_CategoryExpansionGroup> {
|
|
bool _isExpanded = true;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final expansionColor = Theme.of(context).colorScheme.focused;
|
|
final grayTextColor = Theme.of(context).colorScheme.caption;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () {
|
|
setState(() {
|
|
_isExpanded = !_isExpanded;
|
|
});
|
|
},
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: expansionColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
widget.title,
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.normal,
|
|
color: DesignConfig.isDark
|
|
? Theme.of(context).colorScheme.caption
|
|
: const Color.fromARGB(255, 41, 41, 41),
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
AnimatedRotation(
|
|
turns: _isExpanded ? 0.5 : 0,
|
|
duration: const Duration(milliseconds: 200),
|
|
child: SvgPicture.asset(
|
|
'lib/assets/icons/arrow-down.svg',
|
|
color: DesignConfig.isDark
|
|
? Theme.of(context).colorScheme.caption
|
|
: const Color.fromARGB(255, 41, 41, 41),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
AnimatedCrossFade(
|
|
duration: const Duration(milliseconds: 200),
|
|
crossFadeState: _isExpanded
|
|
? CrossFadeState.showFirst
|
|
: CrossFadeState.showSecond,
|
|
firstChild: Column(
|
|
children: [
|
|
const SizedBox(height: 12),
|
|
...widget.items.map((item) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Container(
|
|
height: 70,
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(10),
|
|
color: DesignConfig.isDark
|
|
? null
|
|
: Theme.of(context).colorScheme.surface,
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Center(
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
item.name!,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.bodyMedium
|
|
?.copyWith(
|
|
color: grayTextColor,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
),
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CupertinoSwitch(
|
|
value: item.selected ?? false,
|
|
activeColor:
|
|
Theme.of(context).colorScheme.primary,
|
|
onChanged: (value) {
|
|
setState(() {
|
|
item.selected = value;
|
|
});
|
|
widget.state.update();
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 15,
|
|
),
|
|
const DidvanDivider(
|
|
verticalPadding: 5,
|
|
)
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
],
|
|
),
|
|
secondChild: const SizedBox.shrink(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|