Files
cxyz/cxyz-android/lib/sources/mangatown/urls.dart
T
nsrb a7c6058540 Add cxyz Android project structure and initial configuration
- Introduced a new Flutter project for the cxyz Android manga/comic reader, including essential files such as pubspec.yaml, AndroidManifest.xml, and Gradle configuration.
- Added .gitignore files to exclude unnecessary files and directories for both the main project and Android-specific files.
- Implemented initial README.md with project description, features, setup instructions, and API details for ComicLand, MangaRead, and MangaTown.
- Configured analysis options and dependencies in pubspec.yaml to support Flutter development.
- Established a pnpm workspace and lockfile for dependency management.
- Included various Android resources such as icons, styles, and launch backgrounds for a complete application setup.
2026-08-29 17:50:31 +07:00

246 lines
7.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import '../source.dart';
import 'constants.dart';
/// Hot ranking period for `/hot` feeds.
enum MangaTownHotPeriod {
total,
today,
week,
month,
}
String? hotPeriodQuery(MangaTownHotPeriod period) {
switch (period) {
case MangaTownHotPeriod.total:
return null;
case MangaTownHotPeriod.today:
return 'tviews.za';
case MangaTownHotPeriod.week:
return 'wviews.za';
case MangaTownHotPeriod.month:
return 'mviews.za';
}
}
MangaTownHotPeriod? hotPeriodForMenu(CatalogMenuKind kind) {
switch (kind) {
case CatalogMenuKind.popular:
return MangaTownHotPeriod.total;
case CatalogMenuKind.hotToday:
return MangaTownHotPeriod.today;
case CatalogMenuKind.hotWeek:
return MangaTownHotPeriod.week;
case CatalogMenuKind.hotMonth:
return MangaTownHotPeriod.month;
default:
return null;
}
}
/// Page 1 uses the bare path; page 2+ appends `/{n}.html`.
String _pagedPath(String path, int page) {
final base = path.replaceAll(RegExp(r'/+$'), '');
if (page <= 1) return base;
return '$base/$page.html';
}
Uri _withOptionalQuery(String path, String? queryToken) {
final uri = Uri.parse('$mangaTownOrigin$path');
if (queryToken == null || queryToken.isEmpty) return uri;
// Site uses bare tokens like `?wviews.za` (no `=` value).
return Uri.parse('${uri.toString()}?$queryToken');
}
String catalogLatestUrl({int page = 1}) {
return _withOptionalQuery(_pagedPath('/latest', page), null).toString();
}
String catalogHotUrl({
MangaTownHotPeriod period = MangaTownHotPeriod.total,
int page = 1,
}) {
return _withOptionalQuery(
_pagedPath('/hot', page),
hotPeriodQuery(period),
).toString();
}
/// `/directory/{demographic}-{genre}-0-{status}-{initial}-0`
String directoryUrl(
CatalogBrowseParams browse, {
int page = 1,
}) {
final demo = _seg(browse.demographic);
final genre = _seg(browse.genre);
final status = _seg(browse.status);
final initial = _seg(browse.initial);
final path = _pagedPath(
'/directory/$demo-$genre-0-$status-$initial-0',
page,
);
return '$mangaTownOrigin$path';
}
String catalogSearchUrl({required String query, int page = 1}) {
final path = _pagedPath('/search', page);
return Uri.parse('$mangaTownOrigin$path').replace(
queryParameters: {'name': query},
).toString();
}
String _seg(String raw) {
final t = raw.trim();
if (t.isEmpty) return '0';
return t;
}
String mangaSlugFromUrl(String pageUrl) {
try {
final parts =
Uri.parse(pageUrl).pathSegments.where((s) => s.isNotEmpty).toList();
final mi = parts.indexOf('manga');
if (mi != -1 && mi + 1 < parts.length) {
return parts[mi + 1];
}
} catch (_) {}
return 'comic';
}
String listingUrlForSlug(String slug, {String origin = mangaTownOrigin}) {
final base = origin.replaceAll(RegExp(r'/+$'), '');
return '$base/manga/$slug';
}
String mangaBaseUrlFromListingUrl(String pageUrl) {
final u = Uri.parse(pageUrl);
final parts = u.pathSegments.where((s) => s.isNotEmpty).toList();
final mi = parts.indexOf('manga');
if (mi == -1 || mi + 1 >= parts.length) {
throw StateError(
'Could not find /manga/{slug}/ in URL; use a manga or chapter link from MangaTown.',
);
}
final slug = parts[mi + 1];
return Uri(scheme: u.scheme, host: u.host, path: '/manga/$slug').toString();
}
/// Strip reader page suffixes like `/2.html` so we keep `/manga/{slug}/c005/`.
String chapterBaseUrl(String chapterUrl) {
final u = Uri.parse(chapterUrl);
final parts = u.pathSegments.where((s) => s.isNotEmpty).toList();
if (parts.isEmpty) return chapterUrl;
final last = parts.last;
if (RegExp(r'^\d+\.html$', caseSensitive: false).hasMatch(last)) {
parts.removeLast();
}
final path = '/${parts.join('/')}/';
return Uri(scheme: u.scheme, host: u.host, path: path).toString();
}
/// 1-based reader page URL under a chapter base.
String chapterPageUrl(String chapterBase, int pageIndex) {
final base = chapterBaseUrl(chapterBase);
if (pageIndex <= 1) return base;
final trimmed = base.replaceAll(RegExp(r'/+$'), '');
return '$trimmed/$pageIndex.html';
}
String chapterKeyFromUrl(String chapterUrl) {
try {
final segs = Uri.parse(chapterBaseUrl(chapterUrl))
.pathSegments
.where((s) => s.isNotEmpty)
.toList();
return segs.isEmpty ? 'chapter' : segs.last;
} catch (_) {
return 'chapter';
}
}
String? chapterNumberFromTitle(String title) {
final cleaned = title.trim().replaceAll(RegExp(r'\s+'), ' ');
final m = RegExp(
r'(?:Chapter|Ch\.?|C\.?)\s*([\d.]+)',
caseSensitive: false,
).firstMatch(cleaned);
return m?.group(1);
}
String absoluteUrl(String href, String baseHref) {
final resolved = Uri.parse(baseHref).resolve(href.trim()).toString();
if (resolved.startsWith('//')) {
return 'https:$resolved';
}
return resolved;
}
/// Demographics offered by MangaTown directory filters.
const mangaTownDemographics = <MapEntry<String, String>>[
MapEntry('0', 'All'),
MapEntry('josei', 'Josei'),
MapEntry('seinen', 'Seinen'),
MapEntry('shoujo', 'Shoujo'),
MapEntry('shoujo_ai', 'Shoujo Ai'),
MapEntry('shounen', 'Shounen'),
MapEntry('shounen_ai', 'Shounen Ai'),
MapEntry('yaoi', 'Yaoi'),
MapEntry('yuri', 'Yuri'),
];
/// Genre slug → label for directory filters.
const mangaTownGenres = <MapEntry<String, String>>[
MapEntry('0', 'All'),
MapEntry('4_koma', '4 Koma'),
MapEntry('action', 'Action'),
MapEntry('adventure', 'Adventure'),
MapEntry('comedy', 'Comedy'),
MapEntry('cooking', 'Cooking'),
MapEntry('doujinshi', 'Doujinshi'),
MapEntry('drama', 'Drama'),
MapEntry('ecchi', 'Ecchi'),
MapEntry('fantasy', 'Fantasy'),
MapEntry('gender_bender', 'Gender Bender'),
MapEntry('harem', 'Harem'),
MapEntry('historical', 'Historical'),
MapEntry('horror', 'Horror'),
MapEntry('martial_arts', 'Martial Arts'),
MapEntry('mature', 'Mature'),
MapEntry('mecha', 'Mecha'),
MapEntry('music', 'Music'),
MapEntry('mystery', 'Mystery'),
MapEntry('one_shot', 'One Shot'),
MapEntry('psychological', 'Psychological'),
MapEntry('reverse_harem', 'Reverse Harem'),
MapEntry('romance', 'Romance'),
MapEntry('school_life', 'School Life'),
MapEntry('sci_fi', 'Sci Fi'),
MapEntry('shotacon', 'Shotacon'),
MapEntry('slice_of_life', 'Slice Of Life'),
MapEntry('smut', 'Smut'),
MapEntry('sports', 'Sports'),
MapEntry('supernatural', 'Supernatural'),
MapEntry('suspense', 'Suspense'),
MapEntry('tragedy', 'Tragedy'),
MapEntry('vampire', 'Vampire'),
MapEntry('webtoons', 'Webtoons'),
MapEntry('youkai', 'Youkai'),
];
const mangaTownStatuses = <MapEntry<String, String>>[
MapEntry('0', 'All'),
MapEntry('ongoing', 'Ongoing'),
MapEntry('new', 'New'),
MapEntry('completed', 'Completed'),
];
/// A–Z plus `0-9`.
List<MapEntry<String, String>> mangaTownInitials() {
final out = <MapEntry<String, String>>[const MapEntry('0', 'All')];
for (var c = 0; c < 26; c++) {
final letter = String.fromCharCode('a'.codeUnitAt(0) + c);
out.add(MapEntry(letter, letter.toUpperCase()));
}
out.add(const MapEntry('0-9', '0-9'));
return out;
}