diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 73b4bfce..1630b743 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -806,11 +806,11 @@ abstract class AppLocalizations { /// **'Feedback submitted successfully!'** String get contactUsSuccess; - /// No description provided for @contactUsShortcut. + /// No description provided for @hideNewsIcon. /// /// In en, this message translates to: - /// **'Hide Feedback Icon'** - String get contactUsShortcut; + /// **'Hide News Icon'** + String get hideNewsIcon; /// No description provided for @settingsHelpTerms. /// diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 37df22b6..cee06bc0 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -386,7 +386,7 @@ class AppLocalizationsDe extends AppLocalizations { String get contactUsSuccess => 'Feedback erfolgreich übermittelt!'; @override - String get contactUsShortcut => 'Feedback-Symbol ausblenden'; + String get hideNewsIcon => 'Neuigkeiten-Symbol ausblenden'; @override String get settingsHelpTerms => 'Nutzungsbedingungen'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index c87ff548..fa36c5c3 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -382,7 +382,7 @@ class AppLocalizationsEn extends AppLocalizations { String get contactUsSuccess => 'Feedback submitted successfully!'; @override - String get contactUsShortcut => 'Hide Feedback Icon'; + String get hideNewsIcon => 'Hide News Icon'; @override String get settingsHelpTerms => 'Terms of Service'; diff --git a/lib/src/localization/translations b/lib/src/localization/translations index bb353aa5..1727cd6d 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit bb353aa572623d20578f470d2c683ead79669ed8 +Subproject commit 1727cd6d4f2c56f976883497c182b1456033b614 diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index 8ee4b648..8c3e3fe0 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -72,7 +72,7 @@ class UserData { bool videoStabilizationEnabled = true; @JsonKey(defaultValue: true) - bool showFeedbackShortcut = true; + bool showNewsShortcut = true; @JsonKey(defaultValue: false) bool showShowImagePreviewWhenSending = false; diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index 08825f09..cf2b4b9f 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -41,7 +41,7 @@ UserData _$UserDataFromJson(Map json) => json['automaticallyMarkEqualMediaFilesAsOpened'] as bool? ?? false ..videoStabilizationEnabled = json['videoStabilizationEnabled'] as bool? ?? true - ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true + ..showNewsShortcut = json['showNewsShortcut'] as bool? ?? true ..showShowImagePreviewWhenSending = json['showShowImagePreviewWhenSending'] as bool? ?? false ..startWithCameraOpen = json['startWithCameraOpen'] as bool? ?? true @@ -142,7 +142,7 @@ Map _$UserDataToJson(UserData instance) => { 'automaticallyMarkEqualMediaFilesAsOpened': instance.automaticallyMarkEqualMediaFilesAsOpened, 'videoStabilizationEnabled': instance.videoStabilizationEnabled, - 'showFeedbackShortcut': instance.showFeedbackShortcut, + 'showNewsShortcut': instance.showNewsShortcut, 'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending, 'startWithCameraOpen': instance.startWithCameraOpen, 'preSelectedEmojies': instance.preSelectedEmojies, diff --git a/lib/src/services/news.service.dart b/lib/src/services/news.service.dart index 116c9638..63c00b93 100644 --- a/lib/src/services/news.service.dart +++ b/lib/src/services/news.service.dart @@ -18,13 +18,15 @@ class BlogEntry { }); factory BlogEntry.fromJson(Map json) => BlogEntry( - title: (json['title'] as String?) ?? '', - link: (json['link'] as String?) ?? '', - guid: (json['guid'] as String?) ?? '', - description: (json['description'] as String?) ?? '', - imageUrl: (json['imageUrl'] as String?) ?? '', - pubDate: json['pubDate'] != null ? DateTime.tryParse(json['pubDate'] as String) : null, - ); + title: (json['title'] as String?) ?? '', + link: (json['link'] as String?) ?? '', + guid: (json['guid'] as String?) ?? '', + description: (json['description'] as String?) ?? '', + imageUrl: (json['imageUrl'] as String?) ?? '', + pubDate: json['pubDate'] != null + ? DateTime.tryParse(json['pubDate'] as String) + : null, + ); final String title; final String link; @@ -34,13 +36,13 @@ class BlogEntry { final DateTime? pubDate; Map toJson() => { - 'title': title, - 'link': link, - 'guid': guid, - 'description': description, - 'imageUrl': imageUrl, - 'pubDate': pubDate?.toIso8601String(), - }; + 'title': title, + 'link': link, + 'guid': guid, + 'description': description, + 'imageUrl': imageUrl, + 'pubDate': pubDate?.toIso8601String(), + }; @override String toString() { @@ -56,7 +58,8 @@ class NewsService { final ValueNotifier unreadCountNotifier = ValueNotifier(0); - File get _cacheFile => File(join(AppEnvironment.supportDir, 'news_feed.json')); + File get _cacheFile => + File(join(AppEnvironment.supportDir, 'news_feed.json')); Future init() async { try { @@ -152,7 +155,9 @@ class NewsService { _updateUnreadCount(); await saveCache(); - Log.info('Successfully fetched ${entries.length} news entries. Unread count: ${unreadCountNotifier.value}'); + Log.info( + 'Successfully fetched ${entries.length} news entries. Unread count: ${unreadCountNotifier.value}', + ); } catch (e) { Log.error('Failed to fetch news feed: $e'); } @@ -166,32 +171,54 @@ class NewsService { for (final match in matches) { final itemContent = match.group(1) ?? ''; - final titleMatch = RegExp(r'([\s\S]*?)<\/title>').firstMatch(itemContent); + final titleMatch = RegExp( + r'<title>([\s\S]*?)<\/title>', + ).firstMatch(itemContent); final title = _stripCdata(titleMatch?.group(1) ?? ''); - final linkMatch = RegExp(r'<link>([\s\S]*?)<\/link>').firstMatch(itemContent); + final linkMatch = RegExp( + r'<link>([\s\S]*?)<\/link>', + ).firstMatch(itemContent); final link = _stripCdata(linkMatch?.group(1) ?? ''); - final guidMatch = RegExp(r'<guid[^>]*?>([\s\S]*?)<\/guid>').firstMatch(itemContent); + final guidMatch = RegExp( + r'<guid[^>]*?>([\s\S]*?)<\/guid>', + ).firstMatch(itemContent); final guid = _stripCdata(guidMatch?.group(1) ?? link); - final descMatch = RegExp(r'<description>([\s\S]*?)<\/description>').firstMatch(itemContent); + final descMatch = RegExp( + r'<description>([\s\S]*?)<\/description>', + ).firstMatch(itemContent); final descriptionRaw = _stripCdata(descMatch?.group(1) ?? ''); - final description = descriptionRaw.replaceAll(RegExp('<[^>]*>'), ''); + final description = descriptionRaw + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('&', '&') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(''', "'") + .replaceAll(RegExp('<[^>]*>'), '') + .trim(); var imageUrl = ''; - final enclosureMatch = RegExp(r'<enclosure\s+[^>]*?url="([^"]+)"').firstMatch(itemContent); + final enclosureMatch = RegExp( + r'<enclosure\s+[^>]*?url="([^"]+)"', + ).firstMatch(itemContent); if (enclosureMatch != null) { imageUrl = enclosureMatch.group(1) ?? ''; } if (imageUrl.isEmpty) { - final mediaMatch = RegExp(r'<media:content\s+[^>]*?url="([^"]+)"').firstMatch(itemContent); + final mediaMatch = RegExp( + r'<media:content\s+[^>]*?url="([^"]+)"', + ).firstMatch(itemContent); if (mediaMatch != null) { imageUrl = mediaMatch.group(1) ?? ''; } } - final pubDateMatch = RegExp(r'<pubDate>([\s\S]*?)<\/pubDate>').firstMatch(itemContent); + final pubDateMatch = RegExp( + r'<pubDate>([\s\S]*?)<\/pubDate>', + ).firstMatch(itemContent); DateTime? pubDate; if (pubDateMatch != null) { final pubDateStr = pubDateMatch.group(1) ?? ''; @@ -199,14 +226,16 @@ class NewsService { pubDate ??= _parseRfc2822(pubDateStr); } - entries.add(BlogEntry( - title: title, - link: link, - guid: guid, - description: description, - imageUrl: imageUrl, - pubDate: pubDate, - )); + entries.add( + BlogEntry( + title: title, + link: link, + guid: guid, + description: description, + imageUrl: imageUrl, + pubDate: pubDate, + ), + ); } return entries; @@ -238,14 +267,30 @@ class NewsService { final timeParts = parts[3].split(':'); final hour = timeParts.isNotEmpty ? (int.tryParse(timeParts[0]) ?? 0) : 0; - final minute = timeParts.length > 1 ? (int.tryParse(timeParts[1]) ?? 0) : 0; - final second = timeParts.length > 2 ? (int.tryParse(timeParts[2]) ?? 0) : 0; + final minute = timeParts.length > 1 + ? (int.tryParse(timeParts[1]) ?? 0) + : 0; + final second = timeParts.length > 2 + ? (int.tryParse(timeParts[2]) ?? 0) + : 0; const months = { - 'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, - 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12 + 'jan': 1, + 'feb': 2, + 'mar': 3, + 'apr': 4, + 'may': 5, + 'jun': 6, + 'jul': 7, + 'aug': 8, + 'sep': 9, + 'oct': 10, + 'nov': 11, + 'dec': 12, }; - final monthStringPart = monthStr.length > 3 ? monthStr.substring(0, 3) : monthStr; + final monthStringPart = monthStr.length > 3 + ? monthStr.substring(0, 3) + : monthStr; final month = months[monthStringPart] ?? 1; return DateTime.utc(year, month, day, hour, minute, second); diff --git a/lib/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart b/lib/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart deleted file mode 100644 index 498af280..00000000 --- a/lib/src/visual/views/chats/chat_list_components/feedback_btn.comp.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:go_router/go_router.dart'; -import 'package:twonly/locator.dart'; -import 'package:twonly/src/constants/routes.keys.dart'; -import 'package:twonly/src/utils/misc.dart'; - -class FeedbackIconButtonComp extends StatelessWidget { - const FeedbackIconButtonComp({super.key}); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: userService.onUserUpdated, - builder: (context, asyncSnapshot) { - if (!userService.currentUser.showFeedbackShortcut) { - return const SizedBox.shrink(); - } - return IconButton( - onPressed: () => context.push(Routes.settingsHelpContactUs), - color: Colors.grey, - tooltip: context.lang.feedbackTooltip, - icon: const FaIcon(FontAwesomeIcons.commentDots, size: 19), - ); - }, - ); - } -} diff --git a/lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart b/lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart index a914bac3..05ba9752 100644 --- a/lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart +++ b/lib/src/visual/views/chats/chat_list_components/news_btn.comp.dart @@ -5,23 +5,34 @@ import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/notification_badge.comp.dart'; +import 'package:twonly/src/visual/themes/light.dart'; class NewsIconButtonComp extends StatelessWidget { const NewsIconButtonComp({super.key}); @override Widget build(BuildContext context) { - return ValueListenableBuilder<int>( - valueListenable: newsService.unreadCountNotifier, - builder: (context, count, child) { - return NotificationBadgeComp( - count: count.toString(), - child: IconButton( - onPressed: () => context.push(Routes.settingsHelpNews), - color: Colors.grey, - tooltip: context.lang.settingsHelpNews, - icon: const FaIcon(FontAwesomeIcons.newspaper, size: 19), - ), + return StreamBuilder( + stream: userService.onUserUpdated, + builder: (context, snapshot) { + if (!userService.currentUser.showNewsShortcut) { + return const SizedBox.shrink(); + } + return ValueListenableBuilder<int>( + valueListenable: newsService.unreadCountNotifier, + builder: (context, count, child) { + return NotificationBadgeComp( + count: count.toString(), + backgroundColor: primaryColor, + textColor: Colors.black87, + child: IconButton( + onPressed: () => context.push(Routes.settingsHelpNews), + color: Colors.grey, + tooltip: context.lang.settingsHelpNews, + icon: const FaIcon(FontAwesomeIcons.bullhorn, size: 19), + ), + ); + }, ); }, ); diff --git a/lib/src/visual/views/settings/appearance.view.dart b/lib/src/visual/views/settings/appearance.view.dart index e01051be..4caa5a0a 100644 --- a/lib/src/visual/views/settings/appearance.view.dart +++ b/lib/src/visual/views/settings/appearance.view.dart @@ -72,9 +72,9 @@ class _AppearanceViewState extends State<AppearanceView> { } } - Future<void> toggleShowFeedbackIcon() async { + Future<void> toggleShowNewsIcon() async { await UserService.update((u) { - u.showFeedbackShortcut = !u.showFeedbackShortcut; + u.showNewsShortcut = !u.showNewsShortcut; }); } @@ -113,11 +113,11 @@ class _AppearanceViewState extends State<AppearanceView> { }, ), ListTile( - title: Text(context.lang.contactUsShortcut), - onTap: toggleShowFeedbackIcon, + title: Text(context.lang.hideNewsIcon), + onTap: toggleShowNewsIcon, trailing: Switch.adaptive( - value: !userService.currentUser.showFeedbackShortcut, - onChanged: (a) => toggleShowFeedbackIcon(), + value: !userService.currentUser.showNewsShortcut, + onChanged: (a) => toggleShowNewsIcon(), ), ), ListTile( diff --git a/lib/src/visual/views/settings/help/news.view.dart b/lib/src/visual/views/settings/help/news.view.dart index f65c6f7a..fcc59eb8 100644 --- a/lib/src/visual/views/settings/help/news.view.dart +++ b/lib/src/visual/views/settings/help/news.view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart'; import 'package:url_launcher/url_launcher.dart'; class NewsView extends StatefulWidget { @@ -33,8 +34,8 @@ class _NewsViewState extends State<NewsView> { child: Text( 'No news articles found.', style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Colors.grey, - ), + color: Colors.grey, + ), ), ) : ListView.builder( @@ -42,17 +43,23 @@ class _NewsViewState extends State<NewsView> { itemCount: entries.length, itemBuilder: (context, index) { final entry = entries[index]; - return Card( - clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + final isDark = isDarkMode(context); + + return ReactiveTapFeedback( + onTap: () => launchUrl( + Uri.parse(entry.link), + mode: LaunchMode.externalApplication, ), - elevation: 2, - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: InkWell( - onTap: () => launchUrl( - Uri.parse(entry.link), - mode: LaunchMode.externalApplication, + child: Card( + color: isDark ? Colors.grey[800] : Colors.grey[200], + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 0, + margin: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -65,14 +72,15 @@ class _NewsViewState extends State<NewsView> { placeholder: (context, url) => Container( height: 180, color: Colors.grey.withValues(alpha: 0.1), - child: const Center( - child: CircularProgressIndicator(), - ), ), errorWidget: (context, url, error) => Container( height: 180, color: Colors.grey.withValues(alpha: 0.1), - child: const Icon(Icons.broken_image, size: 50, color: Colors.grey), + child: const Icon( + Icons.broken_image, + size: 50, + color: Colors.grey, + ), ), ), Padding( @@ -85,7 +93,8 @@ class _NewsViewState extends State<NewsView> { DateFormat.yMMMMd( Localizations.localeOf(context).toString(), ).format(entry.pubDate!), - style: Theme.of(context).textTheme.bodySmall?.copyWith( + style: Theme.of(context).textTheme.bodySmall + ?.copyWith( color: Colors.grey, ), ), @@ -93,7 +102,8 @@ class _NewsViewState extends State<NewsView> { ], Text( entry.title, - style: Theme.of(context).textTheme.titleLarge?.copyWith( + style: Theme.of(context).textTheme.titleLarge + ?.copyWith( fontWeight: FontWeight.bold, ), ), @@ -102,8 +112,11 @@ class _NewsViewState extends State<NewsView> { entry.description, maxLines: 3, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: context.color.onSurface.withValues(alpha: 0.8), + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + color: context.color.onSurface.withValues( + alpha: 0.8, + ), ), ), ],