mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-09-01 07:04:07 +00:00
show news icon
This commit is contained in:
parent
9f8cb6f735
commit
7e3caec42e
11 changed files with 152 additions and 111 deletions
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit bb353aa572623d20578f470d2c683ead79669ed8
|
||||
Subproject commit 1727cd6d4f2c56f976883497c182b1456033b614
|
||||
|
|
@ -72,7 +72,7 @@ class UserData {
|
|||
bool videoStabilizationEnabled = true;
|
||||
|
||||
@JsonKey(defaultValue: true)
|
||||
bool showFeedbackShortcut = true;
|
||||
bool showNewsShortcut = true;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
bool showShowImagePreviewWhenSending = false;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> 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<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
|
|||
'automaticallyMarkEqualMediaFilesAsOpened':
|
||||
instance.automaticallyMarkEqualMediaFilesAsOpened,
|
||||
'videoStabilizationEnabled': instance.videoStabilizationEnabled,
|
||||
'showFeedbackShortcut': instance.showFeedbackShortcut,
|
||||
'showNewsShortcut': instance.showNewsShortcut,
|
||||
'showShowImagePreviewWhenSending': instance.showShowImagePreviewWhenSending,
|
||||
'startWithCameraOpen': instance.startWithCameraOpen,
|
||||
'preSelectedEmojies': instance.preSelectedEmojies,
|
||||
|
|
|
|||
|
|
@ -18,13 +18,15 @@ class BlogEntry {
|
|||
});
|
||||
|
||||
factory BlogEntry.fromJson(Map<String, dynamic> 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<String, dynamic> 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<int> unreadCountNotifier = ValueNotifier<int>(0);
|
||||
|
||||
File get _cacheFile => File(join(AppEnvironment.supportDir, 'news_feed.json'));
|
||||
File get _cacheFile =>
|
||||
File(join(AppEnvironment.supportDir, 'news_feed.json'));
|
||||
|
||||
Future<void> 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'<title>([\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);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue