From 2d628ec9b1827f1b19a072f70dade9e9c7d295d5 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 1 Nov 2024 23:15:11 +0800 Subject: [PATCH 1/8] fix #11 --- lib/network/download.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/network/download.dart b/lib/network/download.dart index c6d60a8..052a8c0 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -197,6 +197,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { _scheduleTasks(); } }); + downloading++; } } @@ -590,5 +591,7 @@ abstract mixin class _TransferSpeedMixin { void stopRecorder() { timer?.cancel(); timer = null; + _currentSpeed = 0; + _bytesSinceLastSecond = 0; } } From 2db3f5a72e900700b311d7f0b7e8c07d1f7dd4e8 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 2 Nov 2024 10:00:23 +0800 Subject: [PATCH 2/8] make explore pages keep alive and listen for settings change --- lib/components/comic.dart | 1 + lib/foundation/appdata.dart | 4 +- lib/pages/explore_page.dart | 130 ++++++++++++++++++++++++++---------- lib/utils/ext.dart | 12 ++++ 4 files changed, 109 insertions(+), 38 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index ee3a2d4..2c4c4c3 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -872,6 +872,7 @@ class ComicListState extends State { try { if (widget.loadPage != null) { var res = await widget.loadPage!(page); + if(!mounted) return; if (res.success) { if (res.data.isEmpty) { _data[page] = const []; diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 7a083c9..a81f42e 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/utils/io.dart'; @@ -85,7 +86,7 @@ class _Appdata { final appdata = _Appdata(); -class _Settings { +class _Settings with ChangeNotifier { _Settings(); final _data = { @@ -117,6 +118,7 @@ class _Settings { operator []=(String key, dynamic value) { _data[key] = value; + notifyListeners(); } @override diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index f17db5a..90230a0 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -5,6 +5,7 @@ import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/state_controller.dart'; +import 'package:venera/utils/ext.dart'; import 'package:venera/utils/translations.dart'; class ExplorePage extends StatefulWidget { @@ -15,7 +16,7 @@ class ExplorePage extends StatefulWidget { } class _ExplorePageState extends State - with TickerProviderStateMixin { + with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { late TabController controller; bool showFB = true; @@ -24,6 +25,24 @@ class _ExplorePageState extends State late List pages; + void onSettingsChanged() { + var explorePages = List.from(appdata.settings["explore_pages"]); + var all = ComicSource.all() + .map((e) => e.explorePages) + .expand((e) => e.map((e) => e.title)) + .toList(); + explorePages = explorePages.where((e) => all.contains(e)).toList(); + if(!pages.isEqualsTo(explorePages)){ + setState(() { + pages = explorePages; + controller = TabController( + length: pages.length, + vsync: this, + ); + }); + } + } + @override void initState() { pages = List.from(appdata.settings["explore_pages"]); @@ -36,9 +55,17 @@ class _ExplorePageState extends State length: pages.length, vsync: this, ); + appdata.settings.addListener(onSettingsChanged); super.initState(); } + @override + void dispose() { + controller.dispose(); + appdata.settings.removeListener(onSettingsChanged); + super.dispose(); + } + void refresh() { int page = controller.index; String currentPageId = pages[page]; @@ -83,12 +110,14 @@ class _ExplorePageState extends State @override Widget build(BuildContext context) { + super.build(context); if (pages.isEmpty) { return buildEmpty(); } Widget tabBar = Material( child: FilledTabBar( + key: Key(pages.toString()), tabs: pages.map((e) => buildTab(e)).toList(), controller: controller, ), @@ -97,48 +126,50 @@ class _ExplorePageState extends State return Stack( children: [ Positioned.fill( - child: Column( - children: [ - tabBar, - Expanded( - child: NotificationListener( - onNotification: (notifications) { - if (notifications.metrics.axis == Axis.horizontal) { - if (!showFB) { + child: Column( + children: [ + tabBar, + Expanded( + child: NotificationListener( + onNotification: (notifications) { + if (notifications.metrics.axis == Axis.horizontal) { + if (!showFB) { + setState(() { + showFB = true; + }); + } + return true; + } + + var current = notifications.metrics.pixels; + + if ((current > location && current != 0) && showFB) { + setState(() { + showFB = false; + }); + } else if ((current < location || current == 0) && + !showFB) { setState(() { showFB = true; }); } - return true; - } - var current = notifications.metrics.pixels; - - if ((current > location && current != 0) && showFB) { - setState(() { - showFB = false; - }); - } else if ((current < location || current == 0) && !showFB) { - setState(() { - showFB = true; - }); - } - - location = current; - return false; - }, - child: MediaQuery.removePadding( - context: context, - removeTop: true, - child: TabBarView( - controller: controller, - children: pages.map((e) => buildBody(e)).toList(), + location = current; + return false; + }, + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: TabBarView( + controller: controller, + children: pages.map((e) => buildBody(e)).toList(), + ), ), ), - ), - ) - ], - )), + ) + ], + ), + ), Positioned( right: 16, bottom: 16, @@ -159,6 +190,9 @@ class _ExplorePageState extends State ], ); } + + @override + bool get wantKeepAlive => true; } class _SingleExplorePage extends StatefulWidget { @@ -170,7 +204,8 @@ class _SingleExplorePage extends StatefulWidget { State<_SingleExplorePage> createState() => _SingleExplorePageState(); } -class _SingleExplorePageState extends StateWithController<_SingleExplorePage> { +class _SingleExplorePageState extends StateWithController<_SingleExplorePage> + with AutomaticKeepAliveClientMixin<_SingleExplorePage> { late final ExplorePageData data; bool loading = true; @@ -183,6 +218,16 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> { int key = 0; + bool _wantKeepAlive = true; + + void onSettingsChanged() { + var explorePages = appdata.settings["explore_pages"]; + if (!explorePages.contains(widget.title)) { + _wantKeepAlive = false; + updateKeepAlive(); + } + } + @override void initState() { super.initState(); @@ -195,11 +240,19 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> { } } } + appdata.settings.addListener(onSettingsChanged); throw "Explore Page ${widget.title} Not Found!"; } + @override + void dispose() { + appdata.settings.removeListener(onSettingsChanged); + super.dispose(); + } + @override Widget build(BuildContext context) { + super.build(context); if (data.loadMultiPart != null) { return buildMultiPart(); } else if (data.loadPage != null || data.loadNext != null) { @@ -284,6 +337,9 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> { }); } } + + @override + bool get wantKeepAlive => _wantKeepAlive; } class _MixedExplorePage extends StatefulWidget { diff --git a/lib/utils/ext.dart b/lib/utils/ext.dart index ba414a2..172bab3 100644 --- a/lib/utils/ext.dart +++ b/lib/utils/ext.dart @@ -24,6 +24,18 @@ extension ListExt on List{ add(value); } } + + bool isEqualsTo(List list){ + if(length != list.length){ + return false; + } + for(int i=0; i Date: Sat, 2 Nov 2024 12:05:45 +0800 Subject: [PATCH 3/8] implement view more --- assets/translation.json | 6 ++++-- lib/pages/explore_page.dart | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index aa13239..0f666b6 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -146,7 +146,8 @@ "Select a cbz file." : "选择一个cbz文件", "A cbz file" : "一个cbz文件", "Fullscreen": "全屏", - "Exit": "退出" + "Exit": "退出", + "View more": "查看更多" }, "zh_TW": { "Home": "首頁", @@ -295,6 +296,7 @@ "Select a cbz file." : "選擇一個cbz文件", "A cbz file" : "一個cbz文件", "Fullscreen": "全螢幕", - "Exit": "退出" + "Exit": "退出", + "View more": "查看更多" } } \ No newline at end of file diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index 90230a0..ec41bda 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -5,9 +5,12 @@ import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/foundation/state_controller.dart'; +import 'package:venera/pages/search_result_page.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/translations.dart'; +import 'category_comics_page.dart'; + class ExplorePage extends StatefulWidget { const ExplorePage({super.key}); @@ -32,7 +35,7 @@ class _ExplorePageState extends State .expand((e) => e.map((e) => e.title)) .toList(); explorePages = explorePages.where((e) => all.contains(e)).toList(); - if(!pages.isEqualsTo(explorePages)){ + if (!pages.isEqualsTo(explorePages)) { setState(() { pages = explorePages; controller = TabController( @@ -423,13 +426,12 @@ Iterable _buildExplorePagePart( if (part.viewMore != null) TextButton( onPressed: () { - // TODO: view more - /* var context = App.mainNavigatorKey!.currentContext!; if (part.viewMore!.startsWith("search:")) { context.to( - () => SearchResultPage( - keyword: part.viewMore!.replaceFirst("search:", ""), + () => SearchResultPage( + text: part.viewMore!.replaceFirst("search:", ""), + options: const [], sourceKey: sourceKey, ), ); @@ -441,16 +443,16 @@ Iterable _buildExplorePagePart( p = null; } context.to( - () => CategoryComicsPage( + () => CategoryComicsPage( category: c, categoryKey: - ComicSource.find(sourceKey)!.categoryData!.key, + ComicSource.find(sourceKey)!.categoryData!.key, param: p, ), ); - }*/ + } }, - child: Text("查看更多".tl), + child: Text("View more".tl), ) ], ), From 877e2d5e639cfff48f5ba68149b375dbe67e105f Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 2 Nov 2024 18:59:41 +0800 Subject: [PATCH 4/8] fix #14 --- assets/translation.json | 12 ++- lib/foundation/local.dart | 38 +++++++- lib/pages/local_comics_page.dart | 152 +++++++++++++++++++++++++++---- 3 files changed, 181 insertions(+), 21 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 0f666b6..309c9d1 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -147,7 +147,11 @@ "A cbz file" : "一个cbz文件", "Fullscreen": "全屏", "Exit": "退出", - "View more": "查看更多" + "View more": "查看更多", + "Sort": "排序", + "Name": "名称", + "Date": "日期", + "Date Desc": "日期降序" }, "zh_TW": { "Home": "首頁", @@ -297,6 +301,10 @@ "A cbz file" : "一個cbz文件", "Fullscreen": "全螢幕", "Exit": "退出", - "View more": "查看更多" + "View more": "查看更多", + "Sort": "排序", + "Name": "名稱", + "Date": "日期", + "Date Desc": "日期降序" } } \ No newline at end of file diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index c3ccc61..29c46fb 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -261,8 +261,14 @@ class LocalManager with ChangeNotifier { notifyListeners(); } - List getComics() { - final res = _db.select('SELECT * FROM comics;'); + List getComics(LocalSortType sortType) { + var res = _db.select(''' + SELECT * FROM comics + ORDER BY + ${sortType.value == 'name' ? 'title' : 'created_at'} + ${sortType.value == 'time_asc' ? 'ASC' : 'DESC'} + ; + '''); return res.map((row) => LocalComic.fromRow(row)).toList(); } @@ -310,6 +316,15 @@ class LocalManager with ChangeNotifier { return LocalComic.fromRow(res.first); } + List search(String keyword) { + final res = _db.select(''' + SELECT * FROM comics + WHERE title LIKE ? OR tags LIKE ? OR subtitle LIKE ? + ORDER BY created_at DESC; + ''', ['%$keyword%', '%$keyword%', '%$keyword%']); + return res.map((row) => LocalComic.fromRow(row)).toList(); + } + Future> getImages(String id, ComicType type, Object ep) async { if(ep is! String && ep is! int) { throw "Invalid ep"; @@ -429,3 +444,22 @@ class LocalManager with ChangeNotifier { notifyListeners(); } } + +enum LocalSortType { + name("name"), + timeAsc("time_asc"), + timeDesc("time_desc"); + + final String value; + + const LocalSortType(this.value); + + static LocalSortType fromString(String value) { + for (var type in values) { + if (type.value == value) { + return type; + } + } + return name; + } +} \ No newline at end of file diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index 6252a81..eee469b 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/pages/downloading_page.dart'; import 'package:venera/utils/cbz.dart'; @@ -17,15 +18,29 @@ class LocalComicsPage extends StatefulWidget { class _LocalComicsPageState extends State { late List comics; + late LocalSortType sortType; + + String keyword = ""; + + bool searchMode = false; + void update() { - setState(() { - comics = LocalManager().getComics(); - }); + if(keyword.isEmpty) { + setState(() { + comics = LocalManager().getComics(sortType); + }); + } else { + setState(() { + comics = LocalManager().search(keyword); + }); + } } @override void initState() { - comics = LocalManager().getComics(); + var sort = appdata.implicitData["local_sort"] ?? "name"; + sortType = LocalSortType.fromString(sort); + comics = LocalManager().getComics(sortType); LocalManager().addListener(update); super.initState(); } @@ -36,25 +51,129 @@ class _LocalComicsPageState extends State { super.dispose(); } + void sort() { + showDialog( + context: context, + builder: (context) { + return StatefulBuilder(builder: (context, setState) { + return ContentDialog( + title: "Sort".tl, + content: Column( + children: [ + RadioListTile( + title: Text("Name".tl), + value: LocalSortType.name, + groupValue: sortType, + onChanged: (v) { + setState(() { + sortType = v!; + }); + }, + ), + RadioListTile( + title: Text("Date".tl), + value: LocalSortType.timeAsc, + groupValue: sortType, + onChanged: (v) { + setState(() { + sortType = v!; + }); + }, + ), + RadioListTile( + title: Text("Date Desc".tl), + value: LocalSortType.timeDesc, + groupValue: sortType, + onChanged: (v) { + setState(() { + sortType = v!; + }); + }, + ), + ], + ), + actions: [ + FilledButton( + onPressed: () { + appdata.implicitData["local_sort"] = + sortType.value; + appdata.writeImplicitData(); + Navigator.pop(context); + update(); + }, + child: Text("Confirm".tl), + ), + ], + ); + }); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( body: SmoothCustomScrollView( slivers: [ - SliverAppbar( - title: Text("Local".tl), - actions: [ - Tooltip( - message: "Downloading".tl, - child: IconButton( - icon: const Icon(Icons.download), + if(!searchMode) + SliverAppbar( + title: Text("Local".tl), + actions: [ + Tooltip( + message: "Search".tl, + child: IconButton( + icon: const Icon(Icons.search), + onPressed: () { + setState(() { + searchMode = true; + }); + }, + ), + ), + Tooltip( + message: "Sort".tl, + child: IconButton( + icon: const Icon(Icons.sort), + onPressed: sort, + ), + ), + Tooltip( + message: "Downloading".tl, + child: IconButton( + icon: const Icon(Icons.download), + onPressed: () { + showPopUpWidget(context, const DownloadingPage()); + }, + ), + ) + ], + ) + else + SliverAppbar( + title: TextField( + autofocus: true, + decoration: InputDecoration( + hintText: "Search".tl, + border: InputBorder.none, + ), + onChanged: (v) { + keyword = v; + update(); + }, + ), + actions: [ + IconButton( + icon: const Icon(Icons.close), onPressed: () { - showPopUpWidget(context, const DownloadingPage()); + setState(() { + searchMode = false; + keyword = ""; + update(); + }); }, ), - ) - ], - ), + ], + ), SliverGridComics( comics: comics, onTap: (c) { @@ -80,8 +199,7 @@ class _LocalComicsPageState extends State { var file = await CBZ.export(c as LocalComic); await saveFile(filename: file.name, file: file); await file.delete(); - } - catch (e) { + } catch (e) { context.showMessage(message: e.toString()); } controller.close(); From 19a93cbbce6e326481971bb1e0d0b97df739b810 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 2 Nov 2024 19:14:03 +0800 Subject: [PATCH 5/8] improve history --- assets/translation.json | 6 ++- lib/pages/comic_page.dart | 77 ++++++++++++++++++++++++++++----------- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 309c9d1..20be28f 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -151,7 +151,8 @@ "Sort": "排序", "Name": "名称", "Date": "日期", - "Date Desc": "日期降序" + "Date Desc": "日期降序", + "Start": "开始" }, "zh_TW": { "Home": "首頁", @@ -305,6 +306,7 @@ "Sort": "排序", "Name": "名稱", "Date": "日期", - "Date Desc": "日期降序" + "Date Desc": "日期降序", + "Start": "開始" } } \ No newline at end of file diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 6c3f22b..6f04bfd 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -42,12 +42,41 @@ class _ComicPageState extends LoadingState bool isDownloaded = false; + void updateHistory() async { + var newHistory = await HistoryManager() + .find(widget.id, ComicType(widget.sourceKey.hashCode)); + if(newHistory?.ep != history?.ep || newHistory?.page != history?.page) { + history = newHistory; + update(); + } + } + + @override + Widget buildLoading() { + return Column( + children: [ + const Appbar(title: Text("")), + Expanded( + child: super.buildLoading(), + ), + ], + ); + } + @override void initState() { scrollController.addListener(onScroll); + HistoryManager().addListener(updateHistory); super.initState(); } + @override + void dispose() { + scrollController.removeListener(onScroll); + HistoryManager().removeListener(updateHistory); + super.dispose(); + } + @override void update() { setState(() {}); @@ -205,6 +234,7 @@ class _ComicPageState extends LoadingState Widget buildActions() { bool isMobile = context.width < changePoint; + bool hasHistory = history != null && (history!.ep > 1 || history!.page > 1); return SliverToBoxAdapter( child: Column( children: [ @@ -212,17 +242,17 @@ class _ComicPageState extends LoadingState scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 8), children: [ - if (history != null && (history!.ep > 1 || history!.page > 1)) + if (hasHistory && !isMobile) _ActionButton( icon: const Icon(Icons.menu_book), text: 'Continue'.tl, onPressed: continueRead, iconColor: context.useTextColor(Colors.yellow), ), - if (!isMobile) + if(!isMobile || hasHistory) _ActionButton( icon: const Icon(Icons.play_circle_outline), - text: 'Read'.tl, + text: 'Start'.tl, onPressed: read, iconColor: context.useTextColor(Colors.orange), ), @@ -278,7 +308,10 @@ class _ComicPageState extends LoadingState ), const SizedBox(width: 16), Expanded( - child: FilledButton(onPressed: read, child: Text("Read".tl)), + child: hasHistory + ? FilledButton( + onPressed: continueRead, child: Text("Continue".tl)) + : FilledButton(onPressed: read, child: Text("Read".tl)), ) ], ).paddingHorizontal(16).paddingVertical(8), @@ -398,23 +431,23 @@ class _ComicPageState extends LoadingState Text(comic.stars!.toStringAsFixed(2)), ], ).paddingLeft(16).paddingVertical(8), - for (var e in comic.tags.entries) - buildWrap( - children: [ - if(e.value.isNotEmpty) + for (var e in comic.tags.entries) + buildWrap( + children: [ + if (e.value.isNotEmpty) buildTag(text: e.key.ts(comicSource.key), isTitle: true), - for (var tag in e.value) - buildTag( - text: enableTranslation - ? TagsTranslation.translationTagWithNamespace( - tag, - e.key.toLowerCase(), - ) - : tag, - onTap: () => onTapTag(tag, e.key), - ), - ], - ), + for (var tag in e.value) + buildTag( + text: enableTranslation + ? TagsTranslation.translationTagWithNamespace( + tag, + e.key.toLowerCase(), + ) + : tag, + onTap: () => onTapTag(tag, e.key), + ), + ], + ), if (comic.uploader != null) buildWrap( children: [ @@ -458,7 +491,7 @@ class _ComicPageState extends LoadingState } Widget buildRecommend() { - if (comic.recommend == null||comic.recommend!.isEmpty) { + if (comic.recommend == null || comic.recommend!.isEmpty) { return const SliverPadding(padding: EdgeInsets.zero); } return SliverMainAxisGroup(slivers: [ @@ -770,6 +803,7 @@ class _ActionButton extends StatelessWidget { this.isLoading, this.iconColor, }); + final Widget icon; final Widget? activeIcon; @@ -783,6 +817,7 @@ class _ActionButton extends StatelessWidget { final bool? isLoading; final Color? iconColor; + @override Widget build(BuildContext context) { return Container( From c4d867db897eaefd9ff49e9dd1dc990963fb5e4b Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 2 Nov 2024 20:12:48 +0800 Subject: [PATCH 6/8] data exporting & importing --- assets/translation.json | 10 ++- lib/components/button.dart | 16 +++-- lib/foundation/favorites.dart | 4 ++ lib/foundation/history.dart | 6 ++ lib/pages/settings/app.dart | 30 +++++++++ lib/pages/settings/appearance.dart | 3 + lib/pages/settings/setting_components.dart | 2 +- lib/pages/settings/settings_page.dart | 1 + lib/utils/data.dart | 71 ++++++++++++++++++++++ lib/utils/io.dart | 3 +- windows/build.iss | 1 + 11 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 lib/utils/data.dart diff --git a/assets/translation.json b/assets/translation.json index 20be28f..796bab9 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -152,7 +152,10 @@ "Name": "名称", "Date": "日期", "Date Desc": "日期降序", - "Start": "开始" + "Start": "开始", + "Export App Data": "导出应用数据", + "Import App Data": "导入应用数据", + "Export": "导出" }, "zh_TW": { "Home": "首頁", @@ -307,6 +310,9 @@ "Name": "名稱", "Date": "日期", "Date Desc": "日期降序", - "Start": "開始" + "Start": "開始", + "Export App Data": "匯出應用數據", + "Import App Data": "匯入應用數據", + "Export": "匯出" } } \ No newline at end of file diff --git a/lib/components/button.dart b/lib/components/button.dart index 96f7e15..2a1f6e6 100644 --- a/lib/components/button.dart +++ b/lib/components/button.dart @@ -156,7 +156,7 @@ class _ButtonState extends State