import 'package:didvan/models/notification_message.dart'; import 'package:didvan/models/didvan_voice_model.dart'; import 'package:didvan/models/didvan_plus_model.dart'; import 'package:didvan/services/app_initalizer.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:home_widget/home_widget.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../../main.dart'; import '../../models/requests/infography.dart'; import '../../models/requests/news.dart'; import '../../models/requests/radar.dart'; import '../../models/widget_response.dart'; import '../../routes/routes.dart'; import '../network/request.dart'; import '../network/request_helper.dart'; class HomeWidgetRepository { /// Helper method to safely parse ID from notification data static int? _safeParseId(dynamic id) { if (id == null) return null; String idStr = id.toString().trim(); // Check if string contains "null" or is empty if (idStr.isEmpty || idStr == 'null' || idStr.toLowerCase().contains('null')) { return null; } try { return int.parse(idStr); } catch (e) { if (kDebugMode) { print("Failed to parse ID: $idStr - Error: $e"); } return null; } } /// True when [id] looks like a UUID (e.g. cdaa8830-d03d-420e-9921-4997a44457e9), /// which means the notification refers to a content from the new /// content-service (UUID-based) rather than a legacy int-id record. static bool _isUuid(dynamic id) { if (id == null) return false; final s = id.toString().trim(); // Loose check — 8-4-4-4-12 hex with dashes, length 36. final re = RegExp( r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', ); return re.hasMatch(s); } static Future fetchWidget() async { // RequestService.token = await StorageService.getValue(key: 'token'); final service = RequestService( RequestHelper.widgetNews(), ); await service.httpGet(); if (service.isSuccess) { final favourites = service.data('content'); HomeWidget.saveWidgetData("token", RequestService.token.toString()); for (var i = 0; i < favourites.length; i++) { HomeWidget.saveWidgetData( "id${i + 1}", WidgetResponse.fromJson(favourites[i]).id.toString()); HomeWidget.saveWidgetData("title${i + 1}", WidgetResponse.fromJson(favourites[i]).title.toString()); HomeWidget.saveWidgetData( "createdAt${i + 1}", DateTime.parse( WidgetResponse.fromJson(favourites[i]).createdAt.toString()) .toPersianDateStr()); HomeWidget.saveWidgetData("type${i + 1}", WidgetResponse.fromJson(favourites[i]).type.toString()); HomeWidget.saveWidgetData("link${i + 1}", WidgetResponse.fromJson(favourites[i]).link.toString()); HomeWidget.saveWidgetData("category${i + 1}", WidgetResponse.fromJson(favourites[i]).category.toString()); HomeWidget.saveWidgetData("image${i + 1}", WidgetResponse.fromJson(favourites[i]).image.toString()); } HomeWidget.updateWidget( androidName: "FavWidget", ); } } static Future decideWhereToGo() async { String? uri = await HomeWidget.getWidgetData('uri', defaultValue: ""); if (uri!.isNotEmpty) { int row = 0; String route = ""; dynamic args; switch (uri) { case 'setting': route = Routes.favouritesStep; args = {"toTimer": false}; break; case 'rowfirst': row = 1; break; case 'rowmiddle': row = 2; break; case 'rowlast': row = 3; break; } if (row != 0) { String? id = await HomeWidget.getWidgetData("id$row", defaultValue: ""); final parsedId = _safeParseId(id); if (parsedId == null) { if (kDebugMode) { print("Invalid widget ID: $id - cannot create WidgetResponse"); } return; } WidgetResponse data = WidgetResponse( id: parsedId, title: await HomeWidget.getWidgetData("title$row", defaultValue: ""), createdAt: await HomeWidget.getWidgetData("createdAt$row", defaultValue: ""), type: await HomeWidget.getWidgetData("type$row", defaultValue: ""), link: await HomeWidget.getWidgetData("link$row", defaultValue: ""), category: await HomeWidget.getWidgetData("category$row", defaultValue: ""), image: await HomeWidget.getWidgetData("image$row", defaultValue: ""), ); if (data.link!.startsWith('http')) { AppInitializer.openWebLink( navigatorKey.currentContext!, '${data.link}', mode: LaunchMode.inAppWebView, ); } else { switch (data.type!) { case "infography": route = Routes.infography; args = { 'id': data.id, 'args': const InfographyRequestArgs(page: 0), 'hasUnmarkConfirmation': false, }; break; case "news": route = Routes.newsDetails; args = { 'id': data.id, 'args': const NewsRequestArgs(page: 0), 'hasUnmarkConfirmation': false, }; break; case "radar": route = Routes.radarDetails; args = { 'id': data.id, 'args': const RadarRequestArgs(page: 0), 'hasUnmarkConfirmation': false, }; break; case "video": route = Routes.studioDetails; args = { 'type': 'podcast', 'id': data.id, }; break; case "podcast": route = Routes.podcasts; args = { 'type': 'podcast', 'id': data.id, }; break; } } } if (route.isNotEmpty) { Future.delayed( const Duration(milliseconds: 500), () => Navigator.of(navigatorKey.currentContext!) .pushNamed(route.toString(), arguments: args)); } } await HomeWidget.saveWidgetData("uri", ""); return; } static NotificationMessage? data; static Future decideWhereToGoNotif() async { NotificationMessage? localData = HomeWidgetRepository.data; if (localData == null) { if (kDebugMode) { print("=== NAVIGATION ABORTED ==="); print("Reason: Notification data is null"); print("==========================="); } return; } if (RequestService.token == null || RequestService.token.toString().isEmpty) { if (kDebugMode) { print("⏳ Token not loaded yet. Deferring navigation to Home/MainPage."); } return; } HomeWidgetRepository.data = null; if (kDebugMode) { print("=== NAVIGATION DECISION ==="); print("Notification Data: ${localData.toJson()}"); print("Type: ${localData.type}"); print("ID: ${localData.id}"); print("Link: ${localData.link}"); print("Notification Type: ${localData.notificationType}"); } String route = ""; dynamic args; bool openComments = localData.notificationType.toString() == "2"; if (localData.link.toString().isEmpty || localData.link.toString() == "null") { // ---------------- UUID short-circuit ---------------- // Content from the new content-service uses UUIDs. The legacy routes // below assume int IDs and can't be reached for UUID content. Route // by `type` to the matching detail screen, all of which know how to // hydrate from content-service when given `contentUuid`. // // Exception: trend / technology / startup intentionally render in // their dedicated webview — those fall through to the legacy switch // below so the URL is constructed and opened in a WebView even when // the id is a UUID. const _webViewTypes = {'trend', 'technology', 'startup'}; final _isWebViewType = _webViewTypes.contains(localData.type); if (_isUuid(localData.id) && !_isWebViewType) { // Per-category routing for new (UUID-based) content. Each branch // sends the user to that category's native detail screen, all of // which can hydrate from content-service via `contentUuid`. // // Categories whose detail screen isn't UUID-aware yet fall through // to InfographyDetails (the generic content reader). When you wire // a new screen, add another case here. final type = localData.type ?? ''; switch (type) { // Hard news / story / poyesh / swot all live in the NewsDetails // pager — same UI, different category filter. case 'news': case 'story': case 'poyesh': case 'swot': route = Routes.newsDetails; args = { 'contentUuid': localData.id, 'args': const NewsRequestArgs(page: 0), 'link': localData.link, 'goToComment': openComments, }; break; // Infography & banner: the existing InfographyDetails already // knows how to load by UUID. case 'infography': case 'banner': route = Routes.infographyDetails; args = { 'contentUuid': localData.id, 'link': localData.link, 'goToComment': openComments, }; break; // Podcast detail page. case 'podcast': route = Routes.studioDetails; args = { 'type': 'podcast', 'contentUuid': localData.id, 'goToComment': openComments, }; break; // Video / video-cast detail page. case 'video': route = Routes.videoDetails; args = { 'contentUuid': localData.id, 'goToComment': openComments, }; break; // Monthly notifications normally arrive with `link` populated and // go to PDFViewer in the `else` branch below. Reaching here means // there's no PDF URL yet — show the list so the user can browse. case 'monthly': route = Routes.monthlyList; break; case 'didvanplus': // Open the player directly. We don't have a full DidvanPlusModel // here (no record in the legacy DB), so we synthesize one from // the notification payload — the player only reads file/image/ // title/description. if (localData.link != null && localData.link!.isNotEmpty) { route = Routes.didvanPlusVideo; args = DidvanPlusModel( id: 0, // unused for navigation title: localData.title ?? '', description: localData.body ?? '', image: localData.imageUrl ?? '', file: localData.link!, publishedAt: DateTime.now().toIso8601String(), ); } else { // Fallback: no video URL → just show the list. route = Routes.didvanPlusList; } break; case 'didvanvoice': case 'media': route = Routes.didvanVoiceList; break; // Unknown type with a UUID — best effort: generic reader. default: if (kDebugMode) { print("UUID with unmapped type='$type' — defaulting to " "InfographyDetails"); } route = Routes.infographyDetails; args = { 'contentUuid': localData.id, 'link': localData.link, 'goToComment': openComments, }; } if (kDebugMode) { print("UUID notification routed: type=$type → route=$route"); } } else if (localData.type == null || localData.type!.isEmpty) { if (kDebugMode) { print("=== NAVIGATION ABORTED ==="); print("Reason: Notification type is null or empty"); print("Defaulting to home route"); print("==========================="); } route = Routes.home; } else { switch (localData.type!) { case "infography": final infographyId = _safeParseId(localData.id); if (infographyId == null) { if (kDebugMode) { print( "WARNING: Infography notification without valid ID - navigating to home"); } route = Routes.home; } else { route = Routes.infography; args = { 'id': infographyId, 'args': const InfographyRequestArgs(page: 0), 'hasUnmarkConfirmation': false, 'goToComment': openComments }; } break; case "news": final newsId = _safeParseId(localData.id); if (newsId == null) { if (kDebugMode) { print( "WARNING: News notification without valid ID - navigating to home"); } route = Routes.home; } else { route = Routes.newsDetails; args = { 'id': newsId, 'args': const NewsRequestArgs(page: 0), 'hasUnmarkConfirmation': false, 'goToComment': openComments }; if (kDebugMode) { print("News navigation - ID: $newsId"); } } break; case "radar": final radarId = _safeParseId(localData.id); if (radarId == null) { if (kDebugMode) { print( "WARNING: Radar notification without valid ID - navigating to home"); } route = Routes.home; } else { route = Routes.radarDetails; args = { 'id': radarId, 'args': const RadarRequestArgs(page: 0), 'hasUnmarkConfirmation': false, 'goToComment': openComments }; } break; case "studio": final studioId = _safeParseId(localData.id); if (studioId == null) { if (kDebugMode) { print( "WARNING: Studio notification without valid ID - navigating to home"); } route = Routes.home; } else { route = Routes.studioDetails; args = { 'type': 'podcast', 'id': studioId, 'goToComment': openComments }; } break; case "video": final videoId = _safeParseId(localData.id); if (videoId == null) { if (kDebugMode) { print( "WARNING: Video notification without valid ID - navigating to home"); } route = Routes.home; } else { route = Routes.studioDetails; args = { 'type': 'podcast', 'id': videoId, 'goToComment': openComments }; } break; case "podcast": final podcastId = _safeParseId(localData.id); if (podcastId == null) { if (kDebugMode) { print( "WARNING: Podcast notification without valid ID - navigating to home"); } route = Routes.home; } else { route = Routes.podcasts; args = { 'type': 'podcast', 'id': podcastId, 'goToComment': openComments }; } break; case "monthly": route = Routes.monthlyList; break; case "didvanplus": case "didvan_plus": case "didvanPlus": if (kDebugMode) { print( "🎬 Fetching didvan plus list for notification navigation..."); } try { final service = RequestService(RequestHelper.didvanPlus); await service.httpGet(); if (kDebugMode) { print("🎬 DidvanPlus API Response - Status: ${service.statusCode}"); print("🎬 DidvanPlus API Response - Success: ${service.isSuccess}"); } if (service.statusCode == 200) { final rawData = service.data('result'); if (kDebugMode) { print("🎬 Raw Data Type: ${rawData.runtimeType}"); print("🎬 Raw Data: $rawData"); } List videos = []; if (rawData is List && rawData.isNotEmpty) { videos = rawData .map((e) => DidvanPlusModel.fromJson( Map.from(e as Map))) .toList(); } else if (rawData is Map) { videos = [ DidvanPlusModel.fromJson( Map.from(rawData)) ]; } if (kDebugMode) { print("🎬 Parsed ${videos.length} didvan plus videos"); for (var video in videos) { print(" - ${video.title} (ID: ${video.id})"); } } if (videos.isNotEmpty) { route = Routes.didvanPlusList; args = videos; if (kDebugMode) { print( "🎬 ✅ Successfully prepared navigation with ${videos.length} videos"); print("🎬 Args type: ${args.runtimeType}"); } } else { route = Routes.home; if (kDebugMode) { print("🎬 ⚠️ No didvan plus videos found, navigating to home"); } } } else { route = Routes.home; if (kDebugMode) { print( "🎬 ❌ Failed to fetch didvan plus (status: ${service.statusCode}), navigating to home"); } } } catch (e, stackTrace) { route = Routes.home; if (kDebugMode) { print("🎬 ❌ Error fetching didvan plus: $e"); print("🎬 Stack trace: $stackTrace"); } } break; case "didvanvoice": case "didvan_voice": case "didvan_Voice": case "didvanVoice": case "media": if (kDebugMode) { print( "Fetching didvan voice list for notification navigation..."); } try { final service = RequestService(RequestHelper.didvanVoice); await service.httpGet(); if (service.statusCode == 200) { final rawData = service.data('result'); List voices = []; if (rawData is List && rawData.isNotEmpty) { voices = rawData .map((e) => DidvanVoiceModel.fromJson( Map.from(e as Map))) .toList(); } else if (rawData is Map) { voices = [ DidvanVoiceModel.fromJson( Map.from(rawData)) ]; } if (voices.isNotEmpty) { route = Routes.didvanVoiceList; args = voices; if (kDebugMode) { print( "Successfully fetched ${voices.length} didvan voices"); } } else { route = Routes.home; if (kDebugMode) { print("No didvan voices found, navigating to home"); } } } else { route = Routes.home; if (kDebugMode) { print( "Failed to fetch didvan voices (status: ${service.statusCode}), navigating to home"); } } } catch (e) { route = Routes.home; if (kDebugMode) { print("Error fetching didvan voices: $e, navigating to home"); } } break; case "startup": case "technology": case "trend": if (localData.link != null && localData.link!.isNotEmpty && localData.link! != "null") { if (kDebugMode) { print( "Opening external link for ${localData.type}: ${localData.link}"); } AppInitializer.openWebLink( navigatorKey.currentContext!, localData.link!, mode: LaunchMode.inAppWebView, ); } else if (localData.id != null && localData.id.toString().isNotEmpty) { String url = ""; // All three apps now live on app.didvan.com (the Next.js // frontend). The old subdomain URLs (trend.didvan.app, …) // are obsolete. The Next.js routes are a single `[id]` // segment — no title slug — so we only append the id. switch (localData.type) { case "startup": url = "https://app.didvan.com/startup/${localData.id}?accessToken=${RequestService.token}"; break; case "technology": url = "https://app.didvan.com/technology/${localData.id}?accessToken=${RequestService.token}"; break; case "trend": url = "https://app.didvan.com/trend/${localData.id}?accessToken=${RequestService.token}"; break; } if (url.isNotEmpty) { if (kDebugMode) { print("Opening constructed URL for ${localData.type}: $url"); } AppInitializer.openWebLink( navigatorKey.currentContext!, url, mode: LaunchMode.inAppWebView, ); } else { route = Routes.home; if (kDebugMode) { print( "Unable to construct URL for ${localData.type} - navigating to home"); } } } else { route = Routes.home; if (kDebugMode) { print( "No ID or link available for ${localData.type} - navigating to home"); } } break; default: route = Routes.home; if (kDebugMode) { print( "Unknown notification type: ${localData.type} - navigating to home"); } break; } } } else { if (kDebugMode) { print("External link detected: ${localData.link}"); } if (localData.type == 'monthly') { route = Routes.pdfViewer; args = { 'pdfUrl': localData.link, 'title': localData.title ?? 'ماهنامه', }; } else if (localData.link!.startsWith('http')) { String linkWithToken = localData.link!; if (RequestService.token != null && RequestService.token!.isNotEmpty) { String separator = localData.link!.contains('?') ? '&' : '?'; linkWithToken = "${localData.link}${separator}accessToken=${RequestService.token}"; } if (kDebugMode) { print("Opening external link with token: $linkWithToken"); } AppInitializer.openWebLink( navigatorKey.currentContext!, linkWithToken, mode: LaunchMode.inAppWebView, ); } } if (kDebugMode) { print("Final navigation decision:"); print("Route: $route"); print("Args type: ${args.runtimeType}"); if (args is List) { print("Args list length: ${args.length}"); } print("Args: $args"); print("==========================="); } if (route.isNotEmpty) { await Future.delayed(const Duration(milliseconds: 1000)); if (kDebugMode) { print("Attempting navigation after delay..."); print("Navigator ready: ${navigatorKey.currentState != null}"); } int retryCount = 0; while (navigatorKey.currentState == null && retryCount < 10) { if (kDebugMode) { print( "Navigator not ready, waiting... (attempt ${retryCount + 1}/10)"); } await Future.delayed(const Duration(milliseconds: 500)); retryCount++; } if (navigatorKey.currentState != null) { if (kDebugMode) { print("Navigator is ready, performing navigation to: $route"); print("With arguments type: ${args.runtimeType}"); } navigatorKey.currentState!.pushNamed(route, arguments: args); } else { if (kDebugMode) { print( "ERROR: Navigator still not ready after waiting. Navigation aborted."); } } } return; } }