Files
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

150 lines
4.6 KiB
Dart

import '../source.dart';
import 'constants.dart';
class ComicLandUrlParts {
const ComicLandUrlParts({
required this.origin,
required this.slug,
this.chapterIndex,
});
final String origin;
final String slug;
final String? chapterIndex;
}
bool isLikelyComicLandSite(String url) {
try {
final host = Uri.parse(url).host.toLowerCase();
return comicLandHosts.contains(host) || host.contains('comicland');
} catch (_) {
return false;
}
}
String seriesSlugFromListingUrl(String pageUrl) {
try {
final parts = Uri.parse(pageUrl).pathSegments.where((s) => s.isNotEmpty).toList();
final comicIdx = parts.indexOf('comic');
if (comicIdx != -1 && comicIdx + 1 < parts.length) {
return parts[comicIdx + 1];
}
} catch (_) {}
return 'comic';
}
ComicLandUrlParts parseComicLandUrl(String pageUrl) {
final u = Uri.parse(pageUrl);
final parts = u.pathSegments.where((s) => s.isNotEmpty).toList();
final comicIdx = parts.indexOf('comic');
final slug =
comicIdx != -1 && comicIdx + 1 < parts.length ? parts[comicIdx + 1] : 'comic';
String? chapterIndex;
final chapterIdx = parts.indexOf('chapter');
if (chapterIdx != -1 && chapterIdx + 1 < parts.length) {
chapterIndex = parts[chapterIdx + 1];
}
return ComicLandUrlParts(
origin: '${u.scheme}://${u.host}',
slug: slug,
chapterIndex: chapterIndex,
);
}
String chapterUrlFromParts(String siteOrigin, String slug, Object chapterIndex) {
final origin = siteOrigin.replaceAll(RegExp(r'/+$'), '');
return '$origin/comic/$slug/chapter/$chapterIndex';
}
String listingUrlForSlug(String slug) =>
'${comicLandOrigin.replaceAll(RegExp(r'/+$'), '')}/comic/$slug';
String chapterKeyFromUrl(String chapterUrl) {
try {
final segs = Uri.parse(chapterUrl).pathSegments.where((s) => s.isNotEmpty).toList();
return segs.isEmpty ? 'chapter' : segs.last;
} catch (_) {
return 'chapter';
}
}
String? chapterNumberFromTitle(String title) {
final m = RegExp(r'^Chapter\s+([\d.]+)', caseSensitive: false)
.firstMatch(title.trim().replaceAll(RegExp(r'\s+'), ' '));
return m?.group(1);
}
String sanitizeFsStem(String stem) {
final cleaned = stem
.replaceAll(RegExp(r'[^a-z0-9._-]+', caseSensitive: false), '_')
.replaceAll(RegExp(r'^_+|_+$'), '');
if (cleaned.isEmpty) return 'chapter';
return cleaned.length > 180 ? cleaned.substring(0, 180) : cleaned;
}
String sanitizeComicDirName(String name) {
final collapsed = name.trim().replaceAll(RegExp(r'\s+'), ' ');
final cleaned = String.fromCharCodes(
collapsed.runes.where((code) {
if (code < 32 || code == 127) return false;
final ch = String.fromCharCode(code);
return !RegExp(r'[<>:"/\\|?*]').hasMatch(ch);
}),
).replaceAll(RegExp(r'\.+$'), '').trim();
if (cleaned.isEmpty) return 'comic';
return cleaned.length > 180 ? cleaned.substring(0, 180) : cleaned;
}
String stemForComicLandChapterCbz(String chapterPageUrl) {
try {
final segs =
Uri.parse(chapterPageUrl).pathSegments.where((s) => s.isNotEmpty).toList();
final comicIdx = segs.indexOf('comic');
if (comicIdx != -1 &&
segs.length >= comicIdx + 4 &&
segs[comicIdx + 2] == 'chapter') {
return '${segs[comicIdx + 1]}-chapter-${segs[comicIdx + 3]}';
}
if (segs.length >= 2) {
return '${segs[segs.length - 2]}-${segs.last}';
}
return segs.isEmpty ? 'chapter' : segs.last;
} catch (_) {
return 'chapter';
}
}
String transferLinkBasename(String? chapterNumber, String? label, String? cbzPath) {
final candidates = [chapterNumber, label]
.whereType<String>()
.where((s) => s.trim().isNotEmpty);
for (final raw in candidates) {
final text = raw.trim();
final m = RegExp(r'(\d+)((?:\.\d+)*)([a-zA-Z]*)').firstMatch(text);
if (m != null) {
final padded = m.group(1)!.padLeft(3, '0');
return 'chapter $padded${m.group(2) ?? ''}${m.group(3) ?? ''}.cbz';
}
}
if (cbzPath != null && cbzPath.isNotEmpty) {
final base = cbzPath.split('/').last;
if (base.isNotEmpty) return base;
}
return 'chapter unknown.cbz';
}
CatalogComic catalogFromJson(Map<String, dynamic> row) {
final slug = (row['slug'] as String?)?.trim() ?? 'comic';
return CatalogComic(
source: SourceId.comicland,
slug: slug,
title: (row['title'] as String?)?.trim().isNotEmpty == true
? (row['title'] as String).trim()
: slug,
listingUrl: listingUrlForSlug(slug),
coverUrl: row['cover_url'] as String?,
description: row['description'] as String?,
rating: (row['rating'] is num) ? (row['rating'] as num).toDouble() : null,
);
}