didvan-app/lib/services/app_home_widget/home_widget_repository.dart

785 lines
26 KiB
Dart

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<void> 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<void> decideWhereToGo() async {
String? uri =
await HomeWidget.getWidgetData<String>('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<String>("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<void> 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";
// ---------------- UUID dispatch (FIRST) ----------------
// Content from the new content-service uses UUIDs. We must decide on
// the destination BEFORE checking `link`, because some categories
// (didvanplus / video / monthly) legitimately ship with a `link` (the
// media URL) and would otherwise get sent to a generic WebView, which
// breaks Arvan signed URLs.
//
// Exception: trend / technology / startup are NOT in-app screens —
// they live on app.didvan.com and we open them in a WebView, even
// when the id is a UUID.
const _webViewTypes = {'trend', 'technology', 'startup'};
final _isWebViewType = _webViewTypes.contains(localData.type);
final _isUuidId = _isUuid(localData.id);
final _hasLink = localData.link != null &&
localData.link!.isNotEmpty &&
localData.link!.toString() != 'null';
if (_isUuidId && !_isWebViewType) {
final type = localData.type ?? '';
switch (type) {
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;
case 'infography':
case 'banner':
route = Routes.infographyDetails;
args = {
'contentUuid': localData.id,
'link': localData.link,
'goToComment': openComments,
};
break;
case 'podcast':
route = Routes.studioDetails;
args = {
'type': 'podcast',
'contentUuid': localData.id,
'goToComment': openComments,
};
break;
case 'video':
route = Routes.videoDetails;
args = {
'contentUuid': localData.id,
'goToComment': openComments,
};
break;
case 'monthly':
// PDF URL is in `link`; if present, open PdfViewer directly.
if (_hasLink) {
route = Routes.pdfViewer;
args = {
'pdfUrl': localData.link,
'title': localData.title ?? 'ماهنامه',
};
} else {
route = Routes.monthlyList;
}
break;
case 'didvanplus':
// Open the player directly with a synthesized model.
if (_hasLink) {
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 {
route = Routes.didvanPlusList;
}
break;
case 'didvanvoice':
case 'media':
// DidvanVoiceList expects List<DidvanVoiceModel>. The notification
// doesn't carry the full list, so we fetch it on the fly the same
// way the legacy int-id branch does.
try {
final service = RequestService(RequestHelper.didvanVoice);
await service.httpGet();
if (service.statusCode == 200) {
final rawData = service.data('result');
List<DidvanVoiceModel> voices = [];
if (rawData is List && rawData.isNotEmpty) {
voices = rawData
.map((e) => DidvanVoiceModel.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList();
} else if (rawData is Map) {
voices = [
DidvanVoiceModel.fromJson(
Map<String, dynamic>.from(rawData)),
];
}
if (voices.isNotEmpty) {
route = Routes.didvanVoiceList;
args = voices;
} else {
route = Routes.home;
}
} else {
route = Routes.home;
}
} catch (e) {
if (kDebugMode) {
print("Error fetching didvan voices for notification: $e");
}
route = Routes.home;
}
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 (!_hasLink) {
// Legacy int-id branch: no link, fall through to per-type detail
// pages keyed by int id (or home if no type).
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<DidvanPlusModel> videos = [];
if (rawData is List && rawData.isNotEmpty) {
videos = rawData
.map((e) => DidvanPlusModel.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList();
} else if (rawData is Map) {
videos = [
DidvanPlusModel.fromJson(
Map<String, dynamic>.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<DidvanVoiceModel> voices = [];
if (rawData is List && rawData.isNotEmpty) {
voices = rawData
.map((e) => DidvanVoiceModel.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList();
} else if (rawData is Map) {
voices = [
DidvanVoiceModel.fromJson(
Map<String, dynamic>.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;
}
}