Skip to content

Commit f8ffc30

Browse files
committed
feat: Enhance Komiktap scraper to parse last update and favorite counts, fix ContentByTag screen state, and add package tests.
1 parent d42a799 commit f8ffc30

File tree

8 files changed

+285
-83
lines changed

8 files changed

+285
-83
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,7 @@ projects/
5252
.fvm/
5353

5454
# APK files (should not be tracked in git)
55-
*.apk
55+
*.apk
56+
# Project documentation and assets
57+
/docs/technical/*
58+
/docs/assets/*

lib/presentation/pages/content_by_tag/content_by_tag_screen.dart

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import 'package:nhasixapp/presentation/widgets/sorting_widget.dart';
1515
import 'package:nhasixapp/presentation/widgets/offline_indicator_widget.dart';
1616
import 'package:nhasixapp/presentation/widgets/progress_indicator_widget.dart';
1717
import 'package:nhasixapp/domain/repositories/user_data_repository.dart';
18+
import 'package:nhasixapp/domain/repositories/content_repository.dart';
19+
import 'package:nhasixapp/domain/usecases/content/content_usecases.dart';
1820

1921
/// Screen for browsing content by specific tag
2022
///
@@ -41,7 +43,15 @@ class _ContentByTagScreenState extends State<ContentByTagScreen> {
4143
@override
4244
void initState() {
4345
super.initState();
44-
_contentBloc = getIt<ContentBloc>();
46+
// Use a fresh ContentBloc instance to avoid polluting the global/home ContentBloc state
47+
// This fixes the bug where returning to Home shows the tag search results
48+
_contentBloc = ContentBloc(
49+
getContentListUseCase: getIt<GetContentListUseCase>(),
50+
searchContentUseCase: getIt<SearchContentUseCase>(),
51+
getRandomContentUseCase: getIt<GetRandomContentUseCase>(),
52+
contentRepository: getIt<ContentRepository>(),
53+
logger: getIt<Logger>(),
54+
);
4555
_initializeContent();
4656
}
4757

@@ -78,8 +88,8 @@ class _ContentByTagScreenState extends State<ContentByTagScreen> {
7888

7989
@override
8090
void dispose() {
81-
// Don't close ContentBloc - it's a singleton managed by DI container
82-
// _contentBloc.close();
91+
// Close the local ContentBloc instance
92+
_contentBloc.close();
8393
super.dispose();
8494
}
8595

lib/presentation/pages/detail/detail_screen.dart

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -417,22 +417,6 @@ class _DetailScreenState extends State<DetailScreen> {
417417
color: Theme.of(context).colorScheme.surfaceContainer,
418418
onSelected: (value) => _handleMenuAction(value, content),
419419
itemBuilder: (context) => [
420-
PopupMenuItem(
421-
value: 'download',
422-
child: Row(
423-
children: [
424-
Icon(Icons.download,
425-
color: Theme.of(context).colorScheme.onSurface),
426-
const SizedBox(width: 12),
427-
Text(
428-
AppLocalizations.of(context)!.download,
429-
style: TextStyleConst.bodyMedium.copyWith(
430-
color: Theme.of(context).colorScheme.onSurface,
431-
),
432-
),
433-
],
434-
),
435-
),
436420
PopupMenuItem(
437421
value: 'copy_link',
438422
child: Row(

packages/kuron_komiktap/.gitignore

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Miscellaneous
2+
*.class
3+
*.log
4+
*.pyc
5+
*.swp
6+
.DS_Store
7+
.atom/
8+
.buildlog/
9+
.history
10+
.svn/
11+
12+
# IntelliJ related
13+
*.iml
14+
*.ipr
15+
*.iws
16+
.idea/
17+
18+
# The .vscode folder contains launch configuration and tasks you configure in
19+
# VS Code which you may wish to be included in version control.
20+
.vscode/
21+
22+
# Flutter/Dart/Pub related
23+
**/doc/api/
24+
**/ios/Flutter/.last_build_id
25+
.dart_tool/
26+
.flutter-plugins
27+
.flutter-plugins-dependencies
28+
.pub-cache/
29+
.pub/
30+
build/
31+
32+
# Symbolication related
33+
app.*.symbols
34+
35+
# Obfuscation related
36+
app.*.map.json

packages/kuron_komiktap/lib/src/komiktap_scraper.dart

Lines changed: 107 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ class KomiktapScraper {
3838
// series detail
3939
'detail_title': '.entry-title',
4040
'detail_cover': '.thumb img',
41-
'detail_status': '.imptdt',
42-
'detail_type': '.imptdt',
43-
'detail_infoList': '.fmed',
41+
'detail_status': '.infotable tr',
42+
'detail_type': '.infotable tr',
43+
'detail_infoList': '.infotable tr',
4444
'detail_genres': '.seriestugenre a',
4545
'detail_synopsis': '[itemprop="description"]',
4646

@@ -250,6 +250,9 @@ class KomiktapScraper {
250250

251251
// Parse chapters
252252
final chapters = _parseChapterList(document);
253+
254+
// Parse favorites
255+
final favorites = _parseFavorites(document);
253256

254257
return KomiktapSeriesDetail(
255258
id: slug,
@@ -259,11 +262,100 @@ class KomiktapScraper {
259262
status: status,
260263
type: contentType,
261264
tags: genres,
262-
author: metadata['author'] as String?,
265+
author: (metadata['author'] ?? metadata['artist'] ?? metadata['posted_by']) as String?,
266+
lastUpdate: (metadata['updated_on'] ?? metadata['posted_on']) as DateTime?,
263267
chapters: chapters,
268+
favorites: favorites,
264269
);
265270
}
266271

272+
// ... (keep existing methods)
273+
274+
Map<String, dynamic> _parseMetadata(Document doc) {
275+
final metadata = <String, dynamic>{};
276+
277+
// Parse all .infotable rows
278+
final infoRows = doc.querySelectorAll(_getSelector('detail_infoList'));
279+
280+
for (final row in infoRows) {
281+
final cells = row.querySelectorAll('td');
282+
if (cells.length < 2) continue;
283+
284+
final label = cells[0].text.trim().toLowerCase();
285+
final value = cells[1].text.trim();
286+
287+
if (label.contains('author')) {
288+
metadata['author'] = value;
289+
} else if (label.contains('artist')) {
290+
metadata['artist'] = value;
291+
} else if (label.contains('serialization')) {
292+
metadata['serialization'] = value;
293+
} else if (label.contains('posted by')) {
294+
metadata['posted_by'] = value;
295+
} else if (label.contains('updated on') || label.contains('posted on')) {
296+
// Extract date from time tag or text
297+
DateTime? date;
298+
final timeEl = cells[1].querySelector('time');
299+
if (timeEl != null) {
300+
final datetime = timeEl.attributes['datetime'];
301+
if (datetime != null) {
302+
date = DateTime.tryParse(datetime);
303+
}
304+
}
305+
306+
// Fallback to text parsing
307+
date ??= _parseDate(value);
308+
309+
if (date != null) {
310+
if (label.contains('updated')) {
311+
metadata['updated_on'] = date;
312+
} else {
313+
metadata['posted_on'] = date;
314+
}
315+
}
316+
}
317+
}
318+
319+
return metadata;
320+
}
321+
322+
DateTime? _parseDate(String? dateText) {
323+
if (dateText == null || dateText.isEmpty) return null;
324+
325+
try {
326+
// Try direct ISO parse first
327+
final parsed = DateTime.tryParse(dateText);
328+
if (parsed != null) return parsed;
329+
330+
// Handle relative dates like "2 hours ago", "3 days ago"
331+
final relativePattern = RegExp(r'(\d+)\s+(hour|day|week|month)s?\s+ago',
332+
caseSensitive: false);
333+
final match = relativePattern.firstMatch(dateText);
334+
335+
if (match != null) {
336+
final amount = int.tryParse(match.group(1)!) ?? 0;
337+
final unit = match.group(2)!.toLowerCase();
338+
339+
final now = DateTime.now();
340+
switch (unit) {
341+
case 'hour':
342+
return now.subtract(Duration(hours: amount));
343+
case 'day':
344+
return now.subtract(Duration(days: amount));
345+
case 'week':
346+
return now.subtract(Duration(days: amount * 7));
347+
case 'month':
348+
return DateTime(now.year, now.month - amount, now.day);
349+
}
350+
}
351+
352+
return null;
353+
} catch (_) {
354+
return null;
355+
}
356+
}
357+
358+
267359
List<KomiktapChapterInfo> _parseChapterList(Document document) {
268360
final items = document.querySelectorAll(_getSelector('detail_chapterList'));
269361

@@ -455,66 +547,19 @@ class KomiktapScraper {
455547
.toList();
456548
}
457549

458-
Map<String, dynamic> _parseMetadata(Document doc) {
459-
final metadata = <String, dynamic>{};
460-
461-
// Parse all .infotable rows
462-
final infoRows = doc.querySelectorAll(_getSelector('detail_infoList'));
463-
464-
for (final row in infoRows) {
465-
final cells = row.querySelectorAll('td');
466-
if (cells.length < 2) continue;
467-
468-
final label = cells[0].text.trim().toLowerCase();
469-
final value = cells[1].text.trim();
470-
471-
if (label.contains('author')) {
472-
metadata['author'] = value;
473-
} else if (label.contains('artist')) {
474-
metadata['artist'] = value;
475-
} else if (label.contains('serialization')) {
476-
metadata['serialization'] = value;
477-
} else if (label.contains('posted by')) {
478-
metadata['posted_by'] = value;
479-
}
480-
}
481-
482-
return metadata;
483-
}
484-
485-
DateTime? _parseDate(String? dateText) {
486-
if (dateText == null || dateText.isEmpty) return null;
487-
550+
int? _parseFavorites(Document doc) {
488551
try {
489-
// Try direct ISO parse first
490-
final parsed = DateTime.tryParse(dateText);
491-
if (parsed != null) return parsed;
492-
493-
// Handle relative dates like "2 hours ago", "3 days ago"
494-
final relativePattern = RegExp(r'(\d+)\s+(hour|day|week|month)s?\s+ago',
495-
caseSensitive: false);
496-
final match = relativePattern.firstMatch(dateText);
497-
498-
if (match != null) {
499-
final amount = int.tryParse(match.group(1)!) ?? 0;
500-
final unit = match.group(2)!.toLowerCase();
501-
502-
final now = DateTime.now();
503-
switch (unit) {
504-
case 'hour':
505-
return now.subtract(Duration(hours: amount));
506-
case 'day':
507-
return now.subtract(Duration(days: amount));
508-
case 'week':
509-
return now.subtract(Duration(days: amount * 7));
510-
case 'month':
511-
return DateTime(now.year, now.month - amount, now.day);
552+
// <div class="bmc">Followed by 21 people</div>
553+
final bmcEl = doc.querySelector('.bmc');
554+
if (bmcEl != null) {
555+
final text = bmcEl.text.trim();
556+
// Extract number from "Followed by 21 people"
557+
final match = RegExp(r'(\d+)').firstMatch(text);
558+
if (match != null) {
559+
return int.tryParse(match.group(1)!);
512560
}
513561
}
514-
515-
return null;
516-
} catch (_) {
517-
return null;
518-
}
562+
} catch (_) {}
563+
return null;
519564
}
520565
}

packages/kuron_komiktap/lib/src/komiktap_source.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ class KomiktapSource implements ContentSource {
445445
groups: [],
446446
language: 'indonesian',
447447
uploadDate: detail.lastUpdate ?? DateTime.now(),
448-
favorites: 0,
448+
favorites: detail.favorites ?? 0,
449449
englishTitle: detail.alternativeTitle,
450450
);
451451
}

packages/kuron_komiktap/lib/src/models/komiktap_models.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class KomiktapSeriesDetail {
3131
final DateTime? lastUpdate;
3232
final List<String> tags; // genre names
3333
final List<KomiktapChapterInfo>? chapters;
34+
final int? favorites;
3435

3536
const KomiktapSeriesDetail({
3637
required this.id,
@@ -45,6 +46,7 @@ class KomiktapSeriesDetail {
4546
this.lastUpdate,
4647
this.tags = const [],
4748
this.chapters,
49+
this.favorites,
4850
});
4951
}
5052

0 commit comments

Comments
 (0)