mirror of
https://github.com/simon-ding/polaris.git
synced 2026-06-06 01:57:40 +08:00
feat: better seeding status
This commit is contained in:
2
db/db.go
2
db/db.go
@@ -491,7 +491,7 @@ func (c *Client) GetHistories() ent.Histories {
|
|||||||
|
|
||||||
func (c *Client) GetRunningHistories() ent.Histories {
|
func (c *Client) GetRunningHistories() ent.Histories {
|
||||||
h, err := c.ent.History.Query().Where(history.Or(history.StatusEQ(history.StatusRunning),
|
h, err := c.ent.History.Query().Where(history.Or(history.StatusEQ(history.StatusRunning),
|
||||||
history.StatusEQ(history.StatusUploading))).All(context.TODO())
|
history.StatusEQ(history.StatusUploading), history.StatusEQ(history.StatusSeeding))).All(context.TODO())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ const (
|
|||||||
StatusSuccess Status = "success"
|
StatusSuccess Status = "success"
|
||||||
StatusFail Status = "fail"
|
StatusFail Status = "fail"
|
||||||
StatusUploading Status = "uploading"
|
StatusUploading Status = "uploading"
|
||||||
|
StatusSeeding Status = "seeding"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s Status) String() string {
|
func (s Status) String() string {
|
||||||
@@ -85,7 +86,7 @@ func (s Status) String() string {
|
|||||||
// StatusValidator is a validator for the "status" field enum values. It is called by the builders before save.
|
// StatusValidator is a validator for the "status" field enum values. It is called by the builders before save.
|
||||||
func StatusValidator(s Status) error {
|
func StatusValidator(s Status) error {
|
||||||
switch s {
|
switch s {
|
||||||
case StatusRunning, StatusSuccess, StatusFail, StatusUploading:
|
case StatusRunning, StatusSuccess, StatusFail, StatusUploading, StatusSeeding:
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("history: invalid enum value for status field: %q", s)
|
return fmt.Errorf("history: invalid enum value for status field: %q", s)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ var (
|
|||||||
{Name: "size", Type: field.TypeInt, Default: 0},
|
{Name: "size", Type: field.TypeInt, Default: 0},
|
||||||
{Name: "download_client_id", Type: field.TypeInt, Nullable: true},
|
{Name: "download_client_id", Type: field.TypeInt, Nullable: true},
|
||||||
{Name: "indexer_id", Type: field.TypeInt, Nullable: true},
|
{Name: "indexer_id", Type: field.TypeInt, Nullable: true},
|
||||||
{Name: "status", Type: field.TypeEnum, Enums: []string{"running", "success", "fail", "uploading"}},
|
{Name: "status", Type: field.TypeEnum, Enums: []string{"running", "success", "fail", "uploading", "seeding"}},
|
||||||
{Name: "saved", Type: field.TypeString, Nullable: true},
|
{Name: "saved", Type: field.TypeString, Nullable: true},
|
||||||
}
|
}
|
||||||
// HistoriesTable holds the schema information for the "histories" table.
|
// HistoriesTable holds the schema information for the "histories" table.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func (History) Fields() []ent.Field {
|
|||||||
field.Int("size").Default(0),
|
field.Int("size").Default(0),
|
||||||
field.Int("download_client_id").Optional(),
|
field.Int("download_client_id").Optional(),
|
||||||
field.Int("indexer_id").Optional(),
|
field.Int("indexer_id").Optional(),
|
||||||
field.Enum("status").Values("running", "success", "fail", "uploading"),
|
field.Enum("status").Values("running", "success", "fail", "uploading", "seeding"),
|
||||||
field.String("saved").Optional(),
|
field.String("saved").Optional(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -101,6 +101,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
|
|||||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||||
@@ -116,6 +118,8 @@ github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4
|
|||||||
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
||||||
github.com/nikoksr/notify v1.0.0 h1:qe9/6FRsWdxBgQgWcpvQ0sv8LRGJZDpRB4TkL2uNdO8=
|
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/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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -139,6 +143,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
|||||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
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 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||||
@@ -201,6 +207,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||||
|
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
|
|||||||
@@ -21,30 +21,35 @@ type Activity struct {
|
|||||||
|
|
||||||
func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
|
func (s *Server) GetAllActivities(c *gin.Context) (interface{}, error) {
|
||||||
q := c.Query("status")
|
q := c.Query("status")
|
||||||
his := s.db.GetHistories()
|
var activities = make([]Activity, 0)
|
||||||
var activities = make([]Activity, 0, len(his))
|
if q == "active" {
|
||||||
for _, h := range his {
|
his := s.db.GetRunningHistories()
|
||||||
if q == "archive" && (h.Status == history.StatusRunning || h.Status == history.StatusUploading) {
|
for _, h := range his {
|
||||||
continue //archived downloads
|
a := Activity{
|
||||||
}
|
History: h,
|
||||||
|
|
||||||
a := Activity{
|
|
||||||
History: h,
|
|
||||||
}
|
|
||||||
existInDownloadClient := false
|
|
||||||
for id, task := range s.core.GetTasks() {
|
|
||||||
if h.ID == id && task.Exists() {
|
|
||||||
a.Progress = task.Progress()
|
|
||||||
a.SeedRatio = float32(*task.SeedRatio())
|
|
||||||
existInDownloadClient = true
|
|
||||||
}
|
}
|
||||||
|
for id, task := range s.core.GetTasks() {
|
||||||
|
if h.ID == id && task.Exists() {
|
||||||
|
a.Progress = task.Progress()
|
||||||
|
a.SeedRatio = float32(*task.SeedRatio())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activities = append(activities, a)
|
||||||
}
|
}
|
||||||
if q == "active" && !existInDownloadClient {
|
} else {
|
||||||
continue
|
his := s.db.GetHistories()
|
||||||
}
|
for _, h := range his {
|
||||||
activities = append(activities, a)
|
if h.Status == history.StatusRunning || h.Status == history.StatusUploading || h.Status == history.StatusSeeding {
|
||||||
}
|
continue //archived downloads
|
||||||
|
}
|
||||||
|
|
||||||
|
a := Activity{
|
||||||
|
History: h,
|
||||||
|
}
|
||||||
|
activities = append(activities, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
return activities, nil
|
return activities, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func (c *Client) Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) reloadTasks() {
|
func (c *Client) reloadTasks() {
|
||||||
allTasks := c.db.GetHistories()
|
allTasks := c.db.GetRunningHistories()
|
||||||
for _, t := range allTasks {
|
for _, t := range allTasks {
|
||||||
torrent, err := transmission.ReloadTorrent(t.Saved)
|
torrent, err := transmission.ReloadTorrent(t.Saved)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,8 +78,7 @@ func (c *Client) MustTMDB() *tmdb.Client {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) RemoveTaskAndTorrent(id int) error {
|
||||||
func (c *Client) RemoveTaskAndTorrent(id int)error {
|
|
||||||
torrent := c.tasks[id]
|
torrent := c.tasks[id]
|
||||||
if torrent != nil {
|
if torrent != nil {
|
||||||
if err := torrent.Remove(); err != nil {
|
if err := torrent.Remove(); err != nil {
|
||||||
@@ -92,4 +91,4 @@ func (c *Client) RemoveTaskAndTorrent(id int)error {
|
|||||||
|
|
||||||
func (c *Client) GetTasks() map[int]*Task {
|
func (c *Client) GetTasks() map[int]*Task {
|
||||||
return c.tasks
|
return c.tasks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,23 +40,18 @@ func (c *Client) checkTasks() {
|
|||||||
if !t.Exists() {
|
if !t.Exists() {
|
||||||
log.Infof("task no longer exists: %v", id)
|
log.Infof("task no longer exists: %v", id)
|
||||||
|
|
||||||
if r.Status == history.StatusRunning || r.Status == history.StatusUploading {
|
|
||||||
log.Warnf("task is running but no longer available in download client, mark as fail, task name: %s", r.SourceTitle)
|
|
||||||
c.db.SetHistoryStatus(id, history.StatusFail)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(c.tasks, id)
|
delete(c.tasks, id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Infof("task (%s) percentage done: %d%%", t.Name(), t.Progress())
|
log.Infof("task (%s) percentage done: %d%%", t.Name(), t.Progress())
|
||||||
if t.Progress() == 100 {
|
if t.Progress() == 100 {
|
||||||
|
|
||||||
if r.Status == history.StatusSuccess {
|
if r.Status == history.StatusSeeding {
|
||||||
//task already success, check seed ratio
|
//task already success, check seed ratio
|
||||||
torrent := c.tasks[id]
|
torrent := c.tasks[id]
|
||||||
ok := c.isSeedRatioLimitReached(r.IndexerID, torrent)
|
ok := c.isSeedRatioLimitReached(r.IndexerID, torrent)
|
||||||
if ok {
|
if ok {
|
||||||
log.Infof("torrent file seed ratio reached, remove: %v, current seed ratio: %v", torrent.Name(), torrent.SeedRatio())
|
log.Infof("torrent file seed ratio reached, remove: %v, current seed ratio: %v", torrent.Name(), *torrent.SeedRatio())
|
||||||
torrent.Remove()
|
torrent.Remove()
|
||||||
delete(c.tasks, id)
|
delete(c.tasks, id)
|
||||||
} else {
|
} else {
|
||||||
@@ -104,7 +99,7 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
|||||||
} else {
|
} else {
|
||||||
c.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusMissing)
|
c.db.SetSeasonAllEpisodeStatus(r.MediaID, seasonNum, episode.StatusMissing)
|
||||||
}
|
}
|
||||||
c.sendMsg(fmt.Sprintf(message.ProcessingFailed, err))
|
c.sendMsg(fmt.Sprintf(message.ProcessingFailed, err1))
|
||||||
if downloadclient.RemoveFailedDownloads {
|
if downloadclient.RemoveFailedDownloads {
|
||||||
log.Debugf("task failed, remove failed torrent and files related")
|
log.Debugf("task failed, remove failed torrent and files related")
|
||||||
delete(c.tasks, r.ID)
|
delete(c.tasks, r.ID)
|
||||||
@@ -134,7 +129,7 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
|||||||
return errors.Wrap(err, "move file")
|
return errors.Wrap(err, "move file")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.db.SetHistoryStatus(r.ID, history.StatusSuccess)
|
c.db.SetHistoryStatus(r.ID, history.StatusSeeding)
|
||||||
if r.EpisodeID != 0 {
|
if r.EpisodeID != 0 {
|
||||||
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusDownloaded)
|
c.db.SetEpisodeStatus(r.EpisodeID, episode.StatusDownloaded)
|
||||||
} else {
|
} else {
|
||||||
@@ -145,7 +140,8 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
|||||||
//判断是否需要删除本地文件
|
//判断是否需要删除本地文件
|
||||||
ok := c.isSeedRatioLimitReached(r.IndexerID, torrent)
|
ok := c.isSeedRatioLimitReached(r.IndexerID, torrent)
|
||||||
if downloadclient.RemoveCompletedDownloads && ok {
|
if downloadclient.RemoveCompletedDownloads && ok {
|
||||||
log.Debugf("download complete,remove torrent and files related, torrent: %v, seed ratio: %v", torrentName, torrent.SeedRatio())
|
log.Debugf("download complete,remove torrent and files related, torrent: %v, seed ratio: %v", torrentName, *torrent.SeedRatio())
|
||||||
|
c.db.SetHistoryStatus(r.ID, history.StatusSuccess)
|
||||||
delete(c.tasks, r.ID)
|
delete(c.tasks, r.ID)
|
||||||
torrent.Remove()
|
torrent.Remove()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,17 +89,16 @@ class _ActivityPageState extends ConsumerState<ActivityPage>
|
|||||||
Icons.close,
|
Icons.close,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
));
|
));
|
||||||
|
} else if (ac.status == "seeding") {
|
||||||
|
//seeding
|
||||||
|
return const Tooltip(
|
||||||
|
message: "做种中",
|
||||||
|
child: Icon(
|
||||||
|
Icons.upload,
|
||||||
|
//color: Colors.blue,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (ac.status == "success") {
|
} else if (ac.status == "success") {
|
||||||
if (ac.progress == 100) {
|
|
||||||
//seeding
|
|
||||||
return const Tooltip(
|
|
||||||
message: "做种中",
|
|
||||||
child: Icon(
|
|
||||||
Icons.upload,
|
|
||||||
//color: Colors.blue,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return const Tooltip(
|
return const Tooltip(
|
||||||
message: "下载成功",
|
message: "下载成功",
|
||||||
child: Icon(
|
child: Icon(
|
||||||
|
|||||||
Reference in New Issue
Block a user