Compare commits

...

19 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
e1fbdfbd50 Update version code 2025-10-07 16:10:14 +08:00
0a5b70b161 Add font patching script for linux arm64. Close #468 2025-10-07 16:09:30 +08:00
9173665afe Fix invalid total comics count. Close #524 2025-10-07 09:47:25 +08:00
e0ea449c17 Improve updating following and fix potential crash. 2025-10-06 10:17:01 +08:00
c438a84537 flutter 3.35.5 2025-10-05 17:31:57 +08:00
8c625e212a fix downloading issue when chapter name contains special characters. Close #533 2025-10-05 17:31:24 +08:00
ab786ed2ab fix padding check. Close #527 2025-10-05 16:58:41 +08:00
d9303aab2e fix activity name. Close #528 2025-10-05 16:56:32 +08:00
23 changed files with 684 additions and 385 deletions

View File

@@ -116,6 +116,8 @@ jobs:
run: |
choco install yq -y
pip install httpx
- name: Install Inno Setup
run: choco install innosetup --no-progress
- uses: subosito/flutter-action@v2
with:
channel: "stable"
@@ -170,6 +172,9 @@ jobs:
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1
dart pub global activate flutter_to_debian
- name: "Patch font"
run: |
dart run patch/font.dart
- run: python3 debian/build.py arm64
- uses: actions/upload-artifact@v4
with:

View File

@@ -31,19 +31,30 @@ jobs:
- name: Update AltStore source
id: update_source
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
python update_alt_store.py
git config --global user.name 'GitHub Action'
git config --global user.email 'action@github.com'
git add alt_store.json
if git diff --staged --quiet; then
echo "changes=false" >> $GITHUB_OUTPUT
else
git commit -m "Updated source with latest release"
git push
echo "changes=true" >> $GITHUB_OUTPUT
fi
python update_alt_store.py
git config --global user.name 'GitHub Action'
git config --global user.email 'action@github.com'
git add alt_store.json
if git diff --staged --quiet; then
echo "changes=false" >> $GITHUB_OUTPUT
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 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
id: duration

View File

@@ -23,7 +23,7 @@ linter:
rules:
collection_methods_unrelated_type: 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
# Additional information about this file can be found at

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:convert';
import 'dart:ffi';
import 'dart:isolate';
@@ -213,12 +214,10 @@ class LocalFavoritesManager with ChangeNotifier {
late Map<String, int> counts;
var _hashedIds = <int, int>{};
int get totalComics {
int total = 0;
for (var t in counts.values) {
total += t;
}
return total;
return _hashedIds.length;
}
int folderComics(String folder) {
@@ -280,6 +279,48 @@ class LocalFavoritesManager with ChangeNotifier {
for (var folder in folderNames) {
counts[folder] = count(folder);
}
_initHashedIds(folderNames, _db.handle).then((value) {
_hashedIds = value;
notifyListeners();
});
}
void refreshHashedIds() {
_initHashedIds(folderNames, _db.handle).then((value) {
_hashedIds = value;
notifyListeners();
});
}
void reduceHashedId(String id, int type) {
var hash = id.hashCode ^ type;
if (_hashedIds.containsKey(hash)) {
if (_hashedIds[hash]! > 1) {
_hashedIds[hash] = _hashedIds[hash]! - 1;
} else {
_hashedIds.remove(hash);
}
}
}
static Future<Map<int, int>> _initHashedIds(
List<String> folders, Pointer<void> p) {
return Isolate.run(() {
var db = sqlite3.fromPointer(p);
var hashedIds = <int, int>{};
for (var folder in folders) {
var rows = db.select("""
select id, type from "$folder";
""");
for (var row in rows) {
var id = row["id"] as String;
var type = row["type"] as int;
var hash = id.hashCode ^ type;
hashedIds[hash] = (hashedIds[hash] ?? 0) + 1;
}
}
return hashedIds;
});
}
List<String> find(String id, ComicType type) {
@@ -559,7 +600,6 @@ class LocalFavoritesManager with ChangeNotifier {
/// return true if success, false if already exists
bool addComic(String folder, FavoriteItem comic,
[int? order, String? updateTime]) {
_modifiedAfterLastCache = true;
if (!existsFolder(folder)) {
throw Exception("Folder does not exists");
}
@@ -614,14 +654,14 @@ class LocalFavoritesManager with ChangeNotifier {
} else {
counts[folder] = counts[folder]! + 1;
}
var hash = comic.id.hashCode ^ comic.type.value;
_hashedIds[hash] = (_hashedIds[hash] ?? 0) + 1;
notifyListeners();
return true;
}
void moveFavorite(
String sourceFolder, String targetFolder, String id, ComicType type) {
_modifiedAfterLastCache = true;
if (!existsFolder(sourceFolder)) {
throw Exception("Source folder does not exist");
}
@@ -655,8 +695,6 @@ class LocalFavoritesManager with ChangeNotifier {
void batchMoveFavorites(
String sourceFolder, String targetFolder, List<FavoriteItem> items) {
_modifiedAfterLastCache = true;
if (!existsFolder(sourceFolder)) {
throw Exception("Source folder does not exist");
}
@@ -691,25 +729,15 @@ class LocalFavoritesManager with ChangeNotifier {
_db.execute("COMMIT");
// Update counts
if (counts[targetFolder] == null) {
counts[targetFolder] = count(targetFolder);
} else {
counts[targetFolder] = counts[targetFolder]! + items.length;
}
if (counts[sourceFolder] != null) {
counts[sourceFolder] = counts[sourceFolder]! - items.length;
} else {
counts[sourceFolder] = count(sourceFolder);
}
counts[targetFolder] = count(targetFolder);
counts[sourceFolder] = count(sourceFolder);
refreshHashedIds();
notifyListeners();
}
void batchCopyFavorites(
String sourceFolder, String targetFolder, List<FavoriteItem> items) {
_modifiedAfterLastCache = true;
if (!existsFolder(sourceFolder)) {
throw Exception("Source folder does not exist");
}
@@ -740,18 +768,14 @@ class LocalFavoritesManager with ChangeNotifier {
_db.execute("COMMIT");
// Update counts
if (counts[targetFolder] == null) {
counts[targetFolder] = count(targetFolder);
} else {
counts[targetFolder] = counts[targetFolder]! + items.length;
}
counts[targetFolder] = count(targetFolder);
refreshHashedIds();
notifyListeners();
}
/// delete a folder
void deleteFolder(String name) {
_modifiedAfterLastCache = true;
_db.execute("""
drop table "$name";
""");
@@ -760,11 +784,11 @@ class LocalFavoritesManager with ChangeNotifier {
where folder_name == ?;
""", [name]);
counts.remove(name);
refreshHashedIds();
notifyListeners();
}
void deleteComicWithId(String folder, String id, ComicType type) {
_modifiedAfterLastCache = true;
LocalFavoriteImageProvider.delete(id, type.value);
_db.execute("""
delete from "$folder"
@@ -775,11 +799,11 @@ class LocalFavoritesManager with ChangeNotifier {
} else {
counts[folder] = count(folder);
}
reduceHashedId(id, type.value);
notifyListeners();
}
void batchDeleteComics(String folder, List<FavoriteItem> comics) {
_modifiedAfterLastCache = true;
_db.execute("BEGIN TRANSACTION");
try {
for (var comic in comics) {
@@ -800,11 +824,13 @@ class LocalFavoritesManager with ChangeNotifier {
return;
}
_db.execute("COMMIT");
for (var comic in comics) {
reduceHashedId(comic.id, comic.type.value);
}
notifyListeners();
}
void batchDeleteComicsInAllFolders(List<ComicID> comics) {
_modifiedAfterLastCache = true;
_db.execute("BEGIN TRANSACTION");
var folderNames = _getFolderNamesWithDB();
try {
@@ -824,6 +850,10 @@ class LocalFavoritesManager with ChangeNotifier {
}
initCounts();
_db.execute("COMMIT");
for (var comic in comics) {
var hash = comic.id.hashCode ^ comic.type.value;
_hashedIds.remove(hash);
}
notifyListeners();
}
@@ -908,7 +938,6 @@ class LocalFavoritesManager with ChangeNotifier {
markAsRead(id, type);
return;
}
_modifiedAfterLastCache = true;
var followUpdatesFolder = appdata.settings['followUpdatesFolder'];
for (final folder in folderNames) {
var rows = _db.select("""
@@ -1029,28 +1058,9 @@ class LocalFavoritesManager with ChangeNotifier {
notifyListeners();
}
final _cachedFavoritedIds = <String, bool>{};
bool isExist(String id, ComicType type) {
if (_modifiedAfterLastCache) {
_cacheFavoritedIds();
}
return _cachedFavoritedIds.containsKey("$id@${type.value}");
}
bool _modifiedAfterLastCache = true;
void _cacheFavoritedIds() {
_modifiedAfterLastCache = false;
_cachedFavoritedIds.clear();
for (var folder in folderNames) {
var rows = _db.select("""
select id, type from "$folder";
""");
for (var row in rows) {
_cachedFavoritedIds["${row["id"]}@${row["type"]}"] = true;
}
}
var hash = id.hashCode ^ type.value;
return _hashedIds.containsKey(hash);
}
void updateInfo(String folder, FavoriteItem comic, [bool notify = true]) {

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:venera/foundation/favorites.dart';
import 'package:venera/foundation/log.dart';
import 'package:venera/utils/channel.dart';
class ComicUpdateResult {
final bool updated;
@@ -62,6 +63,7 @@ Future<ComicUpdateResult> updateComic(
return ComicUpdateResult(updated, null);
} catch (e, s) {
Log.error("Check Updates", e, s);
await Future.delayed(const Duration(seconds: 2));
retries--;
if (retries == 0) {
return ComicUpdateResult(false, e.toString());
@@ -114,23 +116,50 @@ void updateFolderBase(
current = 0;
stream.add(UpdateProgress(total, current, errors, updated));
var futures = <Future>[];
for (var comic in comicsToUpdate) {
var future = updateComic(comic, folder).then((result) {
current++;
if (result.updated) {
updated++;
var channel = Channel<FavoriteItemWithUpdateInfo>(10);
// Producer
() async {
var c = 0;
for (var comic in comicsToUpdate) {
await channel.push(comic);
c++;
// Throttle
if (c % 5 == 0) {
var delay = c % 100 + 1;
if (delay > 10) {
delay = 10;
}
await Future.delayed(Duration(seconds: delay));
}
if (result.errorMessage != null) {
errors++;
}
channel.close();
}();
// Consumers
var updateFutures = <Future>[];
for (var i = 0; i < 5; i++) {
var f = () async {
while (true) {
var comic = await channel.pop();
if (comic == null) {
break;
}
var result = await updateComic(comic, folder);
current++;
if (result.updated) {
updated++;
}
if (result.errorMessage != null) {
errors++;
}
stream.add(UpdateProgress(total, current, errors, updated, comic, result.errorMessage));
}
stream.add(
UpdateProgress(total, current, errors, updated, comic, result.errorMessage));
});
futures.add(future);
}();
updateFutures.add(f);
}
await Future.wait(futures);
await Future.wait(updateFutures);
if (updated > 0) {
LocalFavoritesManager().notifyChanges();

View File

@@ -423,6 +423,7 @@ class LocalManager with ChangeNotifier {
if (comic.hasChapters) {
var cid =
ep is int ? comic.chapters!.ids.elementAt(ep - 1) : (ep as String);
cid = getChapterDirectoryName(cid);
directory = Directory(FilePath.join(directory.path, cid));
}
var files = <File>[];
@@ -600,7 +601,10 @@ class LocalManager with ChangeNotifier {
}
var shouldRemovedDirs = <Directory>[];
for (var chapter in chapters) {
var dir = Directory(FilePath.join(c.baseDir, chapter));
var dir = Directory(FilePath.join(
c.baseDir,
getChapterDirectoryName(chapter),
));
if (dir.existsSync()) {
shouldRemovedDirs.add(dir);
}
@@ -668,6 +672,21 @@ class LocalManager with ChangeNotifier {
}
});
}
static String getChapterDirectoryName(String name) {
var builder = StringBuffer();
for (var i = 0; i < name.length; i++) {
var char = name[i];
if (char == '/' || char == '\\' || char == ':' || char == '*' ||
char == '?'
|| char == '"' || char == '<' || char == '>' || char == '|') {
builder.write('_');
} else {
builder.write(char);
}
}
return builder.toString();
}
}
enum LocalSortType {

View File

@@ -199,6 +199,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
tertiary = light.tertiary;
}
return MaterialApp(
title: "venera",
home: home,
debugShowCheckedModeBanner: false,
theme: getTheme(primary, secondary, tertiary, Brightness.light),
@@ -246,7 +247,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
/// https://github.com/flutter/flutter/issues/161086
var isPaddingCheckError =
MediaQuery.of(context).viewPadding.top <= 0 ||
MediaQuery.of(context).viewPadding.top > 50;
MediaQuery.of(context).viewPadding.top > 200;
if (isPaddingCheckError && Platform.isAndroid) {
widget = MediaQuery(

View File

@@ -107,7 +107,21 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
var local = LocalManager().find(id, comicType);
if (path != null) {
if (local == null) {
Directory(path!).deleteIgnoreError(recursive: true);
Future.sync(() async {
var tasks = this.tasks.values.toList();
for (var i = 0; i < tasks.length; i++) {
if (!tasks[i].isComplete) {
tasks[i].cancel();
await tasks[i].wait();
}
}
try {
await Directory(path!).delete(recursive: true);
}
catch(e) {
Log.error("Download", "Failed to delete directory: $e");
}
});
} else if (chapters != null) {
for (var c in chapters!) {
var dir = Directory(FilePath.join(path!, c));
@@ -197,7 +211,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin {
if (comic!.chapters != null) {
saveTo = Directory(FilePath.join(
path!,
_images!.keys.elementAt(_chapter),
LocalManager.getChapterDirectoryName(
_images!.keys.elementAt(_chapter),
),
));
if (!saveTo.existsSync()) {
saveTo.createSync(recursive: true);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

58
lib/utils/channel.dart Normal file
View File

@@ -0,0 +1,58 @@
import 'dart:async';
import 'dart:collection';
class Channel<T> {
final Queue<T> _queue;
final int size;
Channel(this.size) : _queue = Queue<T>();
Completer? _releaseCompleter;
Completer? _pushCompleter;
var currentSize = 0;
var isClosed = false;
Future<void> push(T item) async {
if (currentSize >= size) {
_releaseCompleter ??= Completer();
return _releaseCompleter!.future.then((_) {
if (isClosed) {
return;
}
_queue.addLast(item);
currentSize++;
});
}
_queue.addLast(item);
currentSize++;
_pushCompleter?.complete();
_pushCompleter = null;
}
Future<T?> pop() async {
while (_queue.isEmpty) {
if (isClosed) {
return null;
}
_pushCompleter ??= Completer();
await _pushCompleter!.future;
}
var item = _queue.removeFirst();
currentSize--;
if (_releaseCompleter != null && currentSize < size) {
_releaseCompleter!.complete();
_releaseCompleter = null;
}
return item;
}
void close() {
isClosed = true;
_pushCompleter?.complete();
_releaseCompleter?.complete();
}
}

28
patch/font.dart Normal file
View File

@@ -0,0 +1,28 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:dio/dio.dart';
void main() async {
const harmonySansLink = "https://developer.huawei.com/images/download/general/HarmonyOS-Sans.zip";
var dio = Dio();
await dio.download(harmonySansLink, "HarmonyOS-Sans.zip");
await extractFileToDisk("HarmonyOS-Sans.zip", "./assets/");
File("HarmonyOS-Sans.zip").deleteSync();
var pubspec = await File("pubspec.yaml").readAsString();
pubspec = pubspec.replaceFirst("# fonts:",
""" fonts:
- family: HarmonyOS Sans
fonts:
- asset: assets/HarmonyOS Sans/HarmonyOS_Sans_SC/HarmonyOS_Sans_SC_Regular.ttf
""");
await File("pubspec.yaml").writeAsString(pubspec);
var mainDart = await File("lib/main.dart").readAsString();
mainDart = mainDart.replaceFirst("Noto Sans CJK", "HarmonyOS Sans");
await File("lib/main.dart").writeAsString(mainDart);
print("Successfully patched font.");
}

View File

@@ -33,6 +33,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
archive:
dependency: "direct dev"
description:
name: archive
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
url: "https://pub.dev"
source: hosted
version: "4.0.7"
args:
dependency: transitive
description:
@@ -408,10 +416,10 @@ packages:
dependency: "direct main"
description:
name: flutter_memory_info
sha256: "1f112f1d7503aa1681fc8e923f6cd0e847bb2fbeec3753ed021cf1e5f7e9cd74"
sha256: eacfd0dd01ff596b4e5bf022442769a1807a73f2af43d62802436f0a5de99137
url: "https://pub.dev"
source: hosted
version: "0.0.1"
version: "0.0.3"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -770,6 +778,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.0"
posix:
dependency: transitive
description:
name: posix
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
rhttp:
dependency: "direct main"
description:
@@ -1116,4 +1132,4 @@ packages:
version: "0.0.12"
sdks:
dart: ">=3.8.0 <4.0.0"
flutter: ">=3.35.2"
flutter: ">=3.35.5"

View File

@@ -2,11 +2,11 @@ name: venera
description: "A comic app."
publish_to: 'none'
version: 1.5.1+151
version: 1.5.3+153
environment:
sdk: '>=3.8.0 <4.0.0'
flutter: 3.35.3
flutter: 3.35.5
dependencies:
flutter:
@@ -75,7 +75,7 @@ dependencies:
ref: fe182cdf40e5fa6230f451bc1d643b860f610d13
dynamic_color: ^1.7.0
shimmer_animation: ^2.1.0
flutter_memory_info: ^0.0.1
flutter_memory_info: ^0.0.3
syntax_highlight: ^0.4.0
flutter_7zip:
git:
@@ -94,6 +94,7 @@ dev_dependencies:
flutter_lints: ^5.0.0
flutter_to_arch: ^1.0.1
flutter_to_debian: ^2.0.2
archive: any
flutter:
uses-material-design: true
@@ -104,6 +105,7 @@ flutter:
- assets/tags.json
- assets/tags_tw.json
- assets/opencc.txt
# fonts:
flutter_to_arch:
name: Venera

115
test/channel_test.dart Normal file
View File

@@ -0,0 +1,115 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:venera/utils/channel.dart';
void main() {
test("1-1-1", () async {
var channel = Channel<int>(1);
await channel.push(1);
var item = await channel.pop();
expect(item, 1);
});
test("1-3-1", () async {
var channel = Channel<int>(1);
// producer
() async {
await channel.push(1);
}();
() async {
await channel.push(2);
}();
() async {
await channel.push(3);
}();
// consumer
var results = <int>[];
for (var i = 0; i < 3; i++) {
var item = await channel.pop();
if (item != null) {
results.add(item);
}
}
expect(results.length, 3);
});
test("2-3-1", () async {
var channel = Channel<int>(2);
// producer
() async {
await channel.push(1);
}();
() async {
await channel.push(2);
}();
() async {
await channel.push(3);
}();
// consumer
var results = <int>[];
for (var i = 0; i < 3; i++) {
var item = await channel.pop();
if (item != null) {
results.add(item);
}
}
expect(results.length, 3);
});
test("1-1-3", () async {
var channel = Channel<int>(1);
// producer
() async {
print("push 1");
await channel.push(1);
print("push 2");
await channel.push(2);
print("push 3");
await channel.push(3);
print("push done");
channel.close();
}();
// consumer
var consumers = <Future>[];
var results = <int>[];
for (var i = 0; i < 3; i++) {
consumers.add(() async {
while (true) {
var item = await channel.pop();
if (item == null) {
break;
}
print("pop $item");
results.add(item);
}
}());
}
await Future.wait(consumers);
expect(results.length, 3);
});
test("close", () async {
var channel = Channel<int>(2);
// producer
() async {
await channel.push(1);
await channel.push(2);
await channel.push(3);
channel.close();
}();
// consumer
await channel.pop();
await channel.pop();
await channel.pop();
var item4 = await channel.pop();
expect(item4, null);
});
}