implement saving image, sharing image, reading settings and chapters view

This commit is contained in:
nyne
2024-10-08 16:52:20 +08:00
parent b44998663a
commit 5deb71e10a
15 changed files with 723 additions and 213 deletions

View File

@@ -122,7 +122,6 @@ class SliverAppbar extends StatelessWidget {
required this.title,
this.leading,
this.actions,
this.color,
this.radius = 0,
});
@@ -132,8 +131,6 @@ class SliverAppbar extends StatelessWidget {
final List<Widget>? actions;
final Color? color;
final double radius;
@override
@@ -145,14 +142,13 @@ class SliverAppbar extends StatelessWidget {
title: title,
actions: actions,
topPadding: MediaQuery.of(context).padding.top,
color: color,
radius: radius,
),
);
}
}
const _kAppBarHeight = 58.0;
const _kAppBarHeight = 52.0;
class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final Widget? leading;
@@ -163,15 +159,12 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final double topPadding;
final Color? color;
final double radius;
_MySliverAppBarDelegate(
{this.leading,
required this.title,
this.actions,
this.color,
required this.topPadding,
this.radius = 0});
@@ -179,8 +172,10 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(
child: BlurEffect(
blur: 15,
child: Material(
color: color,
color: context.colorScheme.surface.withOpacity(0.72),
elevation: 0,
borderRadius: BorderRadius.circular(radius),
child: Row(
@@ -189,7 +184,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
leading ??
(Navigator.of(context).canPop()
? Tooltip(
message: "返回".tl,
message: "Back".tl,
child: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
@@ -215,6 +210,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
],
).paddingTop(topPadding),
),
),
);
}

View File

@@ -1,133 +1,71 @@
part of 'components.dart';
class Select extends StatefulWidget {
class Select extends StatelessWidget {
const Select({
required this.initialValue,
this.width = 120,
required this.onChange,
super.key,
required this.current,
required this.values,
this.disabledValues = const [],
this.outline = false,
this.onTap,
});
///初始值, 提供values的下标
final int? initialValue;
final String current;
///可供选取的值
final List<String> values;
///宽度
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;
final void Function(int index)? onTap;
@override
Widget build(BuildContext context) {
if (value != null && value! < 0) value = null;
return MouseRegion(
onEnter: (_) => setState(() => isHover = true),
onExit: (_) => setState(() => isHover = false),
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,
return Container(
decoration: BoxDecoration(
border: Border.all(color: context.colorScheme.outlineVariant),
borderRadius: BorderRadius.circular(4),
),
color: context.colorScheme.surfaceContainerLowest,
items: [
for (int i = 0; i < widget.values.length; i++)
if (!widget.disabledValues.contains(i))
PopupMenuItem(
value: i,
height: App.isDesktop ? 38 : 42,
child: InkWell(
onTap: () {
setState(() {
value = i;
widget.onChange(i);
var renderBox = context.findRenderObject() as RenderBox;
var offset = renderBox.localToGlobal(Offset.zero);
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(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
width: 12,
),
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,
),
Text(current, style: ts.s14),
const SizedBox(width: 8),
const Icon(Icons.arrow_drop_down),
],
),
),
).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 {

View File

@@ -12,49 +12,51 @@ class _ReaderImagesState extends State<_ReaderImages> {
bool inProgress = false;
late _ReaderState reader;
@override
void initState() {
context.reader.isLoading = true;
reader = context.reader;
reader.isLoading = true;
super.initState();
}
void load() async {
if (inProgress) return;
inProgress = true;
if (context.reader.type == ComicType.local ||
(await LocalManager().isDownloaded(
context.reader.cid, context.reader.type, context.reader.chapter))) {
if (reader.type == ComicType.local ||
(await LocalManager()
.isDownloaded(reader.cid, reader.type, reader.chapter))) {
try {
var images = await LocalManager().getImages(
context.reader.cid, context.reader.type, context.reader.chapter);
var images = await LocalManager()
.getImages(reader.cid, reader.type, reader.chapter);
setState(() {
context.reader.images = images;
context.reader.isLoading = false;
reader.images = images;
reader.isLoading = false;
inProgress = false;
});
} catch (e) {
setState(() {
error = e.toString();
context.reader.isLoading = false;
reader.isLoading = false;
inProgress = false;
});
}
} else {
var res = await context.reader.type.comicSource!.loadComicPages!(
context.reader.widget.cid,
context.reader.widget.chapters?.keys
.elementAt(context.reader.chapter - 1),
var res = await reader.type.comicSource!.loadComicPages!(
reader.widget.cid,
reader.widget.chapters?.keys.elementAt(reader.chapter - 1),
);
if (res.error) {
setState(() {
error = res.errorMessage;
context.reader.isLoading = false;
reader.isLoading = false;
inProgress = false;
});
} else {
setState(() {
context.reader.images = res.data;
context.reader.isLoading = false;
reader.images = res.data;
reader.isLoading = false;
inProgress = false;
});
}
@@ -64,7 +66,7 @@ class _ReaderImagesState extends State<_ReaderImages> {
@override
Widget build(BuildContext context) {
if (context.reader.isLoading) {
if (reader.isLoading) {
load();
return const Center(
child: CircularProgressIndicator(),
@@ -74,14 +76,14 @@ class _ReaderImagesState extends State<_ReaderImages> {
message: error!,
retry: () {
setState(() {
context.reader.isLoading = true;
reader.isLoading = true;
error = null;
});
},
);
} else {
if (context.reader.mode.isGallery) {
return _GalleryMode(key: Key(context.reader.mode.key));
if (reader.mode.isGallery) {
return _GalleryMode(key: Key(reader.mode.key));
} else {
// TODO: Implement other modes
throw UnimplementedError();
@@ -107,17 +109,20 @@ class _GalleryModeState extends State<_GalleryMode>
var photoViewControllers = <int, PhotoViewController>{};
late _ReaderState reader;
@override
void initState() {
controller = PageController(initialPage: context.reader.page);
context.reader._imageViewController = this;
cached = List.filled(context.reader.maxPage + 2, false);
reader = context.reader;
controller = PageController(initialPage: reader.page);
reader._imageViewController = this;
cached = List.filled(reader.maxPage + 2, false);
super.initState();
}
void cache(int current) {
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);
cached[i] = true;
}
@@ -130,14 +135,14 @@ class _GalleryModeState extends State<_GalleryMode>
backgroundDecoration: BoxDecoration(
color: context.colorScheme.surface,
),
reverse: context.reader.mode == ReaderMode.galleryRightToLeft,
scrollDirection: context.reader.mode == ReaderMode.galleryTopToBottom
reverse: reader.mode == ReaderMode.galleryRightToLeft,
scrollDirection: reader.mode == ReaderMode.galleryTopToBottom
? Axis.vertical
: Axis.horizontal,
itemCount: context.reader.images!.length + 2,
itemCount: reader.images!.length + 2,
builder: (BuildContext context, int index) {
ImageProvider? imageProvider;
if (index != 0 && index != context.reader.images!.length + 1) {
if (index != 0 && index != reader.images!.length + 1) {
imageProvider = _createImageProvider(index, context);
} else {
return PhotoViewGalleryPageOptions.customChild(
@@ -176,15 +181,15 @@ class _GalleryModeState extends State<_GalleryMode>
),
onPageChanged: (i) {
if (i == 0) {
if (!context.reader.toNextChapter()) {
context.reader.toPage(1);
if (!reader.toNextChapter()) {
reader.toPage(1);
}
} else if (i == context.reader.maxPage + 1) {
if (!context.reader.toPrevChapter()) {
context.reader.toPage(context.reader.maxPage);
} else if (i == reader.maxPage + 1) {
if (!reader.toPrevChapter()) {
reader.toPage(reader.maxPage);
}
} else {
context.reader.setPage(i);
reader.setPage(i);
context.readerScaffold.update();
}
},
@@ -210,21 +215,22 @@ class _GalleryModeState extends State<_GalleryMode>
@override
void handleDoubleTap(Offset location) {
var controller = photoViewControllers[context.reader.page]!;
var controller = photoViewControllers[reader.page]!;
controller.onDoubleClick?.call();
}
}
ImageProvider _createImageProvider(int page, BuildContext context) {
var imageKey = context.reader.images![page-1];
if(imageKey.startsWith('file://')) {
var reader = context.reader;
var imageKey = reader.images![page - 1];
if (imageKey.startsWith('file://')) {
return FileImage(File(imageKey.replaceFirst("file://", '')));
} else {
return ReaderImageProvider(
imageKey,
context.reader.type.comicSource!.key,
context.reader.cid,
context.reader.eid,
reader.type.comicSource!.key,
reader.cid,
reader.eid,
);
}
}

View File

@@ -10,10 +10,13 @@ import 'package:photo_view/photo_view_gallery.dart';
import 'package:venera/components/components.dart';
import 'package:venera/foundation/app.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/history.dart';
import 'package:venera/foundation/image_provider/reader_image.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/translations.dart';
import 'package:window_manager/window_manager.dart';

View File

@@ -63,6 +63,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
child: Container(
padding: EdgeInsets.only(top: context.padding.top),
decoration: BoxDecoration(
color: context.colorScheme.surface.withOpacity(0.82),
border: Border(
bottom: BorderSide(
color: Colors.grey.withOpacity(0.5),
@@ -73,16 +74,20 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
child: Row(
children: [
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
const BackButton(),
const SizedBox(width: 8),
Expanded(
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
? const Icon(Icons.timer)
: const Icon(Icons.timer_sharp),
onPressed: context.reader.autoPageTurning,
onPressed: () {
context.reader.autoPageTurning();
update();
},
),
),
if (context.reader.widget.chapters != null)
@@ -226,6 +234,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
return BlurEffect(
child: Container(
decoration: BoxDecoration(
color: context.colorScheme.surface.withOpacity(0.82),
border: Border(
top: BorderSide(
color: Colors.grey.withOpacity(0.5),
@@ -243,7 +252,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
return Slider(
value: context.reader.page.toDouble(),
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),
onChanged: (i) {
context.reader.toPage(i.toInt());
@@ -285,18 +295,131 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
}
void openChapterDrawer() {
// TODO
showSideBar(
context,
_ChaptersView(context.reader),
width: 400,
);
}
void saveCurrentImage() {
// TODO
Future<Uint8List> _getCurrentImageData() async {
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() {
// TODO
void saveCurrentImage() async {
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() {
// 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,
),
),
],
),
);
}
}

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

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

View 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
View 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');
}

View File

@@ -6,14 +6,15 @@ import 'package:flutter/services.dart';
import 'package:venera/foundation/app.dart';
import 'package:venera/utils/ext.dart';
import 'package:path/path.dart' as p;
import 'package:share_plus/share_plus.dart' as s;
export 'dart:io';
class 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);
}
}
@@ -121,7 +122,7 @@ class DirectoryPicker {
final _methodChannel = const MethodChannel("venera/method_channel");
Future<Directory?> pickDirectory() async {
if(App.isWindows || App.isLinux) {
if (App.isWindows || App.isLinux) {
var d = await FilePicker.platform.getDirectoryPath();
_directory = d;
return d == null ? null : Directory(d);
@@ -138,15 +139,50 @@ class DirectoryPicker {
}
Future<void> dispose() async {
if(_directory == null) {
if (_directory == null) {
return;
}
if(App.isAndroid && _directory != null) {
if (App.isAndroid && _directory != null) {
return Directory(_directory!).deleteIgnoreError(recursive: true);
}
if(App.isIOS || App.isMacOS) {
if (App.isIOS || App.isMacOS) {
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);
}
}

View File

@@ -7,6 +7,7 @@ import Foundation
import path_provider_foundation
import screen_retriever
import share_plus
import sqlite3_flutter_libs
import url_launcher_macos
import window_manager
@@ -14,6 +15,7 @@ import window_manager
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))

View File

@@ -105,6 +105,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.3"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
file_picker:
dependency: "direct main"
description:
@@ -113,6 +121,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.1.2"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
@@ -246,6 +262,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.12.0"
mime:
dependency: "direct main"
description:
name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
path:
dependency: "direct main"
description:
@@ -343,6 +367,22 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description: flutter
@@ -356,6 +396,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
sqlite3:
dependency: "direct main"
description:
@@ -484,6 +532,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_math:
dependency: transitive
description:

View File

@@ -33,6 +33,8 @@ dependencies:
git:
url: https://github.com/wgh136/photo_view
ref: 94724a0b
mime: ^1.0.5
share_plus: ^10.0.2
dev_dependencies:
flutter_test:

View File

@@ -8,6 +8,7 @@
#include <flutter_qjs/flutter_qjs_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 <url_launcher_windows/url_launcher_windows.h>
#include <window_manager/window_manager_plugin.h>
@@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterQjsPlugin"));
ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(

View File

@@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_qjs
screen_retriever
share_plus
sqlite3_flutter_libs
url_launcher_windows
window_manager