diff --git a/db/db.go b/db/db.go index 70c09cd..c7258ab 100644 --- a/db/db.go +++ b/db/db.go @@ -431,3 +431,8 @@ func (c *Client) UpdateEpisodeFile(seriesID int, seasonNum, episodeNum int, file } return ep.Update().SetFileInStorage(file).Exec(context.TODO()) } + + +func (c *Client) SetEpisodeStatus(id int, status episode.Status) error { + return c.ent.Episode.Update().Where(episode.ID(id)).SetStatus(status).Exec(context.TODO()) +} \ No newline at end of file diff --git a/ent/episode.go b/ent/episode.go index 03b48c4..4173dfb 100644 --- a/ent/episode.go +++ b/ent/episode.go @@ -29,6 +29,8 @@ type Episode struct { Overview string `json:"overview,omitempty"` // AirDate holds the value of the "air_date" field. AirDate string `json:"air_date,omitempty"` + // Status holds the value of the "status" field. + Status episode.Status `json:"status,omitempty"` // FileInStorage holds the value of the "file_in_storage" field. FileInStorage string `json:"file_in_storage,omitempty"` // Edges holds the relations/edges for other nodes in the graph. @@ -64,7 +66,7 @@ func (*Episode) scanValues(columns []string) ([]any, error) { switch columns[i] { case episode.FieldID, episode.FieldSeriesID, episode.FieldSeasonNumber, episode.FieldEpisodeNumber: values[i] = new(sql.NullInt64) - case episode.FieldTitle, episode.FieldOverview, episode.FieldAirDate, episode.FieldFileInStorage: + case episode.FieldTitle, episode.FieldOverview, episode.FieldAirDate, episode.FieldStatus, episode.FieldFileInStorage: values[i] = new(sql.NullString) default: values[i] = new(sql.UnknownType) @@ -123,6 +125,12 @@ func (e *Episode) assignValues(columns []string, values []any) error { } else if value.Valid { e.AirDate = value.String } + case episode.FieldStatus: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field status", values[i]) + } else if value.Valid { + e.Status = episode.Status(value.String) + } case episode.FieldFileInStorage: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field file_in_storage", values[i]) @@ -188,6 +196,9 @@ func (e *Episode) String() string { builder.WriteString("air_date=") builder.WriteString(e.AirDate) builder.WriteString(", ") + builder.WriteString("status=") + builder.WriteString(fmt.Sprintf("%v", e.Status)) + builder.WriteString(", ") builder.WriteString("file_in_storage=") builder.WriteString(e.FileInStorage) builder.WriteByte(')') diff --git a/ent/episode/episode.go b/ent/episode/episode.go index 076ebeb..1e0de93 100644 --- a/ent/episode/episode.go +++ b/ent/episode/episode.go @@ -3,6 +3,8 @@ package episode import ( + "fmt" + "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" ) @@ -24,6 +26,8 @@ const ( FieldOverview = "overview" // FieldAirDate holds the string denoting the air_date field in the database. FieldAirDate = "air_date" + // FieldStatus holds the string denoting the status field in the database. + FieldStatus = "status" // FieldFileInStorage holds the string denoting the file_in_storage field in the database. FieldFileInStorage = "file_in_storage" // EdgeSeries holds the string denoting the series edge name in mutations. @@ -48,6 +52,7 @@ var Columns = []string{ FieldTitle, FieldOverview, FieldAirDate, + FieldStatus, FieldFileInStorage, } @@ -61,6 +66,33 @@ func ValidColumn(column string) bool { return false } +// Status defines the type for the "status" enum field. +type Status string + +// StatusMissing is the default value of the Status enum. +const DefaultStatus = StatusMissing + +// Status values. +const ( + StatusMissing Status = "missing" + StatusDownloading Status = "downloading" + StatusDownloaded Status = "downloaded" +) + +func (s Status) String() string { + return string(s) +} + +// StatusValidator is a validator for the "status" field enum values. It is called by the builders before save. +func StatusValidator(s Status) error { + switch s { + case StatusMissing, StatusDownloading, StatusDownloaded: + return nil + default: + return fmt.Errorf("episode: invalid enum value for status field: %q", s) + } +} + // OrderOption defines the ordering options for the Episode queries. type OrderOption func(*sql.Selector) @@ -99,6 +131,11 @@ func ByAirDate(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldAirDate, opts...).ToFunc() } +// ByStatus orders the results by the status field. +func ByStatus(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStatus, opts...).ToFunc() +} + // ByFileInStorage orders the results by the file_in_storage field. func ByFileInStorage(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldFileInStorage, opts...).ToFunc() diff --git a/ent/episode/where.go b/ent/episode/where.go index 3cc002c..2acf3ef 100644 --- a/ent/episode/where.go +++ b/ent/episode/where.go @@ -394,6 +394,26 @@ func AirDateContainsFold(v string) predicate.Episode { return predicate.Episode(sql.FieldContainsFold(FieldAirDate, v)) } +// StatusEQ applies the EQ predicate on the "status" field. +func StatusEQ(v Status) predicate.Episode { + return predicate.Episode(sql.FieldEQ(FieldStatus, v)) +} + +// StatusNEQ applies the NEQ predicate on the "status" field. +func StatusNEQ(v Status) predicate.Episode { + return predicate.Episode(sql.FieldNEQ(FieldStatus, v)) +} + +// StatusIn applies the In predicate on the "status" field. +func StatusIn(vs ...Status) predicate.Episode { + return predicate.Episode(sql.FieldIn(FieldStatus, vs...)) +} + +// StatusNotIn applies the NotIn predicate on the "status" field. +func StatusNotIn(vs ...Status) predicate.Episode { + return predicate.Episode(sql.FieldNotIn(FieldStatus, vs...)) +} + // FileInStorageEQ applies the EQ predicate on the "file_in_storage" field. func FileInStorageEQ(v string) predicate.Episode { return predicate.Episode(sql.FieldEQ(FieldFileInStorage, v)) diff --git a/ent/episode_create.go b/ent/episode_create.go index 516e9b2..5aafd7a 100644 --- a/ent/episode_create.go +++ b/ent/episode_create.go @@ -64,6 +64,20 @@ func (ec *EpisodeCreate) SetAirDate(s string) *EpisodeCreate { return ec } +// SetStatus sets the "status" field. +func (ec *EpisodeCreate) SetStatus(e episode.Status) *EpisodeCreate { + ec.mutation.SetStatus(e) + return ec +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (ec *EpisodeCreate) SetNillableStatus(e *episode.Status) *EpisodeCreate { + if e != nil { + ec.SetStatus(*e) + } + return ec +} + // SetFileInStorage sets the "file_in_storage" field. func (ec *EpisodeCreate) SetFileInStorage(s string) *EpisodeCreate { ec.mutation.SetFileInStorage(s) @@ -90,6 +104,7 @@ func (ec *EpisodeCreate) Mutation() *EpisodeMutation { // Save creates the Episode in the database. func (ec *EpisodeCreate) Save(ctx context.Context) (*Episode, error) { + ec.defaults() return withHooks(ctx, ec.sqlSave, ec.mutation, ec.hooks) } @@ -115,6 +130,14 @@ func (ec *EpisodeCreate) ExecX(ctx context.Context) { } } +// defaults sets the default values of the builder before save. +func (ec *EpisodeCreate) defaults() { + if _, ok := ec.mutation.Status(); !ok { + v := episode.DefaultStatus + ec.mutation.SetStatus(v) + } +} + // check runs all checks and user-defined validators on the builder. func (ec *EpisodeCreate) check() error { if _, ok := ec.mutation.SeasonNumber(); !ok { @@ -132,6 +155,14 @@ func (ec *EpisodeCreate) check() error { if _, ok := ec.mutation.AirDate(); !ok { return &ValidationError{Name: "air_date", err: errors.New(`ent: missing required field "Episode.air_date"`)} } + if _, ok := ec.mutation.Status(); !ok { + return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "Episode.status"`)} + } + if v, ok := ec.mutation.Status(); ok { + if err := episode.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Episode.status": %w`, err)} + } + } return nil } @@ -178,6 +209,10 @@ func (ec *EpisodeCreate) createSpec() (*Episode, *sqlgraph.CreateSpec) { _spec.SetField(episode.FieldAirDate, field.TypeString, value) _node.AirDate = value } + if value, ok := ec.mutation.Status(); ok { + _spec.SetField(episode.FieldStatus, field.TypeEnum, value) + _node.Status = value + } if value, ok := ec.mutation.FileInStorage(); ok { _spec.SetField(episode.FieldFileInStorage, field.TypeString, value) _node.FileInStorage = value @@ -220,6 +255,7 @@ func (ecb *EpisodeCreateBulk) Save(ctx context.Context) ([]*Episode, error) { for i := range ecb.builders { func(i int, root context.Context) { builder := ecb.builders[i] + builder.defaults() var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { mutation, ok := m.(*EpisodeMutation) if !ok { diff --git a/ent/episode_update.go b/ent/episode_update.go index 2829865..761a025 100644 --- a/ent/episode_update.go +++ b/ent/episode_update.go @@ -132,6 +132,20 @@ func (eu *EpisodeUpdate) SetNillableAirDate(s *string) *EpisodeUpdate { return eu } +// SetStatus sets the "status" field. +func (eu *EpisodeUpdate) SetStatus(e episode.Status) *EpisodeUpdate { + eu.mutation.SetStatus(e) + return eu +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (eu *EpisodeUpdate) SetNillableStatus(e *episode.Status) *EpisodeUpdate { + if e != nil { + eu.SetStatus(*e) + } + return eu +} + // SetFileInStorage sets the "file_in_storage" field. func (eu *EpisodeUpdate) SetFileInStorage(s string) *EpisodeUpdate { eu.mutation.SetFileInStorage(s) @@ -195,7 +209,20 @@ func (eu *EpisodeUpdate) ExecX(ctx context.Context) { } } +// check runs all checks and user-defined validators on the builder. +func (eu *EpisodeUpdate) check() error { + if v, ok := eu.mutation.Status(); ok { + if err := episode.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Episode.status": %w`, err)} + } + } + return nil +} + func (eu *EpisodeUpdate) sqlSave(ctx context.Context) (n int, err error) { + if err := eu.check(); err != nil { + return n, err + } _spec := sqlgraph.NewUpdateSpec(episode.Table, episode.Columns, sqlgraph.NewFieldSpec(episode.FieldID, field.TypeInt)) if ps := eu.mutation.predicates; len(ps) > 0 { _spec.Predicate = func(selector *sql.Selector) { @@ -225,6 +252,9 @@ func (eu *EpisodeUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := eu.mutation.AirDate(); ok { _spec.SetField(episode.FieldAirDate, field.TypeString, value) } + if value, ok := eu.mutation.Status(); ok { + _spec.SetField(episode.FieldStatus, field.TypeEnum, value) + } if value, ok := eu.mutation.FileInStorage(); ok { _spec.SetField(episode.FieldFileInStorage, field.TypeString, value) } @@ -384,6 +414,20 @@ func (euo *EpisodeUpdateOne) SetNillableAirDate(s *string) *EpisodeUpdateOne { return euo } +// SetStatus sets the "status" field. +func (euo *EpisodeUpdateOne) SetStatus(e episode.Status) *EpisodeUpdateOne { + euo.mutation.SetStatus(e) + return euo +} + +// SetNillableStatus sets the "status" field if the given value is not nil. +func (euo *EpisodeUpdateOne) SetNillableStatus(e *episode.Status) *EpisodeUpdateOne { + if e != nil { + euo.SetStatus(*e) + } + return euo +} + // SetFileInStorage sets the "file_in_storage" field. func (euo *EpisodeUpdateOne) SetFileInStorage(s string) *EpisodeUpdateOne { euo.mutation.SetFileInStorage(s) @@ -460,7 +504,20 @@ func (euo *EpisodeUpdateOne) ExecX(ctx context.Context) { } } +// check runs all checks and user-defined validators on the builder. +func (euo *EpisodeUpdateOne) check() error { + if v, ok := euo.mutation.Status(); ok { + if err := episode.StatusValidator(v); err != nil { + return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Episode.status": %w`, err)} + } + } + return nil +} + func (euo *EpisodeUpdateOne) sqlSave(ctx context.Context) (_node *Episode, err error) { + if err := euo.check(); err != nil { + return _node, err + } _spec := sqlgraph.NewUpdateSpec(episode.Table, episode.Columns, sqlgraph.NewFieldSpec(episode.FieldID, field.TypeInt)) id, ok := euo.mutation.ID() if !ok { @@ -507,6 +564,9 @@ func (euo *EpisodeUpdateOne) sqlSave(ctx context.Context) (_node *Episode, err e if value, ok := euo.mutation.AirDate(); ok { _spec.SetField(episode.FieldAirDate, field.TypeString, value) } + if value, ok := euo.mutation.Status(); ok { + _spec.SetField(episode.FieldStatus, field.TypeEnum, value) + } if value, ok := euo.mutation.FileInStorage(); ok { _spec.SetField(episode.FieldFileInStorage, field.TypeString, value) } diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index 1b61545..592e797 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -37,6 +37,7 @@ var ( {Name: "title", Type: field.TypeString}, {Name: "overview", Type: field.TypeString}, {Name: "air_date", Type: field.TypeString}, + {Name: "status", Type: field.TypeEnum, Enums: []string{"missing", "downloading", "downloaded"}, Default: "missing"}, {Name: "file_in_storage", Type: field.TypeString, Nullable: true}, {Name: "series_id", Type: field.TypeInt, Nullable: true}, } @@ -48,7 +49,7 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "episodes_series_episodes", - Columns: []*schema.Column{EpisodesColumns[7]}, + Columns: []*schema.Column{EpisodesColumns[8]}, RefColumns: []*schema.Column{SeriesColumns[0]}, OnDelete: schema.SetNull, }, diff --git a/ent/mutation.go b/ent/mutation.go index 794d69e..4e55164 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -918,6 +918,7 @@ type EpisodeMutation struct { title *string overview *string air_date *string + status *episode.Status file_in_storage *string clearedFields map[string]struct{} series *int @@ -1294,6 +1295,42 @@ func (m *EpisodeMutation) ResetAirDate() { m.air_date = nil } +// SetStatus sets the "status" field. +func (m *EpisodeMutation) SetStatus(e episode.Status) { + m.status = &e +} + +// Status returns the value of the "status" field in the mutation. +func (m *EpisodeMutation) Status() (r episode.Status, exists bool) { + v := m.status + if v == nil { + return + } + return *v, true +} + +// OldStatus returns the old "status" field's value of the Episode entity. +// If the Episode object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EpisodeMutation) OldStatus(ctx context.Context) (v episode.Status, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldStatus is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldStatus requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldStatus: %w", err) + } + return oldValue.Status, nil +} + +// ResetStatus resets all changes to the "status" field. +func (m *EpisodeMutation) ResetStatus() { + m.status = nil +} + // SetFileInStorage sets the "file_in_storage" field. func (m *EpisodeMutation) SetFileInStorage(s string) { m.file_in_storage = &s @@ -1404,7 +1441,7 @@ func (m *EpisodeMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *EpisodeMutation) Fields() []string { - fields := make([]string, 0, 7) + fields := make([]string, 0, 8) if m.series != nil { fields = append(fields, episode.FieldSeriesID) } @@ -1423,6 +1460,9 @@ func (m *EpisodeMutation) Fields() []string { if m.air_date != nil { fields = append(fields, episode.FieldAirDate) } + if m.status != nil { + fields = append(fields, episode.FieldStatus) + } if m.file_in_storage != nil { fields = append(fields, episode.FieldFileInStorage) } @@ -1446,6 +1486,8 @@ func (m *EpisodeMutation) Field(name string) (ent.Value, bool) { return m.Overview() case episode.FieldAirDate: return m.AirDate() + case episode.FieldStatus: + return m.Status() case episode.FieldFileInStorage: return m.FileInStorage() } @@ -1469,6 +1511,8 @@ func (m *EpisodeMutation) OldField(ctx context.Context, name string) (ent.Value, return m.OldOverview(ctx) case episode.FieldAirDate: return m.OldAirDate(ctx) + case episode.FieldStatus: + return m.OldStatus(ctx) case episode.FieldFileInStorage: return m.OldFileInStorage(ctx) } @@ -1522,6 +1566,13 @@ func (m *EpisodeMutation) SetField(name string, value ent.Value) error { } m.SetAirDate(v) return nil + case episode.FieldStatus: + v, ok := value.(episode.Status) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetStatus(v) + return nil case episode.FieldFileInStorage: v, ok := value.(string) if !ok { @@ -1638,6 +1689,9 @@ func (m *EpisodeMutation) ResetField(name string) error { case episode.FieldAirDate: m.ResetAirDate() return nil + case episode.FieldStatus: + m.ResetStatus() + return nil case episode.FieldFileInStorage: m.ResetFileInStorage() return nil diff --git a/ent/runtime.go b/ent/runtime.go index d6f0af2..6998b5f 100644 --- a/ent/runtime.go +++ b/ent/runtime.go @@ -45,6 +45,8 @@ func init() { downloadclientsDescTags := downloadclientsFields[10].Descriptor() // downloadclients.DefaultTags holds the default value on creation for the tags field. downloadclients.DefaultTags = downloadclientsDescTags.Default.(string) + episodeFields := schema.Episode{}.Fields() + _ = episodeFields indexersFields := schema.Indexers{}.Fields() _ = indexersFields // indexersDescEnableRss is the schema descriptor for enable_rss field. diff --git a/ent/schema/episode.go b/ent/schema/episode.go index 144d1e0..9ada290 100644 --- a/ent/schema/episode.go +++ b/ent/schema/episode.go @@ -20,6 +20,7 @@ func (Episode) Fields() []ent.Field { field.String("title"), field.String("overview"), field.String("air_date"), + field.Enum("status").Values("missing", "downloading", "downloaded").Default("missing"), field.String("file_in_storage").Optional(), } } diff --git a/server/resources.go b/server/resources.go index 083d04a..5bc7048 100644 --- a/server/resources.go +++ b/server/resources.go @@ -4,6 +4,7 @@ import ( "fmt" "polaris/db" "polaris/ent" + "polaris/ent/episode" "polaris/ent/history" "polaris/log" "polaris/pkg/torznab" @@ -123,6 +124,7 @@ func (s *Server) searchAndDownload(seriesId, seasonNum, episodeNum int) (*string Status: history.StatusRunning, Saved: torrent.Save(), }) + s.db.SetEpisodeStatus(ep.ID, episode.StatusDownloading) if err != nil { return nil, errors.Wrap(err, "save record") } diff --git a/server/scheduler.go b/server/scheduler.go index be35edd..7ee0ebb 100644 --- a/server/scheduler.go +++ b/server/scheduler.go @@ -3,6 +3,7 @@ package server import ( "path/filepath" "polaris/ent" + "polaris/ent/episode" "polaris/ent/history" storage1 "polaris/ent/storage" "polaris/log" @@ -17,7 +18,7 @@ import ( func (s *Server) scheduler() { s.mustAddCron("@every 1m", s.checkTasks) - s.mustAddCron("@every 10m", s.checkAllFiles) + //s.mustAddCron("@every 1h", s.checkAllFiles) s.cron.Start() } @@ -59,10 +60,13 @@ func (s *Server) moveCompletedTask(id int) (err error) { defer func () { if err != nil { s.db.SetHistoryStatus(r.ID, history.StatusFail) + s.db.SetEpisodeStatus(r.EpisodeID, episode.StatusMissing) } else { - torrent.Remove() delete(s.tasks, r.ID) s.db.SetHistoryStatus(r.ID, history.StatusSuccess) + s.db.SetEpisodeStatus(r.EpisodeID, episode.StatusDownloaded) + + torrent.Remove() } }() diff --git a/ui/lib/activity.dart b/ui/lib/activity.dart index 2912a1c..3977805 100644 --- a/ui/lib/activity.dart +++ b/ui/lib/activity.dart @@ -32,9 +32,10 @@ class ActivityPage extends ConsumerWidget { DataCell(Text("${activity.date!.toLocal()}")), DataCell(() { if (activity.status == "uploading") { - return const MyProgressIndicator( - size: 20, - ); + return const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator()); } else if (activity.status == "fail") { return const Icon( Icons.close, diff --git a/ui/lib/providers/series_details.dart b/ui/lib/providers/series_details.dart index d4d70d8..1e6e587 100644 --- a/ui/lib/providers/series_details.dart +++ b/ui/lib/providers/series_details.dart @@ -102,6 +102,7 @@ class Episodes { String? airDate; int? seasonNumber; String? overview; + String? status; Episodes( {this.id, @@ -110,6 +111,7 @@ class Episodes { this.title, this.airDate, this.seasonNumber, + this.status, this.overview}); Episodes.fromJson(Map json) { @@ -119,6 +121,7 @@ class Episodes { title = json['title']; airDate = json['air_date']; seasonNumber = json['season_number']; + status = json['status']; overview = json['overview']; } } diff --git a/ui/lib/tv_details.dart b/ui/lib/tv_details.dart index c58816b..66c9771 100644 --- a/ui/lib/tv_details.dart +++ b/ui/lib/tv_details.dart @@ -46,57 +46,59 @@ class _TvDetailsPageState extends ConsumerState { builder: (context, snapshot) { return seriesDetails.when( data: (details) { - Map> m = Map(); + Map> m = Map(); for (final ep in details.episodes!) { - var w = Container( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: 70, - child: Text("第 ${ep.episodeNumber} 集"), - ), - SizedBox( - width: 100, - child: Opacity( - opacity: 0.5, - child: Text("${ep.airDate}"), - ), - ), - Text("${ep.title}", textAlign: TextAlign.left), - const Expanded(child: Text("")), - IconButton( - onPressed: () async { - var f = ref - .read( - seriesDetailsProvider(seriesId).notifier) - .searchAndDownload(seriesId, ep.seasonNumber!, - ep.episodeNumber!); - setState(() { - _pendingFuture = f; - }); - if (!Utils.showError(context, snapshot)) { - var name = await f; - Utils.showSnakeBar(context, "开始下载: $name"); - } - }, - icon: const Icon(Icons.search)) - ], - ), - ); + var row = DataRow(cells: [ + DataCell(Text("${ep.episodeNumber}")), + DataCell(Text("${ep.title}")), + DataCell(Opacity( + opacity: 0.5, + child: Text("${ep.airDate}"), + )), + DataCell(ep.status == "dwnloading" + ? const Icon(Icons.cloud_download) + : (ep.status == "dwnloaded" + ? const Icon(Icons.cloud_done) + : const Icon(Icons.cloud_off))), + DataCell(IconButton( + onPressed: () async { + var f = ref + .read(seriesDetailsProvider(seriesId).notifier) + .searchAndDownload(seriesId, ep.seasonNumber!, + ep.episodeNumber!); + setState(() { + _pendingFuture = f; + }); + if (!Utils.showError(context, snapshot)) { + var name = await f; + Utils.showSnakeBar(context, "开始下载: $name"); + } + }, + icon: const Icon(Icons.search))) + ]); + if (m[ep.seasonNumber] == null) { m[ep.seasonNumber!] = List.empty(growable: true); } - m[ep.seasonNumber!]!.add(w); + m[ep.seasonNumber!]!.add(row); } List list = List.empty(growable: true); for (final k in m.keys.toList().reversed) { var seasonList = ExpansionTile( tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0), - initiallyExpanded: k == 0 ? false : true, + //childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0), + initiallyExpanded: false, title: k == 0 ? const Text("特集") : Text("第 $k 季"), - children: m[k]!, + expandedCrossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DataTable(columns: const [ + DataColumn(label: Text("#")), + DataColumn(label: SizedBox(width: 500, child: Text("标题"),)), + DataColumn(label: Text("播出时间")), + DataColumn(label: Text("状态")), + DataColumn(label: Text("操作")) + ], rows: m[k]!), + ], ); list.add(seasonList); } @@ -145,7 +147,6 @@ class _TvDetailsPageState extends ConsumerState { Text("$error"), loading: () => const MyProgressIndicator()), - ], ), const Divider(thickness: 1, height: 1),