Compare commits

...

23 Commits

Author SHA1 Message Date
Simon Ding
ae611943c3 doc: update 2024-11-05 13:23:37 +08:00
Simon Ding
4fd11540cd update 2024-11-05 13:16:28 +08:00
Simon Ding
587a28127b ui: improve search error display 2024-11-05 13:00:23 +08:00
Simon Ding
05ae58030c ui: improve error 2024-11-05 12:54:52 +08:00
Simon Ding
f1c4e306f4 ui: improve error readablity 2024-11-05 12:43:17 +08:00
Simon Ding
949b6e5188 ui: remove main selectionArea 2024-11-05 11:51:08 +08:00
Simon Ding
0d4b453d0a feat: prowlarr enable to disable 2024-11-05 10:52:14 +08:00
Simon Ding
bce4d93ab1 feat: add prowlarr enable button 2024-11-04 23:48:52 +08:00
Simon Ding
36b72e6461 fix: text selectable 2024-11-04 18:37:44 +08:00
Simon Ding
62417727f9 ui: revert searchbar 2024-11-04 18:13:02 +08:00
Simon Ding
03f72b9d86 ui: change appbar layout 2024-11-04 18:02:52 +08:00
Simon Ding
c17cf750e5 feat: better prowlarr support 2024-11-04 15:10:56 +08:00
Simon Ding
b176253fc4 feat: add log and defer task loading 2024-11-04 12:04:28 +08:00
Simon Ding
1e2d8b8520 fix: add default behavior 2024-11-04 12:03:32 +08:00
Simon Ding
3739f2c960 fix: page order 2024-11-04 11:30:48 +08:00
Simon Ding
bb6da47efb fix: use StatefulShellRoute to fix ui rerendering 2024-11-04 11:24:27 +08:00
Simon Ding
c28373bde1 ui: update deps 2024-11-01 22:23:21 +08:00
Simon Ding
8ce7045466 fix: return null 2024-11-01 22:05:06 +08:00
Simon Ding
0b1bd8226d fix: add validator 2024-11-01 22:00:18 +08:00
Simon Ding
e67413cec2 feat: add refresh button & parse dialog 2024-11-01 21:53:38 +08:00
Simon Ding
2da02fa706 ui: add filter option 2024-11-01 18:14:32 +08:00
Simon Ding
bc50dd888a ui: change icon 2024-10-27 21:26:53 +08:00
Simon Ding
0305c0709d ui: add donate button 2024-10-27 21:19:02 +08:00
36 changed files with 832 additions and 420 deletions

View File

@@ -63,6 +63,7 @@ type Limiter struct {
}
type ProwlarrSetting struct {
ApiKey string `json:"api_key"`
URL string `json:"url"`
Disabled bool `json:"disabled"`
ApiKey string `json:"api_key"`
URL string `json:"url"`
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 843 KiB

View File

@@ -12,15 +12,15 @@ services:
image: ghcr.io/simon-ding/polaris:latest
restart: always
environment:
- PUID=99
- PGID=100
- TZ=Asia/Shanghai
- PUID=99 #程序运行的用户UID
- PGID=100 #程序运行的用户GID
- TZ=Asia/Shanghai #时区
volumes:
- <配置文件路径>:/app/data #程序配置文件路径
- <下载路径>:/downloads #下载路径,需要和下载客户端配置一致
- <媒体文件路径>:/data #媒体数据存储路径也可以启动自己配置webdav存储
ports:
- 8080:8080
- 8080:8080 #端口映射,冒号前的端口可自行改为需要的
```
### 1.2 Docker 方式安装

View File

@@ -12,6 +12,13 @@ import (
"golift.io/starr/prowlarr"
)
type ProwlarrSupportType string
const (
TV ProwlarrSupportType = "tv"
Movie ProwlarrSupportType = "movie"
)
type Client struct {
p *prowlarr.Prowlarr
apiKey string
@@ -24,7 +31,7 @@ func New(apiKey, url string) *Client {
return &Client{p: p, apiKey: apiKey, url: url}
}
func (c *Client) GetIndexers() ([]*db.TorznabInfo, error) {
func (c *Client) GetIndexers(t ProwlarrSupportType) ([]*db.TorznabInfo, error) {
ins, err := c.p.GetIndexers()
if err != nil {
return nil, err
@@ -34,6 +41,11 @@ func (c *Client) GetIndexers() ([]*db.TorznabInfo, error) {
if !in.Enable {
continue
}
if t == "tv" && len(in.Capabilities.TvSearchParams) == 0 { //no tv resource in this indexer
continue
} else if t == "movie" && len(in.Capabilities.MovieSearchParams) == 0 { //no movie resource in this indexer
continue
}
seedRatio := 0.0
for _, f := range in.Fields {
if f.Name == "torrentBaseSettings.seedRatio" && f.Value != nil {
@@ -57,7 +69,7 @@ func (c *Client) GetIndexers() ([]*db.TorznabInfo, error) {
}
indexers = append(indexers, &db.TorznabInfo{
Indexers: &entIndexer,
Indexers: &entIndexer,
TorznabSetting: setting,
})
}

View File

@@ -7,7 +7,7 @@ import (
func Test111(t *testing.T) {
c := New("", "http://10.0.0.8:9696/")
apis , err := c.GetIndexers()
apis , err := c.GetIndexers("tv")
log.Infof("errors: %v", err)
log.Infof("indexers: %+v", apis[0])
}

View File

@@ -85,6 +85,12 @@ func (r *Response) ToResults(indexer *db.TorznabInfo) []Result {
// log.Warnf("converting link to magnet error, error: %v, link: %v", err, item.Link)
// continue
// }
imdb := ""
if item.GetAttr("imdbid") != "" {
imdb = item.GetAttr("imdbid")
} else if item.GetAttr("imdb") != "" {
imdb = item.GetAttr("imdb")
}
r := Result{
Name: item.Title,
Link: item.Link,
@@ -92,7 +98,7 @@ func (r *Response) ToResults(indexer *db.TorznabInfo) []Result {
Seeders: mustAtoI(item.GetAttr("seeders")),
Peers: mustAtoI(item.GetAttr("peers")),
Category: mustAtoI(item.GetAttr("category")),
ImdbId: item.GetAttr("imdbid"),
ImdbId: imdb,
DownloadVolumeFactor: tryParseFloat(item.GetAttr("downloadvolumefactor")),
UploadVolumeFactor: tryParseFloat(item.GetAttr("uploadvolumefactor")),
Source: indexer.Name,

View File

@@ -44,7 +44,7 @@ func (c *Client) registerCronJob(name string, cron string, f func() error) {
}
func (c *Client) Init() {
c.reloadTasks()
go c.reloadTasks()
c.addSysCron()
}
@@ -82,6 +82,7 @@ func (c *Client) reloadTasks() {
}
}
log.Infof("------ task reloading done ------")
}
func (c *Client) GetDownloadClient() (pkg.Downloader, *ent.DownloadClients, error) {

View File

@@ -36,6 +36,7 @@ func (c *Client) addSysCron() {
return true
})
c.cron.Start()
log.Infof("--------- add cron jobs done --------")
}
func (c *Client) mustAddCron(spec string, cmd func()) {

View File

@@ -33,7 +33,7 @@ func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error
}
log.Debugf("check tv series %s, season %d, episode %v", series.NameEn, param.SeasonNum, param.Episodes)
res := searchWithTorznab(db1, series.NameEn, series.NameCn, series.OriginalName)
res := searchWithTorznab(db1, prowlarr.TV, series.NameEn, series.NameCn, series.OriginalName)
var filtered []torznab.Result
for _, r := range res {
@@ -171,9 +171,9 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
return nil, errors.New("no media found of id")
}
res := searchWithTorznab(db1, movieDetail.NameEn, movieDetail.NameCn, movieDetail.OriginalName)
res := searchWithTorznab(db1, prowlarr.Movie, movieDetail.NameEn, movieDetail.NameCn, movieDetail.OriginalName)
if movieDetail.Extras.IsJav() {
res1 := searchWithTorznab(db1, movieDetail.Extras.JavId)
res1 := searchWithTorznab(db1, prowlarr.Movie, movieDetail.Extras.JavId)
res = append(res, res1...)
}
@@ -227,15 +227,15 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
}
func searchWithTorznab(db *db.Client, queries ...string) []torznab.Result {
func searchWithTorznab(db *db.Client, t prowlarr.ProwlarrSupportType, queries ...string) []torznab.Result {
var res []torznab.Result
allTorznab := db.GetAllTorznabInfo()
p, err := db.GetProwlarrSetting()
if err == nil { //prowlarr exists
if err == nil && !p.Disabled { //prowlarr exists
c := prowlarr.New(p.ApiKey, p.URL)
all, err := c.GetIndexers()
all, err := c.GetIndexers(t)
if err != nil {
log.Warnf("get prowlarr all indexer error: %v", err)
} else {

View File

@@ -175,6 +175,10 @@ func (s *Server) DownloadAll(c *gin.Context) (interface{}, error) {
if err != nil {
return nil, errors.Wrap(err, "convert")
}
return s.downloadAllEpisodes(id)
}
func (s *Server) downloadAllEpisodes(id int) (interface{}, error) {
m, err := s.db.GetMedia(id)
if err != nil {
return nil, errors.Wrap(err, "get media")
@@ -186,3 +190,27 @@ func (s *Server) DownloadAll(c *gin.Context) (interface{}, error) {
return []string{name}, err
}
func (s *Server) DownloadAllTv(c *gin.Context) (interface{}, error) {
tvs := s.db.GetMediaWatchlist(media.MediaTypeTv)
var allNames []string
for _, tv := range tvs {
names, err := s.downloadAllEpisodes(tv.ID)
if err == nil {
allNames = append(allNames, names.([]string)...)
}
}
return allNames, nil
}
func (s *Server) DownloadAllMovies(c *gin.Context) (interface{}, error) {
movies := s.db.GetMediaWatchlist(media.MediaTypeMovie)
var allNames []string
for _, mv := range movies {
names, err := s.downloadAllEpisodes(mv.ID)
if err == nil {
allNames = append(allNames, names.([]string)...)
}
}
return allNames, nil
}

View File

@@ -96,6 +96,8 @@ func (s *Server) Serve() error {
tv.GET("/suggest/tv/:tmdb_id", HttpHandler(s.SuggestedSeriesFolderName))
tv.GET("/suggest/movie/:tmdb_id", HttpHandler(s.SuggestedMovieFolderName))
tv.GET("/downloadall/:id", HttpHandler(s.DownloadAll))
tv.GET("/download/tv", HttpHandler(s.DownloadAllTv))
tv.GET("/download/movie", HttpHandler(s.DownloadAllMovies))
}
indexer := api.Group("/indexer")
{
@@ -130,6 +132,7 @@ func (s *Server) Serve() error {
importlist.POST("/add", HttpHandler(s.addImportlist))
importlist.DELETE("/delete", HttpHandler(s.deleteImportList))
}
log.Infof("----------- Polaris Server Successfully Started ------------")
return s.r.Run(":8080")
}

View File

@@ -105,6 +105,7 @@ func (s *Server) SetSetting(c *gin.Context) (interface{}, error) {
return nil, nil
}
func (s *Server) GetSetting(c *gin.Context) (interface{}, error) {
tmdb := s.db.GetSetting(db.SettingTmdbApiKey)
downloadDir := s.db.GetSetting(db.SettingDownloadDir)
@@ -317,9 +318,11 @@ func (s *Server) SaveProwlarrSetting(c *gin.Context) (interface{}, error) {
if err := c.ShouldBindJSON(&in); err != nil {
return nil, err
}
client := prowlarr.New(in.ApiKey, in.URL)
if _, err := client.GetIndexers(); err != nil {
return nil, errors.Wrap(err, "connect to prowlarr error")
if !in.Disabled {
client := prowlarr.New(in.ApiKey, in.URL)
if _, err := client.GetIndexers(prowlarr.TV); err != nil {
return nil, errors.Wrap(err, "connect to prowlarr error")
}
}
err := s.db.SaveProwlarrSetting(&in)
if err != nil {

BIN
ui/assets/wechat.jpg Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -148,7 +148,7 @@ class _ActivityPageState extends ConsumerState<ActivityPage>
},
));
},
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
})
],

View File

@@ -49,76 +49,79 @@ class _MyAppState extends ConsumerState<MyApp> {
@override
Widget build(BuildContext context) {
var padding = isSmallScreen(context) ? 5.0 : 20.0;
// GoRouter configuration
final shellRoute = ShellRoute(
builder: (BuildContext context, GoRouterState state, Widget child) {
return SelectionArea(
child: MainSkeleton(
body: Padding(
padding: EdgeInsets.only(left: padding, right: padding, top: 5, bottom: 5),
child: child),
),
final shellRoute = StatefulShellRoute.indexedStack(
builder: (BuildContext context, GoRouterState state,
StatefulNavigationShell navigationShell) {
return MainSkeleton(
body: navigationShell,
);
},
routes: [
GoRoute(
path: "/",
redirect: (context, state) => WelcomePage.routeTv,
),
GoRoute(
path: WelcomePage.routeTv,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const WelcomePage()),
),
GoRoute(
path: WelcomePage.routeMoivie,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const WelcomePage()),
),
GoRoute(
path: TvDetailsPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: TvDetailsPage(seriesId: state.pathParameters['id']!)),
),
GoRoute(
path: MovieDetailsPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: MovieDetailsPage(id: state.pathParameters['id']!)),
),
GoRoute(
path: SearchPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: SearchPage(query: state.uri.queryParameters["query"])),
),
GoRoute(
path: SystemSettingsPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: const SystemSettingsPage()),
),
GoRoute(
path: ActivityPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const ActivityPage()),
),
GoRoute(
path: SystemPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const SystemPage()),
)
branches: [
StatefulShellBranch(initialLocation: WelcomePage.routeTv, routes: [
GoRoute(
path: WelcomePage.routeTv,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const WelcomePage()),
),
GoRoute(
path: TvDetailsPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: TvDetailsPage(seriesId: state.pathParameters['id']!)),
),
GoRoute(
path: SearchPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: SearchPage(query: state.uri.queryParameters["query"])),
),
]),
StatefulShellBranch(initialLocation: WelcomePage.routeMoivie, routes: [
GoRoute(
path: WelcomePage.routeMoivie,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const WelcomePage()),
),
GoRoute(
path: MovieDetailsPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: MovieDetailsPage(id: state.pathParameters['id']!)),
),
]),
StatefulShellBranch(routes: [
GoRoute(
path: ActivityPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const ActivityPage()),
),
]),
StatefulShellBranch(routes: [
GoRoute(
path: SystemSettingsPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context,
state: state,
child: const SystemSettingsPage()),
),
]),
StatefulShellBranch(routes: [
GoRoute(
path: SystemPage.route,
pageBuilder: (context, state) => buildPageWithDefaultTransition(
context: context, state: state, child: const SystemPage()),
)
]),
],
);
final router = GoRouter(
navigatorKey: APIs.navigatorKey,
initialLocation: WelcomePage.routeTv,
routes: [
shellRoute,
GoRoute(
@@ -153,7 +156,7 @@ class _MyAppState extends ConsumerState<MyApp> {
}
class MainSkeleton extends StatefulWidget {
final Widget body;
final StatefulNavigationShell body;
const MainSkeleton({super.key, required this.body});
@override
@@ -163,23 +166,9 @@ class MainSkeleton extends StatefulWidget {
}
class _MainSkeletonState extends State<MainSkeleton> {
var _selectedTab;
@override
Widget build(BuildContext context) {
var uri = GoRouterState.of(context).uri.toString();
if (uri.contains(WelcomePage.routeTv)) {
_selectedTab = 0;
} else if (uri.contains(WelcomePage.routeMoivie)) {
_selectedTab = 1;
} else if (uri.contains(ActivityPage.route)) {
_selectedTab = 2;
} else if (uri.contains(SystemSettingsPage.route)) {
_selectedTab = 3;
} else if (uri.contains(SystemPage.route)) {
_selectedTab = 4;
}
var padding = isSmallScreen(context) ? 5.0 : 20.0;
return AdaptiveScaffold(
appBarBreakpoint: Breakpoints.standard,
appBar: AppBar(
@@ -189,24 +178,28 @@ class _MainSkeletonState extends State<MainSkeleton> {
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: TextButton(
onPressed: () => context.go(WelcomePage.routeTv),
child: const Text(
"Polaris",
overflow: TextOverflow.clip,
style: TextStyle(fontSize: 28),
leading: Container(
alignment: Alignment.centerLeft,
child: TextButton(
onPressed: () => context.go(WelcomePage.routeTv),
child: const Text(
"Polaris",
overflow: TextOverflow.clip,
style: TextStyle(fontSize: 28),
),
),
),
actions: [
SearchAnchor(
leadingWidth: isSmallScreen(context) ? 0 : 190,
title: Container(
alignment: Alignment.bottomLeft,
child: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return Container(
constraints: const BoxConstraints(maxWidth: 250, maxHeight: 40),
child: Opacity(
opacity: 0.8,
child: SearchBar(
hintText: "搜索...",
hintText: "在此搜索...",
leading: const Icon(Icons.search),
controller: controller,
shadowColor: WidgetStateColor.transparent,
@@ -222,9 +215,19 @@ class _MainSkeletonState extends State<MainSkeleton> {
(BuildContext context, SearchController controller) {
return [Text("dadada")];
}),
),
actions: [
// IconButton(
// onPressed: () => showCalendar(context),
// icon: Icon(Icons.calendar_month)),
IconButton(
onPressed: () => showCalendar(context),
icon: Icon(Icons.calendar_month)),
onPressed: () => showDonate(context),
icon: Icon(
Icons.favorite_rounded,
color: Colors.red,
)),
MenuAnchor(
menuChildren: [
MenuItemButton(
@@ -247,27 +250,14 @@ class _MainSkeletonState extends State<MainSkeleton> {
child: const Icon(Icons.account_circle),
);
},
)
),
],
),
useDrawer: false,
selectedIndex: _selectedTab,
onSelectedIndexChange: (int index) {
setState(() {
_selectedTab = index;
});
if (index == 0) {
context.go(WelcomePage.routeTv);
} else if (index == 1) {
context.go(WelcomePage.routeMoivie);
} else if (index == 2) {
context.go(ActivityPage.route);
} else if (index == 3) {
context.go(SystemSettingsPage.route);
} else if (index == 4) {
context.go(SystemPage.route);
}
},
selectedIndex: widget.body.currentIndex,
onSelectedIndexChange: (p0) => widget.body
.goBranch(p0, initialLocation: p0 == widget.body.currentIndex),
destinations: const <NavigationDestination>[
NavigationDestination(
icon: Icon(Icons.live_tv_outlined),
@@ -295,11 +285,33 @@ class _MainSkeletonState extends State<MainSkeleton> {
label: '系统',
),
],
body: (context) => SafeArea(child: widget.body),
body: (context) => SafeArea(
child: Padding(
padding: EdgeInsets.only(
left: padding, right: padding, top: 5, bottom: 5),
child: widget.body)),
// Define a default secondaryBody.
// Override the default secondaryBody during the smallBreakpoint to be
// empty. Must use AdaptiveScaffold.emptyBuilder to ensure it is properly
// overridden.
);
}
showDonate(BuildContext context) {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: Text("项目开发不易,给开发者加个鸡腿:"),
content: SizedBox(
width: 350,
height: 400,
child: Ink.image(
fit: BoxFit.fitWidth,
image: AssetImage("assets/wechat.jpg"))),
);
},
);
}
}

View File

@@ -28,21 +28,22 @@ class _MovieDetailsPageState extends ConsumerState<MovieDetailsPage> {
Widget build(BuildContext context) {
var seriesDetails = ref.watch(mediaDetailsProvider(widget.id));
return seriesDetails.when(
data: (details) {
return ListView(
children: [
DetailCard(details: details),
NestedTabBar(
id: widget.id,
)
],
);
},
error: (err, trace) {
return Text("$err");
},
loading: () => const MyProgressIndicator());
return SelectionArea(
child: seriesDetails.when(
data: (details) {
return ListView(
children: [
DetailCard(details: details),
NestedTabBar(
id: widget.id,
)
],
);
},
error: (err, trace) {
return Text("$err");
},
loading: () => const MyProgressIndicator()));
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -8,9 +10,12 @@ class APIs {
static final _baseUrl = baseUrl();
static final searchUrl = "$_baseUrl/api/v1/media/search";
static final editMediaUrl = "$_baseUrl/api/v1/media/edit";
static final downloadAllUrl = "$_baseUrl/api/v1/media/downloadall/";
static final downloadAllEpisodesUrl = "$_baseUrl/api/v1/media/downloadall/";
static final downloadAllTvUrl = "$_baseUrl/api/v1/media/download/tv";
static final downloadAllMovieUrl = "$_baseUrl/api/v1/media/download/movie";
static final settingsUrl = "$_baseUrl/api/v1/setting/do";
static final settingsGeneralUrl = "$_baseUrl/api/v1/setting/general";
//static final singleSettingUrl = "$_baseUrl/api/v1/setting/";
static final watchlistTvUrl = "$_baseUrl/api/v1/media/tv/watchlist";
static final watchlistMovieUrl = "$_baseUrl/api/v1/media/movie/watchlist";
static final availableTorrentsUrl = "$_baseUrl/api/v1/media/torrents/";
@@ -50,6 +55,9 @@ class APIs {
static final cronJobUrl = "$_baseUrl/api/v1/setting/cron/trigger";
static final tvParseUrl = "$_baseUrl/api/v1/setting/parse/tv";
static final movieParseUrl = "$_baseUrl/api/v1/setting/parse/movie";
static const tmdbApiKey = "tmdb_api_key";
static const downloadDirKey = "download_dir";
@@ -114,4 +122,50 @@ class APIs {
throw sp.message;
}
}
static Future<List<String>> downloadAllTv() async {
var resp = await getDio().get(APIs.downloadAllTvUrl);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
return sp.data==null? []:sp.data as List<String>;
}
static Future<List<String>> downloadAllMovies() async {
var resp = await getDio().get(APIs.downloadAllMovieUrl);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
return sp.data==null? []:sp.data as List<String>;
}
static Future<String> parseTvName(String s) async {
var resp = await getDio().post(APIs.tvParseUrl, data: {"s": s});
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
JsonEncoder encoder = new JsonEncoder.withIndent(' ');
return encoder.convert(sp.data);
}
static Future<String> parseMovieName(String s) async {
var resp = await getDio().post(APIs.movieParseUrl, data: {"s": s});
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
JsonEncoder encoder = new JsonEncoder.withIndent(' ');
return encoder.convert(sp.data);
}
}

View File

@@ -84,7 +84,7 @@ class SeriesDetailData
Future<dynamic> downloadall() async {
final dio = APIs.getDio();
var resp = await dio.get(APIs.downloadAllUrl + id!);
var resp = await dio.get(APIs.downloadAllEpisodesUrl + id!);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;

View File

@@ -32,7 +32,7 @@ var prowlarrSettingDataProvider =
class EditSettingData extends AutoDisposeAsyncNotifier<GeneralSetting> {
@override
FutureOr<GeneralSetting> build() async {
final dio = await APIs.getDio();
final dio = APIs.getDio();
var resp = await dio.get(APIs.settingsGeneralUrl);
var rrr = ServerResponse.fromJson(resp.data);
@@ -509,14 +509,20 @@ class ImportListData extends AutoDisposeAsyncNotifier<List<ImportList>> {
}
class ProwlarrSetting {
final bool disabled;
final String apiKey;
final String url;
ProwlarrSetting({required this.apiKey, required this.url});
ProwlarrSetting(
{required this.apiKey, required this.url, required this.disabled});
factory ProwlarrSetting.fromJson(Map<String, dynamic> json) {
return ProwlarrSetting(apiKey: json["api_key"], url: json["url"]);
return ProwlarrSetting(
apiKey: json["api_key"],
url: json["url"],
disabled: json["disabled"] ?? false);
}
Map<String, dynamic> tojson() => {"api_key": apiKey, "url": url};
Map<String, dynamic> tojson() =>
{"api_key": apiKey, "url": url, "disabled": disabled};
}
class ProwlarrSettingData extends AutoDisposeAsyncNotifier<ProwlarrSetting> {

View File

@@ -5,6 +5,7 @@ import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/welcome_data.dart';
import 'package:ui/search_page/submit_dialog.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/widgets.dart';
class SearchPage extends ConsumerStatefulWidget {
const SearchPage({super.key, this.query});
@@ -110,7 +111,7 @@ class _SearchPageState extends ConsumerState<SearchPage> {
}
return cards;
},
error: (err, trace) => [Text("$err")],
error: (err, trace) => [PoError(msg: "网络错误请确认TMDB Key正确配置并且服务端能够正常连接TMDB网站", err: err)],
loading: () => [const MyProgressIndicator()]);
var f = NotificationListener(

View File

@@ -94,7 +94,7 @@ class _AuthState extends ConsumerState<AuthSettings> {
],
));
},
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
}
}

View File

@@ -35,7 +35,7 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
onTap: () => showDownloadClientDetails(DownloadClient()),
child: const Icon(Icons.add));
})),
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
}

View File

@@ -170,7 +170,7 @@ class _GeneralState extends ConsumerState<GeneralSettings> {
),
);
},
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
}
}

View File

@@ -35,7 +35,7 @@ class _ImportlistState extends ConsumerState<Importlist> {
child: const Icon(Icons.add));
}),
),
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
}

View File

@@ -33,7 +33,7 @@ class _IndexerState extends ConsumerState<IndexerSettings> {
child: const Icon(Icons.add));
}),
),
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
}

View File

@@ -48,7 +48,7 @@ class _NotifierState extends ConsumerState<NotifierSettings> {
child: const Icon(Icons.add));
}),
),
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
}

View File

@@ -25,7 +25,11 @@ class ProwlarrSettingState extends ConsumerState<ProwlarrSettingPage> {
data: (v) => FormBuilder(
key: _formKey, //设置globalKey用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
initialValue: {"api_key": v.apiKey, "url": v.url},
initialValue: {
"api_key": v.apiKey,
"url": v.url,
"disabled": v.disabled
},
child: Column(
children: [
FormBuilderTextField(
@@ -44,6 +48,11 @@ class ProwlarrSettingState extends ConsumerState<ProwlarrSettingPage> {
helperText: "Prowlarr 设置 -> 通用 -> API 密钥"),
validator: FormBuilderValidators.required(),
),
FormBuilderSwitch(
name: "disabled",
title: const Text("禁用 Prowlarr"),
decoration: InputDecoration(icon: Icon(Icons.do_not_disturb)),
),
Center(
child: Padding(
padding: const EdgeInsets.all(10),
@@ -55,18 +64,22 @@ class ProwlarrSettingState extends ConsumerState<ProwlarrSettingPage> {
.read(prowlarrSettingDataProvider.notifier)
.save(ProwlarrSetting(
apiKey: values["api_key"],
url: values["url"]))
url: values["url"],
disabled: values["disabled"]))
.then((v) => showSnakeBar("更新成功"));
showLoadingWithFuture(f);
}
},
child: const Padding(padding: EdgeInsets.all(10), child: Text("保存"),)),
child: const Padding(
padding: EdgeInsets.all(10),
child: Text("保存"),
)),
),
)
],
),
),
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
}
}

View File

@@ -22,7 +22,8 @@ class SystemSettingsPage extends ConsumerStatefulWidget {
class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
@override
Widget build(BuildContext context) {
return ListView(
return SelectionArea(
child: ListView(
children: [
getExpansionTile("常规", const GeneralSettings()),
getExpansionTile("索引器", const IndexerSettings()),
@@ -33,7 +34,7 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
getExpansionTile("监控列表", const Importlist()),
getExpansionTile("认证", const AuthSettings())
],
);
));
}
Widget getExpansionTile(String name, Widget body) {

View File

@@ -35,7 +35,7 @@ class _StorageState extends ConsumerState<StorageSettings> {
child: const Icon(Icons.add));
}),
),
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
}

View File

@@ -23,7 +23,8 @@ class _SystemPageState extends ConsumerState<SystemPage> {
Widget build(BuildContext context) {
final logs = ref.watch(logFileDataProvider);
final about = ref.watch(aboutDataProvider);
return SingleChildScrollView(
return SelectionArea(
child: SingleChildScrollView(
child: Column(
children: [
ExpansionTile(
@@ -55,7 +56,7 @@ class _SystemPageState extends ConsumerState<SystemPage> {
]);
}));
},
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator())
],
),
@@ -110,7 +111,7 @@ class _SystemPageState extends ConsumerState<SystemPage> {
]),
]);
},
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator())
],
),
@@ -184,12 +185,12 @@ class _SystemPageState extends ConsumerState<SystemPage> {
],
);
},
error: (err, trace) => Text("$err"),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator())
],
)
],
),
);
));
}
}

View File

@@ -34,156 +34,157 @@ class _TvDetailsPageState extends ConsumerState<TvDetailsPage> {
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
var seriesDetails = ref.watch(mediaDetailsProvider(widget.seriesId));
return seriesDetails.when(
data: (details) {
Map<int, List<DataRow>> m = {};
for (final ep in details.episodes!) {
var row = DataRow(cells: [
DataCell(Text("${ep.episodeNumber}")),
DataCell(Text("${ep.title}")),
DataCell(Opacity(
opacity: 0.5,
child: Text(ep.airDate ?? "-"),
)),
DataCell(
Opacity(
opacity: 0.7,
child: ep.status == "downloading"
? const IconButton(
tooltip: "下载中",
onPressed: null,
icon: Icon(Icons.downloading))
: (ep.status == "downloaded"
return SelectionArea(
child: seriesDetails.when(
data: (details) {
Map<int, List<DataRow>> m = {};
for (final ep in details.episodes!) {
var row = DataRow(cells: [
DataCell(Text("${ep.episodeNumber}")),
DataCell(Text("${ep.title}")),
DataCell(Opacity(
opacity: 0.5,
child: Text(ep.airDate ?? "-"),
)),
DataCell(
Opacity(
opacity: 0.7,
child: ep.status == "downloading"
? const IconButton(
tooltip: "下载",
tooltip: "下载",
onPressed: null,
icon: Icon(Icons.download_done))
: (ep.monitored == true
? IconButton(
tooltip: "监控中",
onPressed: () {
ref
.read(mediaDetailsProvider(
widget.seriesId)
.notifier)
.changeMonitoringStatus(
ep.id!, false);
},
icon: const Icon(Icons.alarm))
: Opacity(
opacity: 0.7,
child: IconButton(
tooltip: "未监控",
icon: Icon(Icons.downloading))
: (ep.status == "downloaded"
? const IconButton(
tooltip: "已下载",
onPressed: null,
icon: Icon(Icons.download_done))
: (ep.monitored == true
? IconButton(
tooltip: "监控中",
onPressed: () {
ref
.read(mediaDetailsProvider(
widget.seriesId)
.notifier)
.changeMonitoringStatus(
ep.id!, true);
ep.id!, false);
},
icon: const Icon(Icons.alarm_off)),
)))),
),
DataCell(Row(
children: [
LoadingIconButton(
tooltip: "搜索下载对应剧集",
onPressed: () async {
await ref
.read(
mediaDetailsProvider(widget.seriesId).notifier)
.searchAndDownload(widget.seriesId,
ep.seasonNumber!, ep.episodeNumber!)
.then((v) => showSnakeBar("开始下载: $v"));
},
icon: Icons.download),
const SizedBox(
width: 10,
icon: const Icon(Icons.alarm))
: Opacity(
opacity: 0.7,
child: IconButton(
tooltip: "未监控",
onPressed: () {
ref
.read(mediaDetailsProvider(
widget.seriesId)
.notifier)
.changeMonitoringStatus(
ep.id!, true);
},
icon: const Icon(Icons.alarm_off)),
)))),
),
Tooltip(
message: "查看可用资源",
child: IconButton(
onPressed: () => showAvailableTorrents(widget.seriesId,
ep.seasonNumber ?? 0, ep.episodeNumber ?? 0),
icon: const Icon(Icons.manage_search)),
)
],
))
]);
if (m[ep.seasonNumber] == null) {
m[ep.seasonNumber!] = List.empty(growable: true);
}
m[ep.seasonNumber!]!.add(row);
}
List<ExpansionTile> list = List.empty(growable: true);
for (final k in m.keys.toList().reversed) {
final seasonEpisodes = DataTable(columns: [
const DataColumn(label: Text("#")),
const DataColumn(
label: Text("标题"),
),
const DataColumn(label: Text("播出时间")),
const DataColumn(label: Text("状态")),
DataColumn(
label: Row(
children: [
LoadingIconButton(
tooltip: "搜索下载全部剧集",
onPressed: () async {
await ref
.read(
mediaDetailsProvider(widget.seriesId).notifier)
.searchAndDownload(widget.seriesId, k, 0)
.then((v) => showSnakeBar("开始下载: $v"));
//showLoadingWithFuture(f);
},
icon: Icons.download),
const SizedBox(
width: 10,
),
Tooltip(
message: "查看可用资源",
child: IconButton(
onPressed: () =>
showAvailableTorrents(widget.seriesId, k, 0),
icon: const Icon(Icons.manage_search)),
)
],
))
], rows: m[k]!);
var seasonList = ExpansionTile(
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
//childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
initiallyExpanded: false,
title: k == 0 ? const Text("特别篇") : Text("$k"),
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
children: [
screenWidth < 600
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: seasonEpisodes,
DataCell(Row(
children: [
LoadingIconButton(
tooltip: "搜索下载对应剧集",
onPressed: () async {
await ref
.read(mediaDetailsProvider(widget.seriesId)
.notifier)
.searchAndDownload(widget.seriesId,
ep.seasonNumber!, ep.episodeNumber!)
.then((v) => showSnakeBar("开始下载: $v"));
},
icon: Icons.download),
const SizedBox(
width: 10,
),
Tooltip(
message: "查看可用资源",
child: IconButton(
onPressed: () => showAvailableTorrents(
widget.seriesId,
ep.seasonNumber ?? 0,
ep.episodeNumber ?? 0),
icon: const Icon(Icons.manage_search)),
)
: seasonEpisodes
],
);
list.add(seasonList);
}
return ListView(
children: [
DetailCard(details: details),
Column(
children: list,
),
],
);
},
error: (err, trace) {
return Text("$err");
},
loading: () => const MyProgressIndicator());
],
))
]);
if (m[ep.seasonNumber] == null) {
m[ep.seasonNumber!] = List.empty(growable: true);
}
m[ep.seasonNumber!]!.add(row);
}
List<ExpansionTile> list = List.empty(growable: true);
for (final k in m.keys.toList().reversed) {
final seasonEpisodes = DataTable(columns: [
const DataColumn(label: Text("#")),
const DataColumn(
label: Text("标题"),
),
const DataColumn(label: Text("播出时间")),
const DataColumn(label: Text("状态")),
DataColumn(
label: Row(
children: [
LoadingIconButton(
tooltip: "搜索下载全部剧集",
onPressed: () async {
await ref
.read(mediaDetailsProvider(widget.seriesId)
.notifier)
.searchAndDownload(widget.seriesId, k, 0)
.then((v) => showSnakeBar("开始下载: $v"));
//showLoadingWithFuture(f);
},
icon: Icons.download),
const SizedBox(
width: 10,
),
Tooltip(
message: "查看可用资源",
child: IconButton(
onPressed: () =>
showAvailableTorrents(widget.seriesId, k, 0),
icon: const Icon(Icons.manage_search)),
)
],
))
], rows: m[k]!);
var seasonList = ExpansionTile(
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
//childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
initiallyExpanded: false,
title: k == 0 ? const Text("特别篇") : Text("$k"),
expandedCrossAxisAlignment: CrossAxisAlignment.stretch,
children: [
screenWidth < 600
? SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: seasonEpisodes,
)
: seasonEpisodes
],
);
list.add(seasonList);
}
return ListView(
children: [
DetailCard(details: details),
Column(
children: list,
),
],
);
},
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator()));
}
Future<void> showAvailableTorrents(String id, int season, int episode) {

View File

@@ -1,59 +1,244 @@
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:go_router/go_router.dart';
import 'package:ui/movie_watchlist.dart';
import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/welcome_data.dart';
import 'package:ui/tv_details.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/utils.dart';
import 'package:ui/widgets/widgets.dart';
class WelcomePage extends ConsumerWidget {
class WelcomePage extends ConsumerStatefulWidget {
const WelcomePage({super.key});
static const routeTv = "/series";
static const routeMoivie = "/movies";
const WelcomePage({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return WelcomePageState();
}
}
class WelcomePageState extends ConsumerState<WelcomePage> {
//WelcomePageState({super.key});
final _formKey = GlobalKey<FormBuilderState>();
bool onlyShowUnfinished = false;
@override
Widget build(BuildContext context, WidgetRef ref) {
Widget build(BuildContext context) {
var uri = GoRouterState.of(context).uri.toString();
AsyncValue<List<MediaDetail>> data;
if (uri == routeMoivie) {
if (uri == WelcomePage.routeMoivie) {
data = ref.watch(movieWatchlistDataProvider);
} else {
data = ref.watch(tvWatchlistDataProvider);
}
return switch (data) {
AsyncData(:final value) => SingleChildScrollView(
child: Wrap(
alignment: WrapAlignment.start,
spacing: isSmallScreen(context) ? 0 : 10,
runSpacing: isSmallScreen(context) ? 10 : 20,
children: value.isEmpty
? [
Container(
height: MediaQuery.of(context).size.height * 0.6,
alignment: Alignment.center,
child: const Text(
"啥都没有...",
style: TextStyle(fontSize: 16),
))
]
: List.generate(value.length, (i) {
final item = value[i];
return MediaCard(item: item);
}),
),
),
_ => const MyProgressIndicator(),
};
return SelectionArea(
child: Stack(
//alignment: Alignment.bottomRight,
children: [
() {
return data.when(
data: (value) => SingleChildScrollView(
child: Wrap(
alignment: WrapAlignment.start,
spacing: isSmallScreen(context) ? 0 : 10,
runSpacing: isSmallScreen(context) ? 10 : 20,
children: getMediaAll(value),
),
),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
}(),
getMoreButtonAndActions(uri)
],
),
);
}
Widget getMoreButtonAndActions(String uri) {
return Row(
children: [
Expanded(child: Container()),
Column(
children: [
Expanded(child: Container()),
Padding(
padding: EdgeInsets.all(20),
child: MenuAnchor(
style: MenuStyle(
alignment: Alignment.topLeft,
backgroundColor: WidgetStatePropertyAll(Theme.of(context)
.colorScheme
.inversePrimary
.withOpacity(0.7)),
),
menuChildren: [parseName(), onlyUnfinished(), refreshAll(uri)],
builder: (context, controller, child) {
return Opacity(
opacity: 0.7,
child: FloatingActionButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Icon(Icons.more_horiz),
));
},
),
),
],
)
],
);
}
Widget onlyUnfinished() {
return CheckboxListTile(
value: onlyShowUnfinished,
onChanged: (b) {
setState(() {
onlyShowUnfinished = b!;
});
},
title: const Text(
"未完成",
style: TextStyle(fontSize: 16),
softWrap: false,
),
controlAffinity: ListTileControlAffinity.leading,
);
}
Widget refreshAll(String uri) {
return LoadingListTile(
icon: Icons.refresh,
text: "全部更新",
onPressed: () async {
if (uri == WelcomePage.routeMoivie) {
await APIs.downloadAllMovies().then((v) {
if (v.isNotEmpty) {
showSnakeBar("开始下载电影:$v");
}
});
} else {
await APIs.downloadAllTv().then((v) {
if (v.isNotEmpty) {
showSnakeBar("开始下载剧集:$v");
}
});
}
},
);
}
Widget parseName() {
return ListTile(
leading: Icon(Icons.calculate),
title: Text("测试解析"),
onTap: () => _showNameParsingDialog(),
);
}
bool isSmallScreen(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return screenWidth < 600;
}
List<Widget> getMediaAll(List<MediaDetail> list) {
if (list.isEmpty) {
return [
Container(
height: MediaQuery.of(context).size.height * 0.6,
alignment: Alignment.center,
child: const Text(
"啥都没有...",
style: TextStyle(fontSize: 16),
))
];
}
if (onlyShowUnfinished) {
list = list.where((v) => v.downloadedNum != v.monitoredNum).toList();
}
return List.generate(list.length, (i) {
final item = list[i];
return MediaCard(item: item);
});
}
Future<void> _showNameParsingDialog() async {
final resultController = TextEditingController();
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('测试名称解析'),
content: SizedBox(
width: 500,
height: 400,
child: FormBuilder(
key: _formKey,
initialValue: {"name": "", "type": "tv"},
child: Column(
children: [
FormBuilderTextField(
name: "name",
decoration: InputDecoration(labelText: "要解析的名字"),
validator: FormBuilderValidators.required(),
),
FormBuilderDropdown(
name: "type",
items: [
DropdownMenuItem(
value: "tv",
child: const Text("电视剧"),
),
DropdownMenuItem(value: "movie", child: const Text("电影"))
],
),
Center(
child: Padding(
padding: EdgeInsets.all(10),
child: LoadingTextButton(
onPressed: () async {
if (_formKey.currentState!.saveAndValidate()) {
final values = _formKey.currentState!.value;
//print(values);
if (values["type"] == "tv") {
var s = await APIs.parseTvName(values["name"]);
resultController.text = s;
} else {
var s =
await APIs.parseMovieName(values["name"]);
resultController.text = s;
}
}
return;
},
label: Text("解析")),
),
),
TextField(
maxLines: 8,
controller: resultController,
)
],
),
),
),
);
},
);
}
}
class MediaCard extends StatelessWidget {

View File

@@ -1,6 +1,7 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
import 'package:intl/intl.dart';
import 'package:ui/providers/APIs.dart';
import 'dart:io' show Platform;
@@ -60,5 +61,5 @@ bool isDesktop() {
bool isSmallScreen(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return screenWidth < 600;
return screenWidth < Breakpoints.small.endWidth!.toDouble();
}

View File

@@ -141,8 +141,58 @@ class _MySliderState extends State<MyRangeSlider> {
}
}
class LoadingListTile extends StatefulWidget {
const LoadingListTile(
{super.key,
required this.onPressed,
required this.icon,
required this.text});
final Future<void> Function() onPressed;
final IconData icon;
final String text;
@override
State<StatefulWidget> createState() {
return LoadingListTileState();
}
}
class LoadingListTileState extends State<LoadingListTile> {
bool loading = false;
@override
Widget build(BuildContext context) {
return ListTile(
onTap: loading
? null
: () async {
setState(() => loading = true);
try {
await widget.onPressed();
} catch (e) {
showSnakeBar("操作失败:$e");
} finally {
setState(() => loading = false);
}
},
title: Text(widget.text),
leading: loading
? Container(
width: 24,
height: 24,
padding: const EdgeInsets.all(2.0),
child: const CircularProgressIndicator(
color: Colors.grey,
strokeWidth: 3,
),
)
: Icon(widget.icon));
}
}
class LoadingIconButton extends StatefulWidget {
const LoadingIconButton({super.key, required this.onPressed, required this.icon, this.tooltip});
const LoadingIconButton(
{super.key, required this.onPressed, required this.icon, this.tooltip});
final Future<void> Function() onPressed;
final IconData icon;
final String? tooltip;
@@ -159,7 +209,7 @@ class _LoadingIconButtonState extends State<LoadingIconButton> {
@override
Widget build(BuildContext context) {
return IconButton(
tooltip: widget.tooltip,
tooltip: widget.tooltip,
onPressed: loading
? null
: () async {
@@ -187,7 +237,8 @@ class _LoadingIconButtonState extends State<LoadingIconButton> {
}
class LoadingTextButton extends StatefulWidget {
const LoadingTextButton({super.key, required this.onPressed, required this.label});
const LoadingTextButton(
{super.key, required this.onPressed, required this.label});
final Future<void> Function() onPressed;
final Widget label;
@@ -230,3 +281,32 @@ class _LoadingTextButtonState extends State<LoadingTextButton> {
);
}
}
class PoError extends StatelessWidget {
const PoError({super.key, required this.msg, required this.err});
final String msg;
final dynamic err;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("$msg ", style: TextStyle(color:Theme.of(context).colorScheme.error),),
Tooltip(
message: "$err",
child: Icon(Icons.info,color: Theme.of(context).colorScheme.error,),
)
],
);
}
}
class PoNetworkError extends StatelessWidget {
const PoNetworkError({super.key, required this.err});
final dynamic err;
@override
Widget build(BuildContext context) {
return PoError(msg: "网络错误,请检查网络链接", err: err);
}
}

View File

@@ -6,7 +6,7 @@ packages:
description:
name: another_flushbar
sha256: "19bf9520230ec40b300aaf9dd2a8fefcb277b25ecd1c4838f530566965befc2a"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.12.30"
another_transformer_page_view:
@@ -14,7 +14,7 @@ packages:
description:
name: another_transformer_page_view
sha256: a7cd46ede62d621c5abe7e58c7cb2745abe67b3bfec64f59b8889c93d7be7a8e
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
async:
@@ -22,7 +22,7 @@ packages:
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
boolean_selector:
@@ -30,7 +30,7 @@ packages:
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
characters:
@@ -38,7 +38,7 @@ packages:
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
clock:
@@ -46,7 +46,7 @@ packages:
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
collection:
@@ -54,7 +54,7 @@ packages:
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
cupertino_icons:
@@ -62,7 +62,7 @@ packages:
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dio:
@@ -70,7 +70,7 @@ packages:
description:
name: dio
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "5.7.0"
dio_web_adapter:
@@ -78,7 +78,7 @@ packages:
description:
name: dio_web_adapter
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
equatable:
@@ -86,7 +86,7 @@ packages:
description:
name: equatable
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.0.5"
fake_async:
@@ -94,7 +94,7 @@ packages:
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
flutter:
@@ -107,7 +107,7 @@ packages:
description:
name: flutter_adaptive_scaffold
sha256: "8c515a2cb8abb3a567f8e77f10b33f47bb6fcadfe31f62364e0aca36280cdf93"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
flutter_form_builder:
@@ -115,7 +115,7 @@ packages:
description:
name: flutter_form_builder
sha256: c278ef69b08957d484f83413f0e77b656a39b7a7bb4eb8a295da3a820ecc6545
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "9.5.0"
flutter_lints:
@@ -123,7 +123,7 @@ packages:
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_localizations:
@@ -136,17 +136,17 @@ packages:
description:
name: flutter_login
sha256: "1f7c46d0d76081cf4c5180e3a265b1f5b1d7e48c81859f58f03a8dcd27338b85"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "6eda4e247774474c715a0805a2fb8e3cd55fbae4ead641e063c95b4bd5f3b317"
url: "https://pub.flutter-io.cn"
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
version: "2.6.1"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -162,7 +162,7 @@ packages:
description:
name: font_awesome_flutter
sha256: "275ff26905134bcb59417cf60ad979136f1f8257f2f449914b2c3e05bbb4cd6f"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "10.7.0"
form_builder_validators:
@@ -170,7 +170,7 @@ packages:
description:
name: form_builder_validators
sha256: c61ed7b1deecf0e1ebe49e2fa79e3283937c5a21c7e48e3ed9856a4a14e1191a
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "11.0.0"
go_router:
@@ -178,7 +178,7 @@ packages:
description:
name: go_router
sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "14.3.0"
http_parser:
@@ -186,7 +186,7 @@ packages:
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
intl:
@@ -194,7 +194,7 @@ packages:
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.19.0"
intl_phone_number_input:
@@ -202,7 +202,7 @@ packages:
description:
name: intl_phone_number_input
sha256: "1c4328713a9503ab26a1fdbb6b00b4cada68c18aac922b35bedbc72eff1297c3"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.7.4"
js:
@@ -210,7 +210,7 @@ packages:
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.6.7"
leak_tracker:
@@ -218,7 +218,7 @@ packages:
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
leak_tracker_flutter_testing:
@@ -226,7 +226,7 @@ packages:
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
leak_tracker_testing:
@@ -234,7 +234,7 @@ packages:
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
libphonenumber_platform_interface:
@@ -242,7 +242,7 @@ packages:
description:
name: libphonenumber_platform_interface
sha256: f801f6c65523f56504b83f0890e6dad584ab3a7507dca65fec0eed640afea40f
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.4.2"
libphonenumber_plugin:
@@ -250,7 +250,7 @@ packages:
description:
name: libphonenumber_plugin
sha256: c615021d9816fbda2b2587881019ed595ecdf54d999652d7e4cce0e1f026368c
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.3.3"
libphonenumber_web:
@@ -258,7 +258,7 @@ packages:
description:
name: libphonenumber_web
sha256: "8186f420dbe97c3132283e52819daff1e55d60d6db46f7ea5ac42f42a28cc2ef"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
lints:
@@ -266,7 +266,7 @@ packages:
description:
name: lints
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
logging:
@@ -274,7 +274,7 @@ packages:
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
@@ -282,7 +282,7 @@ packages:
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
material_color_utilities:
@@ -290,7 +290,7 @@ packages:
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
@@ -298,7 +298,7 @@ packages:
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
nested:
@@ -306,7 +306,7 @@ packages:
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
path:
@@ -314,7 +314,7 @@ packages:
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
phone_numbers_parser:
@@ -322,7 +322,7 @@ packages:
description:
name: phone_numbers_parser
sha256: "62451b689d842791ed1fd5dc9eacf36ffa8bad23a78ad6cde732dc2fb222fae2"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "8.3.0"
plugin_platform_interface:
@@ -330,7 +330,7 @@ packages:
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
@@ -338,7 +338,7 @@ packages:
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.1.2"
quiver:
@@ -346,23 +346,23 @@ packages:
description:
name: quiver
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: bd6e656a764e3d27f211975626e0c4f9b8d06ab16acf3c7ba7a8061e09744c75
url: "https://pub.flutter-io.cn"
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
url: "https://pub.dev"
source: hosted
version: "2.6.0"
version: "2.6.1"
sign_in_button:
dependency: transitive
description:
name: sign_in_button
sha256: "977b9b0415d2f3909e642275dfabba7919ba8e111324641b76cae6d1acbd183e"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
simple_gesture_detector:
@@ -370,7 +370,7 @@ packages:
description:
name: simple_gesture_detector
sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.2.1"
sky_engine:
@@ -383,7 +383,7 @@ packages:
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
stack_trace:
@@ -391,7 +391,7 @@ packages:
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
state_notifier:
@@ -399,7 +399,7 @@ packages:
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
stream_channel:
@@ -407,7 +407,7 @@ packages:
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
string_scanner:
@@ -415,7 +415,7 @@ packages:
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
table_calendar:
@@ -423,7 +423,7 @@ packages:
description:
name: table_calendar
sha256: "4ca32b2fc919452c9974abd4c6ea611a63e33b9e4f0b8c38dba3ac1f4a6549d1"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
term_glyph:
@@ -431,7 +431,7 @@ packages:
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test_api:
@@ -439,7 +439,7 @@ packages:
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
timeago:
@@ -447,7 +447,7 @@ packages:
description:
name: timeago
sha256: "054cedf68706bb142839ba0ae6b135f6b68039f0b8301cbe8784ae653d5ff8de"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.7.0"
typed_data:
@@ -455,7 +455,7 @@ packages:
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
url_launcher:
@@ -463,23 +463,23 @@ packages:
description:
name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2"
url: "https://pub.flutter-io.cn"
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
url: "https://pub.dev"
source: hosted
version: "6.3.12"
version: "6.3.14"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_linux:
@@ -487,7 +487,7 @@ packages:
description:
name: url_launcher_linux
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
url_launcher_macos:
@@ -495,7 +495,7 @@ packages:
description:
name: url_launcher_macos
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
url_launcher_platform_interface:
@@ -503,7 +503,7 @@ packages:
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
@@ -511,7 +511,7 @@ packages:
description:
name: url_launcher_web
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
url_launcher_windows:
@@ -519,7 +519,7 @@ packages:
description:
name: url_launcher_windows
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
vector_math:
@@ -527,7 +527,7 @@ packages:
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
@@ -535,7 +535,7 @@ packages:
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "14.2.5"
web:
@@ -543,7 +543,7 @@ packages:
description:
name: web
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:

View File

@@ -30,21 +30,21 @@ environment:
dependencies:
flutter:
sdk: flutter
dio: ^5.4.3+1
dio: ^5.7.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
go_router: ^14.2.0
flutter_riverpod: ^2.5.1
quiver: ^3.2.1
cupertino_icons: ^1.0.8
go_router: ^14.3.0
flutter_riverpod: ^2.6.1
quiver: ^3.2.2
flutter_login: ^5.0.0
intl: ^0.19.0
flutter_adaptive_scaffold: ^0.3.1
flutter_form_builder: ^9.3.0
flutter_form_builder: ^9.5.0
form_builder_validators: ^11.0.0
url_launcher: ^6.3.0
url_launcher: ^6.3.1
timeago: ^3.7.0
table_calendar: ^3.1.2
@@ -71,7 +71,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
assets:
- assets/
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg