mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
implement saving image, sharing image, reading settings and chapters view
This commit is contained in:
@@ -122,7 +122,6 @@ class SliverAppbar extends StatelessWidget {
|
|||||||
required this.title,
|
required this.title,
|
||||||
this.leading,
|
this.leading,
|
||||||
this.actions,
|
this.actions,
|
||||||
this.color,
|
|
||||||
this.radius = 0,
|
this.radius = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,8 +131,6 @@ class SliverAppbar extends StatelessWidget {
|
|||||||
|
|
||||||
final List<Widget>? actions;
|
final List<Widget>? actions;
|
||||||
|
|
||||||
final Color? color;
|
|
||||||
|
|
||||||
final double radius;
|
final double radius;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -145,14 +142,13 @@ class SliverAppbar extends StatelessWidget {
|
|||||||
title: title,
|
title: title,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
topPadding: MediaQuery.of(context).padding.top,
|
topPadding: MediaQuery.of(context).padding.top,
|
||||||
color: color,
|
|
||||||
radius: radius,
|
radius: radius,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const _kAppBarHeight = 58.0;
|
const _kAppBarHeight = 52.0;
|
||||||
|
|
||||||
class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
@@ -163,15 +159,12 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
|
|
||||||
final double topPadding;
|
final double topPadding;
|
||||||
|
|
||||||
final Color? color;
|
|
||||||
|
|
||||||
final double radius;
|
final double radius;
|
||||||
|
|
||||||
_MySliverAppBarDelegate(
|
_MySliverAppBarDelegate(
|
||||||
{this.leading,
|
{this.leading,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.actions,
|
this.actions,
|
||||||
this.color,
|
|
||||||
required this.topPadding,
|
required this.topPadding,
|
||||||
this.radius = 0});
|
this.radius = 0});
|
||||||
|
|
||||||
@@ -179,8 +172,10 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
Widget build(
|
Widget build(
|
||||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||||
return SizedBox.expand(
|
return SizedBox.expand(
|
||||||
|
child: BlurEffect(
|
||||||
|
blur: 15,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: color,
|
color: context.colorScheme.surface.withOpacity(0.72),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
borderRadius: BorderRadius.circular(radius),
|
borderRadius: BorderRadius.circular(radius),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -189,7 +184,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
leading ??
|
leading ??
|
||||||
(Navigator.of(context).canPop()
|
(Navigator.of(context).canPop()
|
||||||
? Tooltip(
|
? Tooltip(
|
||||||
message: "返回".tl,
|
message: "Back".tl,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
@@ -215,6 +210,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
],
|
],
|
||||||
).paddingTop(topPadding),
|
).paddingTop(topPadding),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,133 +1,71 @@
|
|||||||
part of 'components.dart';
|
part of 'components.dart';
|
||||||
|
|
||||||
class Select extends StatefulWidget {
|
class Select extends StatelessWidget {
|
||||||
const Select({
|
const Select({
|
||||||
required this.initialValue,
|
|
||||||
this.width = 120,
|
|
||||||
required this.onChange,
|
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.current,
|
||||||
required this.values,
|
required this.values,
|
||||||
this.disabledValues = const [],
|
this.onTap,
|
||||||
this.outline = false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
///初始值, 提供values的下标
|
final String current;
|
||||||
final int? initialValue;
|
|
||||||
|
|
||||||
///可供选取的值
|
|
||||||
final List<String> values;
|
final List<String> values;
|
||||||
|
|
||||||
///宽度
|
final void Function(int index)? onTap;
|
||||||
final double width;
|
|
||||||
|
|
||||||
///发生改变时的回调
|
|
||||||
final void Function(int) onChange;
|
|
||||||
|
|
||||||
/// 禁用的值
|
|
||||||
final List<int> disabledValues;
|
|
||||||
|
|
||||||
/// 是否为边框模式
|
|
||||||
final bool outline;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<Select> createState() => _SelectState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SelectState extends State<Select> {
|
|
||||||
late int? value = widget.initialValue;
|
|
||||||
bool isHover = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (value != null && value! < 0) value = null;
|
return Container(
|
||||||
return MouseRegion(
|
decoration: BoxDecoration(
|
||||||
onEnter: (_) => setState(() => isHover = true),
|
border: Border.all(color: context.colorScheme.outlineVariant),
|
||||||
onExit: (_) => setState(() => isHover = false),
|
borderRadius: BorderRadius.circular(4),
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
if (widget.values.isEmpty) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final renderBox = context.findRenderObject() as RenderBox;
|
|
||||||
var offset = renderBox.localToGlobal(Offset.zero);
|
|
||||||
var size = MediaQuery.of(context).size;
|
|
||||||
showMenu<int>(
|
|
||||||
context: App.rootNavigatorKey.currentContext!,
|
|
||||||
initialValue: value,
|
|
||||||
position: RelativeRect.fromLTRB(offset.dx, offset.dy,
|
|
||||||
offset.dx + widget.width, size.height - offset.dy),
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: widget.width,
|
|
||||||
minWidth: widget.width,
|
|
||||||
),
|
),
|
||||||
color: context.colorScheme.surfaceContainerLowest,
|
child: InkWell(
|
||||||
items: [
|
|
||||||
for (int i = 0; i < widget.values.length; i++)
|
|
||||||
if (!widget.disabledValues.contains(i))
|
|
||||||
PopupMenuItem(
|
|
||||||
value: i,
|
|
||||||
height: App.isDesktop ? 38 : 42,
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
var renderBox = context.findRenderObject() as RenderBox;
|
||||||
value = i;
|
var offset = renderBox.localToGlobal(Offset.zero);
|
||||||
widget.onChange(i);
|
var size = renderBox.size;
|
||||||
|
showMenu(
|
||||||
|
elevation: 3,
|
||||||
|
color: context.colorScheme.surface,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minWidth: size.width,
|
||||||
|
maxWidth: size.width,
|
||||||
|
),
|
||||||
|
position: RelativeRect.fromLTRB(
|
||||||
|
offset.dx,
|
||||||
|
offset.dy + size.height,
|
||||||
|
offset.dx + size.height,
|
||||||
|
offset.dy,
|
||||||
|
),
|
||||||
|
items: values
|
||||||
|
.map((e) => PopupMenuItem(
|
||||||
|
height: App.isMobile ? 46 : 40,
|
||||||
|
value: e,
|
||||||
|
child: Text(e),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
onTap?.call(values.indexOf(value));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: Text(widget.values[i]),
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
child: AnimatedContainer(
|
|
||||||
duration: _fastAnimationDuration,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: color,
|
|
||||||
borderRadius: BorderRadius.circular(widget.outline ? 4 : 8),
|
|
||||||
border: widget.outline
|
|
||||||
? Border.all(
|
|
||||||
color: context.colorScheme.outline,
|
|
||||||
width: 1,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
width: widget.width,
|
|
||||||
height: 38,
|
|
||||||
child: Row(
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
Text(current, style: ts.s14),
|
||||||
width: 12,
|
const SizedBox(width: 8),
|
||||||
),
|
const Icon(Icons.arrow_drop_down),
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
value == null ? "" : widget.values[value!],
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(Icons.arrow_drop_down_sharp),
|
|
||||||
const SizedBox(
|
|
||||||
width: 4,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
).padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 4)),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color get color {
|
|
||||||
if (widget.outline) {
|
|
||||||
return isHover
|
|
||||||
? context.colorScheme.outline.withOpacity(0.1)
|
|
||||||
: Colors.transparent;
|
|
||||||
} else {
|
|
||||||
var color = context.colorScheme.surfaceContainerHigh;
|
|
||||||
if (isHover) {
|
|
||||||
color = color.withOpacity(0.8);
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FilterChipFixedWidth extends StatefulWidget {
|
class FilterChipFixedWidth extends StatefulWidget {
|
||||||
|
@@ -12,49 +12,51 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
|
|
||||||
bool inProgress = false;
|
bool inProgress = false;
|
||||||
|
|
||||||
|
late _ReaderState reader;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
context.reader.isLoading = true;
|
reader = context.reader;
|
||||||
|
reader.isLoading = true;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void load() async {
|
void load() async {
|
||||||
if (inProgress) return;
|
if (inProgress) return;
|
||||||
inProgress = true;
|
inProgress = true;
|
||||||
if (context.reader.type == ComicType.local ||
|
if (reader.type == ComicType.local ||
|
||||||
(await LocalManager().isDownloaded(
|
(await LocalManager()
|
||||||
context.reader.cid, context.reader.type, context.reader.chapter))) {
|
.isDownloaded(reader.cid, reader.type, reader.chapter))) {
|
||||||
try {
|
try {
|
||||||
var images = await LocalManager().getImages(
|
var images = await LocalManager()
|
||||||
context.reader.cid, context.reader.type, context.reader.chapter);
|
.getImages(reader.cid, reader.type, reader.chapter);
|
||||||
setState(() {
|
setState(() {
|
||||||
context.reader.images = images;
|
reader.images = images;
|
||||||
context.reader.isLoading = false;
|
reader.isLoading = false;
|
||||||
inProgress = false;
|
inProgress = false;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() {
|
setState(() {
|
||||||
error = e.toString();
|
error = e.toString();
|
||||||
context.reader.isLoading = false;
|
reader.isLoading = false;
|
||||||
inProgress = false;
|
inProgress = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var res = await context.reader.type.comicSource!.loadComicPages!(
|
var res = await reader.type.comicSource!.loadComicPages!(
|
||||||
context.reader.widget.cid,
|
reader.widget.cid,
|
||||||
context.reader.widget.chapters?.keys
|
reader.widget.chapters?.keys.elementAt(reader.chapter - 1),
|
||||||
.elementAt(context.reader.chapter - 1),
|
|
||||||
);
|
);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
setState(() {
|
setState(() {
|
||||||
error = res.errorMessage;
|
error = res.errorMessage;
|
||||||
context.reader.isLoading = false;
|
reader.isLoading = false;
|
||||||
inProgress = false;
|
inProgress = false;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
context.reader.images = res.data;
|
reader.images = res.data;
|
||||||
context.reader.isLoading = false;
|
reader.isLoading = false;
|
||||||
inProgress = false;
|
inProgress = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -64,7 +66,7 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (context.reader.isLoading) {
|
if (reader.isLoading) {
|
||||||
load();
|
load();
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
@@ -74,14 +76,14 @@ class _ReaderImagesState extends State<_ReaderImages> {
|
|||||||
message: error!,
|
message: error!,
|
||||||
retry: () {
|
retry: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
context.reader.isLoading = true;
|
reader.isLoading = true;
|
||||||
error = null;
|
error = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (context.reader.mode.isGallery) {
|
if (reader.mode.isGallery) {
|
||||||
return _GalleryMode(key: Key(context.reader.mode.key));
|
return _GalleryMode(key: Key(reader.mode.key));
|
||||||
} else {
|
} else {
|
||||||
// TODO: Implement other modes
|
// TODO: Implement other modes
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
@@ -107,17 +109,20 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
var photoViewControllers = <int, PhotoViewController>{};
|
var photoViewControllers = <int, PhotoViewController>{};
|
||||||
|
|
||||||
|
late _ReaderState reader;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
controller = PageController(initialPage: context.reader.page);
|
reader = context.reader;
|
||||||
context.reader._imageViewController = this;
|
controller = PageController(initialPage: reader.page);
|
||||||
cached = List.filled(context.reader.maxPage + 2, false);
|
reader._imageViewController = this;
|
||||||
|
cached = List.filled(reader.maxPage + 2, false);
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void cache(int current) {
|
void cache(int current) {
|
||||||
for (int i = current + 1; i <= current + preCacheCount; i++) {
|
for (int i = current + 1; i <= current + preCacheCount; i++) {
|
||||||
if (i <= context.reader.maxPage && !cached[i]) {
|
if (i <= reader.maxPage && !cached[i]) {
|
||||||
_precacheImage(i, context);
|
_precacheImage(i, context);
|
||||||
cached[i] = true;
|
cached[i] = true;
|
||||||
}
|
}
|
||||||
@@ -130,14 +135,14 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
backgroundDecoration: BoxDecoration(
|
backgroundDecoration: BoxDecoration(
|
||||||
color: context.colorScheme.surface,
|
color: context.colorScheme.surface,
|
||||||
),
|
),
|
||||||
reverse: context.reader.mode == ReaderMode.galleryRightToLeft,
|
reverse: reader.mode == ReaderMode.galleryRightToLeft,
|
||||||
scrollDirection: context.reader.mode == ReaderMode.galleryTopToBottom
|
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
|
||||||
? Axis.vertical
|
? Axis.vertical
|
||||||
: Axis.horizontal,
|
: Axis.horizontal,
|
||||||
itemCount: context.reader.images!.length + 2,
|
itemCount: reader.images!.length + 2,
|
||||||
builder: (BuildContext context, int index) {
|
builder: (BuildContext context, int index) {
|
||||||
ImageProvider? imageProvider;
|
ImageProvider? imageProvider;
|
||||||
if (index != 0 && index != context.reader.images!.length + 1) {
|
if (index != 0 && index != reader.images!.length + 1) {
|
||||||
imageProvider = _createImageProvider(index, context);
|
imageProvider = _createImageProvider(index, context);
|
||||||
} else {
|
} else {
|
||||||
return PhotoViewGalleryPageOptions.customChild(
|
return PhotoViewGalleryPageOptions.customChild(
|
||||||
@@ -176,15 +181,15 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
),
|
),
|
||||||
onPageChanged: (i) {
|
onPageChanged: (i) {
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
if (!context.reader.toNextChapter()) {
|
if (!reader.toNextChapter()) {
|
||||||
context.reader.toPage(1);
|
reader.toPage(1);
|
||||||
}
|
}
|
||||||
} else if (i == context.reader.maxPage + 1) {
|
} else if (i == reader.maxPage + 1) {
|
||||||
if (!context.reader.toPrevChapter()) {
|
if (!reader.toPrevChapter()) {
|
||||||
context.reader.toPage(context.reader.maxPage);
|
reader.toPage(reader.maxPage);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
context.reader.setPage(i);
|
reader.setPage(i);
|
||||||
context.readerScaffold.update();
|
context.readerScaffold.update();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -210,21 +215,22 @@ class _GalleryModeState extends State<_GalleryMode>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void handleDoubleTap(Offset location) {
|
void handleDoubleTap(Offset location) {
|
||||||
var controller = photoViewControllers[context.reader.page]!;
|
var controller = photoViewControllers[reader.page]!;
|
||||||
controller.onDoubleClick?.call();
|
controller.onDoubleClick?.call();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageProvider _createImageProvider(int page, BuildContext context) {
|
ImageProvider _createImageProvider(int page, BuildContext context) {
|
||||||
var imageKey = context.reader.images![page-1];
|
var reader = context.reader;
|
||||||
if(imageKey.startsWith('file://')) {
|
var imageKey = reader.images![page - 1];
|
||||||
|
if (imageKey.startsWith('file://')) {
|
||||||
return FileImage(File(imageKey.replaceFirst("file://", '')));
|
return FileImage(File(imageKey.replaceFirst("file://", '')));
|
||||||
} else {
|
} else {
|
||||||
return ReaderImageProvider(
|
return ReaderImageProvider(
|
||||||
imageKey,
|
imageKey,
|
||||||
context.reader.type.comicSource!.key,
|
reader.type.comicSource!.key,
|
||||||
context.reader.cid,
|
reader.cid,
|
||||||
context.reader.eid,
|
reader.eid,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,10 +10,13 @@ import 'package:photo_view/photo_view_gallery.dart';
|
|||||||
import 'package:venera/components/components.dart';
|
import 'package:venera/components/components.dart';
|
||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/foundation/appdata.dart';
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
import 'package:venera/foundation/cache_manager.dart';
|
||||||
import 'package:venera/foundation/comic_type.dart';
|
import 'package:venera/foundation/comic_type.dart';
|
||||||
import 'package:venera/foundation/history.dart';
|
import 'package:venera/foundation/history.dart';
|
||||||
import 'package:venera/foundation/image_provider/reader_image.dart';
|
import 'package:venera/foundation/image_provider/reader_image.dart';
|
||||||
import 'package:venera/foundation/local.dart';
|
import 'package:venera/foundation/local.dart';
|
||||||
|
import 'package:venera/pages/settings/settings_page.dart';
|
||||||
|
import 'package:venera/utils/file_type.dart';
|
||||||
import 'package:venera/utils/io.dart';
|
import 'package:venera/utils/io.dart';
|
||||||
import 'package:venera/utils/translations.dart';
|
import 'package:venera/utils/translations.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
@@ -63,6 +63,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.only(top: context.padding.top),
|
padding: EdgeInsets.only(top: context.padding.top),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surface.withOpacity(0.82),
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: Colors.grey.withOpacity(0.5),
|
color: Colors.grey.withOpacity(0.5),
|
||||||
@@ -73,16 +74,20 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
const BackButton(),
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(context.reader.widget.name, style: ts.s18),
|
child: Text(context.reader.widget.name, style: ts.s18),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Tooltip(
|
||||||
|
message: "Settings".tl,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.settings),
|
||||||
|
onPressed: openSetting,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -191,7 +196,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
icon: context.reader.autoPageTurningTimer != null
|
icon: context.reader.autoPageTurningTimer != null
|
||||||
? const Icon(Icons.timer)
|
? const Icon(Icons.timer)
|
||||||
: const Icon(Icons.timer_sharp),
|
: const Icon(Icons.timer_sharp),
|
||||||
onPressed: context.reader.autoPageTurning,
|
onPressed: () {
|
||||||
|
context.reader.autoPageTurning();
|
||||||
|
update();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (context.reader.widget.chapters != null)
|
if (context.reader.widget.chapters != null)
|
||||||
@@ -226,6 +234,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
return BlurEffect(
|
return BlurEffect(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surface.withOpacity(0.82),
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: BorderSide(
|
||||||
color: Colors.grey.withOpacity(0.5),
|
color: Colors.grey.withOpacity(0.5),
|
||||||
@@ -243,7 +252,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
return Slider(
|
return Slider(
|
||||||
value: context.reader.page.toDouble(),
|
value: context.reader.page.toDouble(),
|
||||||
min: 1,
|
min: 1,
|
||||||
max: context.reader.maxPage.clamp(context.reader.page, 1 << 16).toDouble(),
|
max:
|
||||||
|
context.reader.maxPage.clamp(context.reader.page, 1 << 16).toDouble(),
|
||||||
divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16),
|
divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16),
|
||||||
onChanged: (i) {
|
onChanged: (i) {
|
||||||
context.reader.toPage(i.toInt());
|
context.reader.toPage(i.toInt());
|
||||||
@@ -285,18 +295,131 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void openChapterDrawer() {
|
void openChapterDrawer() {
|
||||||
// TODO
|
showSideBar(
|
||||||
|
context,
|
||||||
|
_ChaptersView(context.reader),
|
||||||
|
width: 400,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveCurrentImage() {
|
Future<Uint8List> _getCurrentImageData() async {
|
||||||
// TODO
|
var imageKey = context.reader.images![context.reader.page - 1];
|
||||||
|
if (imageKey.startsWith("file://")) {
|
||||||
|
return await File(imageKey.substring(7)).readAsBytes();
|
||||||
|
} else {
|
||||||
|
return (await CacheManager()
|
||||||
|
.findCache("$imageKey@${context.reader.type.comicSource!.key}"))!
|
||||||
|
.readAsBytes();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void share() {
|
void saveCurrentImage() async {
|
||||||
// TODO
|
var data = await _getCurrentImageData();
|
||||||
|
var fileType = detectFileType(data);
|
||||||
|
var filename = "${context.reader.page}${fileType.ext}";
|
||||||
|
saveFile(data: data, filename: filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
void share() async {
|
||||||
|
var data = await _getCurrentImageData();
|
||||||
|
var fileType = detectFileType(data);
|
||||||
|
var filename = "${context.reader.page}${fileType.ext}";
|
||||||
|
Share.shareFile(
|
||||||
|
data: data,
|
||||||
|
filename: filename,
|
||||||
|
mime: fileType.mime,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void openSetting() {
|
void openSetting() {
|
||||||
// TODO
|
showSideBar(
|
||||||
|
context,
|
||||||
|
ReaderSettings(
|
||||||
|
onChanged: (key) {
|
||||||
|
if(key == "readerMode") {
|
||||||
|
context.reader.mode = ReaderMode.fromKey(appdata.settings[key]);
|
||||||
|
App.rootContext.pop();
|
||||||
|
}
|
||||||
|
context.reader.update();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
width: 400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChaptersView extends StatefulWidget {
|
||||||
|
const _ChaptersView(this.reader);
|
||||||
|
|
||||||
|
final _ReaderState reader;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ChaptersView> createState() => _ChaptersViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChaptersViewState extends State<_ChaptersView> {
|
||||||
|
bool desc = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var chapters = widget.reader.widget.chapters!;
|
||||||
|
var current = widget.reader.chapter - 1;
|
||||||
|
return Scaffold(
|
||||||
|
body: SmoothCustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppbar(
|
||||||
|
title: Text("Chapters".tl),
|
||||||
|
actions: [
|
||||||
|
Tooltip(
|
||||||
|
message: "Click to change the order".tl,
|
||||||
|
child: TextButton.icon(
|
||||||
|
icon: Icon(
|
||||||
|
!desc ? Icons.arrow_upward : Icons.arrow_downward,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
label: Text(!desc ? "Ascending".tl : "Descending".tl),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
desc = !desc;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SliverList(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, index) {
|
||||||
|
if (desc) {
|
||||||
|
index = chapters.length - 1 - index;
|
||||||
|
}
|
||||||
|
var chapter = chapters.values.elementAt(index);
|
||||||
|
return ListTile(
|
||||||
|
shape: Border(
|
||||||
|
left: BorderSide(
|
||||||
|
color: current == index
|
||||||
|
? context.colorScheme.primary
|
||||||
|
: Colors.transparent,
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
chapter,
|
||||||
|
style: current == index
|
||||||
|
? ts.withColor(context.colorScheme.primary).bold
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
widget.reader.toChapter(index + 1);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
childCount: chapters.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
60
lib/pages/settings/reader.dart
Normal file
60
lib/pages/settings/reader.dart
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
part of 'settings_page.dart';
|
||||||
|
|
||||||
|
class ReaderSettings extends StatefulWidget {
|
||||||
|
const ReaderSettings({super.key, this.onChanged});
|
||||||
|
|
||||||
|
final void Function(String key)? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ReaderSettings> createState() => _ReaderSettingsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReaderSettingsState extends State<ReaderSettings> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SmoothCustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverAppbar(title: Text("Settings".tl)),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Tap to turn Pages".tl,
|
||||||
|
settingKey: "enableTapToTurnPages",
|
||||||
|
onChanged: () {
|
||||||
|
widget.onChanged?.call("enableTapToTurnPages");
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
_SwitchSetting(
|
||||||
|
title: "Page animation".tl,
|
||||||
|
settingKey: "enablePageAnimation",
|
||||||
|
onChanged: () {
|
||||||
|
widget.onChanged?.call("enablePageAnimation");
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
SelectSetting(
|
||||||
|
title: "Reading mode".tl,
|
||||||
|
settingKey: "readerMode",
|
||||||
|
optionTranslation: {
|
||||||
|
"galleryLeftToRight": "Gallery Left to Right".tl,
|
||||||
|
"galleryRightToLeft": "Gallery Right to Left".tl,
|
||||||
|
"galleryTopToBottom": "Gallery Top to Bottom".tl,
|
||||||
|
"continuousLeftToRight": "Continuous Left to Right".tl,
|
||||||
|
"continuousRightToLeft": "Continuous Right to Left".tl,
|
||||||
|
"continuousTopToBottom": "Continuous Top to Bottom".tl,
|
||||||
|
},
|
||||||
|
onChanged: () {
|
||||||
|
widget.onChanged?.call("readerMode");
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
_SliderSetting(
|
||||||
|
title: "Auto page turning interval".tl,
|
||||||
|
settingsIndex: "autoPageTurningInterval",
|
||||||
|
interval: 1,
|
||||||
|
min: 1,
|
||||||
|
max: 20,
|
||||||
|
onChanged: () {
|
||||||
|
widget.onChanged?.call("autoPageTurningInterval");
|
||||||
|
},
|
||||||
|
).toSliver(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
257
lib/pages/settings/setting_components.dart
Normal file
257
lib/pages/settings/setting_components.dart
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
part of 'settings_page.dart';
|
||||||
|
|
||||||
|
class _SwitchSetting extends StatefulWidget {
|
||||||
|
const _SwitchSetting({
|
||||||
|
required this.title,
|
||||||
|
this.subtitle,
|
||||||
|
required this.settingKey,
|
||||||
|
this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
final String? subtitle;
|
||||||
|
|
||||||
|
final String settingKey;
|
||||||
|
|
||||||
|
final VoidCallback? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SwitchSetting> createState() => _SwitchSettingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SwitchSettingState extends State<_SwitchSetting> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
assert(appdata.settings[widget.settingKey] is bool);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(widget.title),
|
||||||
|
subtitle: widget.subtitle == null ? null : Text(widget.subtitle!),
|
||||||
|
trailing: Switch(
|
||||||
|
value: appdata.settings[widget.settingKey],
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings[widget.settingKey] = value;
|
||||||
|
appdata.saveData();
|
||||||
|
});
|
||||||
|
widget.onChanged?.call();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectSetting extends StatelessWidget {
|
||||||
|
const SelectSetting({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.settingKey,
|
||||||
|
required this.optionTranslation,
|
||||||
|
this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
final String settingKey;
|
||||||
|
|
||||||
|
final Map<String, String> optionTranslation;
|
||||||
|
|
||||||
|
final VoidCallback? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
if (constraints.maxWidth < 450) {
|
||||||
|
return _DoubleLineSelectSettings(
|
||||||
|
title: title,
|
||||||
|
settingKey: settingKey,
|
||||||
|
optionTranslation: optionTranslation,
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _EndSelectorSelectSetting(
|
||||||
|
title: title,
|
||||||
|
settingKey: settingKey,
|
||||||
|
optionTranslation: optionTranslation,
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DoubleLineSelectSettings extends StatefulWidget {
|
||||||
|
const _DoubleLineSelectSettings({
|
||||||
|
required this.title,
|
||||||
|
required this.settingKey,
|
||||||
|
required this.optionTranslation,
|
||||||
|
this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
final String settingKey;
|
||||||
|
|
||||||
|
final Map<String, String> optionTranslation;
|
||||||
|
|
||||||
|
final VoidCallback? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_DoubleLineSelectSettings> createState() =>
|
||||||
|
_DoubleLineSelectSettingsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: Text(widget.title),
|
||||||
|
subtitle:
|
||||||
|
Text(widget.optionTranslation[appdata.settings[widget.settingKey]]!),
|
||||||
|
trailing: const Icon(Icons.arrow_drop_down),
|
||||||
|
onTap: () {
|
||||||
|
var renderBox = context.findRenderObject() as RenderBox;
|
||||||
|
var offset = renderBox.localToGlobal(Offset.zero);
|
||||||
|
var size = renderBox.size;
|
||||||
|
var rect = offset & size;
|
||||||
|
showMenu(
|
||||||
|
elevation: 3,
|
||||||
|
color: context.colorScheme.surface,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
context: context,
|
||||||
|
position: RelativeRect.fromRect(
|
||||||
|
rect,
|
||||||
|
Offset.zero & MediaQuery.of(context).size,
|
||||||
|
),
|
||||||
|
items: widget.optionTranslation.keys
|
||||||
|
.map((key) => PopupMenuItem(
|
||||||
|
value: key,
|
||||||
|
height: App.isMobile ? 46 : 40,
|
||||||
|
child: Text(widget.optionTranslation[key]!),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
).then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings[widget.settingKey] = value;
|
||||||
|
});
|
||||||
|
appdata.saveData();
|
||||||
|
widget.onChanged?.call();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EndSelectorSelectSetting extends StatefulWidget {
|
||||||
|
const _EndSelectorSelectSetting({
|
||||||
|
required this.title,
|
||||||
|
required this.settingKey,
|
||||||
|
required this.optionTranslation,
|
||||||
|
this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
final String settingKey;
|
||||||
|
|
||||||
|
final Map<String, String> optionTranslation;
|
||||||
|
|
||||||
|
final VoidCallback? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_EndSelectorSelectSetting> createState() =>
|
||||||
|
_EndSelectorSelectSettingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var options = widget.optionTranslation;
|
||||||
|
return ListTile(
|
||||||
|
title: Text(widget.title),
|
||||||
|
trailing: Select(
|
||||||
|
current: options[appdata.settings[widget.settingKey]]!,
|
||||||
|
values: options.values.toList(),
|
||||||
|
onTap: (index) {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings[widget.settingKey] = options.keys.elementAt(index);
|
||||||
|
});
|
||||||
|
appdata.saveData();
|
||||||
|
widget.onChanged?.call();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliderSetting extends StatefulWidget {
|
||||||
|
const _SliderSetting({
|
||||||
|
required this.title,
|
||||||
|
required this.settingsIndex,
|
||||||
|
required this.interval,
|
||||||
|
required this.min,
|
||||||
|
required this.max,
|
||||||
|
this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
final String settingsIndex;
|
||||||
|
|
||||||
|
final double interval;
|
||||||
|
|
||||||
|
final double min;
|
||||||
|
|
||||||
|
final double max;
|
||||||
|
|
||||||
|
final VoidCallback? onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SliderSetting> createState() => _SliderSettingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SliderSettingState extends State<_SliderSetting> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Text(widget.title),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
appdata.settings[widget.settingsIndex].toString(),
|
||||||
|
style: ts.s12,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Slider(
|
||||||
|
value: appdata.settings[widget.settingsIndex].toDouble(),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value.toInt() == value) {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings[widget.settingsIndex] = value.toInt();
|
||||||
|
appdata.saveData();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
appdata.settings[widget.settingsIndex] = value;
|
||||||
|
appdata.saveData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
widget.onChanged?.call();
|
||||||
|
},
|
||||||
|
divisions: ((widget.max - widget.min) / widget.interval).toInt(),
|
||||||
|
min: widget.min,
|
||||||
|
max: widget.max,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
8
lib/pages/settings/settings_page.dart
Normal file
8
lib/pages/settings/settings_page.dart
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:venera/components/components.dart';
|
||||||
|
import 'package:venera/foundation/app.dart';
|
||||||
|
import 'package:venera/foundation/appdata.dart';
|
||||||
|
import 'package:venera/utils/translations.dart';
|
||||||
|
|
||||||
|
part 'reader.dart';
|
||||||
|
part 'setting_components.dart';
|
19
lib/utils/file_type.dart
Normal file
19
lib/utils/file_type.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:mime/mime.dart';
|
||||||
|
|
||||||
|
class FileType {
|
||||||
|
final String ext;
|
||||||
|
final String mime;
|
||||||
|
|
||||||
|
const FileType(this.ext, this.mime);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileType detectFileType(List<int> data) {
|
||||||
|
var mime = lookupMimeType('no-file', headerBytes: data);
|
||||||
|
var ext = mime == null ? '' : extensionFromMime(mime);
|
||||||
|
if(ext == 'jpe') {
|
||||||
|
ext = 'jpg';
|
||||||
|
}
|
||||||
|
return FileType(".$ext", mime ?? 'application/octet-stream');
|
||||||
|
}
|
@@ -6,14 +6,15 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:venera/foundation/app.dart';
|
import 'package:venera/foundation/app.dart';
|
||||||
import 'package:venera/utils/ext.dart';
|
import 'package:venera/utils/ext.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:share_plus/share_plus.dart' as s;
|
||||||
|
|
||||||
export 'dart:io';
|
export 'dart:io';
|
||||||
|
|
||||||
|
|
||||||
class FilePath {
|
class FilePath {
|
||||||
const FilePath._();
|
const FilePath._();
|
||||||
|
|
||||||
static String join(String path1, String path2, [String? path3, String? path4, String? path5]) {
|
static String join(String path1, String path2,
|
||||||
|
[String? path3, String? path4, String? path5]) {
|
||||||
return p.join(path1, path2, path3, path4, path5);
|
return p.join(path1, path2, path3, path4, path5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +122,7 @@ class DirectoryPicker {
|
|||||||
final _methodChannel = const MethodChannel("venera/method_channel");
|
final _methodChannel = const MethodChannel("venera/method_channel");
|
||||||
|
|
||||||
Future<Directory?> pickDirectory() async {
|
Future<Directory?> pickDirectory() async {
|
||||||
if(App.isWindows || App.isLinux) {
|
if (App.isWindows || App.isLinux) {
|
||||||
var d = await FilePicker.platform.getDirectoryPath();
|
var d = await FilePicker.platform.getDirectoryPath();
|
||||||
_directory = d;
|
_directory = d;
|
||||||
return d == null ? null : Directory(d);
|
return d == null ? null : Directory(d);
|
||||||
@@ -138,15 +139,50 @@ class DirectoryPicker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
if(_directory == null) {
|
if (_directory == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(App.isAndroid && _directory != null) {
|
if (App.isAndroid && _directory != null) {
|
||||||
return Directory(_directory!).deleteIgnoreError(recursive: true);
|
return Directory(_directory!).deleteIgnoreError(recursive: true);
|
||||||
}
|
}
|
||||||
if(App.isIOS || App.isMacOS) {
|
if (App.isIOS || App.isMacOS) {
|
||||||
await _methodChannel.invokeMethod("stopAccessingSecurityScopedResource");
|
await _methodChannel.invokeMethod("stopAccessingSecurityScopedResource");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> saveFile(
|
||||||
|
{required Uint8List data, required String filename}) async {
|
||||||
|
var res = await FilePicker.platform.saveFile(
|
||||||
|
bytes: data,
|
||||||
|
fileName: filename,
|
||||||
|
lockParentWindow: true,
|
||||||
|
);
|
||||||
|
if (App.isDesktop && res != null) {
|
||||||
|
await File(res).writeAsBytes(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Share {
|
||||||
|
static void shareFile({
|
||||||
|
required Uint8List data,
|
||||||
|
required String filename,
|
||||||
|
required String mime,
|
||||||
|
}) {
|
||||||
|
if (!App.isWindows) {
|
||||||
|
s.Share.shareXFiles(
|
||||||
|
[s.XFile.fromData(data, mimeType: mime)],
|
||||||
|
fileNameOverrides: [filename],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// write to cache
|
||||||
|
var file = File(FilePath.join(Directory.systemTemp.path, filename));
|
||||||
|
file.writeAsBytesSync(data);
|
||||||
|
s.Share.shareXFiles([s.XFile(file.path)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void shareText(String text) {
|
||||||
|
s.Share.share(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import Foundation
|
|||||||
|
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import screen_retriever
|
import screen_retriever
|
||||||
|
import share_plus
|
||||||
import sqlite3_flutter_libs
|
import sqlite3_flutter_libs
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
import window_manager
|
import window_manager
|
||||||
@@ -14,6 +15,7 @@ import window_manager
|
|||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
|
||||||
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||||
|
56
pubspec.lock
56
pubspec.lock
@@ -105,6 +105,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
file_picker:
|
file_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -113,6 +121,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.2"
|
version: "8.1.2"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -246,6 +262,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.0"
|
version: "1.12.0"
|
||||||
|
mime:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.6"
|
||||||
path:
|
path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -343,6 +367,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.9"
|
version: "0.1.9"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.0.2"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -356,6 +396,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.10.0"
|
||||||
|
sprintf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sprintf
|
||||||
|
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
sqlite3:
|
sqlite3:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -484,6 +532,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.1"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@@ -33,6 +33,8 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/wgh136/photo_view
|
url: https://github.com/wgh136/photo_view
|
||||||
ref: 94724a0b
|
ref: 94724a0b
|
||||||
|
mime: ^1.0.5
|
||||||
|
share_plus: ^10.0.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <flutter_qjs/flutter_qjs_plugin.h>
|
#include <flutter_qjs/flutter_qjs_plugin.h>
|
||||||
#include <screen_retriever/screen_retriever_plugin.h>
|
#include <screen_retriever/screen_retriever_plugin.h>
|
||||||
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
@@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("FlutterQjsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterQjsPlugin"));
|
||||||
ScreenRetrieverPluginRegisterWithRegistrar(
|
ScreenRetrieverPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
||||||
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
flutter_qjs
|
flutter_qjs
|
||||||
screen_retriever
|
screen_retriever
|
||||||
|
share_plus
|
||||||
sqlite3_flutter_libs
|
sqlite3_flutter_libs
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
window_manager
|
window_manager
|
||||||
|
Reference in New Issue
Block a user