Files
cxyz/cxyz-android/lib/ui/catalog/source_picker_page.dart
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

198 lines
5.9 KiB
Dart

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;
}
}
}