mirror of
https://github.com/simon-ding/polaris.git
synced 2026-02-23 20:20:46 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae611943c3 | ||
|
|
4fd11540cd | ||
|
|
587a28127b | ||
|
|
05ae58030c | ||
|
|
f1c4e306f4 | ||
|
|
949b6e5188 | ||
|
|
0d4b453d0a | ||
|
|
bce4d93ab1 | ||
|
|
36b72e6461 | ||
|
|
62417727f9 | ||
|
|
03f72b9d86 | ||
|
|
c17cf750e5 | ||
|
|
b176253fc4 | ||
|
|
1e2d8b8520 | ||
|
|
3739f2c960 | ||
|
|
bb6da47efb | ||
|
|
c28373bde1 | ||
|
|
8ce7045466 | ||
|
|
0b1bd8226d | ||
|
|
e67413cec2 | ||
|
|
2da02fa706 | ||
|
|
bc50dd888a | ||
|
|
0305c0709d |
@@ -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"`
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 MiB After Width: | Height: | Size: 843 KiB |
@@ -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 方式安装
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
BIN
ui/assets/wechat.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -148,7 +148,7 @@ class _ActivityPageState extends ConsumerState<ActivityPage>
|
||||
},
|
||||
));
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
})
|
||||
],
|
||||
|
||||
230
ui/lib/main.dart
230
ui/lib/main.dart
@@ -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"))),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -94,7 +94,7 @@ class _AuthState extends ConsumerState<AuthSettings> {
|
||||
],
|
||||
));
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ class _GeneralState extends ConsumerState<GeneralSettings> {
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
142
ui/pubspec.lock
142
ui/pubspec.lock
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user