Compare commits

...

34 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
Simon Ding
5e9e85206e doc: update 2024-10-27 20:12:46 +08:00
Simon Ding
3babb9f5c7 doc: update prowlarr setting 2024-10-27 20:11:04 +08:00
Simon Ding
c9928f10ce revert: to html renderer 2024-10-21 09:24:35 +08:00
Simon Ding
a2da1e7479 feat: use canvas render 2024-10-21 08:59:51 +08:00
Simon Ding
038f643ce3 ui: upgrade dependences 2024-10-21 08:46:10 +08:00
Simon Ding
cf2fa90d5d ui: change prowlarr setting text 2024-10-20 22:28:12 +08:00
Simon Ding
677923ea8a fix: match priority 2024-10-20 16:36:34 +08:00
Simon Ding
1192d00fe0 feat: change icon 2024-10-20 16:24:28 +08:00
Simon Ding
b317636a8a feat: support prowlarr connection 2024-10-20 16:21:58 +08:00
Simon Ding
0e6465593b update 2024-10-20 13:37:33 +08:00
Simon
2cb8a5b6fb Update README.md 2024-10-20 13:20:13 +08:00
44 changed files with 1193 additions and 492 deletions

View File

@@ -1,4 +1,10 @@
# Polaris
![main_page](./doc/assets/main_page.png)
<h1 align="center">Polaris</h1>
<div align="center">
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/simon-ding/polaris/go.yml)
![GitHub Release](https://img.shields.io/github/v/release/simon-ding/polaris)
@@ -6,11 +12,10 @@
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/simon-ding/polaris)
Polaris 是一个电视剧和电影的追踪下载软件。对动漫日剧美剧都有良好的匹配支持webdav或者本地存储。
**Polaris 是一个电视剧和电影的追踪下载软件。对动漫日剧美剧都有良好的匹配支持webdav或者本地存储。**
</div>
![main_page](./doc/assets/main_page.png)
![detail_page](./doc/assets/detail_page.png)
![anime](./doc/assets/anime_match.png)
交流群: https://t.me/+8R2nzrlSs2JhMDgx
@@ -40,6 +45,13 @@ Polaris 是一个电视剧和电影的追踪下载软件。对动漫日剧美剧
- [ ] 手机客户端
## 截图
![main_page](./doc/assets/main_page.png)
![detail_page](./doc/assets/detail_page.png)
![anime](./doc/assets/anime_match.png)
## 运行原理
Polaris本身不提供任何资源要使其能正常工作需要同时安装一个下载客户端transmission和一个索引客户端jackett
@@ -48,13 +60,17 @@ Polaris通过索引客户端查询相关的BT/PT站点然后把查到的资
![](./doc/assets/yuanli.png)
## 对比 sonarr/radarr
<!-- ## 对比 sonarr/radarr
* 更好的中文支持
* 对于动漫、日剧的良好支持,配合国内站点基本能匹配上对应资源
* 支持 webdav 后端存储,可以配合 alist 或者阿里云来实现下载后实时传到云上的功能。这样外出就可以不依靠家里的宽带来看电影了,或者实现个轻 NAS 功能,下载功能放在本地,数据放在云盘
* golang 实现后端,相比于 .NET 更节省资源
* 一个程序同时实现了电影、电视剧功能,不需要装两个程序
* 当然 sonarr/radarr 也是非常优秀的开源项目,目前 Polaris 功能还没有 sonarr/radarr 丰富
* 当然 sonarr/radarr 也是非常优秀的开源项目,目前 Polaris 功能还没有 sonarr/radarr 丰富 -->
## Stargazers over time
[![Stargazers over time](https://starchart.cc/simon-ding/polaris.svg?variant=adaptive)](https://starchart.cc/simon-ding/polaris)
-------------

View File

@@ -17,6 +17,7 @@ const (
SetttingSizeLimiter = "size_limiter"
SettingTvNamingFormat = "tv_naming_format"
SettingMovieNamingFormat = "movie_naming_format"
SettingProwlarrInfo = "prowlarr_info"
)
const (
@@ -60,3 +61,9 @@ type Limiter struct {
Max int `json:"max"`
Min int `json:"min"`
}
type ProwlarrSetting struct {
Disabled bool `json:"disabled"`
ApiKey string `json:"api_key"`
URL string `json:"url"`
}

View File

@@ -660,3 +660,24 @@ func (c *Client) CleanAllDanglingEpisodes() error {
func (c *Client) AddBlacklistItem(item *ent.Blacklist) error {
return c.ent.Blacklist.Create().SetType(item.Type).SetValue(item.Value).SetNotes(item.Notes).Exec(context.Background())
}
func (c *Client) GetProwlarrSetting() (*ProwlarrSetting, error) {
s := c.GetSetting(SettingProwlarrInfo)
if s == "" {
return nil, errors.New("prowlarr setting not set")
}
var se ProwlarrSetting
if err := json.Unmarshal([]byte(s), &se); err != nil {
return nil, err
}
return &se, nil
}
func (c *Client) SaveProwlarrSetting(se *ProwlarrSetting) error {
data, err := json.Marshal(se)
if err != nil {
return err
}
return c.SetSetting(SettingProwlarrInfo, string(data))
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 843 KiB

View File

@@ -2,7 +2,7 @@
要正确使用此程序,需要配置好以下设置:
### TMDB设置
### 1. TMDB设置
1. 因为此程序需要使用到 TMDB 的数据,使用此程序首先要申请一个 TMDB 的 Api Key. 申请教程请 google [tmdb api key申请](https://www.google.com/search?q=tmdb+api+key%E7%94%B3%E8%AF%B7)
@@ -10,35 +10,16 @@
**注意:** TMDB可能需要翻墙才能使用参考 [TMDB 访问问题](./tmdb.md)
### 索引器
### 2. 索引器
索引器是资源提供者,目前支持 torznab 协议,意味着 polarr 或者 jackett 都可以支持。请自行部署相关程序,或者使用的 docker compose 配置一起拉起
使用配置页面索引器配置或者prowlarr设置其中一个即可。
推荐使用 linuxserver 的镜像https://docs.linuxserver.io/images/docker-jackett/
#### jackett配置参考 [jackett](./jackett.md)
#### 索引器配
#### prowlarr设
索引器配置这里以 jackett 为例。使用默认 docker compose 配置拉起后以 http://< ip >:9117 可访问 jackett 的主页。
1. 打开 jackett 主页后,点击页面上面的 Add indexer会出现 BT/PT 站点列表,选择你需要的站点点击+号添加。如果是PT请自行配置好相关配置
![add indexer](./assets/add_indexer.png)
![search add](./assets/search_add.png)
2. 添加后主页即会显示相应的BT/PT站点点击 *Copy Torznab Feed* 即得到了我们需要的地址
![copy feed](./assets/copy_feed.png)
3. 回到我们的主程序 Polaris 当中,点击 *设置 -> 索引器设置* -> 点击+号增加新的索引器输入一个名称拷贝我们第2步得到的地址到地址栏
![polaris add indexer](./assets/polaris_add_indexer.png)
4. 选相框中我们可以看到,还需要一个 API Key我们回到 Jackett 中,在页面右上角,复制我们需要的 API Key
![api key](./assets/jackett_api_key.png)
5. 恭喜!你已经成功完成了索引器配置。如需要更多的站点,请重复相同的操作完成配置
1) 取得prowlarr的url和api key api key在 *Prowlarr 设置 -> 通用 -> API 密钥* 处取得
2) 对应参数填到 polaris程序*设置->prowlarr设置*当中
### 下载器

21
doc/jackett.md Normal file
View File

@@ -0,0 +1,21 @@
## jackett 索引器配置
1. 打开 jackett 主页后,点击页面上面的 Add indexer会出现 BT/PT 站点列表,选择你需要的站点点击+号添加。如果是PT请自行配置好相关配置
![add indexer](./assets/add_indexer.png)
![search add](./assets/search_add.png)
2. 添加后主页即会显示相应的BT/PT站点点击 *Copy Torznab Feed* 即得到了我们需要的地址
![copy feed](./assets/copy_feed.png)
3. 回到我们的主程序 Polaris 当中,点击 *设置 -> 索引器设置* -> 点击+号增加新的索引器输入一个名称拷贝我们第2步得到的地址到地址栏
![polaris add indexer](./assets/polaris_add_indexer.png)
4. 选相框中我们可以看到,还需要一个 API Key我们回到 Jackett 中,在页面右上角,复制我们需要的 API Key
![api key](./assets/jackett_api_key.png)
5. 恭喜!你已经成功完成了索引器配置。如需要更多的站点,请重复相同的操作完成配置

View File

@@ -1,8 +1,8 @@
# 快速开始
## 安装 Polaris
## 1. 安装 Polaris
### Docker Compose 方式安装
### 1.1 Docker Compose 方式安装
最简单使用本程序的方式是通过docker compose下面内容保存成 docker-compose.yml然后执行 docker compose up -d, 即可拉起程序。
@@ -12,18 +12,18 @@ 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 #端口映射,冒号前的端口可自行改为需要的
```
### Docker 方式安装
### 1.2 Docker 方式安装
也可以通过原始 docker 命令的方式安装 Polaris
@@ -40,19 +40,19 @@ docker run -d \
ghcr.io/simon-ding/polaris:latest
```
### Unraid 安装
### 1.3 Unraid 安装
参考下图进行配置
![](./assets/unraid.png)
### 访问
### 1.4 访问
拉起之后访问 http://< ip >:8080 即可访问 Polaris 的主页:
![](./assets/main_page.png)
## 安装下载客户端
## 2. 安装下载客户端
Polaris 需要下载客户端的配合使用,目前支持 Transmission 和 Qbittorrent。推荐使用linuxserver镜像进行安装
@@ -62,16 +62,18 @@ Polaris 需要下载客户端的配合使用,目前支持 Transmission 和 Qbi
需要注意的是下载客户端内 /downloads 路径的映射地址要和 Polaris的/downloads路径映射保持一致。也就是说他俩都要映射到同一路径。
## 安装 Jackett
Polaris 如果要正常工作还需要一个索引客户端的支持目前支持jackett索引客户端。
## 3. 安装 Jackett/Prowlarr
Polaris 如果要正常工作还需要一个索引客户端的支持目前支持jackett/prowlarr索引客户端。推荐使用prowlarr设置更简单
安装方式见:
* [linuxserver/jackett](https://docs.linuxserver.io/images/docker-jackett/)
* [linuxserver/prowlarr](https://docs.linuxserver.io/images/docker-prowlarr/)
## 联合安装
## 4. 联合安装
如果觉得一个个安装麻烦也可以使用下面docker compose文件一键拉起所有组件
@@ -125,7 +127,7 @@ docker compose up -d
```
## 配置
## 5. 配置
详细配置请看 [配置篇](./configuration.md)

1
go.mod
View File

@@ -21,6 +21,7 @@ require (
github.com/ncruces/go-sqlite3 v0.18.4
github.com/nikoksr/notify v1.0.0
github.com/stretchr/testify v1.9.0
golift.io/starr v1.0.0
)
require (

10
go.sum
View File

@@ -216,8 +216,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@@ -251,8 +249,6 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/nikoksr/notify v1.0.0 h1:qe9/6FRsWdxBgQgWcpvQ0sv8LRGJZDpRB4TkL2uNdO8=
github.com/nikoksr/notify v1.0.0/go.mod h1:hPaaDt30d6LAA7/5nb0e48Bp/MctDfycCSs8VEgN29I=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
@@ -310,8 +306,6 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
@@ -453,10 +447,10 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golift.io/starr v1.0.0 h1:IDSaSL+ZYxdLT/Lg//dg/iwZ39LHO3D5CmbLCOgSXbI=
golift.io/starr v1.0.0/go.mod h1:xnUwp4vK62bDvozW9QHUYc08m6kjwaZnGw3Db65fQHw=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

View File

@@ -37,6 +37,7 @@ func ParseDoulist(doulistUrl string) (*importlist.Response, error) {
if err != nil {
return nil, err
}
var items []importlist.Item
doc.Find("div[class=doulist-item]").Each(func(i int, selection *goquery.Selection) {
titleDiv := selection.Find("div[class=title]")
link := titleDiv.Find("div>a")
@@ -64,18 +65,26 @@ func ParseDoulist(doulistUrl string) (*importlist.Response, error) {
}
}
}
_, err := parseDetailPage(strings.TrimSpace(href))
if err != nil {
log.Errorf("get detail page: %v", err)
return
}
item := importlist.Item{
Title: strings.TrimSpace(link.Text()),
Year: year,
}
items = append(items, item)
_ = item
println(link.Text(), href)
//println(link.Text(), href)
})
return nil, nil
return &importlist.Response{Items: items}, nil
}
func parseDetailPage(url string) (string, error) {
println(url)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
@@ -95,6 +104,14 @@ func parseDetailPage(url string) (string, error) {
if err != nil {
return "", err
}
doc.Find("div[class='subject clearfix']").Each(func(i int, se *goquery.Selection) {
println(se.Text())
se.Children().Get(1)
imdb := se.Find("div[class='info']").First().Children().Last()
println(imdb.Text())
})
_ = doc
return "", nil
}

View File

@@ -6,6 +6,6 @@ import (
)
func TestParseDoulist(t *testing.T) {
r, err := ParseDoulist("https://www.douban.com/doulist/166422/")
r, err := ParseDoulist("https://www.douban.com/doulist/81580/")
log.Info(r, err)
}

77
pkg/prowlarr/prowlarr.go Normal file
View File

@@ -0,0 +1,77 @@
package prowlarr
import (
"encoding/json"
"fmt"
"polaris/db"
"polaris/ent"
"strings"
"time"
"golift.io/starr"
"golift.io/starr/prowlarr"
)
type ProwlarrSupportType string
const (
TV ProwlarrSupportType = "tv"
Movie ProwlarrSupportType = "movie"
)
type Client struct {
p *prowlarr.Prowlarr
apiKey string
url string
}
func New(apiKey, url string) *Client {
c := starr.New(apiKey, url, 10*time.Second)
p := prowlarr.New(c)
return &Client{p: p, apiKey: apiKey, url: url}
}
func (c *Client) GetIndexers(t ProwlarrSupportType) ([]*db.TorznabInfo, error) {
ins, err := c.p.GetIndexers()
if err != nil {
return nil, err
}
var indexers []*db.TorznabInfo
for _, in := range ins {
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 {
if r, ok := f.Value.(float64); ok {
seedRatio = r
}
}
}
setting := db.TorznabSetting{
URL: fmt.Sprintf("%s/%d/api", strings.TrimSuffix(c.url, "/"), in.ID),
ApiKey: c.apiKey,
}
data, _ := json.Marshal(&setting)
entIndexer := ent.Indexers{
Name: in.Name,
Implementation: "torznab",
Priority: 128 - int(in.Priority),
SeedRatio: float32(seedRatio),
Settings: string(data),
}
indexers = append(indexers, &db.TorznabInfo{
Indexers: &entIndexer,
TorznabSetting: setting,
})
}
return indexers, nil
}

View File

@@ -0,0 +1,13 @@
package prowlarr
import (
"polaris/log"
"testing"
)
func Test111(t *testing.T) {
c := New("", "http://10.0.0.8:9696/")
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

@@ -6,6 +6,7 @@ import (
"polaris/ent/media"
"polaris/log"
"polaris/pkg/metadata"
"polaris/pkg/prowlarr"
"polaris/pkg/torznab"
"slices"
"sort"
@@ -32,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 {
@@ -136,7 +137,7 @@ func torrentSizeOk(detail *db.MediaDetails, torrentSize int, param *SearchParam)
}
}
}
return torrentSize > defaultMinSize * multiplier
return torrentSize > defaultMinSize*multiplier
}
func seasonEpisodeCount(detail *db.MediaDetails, seasonNum int) int {
@@ -170,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...)
}
@@ -226,10 +227,21 @@ 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 && !p.Disabled { //prowlarr exists
c := prowlarr.New(p.ApiKey, p.URL)
all, err := c.GetIndexers(t)
if err != nil {
log.Warnf("get prowlarr all indexer error: %v", err)
} else {
allTorznab = append(allTorznab, all...)
}
}
resChan := make(chan []torznab.Result)
var wg sync.WaitGroup

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

@@ -70,6 +70,8 @@ func (s *Server) Serve() error {
setting.POST("/parse/movie", HttpHandler(s.ParseMovie))
setting.POST("/monitoring", HttpHandler(s.ChangeEpisodeMonitoring))
setting.POST("/cron/trigger", HttpHandler(s.TriggerCronJob))
setting.GET("/prowlarr", HttpHandler(s.GetProwlarrSetting))
setting.POST("/prowlarr", HttpHandler(s.SaveProwlarrSetting))
}
activity := api.Group("/activity")
{
@@ -94,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")
{
@@ -128,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

@@ -8,6 +8,7 @@ import (
"polaris/ent"
"polaris/ent/downloadclients"
"polaris/log"
"polaris/pkg/prowlarr"
"polaris/pkg/qbittorrent"
"polaris/pkg/torznab"
"polaris/pkg/transmission"
@@ -104,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)
@@ -303,3 +305,28 @@ func (s *Server) TriggerCronJob(c *gin.Context) (interface{}, error) {
}
return "success", nil
}
func (s *Server) GetProwlarrSetting(c *gin.Context) (interface{}, error) {
se, err :=s.db.GetProwlarrSetting()
if err != nil {
return &db.ProwlarrSetting{}, nil
}
return se, nil
}
func (s *Server) SaveProwlarrSetting(c *gin.Context) (interface{}, error) {
var in db.ProwlarrSetting
if err := c.ShouldBindJSON(&in); err != nil {
return nil, err
}
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 {
return nil, err
}
return "success", 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/";
@@ -40,6 +45,7 @@ class APIs {
static final addImportlistUrl = "$_baseUrl/api/v1/importlist/add";
static final deleteImportlistUrl = "$_baseUrl/api/v1/importlist/delete";
static final getAllImportlists = "$_baseUrl/api/v1/importlist/";
static final prowlarrUrl = "$_baseUrl/api/v1/setting/prowlarr";
static final notifierAllUrl = "$_baseUrl/api/v1/notifier/all";
static final notifierDeleteUrl = "$_baseUrl/api/v1/notifier/id/";
@@ -49,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";
@@ -113,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

@@ -25,10 +25,14 @@ var importlistProvider =
AsyncNotifierProvider.autoDispose<ImportListData, List<ImportList>>(
ImportListData.new);
var prowlarrSettingDataProvider =
AsyncNotifierProvider.autoDispose<ProwlarrSettingData, ProwlarrSetting>(
ProwlarrSettingData.new);
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);
@@ -503,3 +507,44 @@ class ImportListData extends AutoDisposeAsyncNotifier<List<ImportList>> {
ref.invalidateSelf();
}
}
class ProwlarrSetting {
final bool disabled;
final String apiKey;
final String 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"],
disabled: json["disabled"] ?? false);
}
Map<String, dynamic> tojson() =>
{"api_key": apiKey, "url": url, "disabled": disabled};
}
class ProwlarrSettingData extends AutoDisposeAsyncNotifier<ProwlarrSetting> {
@override
FutureOr<ProwlarrSetting> build() async {
final dio = APIs.getDio();
var resp = await dio.get(APIs.prowlarrUrl);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
var se = ProwlarrSetting.fromJson(sp.data);
return se;
}
Future<void> save(ProwlarrSetting ps) async {
final dio = APIs.getDio();
var resp = await dio.post(APIs.prowlarrUrl, data: ps.tojson());
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
ref.invalidateSelf();
}
}

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

@@ -0,0 +1,85 @@
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:ui/providers/settings.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/utils.dart';
import 'package:ui/widgets/widgets.dart';
class ProwlarrSettingPage extends ConsumerStatefulWidget {
const ProwlarrSettingPage({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() {
return ProwlarrSettingState();
}
}
class ProwlarrSettingState extends ConsumerState<ProwlarrSettingPage> {
final _formKey = GlobalKey<FormBuilderState>();
@override
Widget build(BuildContext context) {
var ps = ref.watch(prowlarrSettingDataProvider);
return ps.when(
data: (v) => FormBuilder(
key: _formKey, //设置globalKey用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
initialValue: {
"api_key": v.apiKey,
"url": v.url,
"disabled": v.disabled
},
child: Column(
children: [
FormBuilderTextField(
name: "url",
decoration: const InputDecoration(
labelText: "Prowlarr URL",
icon: Icon(Icons.web),
hintText: "http://10.0.0.8:9696"),
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "api_key",
decoration: const InputDecoration(
labelText: "API Key",
icon: Icon(Icons.key),
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),
child: ElevatedButton(
onPressed: () {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
var f = ref
.read(prowlarrSettingDataProvider.notifier)
.save(ProwlarrSetting(
apiKey: values["api_key"],
url: values["url"],
disabled: values["disabled"]))
.then((v) => showSnakeBar("更新成功"));
showLoadingWithFuture(f);
}
},
child: const Padding(
padding: EdgeInsets.all(10),
child: Text("保存"),
)),
),
)
],
),
),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
}
}

View File

@@ -6,6 +6,7 @@ import 'package:ui/settings/general.dart';
import 'package:ui/settings/importlist.dart';
import 'package:ui/settings/indexer.dart';
import 'package:ui/settings/notifier.dart';
import 'package:ui/settings/prowlarr.dart';
import 'package:ui/settings/storage.dart';
class SystemSettingsPage extends ConsumerStatefulWidget {
@@ -21,17 +22,19 @@ 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()),
getExpansionTile("Prowlarr 设置", const ProwlarrSettingPage()),
getExpansionTile("下载器", const DownloaderSettings()),
getExpansionTile("存储", const StorageSettings()),
getExpansionTile("通知客户端", const NotifierSettings()),
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,31 +62,31 @@ packages:
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dio:
dependency: "direct main"
description:
name: dio
sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714
url: "https://pub.flutter-io.cn"
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
url: "https://pub.dev"
source: hosted
version: "5.5.0+1"
version: "5.7.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac"
url: "https://pub.flutter-io.cn"
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
version: "2.0.0"
equatable:
dependency: transitive
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:
@@ -106,26 +106,26 @@ packages:
dependency: "direct main"
description:
name: flutter_adaptive_scaffold
sha256: "56d4d81fe88ecffe8ae96b8d89a1ae793c0a85035bb9b74ff28f20eea0cdbdc2"
url: "https://pub.flutter-io.cn"
sha256: "8c515a2cb8abb3a567f8e77f10b33f47bb6fcadfe31f62364e0aca36280cdf93"
url: "https://pub.dev"
source: hosted
version: "0.1.11+1"
version: "0.3.1"
flutter_form_builder:
dependency: "direct main"
description:
name: flutter_form_builder
sha256: "447f8808f68070f7df968e8063aada3c9d2e90e789b5b70f3b44e4b315212656"
url: "https://pub.flutter-io.cn"
sha256: c278ef69b08957d484f83413f0e77b656a39b7a7bb4eb8a295da3a820ecc6545
url: "https://pub.dev"
source: hosted
version: "9.3.0"
version: "9.5.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.flutter-io.cn"
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "5.0.0"
flutter_localizations:
dependency: transitive
description: flutter
@@ -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: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d"
url: "https://pub.flutter-io.cn"
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
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,23 +170,23 @@ 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:
dependency: "direct main"
description:
name: go_router
sha256: ddc16d34b0d74cb313986918c0f0885a7ba2fc24d8fb8419de75f0015144ccfe
url: "https://pub.flutter-io.cn"
sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc"
url: "https://pub.dev"
source: hosted
version: "14.2.3"
version: "14.3.0"
http_parser:
dependency: transitive
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,31 +258,31 @@ packages:
description:
name: libphonenumber_web
sha256: "8186f420dbe97c3132283e52819daff1e55d60d6db46f7ea5ac42f42a28cc2ef"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
lints:
dependency: transitive
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.flutter-io.cn"
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "5.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
url: "https://pub.flutter-io.cn"
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.3.0"
matcher:
dependency: transitive
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,31 +338,31 @@ packages:
description:
name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "6.1.2"
quiver:
dependency: "direct main"
description:
name: quiver
sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47
url: "https://pub.flutter-io.cn"
sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2
url: "https://pub.dev"
source: hosted
version: "3.2.1"
version: "3.2.2"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d
url: "https://pub.flutter-io.cn"
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
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,63 +447,63 @@ packages:
description:
name: timeago
sha256: "054cedf68706bb142839ba0ae6b135f6b68039f0b8301cbe8784ae653d5ff8de"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "3.7.0"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
url: "https://pub.flutter-io.cn"
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.4.0"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
url: "https://pub.flutter-io.cn"
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.1"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9"
url: "https://pub.flutter-io.cn"
sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193"
url: "https://pub.dev"
source: hosted
version: "6.3.8"
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:
dependency: transitive
description:
name: url_launcher_linux
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
url: "https://pub.flutter-io.cn"
sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.2.0"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
url: "https://pub.flutter-io.cn"
sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.2.1"
url_launcher_platform_interface:
dependency: transitive
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,23 +511,23 @@ 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:
dependency: transitive
description:
name: url_launcher_windows
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
url: "https://pub.flutter-io.cn"
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
url: "https://pub.dev"
source: hosted
version: "3.1.2"
version: "3.1.3"
vector_math:
dependency: transitive
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,17 +535,17 @@ packages:
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.flutter-io.cn"
url: "https://pub.dev"
source: hosted
version: "14.2.5"
web:
dependency: transitive
description:
name: web
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
url: "https://pub.flutter-io.cn"
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.1.0"
sdks:
dart: ">=3.4.3 <4.0.0"
flutter: ">=3.22.0"
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"

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.1.11+1
flutter_form_builder: ^9.3.0
flutter_adaptive_scaffold: ^0.3.1
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
@@ -57,7 +57,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^4.0.0
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -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