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:
@@ -0,0 +1,178 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../providers/providers.dart';
|
||||
import '../../sources/source.dart';
|
||||
|
||||
class ComicCover extends ConsumerStatefulWidget {
|
||||
const ComicCover({
|
||||
super.key,
|
||||
required this.url,
|
||||
this.sourceId,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit = BoxFit.cover,
|
||||
});
|
||||
|
||||
final String? url;
|
||||
final SourceId? sourceId;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit fit;
|
||||
|
||||
@override
|
||||
ConsumerState<ComicCover> createState() => _ComicCoverState();
|
||||
}
|
||||
|
||||
class _ComicCoverState extends ConsumerState<ComicCover> {
|
||||
bool _revealed = false;
|
||||
|
||||
void _setRevealed(bool value) {
|
||||
if (_revealed == value) return;
|
||||
setState(() => _revealed = value);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final peek = ref.watch(peekModeProvider);
|
||||
final cache = ref.watch(imageCacheManagerProvider);
|
||||
Map<String, String> headers = const {};
|
||||
final sid = widget.sourceId;
|
||||
if (sid != null) {
|
||||
final registry = ref.watch(sourceRegistryProvider);
|
||||
if (registry.isEnabled(sid)) {
|
||||
headers = registry.get(sid).imageHeaders;
|
||||
}
|
||||
}
|
||||
if (widget.url == null || widget.url!.isEmpty) {
|
||||
return Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
child: const Icon(Icons.menu_book_outlined),
|
||||
);
|
||||
}
|
||||
|
||||
final image = CachedNetworkImage(
|
||||
imageUrl: widget.url!,
|
||||
cacheManager: cache,
|
||||
httpHeaders: headers,
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
fit: widget.fit,
|
||||
placeholder: (context, url) => Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
child: const Icon(Icons.broken_image_outlined),
|
||||
),
|
||||
);
|
||||
|
||||
if (!peek) return image;
|
||||
|
||||
if (_revealed) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPressEnd: (_) => _setRevealed(false),
|
||||
onLongPressCancel: () => _setRevealed(false),
|
||||
child: image,
|
||||
);
|
||||
}
|
||||
|
||||
// Heavy blur + dark veil so covers are unreadable in public.
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPressStart: (_) => _setRevealed(true),
|
||||
child: SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
ClipRect(
|
||||
child: ImageFiltered(
|
||||
imageFilter: ImageFilter.blur(
|
||||
sigmaX: 36,
|
||||
sigmaY: 36,
|
||||
tileMode: TileMode.clamp,
|
||||
),
|
||||
child: image,
|
||||
),
|
||||
),
|
||||
ColoredBox(
|
||||
color: Colors.black.withValues(alpha: 0.45),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.visibility_off,
|
||||
color: Colors.white54,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PeekModeToggleButton extends ConsumerWidget {
|
||||
const PeekModeToggleButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final peek = ref.watch(peekModeProvider);
|
||||
return IconButton(
|
||||
tooltip: peek ? 'Peek mode on (covers hidden)' : 'Peek mode off',
|
||||
onPressed: () => ref.read(peekModeProvider.notifier).toggle(),
|
||||
icon: Icon(peek ? Icons.visibility_off : Icons.visibility),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ComicGridTile extends StatelessWidget {
|
||||
const ComicGridTile({
|
||||
super.key,
|
||||
required this.comic,
|
||||
required this.onTap,
|
||||
this.titleMaxLines = 2,
|
||||
});
|
||||
|
||||
final CatalogComic comic;
|
||||
final VoidCallback onTap;
|
||||
final int titleMaxLines;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: ComicCover(url: comic.coverUrl, sourceId: comic.source),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
comic.title,
|
||||
maxLines: titleMaxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../sources/source.dart';
|
||||
|
||||
/// Bundled source favicon from [SourceId.logoAsset].
|
||||
class SourceLogo extends StatelessWidget {
|
||||
const SourceLogo({
|
||||
super.key,
|
||||
required this.sourceId,
|
||||
this.size = 32,
|
||||
this.borderRadius = 6,
|
||||
});
|
||||
|
||||
final SourceId sourceId;
|
||||
final double size;
|
||||
final double borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: Image.asset(
|
||||
sourceId.logoAsset,
|
||||
width: size,
|
||||
height: size,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) => Icon(
|
||||
Icons.public,
|
||||
size: size * 0.75,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user