Compare commits

...

17 Commits

Author SHA1 Message Date
e8d98e8274 Add support for ArrayBuffer to showInputDialog. 2025-10-28 18:42:59 +08:00
09a1d2821c Enhance onResponse handling in ImageDownloader to support Future and validate result type 2025-10-19 21:50:27 +08:00
nyne
7842b5a1ac Merge pull request #571 from Ftbom/master
调整多收藏夹漫画源的收藏状态显示逻辑
2025-10-19 15:06:18 +08:00
Ftbom
079f574e2f improve network favorite handling in comic details page 2025-10-19 12:23:37 +08:00
GitHub Action
b08f11f6ac Updated source with latest release 2025-10-13 21:24:05 +08:00
nyne
cd925df125 Change base branch from main to master in workflow 2025-10-13 21:19:14 +08:00
nyne
8c87c4a906 Refactor AltStore update workflow script 2025-10-13 21:14:30 +08:00
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
21 changed files with 366 additions and 375 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

@@ -40,8 +40,19 @@ jobs:
if git diff --staged --quiet; then if git diff --staged --quiet; then
echo "changes=false" >> $GITHUB_OUTPUT echo "changes=false" >> $GITHUB_OUTPUT
else else
# Create a new branch for the PR
branch_name="update-altstore-$(date +%Y%m%d-%H%M%S)"
git checkout -b "$branch_name"
git commit -m "Updated source with latest release" git commit -m "Updated source with latest release"
git push 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 master
echo "changes=true" >> $GITHUB_OUTPUT echo "changes=true" >> $GITHUB_OUTPUT
fi fi

View File

@@ -13,15 +13,15 @@
"bundleIdentifier": "com.github.wgh136.venera", "bundleIdentifier": "com.github.wgh136.venera",
"developerName": "wgh136", "developerName": "wgh136",
"subtitle": "A comic reader that supports reading local and network comics", "subtitle": "A comic reader that supports reading local and network comics",
"version": "1.4.5", "version": "1.5.3",
"versionDate": "2025-06-18", "versionDate": "2025-10-13",
"versionDescription": "1. Fixed an abnormal single image height issue when \"imagesPerPage > 1\". 379 \r\n2. Fixed an invalid page calculation issue when \"showSingleImageOnFirstPage\" is enabled. \r\n3. Fixed an issue with incorrect reading history when displaying a single image on the first page. \r\n4. Fixed abnormal history recording when pages are not flipped. 392 \r\n5. Fixed an issue where the download task would stop after exiting the reader. 387 \r\n6. Fixed a \"RangeError\" when translating tags. 356 \r\n7. Reset the current folder to null on the favorites page if the folder is invalid. 389 \r\n8. Fixed various issues when using a custom download path on Android. 400 \r\n9. Set the initial chapter to the first downloaded chapter if no history exists when starting to read a local comic. 405 \r\n10. Removed the config file repository URL from the app.", "versionDescription": "1. Fix an issue where the app freezes after swiping back on Android. 544\r\n2. Enable minification when building for Android. 547\r\n3. Prevent the app from creating an archive download task when the archive URL is an empty string.",
"downloadURL": "https://github.com/venera-app/venera/releases/download/v1.4.5/venera-ios-1.4.5%2B145.ipa", "downloadURL": "https://github.com/venera-app/venera/releases/download/v1.5.3/venera-ios-1.5.3%2B153.ipa",
"localizedDescription": "A comic reader that supports reading local and network comics", "localizedDescription": "A comic reader that supports reading local and network comics",
"iconURL": "https://raw.githubusercontent.com/venera-app/venera/master/assets/app_icon.png", "iconURL": "https://raw.githubusercontent.com/venera-app/venera/master/assets/app_icon.png",
"tintColor": "#0784FC", "tintColor": "#0784FC",
"category": "utilities", "category": "utilities",
"size": 14960268, "size": 15047841,
"appPermissions": { "appPermissions": {
"entitlements": [ "entitlements": [
"application-identifier", "application-identifier",
@@ -39,6 +39,13 @@
} }
}, },
"versions": [ "versions": [
{
"version": "1.5.3",
"date": "2025-10-13",
"localizedDescription": "1. Fix an issue where the app freezes after swiping back on Android. 544\r\n2. Enable minification when building for Android. 547\r\n3. Prevent the app from creating an archive download task when the archive URL is an empty string.",
"downloadURL": "https://github.com/venera-app/venera/releases/download/v1.5.3/venera-ios-1.5.3%2B153.ipa",
"size": 15047841
},
{ {
"version": "1.4.5", "version": "1.4.5",
"date": "2025-06-18", "date": "2025-06-18",
@@ -59,6 +66,16 @@
"tintColor": "#0784FC", "tintColor": "#0784FC",
"title": "v1.4.5 - Venera 18/06/25", "title": "v1.4.5 - Venera 18/06/25",
"url": "https://github.com/venera-app/venera/releases/tag/v1.4.5" "url": "https://github.com/venera-app/venera/releases/tag/v1.4.5"
},
{
"appID": "com.github.wgh136.venera",
"caption": "Update of Venera just got released!",
"date": "2025-10-13T12:47:27Z",
"identifier": "release-v1.5.3",
"notify": true,
"tintColor": "#0784FC",
"title": "v1.5.3 - Venera 13/10/25",
"url": "https://github.com/venera-app/venera/releases/tag/v1.5.3"
} }
] ]
} }

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

@@ -1334,7 +1334,7 @@ let UI = {
* Show an input dialog * Show an input dialog
* @param title {string} * @param title {string}
* @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message. * @param validator {(string) => string | null | undefined} - A function that validates the input. If the function returns a string, the dialog will show the error message.
* @param image {string?} - Available since 1.4.6. An optional image to show in the dialog. You can use this to show a captcha. * @param image {string | ArrayBuffer | null | undefined} - Since 1.4.6, you can pass an image url to show an image in the dialog. Since 1.5.3, you can also pass an ArrayBuffer to show a custom image.
* @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null. * @returns {Promise<string | null>} - The input value. If the dialog is canceled, return null.
*/ */
showInputDialog: (title, validator, image) => { showInputDialog: (title, validator, image) => {

View File

@@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:flutter_qjs/flutter_qjs.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@@ -40,7 +42,6 @@ mixin class JsUiApi {
var image = message['image']; var image = message['image'];
if (title is! String) return; if (title is! String) return;
if (validator != null && validator is! JSInvokable) return; if (validator != null && validator is! JSInvokable) return;
if (image != null && image is! String) return;
return _showInputDialog(title, validator, image); return _showInputDialog(title, validator, image);
case 'showSelectDialog': case 'showSelectDialog':
var title = message['title']; var title = message['title'];
@@ -126,13 +127,25 @@ mixin class JsUiApi {
controller?.close(); controller?.close();
} }
Future<String?> _showInputDialog(String title, JSInvokable? validator, String? image) async { Future<String?> _showInputDialog(String title, JSInvokable? validator, dynamic image) async {
String? result; String? result;
var func = validator == null ? null : JSAutoFreeFunction(validator); var func = validator == null ? null : JSAutoFreeFunction(validator);
String? imageUrl;
Uint8List? imageData;
if (image != null) {
if (image is String) {
imageUrl = image;
} else if (image is Uint8List) {
imageData = image;
} else if (image is List<int>) {
imageData = Uint8List.fromList(image);
}
}
await showInputDialog( await showInputDialog(
context: App.rootContext, context: App.rootContext,
title: title, title: title,
image: image, image: imageUrl,
imageData: imageData,
onConfirm: (v) { onConfirm: (v) {
if (func != null) { if (func != null) {
var res = func.call([v]); var res = func.call([v]);

View File

@@ -360,6 +360,7 @@ Future<void> showInputDialog({
String cancelText = "Cancel", String cancelText = "Cancel",
RegExp? inputValidator, RegExp? inputValidator,
String? image, String? image,
Uint8List? imageData,
}) { }) {
var controller = TextEditingController(text: initialValue); var controller = TextEditingController(text: initialValue);
bool isLoading = false; bool isLoading = false;
@@ -379,6 +380,11 @@ Future<void> showInputDialog({
height: 108, height: 108,
child: Image.network(image, fit: BoxFit.none), child: Image.network(image, fit: BoxFit.none),
).paddingBottom(8), ).paddingBottom(8),
if (image == null && imageData != null)
SizedBox(
height: 108,
child: Image.memory(imageData, fit: BoxFit.none),
).paddingBottom(8),
TextField( TextField(
controller: controller, controller: controller,
decoration: InputDecoration( decoration: InputDecoration(

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

@@ -181,7 +181,15 @@ abstract class ImageDownloader {
} }
if (configs['onResponse'] is JSInvokable) { if (configs['onResponse'] is JSInvokable) {
buffer = (configs['onResponse'] as JSInvokable)([Uint8List.fromList(buffer)]); dynamic result = (configs['onResponse'] as JSInvokable)([Uint8List.fromList(buffer)]);
if (result is Future) {
result = await result;
}
if (result is List<int>) {
buffer = result;
} else {
throw "Error: Invalid onResponse result.";
}
(configs['onResponse'] as JSInvokable).free(); (configs['onResponse'] as JSInvokable).free();
} }

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

@@ -197,11 +197,12 @@ class _NetworkSectionState extends State<_NetworkSection> {
if (res.subData is List) { if (res.subData is List) {
final list = List<String>.from(res.subData); final list = List<String>.from(res.subData);
if (list.isNotEmpty) { if (list.isNotEmpty) {
addedFolders = {list.first}; addedFolders = list.toSet();
localIsFavorite = true;
} else { } else {
addedFolders.clear(); addedFolders.clear();
localIsFavorite = false;
} }
localIsFavorite = addedFolders.isNotEmpty;
} else { } else {
addedFolders.clear(); addedFolders.clear();
localIsFavorite = false; localIsFavorite = false;
@@ -352,62 +353,6 @@ class _NetworkSectionState extends State<_NetworkSection> {
} }
Widget _buildMultiFolder() { Widget _buildMultiFolder() {
if (localIsFavorite == true &&
widget.comicSource.favoriteData!.singleFolderForSingleComic) {
return ListTile(
title: Row(
children: [
Text("Network Favorites".tl),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: context.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Text("Added".tl, style: ts.s12),
),
],
),
trailing: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: _HoverButton(
isFavorite: true,
onTap: () async {
setState(() {
isLoading = true;
});
var res = await widget
.comicSource
.favoriteData!
.addOrDelFavorite!(widget.cid, '', false, null);
if (res.success) {
// Invalidate network cache so subsequent loads see latest
NetworkCacheManager().clear();
setState(() {
localIsFavorite = false;
});
widget.onFavorite(false);
App.rootContext.showMessage(message: "Removed".tl);
if (appdata.settings['autoCloseFavoritePanel'] ?? false) {
context.pop();
}
} else {
context.showMessage(message: res.errorMessage!);
}
setState(() {
isLoading = false;
});
},
),
);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -425,8 +370,10 @@ class _NetworkSectionState extends State<_NetworkSection> {
var name = entry.value; var name = entry.value;
var id = entry.key; var id = entry.key;
var isAdded = addedFolders.contains(id); var isAdded = addedFolders.contains(id);
var hasSelection = addedFolders.isNotEmpty; // When `singleFolderForSingleComic` is `false`, all add and remove buttons are clickable.
var enabled = !hasSelection || isAdded; // When `singleFolderForSingleComic` is `true`, the remove button is always clickable,
// while the add button is only clickable if the comic has not been added to any list.
var enabled = !(widget.comicSource.favoriteData!.singleFolderForSingleComic && addedFolders.isNotEmpty && !isAdded);
return ListTile( return ListTile(
title: Row( title: Row(
@@ -469,11 +416,9 @@ class _NetworkSectionState extends State<_NetworkSection> {
NetworkCacheManager().clear(); NetworkCacheManager().clear();
setState(() { setState(() {
if (isAdded) { if (isAdded) {
addedFolders.clear(); addedFolders.remove(id);
} else { } else {
addedFolders addedFolders.add(id);
..clear()
..add(id);
} }
// sync local flag for single-folder-per-comic logic and parent // sync local flag for single-folder-per-comic logic and parent
localIsFavorite = addedFolders.isNotEmpty; localIsFavorite = addedFolders.isNotEmpty;

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

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