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.
This commit is contained in:
2026-08-29 17:50:31 +07:00
parent 6e7f1ced35
commit a7c6058540
105 changed files with 16721 additions and 275 deletions
@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import '../../sources/mangatown/urls.dart';
import '../../sources/source.dart';
/// MangaTown Directory filter bar (demographic / genre / status / initial).
class MangaTownDirectoryFilters extends StatelessWidget {
const MangaTownDirectoryFilters({
super.key,
required this.value,
required this.onChanged,
});
final CatalogBrowseParams value;
final ValueChanged<CatalogBrowseParams> onChanged;
@override
Widget build(BuildContext context) {
final initials = mangaTownInitials();
return Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
child: Column(
children: [
Row(
children: [
Expanded(
child: _FilterDropdown(
label: 'Demographic',
value: value.demographic,
entries: mangaTownDemographics,
onChanged: (v) => onChanged(value.copyWith(demographic: v)),
),
),
const SizedBox(width: 8),
Expanded(
child: _FilterDropdown(
label: 'Status',
value: value.status,
entries: mangaTownStatuses,
onChanged: (v) => onChanged(value.copyWith(status: v)),
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: _FilterDropdown(
label: 'Genre',
value: value.genre,
entries: mangaTownGenres,
onChanged: (v) => onChanged(value.copyWith(genre: v)),
),
),
const SizedBox(width: 8),
Expanded(
child: _FilterDropdown(
label: 'Letter',
value: value.initial,
entries: initials,
onChanged: (v) => onChanged(value.copyWith(initial: v)),
),
),
],
),
],
),
);
}
}
class _FilterDropdown extends StatelessWidget {
const _FilterDropdown({
required this.label,
required this.value,
required this.entries,
required this.onChanged,
});
final String label;
final String value;
final List<MapEntry<String, String>> entries;
final ValueChanged<String> onChanged;
@override
Widget build(BuildContext context) {
final known = entries.any((e) => e.key == value) ? value : '0';
return InputDecorator(
decoration: InputDecoration(
labelText: label,
border: const OutlineInputBorder(),
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
isDense: true,
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
value: known,
items: [
for (final e in entries)
DropdownMenuItem(value: e.key, child: Text(e.value)),
],
onChanged: (v) {
if (v != null) onChanged(v);
},
),
),
);
}
}
@@ -0,0 +1,197 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../providers/providers.dart';
import '../../sources/source.dart';
import '../widgets/comic_cover.dart';
import '../widgets/source_logo.dart';
/// Catalog tab root helpers live in the router redirect; picker is [SourcePickerPage].
const _kLastCatalogSource = 'catalog_last_source_id';
Future<void> saveLastCatalogSource(SourceId id) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_kLastCatalogSource, id.name);
}
Future<SourceId?> loadLastCatalogSource() async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(_kLastCatalogSource);
if (raw == null) return null;
return SourceId.tryParse(raw);
}
class SourcePickerPage extends ConsumerStatefulWidget {
const SourcePickerPage({super.key});
@override
ConsumerState<SourcePickerPage> createState() => _SourcePickerPageState();
}
class _SourcePickerPageState extends ConsumerState<SourcePickerPage> {
SourceId? _last;
@override
void initState() {
super.initState();
loadLastCatalogSource().then((id) {
if (!mounted) return;
setState(() => _last = id);
});
}
@override
Widget build(BuildContext context) {
final registry = ref.watch(sourceRegistryProvider);
final sources = registry.all;
final last = _last;
final continueSource =
last != null && registry.isEnabled(last) ? registry.get(last) : null;
return Scaffold(
appBar: AppBar(
title: const Text('Catalog'),
actions: [
const PeekModeToggleButton(),
IconButton(
tooltip: 'Settings',
icon: const Icon(Icons.settings_outlined),
onPressed: () => context.push('/settings'),
),
],
),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
if (continueSource != null)
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
child: Align(
alignment: Alignment.centerLeft,
child: ActionChip(
avatar: SourceLogo(sourceId: continueSource.id, size: 18),
label: Text('Continue · ${continueSource.displayName}'),
onPressed: () => _open(continueSource.id),
),
),
),
for (final s in sources)
ListTile(
leading: SourceLogo(sourceId: s.id),
title: Text(s.displayName),
subtitle: Text(s.blurb),
trailing: const Icon(Icons.chevron_right),
onTap: () => _open(s.id),
),
],
),
);
}
void _open(SourceId id) {
saveLastCatalogSource(id);
context.push('/catalog/${id.name}');
}
}
/// Lists [ComicSource.catalogMenus] for one enabled source.
class SourceHomePage extends ConsumerWidget {
const SourceHomePage({
super.key,
required this.sourceId,
this.embeddedAsRoot = false,
});
final SourceId sourceId;
final bool embeddedAsRoot;
@override
Widget build(BuildContext context, WidgetRef ref) {
final registry = ref.watch(sourceRegistryProvider);
if (!registry.isEnabled(sourceId)) {
return Scaffold(
appBar: AppBar(title: const Text('Catalog')),
body: const Center(child: Text('Source not enabled in this build.')),
);
}
final source = registry.get(sourceId);
final menus = source.catalogMenus;
return Scaffold(
appBar: AppBar(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
SourceLogo(sourceId: sourceId, size: 24),
const SizedBox(width: 8),
Text(source.displayName),
],
),
leading: embeddedAsRoot
? null
: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
if (context.canPop()) {
context.pop();
} else {
context.go('/catalog');
}
},
),
actions: [
const PeekModeToggleButton(),
IconButton(
tooltip: 'Settings',
icon: const Icon(Icons.settings_outlined),
onPressed: () => context.push('/settings'),
),
],
),
body: ListView.separated(
itemCount: menus.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, i) {
final menu = menus[i];
return ListTile(
leading: Icon(_iconFor(menu.kind)),
title: Text(menu.title),
trailing: const Icon(Icons.chevron_right),
onTap: () {
saveLastCatalogSource(sourceId);
context.push('/catalog/${sourceId.name}/${menu.kind.pathSegment}');
},
);
},
),
);
}
IconData _iconFor(CatalogMenuKind kind) {
switch (kind) {
case CatalogMenuKind.popular:
return Icons.local_fire_department_outlined;
case CatalogMenuKind.latest:
return Icons.schedule_outlined;
case CatalogMenuKind.official:
return Icons.verified_outlined;
case CatalogMenuKind.uncensored:
return Icons.visibility_outlined;
case CatalogMenuKind.recommended:
return Icons.thumb_up_outlined;
case CatalogMenuKind.search:
return Icons.search;
case CatalogMenuKind.hotToday:
return Icons.wb_sunny_outlined;
case CatalogMenuKind.hotWeek:
return Icons.date_range_outlined;
case CatalogMenuKind.hotMonth:
return Icons.calendar_month_outlined;
case CatalogMenuKind.directory:
return Icons.grid_view_outlined;
}
}
}