Compare commits

...

13 Commits

Author SHA1 Message Date
nyne
c234a53518 Merge pull request #557 from venera-app/v1.5.3-dev
V1.5.3
2025-10-13 20:42:31 +08:00
49fd64358c Improve categories page. 2025-10-13 20:28:03 +08:00
3426d707fe Refactor radio button implementations to use RadioGroup. 2025-10-13 20:12:47 +08:00
ebc106d45b enable minify. Close #547 2025-10-13 19:51:54 +08:00
0cda9a2921 Fix alt_store workflow 2025-10-13 18:39:39 +08:00
0eb5d76687 fix android back gesture. Close #544 2025-10-12 19:49:33 +08:00
29d25f7fcd Update version code 2025-10-12 16:47:08 +08:00
7d60e78f27 ignore empty archive link 2025-10-12 16:44:13 +08:00
nyne
e93b56a008 Add Inno Setup installation to workflow 2025-10-09 22:06:21 +08:00
nyne
d10873a903 Update update_alt_store.yml 2025-10-09 21:39:42 +08:00
nyne
2d27f7d650 Merge pull request #541 from venera-app/v1.5.2-dev
V1.5.2
2025-10-08 20:06:56 +08:00
nyne
5a76a10fb2 Merge pull request #537 from lings03/master
Fix some issue when save or share image in reader.
2025-10-07 15:21:50 +08:00
角砂糖
f09e766a8a Fix some issue when save or share image in reader.
1. Change the image name with comic name and real index
2. Fix wrong equal check
3. Fix wrong selection when image per page > 1 and show single image in first page
2025-10-07 01:19:59 +08:00
18 changed files with 395 additions and 347 deletions

View File

@@ -116,6 +116,8 @@ jobs:
run: | run: |
choco install yq -y choco install yq -y
pip install httpx pip install httpx
- name: Install Inno Setup
run: choco install innosetup --no-progress
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: "stable"

View File

@@ -31,19 +31,30 @@ jobs:
- name: Update AltStore source - name: Update AltStore source
id: update_source id: update_source
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
python update_alt_store.py python update_alt_store.py
git config --global user.name 'GitHub Action' git config --global user.name 'GitHub Action'
git config --global user.email 'action@github.com' git config --global user.email 'action@github.com'
git add alt_store.json git add alt_store.json
if git diff --staged --quiet; then if git diff --staged --quiet; then
echo "changes=false" >> $GITHUB_OUTPUT echo "changes=false" >> $GITHUB_OUTPUT
else else
git commit -m "Updated source with latest release" # Create a new branch for the PR
git push branch_name="update-altstore-$(date +%Y%m%d-%H%M%S)"
echo "changes=true" >> $GITHUB_OUTPUT git checkout -b "$branch_name"
fi git commit -m "Updated source with latest release"
git push -u origin "$branch_name"
# Create PR using GitHub CLI
gh pr create \
--title "Update AltStore source with latest release" \
--body "This PR updates the alt_store.json file with the latest release information." \
--head "$branch_name" \
--base main
echo "changes=true" >> $GITHUB_OUTPUT
fi
- name: Calculate job duration - name: Calculate job duration
id: duration id: duration

View File

@@ -23,7 +23,7 @@ linter:
rules: rules:
collection_methods_unrelated_type: false collection_methods_unrelated_type: false
use_build_context_synchronously: false use_build_context_synchronously: false
# avoid_print: false # Uncomment to disable the `avoid_print` rule avoid_print: false
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at # Additional information about this file can be found at

View File

@@ -84,9 +84,8 @@ android {
buildTypes { buildTypes {
release { release {
// Temporarily solution to fix crash minifyEnabled true
minifyEnabled false shrinkResources true
shrinkResources false
ndk { ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86_64" abiFilters "armeabi-v7a", "arm64-v8a", "x86_64"
} }

View File

@@ -13,7 +13,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.5.2"; final version = "1.5.3";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;

View File

@@ -128,7 +128,7 @@ mixin _AppRouteTransitionMixin<T> on PageRoute<T> {
context, context,
animation, animation,
secondaryAnimation, secondaryAnimation,
enableIOSGesture enableIOSGesture && App.isIOS
? IOSBackGestureDetector( ? IOSBackGestureDetector(
gestureWidth: _kBackGestureWidth, gestureWidth: _kBackGestureWidth,
enabledCallback: () => _isPopGestureEnabled<T>(this), enabledCallback: () => _isPopGestureEnabled<T>(this),
@@ -302,7 +302,7 @@ class _IOSBackGestureDetectorState extends State<IOSBackGestureDetector> {
assert(mounted); assert(mounted);
assert(_backGestureController != null); assert(_backGestureController != null);
_backGestureController!.dragUpdate( _backGestureController!.dragUpdate(
_convertToLogical(details.primaryDelta! / context.size!.width)); _convertToLogical(details.primaryDelta! / context.size!.width));
} }
} }

View File

@@ -17,39 +17,50 @@ class CategoriesPage extends StatefulWidget {
State<CategoriesPage> createState() => _CategoriesPageState(); State<CategoriesPage> createState() => _CategoriesPageState();
} }
class _CategoriesPageState extends State<CategoriesPage> { class _CategoriesPageState extends State<CategoriesPage>
with
TickerProviderStateMixin,
AutomaticKeepAliveClientMixin<CategoriesPage> {
var categories = <String>[]; var categories = <String>[];
late TabController controller;
void onSettingsChanged() { void onSettingsChanged() {
var categories = var categories = List.from(
List.from(appdata.settings["categories"]).whereType<String>().toList(); appdata.settings["categories"],
).whereType<String>().toList();
var allCategories = ComicSource.all() var allCategories = ComicSource.all()
.map((e) => e.categoryData?.key) .map((e) => e.categoryData?.key)
.where((element) => element != null) .where((element) => element != null)
.map((e) => e!) .map((e) => e!)
.toList(); .toList();
categories = categories = categories
categories.where((element) => allCategories.contains(element)).toList(); .where((element) => allCategories.contains(element))
.toList();
if (!categories.isEqualTo(this.categories)) { if (!categories.isEqualTo(this.categories)) {
setState(() { setState(() {
this.categories = categories; this.categories = categories;
}); });
controller = TabController(length: categories.length, vsync: this);
} }
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
var categories = var categories = List.from(
List.from(appdata.settings["categories"]).whereType<String>().toList(); appdata.settings["categories"],
).whereType<String>().toList();
var allCategories = ComicSource.all() var allCategories = ComicSource.all()
.map((e) => e.categoryData?.key) .map((e) => e.categoryData?.key)
.where((element) => element != null) .where((element) => element != null)
.map((e) => e!) .map((e) => e!)
.toList(); .toList();
this.categories = this.categories = categories
categories.where((element) => allCategories.contains(element)).toList(); .where((element) => allCategories.contains(element))
.toList();
appdata.settings.addListener(onSettingsChanged); appdata.settings.addListener(onSettingsChanged);
controller = TabController(length: categories.length, vsync: this);
} }
void addPage() { void addPage() {
@@ -59,6 +70,7 @@ class _CategoriesPageState extends State<CategoriesPage> {
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
controller.dispose();
appdata.settings.removeListener(onSettingsChanged); appdata.settings.removeListener(onSettingsChanged);
} }
@@ -85,46 +97,45 @@ class _CategoriesPageState extends State<CategoriesPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
if (categories.isEmpty) { if (categories.isEmpty) {
return buildEmpty(); return buildEmpty();
} }
return Material( return Material(
child: DefaultTabController( child: Column(
length: categories.length, children: [
key: Key(categories.toString()), AppTabBar(
child: Column( controller: controller,
children: [ key: PageStorageKey(categories.toString()),
AppTabBar( tabs: categories.map((e) {
key: PageStorageKey(categories.toString()), String title = e;
tabs: categories.map((e) { try {
String title = e; title = getCategoryDataWithKey(e).title;
try { } catch (e) {
title = getCategoryDataWithKey(e).title; //
} catch (e) { }
// return Tab(text: title, key: Key(e));
} }).toList(),
return Tab( actionButton: TabActionButton(
text: title, icon: const Icon(Icons.add),
key: Key(e), text: "Add".tl,
); onPressed: addPage,
}).toList(), ),
actionButton: TabActionButton( ).paddingTop(context.padding.top),
icon: const Icon(Icons.add), Expanded(
text: "Add".tl, child: TabBarView(
onPressed: addPage, controller: controller,
), children: categories.map((e) => _CategoryPage(e)).toList(),
).paddingTop(context.padding.top), ),
Expanded( ),
child: TabBarView( ],
children: categories.map((e) => _CategoryPage(e)).toList(),
),
)
],
),
), ),
); );
} }
@override
bool get wantKeepAlive => true;
} }
typedef ClickTagCallback = void Function(String, String?); typedef ClickTagCallback = void Function(String, String?);
@@ -150,38 +161,42 @@ class _CategoryPage extends StatelessWidget {
var children = <Widget>[]; var children = <Widget>[];
if (data.enableRankingPage || data.buttons.isNotEmpty) { if (data.enableRankingPage || data.buttons.isNotEmpty) {
children.add(buildTitle(data.title)); children.add(buildTitle(data.title));
children.add(Padding( children.add(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 16), Padding(
child: Wrap( padding: const EdgeInsets.fromLTRB(10, 0, 10, 16),
children: [ child: Wrap(
if (data.enableRankingPage) children: [
buildTag("Ranking".tl, () { if (data.enableRankingPage)
context.to(() => RankingPage(categoryKey: data.key)); buildTag("Ranking".tl, () {
}), context.to(() => RankingPage(categoryKey: data.key));
for (var buttonData in data.buttons) }),
buildTag(buttonData.label.tl, buttonData.onTap) for (var buttonData in data.buttons)
], buildTag(buttonData.label.tl, buttonData.onTap),
],
),
), ),
)); );
} }
for (var part in data.categories) { for (var part in data.categories) {
if (part.enableRandom) { if (part.enableRandom) {
children.add(StatefulBuilder(builder: (context, updater) { children.add(
return Column( StatefulBuilder(
mainAxisSize: MainAxisSize.min, builder: (context, updater) {
crossAxisAlignment: CrossAxisAlignment.start, return Column(
children: [ mainAxisSize: MainAxisSize.min,
buildTitleWithRefresh(part.title, () => updater(() {})), crossAxisAlignment: CrossAxisAlignment.start,
buildTags(part.categories) children: [
], buildTitleWithRefresh(part.title, () => updater(() {})),
); buildTags(part.categories),
})); ],
);
},
),
);
} else { } else {
children.add(buildTitle(part.title)); children.add(buildTitle(part.title));
children.add( children.add(buildTags(part.categories));
buildTags(part.categories),
);
} }
} }
return SingleChildScrollView( return SingleChildScrollView(
@@ -195,8 +210,10 @@ class _CategoryPage extends StatelessWidget {
Widget buildTitle(String title) { Widget buildTitle(String title) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 5, 10), padding: const EdgeInsets.fromLTRB(16, 10, 5, 10),
child: Text(title.tl, child: Text(
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500)), title.tl,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
); );
} }
@@ -207,21 +224,16 @@ class _CategoryPage extends StatelessWidget {
children: [ children: [
Text( Text(
title.tl, title.tl,
style: const TextStyle( style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
fontSize: 20,
fontWeight: FontWeight.w500,
),
), ),
const Spacer(), const Spacer(),
IconButton(onPressed: onRefresh, icon: const Icon(Icons.refresh)) IconButton(onPressed: onRefresh, icon: const Icon(Icons.refresh)),
], ],
), ),
); );
} }
Widget buildTags( Widget buildTags(List<CategoryItem> categories) {
List<CategoryItem> categories,
) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 16), padding: const EdgeInsets.fromLTRB(10, 0, 10, 16),
child: Wrap( child: Wrap(

View File

@@ -155,64 +155,60 @@ abstract mixin class _ComicPageActions {
builder: (context, setState) { builder: (context, setState) {
return ContentDialog( return ContentDialog(
title: "Download".tl, title: "Download".tl,
content: Column( content: RadioGroup<int>(
mainAxisSize: MainAxisSize.min, groupValue: selected,
children: [ onChanged: (v) {
RadioListTile<int>( setState(() {
value: -1, selected = v ?? selected;
groupValue: selected, });
title: Text("Normal".tl), },
onChanged: (v) { child: Column(
setState(() { mainAxisSize: MainAxisSize.min,
selected = v!; children: [
}); RadioListTile<int>(
}, value: -1,
), title: Text("Normal".tl),
ExpansionTile(
title: Text("Archive".tl),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
), ),
collapsedShape: const RoundedRectangleBorder( ExpansionTile(
borderRadius: BorderRadius.zero, title: Text("Archive".tl),
), shape: const RoundedRectangleBorder(
onExpansionChanged: (b) { borderRadius: BorderRadius.zero,
if (!isLoading && b && archives == null) { ),
isLoading = true; collapsedShape: const RoundedRectangleBorder(
comicSource.archiveDownloader! borderRadius: BorderRadius.zero,
.getArchives(comic.id) ),
.then((value) { onExpansionChanged: (b) {
if (value.success) { if (!isLoading && b && archives == null) {
archives = value.data; isLoading = true;
} else { comicSource.archiveDownloader!
App.rootContext .getArchives(comic.id)
.showMessage(message: value.errorMessage!); .then((value) {
} if (value.success) {
setState(() { archives = value.data;
isLoading = false; } else {
App.rootContext
.showMessage(message: value.errorMessage!);
}
setState(() {
isLoading = false;
});
}); });
}); }
} },
}, children: [
children: [ if (archives == null)
if (archives == null) const ListLoadingIndicator().toCenter()
const ListLoadingIndicator().toCenter() else
else for (int i = 0; i < archives!.length; i++)
for (int i = 0; i < archives!.length; i++) RadioListTile<int>(
RadioListTile<int>( value: i,
value: i, title: Text(archives![i].title),
groupValue: selected, subtitle: Text(archives![i].description),
onChanged: (v) { )
setState(() { ],
selected = v!; )
}); ],
}, ),
title: Text(archives![i].title),
subtitle: Text(archives![i].description),
)
],
)
],
), ),
actions: [ actions: [
Button.filled( Button.filled(
@@ -237,10 +233,12 @@ abstract mixin class _ComicPageActions {
isGettingLink = false; isGettingLink = false;
}); });
} else if (context.mounted) { } else if (context.mounted) {
LocalManager() if (res.data.isNotEmpty) {
LocalManager()
.addTask(ArchiveDownloadTask(res.data, comic)); .addTask(ArchiveDownloadTask(res.data, comic));
App.rootContext App.rootContext
.showMessage(message: "Download started".tl); .showMessage(message: "Download started".tl);
}
context.pop(); context.pop();
} }
}, },

View File

@@ -514,51 +514,53 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
) )
: Column( : RadioGroup<int>(
key: key, groupValue: type,
crossAxisAlignment: CrossAxisAlignment.start, onChanged: (value) {
children: [ setState(() {
const SizedBox(width: 600), type = value ?? type;
...List.generate(importMethods.length, (index) { });
return RadioListTile( },
title: Text(importMethods[index]), child: Column(
value: index, key: key,
groupValue: type, crossAxisAlignment: CrossAxisAlignment.start,
onChanged: (value) { children: [
setState(() { const SizedBox(width: 600),
type = value as int; ...List.generate(importMethods.length, (index) {
}); return RadioListTile<int>(
}, title: Text(importMethods[index]),
); value: index,
}), );
if (type != 4) }),
ListTile( if (type != 4)
title: Text("Add to favorites".tl), ListTile(
trailing: Select( title: Text("Add to favorites".tl),
current: selectedFolder, trailing: Select(
values: folders, current: selectedFolder,
minWidth: 112, values: folders,
onTap: (v) { minWidth: 112,
setState(() { onTap: (v) {
selectedFolder = folders[v]; setState(() {
}); selectedFolder = folders[v];
}, });
), },
).paddingHorizontal(8), ),
if (!App.isIOS && !App.isMacOS && type != 2 && type != 3) ).paddingHorizontal(8),
CheckboxListTile( if (!App.isIOS && !App.isMacOS && type != 2 && type != 3)
enabled: true, CheckboxListTile(
title: Text("Copy to app local path".tl), enabled: true,
value: copyToLocalFolder, title: Text("Copy to app local path".tl),
onChanged: (v) { value: copyToLocalFolder,
setState(() { onChanged: (v) {
copyToLocalFolder = !copyToLocalFolder; setState(() {
}); copyToLocalFolder = !copyToLocalFolder;
}).paddingHorizontal(8), });
const SizedBox(height: 8), }).paddingHorizontal(8),
Text(info).paddingHorizontal(24), const SizedBox(height: 8),
], Text(info).paddingHorizontal(24),
), ],
),
),
actions: [ actions: [
Button.text( Button.text(
child: Row( child: Row(

View File

@@ -404,21 +404,23 @@ class _ImageFavoritesDialogState extends State<_ImageFavoritesDialog> {
children: [ children: [
tabBar, tabBar,
TabViewBody(children: [ TabViewBody(children: [
Column( RadioGroup<ImageFavoriteSortType>(
children: ImageFavoriteSortType.values groupValue: sortType,
.map( onChanged: (v) {
(e) => RadioListTile<ImageFavoriteSortType>( setState(() {
title: Text(e.value.tl), sortType = v ?? sortType;
value: e, });
groupValue: sortType, },
onChanged: (v) { child: Column(
setState(() { children: ImageFavoriteSortType.values
sortType = v!; .map(
}); (e) => RadioListTile<ImageFavoriteSortType>(
}, title: Text(e.value.tl),
), value: e,
) ),
.toList(), )
.toList(),
),
), ),
Column( Column(
children: [ children: [

View File

@@ -70,39 +70,29 @@ class _LocalComicsPageState extends State<LocalComicsPage> {
return StatefulBuilder(builder: (context, setState) { return StatefulBuilder(builder: (context, setState) {
return ContentDialog( return ContentDialog(
title: "Sort".tl, title: "Sort".tl,
content: Column( content: RadioGroup<LocalSortType>(
children: [ groupValue: sortType,
RadioListTile<LocalSortType>( onChanged: (v) {
title: Text("Name".tl), setState(() {
value: LocalSortType.name, sortType = v ?? sortType;
groupValue: sortType, });
onChanged: (v) { },
setState(() { child: Column(
sortType = v!; children: [
}); RadioListTile<LocalSortType>(
}, title: Text("Name".tl),
), value: LocalSortType.name,
RadioListTile<LocalSortType>( ),
title: Text("Date".tl), RadioListTile<LocalSortType>(
value: LocalSortType.timeAsc, title: Text("Date".tl),
groupValue: sortType, value: LocalSortType.timeAsc,
onChanged: (v) { ),
setState(() { RadioListTile<LocalSortType>(
sortType = v!; title: Text("Date Desc".tl),
}); value: LocalSortType.timeDesc,
}, ),
), ],
RadioListTile<LocalSortType>( ),
title: Text("Date Desc".tl),
value: LocalSortType.timeDesc,
groupValue: sortType,
onChanged: (v) {
setState(() {
sortType = v!;
});
},
),
],
), ),
actions: [ actions: [
FilledButton( FilledButton(

View File

@@ -140,12 +140,12 @@ class _GalleryModeState extends State<_GalleryMode>
int get totalPages { int get totalPages {
if (!reader.showSingleImageOnFirstPage()) { if (!reader.showSingleImageOnFirstPage()) {
return (reader.images!.length / return (reader.images!.length /
reader.imagesPerPage()) reader.imagesPerPage)
.ceil(); .ceil();
} else { } else {
return 1 + return 1 +
((reader.images!.length - 1) / ((reader.images!.length - 1) /
reader.imagesPerPage()) reader.imagesPerPage)
.ceil(); .ceil();
} }
} }
@@ -169,7 +169,7 @@ class _GalleryModeState extends State<_GalleryMode>
/// Get the range of images for the given page. [page] is 1-based. /// Get the range of images for the given page. [page] is 1-based.
(int start, int end) getPageImagesRange(int page) { (int start, int end) getPageImagesRange(int page) {
var imagesPerPage = reader.imagesPerPage(); var imagesPerPage = reader.imagesPerPage;
if (reader.showSingleImageOnFirstPage()) { if (reader.showSingleImageOnFirstPage()) {
if (page == 1) { if (page == 1) {
return (0, 1); return (0, 1);
@@ -191,6 +191,16 @@ class _GalleryModeState extends State<_GalleryMode>
} }
} }
/// Get the image indices for current page. Returns null if no images.
/// Returns a single index if only one image, or a range if multiple images.
(int, int)? getCurrentPageImageRange() {
if (reader.images == null || reader.images!.isEmpty) {
return null;
}
var (startIndex, endIndex) = getPageImagesRange(reader.page);
return (startIndex, endIndex);
}
/// [cache] is used to cache the images. /// [cache] is used to cache the images.
/// The count of images to cache is determined by the [preCacheCount] setting. /// The count of images to cache is determined by the [preCacheCount] setting.
/// For previous page and next page, it will do a memory cache. /// For previous page and next page, it will do a memory cache.
@@ -259,7 +269,7 @@ class _GalleryModeState extends State<_GalleryMode>
photoViewControllers[index] ??= PhotoViewController(); photoViewControllers[index] ??= PhotoViewController();
if (reader.imagesPerPage() == 1 || if (reader.imagesPerPage == 1 ||
pageImages.length == 1) { pageImages.length == 1) {
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
filterQuality: FilterQuality.medium, filterQuality: FilterQuality.medium,
@@ -533,17 +543,27 @@ class _GalleryModeState extends State<_GalleryMode>
@override @override
String? getImageKeyByOffset(Offset offset) { String? getImageKeyByOffset(Offset offset) {
String? imageKey; var range = getCurrentPageImageRange();
if (reader.imagesPerPage() == 1) { if (range == null) return null;
imageKey = reader.images![reader.page - 1];
} else { var (startIndex, endIndex) = range;
for (var imageState in imageStates) { int actualImageCount = endIndex - startIndex;
if ((imageState as _ComicImageState).containsPoint(offset)) {
imageKey = (imageState.widget.image as ReaderImageProvider).imageKey; if (actualImageCount == 1) {
return reader.images![startIndex];
}
for (var imageState in imageStates) {
if ((imageState as _ComicImageState).containsPoint(offset)) {
var imageKey = (imageState.widget.image as ReaderImageProvider).imageKey;
int index = reader.images!.indexOf(imageKey);
if (index >= startIndex && index < endIndex) {
return imageKey;
} }
} }
} }
return imageKey;
return reader.images![startIndex];
} }
} }

View File

@@ -116,9 +116,9 @@ class _ReaderState extends State<Reader>
return 1; return 1;
} }
if (!showSingleImageOnFirstPage()) { if (!showSingleImageOnFirstPage()) {
return (images!.length / imagesPerPage()).ceil(); return (images!.length / imagesPerPage).ceil();
} else { } else {
return 1 + ((images!.length - 1) / imagesPerPage()).ceil(); return 1 + ((images!.length - 1) / imagesPerPage).ceil();
} }
} }
@@ -277,13 +277,13 @@ class _ReaderState extends State<Reader>
history!.page = images?.length ?? 1; history!.page = images?.length ?? 1;
} else { } else {
/// Record the first image of the page /// Record the first image of the page
if (!showSingleImageOnFirstPage() || imagesPerPage() == 1) { if (!showSingleImageOnFirstPage() || imagesPerPage == 1) {
history!.page = (page - 1) * imagesPerPage() + 1; history!.page = (page - 1) * imagesPerPage + 1;
} else { } else {
if (page == 1) { if (page == 1) {
history!.page = 1; history!.page = 1;
} else { } else {
history!.page = (page - 2) * imagesPerPage() + 2; history!.page = (page - 2) * imagesPerPage + 2;
} }
} }
} }
@@ -371,13 +371,13 @@ abstract mixin class _ImagePerPageHandler {
ComicType get type; ComicType get type;
void initImagesPerPage(int initialPage) { void initImagesPerPage(int initialPage) {
_lastImagesPerPage = imagesPerPage(); _lastImagesPerPage = imagesPerPage;
_lastOrientation = isPortrait; _lastOrientation = isPortrait;
if (imagesPerPage() != 1) { if (imagesPerPage != 1) {
if (showSingleImageOnFirstPage()) { if (showSingleImageOnFirstPage()) {
page = ((initialPage - 1) / imagesPerPage()).ceil() + 1; page = ((initialPage - 1) / imagesPerPage).ceil() + 1;
} else { } else {
page = (initialPage / imagesPerPage()).ceil(); page = (initialPage / imagesPerPage).ceil();
} }
} }
} }
@@ -386,7 +386,7 @@ abstract mixin class _ImagePerPageHandler {
appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSingleImageOnFirstPage'); appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSingleImageOnFirstPage');
/// The number of images displayed on one screen /// The number of images displayed on one screen
int imagesPerPage() { int get imagesPerPage {
if (mode.isContinuous) return 1; if (mode.isContinuous) return 1;
if (isPortrait) { if (isPortrait) {
return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForPortrait') ?? 1; return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForPortrait') ?? 1;
@@ -397,7 +397,7 @@ abstract mixin class _ImagePerPageHandler {
/// Check if the number of images per page has changed /// Check if the number of images per page has changed
void _checkImagesPerPageChange() { void _checkImagesPerPageChange() {
int currentImagesPerPage = imagesPerPage(); int currentImagesPerPage = imagesPerPage;
bool currentOrientation = isPortrait; bool currentOrientation = isPortrait;
if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) { if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) {

View File

@@ -599,22 +599,24 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
} }
void saveCurrentImage() async { void saveCurrentImage() async {
var data = await selectImageToData(); var result = await selectImageToData();
if (data == null) { if (result == null) {
return; return;
} }
var (imageIndex, data) = result;
var fileType = detectFileType(data); var fileType = detectFileType(data);
var filename = "${context.reader.page}${fileType.ext}"; var filename = "${context.reader.widget.name}_${imageIndex + 1}${fileType.ext}";
saveFile(data: data, filename: filename); saveFile(data: data, filename: filename);
} }
void share() async { void share() async {
var data = await selectImageToData(); var result = await selectImageToData();
if (data == null) { if (result == null) {
return; return;
} }
var (imageIndex, data) = result;
var fileType = detectFileType(data); var fileType = detectFileType(data);
var filename = "${context.reader.page}${fileType.ext}"; var filename = "${context.reader.widget.name}_${imageIndex + 1}${fileType.ext}";
Share.shareFile(data: data, filename: filename, mime: fileType.mime); Share.shareFile(data: data, filename: filename, mime: fileType.mime);
} }
@@ -719,8 +721,29 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
Future<int?> selectImage() async { Future<int?> selectImage() async {
var reader = context.reader; var reader = context.reader;
var imageViewController = context.reader._imageViewController; var imageViewController = context.reader._imageViewController;
if (imageViewController is _GalleryModeState && reader.imagesPerPage == 1) {
return reader.page - 1; bool needsSelection = false;
int? singleImageIndex;
if (imageViewController is _GalleryModeState) {
var range = imageViewController.getCurrentPageImageRange();
if (range != null) {
var (startIndex, endIndex) = range;
int actualImageCount = endIndex - startIndex;
if (actualImageCount == 1) {
needsSelection = false;
singleImageIndex = startIndex;
} else {
needsSelection = true;
}
}
} else if (imageViewController is _ContinuousModeState) {
needsSelection = false;
singleImageIndex = reader.page - 1;
}
if (!needsSelection && singleImageIndex != null) {
return singleImageIndex;
} else { } else {
var location = await _showSelectImageOverlay(); var location = await _showSelectImageOverlay();
if (location == null) { if (location == null) {
@@ -734,20 +757,23 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
} }
} }
/// Same as [selectImage], but return the image data. /// Same as [selectImage], but return the image data with its index.
Future<Uint8List?> selectImageToData() async { /// Returns (imageIndex, imageData) or null if cancelled.
Future<(int, Uint8List)?> selectImageToData() async {
var i = await selectImage(); var i = await selectImage();
if (i == null) { if (i == null) {
return null; return null;
} }
var imageKey = context.reader.images![i]; var imageKey = context.reader.images![i];
Uint8List data;
if (imageKey.startsWith("file://")) { if (imageKey.startsWith("file://")) {
return await File(imageKey.substring(7)).readAsBytes(); data = await File(imageKey.substring(7)).readAsBytes();
} else { } else {
return (await CacheManager().findCache( data = await (await CacheManager().findCache(
"$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}", "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}",
))!.readAsBytes(); ))!.readAsBytes();
} }
return (i, data);
} }
Future<Offset?> _showSelectImageOverlay() { Future<Offset?> _showSelectImageOverlay() {

View File

@@ -428,30 +428,26 @@ class _WebdavSettingState extends State<_WebdavSetting> {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( RadioGroup<bool>(
children: [ groupValue: upload,
Text("Operation".tl), onChanged: (value) {
Radio<bool>( setState(() {
groupValue: upload, upload = value ?? upload;
value: true, });
onChanged: (value) { },
setState(() { child: Row(
upload = value!; children: [
}); Text("Operation".tl),
}, Radio<bool>(
), value: true,
Text("Upload".tl), ),
Radio<bool>( Text("Upload".tl),
groupValue: upload, Radio<bool>(
value: false, value: false,
onChanged: (value) { ),
setState(() { Text("Download".tl),
upload = value!; ],
}); ),
},
),
Text("Download".tl),
],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
AnimatedSize( AnimatedSize(

View File

@@ -111,44 +111,34 @@ class _ProxySettingViewState extends State<_ProxySettingView> {
return PopUpWidgetScaffold( return PopUpWidgetScaffold(
title: "Proxy".tl, title: "Proxy".tl,
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column( child: RadioGroup<String>(
children: [ groupValue: type,
RadioListTile<String>( onChanged: (v) {
title: Text("Direct".tl), setState(() {
value: 'direct', type = v ?? type;
groupValue: type, });
onChanged: (v) { if (type != 'manual') {
setState(() { appdata.settings['proxy'] = toProxyStr();
type = v!; appdata.saveData();
}); }
appdata.settings['proxy'] = toProxyStr(); },
appdata.saveData(); child: Column(
}, children: [
), RadioListTile<String>(
RadioListTile<String>( title: Text("Direct".tl),
title: Text("System".tl), value: 'direct',
value: 'system', ),
groupValue: type, RadioListTile<String>(
onChanged: (v) { title: Text("System".tl),
setState(() { value: 'system',
type = v!; ),
}); RadioListTile(
appdata.settings['proxy'] = toProxyStr(); title: Text("Manual".tl),
appdata.saveData(); value: 'manual',
}, ),
), if (type == 'manual') buildManualProxy(),
RadioListTile( ],
title: Text("Manual".tl), ),
value: 'manual',
groupValue: type,
onChanged: (v) {
setState(() {
type = v!;
});
},
),
if (type == 'manual') buildManualProxy(),
],
), ),
), ),
); );

View File

@@ -416,10 +416,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_memory_info name: flutter_memory_info
sha256: "1f112f1d7503aa1681fc8e923f6cd0e847bb2fbeec3753ed021cf1e5f7e9cd74" sha256: eacfd0dd01ff596b4e5bf022442769a1807a73f2af43d62802436f0a5de99137
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.1" version: "0.0.3"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:

View File

@@ -2,7 +2,7 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.5.2+152 version: 1.5.3+153
environment: environment:
sdk: '>=3.8.0 <4.0.0' sdk: '>=3.8.0 <4.0.0'
@@ -75,7 +75,7 @@ dependencies:
ref: fe182cdf40e5fa6230f451bc1d643b860f610d13 ref: fe182cdf40e5fa6230f451bc1d643b860f610d13
dynamic_color: ^1.7.0 dynamic_color: ^1.7.0
shimmer_animation: ^2.1.0 shimmer_animation: ^2.1.0
flutter_memory_info: ^0.0.1 flutter_memory_info: ^0.0.3
syntax_highlight: ^0.4.0 syntax_highlight: ^0.4.0
flutter_7zip: flutter_7zip:
git: git: