feat: integrate WordPress PDF Aussendungen with in-app reader into News

This commit is contained in:
2026-09-14 21:32:03 +02:00
parent e512c081e3
commit 9a823cbcdb
8 changed files with 1000 additions and 228 deletions
+166
View File
@@ -11,6 +11,9 @@ class PostItem {
final String? street;
final String? category;
final String? status;
final String? pdfUrl;
final String? webUrl;
final bool isAussendung;
PostItem({
required this.id,
@@ -23,6 +26,9 @@ class PostItem {
this.street,
this.category,
this.status = 'Gemeldet',
this.pdfUrl,
this.webUrl,
this.isAussendung = false,
}) : imageUrls = imageUrls ?? (imageUrl.isNotEmpty ? [imageUrl] : []);
factory PostItem.fromFirestore(DocumentSnapshot doc) {
@@ -54,6 +60,9 @@ class PostItem {
street: data['street'] as String?,
category: data['category'] as String?,
status: data['status'] as String? ?? 'Gemeldet',
pdfUrl: data['pdfUrl'] as String?,
webUrl: data['webUrl'] as String?,
isAussendung: data['isAussendung'] as bool? ?? false,
);
}
@@ -68,6 +77,163 @@ class PostItem {
if (street != null) 'street': street,
if (category != null) 'category': category,
if (status != null) 'status': status,
if (pdfUrl != null) 'pdfUrl': pdfUrl,
if (webUrl != null) 'webUrl': webUrl,
if (isAussendung) 'isAussendung': isAussendung,
};
}
/// Convert to standard JSON map for local caching
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'imageUrl': imageUrl,
'imageUrls': imageUrls,
'createdDate': createdDate.toIso8601String(),
'authorUid': authorUid,
'street': street,
'category': category,
'status': status,
'pdfUrl': pdfUrl,
'webUrl': webUrl,
'isAussendung': isAussendung,
};
}
/// Create from standard JSON map (local cache)
factory PostItem.fromJson(Map<String, dynamic> map) {
DateTime date;
if (map['createdDate'] is String) {
date = DateTime.tryParse(map['createdDate'] as String) ?? DateTime.now();
} else {
date = DateTime.now();
}
final String legacyImage = map['imageUrl'] as String? ?? '';
List<String> images = [];
if (map['imageUrls'] is List) {
images = List<String>.from(map['imageUrls'] as List);
} else if (legacyImage.isNotEmpty) {
images = [legacyImage];
}
return PostItem(
id: map['id'] as String? ?? '',
title: map['title'] as String? ?? '',
description: map['description'] as String? ?? '',
imageUrl: legacyImage.isNotEmpty ? legacyImage : (images.isNotEmpty ? images.first : ''),
imageUrls: images,
createdDate: date,
authorUid: map['authorUid'] as String?,
street: map['street'] as String?,
category: map['category'] as String?,
status: map['status'] as String? ?? 'Gemeldet',
pdfUrl: map['pdfUrl'] as String?,
webUrl: map['webUrl'] as String?,
isAussendung: map['isAussendung'] as bool? ?? false,
);
}
/// Decode HTML entities commonly present in WordPress feeds
static String unescapeHtml(String text) {
var result = text
.replaceAll('&amp;', '&')
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&#039;', "'")
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&nbsp;', ' ')
.replaceAll('&#8211;', '–')
.replaceAll('&#8212;', '—')
.replaceAll('&#8216;', '‘')
.replaceAll('&#8217;', '’')
.replaceAll('&#8220;', '“')
.replaceAll('&#8221;', '”')
.replaceAll('&#8230;', '…');
// Decode numerical entities &#DDD;
result = result.replaceAllMapped(RegExp(r'&#(\d+);'), (match) {
final code = int.tryParse(match.group(1)!);
return code != null ? String.fromCharCode(code) : match.group(0)!;
});
// Decode hex numerical entities &#xHH;
result = result.replaceAllMapped(RegExp(r'&#x([0-9a-fA-F]+);'), (match) {
final code = int.tryParse(match.group(1)!, radix: 16);
return code != null ? String.fromCharCode(code) : match.group(0)!;
});
return result.trim();
}
/// Extracts the direct PDF URL from WordPress post HTML content
static String? extractPdfUrl(String htmlContent) {
final pdfRegExp = RegExp(
r'https?://[^\s"<>]+?\.pdf',
caseSensitive: false,
);
final match = pdfRegExp.firstMatch(htmlContent);
if (match != null) {
return match.group(0);
}
// Fallback: check docx or doc files
final docRegExp = RegExp(
r'https?://[^\s"<>]+?\.(?:docx|doc)',
caseSensitive: false,
);
final docMatch = docRegExp.firstMatch(htmlContent);
return docMatch?.group(0);
}
/// Strip HTML tags and clean whitespace for preview text
static String extractCleanText(String htmlContent) {
var clean = htmlContent.replaceAll(RegExp(r'<style[^>]*>.*?</style>', dotAll: true), '');
clean = clean.replaceAll(RegExp(r'<script[^>]*>.*?</script>', dotAll: true), '');
clean = clean.replaceAll(RegExp(r'<[^>]*>'), ' ');
clean = unescapeHtml(clean);
clean = clean.replaceAll(RegExp(r'\s+'), ' ').trim();
return clean;
}
/// Parse a post from the WordPress REST API (/wp-json/wp/v2/posts)
factory PostItem.fromWordPressJson(Map<String, dynamic> json) {
final rawTitle = (json['title'] is Map)
? (json['title']['rendered'] as String? ?? '')
: (json['title'] as String? ?? '');
final title = unescapeHtml(rawTitle);
final rawContent = (json['content'] is Map)
? (json['content']['rendered'] as String? ?? '')
: (json['content'] as String? ?? '');
final pdfUrl = extractPdfUrl(rawContent);
final webUrl = json['link'] as String?;
DateTime date;
if (json['date'] is String) {
date = DateTime.tryParse(json['date'] as String) ?? DateTime.now();
} else {
date = DateTime.now();
}
String desc = extractCleanText(rawContent);
if (desc.isEmpty || desc == title) {
desc = 'Offizielle Aussendung des Bürgerforums Oggau (PDF).';
}
return PostItem(
id: 'wp_${json['id']}',
title: title.isNotEmpty ? title : 'Aussendung',
description: desc,
createdDate: date,
category: 'Aussendung',
status: 'Aussendung',
pdfUrl: pdfUrl,
webUrl: webUrl,
isAussendung: true,
);
}
}