Files
cxyz/cxyz-android/lib/ui/widgets/comic_cover.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

179 lines
4.7 KiB
Dart

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,
),
],
),
);
}
}