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

View File

@@ -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 {

View File

@@ -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;
var imageKey = reader.images![page - 1];
if (imageKey.startsWith('file://')) { 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,
); );
} }
} }

View File

@@ -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';

View File

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

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/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);
} }
} }
@@ -150,3 +151,38 @@ class DirectoryPicker {
} }
} }
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 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"))

View File

@@ -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:

View File

@@ -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:

View File

@@ -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(

View File

@@ -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