diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index 6ef61d8..adfc96d 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -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? 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,41 +172,44 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate { Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return SizedBox.expand( - child: Material( - color: color, - elevation: 0, - borderRadius: BorderRadius.circular(radius), - child: Row( - children: [ - const SizedBox(width: 8), - leading ?? - (Navigator.of(context).canPop() - ? Tooltip( - message: "返回".tl, - child: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.pop(context), - ), - ) - : const SizedBox()), - const SizedBox( - width: 24, - ), - Expanded( - child: DefaultTextStyle( - style: - DefaultTextStyle.of(context).style.copyWith(fontSize: 20), - maxLines: 1, - overflow: TextOverflow.ellipsis, - child: title, + child: BlurEffect( + blur: 15, + child: Material( + color: context.colorScheme.surface.withOpacity(0.72), + elevation: 0, + borderRadius: BorderRadius.circular(radius), + child: Row( + children: [ + const SizedBox(width: 8), + leading ?? + (Navigator.of(context).canPop() + ? Tooltip( + message: "Back".tl, + child: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + ) + : const SizedBox()), + const SizedBox( + width: 24, ), - ), - ...?actions, - const SizedBox( - width: 8, - ) - ], - ).paddingTop(topPadding), + Expanded( + child: DefaultTextStyle( + style: + DefaultTextStyle.of(context).style.copyWith(fontSize: 20), + maxLines: 1, + overflow: TextOverflow.ellipsis, + child: title, + ), + ), + ...?actions, + const SizedBox( + width: 8, + ) + ], + ).paddingTop(topPadding), + ), ), ); } diff --git a/lib/components/select.dart b/lib/components/select.dart index 5327b7c..9e63467 100644 --- a/lib/components/select.dart +++ b/lib/components/select.dart @@ -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 values; - ///宽度 - final double width; - - ///发生改变时的回调 - final void Function(int) onChange; - - /// 禁用的值 - final List disabledValues; - - /// 是否为边框模式 - final bool outline; - - @override - State { - 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( + return Container( + decoration: BoxDecoration( + border: Border.all(color: context.colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(4), + ), + child: InkWell( onTap: () { - if (widget.values.isEmpty) { - return; - } - final renderBox = context.findRenderObject() as RenderBox; + var renderBox = context.findRenderObject() as RenderBox; var offset = renderBox.localToGlobal(Offset.zero); - var size = MediaQuery.of(context).size; - showMenu( - 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, - items: [ - for (int i = 0; i < widget.values.length; i++) - if (!widget.disabledValues.contains(i)) - PopupMenuItem( - value: i, - height: App.isDesktop ? 38 : 42, - onTap: () { - setState(() { - value = i; - widget.onChange(i); - }); - }, - child: Text(widget.values[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: 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( - 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, - ), - ], - ), - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + 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 { diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 3cf602c..69d70d3 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -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 = {}; + 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, ); } } diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 956879d..092eb71 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -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'; diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 0c78327..378435b 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -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 _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, + ), + ), + ], + ), + ); } } diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart new file mode 100644 index 0000000..d137a69 --- /dev/null +++ b/lib/pages/settings/reader.dart @@ -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 createState() => _ReaderSettingsState(); +} + +class _ReaderSettingsState extends State { + @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(), + ], + ); + } +} diff --git a/lib/pages/settings/setting_components.dart b/lib/pages/settings/setting_components.dart new file mode 100644 index 0000000..f9880e6 --- /dev/null +++ b/lib/pages/settings/setting_components.dart @@ -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 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 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 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, + ), + ); + } +} diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart new file mode 100644 index 0000000..ba2beca --- /dev/null +++ b/lib/pages/settings/settings_page.dart @@ -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'; \ No newline at end of file diff --git a/lib/utils/file_type.dart b/lib/utils/file_type.dart new file mode 100644 index 0000000..124c9be --- /dev/null +++ b/lib/utils/file_type.dart @@ -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 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'); +} \ No newline at end of file diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 3c7a5db..c99e98b 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -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 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 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 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); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index afbcf04..e95ebaa 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/pubspec.lock b/pubspec.lock index 31bab90..3538693 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 2e46a11..fea04f3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8dc57f9..3175947 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -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( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 9c08e60..f8a619c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_qjs screen_retriever + share_plus sqlite3_flutter_libs url_launcher_windows window_manager