deep linking

This commit is contained in:
mohamadmahdi jebeli 2025-07-14 08:39:34 +03:30
parent 3827209ade
commit 047de45e3b
14 changed files with 212 additions and 97 deletions

View File

@ -17,7 +17,6 @@
android:icon="@mipmap/ic_launcher"
android:label="Didvan"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true">
<receiver
android:name=".FavWidget"
@ -50,8 +49,6 @@
android:resource="@xml/provider_paths" />
</provider>
<!-- Begin FlutterDownloader customization -->
<!-- disable default Initializer -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
@ -63,18 +60,14 @@
tools:node="remove" />
</provider>
<!-- declare customized Initializer -->
<provider
android:name="vn.hunghd.flutterdownloader.FlutterDownloaderInitializer"
android:authorities="${applicationId}.flutter-downloader-init"
android:exported="false">
<!-- changes this number to configure the maximum number of concurrent tasks -->
<meta-data
android:name="vn.hunghd.flutterdownloader.MAX_CONCURRENT_TASKS"
android:value="5" />
</provider>
<!-- End FlutterDownloader customization -->
<activity android:name=".WebActivity" />
<activity
android:name=".FullscreenWebViewActivity"
@ -92,17 +85,6 @@
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!--
Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI.
-->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
@ -110,19 +92,23 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<!--
Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame.
-->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="web.didvan.com" />
</intent-filter>
</activity>
<activity

View File

@ -4,6 +4,7 @@ import 'dart:async';
import 'dart:io';
import 'package:android_intent_plus/android_intent.dart';
import 'package:app_links/app_links.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/firebase_options.dart';
@ -95,6 +96,8 @@ class Didvan extends StatefulWidget {
}
class _DidvanState extends State<Didvan> with WidgetsBindingObserver {
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
@override
void didChangeDependencies() {
super.didChangeDependencies();
@ -104,17 +107,46 @@ class _DidvanState extends State<Didvan> with WidgetsBindingObserver {
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
_initDeepLinks();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_linkSubscription?.cancel();
if (MediaService.currentPodcast != null) {
MediaService.audioPlayer.dispose();
}
super.dispose();
}
Future<void> _initDeepLinks() async {
_appLinks = AppLinks();
// بررسی لینک اولیه در زمان باز شدن اپلیکیشن
final initialUri = await _appLinks.getInitialLink();
if (initialUri != null) {
_navigateTo(initialUri);
}
// گوش دادن به لینکهای جدید زمانی که اپلیکیشن در حال اجراست
_linkSubscription = _appLinks.uriLinkStream.listen((uri) {
_navigateTo(uri);
});
}
/// تابع کمکی برای ناوبری
void _navigateTo(Uri uri) {
if (mounted) {
String path = uri.path;
if (uri.fragment.isNotEmpty) {
path = "/${uri.fragment}";
}
navigatorKey.currentState?.pushNamed(path);
}
}
bool b = true;
@override

View File

@ -1,6 +1,8 @@
// ignore_for_file: avoid_print
import 'package:didvan/models/ai/ai_chat_args.dart';
import 'package:didvan/models/requests/news.dart';
import 'package:didvan/models/requests/radar.dart';
import 'package:didvan/models/story_model.dart';
import 'package:didvan/views/ai/ai_chat_page.dart';
import 'package:didvan/views/ai/ai_chat_state.dart';
@ -74,6 +76,35 @@ import '../views/notification_time/notification_time.dart';
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
final uri = Uri.parse(settings.name ?? '');
if (uri.pathSegments.isNotEmpty) {
if (uri.pathSegments.first == 'news' && uri.pathSegments.length > 1) {
final id = int.tryParse(uri.pathSegments[1]);
if (id != null) {
return _createRoute(
ChangeNotifierProvider<NewsDetailsState>(
create: (context) => NewsDetailsState(),
child: NewsDetails(
pageData: {'id': id, 'args': const NewsRequestArgs(page: 0)},
),
),
);
}
}
if (uri.pathSegments.first == 'radar' && uri.pathSegments.length > 1) {
final id = int.tryParse(uri.pathSegments[1]);
if (id != null) {
return _createRoute(
ChangeNotifierProvider<RadarDetailsState>(
create: (context) => RadarDetailsState(),
child: RadarDetails(
pageData: {'id': id, 'args': const RadarRequestArgs(page: 0)},
),
),
);
}
}
}
if (!kIsWeb) {
HomeWidget.saveWidgetData("cRoute", settings.name!);
}

View File

@ -61,6 +61,7 @@ class RequestService {
_requestBody == null ? null : jsonEncode(_requestBody);
Future<void> httpGet() async {
log('req is: $url', name: 'RequestService');
try {
final response = await http
.get(

View File

@ -46,9 +46,14 @@ class _AuthenticationState extends State<Authentication> {
body: Consumer<AuthenticationState>(
builder: (context, state, child) => WillPopScope(
onWillPop: () async {
if (state.currentPageIndex == 0) {
if (state.currentPageIndex == 0) {
return true;
}
// Check if on OTP screen and no password exists
if (state.currentPageIndex == 2 && !state.hasPassword) {
state.currentPageIndex = 0; // Go back to username screen
return false;
}
state.currentPageIndex--;
return false;
},

View File

@ -15,6 +15,7 @@ class AuthenticationState extends CoreProvier {
String password = '';
String _verificationCode = '';
bool isShowCustomDialog = true;
bool hasPassword = true;
set currentPageIndex(int value) {
_currentPageIndex = value;
@ -36,6 +37,7 @@ class AuthenticationState extends CoreProvier {
appState = AppState.idle;
final bool hasPassword = service.result['hasPassword'];
this.hasPassword = hasPassword;
if (hasPassword) {
currentPageIndex = 1;

View File

@ -22,6 +22,7 @@ class _ResetPasswordState extends State<ResetPassword> {
@override
Widget build(BuildContext context) {
final authState = context.watch<AuthenticationState>();
return Form(
key: _formKey,
child: AuthenticationLayout(
@ -74,7 +75,7 @@ class _ResetPasswordState extends State<ResetPassword> {
}
}
},
title: 'تغییر رمز عبور',
title: authState.hasPassword ? 'تغییر رمز عبور' : 'تایید رمز عبور',
),
const SizedBox(
height: 48,

View File

@ -1,3 +1,5 @@
// lib/views/authentication/widgets/authentication_app_bar.dart
import 'package:didvan/config/theme_data.dart';
import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/views/authentication/authentication_state.dart';
@ -16,14 +18,26 @@ class AuthenticationAppBar extends StatelessWidget {
return Row(
children: [
DidvanIconButton(
icon: DidvanIcons.back_regular,
onPressed: () {
if (state.currentPageIndex == 0) {
Navigator.of(context).pop();
return;
}
state.currentPageIndex--;
}),
icon: DidvanIcons.back_regular,
onPressed: () {
// اگر در صفحه اول باشیم، از صفحه احراز هویت خارج میشویم
if (state.currentPageIndex == 0) {
Navigator.of(context).pop();
return;
}
// ** منطق جدید برای بازگشت از صفحه OTP **
// اگر کاربر رمز عبور نداشته و در صفحه OTP باشد
if (state.currentPageIndex == 2 && !state.hasPassword) {
// او را به صفحه اول (ورود شماره) برمیگردانیم
state.currentPageIndex = 0;
return;
}
// در غیر این صورت، به صفحه قبلی برمیگردیم
state.currentPageIndex--;
},
),
const SizedBox(
width: 4,
),
@ -36,4 +50,4 @@ class AuthenticationAppBar extends StatelessWidget {
],
);
}
}
}

View File

@ -43,14 +43,13 @@ class MainPageState extends CoreProvier {
try {
swotItems = await SwotService.fetchSwotItems();
} catch (e) {
// در صورت بروز خطا، میتوانید اینجا آن را مدیریت کنید
}
}
Future<void> _fetchStories() async {
try {
stories = await StoryService.getStories();
// ignore: avoid_print
print("Fetched ${stories.length} stories.");
} catch (e) {
stories = [];

View File

@ -23,14 +23,11 @@ class StorySection extends StatelessWidget {
child: _StoryCircle(
userStories: userStories,
onTap: () {
bool allStoriesViewed = stories.every((userStories) =>
userStories.stories.every((story) => story.isViewed.value));
Navigator.of(context).pushNamed(
Routes.storyViewer,
arguments: {
'stories': stories,
'tappedIndex': allStoriesViewed ? 0 : index,
'tappedIndex': index,
},
);
},
@ -50,9 +47,11 @@ class _StoryCircle extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ValueNotifier برای پیگیری وضعیت مشاهده همه استوریها
final allStoriesViewed = ValueNotifier<bool>(
userStories.stories.every((story) => story.isViewed.value));
// افزودن Listener به هر استوری
for (var story in userStories.stories) {
story.isViewed.addListener(() {
allStoriesViewed.value =
@ -65,6 +64,7 @@ class _StoryCircle extends StatelessWidget {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// استفاده از ValueListenableBuilder برای تغییر رنگ حاشیه
ValueListenableBuilder<bool>(
valueListenable: allStoriesViewed,
builder: (context, isViewed, child) {
@ -104,7 +104,7 @@ class _StoryCircle extends StatelessWidget {
child: ClipOval(
child: Image.asset(
userStories.user
.profileImageUrl,
.profileImageUrl, // Assuming this is a local asset
fit: BoxFit.cover,
width: 50.0,
height: 50.0,
@ -131,4 +131,4 @@ class _StoryCircle extends StatelessWidget {
),
);
}
}
}

View File

@ -92,14 +92,14 @@ class _UserStoryViewerState extends State<UserStoryViewer>
super.initState();
_animationController = AnimationController(vsync: this);
final allStoriesInGroupViewed =
widget.userStories.stories.every((story) => story.isViewed.value);
// final allStoriesInGroupViewed =
// widget.userStories.stories.every((story) => story.isViewed.value);
if (allStoriesInGroupViewed) {
for (final story in widget.userStories.stories) {
story.isViewed.value = false;
}
}
// if (allStoriesInGroupViewed) {
// for (final story in widget.userStories.stories) {
// story.isViewed.value = false;
// }
// }
_currentStoryIndex =
widget.userStories.stories.indexWhere((story) => !story.isViewed.value);
@ -244,12 +244,12 @@ class _UserStoryViewerState extends State<UserStoryViewer>
return CachedNetworkImage(
placeholder: (context, url) => const ShimmerPlaceholder(),
imageUrl: story.url,
fit: BoxFit.cover,
fit: BoxFit.fill,
width: double.infinity,
height: double.infinity);
case MediaType.gif:
return Image.network(story.url,
fit: BoxFit.cover, width: double.infinity, height: double.infinity);
fit: BoxFit.fill, width: double.infinity, height: double.infinity);
case MediaType.video:
if (_videoController?.value.isInitialized ?? false) {
return FittedBox(

View File

@ -4,6 +4,7 @@ import 'package:didvan/constants/app_icons.dart';
import 'package:didvan/utils/extension.dart';
import 'package:didvan/views/widgets/animated_visibility.dart';
import 'package:didvan/views/widgets/didvan/text.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -108,49 +109,51 @@ class _DidvanTextFieldState extends State<DidvanTextField> {
? TextDirection.ltr
: TextDirection.rtl,
child: Padding(
padding: const EdgeInsets.fromLTRB(8,8,0,15),
child: TextFormField(
inputFormatters: <TextInputFormatter>[
if (!widget.acceptSpace)
FilteringTextInputFormatter.allow(
RegExp("[0-9a-zA-Z\u0600-\u06FF]")),
],
autofocus: widget.autoFocus,
obscureText: _hideContent,
textAlign: widget.textAlign ?? TextAlign.start,
keyboardType: widget.textInputType,
textInputAction: widget.textInputAction,
focusNode: _focusNode,
controller: _controller,
onFieldSubmitted: widget.onSubmitted,
onChanged: _onChanged,
validator: _validator,
maxLines: _hideContent ? 1 : widget.maxLine,
minLines: widget.minLine,
maxLength: widget.maxLength,
obscuringCharacter: '*',
buildCounter: widget.showLen
? null
: (context,
{required currentLength,
required isFocused,
required maxLength}) =>
const SizedBox(),
style: (widget.isSmall
? Theme.of(context).textTheme.bodySmall!
: Theme.of(context).textTheme.bodyMedium!)
.copyWith(
fontFamily: DesignConfig.fontFamily.padRight(3)),
decoration: InputDecoration(
suffixIcon: _suffixBuilder(),
enabled: widget.enabled,
border: InputBorder.none,
hintText: widget.hintText,
errorStyle: const TextStyle(height: 0.01),
hintStyle: (widget.isSmall
padding: const EdgeInsets.fromLTRB(kIsWeb?8:8,kIsWeb?4:8,kIsWeb?8:0,kIsWeb?0:8),
child: Center(
child: TextFormField(
inputFormatters: <TextInputFormatter>[
if (!widget.acceptSpace)
FilteringTextInputFormatter.allow(
RegExp("[0-9a-zA-Z\u0600-\u06FF]")),
],
autofocus: widget.autoFocus,
obscureText: _hideContent,
textAlign: widget.textAlign ?? TextAlign.start,
keyboardType: widget.textInputType,
textInputAction: widget.textInputAction,
focusNode: _focusNode,
controller: _controller,
onFieldSubmitted: widget.onSubmitted,
onChanged: _onChanged,
validator: _validator,
maxLines: _hideContent ? 1 : widget.maxLine,
minLines: widget.minLine,
maxLength: widget.maxLength,
obscuringCharacter: '*',
buildCounter: widget.showLen
? null
: (context,
{required currentLength,
required isFocused,
required maxLength}) =>
const SizedBox(),
style: (widget.isSmall
? Theme.of(context).textTheme.bodySmall!
: Theme.of(context).textTheme.bodyMedium!)
.copyWith(color: Theme.of(context).colorScheme.hint),
.copyWith(
fontFamily: DesignConfig.fontFamily.padRight(3)),
decoration: InputDecoration(
suffixIcon: _suffixBuilder(),
enabled: widget.enabled,
border: InputBorder.none,
hintText: widget.hintText,
errorStyle: const TextStyle(height: 0.01),
hintStyle: (widget.isSmall
? Theme.of(context).textTheme.bodySmall!
: Theme.of(context).textTheme.bodyMedium!)
.copyWith(color: Theme.of(context).colorScheme.hint),
),
),
),
),

View File

@ -33,6 +33,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.8.4"
app_links:
dependency: "direct main"
description:
name: app_links
sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba"
url: "https://pub.dev"
source: hosted
version: "6.4.0"
app_links_linux:
dependency: transitive
description:
name: app_links_linux
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
url: "https://pub.dev"
source: hosted
version: "1.0.3"
app_links_platform_interface:
dependency: transitive
description:
name: app_links_platform_interface
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
app_links_web:
dependency: transitive
description:
name: app_links_web
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
url: "https://pub.dev"
source: hosted
version: "1.0.4"
args:
dependency: transitive
description:
@ -669,6 +701,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
url: "https://pub.dev"
source: hosted
version: "2.1.0"
highlight:
dependency: transitive
description:

View File

@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 4.0.1+4380
version: 4.0.3+5000
environment:
sdk: ">=2.19.0 <3.0.0"
@ -113,6 +113,7 @@ dependencies:
sms_autofill: ^2.4.1
shimmer: ^3.0.0
device_info_plus: ^11.5.0
app_links: ^6.4.0
# image_gallery_saver: ^2.0.3
# fading_edge_scrollview: ^4.1.1
dev_dependencies: