mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 03:14:09 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a628bb44a | ||
|
|
9955e482a4 | ||
|
|
27dc7aea52 | ||
|
|
14df9490cf | ||
|
|
431fdd0d54 | ||
|
|
25bf3ca65b |
No files matched your search
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
build-windows:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
|
||||
runs-on: windows-2022
|
||||
runs-on: windows-latest
|
||||
needs: [get-release]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -265,7 +265,12 @@ jobs:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: subosito/flutter-action@v2
|
||||
- if: matrix.os == 'ubuntu-22.04'
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- if: matrix.os == 'ubuntu-22.04-arm'
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: master
|
||||
@@ -277,6 +282,7 @@ jobs:
|
||||
sudo apt install -y libfuse2 locate
|
||||
arch=$(uname -m)
|
||||
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
|
||||
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
|
||||
chmod +x appimagetool
|
||||
sudo mv appimagetool /usr/local/bin/
|
||||
- name: Build
|
||||
@@ -502,7 +508,7 @@ jobs:
|
||||
overwrite: true
|
||||
build-ios:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
|
||||
runs-on: macos-14
|
||||
runs-on: macos-latest
|
||||
needs: [get-release]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -53,13 +53,7 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- name: Run Unit tests.
|
||||
run: |
|
||||
# Get packages for testing and coverage (including cmd/web for flags_test.go)
|
||||
PACKAGES=$(go list ./... | grep -v /bind/ | grep -v -E '/cmd/(?!web)')
|
||||
go test -v -coverpkg=$(echo "$PACKAGES" | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $PACKAGES
|
||||
# Filter out main.go from coverage report to avoid lowering coverage percentage
|
||||
grep -v "main.go" coverage.txt > coverage_filtered.txt || true
|
||||
mv coverage_filtered.txt coverage.txt
|
||||
run: go test -v -coverpkg=$(go list ./... | grep -v /cmd/ | grep -v /bind/ | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $(go list ./... | grep -v /cmd/ | grep -v /bind/)
|
||||
- uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage.txt
|
||||
@@ -69,8 +63,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm]
|
||||
needs: [test]
|
||||
os: [ windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm ]
|
||||
needs: [ test ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -102,8 +96,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-14, ubuntu-latest]
|
||||
needs: [test]
|
||||
os: [ macos-latest, ubuntu-latest ]
|
||||
needs: [ test ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -135,7 +129,7 @@ jobs:
|
||||
build-web:
|
||||
name: Build web check
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test]
|
||||
needs: [ test ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
@@ -155,7 +149,7 @@ jobs:
|
||||
build-docker:
|
||||
name: Build docker check
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test]
|
||||
needs: [ test ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-go@v4
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func Dial() (net.Conn, error) {
|
||||
// Get binary path
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return net.Dial("unix", filepath.Join(filepath.Dir(exe), "gopeed_host.sock"))
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/Microsoft/go-winio"
|
||||
)
|
||||
|
||||
func Dial() (net.Conn, error) {
|
||||
return winio.DialPipe(`\\.\pipe\gopeed_host`, nil)
|
||||
}
|
||||
+25
-70
@@ -1,24 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/browser"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
)
|
||||
|
||||
const identifier = "gopeed"
|
||||
|
||||
type Message struct {
|
||||
Method string `json:"method"`
|
||||
Meta map[string]any `json:"meta"`
|
||||
Params json.RawMessage `json:"params"`
|
||||
}
|
||||
|
||||
@@ -28,74 +26,31 @@ type Response struct {
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func check() (data bool, err error) {
|
||||
conn, err := Dial()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer conn.Close()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func wakeup(hidden bool) error {
|
||||
running, _ := check()
|
||||
if running {
|
||||
return nil
|
||||
}
|
||||
|
||||
uri := "gopeed:"
|
||||
if hidden {
|
||||
uri = uri + "?hidden=true"
|
||||
}
|
||||
if err := browser.OpenURL(uri); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
if ok, _ := check(); ok {
|
||||
return nil
|
||||
var apiMap = map[string]func(params json.RawMessage) (data any, err error){
|
||||
"ping": func(params json.RawMessage) (data any, err error) {
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
return fmt.Errorf("start gopeed failed")
|
||||
}
|
||||
|
||||
var apiMap = map[string]func(message *Message) (data any, err error){
|
||||
"ping": func(message *Message) (data any, err error) {
|
||||
return check()
|
||||
for _, p := range processes {
|
||||
name, err := p.Name()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(name), strings.ToLower(identifier)) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
},
|
||||
"create": func(message *Message) (data any, err error) {
|
||||
buf, err := message.Params.MarshalJSON()
|
||||
if err != nil {
|
||||
"create": func(params json.RawMessage) (data any, err error) {
|
||||
var strParams string
|
||||
if err = json.Unmarshal(params, &strParams); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
silent := false
|
||||
if v, ok := message.Meta["silent"]; ok {
|
||||
silent, _ = v.(bool)
|
||||
}
|
||||
|
||||
if err := wakeup(silent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return Dial()
|
||||
},
|
||||
},
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
req, err := http.NewRequest("POST", "http://127.0.0.1/create", bytes.NewBuffer(buf))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if message.Meta != nil {
|
||||
metaJson, _ := json.Marshal(message.Meta)
|
||||
req.Header.Set("X-Gopeed-Host-Meta", string(metaJson))
|
||||
}
|
||||
_, err = client.Do(req)
|
||||
err = browser.OpenURL(fmt.Sprintf("%s:///create?params=%s", identifier, strParams))
|
||||
return
|
||||
},
|
||||
}
|
||||
@@ -133,7 +88,7 @@ func main() {
|
||||
var data any
|
||||
var err error
|
||||
if handler, ok := apiMap[message.Method]; ok {
|
||||
data, err = handler(&message)
|
||||
data, err = handler(message.Params)
|
||||
} else {
|
||||
err = errors.New("Unknown method: " + message.Method)
|
||||
}
|
||||
|
||||
+6
-5
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -18,7 +19,6 @@ func main() {
|
||||
pid := flag.Int("pid", 0, "PID of the process to update")
|
||||
updateChannel := flag.String("channel", "", "Update channel")
|
||||
packagePath := flag.String("asset", "", "Path to the package asset")
|
||||
exeDir := flag.String("exeDir", "", "Directory of the entry executable")
|
||||
logPath := flag.String("log", "", "Log file path")
|
||||
flag.Parse()
|
||||
|
||||
@@ -43,7 +43,7 @@ func main() {
|
||||
restart bool
|
||||
err error
|
||||
)
|
||||
if restart, err = update(*pid, *updateChannel, *packagePath, *exeDir); err != nil {
|
||||
if restart, err = update(*pid, *updateChannel, *packagePath); err != nil {
|
||||
log.Printf("Update failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func update(pid int, updateChannel, packagePath, exeDir string) (restart bool, err error) {
|
||||
func update(pid int, updateChannel, packagePath string) (restart bool, err error) {
|
||||
killSignalChan := make(chan any, 1)
|
||||
|
||||
go func() {
|
||||
@@ -76,8 +76,9 @@ func update(pid int, updateChannel, packagePath, exeDir string) (restart bool, e
|
||||
}
|
||||
}()
|
||||
|
||||
log.Printf("Updating process updateChannel=%s packagePath=%s exeDir=%s\n", updateChannel, packagePath, exeDir)
|
||||
if restart, err = install(killSignalChan, updateChannel, packagePath, exeDir); err != nil {
|
||||
appDir := filepath.Dir(os.Args[0])
|
||||
log.Printf("Updating process updateChannel=%s packagePath=%s appDir=%s\n", updateChannel, packagePath, appDir)
|
||||
if restart, err = install(killSignalChan, updateChannel, packagePath, appDir); err != nil {
|
||||
return false, errors.Wrap(err, "failed to install package")
|
||||
}
|
||||
|
||||
|
||||
@@ -25,20 +25,16 @@ func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error
|
||||
mountPoint := ""
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
if strings.Contains(line, "/Volumes/") {
|
||||
// Find the /Volumes/ path in the line
|
||||
// hdiutil output format: /dev/disk4s1 Apple_HFS /Volumes/Gopeed
|
||||
// or with sequence number: /dev/disk4s1 Apple_HFS /Volumes/Gopeed 1
|
||||
idx := strings.Index(line, "/Volumes/")
|
||||
if idx != -1 {
|
||||
// Extract everything from /Volumes/ onwards and trim whitespace
|
||||
mountPoint = strings.TrimSpace(line[idx:])
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) > 2 {
|
||||
mountPoint = fields[len(fields)-1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if mountPoint == "" {
|
||||
return fmt.Errorf("failed to get mount point from hdiutil output: %s", string(output))
|
||||
return fmt.Errorf("failed to get mount point")
|
||||
}
|
||||
|
||||
// Detach the mounted DMG
|
||||
@@ -49,7 +45,7 @@ func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error
|
||||
return err
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
return fmt.Errorf("no .app found in dmg, mountPoint: %s", mountPoint)
|
||||
return fmt.Errorf("no .app found in dmg")
|
||||
}
|
||||
|
||||
killSignalChan <- nil
|
||||
|
||||
+51
-166
@@ -3,23 +3,19 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
)
|
||||
|
||||
type args struct {
|
||||
Address *string `json:"address"`
|
||||
Port *int `json:"port"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
ApiToken *string `json:"apiToken"`
|
||||
StorageDir *string `json:"storageDir"`
|
||||
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
|
||||
Address *string `json:"address"`
|
||||
Port *int `json:"port"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
ApiToken *string `json:"apiToken"`
|
||||
StorageDir *string `json:"storageDir"`
|
||||
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
||||
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||
|
||||
@@ -27,173 +23,62 @@ type args struct {
|
||||
}
|
||||
|
||||
func parse() *args {
|
||||
cfg := &args{}
|
||||
|
||||
cliConfig := loadCliArgs()
|
||||
loadConfigFile(cfg, *cliConfig.configPath)
|
||||
loadEnvVars(cfg)
|
||||
// override with non-default command line arguments
|
||||
overrideWithCliArgs(cfg, cliConfig)
|
||||
// set default values
|
||||
setDefaults(cfg, cliConfig)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// loadCliArgs parses command line arguments and returns initial config
|
||||
func loadCliArgs() *args {
|
||||
cfg := &args{}
|
||||
cfg.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||
cfg.Port = flag.Int("P", 9999, "Bind Port")
|
||||
cfg.Username = flag.String("u", "gopeed", "Web Authentication Username")
|
||||
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
||||
cfg.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
|
||||
cfg.StorageDir = flag.String("d", "", "Storage directory")
|
||||
whiteDownloadDirs := flag.String("w", "", "White download directories, comma-separated")
|
||||
cfg.configPath = flag.String("c", "./config.json", "Config file path")
|
||||
var cliArgs args
|
||||
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
||||
cliArgs.Username = flag.String("u", "gopeed", "Web Authentication Username")
|
||||
cliArgs.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
||||
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
|
||||
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
||||
flag.Parse()
|
||||
|
||||
// Parse white download directories from comma-separated string
|
||||
if whiteDownloadDirs != nil && *whiteDownloadDirs != "" {
|
||||
dirs := strings.Split(*whiteDownloadDirs, ",")
|
||||
for i := range dirs {
|
||||
dirs[i] = strings.TrimSpace(dirs[i])
|
||||
}
|
||||
cfg.WhiteDownloadDirs = dirs
|
||||
// args priority: config file > cli args
|
||||
cfgArgs := loadConfig(*cliArgs.configPath)
|
||||
if cfgArgs.Address == nil {
|
||||
cfgArgs.Address = cliArgs.Address
|
||||
}
|
||||
|
||||
return cfg
|
||||
if cfgArgs.Port == nil {
|
||||
cfgArgs.Port = cliArgs.Port
|
||||
}
|
||||
if cfgArgs.Username == nil {
|
||||
cfgArgs.Username = cliArgs.Username
|
||||
}
|
||||
if cfgArgs.Password == nil {
|
||||
cfgArgs.Password = cliArgs.Password
|
||||
}
|
||||
if cfgArgs.ApiToken == nil {
|
||||
cfgArgs.ApiToken = cliArgs.ApiToken
|
||||
}
|
||||
if cfgArgs.StorageDir == nil {
|
||||
cfgArgs.StorageDir = cliArgs.StorageDir
|
||||
}
|
||||
return cfgArgs
|
||||
}
|
||||
|
||||
// overrideWithCliArgs overrides config with non-empty command line arguments
|
||||
func overrideWithCliArgs(cfg *args, cliConfig *args) {
|
||||
flag.Visit(func(f *flag.Flag) {
|
||||
switch f.Name {
|
||||
case "A":
|
||||
cfg.Address = cliConfig.Address
|
||||
case "P":
|
||||
cfg.Port = cliConfig.Port
|
||||
case "u":
|
||||
cfg.Username = cliConfig.Username
|
||||
case "p":
|
||||
cfg.Password = cliConfig.Password
|
||||
case "T":
|
||||
cfg.ApiToken = cliConfig.ApiToken
|
||||
case "d":
|
||||
cfg.StorageDir = cliConfig.StorageDir
|
||||
case "w":
|
||||
cfg.WhiteDownloadDirs = cliConfig.WhiteDownloadDirs
|
||||
case "c":
|
||||
cfg.configPath = cliConfig.configPath
|
||||
}
|
||||
})
|
||||
}
|
||||
func loadConfig(path string) *args {
|
||||
var args args
|
||||
|
||||
// setDefaults sets default values for any unset configuration fields
|
||||
func setDefaults(cfg *args, cliConfig *args) {
|
||||
if cfg.Address == nil {
|
||||
cfg.Address = cliConfig.Address
|
||||
}
|
||||
if cfg.Port == nil {
|
||||
cfg.Port = cliConfig.Port
|
||||
}
|
||||
if cfg.Username == nil {
|
||||
cfg.Username = cliConfig.Username
|
||||
}
|
||||
if cfg.Password == nil {
|
||||
cfg.Password = cliConfig.Password
|
||||
}
|
||||
if cfg.ApiToken == nil {
|
||||
cfg.ApiToken = cliConfig.ApiToken
|
||||
}
|
||||
if cfg.StorageDir == nil {
|
||||
cfg.StorageDir = cliConfig.StorageDir
|
||||
}
|
||||
}
|
||||
|
||||
// loadConfigFile loads configuration from file
|
||||
func loadConfigFile(cfg *args, configPath string) {
|
||||
if !filepath.IsAbs(configPath) {
|
||||
if !filepath.IsAbs(path) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return
|
||||
fmt.Println("config dir get failed, reason:" + err.Error())
|
||||
return &args
|
||||
}
|
||||
configPath = filepath.Join(dir, configPath)
|
||||
path = filepath.Join(dir, path)
|
||||
}
|
||||
|
||||
file, err := os.ReadFile(configPath)
|
||||
file, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
return &args
|
||||
}
|
||||
return
|
||||
fmt.Println("config file read failed, reason:" + err.Error())
|
||||
return &args
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(file, cfg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// loadEnvVars loads configuration from environment variables with prefix GOPEED_
|
||||
func loadEnvVars(cfg *args) {
|
||||
v := reflect.ValueOf(cfg).Elem()
|
||||
t := reflect.TypeOf(cfg).Elem()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
fieldType := t.Field(i)
|
||||
|
||||
// Get json tag as environment variable suffix
|
||||
jsonTag := fieldType.Tag.Get("json")
|
||||
if jsonTag == "" || jsonTag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove options like omitempty
|
||||
if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
|
||||
jsonTag = jsonTag[:commaIdx]
|
||||
}
|
||||
|
||||
// Convert to uppercase and add GOPEED_ prefix
|
||||
envKey := "GOPEED_" + strings.ToUpper(jsonTag)
|
||||
envValue := os.Getenv(envKey)
|
||||
|
||||
if envValue == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Set value based on field type
|
||||
if field.Kind() == reflect.Ptr {
|
||||
if field.IsNil() {
|
||||
// Create new pointer instance
|
||||
newVal := reflect.New(field.Type().Elem())
|
||||
field.Set(newVal)
|
||||
}
|
||||
|
||||
switch field.Type().Elem().Kind() {
|
||||
case reflect.String:
|
||||
field.Elem().SetString(envValue)
|
||||
case reflect.Int:
|
||||
if intVal, err := strconv.Atoi(envValue); err == nil {
|
||||
field.Elem().SetInt(int64(intVal))
|
||||
}
|
||||
default:
|
||||
// For complex types like DownloadConfig, try JSON unmarshaling
|
||||
if field.Type().Elem() == reflect.TypeOf(base.DownloaderStoreConfig{}) {
|
||||
var config base.DownloaderStoreConfig
|
||||
if err := json.Unmarshal([]byte(envValue), &config); err == nil {
|
||||
field.Set(reflect.ValueOf(&config))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if field.Kind() == reflect.Slice {
|
||||
// Handle non-pointer slice types (like []string for WhiteDownloadDirs)
|
||||
if field.Type().Elem().Kind() == reflect.String {
|
||||
dirs := strings.Split(envValue, ",")
|
||||
for i := range dirs {
|
||||
dirs[i] = strings.TrimSpace(dirs[i])
|
||||
}
|
||||
field.Set(reflect.ValueOf(dirs))
|
||||
}
|
||||
}
|
||||
if err = json.Unmarshal(file, &args); err != nil {
|
||||
fmt.Println("config file parse failed, reason:" + err.Error())
|
||||
return &args
|
||||
}
|
||||
fmt.Printf("config file loaded: %s\n", path)
|
||||
return &args
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+15
-17
@@ -6,12 +6,11 @@ package main
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/cmd"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/GopeedLab/gopeed/cmd"
|
||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
@@ -32,29 +31,28 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
var storageDir string
|
||||
var dir string
|
||||
if args.StorageDir != nil && *args.StorageDir != "" {
|
||||
storageDir = *args.StorageDir
|
||||
dir = *args.StorageDir
|
||||
} else {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
storageDir = filepath.Join(filepath.Dir(exe), "storage")
|
||||
dir = filepath.Dir(exe)
|
||||
}
|
||||
|
||||
cfg := &model.StartConfig{
|
||||
Network: "tcp",
|
||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||
Storage: model.StorageBolt,
|
||||
StorageDir: storageDir,
|
||||
WhiteDownloadDirs: args.WhiteDownloadDirs,
|
||||
ApiToken: *args.ApiToken,
|
||||
DownloadConfig: args.DownloadConfig,
|
||||
ProductionMode: true,
|
||||
WebEnable: true,
|
||||
WebFS: sub,
|
||||
WebAuth: webAuth,
|
||||
Network: "tcp",
|
||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||
Storage: model.StorageBolt,
|
||||
StorageDir: filepath.Join(dir, "storage"),
|
||||
ApiToken: *args.ApiToken,
|
||||
DownloadConfig: args.DownloadConfig,
|
||||
ProductionMode: true,
|
||||
WebEnable: true,
|
||||
WebFS: sub,
|
||||
WebAuth: webAuth,
|
||||
}
|
||||
cmd.Start(cfg)
|
||||
}
|
||||
|
||||
@@ -3,18 +3,17 @@ package bt
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/test"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||
)
|
||||
|
||||
func TestFetcher_Resolve_Torrent(t *testing.T) {
|
||||
|
||||
@@ -5,12 +5,11 @@ package bt
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
|
||||
"github.com/anacrolix/missinggo/v2"
|
||||
|
||||
"github.com/anacrolix/torrent/common"
|
||||
@@ -35,7 +34,7 @@ func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
|
||||
if opts.FilePathMaker == nil {
|
||||
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
|
||||
var parts []string
|
||||
if opts.Info.Length == 0 || opts.File.Path == nil {
|
||||
if opts.Info.Name != metainfo.NoName {
|
||||
parts = append(parts, opts.Info.Name)
|
||||
}
|
||||
return filepath.Join(append(parts, opts.File.Path...)...)
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"github.com/xiaoqidun/setft"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -173,13 +172,13 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
||||
filename = strings.Replace(filename, "UTF8", "UTF-8", 1)
|
||||
file.Name, _ = decoder.Decode(filename)
|
||||
} else {
|
||||
file.Name = util.TryUrlQueryUnescape(filename)
|
||||
file.Name = filename
|
||||
}
|
||||
} else {
|
||||
substr := "attachment; filename="
|
||||
index := strings.Index(contentDisposition, substr)
|
||||
if index != -1 {
|
||||
file.Name = util.TryUrlQueryUnescape(contentDisposition[index+len(substr):])
|
||||
file.Name = contentDisposition[index+len(substr):]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,20 +385,19 @@ func (f *Fetcher) run(index int, ctx context.Context) (err error) {
|
||||
resp *http.Response
|
||||
)
|
||||
f.redirectLock.Lock()
|
||||
redirectURL := f.redirectURL
|
||||
if redirectURL != "" {
|
||||
if f.redirectURL != "" {
|
||||
f.redirectLock.Unlock()
|
||||
} else {
|
||||
// Only hold the lock when we need to potentially set redirectURL
|
||||
defer func() {
|
||||
if redirectURL == "" && err == nil {
|
||||
f.redirectURL = resp.Request.URL.String()
|
||||
}
|
||||
f.redirectLock.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
err = func() (err error) {
|
||||
defer func() {
|
||||
if f.redirectURL == "" {
|
||||
if err == nil {
|
||||
f.redirectURL = resp.Request.URL.String()
|
||||
}
|
||||
f.redirectLock.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
httpReq, err = f.buildRequest(ctx, f.meta.Req)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
+14
-15
@@ -2,6 +2,15 @@ package download
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/logger"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
"math"
|
||||
gohttp "net/http"
|
||||
"net/url"
|
||||
@@ -13,16 +22,6 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/GopeedLab/gopeed/internal/controller"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/internal/logger"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -533,7 +532,7 @@ func (d *Downloader) ContinueBatch(filter *TaskFilter) (err error) {
|
||||
|
||||
func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
||||
if filter == nil || filter.IsEmpty() {
|
||||
return d.deleteAll(force)
|
||||
return d.deleteAll()
|
||||
}
|
||||
|
||||
deleteTasks := d.GetTasksByFilter(filter)
|
||||
@@ -578,7 +577,7 @@ func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) deleteAll(force bool) (err error) {
|
||||
func (d *Downloader) deleteAll() (err error) {
|
||||
var deleteTasksTemp []*Task
|
||||
func() {
|
||||
d.lock.Lock()
|
||||
@@ -592,7 +591,7 @@ func (d *Downloader) deleteAll(force bool) (err error) {
|
||||
}()
|
||||
|
||||
for _, task := range deleteTasksTemp {
|
||||
if err = d.doDelete(task, force); err != nil {
|
||||
if err = d.doDelete(task, true); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -942,9 +941,9 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
|
||||
}
|
||||
|
||||
// if enable white download directory, check if the download directory is in the white list
|
||||
if len(d.cfg.WhiteDownloadDirs) > 0 {
|
||||
if len(d.cfg.DownloadDirWhiteList) > 0 {
|
||||
inWhiteList := false
|
||||
for _, dir := range d.cfg.WhiteDownloadDirs {
|
||||
for _, dir := range d.cfg.DownloadDirWhiteList {
|
||||
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
|
||||
inWhiteList = true
|
||||
break
|
||||
|
||||
@@ -91,7 +91,7 @@ func TestDownloader_CreateNotInWhite(t *testing.T) {
|
||||
defer listener.Close()
|
||||
|
||||
downloader := NewDownloader(&DownloaderConfig{
|
||||
WhiteDownloadDirs: []string{"./downloads"},
|
||||
DownloadDirWhiteList: []string{"./downloads"},
|
||||
})
|
||||
if err := downloader.Setup(); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -131,7 +131,6 @@ type XMLHttpRequest struct {
|
||||
responseHeaders http.Header
|
||||
aborted bool
|
||||
client *req.Client
|
||||
fingerprint string
|
||||
|
||||
WithCredentials bool `json:"withCredentials"`
|
||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||
@@ -165,7 +164,7 @@ func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
|
||||
}
|
||||
|
||||
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||
setFingerprint(xhr.client, xhr.fingerprint)
|
||||
setFingerprint(xhr.client)
|
||||
|
||||
d := xhr.parseData(data)
|
||||
var (
|
||||
@@ -382,8 +381,7 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
|
||||
}
|
||||
|
||||
instance := &XMLHttpRequest{
|
||||
client: client,
|
||||
fingerprint: util.SafeGet[string](runtime, FingerprintMagicKey),
|
||||
client: client,
|
||||
Upload: &XMLHttpRequestUpload{
|
||||
EventProp: &EventProp{
|
||||
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||
@@ -397,8 +395,8 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
|
||||
instanceValue.SetPrototype(call.This.Prototype())
|
||||
return instanceValue
|
||||
})
|
||||
runtime.Set("__gopeed_setFingerprint", func(fingerprint string) {
|
||||
runtime.Set(FingerprintMagicKey, fingerprint)
|
||||
runtime.Set("__gopeed_setFingerprint", func(fingerprint Fingerprint) {
|
||||
currentFingerprint = fingerprint
|
||||
})
|
||||
if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
package xhr
|
||||
|
||||
import "github.com/imroc/req/v3"
|
||||
import (
|
||||
"github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
type Fingerprint string
|
||||
|
||||
const (
|
||||
FingerprintMagicKey = "__gopeed_xhr_fingerprint"
|
||||
|
||||
fingerprintChrome = "chrome"
|
||||
fingerprintFirefox = "firefox"
|
||||
fingerprintSafari = "safari"
|
||||
fingerprintChrome Fingerprint = "chrome"
|
||||
fingerprintFirefox Fingerprint = "firefox"
|
||||
fingerprintSafari Fingerprint = "safari"
|
||||
)
|
||||
|
||||
func setFingerprint(client *req.Client, fingerprint string) {
|
||||
switch fingerprint {
|
||||
var currentFingerprint Fingerprint = ""
|
||||
|
||||
func setFingerprint(client *req.Client) {
|
||||
switch currentFingerprint {
|
||||
case fingerprintChrome:
|
||||
client.ImpersonateChrome()
|
||||
case fingerprintFirefox:
|
||||
|
||||
@@ -22,16 +22,3 @@ func AssertError[T error](err error) (t T, r bool) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SafeGet[T any](vm *goja.Runtime, name string) T {
|
||||
v := vm.Get(name)
|
||||
if v == nil {
|
||||
var init T
|
||||
return init
|
||||
}
|
||||
if e, ok := v.Export().(T); ok {
|
||||
return e
|
||||
}
|
||||
var init T
|
||||
return init
|
||||
}
|
||||
@@ -137,10 +137,10 @@ type DownloaderConfig struct {
|
||||
Controller *controller.Controller
|
||||
FetchManagers []fetcher.FetcherManager
|
||||
|
||||
RefreshInterval int // RefreshInterval time duration to refresh task progress(ms)
|
||||
Storage Storage
|
||||
StorageDir string
|
||||
WhiteDownloadDirs []string
|
||||
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
||||
Storage Storage
|
||||
StorageDir string
|
||||
DownloadDirWhiteList []string `json:"downloadDirWhiteList"`
|
||||
|
||||
ProductionMode bool
|
||||
|
||||
|
||||
+4
-7
@@ -65,9 +65,7 @@ func Stop() {
|
||||
}()
|
||||
|
||||
if srv != nil {
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
if err := srv.Shutdown(context.TODO()); err != nil {
|
||||
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
|
||||
}
|
||||
}
|
||||
@@ -85,9 +83,8 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
||||
startCfg.Init()
|
||||
|
||||
downloadCfg := &download.DownloaderConfig{
|
||||
ProductionMode: startCfg.ProductionMode,
|
||||
RefreshInterval: startCfg.RefreshInterval,
|
||||
WhiteDownloadDirs: startCfg.WhiteDownloadDirs,
|
||||
ProductionMode: startCfg.ProductionMode,
|
||||
RefreshInterval: startCfg.RefreshInterval,
|
||||
}
|
||||
if startCfg.Storage == model.StorageBolt {
|
||||
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
|
||||
@@ -193,7 +190,7 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
||||
}
|
||||
|
||||
if enableBasicAuth {
|
||||
if !strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/api/web/login" {
|
||||
if r.URL.Path == "/api/web/login" {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -56,13 +56,3 @@ func ProxyUrlToHandler(proxyUrl *url.URL) func(*http.Request) (*url.URL, error)
|
||||
}
|
||||
return http.ProxyURL(proxyUrl)
|
||||
}
|
||||
|
||||
// TryUrlQueryUnescape tries to unescape a URL-encoded string.
|
||||
//
|
||||
// If unescaping fails, it returns the original string.
|
||||
func TryUrlQueryUnescape(s string) string {
|
||||
if decoded, err := url.QueryUnescape(s); err == nil {
|
||||
return decoded
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -128,24 +128,3 @@ func TestParseDataUri(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryURLDecode(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"normal.txt", "normal.txt"},
|
||||
{"%E7%8F%80%E5%B0%94%E8%AF%BA.zip", "珀尔诺.zip"},
|
||||
{"hello%20world.txt", "hello world.txt"},
|
||||
{"bad%2-text", "bad%2-text"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
got := TryUrlQueryUnescape(tt.input)
|
||||
if got != tt.expected {
|
||||
t.Errorf("TryUrlQueryUnescape(%q) = %q, want %q", tt.input, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,6 @@ class ExtraConfig {
|
||||
bool lastDeleteTaskKeep;
|
||||
bool defaultDirectDownload;
|
||||
bool defaultBtClient;
|
||||
bool notifyWhenNewVersion;
|
||||
|
||||
ExtraConfigBt bt = ExtraConfigBt();
|
||||
|
||||
@@ -91,7 +90,6 @@ class ExtraConfig {
|
||||
this.lastDeleteTaskKeep = false,
|
||||
this.defaultDirectDownload = false,
|
||||
this.defaultBtClient = true,
|
||||
this.notifyWhenNewVersion = true,
|
||||
});
|
||||
|
||||
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
||||
|
||||
@@ -74,7 +74,6 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
||||
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
|
||||
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
|
||||
defaultBtClient: json['defaultBtClient'] as bool? ?? true,
|
||||
notifyWhenNewVersion: json['notifyWhenNewVersion'] as bool? ?? true,
|
||||
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
|
||||
|
||||
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||
@@ -84,7 +83,6 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
|
||||
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
|
||||
'defaultDirectDownload': instance.defaultDirectDownload,
|
||||
'defaultBtClient': instance.defaultBtClient,
|
||||
'notifyWhenNewVersion': instance.notifyWhenNewVersion,
|
||||
'bt': instance.bt.toJson(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/app_controller.dart';
|
||||
|
||||
class AppBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<AppController>(
|
||||
() => AppController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/create_task.dart';
|
||||
import '../../../../api/model/downloader_config.dart';
|
||||
import '../../../../api/model/request.dart';
|
||||
import '../../../../core/common/start_config.dart';
|
||||
@@ -32,7 +31,7 @@ import '../../../../util/package_info.dart';
|
||||
import '../../../../util/updater.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import '../../../rpc/rpc.dart';
|
||||
import '../../create/dto/create_router_params.dart';
|
||||
import '../../redirect/views/redirect_view.dart';
|
||||
|
||||
const unixSocketPath = 'gopeed.sock';
|
||||
@@ -79,9 +78,6 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
_initTray().onError(
|
||||
(error, stackTrace) => logger.w("initTray error", error, stackTrace));
|
||||
|
||||
_initRpcServer().onError((error, stackTrace) =>
|
||||
logger.w("initRpcServer error", error, stackTrace));
|
||||
|
||||
_initForegroundTask().onError((error, stackTrace) =>
|
||||
logger.w("initForegroundTask error", error, stackTrace));
|
||||
|
||||
@@ -123,7 +119,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
|
||||
@override
|
||||
void onTrayIconRightMouseDown() {
|
||||
trayManager.popUpContextMenu(bringAppToFront: true);
|
||||
trayManager.popUpContextMenu();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -275,37 +271,6 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
trayManager.addListener(this);
|
||||
}
|
||||
|
||||
Future<void> _initRpcServer() async {
|
||||
if (!Util.isDesktop()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await startRpcServer({
|
||||
"/create": (ctx) async {
|
||||
final meta =
|
||||
ctx.request.headers["X-Gopeed-Host-Meta"]?.firstOrNull ?? "{}";
|
||||
final jsonMeta = jsonDecode(meta);
|
||||
final silent = jsonMeta['silent'] as bool? ?? false;
|
||||
final params = await ctx.readText();
|
||||
final createTaskParams = _decodeToCreatTaskParams(params);
|
||||
if (!silent) {
|
||||
await windowManager.show();
|
||||
_handleToCreate0(createTaskParams);
|
||||
} else {
|
||||
try {
|
||||
await createTask(createTaskParams);
|
||||
} catch (e) {
|
||||
logger.w(
|
||||
"create task from extension fail", e, StackTrace.current);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.w("start rpc server fail", e, StackTrace.current);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initForegroundTask() async {
|
||||
if (!Util.isMobile()) {
|
||||
return;
|
||||
@@ -352,7 +317,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
if (uri.path == "/create") {
|
||||
final params = uri.queryParameters["params"];
|
||||
if (params?.isNotEmpty == true) {
|
||||
_handleToCreate(params!);
|
||||
final safeParams = params!.replaceAll(" ", "+");
|
||||
final paramsJson =
|
||||
String.fromCharCodes(base64Decode(base64.normalize(safeParams)));
|
||||
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
||||
arguments: RedirectArgs(Routes.CREATE,
|
||||
arguments:
|
||||
CreateRouterParams.fromJson(jsonDecode(paramsJson))));
|
||||
return;
|
||||
}
|
||||
Get.rootDelegate.offAndToNamed(Routes.CREATE);
|
||||
@@ -375,7 +346,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
||||
arguments: RedirectArgs(Routes.CREATE,
|
||||
arguments: CreateTask(req: Request(url: path))));
|
||||
arguments: CreateRouterParams(req: Request(url: path))));
|
||||
}
|
||||
|
||||
String runningAddress() {
|
||||
@@ -535,11 +506,6 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
}
|
||||
|
||||
Future<void> _initCheckUpdate() async {
|
||||
// Check if auto check update is enabled
|
||||
if (!downloaderConfig.value.extra.notifyWhenNewVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
final versionInfo = await checkUpdate();
|
||||
if (versionInfo != null) {
|
||||
await showUpdateDialog(Get.context!, versionInfo);
|
||||
@@ -553,21 +519,4 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
apiToken: startConfig.value.apiToken));
|
||||
await putConfig(downloaderConfig.value);
|
||||
}
|
||||
|
||||
CreateTask _decodeToCreatTaskParams(String params) {
|
||||
final safeParams = params.replaceAll('"', "").replaceAll(" ", "+");
|
||||
final paramsJson =
|
||||
String.fromCharCodes(base64Decode(base64.normalize(safeParams)));
|
||||
return CreateTask.fromJson(jsonDecode(paramsJson));
|
||||
}
|
||||
|
||||
_handleToCreate(String params) {
|
||||
final createTaskParams = _decodeToCreatTaskParams(params);
|
||||
_handleToCreate0(createTaskParams);
|
||||
}
|
||||
|
||||
_handleToCreate0(CreateTask createTaskParams) {
|
||||
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
|
||||
arguments: RedirectArgs(Routes.CREATE, arguments: createTaskParams));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import '../../../../api/model/options.dart';
|
||||
import '../../../../api/model/request.dart';
|
||||
|
||||
part 'create_router_params.g.dart';
|
||||
|
||||
@JsonSerializable(explicitToJson: true)
|
||||
class CreateRouterParams {
|
||||
Request? req;
|
||||
Options? opt;
|
||||
|
||||
CreateRouterParams({
|
||||
this.req,
|
||||
this.opt,
|
||||
});
|
||||
|
||||
factory CreateRouterParams.fromJson(
|
||||
Map<String, dynamic> json,
|
||||
) =>
|
||||
_$CreateRouterParamsFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$CreateRouterParamsToJson(this);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'create_router_params.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CreateRouterParams _$CreateRouterParamsFromJson(Map<String, dynamic> json) =>
|
||||
CreateRouterParams(
|
||||
req: json['req'] == null
|
||||
? null
|
||||
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||
opt: json['opt'] == null
|
||||
? null
|
||||
: Options.fromJson(json['opt'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CreateRouterParamsToJson(CreateRouterParams instance) {
|
||||
final val = <String, dynamic>{};
|
||||
|
||||
void writeNotNull(String key, dynamic value) {
|
||||
if (value != null) {
|
||||
val[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
writeNotNull('req', instance.req?.toJson());
|
||||
writeNotNull('opt', instance.opt?.toJson());
|
||||
return val;
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import '../../../views/file_tree_view.dart';
|
||||
import '../../app/controllers/app_controller.dart';
|
||||
import '../../history/views/history_view.dart';
|
||||
import '../controllers/create_controller.dart';
|
||||
import '../dto/create_router_params.dart';
|
||||
|
||||
class CreateView extends GetView<CreateController> {
|
||||
final _confirmFormKey = GlobalKey<FormState>();
|
||||
@@ -75,7 +76,7 @@ class CreateView extends GetView<CreateController> {
|
||||
_pathController.text = appController.downloaderConfig.value.downloadDir;
|
||||
}
|
||||
|
||||
final CreateTask? routerParams = Get.rootDelegate.arguments();
|
||||
final CreateRouterParams? routerParams = Get.rootDelegate.arguments();
|
||||
if (routerParams?.req?.url.isNotEmpty ?? false) {
|
||||
// get url from route arguments
|
||||
final url = routerParams!.req!.url;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../util/network_monitor.dart';
|
||||
import '../controllers/root_controller.dart';
|
||||
|
||||
class RootBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<RootController>(() => RootController(), fenix: true);
|
||||
Get.put<NetworkMonitor>(NetworkMonitor(), permanent: true);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../database/database.dart';
|
||||
import '../../../../util/updater.dart';
|
||||
|
||||
class SettingController extends GetxController {
|
||||
final tapStatues = <String, bool>{}.obs;
|
||||
final latestVersion = Rxn<VersionInfo>();
|
||||
final networkAutoControl = false.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchLatestVersion();
|
||||
// Initialize network auto control setting
|
||||
networkAutoControl.value = Database.instance.getNetworkAutoControl();
|
||||
}
|
||||
|
||||
// set all tap status to false
|
||||
@@ -27,4 +31,10 @@ class SettingController extends GetxController {
|
||||
void fetchLatestVersion() async {
|
||||
latestVersion.value = await checkUpdate();
|
||||
}
|
||||
|
||||
// update network auto control setting
|
||||
void updateNetworkAutoControl(bool value) {
|
||||
networkAutoControl.value = value;
|
||||
Database.instance.saveNetworkAutoControl(value);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import 'package:launch_at_startup/launch_at_startup.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../../api/model/downloader_config.dart';
|
||||
import '../../../../database/database.dart';
|
||||
import '../../../../i18n/message.dart';
|
||||
import '../../../../util/input_formatter.dart';
|
||||
import '../../../../util/locale_manager.dart';
|
||||
@@ -531,25 +532,6 @@ class SettingView extends GetView<SettingController> {
|
||||
);
|
||||
}
|
||||
|
||||
final buildAutoCheckUpdate = _buildConfigItem(
|
||||
'notifyWhenNewVersion',
|
||||
() =>
|
||||
downloaderCfg.value.extra.notifyWhenNewVersion ? 'on'.tr : 'off'.tr,
|
||||
(Key key) {
|
||||
return Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Switch(
|
||||
value: downloaderCfg.value.extra.notifyWhenNewVersion,
|
||||
onChanged: (bool value) async {
|
||||
downloaderCfg.update((val) {
|
||||
val!.extra.notifyWhenNewVersion = value;
|
||||
});
|
||||
await debounceSave();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
buildThanks() {
|
||||
const thankPage =
|
||||
'https://github.com/GopeedLab/gopeed/graphs/contributors';
|
||||
@@ -562,6 +544,24 @@ class SettingView extends GetView<SettingController> {
|
||||
);
|
||||
}
|
||||
|
||||
// Network auto control setting (mobile only)
|
||||
buildNetworkAutoControl() {
|
||||
if (!Platform.isAndroid && !Platform.isIOS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Obx(() => ListTile(
|
||||
title: Text('networkAutoControl'.tr),
|
||||
subtitle: Text('networkAutoControlTip'.tr),
|
||||
trailing: Switch(
|
||||
value: controller.networkAutoControl.value,
|
||||
onChanged: (bool value) {
|
||||
controller.updateNetworkAutoControl(value);
|
||||
},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// advanced config proxy items start
|
||||
final proxy = downloaderCfg.value.proxy;
|
||||
final buildProxy = _buildConfigItem(
|
||||
@@ -976,7 +976,6 @@ class SettingView extends GetView<SettingController> {
|
||||
children: _addDivider([
|
||||
buildHomepage(),
|
||||
buildVersion(),
|
||||
buildAutoCheckUpdate(),
|
||||
buildThanks(),
|
||||
]),
|
||||
)),
|
||||
@@ -1005,7 +1004,10 @@ class SettingView extends GetView<SettingController> {
|
||||
Text('network'.tr),
|
||||
Card(
|
||||
child: Column(
|
||||
children: _addDivider([buildProxy()]),
|
||||
children: _addDivider([
|
||||
buildNetworkAutoControl(),
|
||||
buildProxy(),
|
||||
].where((e) => e != null).cast<Widget>().toList()),
|
||||
)),
|
||||
const Text('API'),
|
||||
Card(
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dart_ipc/dart_ipc.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../util/util.dart';
|
||||
|
||||
/// RPC context class that contains request information and response methods
|
||||
class RpcContext {
|
||||
final HttpRequest request;
|
||||
final HttpResponse response;
|
||||
String? _bodyCache;
|
||||
|
||||
RpcContext(this.request, this.response);
|
||||
|
||||
/// Read and parse JSON request body
|
||||
Future<Map<String, dynamic>> readJSON() async {
|
||||
if (_bodyCache == null) {
|
||||
final content = <int>[];
|
||||
await for (var data in request) {
|
||||
content.addAll(data);
|
||||
}
|
||||
_bodyCache = String.fromCharCodes(content);
|
||||
}
|
||||
|
||||
if (_bodyCache!.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return jsonDecode(_bodyCache!) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<String> readText() async {
|
||||
if (_bodyCache == null) {
|
||||
final content = <int>[];
|
||||
await for (var data in request) {
|
||||
content.addAll(data);
|
||||
}
|
||||
_bodyCache = String.fromCharCodes(content);
|
||||
}
|
||||
|
||||
return _bodyCache ?? '';
|
||||
}
|
||||
|
||||
/// Write JSON response
|
||||
Future<void> writeJSON(Map<String, dynamic> data) async {
|
||||
response.headers.contentType = ContentType.json;
|
||||
response.write(jsonEncode(data));
|
||||
await response.close();
|
||||
}
|
||||
|
||||
/// Write error response
|
||||
Future<void> writeError(String message, [int statusCode = 500]) async {
|
||||
response.statusCode = statusCode;
|
||||
await writeJSON({'error': message});
|
||||
}
|
||||
}
|
||||
|
||||
/// Route handler type definition
|
||||
typedef RouteHandler = Future<void> Function(RpcContext ctx);
|
||||
|
||||
/// Route registry
|
||||
class RouteRegistry {
|
||||
final Map<String, RouteHandler> _routes = {};
|
||||
|
||||
/// Register a route
|
||||
void register(String path, RouteHandler handler) {
|
||||
_routes[path] = handler;
|
||||
}
|
||||
|
||||
/// Get route handler
|
||||
RouteHandler? getHandler(String path) {
|
||||
return _routes[path];
|
||||
}
|
||||
|
||||
/// Get all registered routes
|
||||
List<String> get routes => _routes.keys.toList();
|
||||
}
|
||||
|
||||
/// Start RPC server
|
||||
Future<void> startRpcServer([Map<String, RouteHandler>? routes]) async {
|
||||
String path;
|
||||
if (Util.isWindows()) {
|
||||
path = r'\\.\pipe\gopeed_host';
|
||||
} else {
|
||||
path = await Util.homePathJoin("gopeed_host.sock");
|
||||
// try to delete existing socket file
|
||||
final socketFile = File(path);
|
||||
if (await socketFile.exists()) {
|
||||
try {
|
||||
await socketFile.delete();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create route registry
|
||||
final registry = RouteRegistry();
|
||||
|
||||
// Register provided routes
|
||||
if (routes != null) {
|
||||
routes.forEach((path, handler) {
|
||||
registry.register(path, handler);
|
||||
});
|
||||
}
|
||||
|
||||
final serverSocket = await bind(path);
|
||||
final httpServer = HttpServer.listenOn(serverSocket);
|
||||
|
||||
httpServer.forEach((HttpRequest request) async {
|
||||
final ctx = RpcContext(request, request.response);
|
||||
|
||||
try {
|
||||
// Get request path
|
||||
final requestPath = request.uri.path;
|
||||
// Find route handler
|
||||
final handler = registry.getHandler(requestPath);
|
||||
if (handler != null) {
|
||||
// Execute route handler
|
||||
await handler(ctx);
|
||||
} else {
|
||||
// 404 Route not found
|
||||
await ctx.writeError('Route not found: $requestPath', 404);
|
||||
}
|
||||
} catch (e) {
|
||||
await ctx.writeError('Internal server error: $e', 500);
|
||||
} finally {
|
||||
await request.response.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -10,6 +10,7 @@ const String _windowState = 'windowState';
|
||||
const String _bookmark = 'bookmark';
|
||||
const String _createHistory = 'createHistory';
|
||||
const String _webToken = 'webToken';
|
||||
const String _networkAutoControl = 'networkAutoControl';
|
||||
|
||||
class Database {
|
||||
static final Database _instance = Database._internal();
|
||||
@@ -109,4 +110,12 @@ class Database {
|
||||
void clearCreateHistory() {
|
||||
clear(_createHistory);
|
||||
}
|
||||
|
||||
void saveNetworkAutoControl(bool enabled) {
|
||||
save<bool>(_networkAutoControl, enabled);
|
||||
}
|
||||
|
||||
bool getNetworkAutoControl() {
|
||||
return get<bool>(_networkAutoControl, (json) => json as bool) ?? false;
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
const deDE = {
|
||||
'de_DE': {
|
||||
'label': 'Deutsch',
|
||||
'error': 'Fehler',
|
||||
'tip': 'Tipp',
|
||||
'confirm': 'Bestätigen',
|
||||
'cancel': 'Abbrechen',
|
||||
'on': 'An',
|
||||
'off': 'Aus',
|
||||
'selectAll': 'Alle Auswählen',
|
||||
'select': 'Auswählen',
|
||||
'task': 'Aufgaben',
|
||||
'downloading': 'Wird Heruntergeladen',
|
||||
'downloaded': 'Heruntergeladen',
|
||||
'setting': 'Einstellungen',
|
||||
'donate': 'Spenden',
|
||||
'exit': 'Beenden',
|
||||
'create': 'Aufgabe Erstellen',
|
||||
'directDownload': 'Direkter Download',
|
||||
'advancedOptions': 'Erweiterte Einstellungen',
|
||||
'followSettings': 'Einstellungen Folgen',
|
||||
'downloadLink': 'Download Link',
|
||||
'downloadLinkValid': 'Bitte geben Sie den Download-Link ein',
|
||||
'downloadLinkHit': 'Bitte geben Sie den Download-Link ein, einen pro Zeile@append',
|
||||
'downloadLinkHitDesktop': ', oder ziehen Sie die Torrent-Datei direkt hierher',
|
||||
'download': 'Download',
|
||||
'noFileSelected': 'Bitte wählen Sie mindestens eine Datei aus, um fortzufahren.',
|
||||
'noStoragePermission': 'Speicherberechtigung erforderlich',
|
||||
'selectFile': 'Datei auswählen',
|
||||
'rename': 'Umbenennen',
|
||||
'basic': 'Basic',
|
||||
'advanced': 'Fortschrittlich',
|
||||
'general': 'Allgemein',
|
||||
'downloadDir': 'Download-Verzeichnis',
|
||||
'downloadDirValid': 'Bitte wählen Sie das Download-Verzeichnis',
|
||||
'connections': 'Anschlüsse',
|
||||
'useServerCtime': 'Serverzeit für die Dateierstellung verwenden',
|
||||
'maxRunning': 'Maximale Anzahl ausgeführter Aufgaben',
|
||||
'defaultDirectDownload': 'Direkten Download standardmäßig aktivieren',
|
||||
'items': '@count Artikel',
|
||||
'subscribeTracker': 'Abonnement-Tracker',
|
||||
'subscribeFail':
|
||||
'Anmeldung fehlgeschlagen. Bitte überprüfen Sie das Netzwerk oder versuchen Sie es später erneut.',
|
||||
'update': 'Aktualisieren',
|
||||
'updateDaily': 'Tägliche Aktualisierung',
|
||||
'lastUpdate': 'Letzte Aktualisierung: @time',
|
||||
'addTracker': 'Tracker hinzufügen',
|
||||
'addTrackerHit': 'Bitte geben Sie die URL des Tracker-Servers ein, eine pro Zeile',
|
||||
'ui': 'Benutzeroberfläche',
|
||||
'theme': 'Thema',
|
||||
'themeSystem': 'System',
|
||||
'themeLight': 'Lichtmodus',
|
||||
'themeDark': 'Dunkler Modus',
|
||||
'locale': 'Sprache',
|
||||
'about': 'Über',
|
||||
'homepage': 'Homepage',
|
||||
'version': 'Version',
|
||||
'protocol': 'Protokoll',
|
||||
'port': 'Port',
|
||||
'apiToken': 'API Token',
|
||||
'notSet': 'NS',
|
||||
'set': 'SET',
|
||||
'portInUse': 'Port [@port] wird verwendet, bitte ändern Sie den Port',
|
||||
'effectAfterRestart': 'Wirkung nach Neustart',
|
||||
'developer': 'Entwickler-in',
|
||||
'logDirectory': 'Protokollverzeichnis',
|
||||
'show': 'Zeigen',
|
||||
'continue': 'Weitermachen',
|
||||
'pause': 'Pause',
|
||||
'startAll': 'Alles starten',
|
||||
'pauseAll': 'Alle pausieren',
|
||||
'deleteTask': '@count Aufgaben löschen',
|
||||
'deleteTaskTip': 'Heruntergeladene Dateien behalten',
|
||||
'delete': 'Löschen',
|
||||
'newVersionTitle': 'Entdecken Sie die neue Version @version',
|
||||
'newVersionUpdate': 'Jetzt aktualisieren',
|
||||
'newVersionLater': 'Später',
|
||||
'extensions': 'Erweiterungen',
|
||||
'extensionInstallUrl': 'Installations-URL',
|
||||
'extensionInstallSuccess': 'Erfolgreich installiert',
|
||||
'extensionUpdateSuccess': 'Erfolgreich aktualisiert',
|
||||
'extensionDelete': 'Erweiterung löschen',
|
||||
'extensionAlreadyLatest': 'Es ist bereits die neueste Version',
|
||||
'extensionFind': 'Erweiterungen suchen',
|
||||
'extensionDevelop': 'Erweiterungen entwickeln',
|
||||
'history': 'Verlauf',
|
||||
'clearHistory': 'Verlauf löschen',
|
||||
'noHistoryFound': 'Kein Verlauf gefunden',
|
||||
'serviceTitle': 'Download-Service',
|
||||
'serviceText': 'Läuft',
|
||||
'network': 'Netzwerk',
|
||||
'proxy': 'Proxy',
|
||||
'noProxy': 'Kein Proxy',
|
||||
'systemProxy': 'System-Proxy',
|
||||
'customProxy': 'Benutzerdefinierter Proxy',
|
||||
'server': 'Server',
|
||||
'username': 'Benutzername',
|
||||
'password': 'Passwort',
|
||||
'thanks': 'Danke',
|
||||
'thanksDesc':
|
||||
'Vielen Dank an alle Mitwirkenden, die beim Aufbau und der Entwicklung der Gopeed-Community geholfen haben!',
|
||||
'browserExtension': 'Browsererweiterung',
|
||||
'launchAtStartup': 'Beim Start starten',
|
||||
'seedConfig': 'Seed-Konfiguration',
|
||||
'seedKeep': 'Weiter-seeden bis manuell gestoppt',
|
||||
'seedRatio': 'Seed Verhältniss',
|
||||
'seedTime': 'Seed-Zeit (Minuten)',
|
||||
'setAsDefaultBtClient': 'Als Standard-BT-Client festlegen',
|
||||
'taskDetail': 'Aufgabendetails',
|
||||
'taskName': 'Aufgabenname',
|
||||
'taskUrl': 'Aufgaben-URL',
|
||||
'downloadPath': 'Download-Pfad',
|
||||
'skipVerifyCert': 'Zertifikatsüberprüfung überspringen',
|
||||
'name': 'Name',
|
||||
'size': 'Größe',
|
||||
'unknown': 'Unbekannt',
|
||||
'fileSelectedCount': 'Dateien: ',
|
||||
'fileSelectedSize': 'Größe: ',
|
||||
'httpHeaderName': 'Header-Name',
|
||||
'httpHeaderValue': 'Header-Wert',
|
||||
'login': 'Login',
|
||||
'username_required': 'Bitte geben Sie Ihren Benutzernamen ein',
|
||||
'password_required': 'Bitte geben Sie Ihr Passwort ein',
|
||||
'login_success': 'Anmeldung erfolgreich',
|
||||
'login_failed': 'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihren Benutzernamen und Ihr Passwort',
|
||||
'login_failed_network':
|
||||
'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihre Netzwerkverbindung',
|
||||
},
|
||||
};
|
||||
@@ -52,7 +52,6 @@ const enUS = {
|
||||
'themeLight': 'Light',
|
||||
'themeDark': 'Dark',
|
||||
'locale': 'Language',
|
||||
'notifyWhenNewVersion': 'Notify for updates',
|
||||
'about': 'About',
|
||||
'homepage': 'Homepage',
|
||||
'version': 'Version',
|
||||
@@ -126,5 +125,7 @@ const enUS = {
|
||||
'login_failed': 'Login failed, please check your username and password',
|
||||
'login_failed_network':
|
||||
'Login failed, please check your network connection',
|
||||
'networkAutoControl': 'Network Auto Control',
|
||||
'networkAutoControlTip': 'Automatically pause downloads when switching from WiFi to mobile data, and resume when switching back to WiFi',
|
||||
},
|
||||
};
|
||||
@@ -104,5 +104,7 @@ const esES = {
|
||||
'taskUrl': 'URL de la Tarea',
|
||||
'downloadPath': 'Ruta de Descarga',
|
||||
'skipVerifyCert': 'Omitir Verificación de Certificado',
|
||||
'networkAutoControl': 'Control Automático de Red',
|
||||
'networkAutoControlTip': 'Pausar automáticamente las descargas al cambiar de WiFi a datos móviles y reanudar al volver a WiFi',
|
||||
},
|
||||
};
|
||||
@@ -93,5 +93,7 @@ const frFR = {
|
||||
'browserExtension': 'Extension du navigateur',
|
||||
'launchAtStartup': 'Lancer au démarrage',
|
||||
'skipVerifyCert': 'Bypass Verifikasi Sertifikat',
|
||||
'networkAutoControl': 'Contrôle automatique du réseau',
|
||||
'networkAutoControlTip': 'Mettre automatiquement en pause les téléchargements lors du passage du WiFi aux données mobiles et reprendre lors du retour au WiFi',
|
||||
},
|
||||
};
|
||||
@@ -97,5 +97,7 @@ const itIT = {
|
||||
'browserExtension': 'Estensione del browser',
|
||||
'launchAtStartup': "Lancia all'avvio",
|
||||
'skipVerifyCert': 'Salta la verifica del certificato',
|
||||
'networkAutoControl': 'Controllo automatico della rete',
|
||||
'networkAutoControlTip': 'Mette automaticamente in pausa i download quando si passa dal WiFi ai dati mobili e riprende quando si torna al WiFi',
|
||||
},
|
||||
};
|
||||
@@ -63,5 +63,7 @@ const jaJP = {
|
||||
'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!',
|
||||
'browserExtension': 'ブラウザ拡張機能',
|
||||
'skipVerifyCert': '証明書の検証をスキップ',
|
||||
'networkAutoControl': 'ネットワーク自動制御',
|
||||
'networkAutoControlTip': 'WiFiからモバイルデータに切り替わった時に自動的にダウンロードを一時停止し、WiFiに戻った時に自動的に再開します',
|
||||
}
|
||||
};
|
||||
@@ -119,5 +119,7 @@ const ruRU = {
|
||||
'fileSelectedSize': 'Размер: ',
|
||||
'httpHeaderName': 'Название заголовка HTTP',
|
||||
'httpHeaderValue': 'Значение заголовка HTTP',
|
||||
'networkAutoControl': 'Автоматическое управление сетью',
|
||||
'networkAutoControlTip': 'Автоматически приостанавливать загрузки при переключении с WiFi на мобильные данные и возобновлять при возврате к WiFi',
|
||||
}
|
||||
};
|
||||
@@ -90,5 +90,7 @@ const viVN = {
|
||||
'Cảm ơn tất cả những người đóng góp đã giúp xây dựng và phát triển cộng đồng Gopeed!',
|
||||
'browserExtension': 'Tiện ích mở rộng trình duyệt',
|
||||
'skipVerifyCert': 'Bỏ qua xác thực chứng chỉ',
|
||||
'networkAutoControl': 'Điều khiển mạng tự động',
|
||||
'networkAutoControlTip': 'Tự động tạm dừng tải xuống khi chuyển từ WiFi sang dữ liệu di động và tiếp tục khi quay lại WiFi',
|
||||
},
|
||||
};
|
||||
@@ -51,7 +51,6 @@ const zhCN = {
|
||||
'themeLight': '明亮主题',
|
||||
'themeDark': '暗黑主题',
|
||||
'locale': '语言',
|
||||
'notifyWhenNewVersion': '有新版本时提醒',
|
||||
'about': '关于',
|
||||
'homepage': '主页',
|
||||
'version': '版本',
|
||||
@@ -123,5 +122,7 @@ const zhCN = {
|
||||
'login_success': '登录成功',
|
||||
'login_failed': '登录失败,请检查用户名和密码',
|
||||
'login_failed_network': '登录失败,请检查网络连接',
|
||||
'networkAutoControl': '网络状态自动控制',
|
||||
'networkAutoControlTip': '当网络从WiFi切换到移动数据时自动暂停下载,切换回WiFi时自动恢复下载',
|
||||
}
|
||||
};
|
||||
@@ -116,5 +116,7 @@ const zhTW = {
|
||||
'fileSelectedSize': '大小:',
|
||||
'httpHeaderName': '標頭名稱',
|
||||
'httpHeaderValue': '標頭值',
|
||||
'networkAutoControl': '網路狀態自動控制',
|
||||
'networkAutoControlTip': '當網路從WiFi切換到流動數據時自動暫停下載,切換回WiFi時自動恢復下載',
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'langs/de_de.dart';
|
||||
import 'langs/en_us.dart';
|
||||
import 'langs/fa_ir.dart';
|
||||
import 'langs/fr_fr.dart';
|
||||
@@ -42,6 +41,5 @@ class _Messages extends Translations {
|
||||
...ukUA,
|
||||
...huHU,
|
||||
...ptBR,
|
||||
...deDE
|
||||
};
|
||||
}
|
||||
@@ -24,8 +24,6 @@ class Args {
|
||||
|
||||
bool hidden = false;
|
||||
|
||||
Args();
|
||||
|
||||
Args.parse(List<String> args) {
|
||||
final parser = ArgParser();
|
||||
parser.addFlag(flagHidden);
|
||||
@@ -35,22 +33,7 @@ class Args {
|
||||
}
|
||||
|
||||
void main(List<String> arguments) async {
|
||||
Args args;
|
||||
// Parse url scheme arguments, e.g. gopeed:?hidden=true
|
||||
// TODO: macos open url handle
|
||||
// TODO: macos updater test
|
||||
if (arguments.firstOrNull?.startsWith("gopeed:") == true) {
|
||||
try {
|
||||
final uri = Uri.parse(arguments.first);
|
||||
args = Args()..hidden = uri.queryParameters["hidden"] == "true";
|
||||
} catch (e) {
|
||||
// ignore
|
||||
args = Args.parse([]);
|
||||
}
|
||||
} else {
|
||||
args = Args.parse(arguments);
|
||||
}
|
||||
|
||||
final args = Args.parse(arguments);
|
||||
await init(args);
|
||||
onStart();
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const _firefoxNativeHostsKey = r'Software\Mozilla\NativeMessagingHosts';
|
||||
|
||||
/// Install host binary for browser extension
|
||||
Future<void> doInstallHost() async {
|
||||
final hostPath = await Util.homePathJoin(_hostExecName);
|
||||
final hostPath = Util.homePathJoin(_hostExecName);
|
||||
await Util.installAsset('assets/exec/$_hostExecName', hostPath,
|
||||
executable: true);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ Future<bool> doCheckBrowserInstalled(Browser browser) async {
|
||||
Future<bool> doCheckManifestInstalled(Browser browser) async {
|
||||
if (await checkBrowserInstalled(browser) == false) return false;
|
||||
|
||||
final manifestPath = await _getManifestPath(browser);
|
||||
final manifestPath = _getManifestPath(browser);
|
||||
if (manifestPath == null) return false;
|
||||
|
||||
if (Platform.isWindows) {
|
||||
@@ -86,7 +86,7 @@ Future<void> doInstallManifest(Browser browser) async {
|
||||
if (await checkBrowserInstalled(browser) == false) return;
|
||||
if (await checkManifestInstalled(browser)) return;
|
||||
|
||||
final manifestPath = (await _getManifestPath(browser))!;
|
||||
final manifestPath = _getManifestPath(browser)!;
|
||||
final manifestContent = await _getManifestContent(browser);
|
||||
final manifestDir = path.dirname(manifestPath);
|
||||
await Directory(manifestDir).create(recursive: true);
|
||||
@@ -209,11 +209,11 @@ List<String> _getUnixExecutablePaths(Browser browser) {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _getManifestPath(Browser browser) async {
|
||||
String? _getManifestPath(Browser browser) {
|
||||
final manifestName =
|
||||
browser == Browser.firefox ? '$_hostName.moz.json' : '$_hostName.json';
|
||||
if (Platform.isWindows) {
|
||||
return await Util.homePathJoin(manifestName);
|
||||
return Util.homePathJoin(manifestName);
|
||||
}
|
||||
|
||||
final home =
|
||||
@@ -259,7 +259,7 @@ Future<bool> _checkWindowsRegistry(String keyPath) async {
|
||||
}
|
||||
|
||||
Future<String> _getManifestContent(Browser browser) async {
|
||||
final hostPath = await Util.homePathJoin(_hostExecName);
|
||||
final hostPath = Util.homePathJoin(_hostExecName);
|
||||
final manifest = {
|
||||
'name': _hostName,
|
||||
'description': 'Gopeed browser extension host',
|
||||
|
||||
@@ -2,12 +2,10 @@ import 'package:dio/dio.dart';
|
||||
|
||||
// List of mirrors for GitHub
|
||||
const _sourceMirror = [
|
||||
// github.tbedu.top: https://github.tbedu.top/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://github.tbedu.top/", r"(.*)"],
|
||||
// fastgit.cc: https://fastgit.cc/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://fastgit.cc/", r"(.*)"],
|
||||
// gitproxy.click: https://gitproxy.click/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://gitproxy.click/", r"(.*)"],
|
||||
// ghfast.top: https://ghfast.top/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://ghfast.top/", r"(.*)"],
|
||||
// github.moeyy.xyz: https://github.moeyy.xyz/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||
["https://github.moeyy.xyz/", r"(.*)"],
|
||||
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
||||
[
|
||||
"https://fastly.jsdelivr.net/gh",
|
||||
@@ -17,12 +15,10 @@ const _sourceMirror = [
|
||||
|
||||
// List of mirrors for GitHub release
|
||||
const _assertMirror = [
|
||||
// github.tbedu.top: https://https://github.tbedu.top/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://github.tbedu.top/", r"(.*)"],
|
||||
// fastgit.cc: https://fastgit.cc/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://fastgit.cc/", r"(.*)"],
|
||||
// gitproxy.click: https://gitproxy.click/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://gitproxy.click/", r"(.*)"],
|
||||
// ghfast.top: https://ghfast.top/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://ghfast.top/", r"(.*)"],
|
||||
// github.moeyy.xyz: https://github.moeyy.xyz/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
|
||||
["https://github.moeyy.xyz/", r"(.*)"],
|
||||
];
|
||||
|
||||
enum MirrorType {
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../api/api.dart';
|
||||
import '../database/database.dart';
|
||||
import '../util/log_util.dart';
|
||||
|
||||
enum NetworkType { wifi, mobile, other, none }
|
||||
|
||||
class NetworkMonitor extends GetxService {
|
||||
static NetworkMonitor get to => Get.find();
|
||||
|
||||
late final Connectivity _connectivity;
|
||||
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
|
||||
|
||||
final _isWiFiConnected = false.obs;
|
||||
bool get isWiFiConnected => _isWiFiConnected.value;
|
||||
|
||||
NetworkType _previousNetworkType = NetworkType.none;
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
Future<void> onInit() async {
|
||||
super.onInit();
|
||||
|
||||
logInfo('NetworkMonitor.onInit() called');
|
||||
|
||||
// Only monitor network on mobile platforms
|
||||
if (!Platform.isAndroid && !Platform.isIOS) {
|
||||
logInfo('NetworkMonitor: Not on mobile platform, skipping initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
_connectivity = Connectivity();
|
||||
|
||||
// Check initial connectivity state
|
||||
final initialResult = await _connectivity.checkConnectivity();
|
||||
logInfo('NetworkMonitor: Initial connectivity result: $initialResult');
|
||||
_updateNetworkStatus(initialResult);
|
||||
_previousNetworkType = _getNetworkType(initialResult);
|
||||
logInfo('NetworkMonitor: Initial network type: $_previousNetworkType');
|
||||
_isInitialized = true;
|
||||
|
||||
// Listen to connectivity changes
|
||||
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(
|
||||
_onConnectivityChanged,
|
||||
onError: (error) {
|
||||
logError('Network monitoring error: $error');
|
||||
},
|
||||
);
|
||||
|
||||
logInfo('Network monitor initialized successfully');
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_connectivitySubscription.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
NetworkType _getNetworkType(List<ConnectivityResult> result) {
|
||||
if (result.contains(ConnectivityResult.wifi)) {
|
||||
return NetworkType.wifi;
|
||||
} else if (result.contains(ConnectivityResult.mobile)) {
|
||||
return NetworkType.mobile;
|
||||
} else if (result.contains(ConnectivityResult.none)) {
|
||||
return NetworkType.none;
|
||||
} else {
|
||||
// Ethernet, Bluetooth, VPN, or other connection types
|
||||
return NetworkType.other;
|
||||
}
|
||||
}
|
||||
|
||||
void _updateNetworkStatus(List<ConnectivityResult> result) {
|
||||
final hasWiFi = result.contains(ConnectivityResult.wifi);
|
||||
_isWiFiConnected.value = hasWiFi;
|
||||
}
|
||||
|
||||
void _onConnectivityChanged(List<ConnectivityResult> result) {
|
||||
logInfo('NetworkMonitor: Connectivity changed event received: $result');
|
||||
|
||||
if (!_isInitialized) {
|
||||
logInfo('NetworkMonitor: Not initialized yet, ignoring connectivity change');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if network auto control is enabled
|
||||
final isNetworkAutoControlEnabled = Database.instance.getNetworkAutoControl();
|
||||
logInfo('NetworkMonitor: Network auto control enabled: $isNetworkAutoControlEnabled');
|
||||
if (!isNetworkAutoControlEnabled) {
|
||||
logInfo('NetworkMonitor: Network auto control disabled, ignoring connectivity change');
|
||||
return;
|
||||
}
|
||||
|
||||
_updateNetworkStatus(result);
|
||||
final currentNetworkType = _getNetworkType(result);
|
||||
|
||||
logInfo('Network changed: previous=$_previousNetworkType, current=$currentNetworkType');
|
||||
|
||||
// Only handle WiFi ↔ Mobile transitions, ignore other connection types
|
||||
if (_previousNetworkType == NetworkType.wifi && currentNetworkType == NetworkType.mobile) {
|
||||
_pauseAllDownloads();
|
||||
logInfo('Switched from WiFi to mobile data - pausing all downloads');
|
||||
} else if (_previousNetworkType == NetworkType.mobile && currentNetworkType == NetworkType.wifi) {
|
||||
_resumeAllDownloads();
|
||||
logInfo('Switched from mobile data to WiFi - resuming all downloads');
|
||||
} else {
|
||||
logInfo('Network change ignored: not a WiFi ↔ Mobile transition');
|
||||
}
|
||||
|
||||
_previousNetworkType = currentNetworkType;
|
||||
}
|
||||
|
||||
Future<void> _pauseAllDownloads() async {
|
||||
try {
|
||||
await pauseAllTasks(null);
|
||||
logInfo('Successfully paused all downloads due to network change');
|
||||
} catch (e) {
|
||||
logError('Failed to pause all downloads: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resumeAllDownloads() async {
|
||||
try {
|
||||
await continueAllTasks(null);
|
||||
logInfo('Successfully resumed all downloads due to network change');
|
||||
} catch (e) {
|
||||
logError('Failed to resume all downloads: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ final _updaterBin = "updater${Util.isWindows() ? ".exe" : ""}";
|
||||
|
||||
Future<void> installUpdater() async {
|
||||
await Util.installAsset(
|
||||
'assets/exec/$_updaterBin', await Util.homePathJoin(_updaterBin),
|
||||
'assets/exec/$_updaterBin', Util.homePathJoin(_updaterBin),
|
||||
executable: true);
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ Future<void> _update(String version, Function(int, int) onProgress) async {
|
||||
case Channel.linuxFlathub:
|
||||
case Channel.linuxSnap:
|
||||
case Channel.linuxDeb:
|
||||
final updaterPath = await Util.homePathJoin(_updaterBin);
|
||||
final updaterPath = Util.homePathJoin(_updaterBin);
|
||||
// Check the updater binary is exists
|
||||
if (!await File(updaterPath).exists()) {
|
||||
await launchUrl(
|
||||
@@ -363,8 +363,6 @@ Future<void> _update(String version, Function(int, int) onProgress) async {
|
||||
Update channel
|
||||
-asset string
|
||||
Path to the package asset
|
||||
-exeDir string
|
||||
Directory of the entry executable
|
||||
-log string
|
||||
Log file path
|
||||
*/
|
||||
@@ -375,8 +373,6 @@ Future<void> _update(String version, Function(int, int) onProgress) async {
|
||||
_channel!.name,
|
||||
"-asset",
|
||||
newVersionAssetPath,
|
||||
"-exeDir",
|
||||
path.dirname(Platform.resolvedExecutable),
|
||||
"-log",
|
||||
path.join(logsDir(), "updater.log")
|
||||
]);
|
||||
|
||||
@@ -54,8 +54,7 @@ class Util {
|
||||
static Future<void> initStorageDir() async {
|
||||
var storageDir = "";
|
||||
if (Util.isWindows()) {
|
||||
storageDir =
|
||||
path.join(File(Platform.resolvedExecutable).parent.path, "storage");
|
||||
storageDir = File(Platform.resolvedExecutable).parent.path;
|
||||
} else if (!Util.isWeb()) {
|
||||
if (Util.isLinux()) {
|
||||
storageDir = File(Platform.resolvedExecutable).parent.path;
|
||||
@@ -157,15 +156,10 @@ class Util {
|
||||
};
|
||||
}
|
||||
|
||||
static Future<String> homePathJoin(String fileName) async {
|
||||
if (Util.isWindows()) {
|
||||
final execPath = Platform.resolvedExecutable;
|
||||
final execDir = path.dirname(execPath);
|
||||
return path.join(execDir, fileName);
|
||||
}
|
||||
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
return path.join(dir.path, fileName);
|
||||
static String homePathJoin(String fileName) {
|
||||
final execPath = Platform.resolvedExecutable;
|
||||
final execDir = path.dirname(execPath);
|
||||
return path.join(execDir, fileName);
|
||||
}
|
||||
|
||||
static Future<void> installAsset(String assetPath, String targetPath,
|
||||
|
||||
+7
-16
@@ -286,14 +286,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dart_ipc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dart_ipc
|
||||
sha256: "6cad558cda5304017c1f581df4c96fd4f8e4ee212aae7bfa4357716236faa9ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1260,12 +1252,11 @@ packages:
|
||||
tray_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "packages/tray_manager"
|
||||
ref: main
|
||||
resolved-ref: "91cdbdc12517d0d832aca715542b9d0329739d5d"
|
||||
url: "https://github.com/monkeyWie/tray_manager.git"
|
||||
source: git
|
||||
version: "0.5.1"
|
||||
name: tray_manager
|
||||
sha256: bdc3ac6c36f3d12d871459e4a9822705ce5a1165a17fa837103bc842719bf3f7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1422,10 +1413,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.8.2+1
|
||||
version: 1.7.1+1
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
@@ -58,11 +58,7 @@ dependencies:
|
||||
form_builder_validators: ^11.0.0
|
||||
flutter_foreground_task: ^8.2.0
|
||||
open_filex: ^4.5.0
|
||||
tray_manager:
|
||||
git:
|
||||
url: https://github.com/monkeyWie/tray_manager.git
|
||||
ref: main
|
||||
path: packages/tray_manager
|
||||
tray_manager: ^0.2.4
|
||||
lecle_downloads_path_provider: ^0.0.2+8
|
||||
hive: ^2.2.3
|
||||
launch_at_startup: ^0.3.1
|
||||
@@ -70,6 +66,7 @@ dependencies:
|
||||
toggle_switch: ^2.3.0
|
||||
permission_handler: ^11.3.1
|
||||
device_info_plus: ^11.1.0
|
||||
connectivity_plus: ^6.1.0
|
||||
checkable_treeview: ^1.3.1
|
||||
contextmenu_plus: ^1.0.1
|
||||
contentsize_tabbarview: ^0.0.2
|
||||
@@ -82,7 +79,6 @@ dependencies:
|
||||
url: https://github.com/hui-z/flutter_install_plugin.git
|
||||
ref: cf08af829f4a4145634f8a047108f505fdbe5eaa
|
||||
flutter_svg: ^2.0.17
|
||||
dart_ipc: ^1.0.1
|
||||
dependency_overrides:
|
||||
permission_handler_windows:
|
||||
git:
|
||||
|
||||
Reference in new issue
Block a user