diff --git a/lib/api/client.dart b/lib/api/client.dart index 160cd47..c5cec64 100644 --- a/lib/api/client.dart +++ b/lib/api/client.dart @@ -2,6 +2,8 @@ import 'dart:convert'; import 'package:cryptography/cryptography.dart'; import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:retrofit/retrofit.dart'; import 'api_result.dart'; @@ -32,11 +34,25 @@ final _pbkdf2b = Pbkdf2( const _utf8Encoder = Utf8Encoder(); final _salt = _utf8Encoder.convert("eh-downloader-salt"); -enum ThumbnailMethod { +enum ThumbnailGenMethod { unknown, cover, contain, fill; + + String localText(BuildContext context) { + final i18n = AppLocalizations.of(context)!; + switch (this) { + case ThumbnailGenMethod.unknown: + return i18n.default0; + case ThumbnailGenMethod.cover: + return i18n.thumbnailCover; + case ThumbnailGenMethod.contain: + return i18n.thumbnailContain; + case ThumbnailGenMethod.fill: + return i18n.thumbnailFill; + } + } } enum ThumbnailAlign { @@ -46,6 +62,17 @@ enum ThumbnailAlign { static const top = left; static const bottom = right; + String localText(BuildContext context) { + final i18n = AppLocalizations.of(context)!; + switch (this) { + case ThumbnailAlign.left: + return i18n.leftOrTop; + case ThumbnailAlign.center: + return i18n.center; + case ThumbnailAlign.right: + return i18n.rightOrBottom; + } + } } enum SortByGid { @@ -193,7 +220,7 @@ abstract class _EHApi { @Query("height") int? height, @Query("quality") int? quality, @Query("force") bool? force, - @Query("method") ThumbnailMethod? method, + @Query("method") ThumbnailGenMethod? method, @Query("align") ThumbnailAlign? align, @CancelRequest() CancelToken? cancel}); @@ -369,7 +396,7 @@ class EHApi extends __EHApi { int? height, int? quality, bool? force, - ThumbnailMethod? method, + ThumbnailGenMethod? method, ThumbnailAlign? align}) { final uri = Uri.parse(_combineBaseUrls(_dio.options.baseUrl, baseUrl)); final queryParameters = { diff --git a/lib/api/client.g.dart b/lib/api/client.g.dart index 58a1eb7..62b5f2d 100644 --- a/lib/api/client.g.dart +++ b/lib/api/client.g.dart @@ -808,7 +808,7 @@ class __EHApi implements _EHApi { int? height, int? quality, bool? force, - ThumbnailMethod? method, + ThumbnailGenMethod? method, ThumbnailAlign? align, CancelToken? cancel, }) async { diff --git a/lib/components/thumbnail.dart b/lib/components/thumbnail.dart index 4fb4370..ce20f47 100644 --- a/lib/components/thumbnail.dart +++ b/lib/components/thumbnail.dart @@ -30,9 +30,11 @@ class Thumbnail extends StatefulWidget { this.gdata, this.isSelectMode = false, this.isSelected = false, - this.onSelectedChange}) + this.onSelectedChange, + this.align, + this.method}) : _pMeta = pMeta, - _max = max ?? 1200, + _max = max ?? 400, _width = width, _height = height, _fileId = fileId; @@ -48,6 +50,8 @@ class Thumbnail extends StatefulWidget { final bool isSelectMode; final bool isSelected; final ValueChanged? onSelectedChange; + final ThumbnailGenMethod? method; + final ThumbnailAlign? align; int get height => _height != null ? _height! @@ -134,8 +138,8 @@ class _Thumbnail extends State { max: widget._max, width: widget._width, height: widget._height, - method: ThumbnailMethod.contain, - align: ThumbnailAlign.center); + method: widget.method, + align: widget.align); if (isImageCacheEnabled) { try { final cache = await imageCaches.getCache(_originalUrl!); @@ -156,8 +160,8 @@ class _Thumbnail extends State { max: widget._max, width: widget._width, height: widget._height, - method: ThumbnailMethod.contain, - align: ThumbnailAlign.center, + method: widget.method, + align: widget.align, cancel: _cancel); if (re.response.statusCode != 200) { throw Exception( diff --git a/lib/components/thumbnail_gridview.dart b/lib/components/thumbnail_gridview.dart index c9d24f4..773be1a 100644 --- a/lib/components/thumbnail_gridview.dart +++ b/lib/components/thumbnail_gridview.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../api/client.dart'; import '../api/file.dart'; import '../api/gallery.dart'; import '../globals.dart'; @@ -27,6 +28,18 @@ class ThumbnailGridView extends StatelessWidget { displayAd ? gdata.pages : gdata.pages.where((e) => !e.isAd).toList(); final baseSize = gridDelegate.maxCrossAxisExtent.toInt(); final max = (baseSize * MediaQuery.of(context).devicePixelRatio).toInt(); + final tgen = prefs.getInt("thumbnailMethod") ?? 0; + final gen = tgen >= 0 && tgen < ThumbnailGenMethod.values.length + ? ThumbnailGenMethod.values[tgen] + : ThumbnailGenMethod.unknown; + final talign = prefs.getInt("thumbnailAlign") ?? 1; + final align = talign >= 0 && talign < ThumbnailAlign.values.length + ? ThumbnailAlign.values[talign] + : ThumbnailAlign.center; + final isDefault = gen == ThumbnailGenMethod.unknown; + final alignUseless = isDefault || gen == ThumbnailGenMethod.fill; + final methodKey = + alignUseless ? "${align.index}" : "${align.index}-${align.index}"; return SliverGrid.builder( gridDelegate: gridDelegate, itemCount: npages.length, @@ -34,7 +47,8 @@ class ThumbnailGridView extends StatelessWidget { final page = npages[index]!; final fileId = files != null ? files!.files[page.token]!.firstOrNull?.id : null; - final key = Key("thumbnail$gid-${page.index}-$fileId-$max"); + final key = + Key("thumbnail$gid-${page.index}-$fileId-$max-$methodKey"); return Container( padding: const EdgeInsets.all(4), child: Thumbnail( @@ -58,6 +72,10 @@ class ThumbnailGridView extends StatelessWidget { } }, max: max, + width: isDefault ? null : max, + height: isDefault ? null : max, + method: isDefault ? null : gen, + align: alignUseless ? null : align, )); }); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7b1f12b..eab6e62 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -256,5 +256,15 @@ "copyThenDelete": "Copy then delete", "move": "Move", "keep": "Keep", - "maxImportImgCount": "Maximum number of images to be imported in parallel" + "maxImportImgCount": "Maximum number of images to be imported in parallel", + "thumbnailScaleMethod": "Thumbnail scale method", + "default0": "Default", + "thumbnailFill": "Stretch to container", + "thumbnailCover": "Touch container from outside", + "thumbnailContain": "Touch container from inside", + "thumbnailAlign": "Thumbnail alignment", + "leftOrTop": "Left / Top", + "center": "Center", + "rightOrBottom": "Right / Bottom", + "thumbnailAlignHelp": "This only works if the scaling method is to touch container from outside/inside." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f7ff268..7e24ebe 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -256,5 +256,15 @@ "copyThenDelete": "复制后删除", "move": "移动", "keep": "保留", - "maxImportImgCount": "最大图片并行导入数" + "maxImportImgCount": "最大图片并行导入数", + "thumbnailScaleMethod": "缩略图缩放方式", + "default0": "默认", + "thumbnailFill": "拉伸至整个容器", + "thumbnailCover": "缩放以填满容器", + "thumbnailContain": "缩放以适合容器", + "thumbnailAlign": "缩略图对齐方式", + "leftOrTop": "左/上", + "center": "居中", + "rightOrBottom": "右/下", + "thumbnailAlignHelp": "仅当缩放方法是缩放以填满/适合容器时生效。" } diff --git a/lib/pages/settings/display.dart b/lib/pages/settings/display.dart index dd732a9..eb10d30 100644 --- a/lib/pages/settings/display.dart +++ b/lib/pages/settings/display.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; +import '../../api/client.dart'; import '../../globals.dart'; import '../../main.dart'; import '../../utils.dart'; @@ -27,6 +28,8 @@ class _DisplaySettingsPage extends State bool _oriUseTitleJpn = false; bool _oriDlUseAvgSpeed = false; ThumbnailSize _oriThumbnailSize = ThumbnailSize.medium; + ThumbnailGenMethod _oriThumbnailMethod = ThumbnailGenMethod.unknown; + ThumbnailAlign _oriThumbnailAlign = ThumbnailAlign.center; bool _displayAd = false; Lang _lang = Lang.system; bool _preventScreenCapture = false; @@ -35,6 +38,8 @@ class _DisplaySettingsPage extends State bool _useTitleJpn = false; bool _dlUseAvgSpeed = false; ThumbnailSize _thumbnailSize = ThumbnailSize.medium; + ThumbnailGenMethod _thumbnailMethod = ThumbnailGenMethod.unknown; + ThumbnailAlign _thumbnailAlign = ThumbnailAlign.center; @override void initState() { super.initState(); @@ -104,6 +109,24 @@ class _DisplaySettingsPage extends State _oriThumbnailSize = ThumbnailSize.medium; _thumbnailSize = ThumbnailSize.medium; } + try { + _oriThumbnailMethod = + ThumbnailGenMethod.values[prefs.getInt("thumbnailMethod") ?? 0]; + _thumbnailMethod = _oriThumbnailMethod; + } catch (e) { + _log.warning("Failed to get thumbnailMethod:", e); + _oriThumbnailMethod = ThumbnailGenMethod.unknown; + _thumbnailMethod = ThumbnailGenMethod.unknown; + } + try { + _thumbnailAlign = + ThumbnailAlign.values[prefs.getInt("thumbnailAlign") ?? 1]; + _oriThumbnailAlign = _thumbnailAlign; + } catch (e) { + _log.warning("Failed to get thumbnailAlign:", e); + _oriThumbnailAlign = ThumbnailAlign.center; + _thumbnailAlign = ThumbnailAlign.center; + } } void fallback(BuildContext context) { @@ -123,6 +146,8 @@ class _DisplaySettingsPage extends State _preventScreenCapture = false; _dlUseAvgSpeed = false; _thumbnailSize = ThumbnailSize.medium; + _thumbnailMethod = ThumbnailGenMethod.unknown; + _thumbnailAlign = ThumbnailAlign.center; }); } @@ -199,6 +224,22 @@ class _DisplaySettingsPage extends State _oriThumbnailSize = _thumbnailSize; } } + if (_thumbnailMethod != _oriThumbnailMethod) { + if (!await prefs.setInt("thumbnailMethod", _thumbnailMethod.index)) { + re = false; + _log.warning("Failed to save thumbnailMethod"); + } else { + _oriThumbnailMethod = _thumbnailMethod; + } + } + if (_thumbnailAlign != _oriThumbnailAlign) { + if (!await prefs.setInt("thumbnailAlign", _thumbnailAlign.index)) { + re = false; + _log.warning("Failed to save thumbnailAlign"); + } else { + _oriThumbnailAlign = _thumbnailAlign; + } + } return re; } @@ -351,24 +392,70 @@ class _DisplaySettingsPage extends State Container( padding: const EdgeInsets.symmetric(vertical: 8), - child: DropdownMenu( - initialSelection: _thumbnailSize, - onSelected: (value) { - if (value != null) { - setState(() { - _thumbnailSize = value; - }); - } - }, - label: Text(i18n.thumbnailSize), - dropdownMenuEntries: ThumbnailSize.values - .map((e) => DropdownMenuEntry( - value: e, - label: e.localText(context))) - .toList(), - leadingIcon: const Icon(Icons.photo_rounded), - width: 250, - )), + child: DropdownButtonFormField( + items: ThumbnailSize.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.localText(context)))) + .toList(), + value: _thumbnailSize, + onChanged: (value) { + if (value != null) { + setState(() { + _thumbnailSize = value; + }); + } + }, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: i18n.thumbnailSize, + ))), + Container( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: + DropdownButtonFormField( + items: ThumbnailGenMethod.values + .map((e) => DropdownMenuItem( + value: e, + child: + Text(e.localText(context)))) + .toList(), + value: _thumbnailMethod, + onChanged: (value) { + if (value != null) { + setState(() { + _thumbnailMethod = value; + }); + } + }, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: i18n.thumbnailScaleMethod, + ))), + Container( + padding: + const EdgeInsets.symmetric(vertical: 8), + child: DropdownButtonFormField( + items: ThumbnailAlign.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.localText(context)))) + .toList(), + value: _thumbnailAlign, + onChanged: (value) { + if (value != null) { + setState(() { + _thumbnailAlign = value; + }); + } + }, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: i18n.thumbnailAlign, + helperText: i18n.thumbnailAlignHelp, + helperMaxLines: 3, + ))), Row( mainAxisAlignment: MainAxisAlignment.center, children: [