Compare commits

...

34 Commits

Author SHA1 Message Date
Simon Ding
0954632b65 AI intergration WIP 2025-01-07 10:27:18 +08:00
Simon Ding
03105a1989 chore: update deps 2024-12-22 13:45:11 +08:00
Simon Ding
e169172c68 doc: update 2024-12-13 14:34:34 +08:00
Simon Ding
937b035634 fix 2024-12-13 13:48:58 +08:00
Simon Ding
c639e11b90 fix: debian do not support arm v6 2024-12-13 13:44:32 +08:00
Simon Ding
f2ac688ed8 feat: release build flutter use github action 2024-12-13 13:33:39 +08:00
Simon Ding
369263a55c fix 2024-12-13 13:25:51 +08:00
Simon Ding
9d4848129f feat: build flutter use github action 2024-12-13 13:22:12 +08:00
Simon Ding
f7e82fa464 feat: search with alternative titles 2024-12-13 12:19:02 +08:00
Simon Ding
d2354ab33c feat: ditch html render and update flutter packages 2024-12-13 11:44:42 +08:00
Simon Ding
67014cfb16 feat: save media alternative titles 2024-12-13 11:35:32 +08:00
Simon
60edeacd0d Merge pull request #10 from simon-ding/dependabot/go_modules/go_modules-5a9c29dde4
chore(deps): bump golang.org/x/crypto from 0.27.0 to 0.31.0 in the go_modules group across 1 directory
2024-12-12 12:33:06 +08:00
dependabot[bot]
4c77cf5798 chore(deps): bump golang.org/x/crypto
Bumps the go_modules group with 1 update in the / directory: [golang.org/x/crypto](https://github.com/golang/crypto).


Updates `golang.org/x/crypto` from 0.27.0 to 0.31.0
- [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-12 00:29:52 +00:00
Simon Ding
3cf48d1f8e fix: remove files 2024-12-11 21:25:28 +08:00
Simon Ding
6d127c6d00 feat: add option to delete storage media files 2024-12-11 21:09:00 +08:00
Simon Ding
22f76e3f57 fix: monitor new episode 2024-12-11 20:27:51 +08:00
Simon Ding
e947396c04 fix: cache 2024-12-11 20:11:33 +08:00
Simon Ding
1020190c01 feat: cache download status number 2024-12-11 20:04:37 +08:00
Simon Ding
7c05acd1cf feat: in favor of gridview.builder for better performance 2024-12-11 14:44:41 +08:00
Simon Ding
76a9183b52 refactor: reduce default poster image size to w500 2024-12-11 12:24:16 +08:00
Simon Ding
6698d368c3 chore: updates 2024-12-06 11:06:50 +08:00
Simon Ding
acb627d011 feat: add arm v7 2024-11-26 18:50:40 +08:00
Simon Ding
7c64d964e8 ui: cancel timer before calling 2024-11-21 10:01:52 +08:00
Simon Ding
bec3b04705 ui: add macos client 2024-11-21 09:45:44 +08:00
Simon Ding
990da92b75 chore: change env name 2024-11-20 19:21:25 +08:00
Simon Ding
ee14cc63b8 chore: updates 2024-11-20 19:20:14 +08:00
Simon Ding
8df7b8665b chore: add sub ext 2024-11-20 16:25:35 +08:00
Simon Ding
ea90e014b1 feat: remove default internal size limiter 2024-11-20 15:34:57 +08:00
Simon Ding
6372c5c6e6 chore: update error msg 2024-11-20 15:06:26 +08:00
Simon Ding
7b6dba1afe feat: only accept video files and subtitles of known formats 2024-11-20 12:03:56 +08:00
Simon Ding
c833f6fab6 feat: complete size limiter feature 2024-11-19 23:54:27 +08:00
Simon Ding
b4c2002ad1 feat: apply global size limiter 2024-11-19 19:51:06 +08:00
Simon Ding
b2a9f1f83b refactor: size limiter 2024-11-19 19:24:43 +08:00
Simon Ding
b69881d26b WIP: size limiter 2024-11-19 18:22:40 +08:00
79 changed files with 3530 additions and 393 deletions

View File

@@ -29,6 +29,19 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3
- name: Build Web
run: |
cd ui
flutter pub get
flutter build web --no-web-resources-cdn
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5

View File

@@ -37,12 +37,25 @@ jobs:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: 3
- name: Build Web
run: |
cd ui
flutter pub get
flutter build web --no-web-resources-cdn
- name: Build and push
id: push
uses: docker/build-push-action@v6
@@ -50,9 +63,7 @@ jobs:
context: .
file: Dockerfile
push: true
platforms: |
linux/amd64
linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/s390x,linux/ppc64le
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,11 +1,3 @@
FROM instrumentisto/flutter:3 AS flutter
WORKDIR /app
COPY ./ui/pubspec.yaml ./ui/pubspec.lock ./
RUN flutter pub get
COPY ./ui/ ./
RUN flutter build web --no-web-resources-cdn --web-renderer html
# 打包依赖阶段使用golang作为基础镜像
FROM golang:1.23 as builder
# 启用go module
@@ -20,7 +12,6 @@ RUN go mod download
COPY . .
COPY --from=flutter /app/build/web ./ui/build/web/
# 指定OS等并go build
RUN CGO_ENABLED=0 go build -o polaris -ldflags="-X polaris/db.Version=$(git describe --tags --long)" ./cmd/

View File

@@ -12,7 +12,7 @@
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/simon-ding/polaris)
**Polaris 是一个电视剧和电影的追踪下载软件。对动漫日剧美剧都有良好的匹配,支持webdav或者本地存储。**
**Polaris 是一个电视剧和电影的追踪下载软件。对美剧动漫日剧都有良好的匹配,支持多种存储方式webdav、alist、本地存储**
</div>
@@ -38,6 +38,15 @@
- [x] 支持导入plex watchlistplex里标记自动导入polaris
- [x] and more...
## 支持的平台
- linux/amd64
- linux/arm64
- linux/arm/v7
- linux/386
- linux/s390x
- linux/ppc64le
## Todos

View File

@@ -3,9 +3,7 @@ package main
import (
"polaris/db"
"polaris/log"
"polaris/pkg/utils"
"polaris/server"
"time"
)
func main() {
@@ -18,13 +16,13 @@ func main() {
log.Panicf("init db error: %v", err)
}
go func() {
time.Sleep(2 * time.Second)
if err := utils.OpenURL("http://127.0.0.1:8080"); err != nil {
log.Errorf("open url error: %v", err)
}
// go func() {
// time.Sleep(2 * time.Second)
// if err := utils.OpenURL("http://127.0.0.1:8080"); err != nil {
// log.Errorf("open url error: %v", err)
// }
}()
// }()
s := server.NewServer(dbClient)
if err := s.Serve(); err != nil {
log.Errorf("server start error: %v", err)

View File

@@ -1,5 +1,7 @@
package db
import "polaris/ent/media"
var Version = "undefined"
const (
@@ -14,10 +16,16 @@ const (
SettingNfoSupportEnabled = "nfo_support_enabled"
SettingAllowQiangban = "filter_qiangban"
SettingEnableTmdbAdultContent = "tmdb_adult_content"
SetttingSizeLimiter = "size_limiter"
SettingTvNamingFormat = "tv_naming_format"
SettingMovieNamingFormat = "movie_naming_format"
SettingProwlarrInfo = "prowlarr_info"
SettingTvSizeLimiter = "tv_size_limiter"
SettingMovieSizeLimiter = "movie_size_limiter"
SettingAcceptedVideoFormats = "accepted_video_formats"
SettingAcceptedSubtitleFormats = "accepted_subtitle_formats"
SettingAIConfig = "ai_config"
)
const (
@@ -40,6 +48,18 @@ const (
const DefaultNamingFormat = "{{.NameCN}} {{.NameEN}} {{if .Year}} ({{.Year}}) {{end}}"
// https://en.wikipedia.org/wiki/Video_file_format
var defaultAcceptedVideoFormats = []string{
".webm", ".mkv", ".flv", ".vob", ".ogv", ".ogg", ".drc", ".mng", ".avi", ".mts", ".m2ts", ".ts",
".mov", ".qt", ".wmv", ".yuv", ".rm", ".rmvb", ".viv", ".amv", ".mp4", ".m4p", ".m4v",
".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".m2v", ".m4v",
".svi", ".3gp", ".3g2", ".nsv",
}
var defaultAcceptedSubtitleFormats = []string{
".ass", ".srt", ".vtt", ".webvtt", ".sub", ".idx",
}
type NamingInfo struct {
NameCN string
NameEN string
@@ -51,15 +71,27 @@ type ResolutionType string
const JwtSerectKey = "jwt_secrect_key"
type SizeLimiter struct {
R720p Limiter `json:"720p"`
R1080p Limiter `json:"1080p"`
R2160p Limiter `json:"2160p"`
type MediaSizeLimiter struct {
P720p SizeLimiter `json:"720p"`
P1080 SizeLimiter `json:"1080p"`
P2160 SizeLimiter `json:"2160p"`
}
type Limiter struct {
Max int `json:"max"`
Min int `json:"min"`
func (m *MediaSizeLimiter) GetLimiter(r media.Resolution) SizeLimiter {
if r == media.Resolution1080p {
return m.P1080
} else if r == media.Resolution720p {
return m.P720p
} else if r == media.Resolution2160p {
return m.P2160
}
return SizeLimiter{}
}
type SizeLimiter struct {
MaxSIze int64 `json:"max_size"`
MinSize int64 `json:"min_size"`
PreferSIze int64 `json:"prefer_size"`
}
type ProwlarrSetting struct {
@@ -67,3 +99,9 @@ type ProwlarrSetting struct {
ApiKey string `json:"api_key"`
URL string `json:"url"`
}
type AIConfig struct {
Enabled bool `json:"enabled"`
GeminiApiKey string `json:"gemini_api_key"`
GeminiModelName string `json:"gemini_model_name"`
}

104
db/db.go
View File

@@ -157,6 +157,7 @@ func (c *Client) AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, er
SetDownloadHistoryEpisodes(m.DownloadHistoryEpisodes).
SetLimiter(m.Limiter).
SetExtras(m.Extras).
SetAlternativeTitles(m.AlternativeTitles).
AddEpisodeIDs(episodes...).
Save(context.TODO())
return r, err
@@ -251,6 +252,7 @@ func (c *Client) SaveEposideDetail2(d *ent.Episode) (int, error) {
SetMediaID(d.MediaID).
SetStatus(d.Status).
SetOverview(d.Overview).
SetMonitored(d.Monitored).
SetTitle(d.Title).Save(context.TODO())
return ep.ID, err
@@ -441,7 +443,7 @@ type Storage struct {
}
func (s *Storage) ToWebDavSetting() WebdavSetting {
if s.Implementation != storage.ImplementationWebdav && s.Implementation != storage.ImplementationAlist{
if s.Implementation != storage.ImplementationWebdav && s.Implementation != storage.ImplementationAlist {
panic("not webdav storage")
}
var webdavSetting WebdavSetting
@@ -631,19 +633,38 @@ func (c *Client) DeleteImportlist(id int) error {
return c.ent.ImportList.DeleteOneID(id).Exec(context.TODO())
}
func (c *Client) GetSizeLimiter() (*SizeLimiter, error) {
v := c.GetSetting(SetttingSizeLimiter)
var limiter SizeLimiter
func (c *Client) GetSizeLimiter(mediaType string) (*MediaSizeLimiter, error) {
var v string
if mediaType == "tv" {
v = c.GetSetting(SettingTvSizeLimiter)
} else if mediaType == "movie" {
v = c.GetSetting(SettingMovieSizeLimiter)
} else {
return nil, errors.Errorf("media type not supported: %v", mediaType)
}
var limiter MediaSizeLimiter
if v == "" {
return &limiter, nil
}
err := json.Unmarshal([]byte(v), &limiter)
return &limiter, err
}
func (c *Client) SetSizeLimiter(limiter *SizeLimiter) error {
func (c *Client) SetSizeLimiter(mediaType string, limiter *MediaSizeLimiter) error {
data, err := json.Marshal(limiter)
if err != nil {
return err
}
return c.SetSetting(SetttingSizeLimiter, string(data))
if mediaType == "tv" {
return c.SetSetting(SettingTvSizeLimiter, string(data))
} else if mediaType == "movie" {
return c.SetSetting(SettingMovieSizeLimiter, string(data))
} else {
return errors.Errorf("media type not supported: %v", mediaType)
}
}
func (c *Client) GetTvNamingFormat() string {
@@ -690,3 +711,74 @@ func (c *Client) SaveProwlarrSetting(se *ProwlarrSetting) error {
}
return c.SetSetting(SettingProwlarrInfo, string(data))
}
func (c *Client) getAcceptedFormats(key string) ([]string, error) {
v := c.GetSetting(key)
if v == "" {
return nil, nil
}
var res []string
err := json.Unmarshal([]byte(v), &res)
return res, err
}
func (c *Client) setAcceptedFormats(key string, v []string) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
return c.SetSetting(key, string(data))
}
func (c *Client) GetAcceptedVideoFormats() ([]string, error) {
res, err := c.getAcceptedFormats(SettingAcceptedVideoFormats)
if err != nil {
return nil, err
}
if res == nil {
return defaultAcceptedVideoFormats, nil
}
return res, nil
}
func (c *Client) SetAcceptedVideoFormats(key string, v []string) error {
return c.setAcceptedFormats(SettingAcceptedVideoFormats, v)
}
func (c *Client) GetAcceptedSubtitleFormats() ([]string, error) {
res, err := c.getAcceptedFormats(SettingAcceptedSubtitleFormats)
if err != nil {
return nil, err
}
if res== nil {
return defaultAcceptedSubtitleFormats, nil
}
return res, nil
}
func (c *Client) SetAcceptedSubtitleFormats(key string, v []string) error {
return c.setAcceptedFormats(SettingAcceptedSubtitleFormats, v)
}
func (c *Client) GetAIConfig() (AIConfig, error) {
cfg := c.GetSetting(SettingAIConfig)
var ai AIConfig
if cfg == "" {
return ai, nil
}
err := json.Unmarshal([]byte(cfg), &ai)
if err != nil {
return AIConfig{}, err
}
return ai, nil
}
func (c *Client) SetAIConfig(cfg *AIConfig) error {
if data, err := json.Marshal(cfg); err != nil {
return err
} else {
return c.SetSetting(SettingAIConfig, string(data))
}
}

View File

@@ -49,6 +49,8 @@ type Media struct {
Limiter schema.MediaLimiter `json:"limiter,omitempty"`
// Extras holds the value of the "extras" field.
Extras schema.MediaExtras `json:"extras,omitempty"`
// AlternativeTitles holds the value of the "alternative_titles" field.
AlternativeTitles []schema.AlternativeTilte `json:"alternative_titles,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the MediaQuery when eager-loading is set.
Edges MediaEdges `json:"edges"`
@@ -78,7 +80,7 @@ func (*Media) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case media.FieldLimiter, media.FieldExtras:
case media.FieldLimiter, media.FieldExtras, media.FieldAlternativeTitles:
values[i] = new([]byte)
case media.FieldDownloadHistoryEpisodes:
values[i] = new(sql.NullBool)
@@ -203,6 +205,14 @@ func (m *Media) assignValues(columns []string, values []any) error {
return fmt.Errorf("unmarshal field extras: %w", err)
}
}
case media.FieldAlternativeTitles:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field alternative_titles", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &m.AlternativeTitles); err != nil {
return fmt.Errorf("unmarshal field alternative_titles: %w", err)
}
}
default:
m.selectValues.Set(columns[i], values[i])
}
@@ -288,6 +298,9 @@ func (m *Media) String() string {
builder.WriteString(", ")
builder.WriteString("extras=")
builder.WriteString(fmt.Sprintf("%v", m.Extras))
builder.WriteString(", ")
builder.WriteString("alternative_titles=")
builder.WriteString(fmt.Sprintf("%v", m.AlternativeTitles))
builder.WriteByte(')')
return builder.String()
}

View File

@@ -45,6 +45,8 @@ const (
FieldLimiter = "limiter"
// FieldExtras holds the string denoting the extras field in the database.
FieldExtras = "extras"
// FieldAlternativeTitles holds the string denoting the alternative_titles field in the database.
FieldAlternativeTitles = "alternative_titles"
// EdgeEpisodes holds the string denoting the episodes edge name in mutations.
EdgeEpisodes = "episodes"
// Table holds the table name of the media in the database.
@@ -76,6 +78,7 @@ var Columns = []string{
FieldDownloadHistoryEpisodes,
FieldLimiter,
FieldExtras,
FieldAlternativeTitles,
}
// ValidColumn reports if the column name is valid (part of the table columns).

View File

@@ -795,6 +795,16 @@ func ExtrasNotNil() predicate.Media {
return predicate.Media(sql.FieldNotNull(FieldExtras))
}
// AlternativeTitlesIsNil applies the IsNil predicate on the "alternative_titles" field.
func AlternativeTitlesIsNil() predicate.Media {
return predicate.Media(sql.FieldIsNull(FieldAlternativeTitles))
}
// AlternativeTitlesNotNil applies the NotNil predicate on the "alternative_titles" field.
func AlternativeTitlesNotNil() predicate.Media {
return predicate.Media(sql.FieldNotNull(FieldAlternativeTitles))
}
// HasEpisodes applies the HasEdge predicate on the "episodes" edge.
func HasEpisodes() predicate.Media {
return predicate.Media(func(s *sql.Selector) {

View File

@@ -184,6 +184,12 @@ func (mc *MediaCreate) SetNillableExtras(se *schema.MediaExtras) *MediaCreate {
return mc
}
// SetAlternativeTitles sets the "alternative_titles" field.
func (mc *MediaCreate) SetAlternativeTitles(st []schema.AlternativeTilte) *MediaCreate {
mc.mutation.SetAlternativeTitles(st)
return mc
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
func (mc *MediaCreate) AddEpisodeIDs(ids ...int) *MediaCreate {
mc.mutation.AddEpisodeIDs(ids...)
@@ -377,6 +383,10 @@ func (mc *MediaCreate) createSpec() (*Media, *sqlgraph.CreateSpec) {
_spec.SetField(media.FieldExtras, field.TypeJSON, value)
_node.Extras = value
}
if value, ok := mc.mutation.AlternativeTitles(); ok {
_spec.SetField(media.FieldAlternativeTitles, field.TypeJSON, value)
_node.AlternativeTitles = value
}
if nodes := mc.mutation.EpisodesIDs(); len(nodes) > 0 {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,

View File

@@ -14,6 +14,7 @@ import (
"entgo.io/ent/dialect/sql"
"entgo.io/ent/dialect/sql/sqlgraph"
"entgo.io/ent/dialect/sql/sqljson"
"entgo.io/ent/schema/field"
)
@@ -290,6 +291,24 @@ func (mu *MediaUpdate) ClearExtras() *MediaUpdate {
return mu
}
// SetAlternativeTitles sets the "alternative_titles" field.
func (mu *MediaUpdate) SetAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdate {
mu.mutation.SetAlternativeTitles(st)
return mu
}
// AppendAlternativeTitles appends st to the "alternative_titles" field.
func (mu *MediaUpdate) AppendAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdate {
mu.mutation.AppendAlternativeTitles(st)
return mu
}
// ClearAlternativeTitles clears the value of the "alternative_titles" field.
func (mu *MediaUpdate) ClearAlternativeTitles() *MediaUpdate {
mu.mutation.ClearAlternativeTitles()
return mu
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
func (mu *MediaUpdate) AddEpisodeIDs(ids ...int) *MediaUpdate {
mu.mutation.AddEpisodeIDs(ids...)
@@ -454,6 +473,17 @@ func (mu *MediaUpdate) sqlSave(ctx context.Context) (n int, err error) {
if mu.mutation.ExtrasCleared() {
_spec.ClearField(media.FieldExtras, field.TypeJSON)
}
if value, ok := mu.mutation.AlternativeTitles(); ok {
_spec.SetField(media.FieldAlternativeTitles, field.TypeJSON, value)
}
if value, ok := mu.mutation.AppendedAlternativeTitles(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, media.FieldAlternativeTitles, value)
})
}
if mu.mutation.AlternativeTitlesCleared() {
_spec.ClearField(media.FieldAlternativeTitles, field.TypeJSON)
}
if mu.mutation.EpisodesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
@@ -779,6 +809,24 @@ func (muo *MediaUpdateOne) ClearExtras() *MediaUpdateOne {
return muo
}
// SetAlternativeTitles sets the "alternative_titles" field.
func (muo *MediaUpdateOne) SetAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdateOne {
muo.mutation.SetAlternativeTitles(st)
return muo
}
// AppendAlternativeTitles appends st to the "alternative_titles" field.
func (muo *MediaUpdateOne) AppendAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdateOne {
muo.mutation.AppendAlternativeTitles(st)
return muo
}
// ClearAlternativeTitles clears the value of the "alternative_titles" field.
func (muo *MediaUpdateOne) ClearAlternativeTitles() *MediaUpdateOne {
muo.mutation.ClearAlternativeTitles()
return muo
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
func (muo *MediaUpdateOne) AddEpisodeIDs(ids ...int) *MediaUpdateOne {
muo.mutation.AddEpisodeIDs(ids...)
@@ -973,6 +1021,17 @@ func (muo *MediaUpdateOne) sqlSave(ctx context.Context) (_node *Media, err error
if muo.mutation.ExtrasCleared() {
_spec.ClearField(media.FieldExtras, field.TypeJSON)
}
if value, ok := muo.mutation.AlternativeTitles(); ok {
_spec.SetField(media.FieldAlternativeTitles, field.TypeJSON, value)
}
if value, ok := muo.mutation.AppendedAlternativeTitles(); ok {
_spec.AddModifier(func(u *sql.UpdateBuilder) {
sqljson.Append(u, media.FieldAlternativeTitles, value)
})
}
if muo.mutation.AlternativeTitlesCleared() {
_spec.ClearField(media.FieldAlternativeTitles, field.TypeJSON)
}
if muo.mutation.EpisodesCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,

View File

@@ -143,6 +143,7 @@ var (
{Name: "download_history_episodes", Type: field.TypeBool, Nullable: true, Default: false},
{Name: "limiter", Type: field.TypeJSON, Nullable: true},
{Name: "extras", Type: field.TypeJSON, Nullable: true},
{Name: "alternative_titles", Type: field.TypeJSON, Nullable: true},
}
// MediaTable holds the schema information for the "media" table.
MediaTable = &schema.Table{

View File

@@ -5115,6 +5115,8 @@ type MediaMutation struct {
download_history_episodes *bool
limiter *schema.MediaLimiter
extras *schema.MediaExtras
alternative_titles *[]schema.AlternativeTilte
appendalternative_titles []schema.AlternativeTilte
clearedFields map[string]struct{}
episodes map[int]struct{}
removedepisodes map[int]struct{}
@@ -5881,6 +5883,71 @@ func (m *MediaMutation) ResetExtras() {
delete(m.clearedFields, media.FieldExtras)
}
// SetAlternativeTitles sets the "alternative_titles" field.
func (m *MediaMutation) SetAlternativeTitles(st []schema.AlternativeTilte) {
m.alternative_titles = &st
m.appendalternative_titles = nil
}
// AlternativeTitles returns the value of the "alternative_titles" field in the mutation.
func (m *MediaMutation) AlternativeTitles() (r []schema.AlternativeTilte, exists bool) {
v := m.alternative_titles
if v == nil {
return
}
return *v, true
}
// OldAlternativeTitles returns the old "alternative_titles" field's value of the Media entity.
// If the Media 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 *MediaMutation) OldAlternativeTitles(ctx context.Context) (v []schema.AlternativeTilte, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldAlternativeTitles is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldAlternativeTitles requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldAlternativeTitles: %w", err)
}
return oldValue.AlternativeTitles, nil
}
// AppendAlternativeTitles adds st to the "alternative_titles" field.
func (m *MediaMutation) AppendAlternativeTitles(st []schema.AlternativeTilte) {
m.appendalternative_titles = append(m.appendalternative_titles, st...)
}
// AppendedAlternativeTitles returns the list of values that were appended to the "alternative_titles" field in this mutation.
func (m *MediaMutation) AppendedAlternativeTitles() ([]schema.AlternativeTilte, bool) {
if len(m.appendalternative_titles) == 0 {
return nil, false
}
return m.appendalternative_titles, true
}
// ClearAlternativeTitles clears the value of the "alternative_titles" field.
func (m *MediaMutation) ClearAlternativeTitles() {
m.alternative_titles = nil
m.appendalternative_titles = nil
m.clearedFields[media.FieldAlternativeTitles] = struct{}{}
}
// AlternativeTitlesCleared returns if the "alternative_titles" field was cleared in this mutation.
func (m *MediaMutation) AlternativeTitlesCleared() bool {
_, ok := m.clearedFields[media.FieldAlternativeTitles]
return ok
}
// ResetAlternativeTitles resets all changes to the "alternative_titles" field.
func (m *MediaMutation) ResetAlternativeTitles() {
m.alternative_titles = nil
m.appendalternative_titles = nil
delete(m.clearedFields, media.FieldAlternativeTitles)
}
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by ids.
func (m *MediaMutation) AddEpisodeIDs(ids ...int) {
if m.episodes == nil {
@@ -5969,7 +6036,7 @@ func (m *MediaMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *MediaMutation) Fields() []string {
fields := make([]string, 0, 15)
fields := make([]string, 0, 16)
if m.tmdb_id != nil {
fields = append(fields, media.FieldTmdbID)
}
@@ -6015,6 +6082,9 @@ func (m *MediaMutation) Fields() []string {
if m.extras != nil {
fields = append(fields, media.FieldExtras)
}
if m.alternative_titles != nil {
fields = append(fields, media.FieldAlternativeTitles)
}
return fields
}
@@ -6053,6 +6123,8 @@ func (m *MediaMutation) Field(name string) (ent.Value, bool) {
return m.Limiter()
case media.FieldExtras:
return m.Extras()
case media.FieldAlternativeTitles:
return m.AlternativeTitles()
}
return nil, false
}
@@ -6092,6 +6164,8 @@ func (m *MediaMutation) OldField(ctx context.Context, name string) (ent.Value, e
return m.OldLimiter(ctx)
case media.FieldExtras:
return m.OldExtras(ctx)
case media.FieldAlternativeTitles:
return m.OldAlternativeTitles(ctx)
}
return nil, fmt.Errorf("unknown Media field %s", name)
}
@@ -6206,6 +6280,13 @@ func (m *MediaMutation) SetField(name string, value ent.Value) error {
}
m.SetExtras(v)
return nil
case media.FieldAlternativeTitles:
v, ok := value.([]schema.AlternativeTilte)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetAlternativeTitles(v)
return nil
}
return fmt.Errorf("unknown Media field %s", name)
}
@@ -6281,6 +6362,9 @@ func (m *MediaMutation) ClearedFields() []string {
if m.FieldCleared(media.FieldExtras) {
fields = append(fields, media.FieldExtras)
}
if m.FieldCleared(media.FieldAlternativeTitles) {
fields = append(fields, media.FieldAlternativeTitles)
}
return fields
}
@@ -6313,6 +6397,9 @@ func (m *MediaMutation) ClearField(name string) error {
case media.FieldExtras:
m.ClearExtras()
return nil
case media.FieldAlternativeTitles:
m.ClearAlternativeTitles()
return nil
}
return fmt.Errorf("unknown Media nullable field %s", name)
}
@@ -6366,6 +6453,9 @@ func (m *MediaMutation) ResetField(name string) error {
case media.FieldExtras:
m.ResetExtras()
return nil
case media.FieldAlternativeTitles:
m.ResetAlternativeTitles()
return nil
}
return fmt.Errorf("unknown Media field %s", name)
}

View File

@@ -31,6 +31,7 @@ func (Media) Fields() []ent.Field {
field.Bool("download_history_episodes").Optional().Default(false).Comment("tv series only"),
field.JSON("limiter", MediaLimiter{}).Optional(),
field.JSON("extras", MediaExtras{}).Optional(),
field.JSON("alternative_titles", []AlternativeTilte{}).Optional(),
}
}
@@ -41,9 +42,16 @@ func (Media) Edges() []ent.Edge {
}
}
type AlternativeTilte struct {
Iso3166_1 string `json:"iso_3166_1"`
Title string `json:"title"`
Type string `json:"type"`
}
type MediaLimiter struct {
SizeMin int `json:"size_min"` //in B
SizeMax int `json:"size_max"` //in B
SizeMin int64 `json:"size_min"` //in B
SizeMax int64 `json:"size_max"` //in B
PreferSize int64 `json:"prefer_max"`
}
type MediaExtras struct {
@@ -51,7 +59,7 @@ type MediaExtras struct {
JavId string `json:"javid"`
//OriginCountry []string `json:"origin_country"`
OriginalLanguage string `json:"original_language"`
Genres []struct {
Genres []struct {
ID int64 `json:"id"`
Name string `json:"name"`
} `json:"genres"`

37
go.mod
View File

@@ -10,7 +10,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/robfig/cron v1.2.0
go.uber.org/zap v1.27.0
golang.org/x/net v0.27.0
golang.org/x/net v0.33.0
)
require (
@@ -25,6 +25,12 @@ require (
)
require (
cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/ai v0.8.0 // indirect
cloud.google.com/go/auth v0.7.1 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
cloud.google.com/go/compute/metadata v0.5.0 // indirect
cloud.google.com/go/longrunning v0.5.10 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect
@@ -33,8 +39,17 @@ require (
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/go-test/deep v1.0.4 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/generative-ai-go v0.19.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
github.com/gregdel/pushover v1.3.1 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
@@ -47,7 +62,19 @@ require (
github.com/stretchr/objx v0.5.2 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/tetratelabs/wazero v1.8.0 // indirect
golang.org/x/sync v0.8.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/api v0.188.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect
google.golang.org/grpc v1.65.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
lukechampine.com/blake3 v1.1.6 // indirect
@@ -95,11 +122,11 @@ require (
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/zclconf/go-cty v1.8.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.27.0
golang.org/x/crypto v0.31.0
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
golang.org/x/mod v0.20.0 // indirect
golang.org/x/sys v0.25.0
golang.org/x/text v0.18.0 // indirect
golang.org/x/sys v0.28.0
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

106
go.sum
View File

@@ -2,6 +2,19 @@ ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 h1:GwdJbXydHCYPedeeLt4x/lrl
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43/go.mod h1:uj3pm+hUTVN/X5yfdBexHlZv+1Xu5u5ZbZx7+CDavNU=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w=
cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE=
cloud.google.com/go/auth v0.7.1 h1:Iv1bbpzJ2OIg16m94XI9/tlzZZl3cdeR3nGVGj78N7s=
cloud.google.com/go/auth v0.7.1/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg=
cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
cloud.google.com/go/longrunning v0.5.10 h1:eB/BniENNRKhjz/xgiillrdcH3G74TGSl3BXinGlI7E=
cloud.google.com/go/longrunning v0.5.10/go.mod h1:tljz5guTr5oc/qhlUjBlk7UAIFMOGuPNxkNDZXlLics=
crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk=
crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
entgo.io/ent v0.13.1 h1:uD8QwN1h6SNphdCCzmkMN3feSUzNnVvV/WIkHKMbzOE=
@@ -68,12 +81,14 @@ github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cyruzin/golang-tmdb v1.6.3 h1:TKK9h+uuwiDOaFlsVispG1KxqhsSM5Y4ZELnUF3GlqU=
github.com/cyruzin/golang-tmdb v1.6.3/go.mod h1:ZSryJLCcY+9TiKU+LbouXKns++YBrM8Tizannr05c+I=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -86,6 +101,12 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -111,6 +132,11 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -135,6 +161,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -145,19 +173,34 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg=
github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -271,6 +314,7 @@ github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
@@ -350,6 +394,18 @@ github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUA
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -361,9 +417,10 @@ golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
@@ -383,18 +440,22 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -403,19 +464,21 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -423,8 +486,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -436,14 +499,17 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
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=
@@ -452,20 +518,39 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
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/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw=
google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag=
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=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
@@ -491,6 +576,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
lukechampine.com/blake3 v1.1.6 h1:H3cROdztr7RCfoaTpGZFQsrqvweFLrqS73j7L7cmR5c=
lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

266
pkg/gemini/gemini.go Normal file
View File

@@ -0,0 +1,266 @@
package gemini
import (
"bytes"
"context"
"encoding/json"
"fmt"
"polaris/log"
"strings"
"github.com/google/generative-ai-go/genai"
"google.golang.org/api/option"
)
func NewClient(apiKey, modelName string) (*Client, error) {
ctx := context.Background()
client, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
if err != nil {
return nil, err
}
return &Client{apiKey: apiKey, modelName: modelName, c: client}, nil
}
type Client struct {
apiKey string
modelName string
c *genai.Client
}
type TvInfo struct {
TitleEnglish string `json:"title_english"`
TitleChinses string `json:"title_chinese"`
Season int `json:"season"`
StartEpisode int `json:"start_episode"`
EndEpisode int `json:"end_episode"`
Resolution string `json:"resolution"`
Subtitle string `json:"subtitle"`
ReleaseGroup string `json:"release_group"`
Year int `json:"year"`
AudioLanguage string `json:"audio_language"`
IsCompleteSeason bool `json:"is_complete_season"`
}
func (c *Client) ParseTvInfo(q string) (*TvInfo, error) {
log.Info(q)
ctx := context.Background()
model := c.c.GenerativeModel(c.modelName)
model.ResponseMIMEType = "application/json"
model.ResponseSchema = &genai.Schema{
Type: genai.TypeObject,
Properties: map[string]*genai.Schema{
"title_english": {Type: genai.TypeString},
"title_chinese": {Type: genai.TypeString},
"season": {Type: genai.TypeInteger, Description: "season number"},
"start_episode": {Type: genai.TypeInteger},
"end_episode": {Type: genai.TypeInteger},
//"episodes": {Type: genai.TypeString},
"resolution": {Type: genai.TypeString},
"subtitle": {Type: genai.TypeString},
"release_group": {Type: genai.TypeString},
"year": {Type: genai.TypeInteger},
"audio_language": {Type: genai.TypeString},
"is_complete_season": {Type: genai.TypeBoolean},
},
Required: []string{"title_english", "title_chinese", "season", "start_episode", "resolution"},
}
resp, err := model.GenerateContent(ctx, genai.Text(q))
if err != nil {
return nil, err
}
for _, part := range resp.Candidates[0].Content.Parts {
if txt, ok := part.(genai.Text); ok {
var info TvInfo
if err := json.Unmarshal([]byte(txt), &info); err != nil {
return nil, err
}
return &info, nil
}
}
return nil, fmt.Errorf("not found")
}
type MovieInfo struct {
TitleEnglish string `json:"title_english"`
TitleChinses string `json:"title_chinese"`
Resolution string `json:"resolution"`
Subtitle string `json:"subtitle"`
ReleaseGroup string `json:"release_group"`
Year int `json:"year"`
AudioLanguage string `json:"audio_language"`
}
func (c *Client) ParseMovieInfo(q string) (*MovieInfo, error) {
log.Info(q)
ctx := context.Background()
model := c.c.GenerativeModel(c.modelName)
model.ResponseMIMEType = "application/json"
model.ResponseSchema = &genai.Schema{
Type: genai.TypeObject,
Properties: map[string]*genai.Schema{
"title_english": {Type: genai.TypeString},
"title_chinese": {Type: genai.TypeString},
"resolution": {Type: genai.TypeString},
"subtitle": {Type: genai.TypeString},
"release_group": {Type: genai.TypeString},
"year": {Type: genai.TypeInteger},
"audio_language": {Type: genai.TypeString},
},
Required: []string{"title_english", "title_chinese", "resolution"},
}
resp, err := model.GenerateContent(ctx, genai.Text(q))
if err != nil {
return nil, err
}
for _, part := range resp.Candidates[0].Content.Parts {
if txt, ok := part.(genai.Text); ok {
var info MovieInfo
if err := json.Unmarshal([]byte(txt), &info); err != nil {
return nil, err
}
return &info, nil
}
}
return nil, fmt.Errorf("not found")
}
func (c *Client) isTvSeries(q string) (bool, error) {
ctx := context.Background()
model := c.c.GenerativeModel(c.modelName)
model.ResponseMIMEType = "application/json"
model.ResponseSchema = &genai.Schema{
Type: genai.TypeBoolean, Nullable: true, Description: "whether the input text implies a tv series",
}
resp, err := model.GenerateContent(ctx, genai.Text(q))
if err != nil {
return false, err
}
for _, part := range resp.Candidates[0].Content.Parts {
if txt, ok := part.(genai.Text); ok {
return strings.ToLower(string(txt)) == "true", nil
}
}
return false, fmt.Errorf("error")
}
func (c *Client) ImpliesSameTvOrMovie(torrentName, mediaName string) bool {
ctx := context.Background()
model := c.c.GenerativeModel(c.modelName)
model.ResponseMIMEType = "application/json"
model.ResponseSchema = &genai.Schema{
Type: genai.TypeBoolean, Nullable: true,
}
q := fmt.Sprintf("whether this file name \"%s\" implies the same TV series or movie with name \"%s\"?", torrentName, mediaName)
resp, err := model.GenerateContent(ctx, genai.Text(q))
if err != nil {
return false
}
for _, part := range resp.Candidates[0].Content.Parts {
if txt, ok := part.(genai.Text); ok {
return strings.ToLower(string(txt)) == "true"
}
}
return false
}
func (c *Client) FilterTvOrMovies(resourcesNames []string, titles ...string) ([]string, error) {
ctx := context.Background()
model := c.c.GenerativeModel(c.modelName)
model.ResponseMIMEType = "application/json"
model.ResponseSchema = &genai.Schema{
Type: genai.TypeArray,
Items: &genai.Schema{Type: genai.TypeString},
}
for i, s := range titles {
titles[i] = "\"" + s + "\""
}
p := &bytes.Buffer{}
p.WriteString(`the following list of file names, list all of which implies the same TV series or movie of name`)
p.WriteString(strings.Join(titles, " or "))
p.WriteString(":\n")
for _, r := range resourcesNames {
p.WriteString(" * ")
p.WriteString(r)
p.WriteString("\n")
}
log.Debugf("FilterTvOrMovies prompt is %s", p.String())
resp, err := model.GenerateContent(ctx, genai.Text(p.String()))
if err != nil {
return nil, err
}
for _, part := range resp.Candidates[0].Content.Parts {
if txt, ok := part.(genai.Text); ok {
var s []string
if err := json.Unmarshal([]byte(txt), &s); err != nil {
return nil, err
}
return s, nil
}
}
return nil, fmt.Errorf("nothing found")
}
func (c *Client) FilterMovies(resourcesNames []string, year int, titles ...string) ([]string, error) {
ctx := context.Background()
model := c.c.GenerativeModel(c.modelName)
model.ResponseMIMEType = "application/json"
model.ResponseSchema = &genai.Schema{
Type: genai.TypeArray,
Items: &genai.Schema{Type: genai.TypeString},
}
for i, s := range titles {
titles[i] = "\"" + s + "\""
}
p := &bytes.Buffer{}
p.WriteString( fmt.Sprint("the following list of file names, list all of which match following criteria: 1. Is movie 2. Released in year %d 3. Have name of ", year))
p.WriteString(strings.Join(titles, " or "))
p.WriteString(":\n")
for _, r := range resourcesNames {
p.WriteString(" * ")
p.WriteString(r)
p.WriteString("\n")
}
log.Debugf("FilterTvOrMovies prompt is %s", p.String())
resp, err := model.GenerateContent(ctx, genai.Text(p.String()))
if err != nil {
return nil, err
}
for _, part := range resp.Candidates[0].Content.Parts {
if txt, ok := part.(genai.Text); ok {
var s []string
if err := json.Unmarshal([]byte(txt), &s); err != nil {
return nil, err
}
return s, nil
}
}
return nil, fmt.Errorf("nothing found")
}

View File

@@ -0,0 +1,8 @@
package gemini
import (
"testing"
)
func Test_any1(t *testing.T) {
}

View File

@@ -10,20 +10,22 @@ import (
"github.com/gabriel-vasile/mimetype"
)
func NewAlist(cfg *alist.Config, dir string) (*Alist, error) {
func NewAlist(cfg *alist.Config, dir string, videoFormats []string, subtitleFormats []string) (*Alist, error) {
cl := alist.New(cfg)
_, err := cl.Login()
if err != nil {
return nil, err
}
return &Alist{baseDir: dir, cfg: cfg, client: cl}, nil
return &Alist{baseDir: dir, cfg: cfg, client: cl, videoFormats: videoFormats, subtitleFormats: subtitleFormats}, nil
}
type Alist struct {
baseDir string
cfg *alist.Config
client *alist.Client
progresser func() float64
baseDir string
cfg *alist.Config
client *alist.Client
progresser func() float64
videoFormats []string
subtitleFormats []string
}
func (a *Alist) Move(src, dest string) error {
@@ -34,7 +36,7 @@ func (a *Alist) Move(src, dest string) error {
}
func (a *Alist) Copy(src, dest string) error {
b, err := NewBase(src)
b, err := NewBase(src, a.videoFormats, a.subtitleFormats)
if err != nil {
return err
}
@@ -70,3 +72,7 @@ func (a *Alist) UploadProgress() float64 {
}
return a.progresser()
}
func (a *Alist) RemoveAll(path string) error {
return nil
}

View File

@@ -7,10 +7,12 @@ import (
"path/filepath"
"polaris/log"
"polaris/pkg/utils"
"strings"
"github.com/gabriel-vasile/mimetype"
"github.com/pkg/errors"
)
type Storage interface {
Move(src, dest string) error
Copy(src, dest string) error
@@ -18,24 +20,69 @@ type Storage interface {
ReadFile(string) ([]byte, error)
WriteFile(string, []byte) error
UploadProgress() float64
RemoveAll(path string) error
}
type uploadFunc func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mimeType *mimetype.MIME) error
type Base struct {
src string
totalSize int64
uploadedSize int64
src string
videoFormats []string
subtitleFormats []string
totalSize int64
uploadedSize int64
}
func NewBase(src string) (*Base, error) {
b := &Base{src: src}
func NewBase(src string, videoFormats []string, subtitleFormats []string) (*Base, error) {
b := &Base{src: src, videoFormats: videoFormats, subtitleFormats: subtitleFormats}
err := b.calculateSize()
return b, err
}
func (b *Base) checkVideoFilesExist() bool {
if len(b.videoFormats) == 0 { // do not check
return true
}
hasVideo := false
filepath.Walk(b.src, func(path string, info fs.FileInfo, err error) error {
ext := filepath.Ext(strings.ToLower(info.Name()))
for _, f := range b.videoFormats {
if f == ext {
hasVideo = true
}
}
return nil
})
return hasVideo
}
func (b *Base) isFileNeeded(name string) bool {
ext := filepath.Ext(strings.ToLower(name))
if len(b.videoFormats) == 0 {
return true
} else {
for _, f := range b.videoFormats {
if f == ext {
return true
}
}
}
if len(b.subtitleFormats) > 0 {
for _, f := range b.subtitleFormats {
if f == ext {
return true
}
}
}
return false
}
func (b *Base) Upload(destDir string, tryLink, detectMime, changeMediaHash bool, upload uploadFunc, mkdir func(string) error) error {
if !b.checkVideoFilesExist() {
return errors.Errorf("torrent has no video file(s)")
}
os.MkdirAll(destDir, os.ModePerm)
targetBase := filepath.Join(destDir, filepath.Base(b.src)) //文件的场景,要加上文件名, move filename ./dir/
@@ -61,6 +108,10 @@ func (b *Base) Upload(destDir string, tryLink, detectMime, changeMediaHash bool,
if info.IsDir() {
mkdir(destName)
} else { //is file
if !b.isFileNeeded(info.Name()) {
log.Debugf("file is not needed, skip: %s", info.Name())
return nil
}
if tryLink {
if err := os.Link(path, destName); err == nil {
return nil //link success
@@ -116,12 +167,11 @@ func (b *Base) calculateSize() error {
}
func (b *Base) Progress() float64 {
return float64(b.uploadedSize)/float64(b.totalSize)
return float64(b.uploadedSize) / float64(b.totalSize)
}
type progressReader struct {
R io.Reader
R io.Reader
Add func(int)
}
@@ -129,4 +179,4 @@ func (pr *progressReader) Read(p []byte) (int, error) {
n, err := pr.R.Read(p)
pr.Add(n)
return n, err
}
}

View File

@@ -11,18 +11,19 @@ import (
"github.com/pkg/errors"
)
func NewLocalStorage(dir string) (*LocalStorage, error) {
func NewLocalStorage(dir string, videoFormats []string, subtitleFormats []string) (*LocalStorage, error) {
os.MkdirAll(dir, 0655)
return &LocalStorage{dir: dir}, nil
return &LocalStorage{dir: dir, videoFormats: videoFormats, subtitleFormats: subtitleFormats}, nil
}
type LocalStorage struct {
dir string
dir string
videoFormats []string
subtitleFormats []string
}
func (l *LocalStorage) Copy(src, destDir string) error {
b, err := NewBase(src)
b, err := NewBase(src, l.videoFormats, l.subtitleFormats)
if err != nil {
return err
}
@@ -69,4 +70,8 @@ func (l *LocalStorage) WriteFile(name string, data []byte) error {
func (l *LocalStorage) UploadProgress() float64 {
return 0
}
func (l *LocalStorage) RemoveAll(path string) error {
return os.RemoveAll(filepath.Join(l.dir, path))
}

View File

@@ -16,10 +16,12 @@ type WebdavStorage struct {
fs *gowebdav.Client
dir string
changeMediaHash bool
progresser func() float64
progresser func() float64
videoFormats []string
subtitleFormats []string
}
func NewWebdavStorage(url, user, password, path string, changeMediaHash bool) (*WebdavStorage, error) {
func NewWebdavStorage(url, user, password, path string, changeMediaHash bool, videoFormats []string, subtitleFormats []string) (*WebdavStorage, error) {
c := gowebdav.NewClient(url, user, password)
if err := c.Connect(); err != nil {
return nil, errors.Wrap(err, "connect webdav")
@@ -27,11 +29,13 @@ func NewWebdavStorage(url, user, password, path string, changeMediaHash bool) (*
return &WebdavStorage{
fs: c,
dir: path,
videoFormats: videoFormats,
subtitleFormats: subtitleFormats,
}, nil
}
func (w *WebdavStorage) Copy(local, remoteDir string) error {
b, err := NewBase(local)
b, err := NewBase(local, w.videoFormats, w.subtitleFormats)
if err != nil {
return err
}
@@ -80,4 +84,8 @@ func (w *WebdavStorage) UploadProgress() float64 {
return 0
}
return w.progresser()
}
}
func (w *WebdavStorage) RemoveAll(path string) error {
return w.fs.RemoveAll(filepath.Join(w.dir, path))
}

View File

@@ -96,9 +96,9 @@ func (r *Response) ToResults(indexer *db.TorznabInfo) []Result {
Description: item.Description,
Link: item.Link,
Size: mustAtoI(item.Size),
Seeders: mustAtoI(item.GetAttr("seeders")),
Peers: mustAtoI(item.GetAttr("peers")),
Category: mustAtoI(item.GetAttr("category")),
Seeders: int(mustAtoI(item.GetAttr("seeders"))),
Peers: int(mustAtoI(item.GetAttr("peers"))),
Category: int(mustAtoI(item.GetAttr("category"))),
ImdbId: imdb,
DownloadVolumeFactor: tryParseFloat(item.GetAttr("downloadvolumefactor")),
UploadVolumeFactor: tryParseFloat(item.GetAttr("uploadvolumefactor")),
@@ -112,8 +112,8 @@ func (r *Response) ToResults(indexer *db.TorznabInfo) []Result {
return res
}
func mustAtoI(key string) int {
i, err := strconv.Atoi(key)
func mustAtoI(key string) int64 {
i, err := strconv.ParseInt(key, 10, 64)
if err != nil {
log.Errorf("must atoi error: %v", err)
panic(err)
@@ -183,7 +183,7 @@ type Result struct {
Name string `json:"name"`
Description string `json:"description"`
Link string `json:"link"`
Size int `json:"size"`
Size int64 `json:"size"`
Seeders int `json:"seeders"`
Peers int `json:"peers"`
Category int `json:"category"`

View File

@@ -46,6 +46,7 @@ func (c *Client) registerCronJob(name string, cron string, f func() error) {
func (c *Client) Init() {
go c.reloadTasks()
c.addSysCron()
go c.checkW500PosterOnStartup()
}
func (c *Client) reloadTasks() {

2
server/core/fliters.go Normal file
View File

@@ -0,0 +1,2 @@
package core

View File

@@ -114,8 +114,9 @@ type AddWatchlistIn struct {
Resolution string `json:"resolution" binding:"required"`
Folder string `json:"folder" binding:"required"`
DownloadHistoryEpisodes bool `json:"download_history_episodes"` //for tv
SizeMin int `json:"size_min"`
SizeMax int `json:"size_max"`
SizeMin int64 `json:"size_min"`
SizeMax int64 `json:"size_max"`
PreferSize int64 `json:"prefer_size"`
}
func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
@@ -139,7 +140,7 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
}
log.Infof("find detail for tv id %d: %+v", in.TmdbID, detail)
lastSeason := 0
lastSeason := 0
for _, season := range detail.Seasons {
if season.SeasonNumber > lastSeason && season.EpisodeCount > 0 { //如果最新一季已经有剧集信息,则以最新一季为准
lastSeason = season.SeasonNumber
@@ -148,6 +149,11 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
log.Debugf("latest season is %v", lastSeason)
alterTitles, err := c.getAlterTitles(in.TmdbID, media.MediaTypeTv)
if err != nil {
return nil, errors.Wrap(err, "get alter titles")
}
var epIds []int
for _, season := range detail.Seasons {
seasonId := season.SeasonNumber
@@ -194,6 +200,7 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
epIds = append(epIds, epid)
}
}
m := &ent.Media{
TmdbID: int(detail.ID),
ImdbID: detail.IMDbID,
@@ -212,6 +219,7 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
OriginalLanguage: detail.OriginalLanguage,
Genres: detail.Genres,
},
AlternativeTitles: alterTitles,
}
r, err := c.db.AddMediaWatchlist(m, epIds)
@@ -222,6 +230,10 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
if err := c.downloadPoster(detail.PosterPath, r.ID); err != nil {
log.Errorf("download poster error: %v", err)
}
if err := c.downloadW500Poster(detail.PosterPath, r.ID); err != nil {
log.Errorf("download w500 poster error: %v", err)
}
if err := c.downloadBackdrop(detail.BackdropPath, r.ID); err != nil {
log.Errorf("download poster error: %v", err)
}
@@ -235,6 +247,42 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
return nil, nil
}
func (c *Client) getAlterTitles(tmdbId int, mediaType media.MediaType) ([]schema.AlternativeTilte, error){
var titles []schema.AlternativeTilte
if mediaType == media.MediaTypeTv {
alterTitles, err := c.MustTMDB().GetTVAlternativeTitles(tmdbId, c.language)
if err != nil {
return nil, errors.Wrap(err, "tmdb")
}
for _, t := range alterTitles.Results {
titles = append(titles, schema.AlternativeTilte{
Iso3166_1: t.Iso3166_1,
Title: t.Title,
Type: t.Type,
})
}
} else if mediaType == media.MediaTypeMovie {
alterTitles, err := c.MustTMDB().GetMovieAlternativeTitles(tmdbId, c.language)
if err != nil {
return nil, errors.Wrap(err, "tmdb")
}
for _, t := range alterTitles.Titles {
titles = append(titles, schema.AlternativeTilte{
Iso3166_1: t.Iso3166_1,
Title: t.Title,
Type: t.Type,
})
}
}
log.Debugf("get alternative titles: %+v", titles)
return titles, nil
}
func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
log.Infof("add movie watchlist input: %+v", in)
detailCn, err := c.MustTMDB().GetMovieDetails(in.TmdbID, db.LanguageCN)
@@ -253,6 +301,12 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
}
log.Infof("find detail for movie id %d: %v", in.TmdbID, detail)
alterTitles, err := c.getAlterTitles(in.TmdbID, media.MediaTypeMovie)
if err != nil {
return nil, errors.Wrap(err, "get alter titles")
}
epid, err := c.db.SaveEposideDetail(&ent.Episode{
SeasonNumber: 1,
EpisodeNumber: 1,
@@ -279,6 +333,7 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
StorageID: in.StorageID,
TargetDir: in.Folder,
Limiter: schema.MediaLimiter{SizeMin: in.SizeMin, SizeMax: in.SizeMax},
AlternativeTitles: alterTitles,
}
extras := schema.MediaExtras{
@@ -300,6 +355,10 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
if err := c.downloadPoster(detail.PosterPath, r.ID); err != nil {
log.Errorf("download poster error: %v", err)
}
if err := c.downloadW500Poster(detail.PosterPath, r.ID); err != nil {
log.Errorf("download w500 poster error: %v", err)
}
if err := c.downloadBackdrop(detail.BackdropPath, r.ID); err != nil {
log.Errorf("download backdrop error: %v", err)
}
@@ -314,7 +373,7 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
}
func (c *Client) checkMovieFolder(m *ent.Media) error {
var storageImpl, err = c.getStorage(m.StorageID, media.MediaTypeMovie)
var storageImpl, err = c.GetStorage(m.StorageID, media.MediaTypeMovie)
if err != nil {
return err
}
@@ -327,8 +386,8 @@ func (c *Client) checkMovieFolder(m *ent.Media) error {
return err
}
for _,f := range files {
if f.IsDir() || f.Size() < 100 * 1000 * 1000 /* 100M */{ //忽略路径和小于100M的文件
for _, f := range files {
if f.IsDir() || f.Size() < 100*1000*1000 /* 100M */ { //忽略路径和小于100M的文件
continue
}
meta := metadata.ParseMovie(f.Name())
@@ -371,6 +430,11 @@ func (c *Client) downloadPoster(path string, mediaID int) error {
return c.downloadImage(url, mediaID, "poster.jpg")
}
func (c *Client) downloadW500Poster(path string, mediaID int) error {
url := "https://image.tmdb.org/t/p/w500" + path
return c.downloadImage(url, mediaID, "poster_w500.jpg")
}
func (c *Client) downloadImage(url string, mediaID int, name string) error {
log.Infof("try to download image: %v", url)
@@ -396,6 +460,46 @@ func (c *Client) downloadImage(url string, mediaID int, name string) error {
}
func (c *Client) checkW500PosterOnStartup() {
log.Infof("check all w500 posters")
all := c.db.GetMediaWatchlist(media.MediaTypeTv)
movies := c.db.GetMediaWatchlist(media.MediaTypeMovie)
all = append(all, movies...)
for _, e := range all {
targetFile := filepath.Join(fmt.Sprintf("%v/%d", db.ImgPath, e.ID), "poster_w500.jpg")
if _, err := os.Stat(targetFile); err != nil {
log.Infof("poster_w500.jpg not exist for %s, will download it", e.NameEn)
if e.MediaType ==media.MediaTypeTv {
detail, err := c.MustTMDB().GetTvDetails(e.TmdbID, db.LanguageCN)
if err != nil {
log.Warnf("get tmdb detail for %s error: %v", e.NameEn, err)
continue
}
if err := c.downloadW500Poster(detail.PosterPath, e.ID); err != nil {
log.Warnf("download w500 poster error: %v", err)
continue
}
} else {
detail, err := c.MustTMDB().GetMovieDetails(e.TmdbID, db.LanguageCN)
if err != nil {
log.Warnf("get tmdb detail for %s error: %v", e.NameEn, err)
continue
}
if err := c.downloadW500Poster(detail.PosterPath, e.ID); err != nil {
log.Warnf("download w500 poster error: %v", err)
continue
}
}
}
}
}
func (c *Client) SuggestedMovieFolderName(tmdbId int) (string, error) {
d1, err := c.MustTMDB().GetMovieDetails(tmdbId, c.language)

View File

@@ -35,7 +35,7 @@ func (c *Client) writeNfoFile(historyId int) error {
}
if md.MediaType == media.MediaTypeTv { //tvshow.nfo
st, err := c.getStorage(md.StorageID, media.MediaTypeTv)
st, err := c.GetStorage(md.StorageID, media.MediaTypeTv)
if err != nil {
return errors.Wrap(err, "get storage")
}
@@ -70,7 +70,7 @@ func (c *Client) writeNfoFile(historyId int) error {
}
} else if md.MediaType == media.MediaTypeMovie { //movie.nfo
st, err := c.getStorage(md.StorageID, media.MediaTypeMovie)
st, err := c.GetStorage(md.StorageID, media.MediaTypeMovie)
if err != nil {
return errors.Wrap(err, "get storage")
}
@@ -122,7 +122,7 @@ func (c *Client) writePlexmatch(historyId int) error {
if series.MediaType != media.MediaTypeTv { //.plexmatch only support tv series
return nil
}
st, err := c.getStorage(series.StorageID, media.MediaTypeTv)
st, err := c.GetStorage(series.StorageID, media.MediaTypeTv)
if err != nil {
return errors.Wrap(err, "get storage")
}
@@ -197,17 +197,26 @@ func (c *Client) nfoSupportEnabled() bool {
return c.db.GetSetting(db.SettingNfoSupportEnabled) == "true"
}
func (c *Client) getStorage(storageId int, mediaType media.MediaType) (storage.Storage, error) {
func (c *Client) GetStorage(storageId int, mediaType media.MediaType) (storage.Storage, error) {
st := c.db.GetStorage(storageId)
targetPath := st.TvPath
if mediaType == media.MediaTypeMovie {
targetPath = st.MoviePath
}
videoFormats, err := c.db.GetAcceptedVideoFormats()
if err != nil {
log.Warnf("get accepted video format error: %v", err)
}
subtitleFormats, err := c.db.GetAcceptedSubtitleFormats()
if err != nil {
log.Warnf("get accepted subtitle format error: %v", err)
}
switch st.Implementation {
case storage1.ImplementationLocal:
storageImpl1, err := storage.NewLocalStorage(targetPath)
storageImpl1, err := storage.NewLocalStorage(targetPath, videoFormats, subtitleFormats)
if err != nil {
return nil, errors.Wrap(err, "new local")
}
@@ -215,14 +224,14 @@ func (c *Client) getStorage(storageId int, mediaType media.MediaType) (storage.S
case storage1.ImplementationWebdav:
ws := st.ToWebDavSetting()
storageImpl1, err := storage.NewWebdavStorage(ws.URL, ws.User, ws.Password, targetPath, ws.ChangeFileHash == "true")
storageImpl1, err := storage.NewWebdavStorage(ws.URL, ws.User, ws.Password, targetPath, ws.ChangeFileHash == "true", videoFormats, subtitleFormats)
if err != nil {
return nil, errors.Wrap(err, "new webdav")
}
return storageImpl1, nil
case storage1.ImplementationAlist:
cfg := st.ToWebDavSetting()
storageImpl1, err := storage.NewAlist(&alist.Config{URL: cfg.URL, Username: cfg.User, Password: cfg.Password}, targetPath)
storageImpl1, err := storage.NewAlist(&alist.Config{URL: cfg.URL, Username: cfg.User, Password: cfg.Password}, targetPath, videoFormats, subtitleFormats)
if err != nil {
return nil, errors.Wrap(err, "alist")
}

View File

@@ -68,7 +68,7 @@ func (c *Client) DownloadEpisodeTorrent(r1 torznab.Result, seriesId, seasonNum i
SourceTitle: r1.Name,
TargetDir: dir,
Status: history.StatusRunning,
Size: r1.Size,
Size: int(r1.Size),
//Saved: torrent.Save(),
Link: magnet,
DownloadClientID: dlc.ID,
@@ -192,7 +192,7 @@ lo:
return torrentNames, nil
}
func (c *Client) DownloadMovie(m *ent.Media, link, name string, size int, indexerID int) (*string, error) {
func (c *Client) DownloadMovie(m *ent.Media, link, name string, size int64, indexerID int) (*string, error) {
trc, dlc, err := c.GetDownloadClient()
if err != nil {
return nil, errors.Wrap(err, "connect transmission")
@@ -219,7 +219,7 @@ func (c *Client) DownloadMovie(m *ent.Media, link, name string, size int, indexe
SourceTitle: name,
TargetDir: m.TargetDir,
Status: history.StatusRunning,
Size: size,
Size: int(size),
//Saved: torrent.Save(),
Link: magnet,
DownloadClientID: dlc.ID,

View File

@@ -2,6 +2,7 @@ package core
import (
"fmt"
"os"
"path/filepath"
"polaris/db"
"polaris/ent"
@@ -20,6 +21,10 @@ import (
func (c *Client) addSysCron() {
c.registerCronJob("check_running_tasks", "@every 1m", c.checkTasks)
c.registerCronJob("check_available_medias_to_download", "0 0 * * * *", func() error {
v := os.Getenv("POLARIS_NO_AUTO_DOWNLOAD")
if v == "true" {
return nil
}
c.downloadAllTvSeries()
c.downloadAllMovies()
return nil
@@ -198,7 +203,7 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
}
st := c.db.GetStorage(series.StorageID)
log.Infof("move task files to target dir: %v", r.TargetDir)
stImpl, err := c.getStorage(st.ID, series.MediaType)
stImpl, err := c.GetStorage(st.ID, series.MediaType)
if err != nil {
return err
}
@@ -238,7 +243,7 @@ func (c *Client) CheckDownloadedSeriesFiles(m *ent.Media) error {
}
log.Infof("check files in directory: %s", m.TargetDir)
var storageImpl, err = c.getStorage(m.StorageID, media.MediaTypeTv)
var storageImpl, err = c.GetStorage(m.StorageID, media.MediaTypeTv)
if err != nil {
return err
}
@@ -434,7 +439,7 @@ func (c *Client) downloadMovieSingleEpisode(ep *ent.Episode, targetDir string) (
SourceTitle: r1.Name,
TargetDir: targetDir,
Status: history.StatusRunning,
Size: r1.Size,
Size: int(r1.Size),
//Saved: torrent.Save(),
Link: magnet,
DownloadClientID: dlc.ID,

View File

@@ -3,8 +3,10 @@ package core
import (
"fmt"
"polaris/db"
"polaris/ent"
"polaris/ent/media"
"polaris/log"
"polaris/pkg/gemini"
"polaris/pkg/metadata"
"polaris/pkg/prowlarr"
"polaris/pkg/torznab"
@@ -26,14 +28,120 @@ type SearchParam struct {
FilterQiangban bool //for movie, 是否过滤枪版电影
}
func names2Query(media *ent.Media) []string {
var names = []string{media.NameEn}
if media.NameCn != "" {
hasName := false
for _, n := range names {
if media.NameCn == n {
hasName = true
}
}
if !hasName {
names = append(names, media.NameCn)
}
}
if media.OriginalName != "" {
hasName := false
for _, n := range names {
if media.OriginalName == n {
hasName = true
}
}
if !hasName {
names = append(names, media.OriginalName)
}
}
for _, t := range media.AlternativeTitles {
if (t.Iso3166_1 == "CN" || t.Iso3166_1 == "US") && t.Type == "" {
hasName := false
for _, n := range names {
if t.Title == n {
hasName = true
}
}
if !hasName {
names = append(names, t.Title)
}
}
}
log.Debugf("name to query %+v", names)
return names
}
func filterBasedOnGemini(cfg db.AIConfig, res []torznab.Result, names ...string) []torznab.Result {
var torrentNames []string
for _, r := range res {
torrentNames = append(torrentNames, r.Name)
}
g, err := gemini.NewClient(cfg.GeminiApiKey, cfg.GeminiModelName)
if err != nil {
log.Warnf("create gemini client: %v", err)
return res
}
resf, err := g.FilterTvOrMovies(torrentNames, names...)
if err != nil {
log.Warnf("filter with gemini: %v", err)
return res
}
var newRes []torznab.Result
for _, r := range res {
if slices.Contains(resf, r.Name) {
newRes = append(newRes, r)
}
}
return newRes
}
func filterBasedOnRules(res []torznab.Result, names ...string) []torznab.Result {
var filtered []torznab.Result
for _, r := range res {
meta := metadata.ParseTv(r.Name)
if meta.IsAcceptable(names...) {
filtered = append(filtered, r)
}
}
return filtered
}
func filterResourceNames(db1 *db.Client, res []torznab.Result, names ...string) []torznab.Result {
n1 := len(res)
cfg, err := db1.GetAIConfig()
if err != nil {
log.Warnf("get ai config: %v", err)
}
if cfg.Enabled {
res = filterBasedOnGemini(cfg, res, names...)
} else {
res = filterBasedOnRules(res, names...)
}
log.Infof("resource before name filtering length is %d, after filtering length is %d", n1, len(res))
return res
}
func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
series := db1.GetMediaDetails(param.MediaId)
if series == nil {
return nil, fmt.Errorf("no tv series of id %v", param.MediaId)
}
limiter, err := db1.GetSizeLimiter("tv")
if err != nil {
log.Warnf("get tv size limiter: %v", err)
limiter = &db.MediaSizeLimiter{}
}
log.Debugf("check tv series %s, season %d, episode %v", series.NameEn, param.SeasonNum, param.Episodes)
res := searchWithTorznab(db1, prowlarr.TV, series.NameEn, series.NameCn, series.OriginalName)
names := names2Query(series.Media)
res := searchWithTorznab(db1, prowlarr.TV, names...)
res = filterResourceNames(db1, res, names...)
var filtered []torznab.Result
lo:
@@ -80,7 +188,7 @@ lo:
continue
}
if !torrentSizeOk(series, r.Size, meta.EndEpisode+1-meta.StartEpisode, param) {
if !torrentSizeOk(series, limiter, r.Size, meta.EndEpisode+1-meta.StartEpisode, param) {
continue
}
@@ -114,14 +222,8 @@ func imdbIDMatchExact(id1, id2 string) bool {
return id1 == id2
}
func torrentSizeOk(detail *db.MediaDetails, torrentSize int, torrentEpisodeNum int, param *SearchParam) bool {
defaultMinSize := 80 * 1000 * 1000 //tv, 80M min
if detail.MediaType == media.MediaTypeMovie {
defaultMinSize = 200 * 1000 * 1000 // movie, 200M min
}
if detail.Limiter.SizeMin > 0 { //if size limiter set, use configured min size
defaultMinSize = detail.Limiter.SizeMin
}
func torrentSizeOk(detail *db.MediaDetails, globalLimiter *db.MediaSizeLimiter, torrentSize int64,
torrentEpisodeNum int, param *SearchParam) bool {
multiplier := 1 //大小倍数正常为1如果是季包则为季内集数
if detail.MediaType == media.MediaTypeTv {
@@ -133,21 +235,34 @@ func torrentSizeOk(detail *db.MediaDetails, torrentSize int, torrentEpisodeNum i
}
if param.CheckFileSize { //check file size when trigger automatic download
if detail.Limiter.SizeMin > 0 { //min size
sizeMin := detail.Limiter.SizeMin * multiplier
sizeMin := detail.Limiter.SizeMin * int64(multiplier)
if torrentSize < sizeMin { //比最小要求的大小还要小, min size not qualify
return false
}
} else if globalLimiter != nil {
resLimiter := globalLimiter.GetLimiter(detail.Resolution)
sizeMin := resLimiter.MinSize * int64(multiplier)
if torrentSize < sizeMin { //比最小要求的大小还要小, min size not qualify
return false
}
}
if detail.Limiter.SizeMax > 0 { //max size
sizeMax := detail.Limiter.SizeMax * multiplier
sizeMax := detail.Limiter.SizeMax * int64(multiplier)
if torrentSize > sizeMax { //larger than max size wanted, max size not qualify
return false
}
} else if globalLimiter != nil {
resLimiter := globalLimiter.GetLimiter(detail.Resolution)
sizeMax := resLimiter.MaxSIze * int64(multiplier)
if torrentSize > sizeMax { //larger than max size wanted, max size not qualify
return false
}
}
}
return torrentSize > defaultMinSize*multiplier
return true
}
func seasonEpisodeCount(detail *db.MediaDetails, seasonNum int) int {
@@ -181,7 +296,16 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
return nil, errors.New("no media found of id")
}
res := searchWithTorznab(db1, prowlarr.Movie, movieDetail.NameEn, movieDetail.NameCn, movieDetail.OriginalName)
limiter, err := db1.GetSizeLimiter("movie")
if err != nil {
log.Warnf("get tv size limiter: %v", err)
limiter = &db.MediaSizeLimiter{}
}
names := names2Query(movieDetail.Media)
res := searchWithTorznab(db1, prowlarr.Movie, names...)
res = filterResourceNames(db1, res, names...)
if movieDetail.Extras.IsJav() {
res1 := searchWithTorznab(db1, prowlarr.Movie, movieDetail.Extras.JavId)
res = append(res, res1...)
@@ -221,7 +345,7 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
continue
}
if !torrentSizeOk(movieDetail, r.Size, 1, param) {
if !torrentSizeOk(movieDetail, limiter, r.Size, 1, param) {
continue
}
@@ -343,5 +467,7 @@ func torrentNameOk(detail *db.MediaDetails, tester NameTester) bool {
if detail.Extras.IsJav() && tester.IsAcceptable(detail.Extras.JavId) {
return true
}
return tester.IsAcceptable(detail.NameCn, detail.NameEn, detail.OriginalName)
names := names2Query(detail.Media)
return tester.IsAcceptable(names...)
}

View File

@@ -7,9 +7,11 @@ import (
"net/url"
"polaris/db"
"polaris/log"
"polaris/pkg/cache"
"polaris/pkg/tmdb"
"polaris/server/core"
"polaris/ui"
"time"
ginzap "github.com/gin-contrib/zap"
@@ -22,20 +24,24 @@ import (
func NewServer(db *db.Client) *Server {
r := gin.Default()
s := &Server{
r: r,
db: db,
language: db.GetLanguage(),
r: r,
db: db,
language: db.GetLanguage(),
monitorNumCache: cache.NewCache[int, int](10 * time.Minute),
downloadNumCache: cache.NewCache[int, int](10 * time.Minute),
}
s.core = core.NewClient(db, s.language)
return s
}
type Server struct {
r *gin.Engine
db *db.Client
core *core.Client
language string
jwtSerect string
r *gin.Engine
db *db.Client
core *core.Client
language string
jwtSerect string
monitorNumCache *cache.Cache[int, int]
downloadNumCache *cache.Cache[int, int]
}
func (s *Server) Serve() error {
@@ -72,6 +78,10 @@ func (s *Server) Serve() error {
setting.POST("/cron/trigger", HttpHandler(s.TriggerCronJob))
setting.GET("/prowlarr", HttpHandler(s.GetProwlarrSetting))
setting.POST("/prowlarr", HttpHandler(s.SaveProwlarrSetting))
setting.GET("/limiter", HttpHandler(s.GetSizeLimiter))
setting.POST("/limiter", HttpHandler(s.SetSizeLimiter))
setting.GET("/ai", HttpHandler(s.GetAIConfig))
setting.POST("/ai", HttpHandler(s.SetAIConfig))
}
activity := api.Group("/activity")
{

View File

@@ -104,7 +104,6 @@ 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)
@@ -306,7 +305,7 @@ func (s *Server) TriggerCronJob(c *gin.Context) (interface{}, error) {
}
func (s *Server) GetProwlarrSetting(c *gin.Context) (interface{}, error) {
se, err :=s.db.GetProwlarrSetting()
se, err := s.db.GetProwlarrSetting()
if err != nil {
return &db.ProwlarrSetting{}, nil
}
@@ -321,7 +320,7 @@ func (s *Server) SaveProwlarrSetting(c *gin.Context) (interface{}, error) {
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 {
@@ -329,3 +328,57 @@ func (s *Server) SaveProwlarrSetting(c *gin.Context) (interface{}, error) {
}
return "success", nil
}
type ResolutionSizeLimiter struct {
TvLimiter *db.MediaSizeLimiter `json:"tv_limiter"`
MovieLimiter *db.MediaSizeLimiter `json:"movie_limiter"`
}
func (s *Server) GetSizeLimiter(c *gin.Context) (interface{}, error) {
tv, err := s.db.GetSizeLimiter("tv")
if err != nil {
return nil, errors.Wrap(err, "db")
}
movie, err := s.db.GetSizeLimiter("movie")
if err != nil {
return nil, errors.Wrap(err, "db")
}
r := ResolutionSizeLimiter{
TvLimiter: tv,
MovieLimiter: movie,
}
return r, nil
}
func (s *Server) SetSizeLimiter(c *gin.Context) (interface{}, error) {
var in ResolutionSizeLimiter
if err := c.ShouldBindJSON(&in); err != nil {
return nil, err
}
if err := s.db.SetSizeLimiter("tv", in.TvLimiter); err != nil {
return nil, errors.Wrap(err, "db")
}
if err := s.db.SetSizeLimiter("movie", in.MovieLimiter); err != nil {
return nil, errors.Wrap(err, "db")
}
return "success", nil
}
func (s *Server) GetAIConfig(c *gin.Context) (interface{}, error) {
aiConfig, err := s.db.GetAIConfig()
if err != nil {
return nil, errors.Wrap(err, "db")
}
return aiConfig, nil
}
func (s *Server) SetAIConfig(c *gin.Context) (interface{}, error) {
var in db.AIConfig
if err := c.ShouldBindJSON(&in); err != nil {
return nil, err
}
if err := s.db.SetAIConfig(&in); err != nil {
return nil, errors.Wrap(err, "db")
}
return "success", nil
}

View File

@@ -30,7 +30,7 @@ func (s *Server) AddStorage(c *gin.Context) (interface{}, error) {
if in.Implementation == "webdav" {
//test webdav
wd := in.ToWebDavSetting()
st, err := storage.NewWebdavStorage(wd.URL, wd.User, wd.Password, in.TvPath, false)
st, err := storage.NewWebdavStorage(wd.URL, wd.User, wd.Password, in.TvPath, false, nil, nil)
if err != nil {
return nil, errors.Wrap(err, "new webdav")
}
@@ -43,7 +43,7 @@ func (s *Server) AddStorage(c *gin.Context) (interface{}, error) {
}
} else if in.Implementation == "alist" {
cfg := in.ToAlistSetting()
_, err := storage.NewAlist(&alist.Config{URL: cfg.URL, Username: cfg.User, Password: cfg.Password}, in.TvPath)
_, err := storage.NewAlist(&alist.Config{URL: cfg.URL, Username: cfg.User, Password: cfg.Password}, in.TvPath, nil, nil)
if err != nil {
return nil, errors.Wrap(err, "alist")
}

View File

@@ -10,6 +10,7 @@ import (
"polaris/log"
"polaris/server/core"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
@@ -102,17 +103,25 @@ func (s *Server) GetTvWatchlist(c *gin.Context) (interface{}, error) {
MonitoredNum: 0,
DownloadedNum: 0,
}
details := s.db.GetMediaDetails(item.ID)
for _, ep := range details.Episodes {
if ep.Monitored {
ms.MonitoredNum++
if ep.Status == episode.StatusDownloaded {
ms.DownloadedNum++
mon, ok1 := s.monitorNumCache.Get(item.ID)
dow, ok2 := s.downloadNumCache.Get(item.ID)
if ok1 && ok2 {
ms.MonitoredNum = mon
ms.DownloadedNum = dow
} else {
details := s.db.GetMediaDetails(item.ID)
for _, ep := range details.Episodes {
if ep.Monitored {
ms.MonitoredNum++
if ep.Status == episode.StatusDownloaded {
ms.DownloadedNum++
}
}
}
s.monitorNumCache.Set(item.ID, ms.MonitoredNum)
s.downloadNumCache.Set(item.ID, ms.DownloadedNum)
}
res[i] = ms
}
return res, nil
@@ -162,9 +171,32 @@ func (s *Server) DeleteFromWatchlist(c *gin.Context) (interface{}, error) {
if err != nil {
return nil, errors.Wrap(err, "convert")
}
deleteFiles := c.Query("delete_files")
if strings.ToLower(deleteFiles) == "true" {
//will delete local media file
log.Infof("will delete local media files for %d", id)
m, err := s.db.GetMedia(id)
if err != nil {
log.Warnf("get media: %v", err)
} else {
st, err := s.core.GetStorage(m.StorageID, m.MediaType)
if err != nil {
log.Warnf("get storage error: %v", err)
} else {
if err := st.RemoveAll(m.TargetDir); err != nil {
log.Warnf("remove all : %v", err)
} else {
log.Infof("delete media files success: %v", m.TargetDir)
}
}
}
}
if err := s.db.DeleteMedia(id); err != nil {
return nil, errors.Wrap(err, "delete db")
}
os.RemoveAll(filepath.Join(db.ImgPath, ids)) //delete image related
return "success", nil
}

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819"
revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668"
channel: "stable"
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
- platform: ios
create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
- platform: macos
create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668
# User provided section

View File

@@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:ui/activity.dart';
import 'package:ui/calendar.dart';
import 'package:ui/init_wizard.dart';
import 'package:ui/login_page.dart';
import 'package:ui/movie_watchlist.dart';

View File

@@ -46,6 +46,7 @@ class APIs {
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 aiConfigUrl = "$_baseUrl/api/v1/setting/ai";
static final notifierAllUrl = "$_baseUrl/api/v1/notifier/all";
static final notifierDeleteUrl = "$_baseUrl/api/v1/notifier/id/";
@@ -58,6 +59,8 @@ class APIs {
static final tvParseUrl = "$_baseUrl/api/v1/setting/parse/tv";
static final movieParseUrl = "$_baseUrl/api/v1/setting/parse/movie";
static final mediaSizeLimiterUrl = "$_baseUrl/api/v1/setting/limiter";
static const tmdbApiKey = "tmdb_api_key";
static const downloadDirKey = "download_dir";
@@ -131,7 +134,7 @@ class APIs {
if (sp.code != 0) {
throw sp.message;
}
return sp.data==null? []:sp.data as List<String>;
return sp.data == null ? [] : sp.data as List<String>;
}
static Future<List<String>> downloadAllMovies() async {
@@ -142,7 +145,7 @@ class APIs {
if (sp.code != 0) {
throw sp.message;
}
return sp.data==null? []:sp.data as List<String>;
return sp.data == null ? [] : sp.data as List<String>;
}
static Future<String> parseTvName(String s) async {

View File

@@ -25,9 +25,15 @@ var mediaHistoryDataProvider = FutureProvider.autoDispose.family(
class ActivityData
extends AutoDisposeFamilyAsyncNotifier<List<Activity>, String> {
Timer? _timer;
@override
FutureOr<List<Activity>> build(String arg) async {
final dio = await APIs.getDio();
if (_timer != null) {
_timer!.cancel();
}
final dio = APIs.getDio();
var resp =
await dio.get(APIs.activityUrl, queryParameters: {"status": arg});
final sp = ServerResponse.fromJson(resp.data);
@@ -41,7 +47,7 @@ class ActivityData
if (arg == "active") {
//refresh active downloads
Timer(const Duration(seconds: 5),
_timer = Timer(const Duration(seconds: 5),
() => ref.invalidateSelf()); //Periodically Refresh
}
return activities;
@@ -73,7 +79,8 @@ class Activity {
required this.saved,
required this.progress,
required this.size,
required this.seedRatio, required this.uploadProgress});
required this.seedRatio,
required this.uploadProgress});
final int? id;
final int? mediaId;
@@ -101,7 +108,6 @@ class Activity {
progress: json["progress"],
seedRatio: json["seed_ratio"],
size: json["size"],
uploadProgress: json["upload_progress"]
);
uploadProgress: json["upload_progress"]);
}
}

View File

@@ -24,9 +24,9 @@ class SeriesDetailData
return SeriesDetails.fromJson(rsp.data);
}
Future<void> delete() async {
final dio = await APIs.getDio();
var resp = await dio.delete("${APIs.seriesDetailUrl}$id");
Future<void> delete(bool removeFiles ) async {
final dio = APIs.getDio();
var resp = await dio.delete("${APIs.seriesDetailUrl}$id", queryParameters: {"delete_files": removeFiles});
var rsp = ServerResponse.fromJson(resp.data);
if (rsp.code != 0) {
throw rsp.message;

View File

@@ -29,6 +29,9 @@ var prowlarrSettingDataProvider =
AsyncNotifierProvider.autoDispose<ProwlarrSettingData, ProwlarrSetting>(
ProwlarrSettingData.new);
var aiConfigDataProvider =
AsyncNotifierProvider.autoDispose<AIConfigData, AIConfig>(AIConfigData.new);
class EditSettingData extends AutoDisposeAsyncNotifier<GeneralSetting> {
@override
FutureOr<GeneralSetting> build() async {
@@ -548,3 +551,50 @@ class ProwlarrSettingData extends AutoDisposeAsyncNotifier<ProwlarrSetting> {
ref.invalidateSelf();
}
}
class AIConfigData extends AutoDisposeAsyncNotifier<AIConfig> {
@override
FutureOr<AIConfig> build() async {
final dio = APIs.getDio();
var resp = await dio.get(APIs.aiConfigUrl);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
return AIConfig.fromJson(sp.data);
}
Future<void> save(AIConfig ai) async {
final dio = APIs.getDio();
var resp = await dio.post(APIs.aiConfigUrl, data: ai.toJson());
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
ref.invalidateSelf();
}
}
class AIConfig {
final bool enabled;
final String geminiApiKey;
final String geminiModelName;
AIConfig(
{required this.enabled,
required this.geminiApiKey,
required this.geminiModelName});
factory AIConfig.fromJson(Map<String, dynamic> json) {
return AIConfig(
enabled: json["enabled"],
geminiApiKey: json["gemini_api_key"],
geminiModelName: json["gemini_model_name"]);
}
Map<String, dynamic> toJson() => {
"enabled": enabled,
"gemini_api_key": geminiApiKey,
"gemini_model_name": geminiModelName
};
}

View File

@@ -0,0 +1,109 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ui/providers/APIs.dart';
import 'package:ui/providers/server_response.dart';
var mediaSizeLimiterDataProvider =
AsyncNotifierProvider.autoDispose<MediaSizeLimiterData, MediaSizeLimiter>(
MediaSizeLimiterData.new);
class MediaSizeLimiterData extends AutoDisposeAsyncNotifier<MediaSizeLimiter> {
@override
FutureOr<MediaSizeLimiter> build() async {
final dio = APIs.getDio();
var resp = await dio.get(APIs.mediaSizeLimiterUrl);
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
return MediaSizeLimiter.fromJson(sp.data);
}
Future<void> submit(MediaSizeLimiter limiter) async {
final dio = APIs.getDio();
var resp = await dio.post(APIs.mediaSizeLimiterUrl, data: limiter.toJson());
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {
throw sp.message;
}
ref.invalidateSelf();
}
}
class MediaSizeLimiter {
SizeLimiter? tvLimiter;
SizeLimiter? movieLimiter;
MediaSizeLimiter({this.tvLimiter, this.movieLimiter});
MediaSizeLimiter.fromJson(Map<String, dynamic> json) {
tvLimiter = json['tv_limiter'] != null
? SizeLimiter.fromJson(json['tv_limiter'])
: null;
movieLimiter = json['movie_limiter'] != null
? SizeLimiter.fromJson(json['movie_limiter'])
: null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (tvLimiter != null) {
data['tv_limiter'] = tvLimiter!.toJson();
}
if (movieLimiter != null) {
data['movie_limiter'] = movieLimiter!.toJson();
}
return data;
}
}
class SizeLimiter {
ResLimiter? p720p;
ResLimiter? p1080p;
ResLimiter? p2160p;
SizeLimiter({this.p720p, this.p1080p, this.p2160p});
SizeLimiter.fromJson(Map<String, dynamic> json) {
p720p = json['720p'] != null ? ResLimiter.fromJson(json['720p']) : null;
p1080p = json['1080p'] != null ? ResLimiter.fromJson(json['1080p']) : null;
p2160p = json['2160p'] != null ? ResLimiter.fromJson(json['2160p']) : null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (p720p != null) {
data['720p'] = p720p!.toJson();
}
if (p1080p != null) {
data['1080p'] = p1080p!.toJson();
}
if (p2160p != null) {
data['2160p'] = p2160p!.toJson();
}
return data;
}
}
class ResLimiter {
int? maxSize;
int? minSize;
int? preferSize;
ResLimiter({this.maxSize, this.minSize, this.preferSize});
ResLimiter.fromJson(Map<String, dynamic> json) {
maxSize = json['max_size'];
minSize = json['min_size'];
preferSize = json['prefer_size'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['max_size'] = maxSize;
data['min_size'] = minSize;
data['prefer_size'] = preferSize;
return data;
}
}

View File

@@ -108,8 +108,8 @@ class SearchPageData
"resolution": resolution,
"folder": folder,
"download_history_episodes": downloadHistoryEpisodes,
"size_min": (limiter.start * 1000*1000).toInt(),
"size_max": (limiter.end * 1000*1000).toInt(),
"size_min": (limiter.start * 1000 * 1000).toInt(),
"size_max": (limiter.end * 1000 * 1000).toInt(),
});
var sp = ServerResponse.fromJson(resp.data);
if (sp.code != 0) {

View File

@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:quiver/strings.dart';
import 'package:ui/providers/settings.dart';
import 'package:ui/providers/size_limiter.dart';
import 'package:ui/settings/dialog.dart';
import 'package:ui/widgets/progress_indicator.dart';
import 'package:ui/widgets/widgets.dart';
@@ -22,20 +23,28 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
@override
Widget build(BuildContext context) {
var downloadClients = ref.watch(dwonloadClientsProvider);
return downloadClients.when(
data: (value) => Wrap(
children: List.generate(value.length + 1, (i) {
if (i < value.length) {
var client = value[i];
return SettingsCard(
onTap: () => showDownloadClientDetails(client),
child: Text(client.name ?? ""));
}
return SettingsCard(
onTap: () => showSelections(), child: const Icon(Icons.add));
})),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
downloadClients.when(
data: (value) => Wrap(
children: List.generate(value.length + 1, (i) {
if (i < value.length) {
var client = value[i];
return SettingsCard(
onTap: () => showDownloadClientDetails(client),
child: Text(client.name ?? ""));
}
return SettingsCard(
onTap: () => showSelections(),
child: const Icon(Icons.add));
})),
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator()),
Divider(),
getSizeLimiterWidget()
],
);
}
Future<void> showDownloadClientDetails(DownloadClient client) {
@@ -199,4 +208,160 @@ class _DownloaderState extends ConsumerState<DownloaderSettings> {
);
});
}
Widget getSizeLimiterWidget() {
var data = ref.watch(mediaSizeLimiterDataProvider);
final _formKey = GlobalKey<FormBuilderState>();
return Container(
padding: EdgeInsets.only(left: 20, right: 20, top: 20),
child: data.when(
data: (value) {
return FormBuilder(
key: _formKey,
initialValue: {
"tv_720p_min": toMbString(value.tvLimiter!.p720p!.minSize!),
"tv_720p_max": toMbString(value.tvLimiter!.p720p!.maxSize!),
"tv_1080p_min": toMbString(value.tvLimiter!.p1080p!.minSize!),
"tv_1080p_max": toMbString(value.tvLimiter!.p1080p!.maxSize!),
"tv_2160p_min": toMbString(value.tvLimiter!.p2160p!.minSize!),
"tv_2160p_max": toMbString(value.tvLimiter!.p2160p!.maxSize!),
"movie_720p_min":
toMbString(value.movieLimiter!.p720p!.minSize!),
"movie_720p_max":
toMbString(value.movieLimiter!.p720p!.maxSize!),
"movie_1080p_min":
toMbString(value.movieLimiter!.p1080p!.minSize!),
"movie_1080p_max":
toMbString(value.movieLimiter!.p1080p!.maxSize!),
"movie_2160p_min":
toMbString(value.movieLimiter!.p2160p!.minSize!),
"movie_2160p_max":
toMbString(value.movieLimiter!.p2160p!.maxSize!),
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"剧集大小限制",
style: TextStyle(fontSize: 18),
),
Divider(),
minMaxRow(" 720p", "tv_720p_min", "tv_720p_max"),
minMaxRow("1080p", "tv_1080p_min", "tv_1080p_max"),
minMaxRow("2160p", "tv_2160p_min", "tv_2160p_max"),
Text(
"电影大小限制",
style: TextStyle(fontSize: 18),
),
Divider(),
minMaxRow(" 720p", "movie_720p_min", "movie_720p_max"),
minMaxRow("1080p", "movie_1080p_min", "movie_1080p_max"),
minMaxRow("2160p", "movie_2160p_min", "movie_2160p_max"),
Center(
child: Padding(
padding: EdgeInsets.all(20),
child: LoadingElevatedButton(
onPressed: () async {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
return ref
.read(mediaSizeLimiterDataProvider.notifier)
.submit(MediaSizeLimiter(
tvLimiter: SizeLimiter(
p720p: ResLimiter(
minSize:
toByteInt(values["tv_720p_min"]),
maxSize:
toByteInt(values["tv_720p_max"])),
p1080p: ResLimiter(
minSize:
toByteInt(values["tv_1080p_min"]),
maxSize: toByteInt(
values["tv_1080p_max"])),
p2160p: ResLimiter(
minSize:
toByteInt(values["tv_2160p_min"]),
maxSize: toByteInt(
values["tv_2160p_max"])),
),
movieLimiter: SizeLimiter(
p720p: ResLimiter(
minSize: toByteInt(
values["movie_720p_min"]),
maxSize: toByteInt(
values["movie_720p_max"])),
p1080p: ResLimiter(
minSize: toByteInt(
values["movie_1080p_min"]),
maxSize: toByteInt(
values["movie_1080p_max"])),
p2160p: ResLimiter(
minSize: toByteInt(
values["movie_2160p_min"]),
maxSize: toByteInt(
values["movie_2160p_max"])),
)));
} else {
throw "validation_error";
}
},
label: Text("保存"),
),
),
)
],
),
);
},
error: (err, trace) => Container(),
loading: () => const MyProgressIndicator()),
);
}
Widget minMaxRow(String title, String nameMin, String nameMax) {
return Row(
children: [
Flexible(flex: 2, child: Container()),
Flexible(
flex: 2,
child: Text(
title,
style: TextStyle(fontSize: 16),
)),
Flexible(flex: 1, child: Container()),
Flexible(
flex: 6,
child: FormBuilderTextField(
name: nameMin,
decoration: InputDecoration(suffixText: "MB", labelText: "最小"),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.numeric()
]),
)),
Flexible(flex: 1, child: Text(" - ")),
Flexible(
flex: 6,
child: FormBuilderTextField(
name: nameMax,
decoration: InputDecoration(suffixText: "MB", labelText: "最大"),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.numeric()
]),
)),
Flexible(flex: 2, child: Container()),
],
);
}
}
String toMbString(int size) {
return (size / 1000 / 1000).toString();
}
int toByteInt(String s) {
return int.parse(s) * 1000 * 1000;
}

View File

@@ -23,152 +23,183 @@ class _GeneralState extends ConsumerState<GeneralSettings> {
@override
Widget build(BuildContext context) {
var settings = ref.watch(settingProvider);
return settings.when(
data: (v) {
return FormBuilder(
key: _formKey, //设置globalKey用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
initialValue: {
"tmdb_api": v.tmdbApiKey,
"download_dir": v.downloadDIr,
"log_level": v.logLevel,
"proxy": v.proxy,
"enable_plexmatch": v.enablePlexmatch,
"allow_qiangban": v.allowQiangban,
"enable_nfo": v.enableNfo,
"enable_adult": v.enableAdult,
"tv_naming_format": v.tvNamingFormat,
"movie_naming_format": v.movieNamingFormat,
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FormBuilderTextField(
name: "tmdb_api",
decoration: Commons.requiredTextFieldStyle(
text: "TMDB Api Key", icon: const Icon(Icons.key)),
//
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "download_dir",
decoration: Commons.requiredTextFieldStyle(
text: "下载路径",
icon: const Icon(Icons.folder),
helperText: "媒体文件临时下载路径,非最终存储路径"),
//
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "proxy",
decoration: const InputDecoration(
labelText: "代理地址",
icon: Icon(Icons.web),
hintText: "http://10.0.0.1:1080",
helperText: "后台联网代理地址,留空表示不启用代理"),
),
FormBuilderTextField(
decoration: const InputDecoration(
icon: Icon(Icons.folder),
labelText: "电视剧路径命名规则",
helperText:
"go template语法可用的变量为.NameCN, .NameEN, .Year, .TmdbID"),
name: "tv_naming_format",
),
FormBuilderTextField(
decoration: const InputDecoration(
icon: Icon(Icons.folder),
labelText: "电影路径命名规则",
helperText:
"go template语法可用的变量为.NameCN, .NameEN, .Year, .TmdbID"),
name: "movie_naming_format",
),
SizedBox(
width: 300,
child: FormBuilderDropdown(
name: "log_level",
decoration: const InputDecoration(
labelText: "日志级别",
icon: Icon(Icons.file_present_rounded),
),
items: const [
DropdownMenuItem(value: "debug", child: Text("DEBUG")),
DropdownMenuItem(value: "info", child: Text("INFO")),
DropdownMenuItem(value: "warn", child: Text("WARN")),
DropdownMenuItem(value: "error", child: Text("ERROR")),
],
validator: FormBuilderValidators.required(),
),
),
SizedBox(
width: 300,
child: FormBuilderSwitch(
decoration:
const InputDecoration(icon: Icon(Icons.back_hand)),
name: "enable_adult",
title: const Text("是否显示成人内容")),
),
SizedBox(
width: 300,
child: FormBuilderSwitch(
decoration:
const InputDecoration(icon: Icon(Icons.token)),
name: "enable_plexmatch",
title: const Text("Plex 刮削支持")),
),
SizedBox(
width: 300,
child: FormBuilderSwitch(
decoration: const InputDecoration(
icon: Icon(Icons.library_books),
helperText: "emby/kodi等软件刮削需要"),
name: "enable_nfo",
title: const Text("nfo 文件支持")),
),
SizedBox(
width: 300,
child: FormBuilderSwitch(
decoration: const InputDecoration(
icon: Icon(Icons.remove_circle)),
name: "allow_qiangban",
title: const Text("是否下载枪版资源")),
),
Center(
child: Padding(
padding: const EdgeInsets.only(top: 28.0),
child: ElevatedButton(
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text("保存"),
var aiConfig = ref.watch(aiConfigDataProvider);
return aiConfig.when(
data: (ai) {
return settings.when(
data: (v) {
return FormBuilder(
key: _formKey, //设置globalKey用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
initialValue: {
"tmdb_api": v.tmdbApiKey,
"download_dir": v.downloadDIr,
"log_level": v.logLevel,
"proxy": v.proxy,
"enable_plexmatch": v.enablePlexmatch,
"allow_qiangban": v.allowQiangban,
"enable_nfo": v.enableNfo,
"enable_adult": v.enableAdult,
"tv_naming_format": v.tvNamingFormat,
"movie_naming_format": v.movieNamingFormat,
"ai_enabled": ai.enabled,
"ai_api_key": ai.geminiApiKey,
"ai_model_name": ai.geminiModelName
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FormBuilderTextField(
name: "tmdb_api",
decoration: Commons.requiredTextFieldStyle(
text: "TMDB Api Key", icon: const Icon(Icons.key)),
//
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "download_dir",
decoration: Commons.requiredTextFieldStyle(
text: "下载路径",
icon: const Icon(Icons.folder),
helperText: "媒体文件临时下载路径,非最终存储路径"),
//
validator: FormBuilderValidators.required(),
),
FormBuilderTextField(
name: "proxy",
decoration: const InputDecoration(
labelText: "代理地址",
icon: Icon(Icons.web),
hintText: "http://10.0.0.1:1080",
helperText: "后台联网代理地址,留空表示不启用代理"),
),
FormBuilderTextField(
decoration: const InputDecoration(
icon: Icon(Icons.folder),
labelText: "电视剧路径命名规则",
helperText:
"go template语法可用的变量为.NameCN, .NameEN, .Year, .TmdbID"),
name: "tv_naming_format",
),
FormBuilderTextField(
decoration: const InputDecoration(
icon: Icon(Icons.folder),
labelText: "电影路径命名规则",
helperText:
"go template语法可用的变量为.NameCN, .NameEN, .Year, .TmdbID"),
name: "movie_naming_format",
),
SizedBox(
width: 300,
child: FormBuilderDropdown(
name: "log_level",
decoration: const InputDecoration(
labelText: "日志级别",
icon: Icon(Icons.file_present_rounded),
),
items: const [
DropdownMenuItem(
value: "debug", child: Text("DEBUG")),
DropdownMenuItem(
value: "info", child: Text("INFO")),
DropdownMenuItem(
value: "warn", child: Text("WARN")),
DropdownMenuItem(
value: "error", child: Text("ERROR")),
],
validator: FormBuilderValidators.required(),
),
onPressed: () {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
var f = ref
.read(settingProvider.notifier)
.updateSettings(GeneralSetting(
tmdbApiKey: values["tmdb_api"],
downloadDIr: values["download_dir"],
logLevel: values["log_level"],
proxy: values["proxy"],
allowQiangban: values["allow_qiangban"],
enableAdult: values["enable_adult"],
enableNfo: values["enable_nfo"],
tvNamingFormat: values["tv_naming_format"],
movieNamingFormat:
values["movie_naming_format"],
enablePlexmatch:
values["enable_plexmatch"]))
.then((v) => showSnakeBar("更新成功"));
showLoadingWithFuture(f);
}
}),
),
SizedBox(
width: 300,
child: FormBuilderSwitch(
decoration: const InputDecoration(
icon: Icon(Icons.back_hand)),
name: "enable_adult",
title: const Text("是否显示成人内容")),
),
SizedBox(
width: 300,
child: FormBuilderSwitch(
decoration:
const InputDecoration(icon: Icon(Icons.token)),
name: "enable_plexmatch",
title: const Text("Plex 刮削支持")),
),
SizedBox(
width: 300,
child: FormBuilderSwitch(
decoration: const InputDecoration(
icon: Icon(Icons.library_books),
helperText: "emby/kodi等软件刮削需要"),
name: "enable_nfo",
title: const Text("nfo 文件支持")),
),
SizedBox(
width: 300,
child: FormBuilderSwitch(
decoration: const InputDecoration(
icon: Icon(Icons.remove_circle)),
name: "allow_qiangban",
title: const Text("是否下载枪版资源")),
),
Divider(),
FormBuilderSwitch(
name: "ai_enabled", title: Text("开启Gemini AI集成")),
FormBuilderTextField(
name: "ai_model_name",
decoration: InputDecoration(labelText: "Gemini 模型名称"),
),
FormBuilderTextField(
name: "ai_api_key",
decoration:
InputDecoration(labelText: "Gemini Api Key"),
),
Center(
child: Padding(
padding: const EdgeInsets.only(top: 28.0),
child: ElevatedButton(
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text("保存"),
),
onPressed: () {
if (_formKey.currentState!.saveAndValidate()) {
var values = _formKey.currentState!.value;
ref.read(aiConfigDataProvider.notifier).save(
AIConfig(
enabled: values["ai_enabled"],
geminiApiKey: values["ai_api_key"],
geminiModelName: values["ai_model_name"]));
var f = ref
.read(settingProvider.notifier)
.updateSettings(GeneralSetting(
tmdbApiKey: values["tmdb_api"],
downloadDIr: values["download_dir"],
logLevel: values["log_level"],
proxy: values["proxy"],
allowQiangban:
values["allow_qiangban"],
enableAdult: values["enable_adult"],
enableNfo: values["enable_nfo"],
tvNamingFormat:
values["tv_naming_format"],
movieNamingFormat:
values["movie_naming_format"],
enablePlexmatch:
values["enable_plexmatch"]))
.then((v) => showSnakeBar("更新成功"));
showLoadingWithFuture(f);
}
}),
),
)
],
),
)
],
),
);
);
},
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
},
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());

View File

@@ -44,14 +44,34 @@ class WelcomePageState extends ConsumerState<WelcomePage> {
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),
),
),
data: (value) {
if (value.isEmpty) {
return Container(
height: MediaQuery.of(context).size.height * 0.7,
alignment: Alignment.center,
child: const Text(
"啥都没有...",
style: TextStyle(fontSize: 16),
));
}
if (onlyShowUnfinished) {
value = value
.where((v) => v.downloadedNum != v.monitoredNum)
.toList();
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: cardWidth(context) ,
childAspectRatio: 0.55,
mainAxisSpacing: isSmallScreen(context) ? 10 : 20,
crossAxisSpacing: isSmallScreen(context) ? 0 : 10),
itemCount: value.length,
itemBuilder: (context, index) =>
MediaCard(item: value[index]),
);
},
error: (err, trace) => PoNetworkError(err: err),
loading: () => const MyProgressIndicator());
}(),
@@ -152,28 +172,6 @@ class WelcomePageState extends ConsumerState<WelcomePage> {
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>(
@@ -243,8 +241,6 @@ class WelcomePageState extends ConsumerState<WelcomePage> {
class MediaCard extends StatelessWidget {
final MediaDetail item;
static const double smallWidth = 110;
static const double largeWidth = 140;
const MediaCard({super.key, required this.item});
@override
@@ -265,19 +261,18 @@ class MediaCard extends StatelessWidget {
context.go(TvDetailsPage.toRoute(item.id!));
}
},
child: Column(
child: LayoutBuilder(builder: (context, constraints) => Wrap(
direction: Axis.horizontal,
children: <Widget>[
Ink.image(
width: constraints.maxWidth,
height: constraints.maxWidth / 2 * 3,
fit: BoxFit.cover,
image: NetworkImage(
"${APIs.imagesUrl}/${item.id}/poster_w500.jpg",
)),
SizedBox(
width: cardWidth(context),
height: cardWidth(context) / 2 * 3,
child: Ink.image(
fit: BoxFit.cover,
image: NetworkImage(
"${APIs.imagesUrl}/${item.id}/poster.jpg",
)),
),
SizedBox(
width: cardWidth(context),
width: constraints.maxWidth,
child: Column(
children: [
LinearProgressIndicator(
@@ -297,15 +292,18 @@ class MediaCard extends StatelessWidget {
],
)),
],
),
)),
));
}
double cardWidth(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
if (screenWidth < 600) {
return smallWidth;
}
return largeWidth;
}
}
double cardWidth(BuildContext context) {
const double smallWidth = 110;
const double largeWidth = 140;
final screenWidth = MediaQuery.of(context).size.width;
if (screenWidth < 600) {
return smallWidth;
}
return largeWidth;
}

View File

@@ -193,13 +193,25 @@ class _DetailCardState extends ConsumerState<DetailCard> {
}
Future<void> showConfirmDialog(BuildContext oriContext) {
var deleteFiles = false;
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: const Text("确认删除"),
content: Text("${widget.details.name}"),
title: const Text("确认删除"),
content: StatefulBuilder(builder: (context, setState) {
return CheckboxListTile(
value: deleteFiles,
title: Text("删除媒体文件"),
onChanged: (v) {
setState(
() {
deleteFiles = v!;
},
);
});
}),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
@@ -209,7 +221,7 @@ class _DetailCardState extends ConsumerState<DetailCard> {
ref
.read(mediaDetailsProvider(widget.details.id.toString())
.notifier)
.delete()
.delete(deleteFiles)
.then((v) {
if (oriContext.mounted) {
oriContext.go(widget.details.mediaType == "tv"

View File

@@ -282,6 +282,52 @@ class _LoadingTextButtonState extends State<LoadingTextButton> {
}
}
class LoadingElevatedButton extends StatefulWidget {
const LoadingElevatedButton(
{super.key, required this.onPressed, required this.label});
final Future<void> Function() onPressed;
final Widget label;
@override
State<StatefulWidget> createState() {
return _LoadingElevatedButtonState();
}
}
class _LoadingElevatedButtonState extends State<LoadingElevatedButton> {
bool loading = false;
@override
Widget build(BuildContext context) {
return ElevatedButton.icon(
onPressed: loading
? null
: () async {
setState(() => loading = true);
try {
await widget.onPressed();
} catch (e) {
showSnakeBar("操作失败:$e");
} finally {
setState(() => loading = false);
}
},
icon: loading
? Container(
width: 24,
height: 24,
padding: const EdgeInsets.all(2.0),
child: const CircularProgressIndicator(
color: Colors.grey,
strokeWidth: 3,
),
)
: Text(""),
label: widget.label,
);
}
}
class PoError extends StatelessWidget {
const PoError({super.key, required this.msg, required this.err});
final String msg;
@@ -292,10 +338,16 @@ class PoError extends StatelessWidget {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("$msg ", style: TextStyle(color:Theme.of(context).colorScheme.error),),
Text(
"$msg ",
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
Tooltip(
message: "$err",
child: Icon(Icons.info,color: Theme.of(context).colorScheme.error,),
child: Icon(
Icons.info,
color: Theme.of(context).colorScheme.error,
),
)
],
);
@@ -304,9 +356,30 @@ class PoError extends StatelessWidget {
class PoNetworkError extends StatelessWidget {
const PoNetworkError({super.key, required this.err});
final dynamic err;
final dynamic err;
@override
Widget build(BuildContext context) {
return PoError(msg: "网络错误,请检查网络链接", err: err);
}
}
class PoProgressIndicator extends StatelessWidget {
const PoProgressIndicator({super.key, this.backgroundColor, this.value, this.icon});
final double? value;
final Color? backgroundColor;
final IconData? icon;
@override
Widget build(BuildContext context) {
return Stack(
alignment: AlignmentDirectional.center,
children: [
CircularProgressIndicator(
backgroundColor: backgroundColor,
value: value,
),
icon != null ? Opacity(opacity: 0.7, child: Icon(icon, color: Theme.of(context).colorScheme.primary,),):Container()
],
);
}
}

7
ui/macos/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Flutter-related
**/Flutter/ephemeral/
**/Pods/
# Xcode-related
**/dgph
**/xcuserdata/

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig"

View File

@@ -0,0 +1,12 @@
//
// Generated file. Do not edit.
//
import FlutterMacOS
import Foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

43
ui/macos/Podfile Normal file
View File

@@ -0,0 +1,43 @@
platform :osx, '10.14'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end

22
ui/macos/Podfile.lock Normal file
View File

@@ -0,0 +1,22 @@
PODS:
- FlutterMacOS (1.0.0)
- url_launcher_macos (0.0.1):
- FlutterMacOS
DEPENDENCIES:
- FlutterMacOS (from `Flutter/ephemeral`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
url_launcher_macos:
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
SPEC CHECKSUMS:
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
COCOAPODS: 1.15.2

View File

@@ -0,0 +1,801 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXAggregateTarget section */
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
isa = PBXAggregateTarget;
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
buildPhases = (
33CC111E2044C6BF0003C045 /* ShellScript */,
);
dependencies = (
);
name = "Flutter Assemble";
productName = FLX;
};
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
1923000523C26F70A19C5B17 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAAFECE3C7921C6460954E3D /* Pods_RunnerTests.framework */; };
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
465BCE0EA17751098AAEB5E0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 338C847C60E19B48128A7DBB /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
remoteInfo = Runner;
};
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
remoteInfo = FLX;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
33CC110E2044A8840003C045 /* Bundle Framework */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Bundle Framework";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
338C847C60E19B48128A7DBB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10ED2044A3C60003C045 /* ui.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ui.app; sourceTree = BUILT_PRODUCTS_DIR; };
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
479C447F60C752B08C7F2A48 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
49BC4AE8CB23ABB9DF839C61 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
864A824B0E9687E6B7C26662 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
AE69DDBE6A2D973F2DDE4773 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
EAAFECE3C7921C6460954E3D /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
EC6DB94E6F2EE2EBF7D7A8D3 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
F04941AF3F4D22EC07E9EC57 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
331C80D2294CF70F00263BE5 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1923000523C26F70A19C5B17 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EA2044A3C60003C045 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
465BCE0EA17751098AAEB5E0 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C80D6294CF71000263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
33BA886A226E78AF003329D5 /* Configs */ = {
isa = PBXGroup;
children = (
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
);
path = Configs;
sourceTree = "<group>";
};
33CC10E42044A3C60003C045 = {
isa = PBXGroup;
children = (
33FAB671232836740065AC1E /* Runner */,
33CEB47122A05771004F2AC0 /* Flutter */,
331C80D6294CF71000263BE5 /* RunnerTests */,
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
8E8421BAD07F4A7F642628C0 /* Pods */,
);
sourceTree = "<group>";
};
33CC10EE2044A3C60003C045 /* Products */ = {
isa = PBXGroup;
children = (
33CC10ED2044A3C60003C045 /* ui.app */,
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
33CC11242044D66E0003C045 /* Resources */ = {
isa = PBXGroup;
children = (
33CC10F22044A3C60003C045 /* Assets.xcassets */,
33CC10F42044A3C60003C045 /* MainMenu.xib */,
33CC10F72044A3C60003C045 /* Info.plist */,
);
name = Resources;
path = ..;
sourceTree = "<group>";
};
33CEB47122A05771004F2AC0 /* Flutter */ = {
isa = PBXGroup;
children = (
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
);
path = Flutter;
sourceTree = "<group>";
};
33FAB671232836740065AC1E /* Runner */ = {
isa = PBXGroup;
children = (
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
33E51914231749380026EE4D /* Release.entitlements */,
33CC11242044D66E0003C045 /* Resources */,
33BA886A226E78AF003329D5 /* Configs */,
);
path = Runner;
sourceTree = "<group>";
};
8E8421BAD07F4A7F642628C0 /* Pods */ = {
isa = PBXGroup;
children = (
EC6DB94E6F2EE2EBF7D7A8D3 /* Pods-Runner.debug.xcconfig */,
F04941AF3F4D22EC07E9EC57 /* Pods-Runner.release.xcconfig */,
864A824B0E9687E6B7C26662 /* Pods-Runner.profile.xcconfig */,
479C447F60C752B08C7F2A48 /* Pods-RunnerTests.debug.xcconfig */,
49BC4AE8CB23ABB9DF839C61 /* Pods-RunnerTests.release.xcconfig */,
AE69DDBE6A2D973F2DDE4773 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
isa = PBXGroup;
children = (
338C847C60E19B48128A7DBB /* Pods_Runner.framework */,
EAAFECE3C7921C6460954E3D /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
46368FA552F764B76BC73E54 /* [CP] Check Pods Manifest.lock */,
331C80D1294CF70F00263BE5 /* Sources */,
331C80D2294CF70F00263BE5 /* Frameworks */,
331C80D3294CF70F00263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
33CC10EC2044A3C60003C045 /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
EC8714E8A2401D3E05CE5A6C /* [CP] Check Pods Manifest.lock */,
33CC10E92044A3C60003C045 /* Sources */,
33CC10EA2044A3C60003C045 /* Frameworks */,
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
C724001738DB1B5869C33FBB /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* ui.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
33CC10E52044A3C60003C045 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C80D4294CF70F00263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 33CC10EC2044A3C60003C045;
};
33CC10EC2044A3C60003C045 = {
CreatedOnToolsVersion = 9.2;
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
enabled = 1;
};
};
};
33CC111A2044C6BA0003C045 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Manual;
};
};
};
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
33CC10EC2044A3C60003C045 /* Runner */,
331C80D4294CF70F00263BE5 /* RunnerTests */,
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C80D3294CF70F00263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10EB2044A3C60003C045 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3399D490228B24CF009A79C7 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
};
33CC111E2044C6BF0003C045 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
Flutter/ephemeral/FlutterInputs.xcfilelist,
);
inputPaths = (
Flutter/ephemeral/tripwire,
);
outputFileListPaths = (
Flutter/ephemeral/FlutterOutputs.xcfilelist,
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
46368FA552F764B76BC73E54 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
C724001738DB1B5869C33FBB /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
EC8714E8A2401D3E05CE5A6C /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C80D1294CF70F00263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
33CC10E92044A3C60003C045 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC10EC2044A3C60003C045 /* Runner */;
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
};
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
isa = PBXVariantGroup;
children = (
33CC10F52044A3C60003C045 /* Base */,
);
name = MainMenu.xib;
path = Runner;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
331C80DB294CF71000263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 479C447F60C752B08C7F2A48 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ui";
};
name = Debug;
};
331C80DC294CF71000263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 49BC4AE8CB23ABB9DF839C61 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ui";
};
name = Release;
};
331C80DD294CF71000263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = AE69DDBE6A2D973F2DDE4773 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ui.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ui";
};
name = Profile;
};
338D0CE9231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Profile;
};
338D0CEA231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Profile;
};
338D0CEB231458BD00FA5F75 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Profile;
};
33CC10F92044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
33CC10FA2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CODE_SIGN_IDENTITY = "-";
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
};
33CC10FC2044A3C60003C045 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
33CC10FD2044A3C60003C045 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_VERSION = 5.0;
};
name = Release;
};
33CC111C2044C6BA0003C045 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Manual;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Debug;
};
33CC111D2044C6BA0003C045 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C80DB294CF71000263BE5 /* Debug */,
331C80DC294CF71000263BE5 /* Release */,
331C80DD294CF71000263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10F92044A3C60003C045 /* Debug */,
33CC10FA2044A3C60003C045 /* Release */,
338D0CE9231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC10FC2044A3C60003C045 /* Debug */,
33CC10FD2044A3C60003C045 /* Release */,
338D0CEA231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
isa = XCConfigurationList;
buildConfigurations = (
33CC111C2044C6BA0003C045 /* Debug */,
33CC111D2044C6BA0003C045 /* Release */,
338D0CEB231458BD00FA5F75 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "ui.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,9 @@
import Cocoa
import FlutterMacOS
@main
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}

View File

@@ -0,0 +1,68 @@
{
"images" : [
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
},
{
"size" : "16x16",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "2x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_32.png",
"scale" : "1x"
},
{
"size" : "32x32",
"idiom" : "mac",
"filename" : "app_icon_64.png",
"scale" : "2x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_128.png",
"scale" : "1x"
},
{
"size" : "128x128",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "2x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_256.png",
"scale" : "1x"
},
{
"size" : "256x256",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "2x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_512.png",
"scale" : "1x"
},
{
"size" : "512x512",
"idiom" : "mac",
"filename" : "app_icon_1024.png",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,343 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
<connections>
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="APP_NAME" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="APP_NAME" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About APP_NAME" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
</menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">
<connections>
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
</connections>
</menuItem>
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
</connections>
</menuItem>
<menuItem title="Show All" id="Kd2-mp-pUS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
<menuItem title="Quit APP_NAME" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Edit" id="5QF-Oa-p0T">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Edit" id="W48-6f-4Dl">
<items>
<menuItem title="Undo" keyEquivalent="z" id="dRJ-4n-Yzg">
<connections>
<action selector="undo:" target="-1" id="M6e-cu-g7V"/>
</connections>
</menuItem>
<menuItem title="Redo" keyEquivalent="Z" id="6dh-zS-Vam">
<connections>
<action selector="redo:" target="-1" id="oIA-Rs-6OD"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="WRV-NI-Exz"/>
<menuItem title="Cut" keyEquivalent="x" id="uRl-iY-unG">
<connections>
<action selector="cut:" target="-1" id="YJe-68-I9s"/>
</connections>
</menuItem>
<menuItem title="Copy" keyEquivalent="c" id="x3v-GG-iWU">
<connections>
<action selector="copy:" target="-1" id="G1f-GL-Joy"/>
</connections>
</menuItem>
<menuItem title="Paste" keyEquivalent="v" id="gVA-U4-sdL">
<connections>
<action selector="paste:" target="-1" id="UvS-8e-Qdg"/>
</connections>
</menuItem>
<menuItem title="Paste and Match Style" keyEquivalent="V" id="WeT-3V-zwk">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="pasteAsPlainText:" target="-1" id="cEh-KX-wJQ"/>
</connections>
</menuItem>
<menuItem title="Delete" id="pa3-QI-u2k">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="delete:" target="-1" id="0Mk-Ml-PaM"/>
</connections>
</menuItem>
<menuItem title="Select All" keyEquivalent="a" id="Ruw-6m-B2m">
<connections>
<action selector="selectAll:" target="-1" id="VNm-Mi-diN"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="uyl-h8-XO2"/>
<menuItem title="Find" id="4EN-yA-p0u">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="1b7-l0-nxx">
<items>
<menuItem title="Find…" tag="1" keyEquivalent="f" id="Xz5-n4-O0W">
<connections>
<action selector="performFindPanelAction:" target="-1" id="cD7-Qs-BN4"/>
</connections>
</menuItem>
<menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="YEy-JH-Tfz">
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
<connections>
<action selector="performFindPanelAction:" target="-1" id="WD3-Gg-5AJ"/>
</connections>
</menuItem>
<menuItem title="Find Next" tag="2" keyEquivalent="g" id="q09-fT-Sye">
<connections>
<action selector="performFindPanelAction:" target="-1" id="NDo-RZ-v9R"/>
</connections>
</menuItem>
<menuItem title="Find Previous" tag="3" keyEquivalent="G" id="OwM-mh-QMV">
<connections>
<action selector="performFindPanelAction:" target="-1" id="HOh-sY-3ay"/>
</connections>
</menuItem>
<menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="buJ-ug-pKt">
<connections>
<action selector="performFindPanelAction:" target="-1" id="U76-nv-p5D"/>
</connections>
</menuItem>
<menuItem title="Jump to Selection" keyEquivalent="j" id="S0p-oC-mLd">
<connections>
<action selector="centerSelectionInVisibleArea:" target="-1" id="IOG-6D-g5B"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Spelling and Grammar" id="Dv1-io-Yv7">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Spelling" id="3IN-sU-3Bg">
<items>
<menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="HFo-cy-zxI">
<connections>
<action selector="showGuessPanel:" target="-1" id="vFj-Ks-hy3"/>
</connections>
</menuItem>
<menuItem title="Check Document Now" keyEquivalent=";" id="hz2-CU-CR7">
<connections>
<action selector="checkSpelling:" target="-1" id="fz7-VC-reM"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="bNw-od-mp5"/>
<menuItem title="Check Spelling While Typing" id="rbD-Rh-wIN">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleContinuousSpellChecking:" target="-1" id="7w6-Qz-0kB"/>
</connections>
</menuItem>
<menuItem title="Check Grammar With Spelling" id="mK6-2p-4JG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleGrammarChecking:" target="-1" id="muD-Qn-j4w"/>
</connections>
</menuItem>
<menuItem title="Correct Spelling Automatically" id="78Y-hA-62v">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticSpellingCorrection:" target="-1" id="2lM-Qi-WAP"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Substitutions" id="9ic-FL-obx">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Substitutions" id="FeM-D8-WVr">
<items>
<menuItem title="Show Substitutions" id="z6F-FW-3nz">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontSubstitutionsPanel:" target="-1" id="oku-mr-iSq"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="gPx-C9-uUO"/>
<menuItem title="Smart Copy/Paste" id="9yt-4B-nSM">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleSmartInsertDelete:" target="-1" id="3IJ-Se-DZD"/>
</connections>
</menuItem>
<menuItem title="Smart Quotes" id="hQb-2v-fYv">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="ptq-xd-QOA"/>
</connections>
</menuItem>
<menuItem title="Smart Dashes" id="rgM-f4-ycn">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDashSubstitution:" target="-1" id="oCt-pO-9gS"/>
</connections>
</menuItem>
<menuItem title="Smart Links" id="cwL-P1-jid">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticLinkDetection:" target="-1" id="Gip-E3-Fov"/>
</connections>
</menuItem>
<menuItem title="Data Detectors" id="tRr-pd-1PS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticDataDetection:" target="-1" id="R1I-Nq-Kbl"/>
</connections>
</menuItem>
<menuItem title="Text Replacement" id="HFQ-gK-NFA">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleAutomaticTextReplacement:" target="-1" id="DvP-Fe-Py6"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Transformations" id="2oI-Rn-ZJC">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Transformations" id="c8a-y6-VQd">
<items>
<menuItem title="Make Upper Case" id="vmV-6d-7jI">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="uppercaseWord:" target="-1" id="sPh-Tk-edu"/>
</connections>
</menuItem>
<menuItem title="Make Lower Case" id="d9M-CD-aMd">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="lowercaseWord:" target="-1" id="iUZ-b5-hil"/>
</connections>
</menuItem>
<menuItem title="Capitalize" id="UEZ-Bs-lqG">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="capitalizeWord:" target="-1" id="26H-TL-nsh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Speech" id="xrE-MZ-jX0">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Speech" id="3rS-ZA-NoH">
<items>
<menuItem title="Start Speaking" id="Ynk-f8-cLZ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="startSpeaking:" target="-1" id="654-Ng-kyl"/>
</connections>
</menuItem>
<menuItem title="Stop Speaking" id="Oyz-dy-DGm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="stopSpeaking:" target="-1" id="dX8-6p-jy9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="View" id="H8h-7b-M4v">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="View" id="HyV-fh-RgO">
<items>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleFullScreen:" target="-1" id="dU3-MA-1Rq"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Window" id="aUF-d1-5bR">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Window" systemMenu="window" id="Td7-aD-5lo">
<items>
<menuItem title="Minimize" keyEquivalent="m" id="OY7-WF-poV">
<connections>
<action selector="performMiniaturize:" target="-1" id="VwT-WD-YPe"/>
</connections>
</menuItem>
<menuItem title="Zoom" id="R4o-n2-Eq4">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="performZoom:" target="-1" id="DIl-cC-cCs"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="eu3-7i-yIM"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem title="Help" id="EPT-qC-fAb">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Help" systemMenu="help" id="rJ0-wn-3NY"/>
</menuItem>
</items>
<point key="canvasLocation" x="142" y="-258"/>
</menu>
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</window>
</objects>
</document>

View File

@@ -0,0 +1,14 @@
// Application-level settings for the Runner target.
//
// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
// future. If not, the values below would default to using the project name when this becomes a
// 'flutter create' template.
// The application's name. By default this is also the title of the Flutter window.
PRODUCT_NAME = ui
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.ui
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved.

View File

@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Debug.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -0,0 +1,2 @@
#include "../../Flutter/Flutter-Release.xcconfig"
#include "Warnings.xcconfig"

View File

@@ -0,0 +1,13 @@
WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
GCC_WARN_UNDECLARED_SELECTOR = YES
CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
CLANG_WARN_PRAGMA_PACK = YES
CLANG_WARN_STRICT_PROTOTYPES = YES
CLANG_WARN_COMMA = YES
GCC_WARN_STRICT_SELECTOR_MATCH = YES
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
GCC_WARN_SHADOW = YES
CLANG_WARN_UNREACHABLE_CODE = YES

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
import Cocoa
import FlutterMacOS
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
let flutterViewController = FlutterViewController()
let windowFrame = self.frame
self.contentViewController = flutterViewController
self.setFrame(windowFrame, display: true)
RegisterGeneratedPlugins(registry: flutterViewController)
super.awakeFromNib()
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,12 @@
import Cocoa
import FlutterMacOS
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

View File

@@ -53,10 +53,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.18.0"
version: "1.19.0"
cupertino_icons:
dependency: "direct main"
description:
@@ -217,18 +217,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.5"
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.5"
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
@@ -377,7 +377,7 @@ packages:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
version: "0.0.0"
source_span:
dependency: transitive
description:
@@ -390,10 +390,10 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.11.1"
version: "1.12.0"
state_notifier:
dependency: transitive
description:
@@ -414,10 +414,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
version: "1.3.0"
table_calendar:
dependency: "direct main"
description:
@@ -438,10 +438,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.2"
version: "0.7.3"
timeago:
dependency: "direct main"
description:
@@ -534,10 +534,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.flutter-io.cn"
source: hosted
version: "14.2.5"
version: "14.3.0"
web:
dependency: transitive
description: