From 3a2b1a2326db79f0c6351396e42a0b65d8c38e28 Mon Sep 17 00:00:00 2001 From: liwei Date: Tue, 13 Aug 2024 18:11:25 +0800 Subject: [PATCH 1/4] temp --- pkg/base/model.go | 10 +------- pkg/download/model.go | 11 +------- pkg/util/json.go | 14 ++++++++++ pkg/util/json_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 pkg/util/json_test.go diff --git a/pkg/base/model.go b/pkg/base/model.go index 72f68c83e..06f483837 100644 --- a/pkg/base/model.go +++ b/pkg/base/model.go @@ -94,15 +94,7 @@ func (o *Options) InitSelectFiles(fileSize int) { } func (o *Options) Clone() *Options { - if o == nil { - return nil - } - return &Options{ - Name: o.Name, - Path: o.Path, - SelectFiles: o.SelectFiles, - Extra: o.Extra, - } + return util.DeepClone(o) } func ParseReqExtra[E any](req *Request) error { diff --git a/pkg/download/model.go b/pkg/download/model.go index 0ec58fda0..59bdf3aeb 100644 --- a/pkg/download/model.go +++ b/pkg/download/model.go @@ -55,16 +55,7 @@ func (t *Task) updateStatus(status base.Status) { } func (t *Task) clone() *Task { - return &Task{ - ID: t.ID, - Protocol: t.Protocol, - Meta: t.Meta, - Status: t.Status, - Uploading: t.Uploading, - Progress: t.Progress, - CreatedAt: t.CreatedAt, - UpdatedAt: t.UpdatedAt, - } + return util.DeepClone(t) } func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 { diff --git a/pkg/util/json.go b/pkg/util/json.go index 081feecc5..b4b7dc88f 100644 --- a/pkg/util/json.go +++ b/pkg/util/json.go @@ -12,3 +12,17 @@ func MapToStruct(s any, v any) error { } return json.Unmarshal(b, v) } + +func DeepClone[T any](v *T) *T { + if v == nil { + return nil + } + + var t T + b, err := json.Marshal(v) + if err != nil { + return &t + } + json.Unmarshal(b, &t) + return &t +} diff --git a/pkg/util/json_test.go b/pkg/util/json_test.go new file mode 100644 index 000000000..83b335453 --- /dev/null +++ b/pkg/util/json_test.go @@ -0,0 +1,60 @@ +package util + +import ( + "reflect" + "testing" +) + +func TestDeepClone(t *testing.T) { + type user struct { + Name string `json:"name"` + Age int `json:"age"` + + v int + } + + type args[T any] struct { + v *T + } + type testCase[T any] struct { + name string + args args[T] + want *T + } + tests := []testCase[user]{ + { + name: "case 1", + args: args[user]{ + v: &user{ + Name: "test", + Age: 10, + }, + }, + want: &user{ + Name: "test", + Age: 10, + }, + }, + { + name: "case 2", + args: args[user]{ + v: &user{ + Name: "test", + Age: 10, + v: 1, + }, + }, + want: &user{ + Name: "test", + Age: 10, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := DeepClone(tt.args.v); !reflect.DeepEqual(got, tt.want) { + t.Errorf("DeepClone() = %v, want %v", got, tt.want) + } + }) + } +} From 6beefc4839ccecdc0d520020128a4f492f3e61f9 Mon Sep 17 00:00:00 2001 From: liwei Date: Thu, 15 Aug 2024 18:45:57 +0800 Subject: [PATCH 2/4] feat: support proxy for each task --- internal/controller/controller.go | 7 +++- internal/protocol/bt/fetcher.go | 10 ++--- internal/protocol/bt/fetcher_test.go | 6 ++- internal/protocol/http/fetcher.go | 4 +- internal/protocol/http/fetcher_test.go | 12 ++++-- pkg/base/model.go | 30 +++++++++++++++ pkg/download/downloader.go | 17 ++++++++- pkg/download/downloader_test.go | 53 ++++++++++++++++++++------ ui/flutter/lib/main.dart | 3 +- 9 files changed, 114 insertions(+), 28 deletions(-) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 08136fa8e..f2c0d80d6 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -2,13 +2,15 @@ package controller import ( "github.com/GopeedLab/gopeed/pkg/base" + "net/http" + "net/url" "os" "path/filepath" ) type Controller struct { - GetConfig func(v any) - ProxyConfig *base.DownloaderProxyConfig + GetConfig func(v any) + GetProxy func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) FileController //ContextDialer() (proxy.Dialer, error) } @@ -23,6 +25,7 @@ type DefaultFileController struct { func NewController() *Controller { return &Controller{ GetConfig: func(v any) {}, + GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil }, FileController: &DefaultFileController{}, } } diff --git a/internal/protocol/bt/fetcher.go b/internal/protocol/bt/fetcher.go index 1b2796c72..a090445b6 100644 --- a/internal/protocol/bt/fetcher.go +++ b/internal/protocol/bt/fetcher.go @@ -73,7 +73,7 @@ func (f *Fetcher) initClient() (err error) { cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20()) cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version) cfg.ListenPort = f.config.ListenPort - cfg.HTTPProxy = f.ctl.ProxyConfig.ToHandler() + cfg.HTTPProxy = f.ctl.GetProxy(f.meta.Req.Proxy) cfg.DefaultStorage = newFileOpts(newFileClientOpts{ ClientBaseDir: cfg.DataDir, HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) { @@ -98,11 +98,14 @@ func (f *Fetcher) initClient() (err error) { } func (f *Fetcher) Resolve(req *base.Request) error { + if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil { + return err + } + f.meta.Req = req if err := f.addTorrent(req, false); err != nil { return err } f.updateRes() - f.meta.Req = req return nil } @@ -339,9 +342,6 @@ func (f *Fetcher) WaitUpload() (err error) { } func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) { - if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil { - return - } if err = f.initClient(); err != nil { return } diff --git a/internal/protocol/bt/fetcher_test.go b/internal/protocol/bt/fetcher_test.go index be20321af..012c4134a 100644 --- a/internal/protocol/bt/fetcher_test.go +++ b/internal/protocol/bt/fetcher_test.go @@ -8,6 +8,8 @@ import ( "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" "reflect" "testing" @@ -210,7 +212,9 @@ func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher newController.GetConfig = func(v any) { json.Unmarshal([]byte(test.ToJson(mockCfg)), v) } - newController.ProxyConfig = proxyConfig + newController.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) { + return proxyConfig.ToHandler() + } fetcher.Setup(newController) return fetcher } diff --git a/internal/protocol/http/fetcher.go b/internal/protocol/http/fetcher.go index 3647c2c4b..399ea01c2 100644 --- a/internal/protocol/http/fetcher.go +++ b/internal/protocol/http/fetcher.go @@ -79,6 +79,7 @@ func (f *Fetcher) Resolve(req *base.Request) error { if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil { return err } + f.meta.Req = req httpReq, err := f.buildRequest(nil, req) if err != nil { return err @@ -153,7 +154,6 @@ func (f *Fetcher) Resolve(req *base.Request) error { file.Name = httpReq.URL.Hostname() } res.Files = append(res.Files, file) - f.meta.Req = req f.meta.Res = res return nil } @@ -444,7 +444,7 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) { func (f *Fetcher) buildClient() *http.Client { transport := &http.Transport{ - Proxy: f.ctl.ProxyConfig.ToHandler(), + Proxy: f.ctl.GetProxy(f.meta.Req.Proxy), } // Cookie handle jar, _ := cookiejar.New(nil) diff --git a/internal/protocol/http/fetcher_test.go b/internal/protocol/http/fetcher_test.go index d1507adfe..8109cbb3d 100644 --- a/internal/protocol/http/fetcher_test.go +++ b/internal/protocol/http/fetcher_test.go @@ -9,6 +9,8 @@ import ( "github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/protocol/http" "net" + gohttp "net/http" + "net/url" "testing" "time" ) @@ -385,10 +387,12 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) { func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) { fetcher := downloadReady(httpListener, 4, t) ctl := controller.NewController() - ctl.ProxyConfig = &base.DownloaderProxyConfig{ - Enable: true, - Scheme: "socks5", - Host: proxyListener.Addr().String(), + ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) { + return (&base.DownloaderProxyConfig{ + Enable: true, + Scheme: "socks5", + Host: proxyListener.Addr().String(), + }).ToHandler() } fetcher.Setup(ctl) err := fetcher.Start() diff --git a/pkg/base/model.go b/pkg/base/model.go index 06f483837..812a0df2f 100644 --- a/pkg/base/model.go +++ b/pkg/base/model.go @@ -16,6 +16,8 @@ type Request struct { Extra any `json:"extra"` // Labels is used to mark the download task Labels map[string]string `json:"labels"` + // Proxy is special proxy config for request + Proxy *RequestProxy `json:"proxy"` } func (r *Request) Validate() error { @@ -25,6 +27,34 @@ func (r *Request) Validate() error { return nil } +type RequestProxyMode int + +const ( + RequestProxyModeNone RequestProxyMode = iota + RequestProxyModeGlobal + RequestProxyModeCustom +) + +type RequestProxy struct { + Mode RequestProxyMode `json:"mode"` + Scheme string `json:"scheme"` + Host string `json:"host"` + Usr string `json:"usr"` + Pwd string `json:"pwd"` +} + +func (p *RequestProxy) ToHandler() func(r *http.Request) (*url.URL, error) { + if p == nil || p.Mode != RequestProxyModeCustom { + return nil + } + + if p.Scheme == "" || p.Host == "" { + return nil + } + + return http.ProxyURL(util.BuildProxyUrl(p.Scheme, p.Host, p.Usr, p.Pwd)) +} + // Resource download resource type Resource struct { // if name is not empty, the resource is a folder and the name is the folder name diff --git a/pkg/download/downloader.go b/pkg/download/downloader.go index 8bb2c7f49..c8eef3f84 100644 --- a/pkg/download/downloader.go +++ b/pkg/download/downloader.go @@ -12,6 +12,8 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/pkgerrors" "github.com/virtuald/go-paniclog" + gohttp "net/http" + "net/url" "os" "path/filepath" "sort" @@ -253,7 +255,20 @@ func (d *Downloader) setupFetcher(fm fetcher.FetcherManager, fetcher fetcher.Fet ctl.GetConfig = func(v any) { d.getProtocolConfig(fm.Name(), v) } - ctl.ProxyConfig = d.cfg.Proxy + // Get proxy config, task request proxy config has higher priority, then use global proxy config + ctl.GetProxy = func(requestProxy *base.RequestProxy) func(*gohttp.Request) (*url.URL, error) { + if requestProxy == nil { + return d.cfg.Proxy.ToHandler() + } + switch requestProxy.Mode { + case base.RequestProxyModeNone: + return nil + case base.RequestProxyModeCustom: + return requestProxy.ToHandler() + default: + return d.cfg.Proxy.ToHandler() + } + } fetcher.Setup(ctl) } diff --git a/pkg/download/downloader_test.go b/pkg/download/downloader_test.go index 2e8976d8a..3c7c59dcb 100644 --- a/pkg/download/downloader_test.go +++ b/pkg/download/downloader_test.go @@ -159,21 +159,21 @@ func TestDownloader_CreateDirectBatch(t *testing.T) { func TestDownloader_CreateWithProxy(t *testing.T) { // No proxy - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { return nil }, nil) // Disable proxy - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { proxyCfg.Enable = false return proxyCfg }, nil) // Enable system proxy but not set proxy environment variable - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { proxyCfg.System = true return proxyCfg }, nil) // Enable proxy but error proxy environment variable - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234") os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234") proxyCfg.System = true @@ -184,33 +184,50 @@ func TestDownloader_CreateWithProxy(t *testing.T) { } }) // Enable system proxy and set proxy environment variable - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String()) os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String()) proxyCfg.System = true return proxyCfg }, nil) // Invalid proxy scheme - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { proxyCfg.Scheme = "" return proxyCfg }, nil) // Invalid proxy host - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { proxyCfg.Host = "" return proxyCfg }, nil) // Use proxy without auth - doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, false, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { return proxyCfg }, nil) // Use proxy with auth - doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { + doTestDownloaderCreateWithProxy(t, true, nil, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig { return proxyCfg }, nil) + + // Request proxy mode none + doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy { + reqProxy.Mode = base.RequestProxyModeNone + return reqProxy + }, nil, nil) + + // Request proxy mode global + doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy { + reqProxy.Mode = base.RequestProxyModeGlobal + return reqProxy + }, nil, nil) + + // Request proxy mode custom + doTestDownloaderCreateWithProxy(t, false, func(reqProxy *base.RequestProxy) *base.RequestProxy { + return reqProxy + }, nil, nil) } -func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) { +func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildReqProxy func(reqProxy *base.RequestProxy) *base.RequestProxy, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) { usr, pwd := "", "" if auth { usr, pwd = "admin", "123" @@ -223,17 +240,29 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f t.Fatal(err) } defer downloader.Clear() - downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&base.DownloaderProxyConfig{ + globalProxyCfg := &base.DownloaderProxyConfig{ Enable: true, Scheme: "socks5", Host: proxyListener.Addr().String(), Usr: usr, Pwd: pwd, - }) + } + if buildProxyConfig != nil { + globalProxyCfg = buildProxyConfig(globalProxyCfg) + } + downloader.cfg.DownloaderStoreConfig.Proxy = globalProxyCfg req := &base.Request{ URL: test.ExternalDownloadUrl, } + if buildReqProxy != nil { + req.Proxy = buildReqProxy(&base.RequestProxy{ + Scheme: "socks5", + Host: proxyListener.Addr().String(), + Usr: usr, + Pwd: pwd, + }) + } rr, err := downloader.Resolve(req) if err != nil { if errHandler == nil { diff --git a/ui/flutter/lib/main.dart b/ui/flutter/lib/main.dart index 06b14f4f8..1d47960fd 100644 --- a/ui/flutter/lib/main.dart +++ b/ui/flutter/lib/main.dart @@ -75,7 +75,8 @@ Future init(Args args) async { try { await controller.loadStartConfig(); final startCfg = controller.startConfig.value; - controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg); + // controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg); + controller.runningPort.value = 9999; api.init(startCfg.network, controller.runningAddress(), startCfg.apiToken); } catch (e) { logger.e("libgopeed init fail", e); From d4b94ca749027761d7817b23d55679d6fedbd997 Mon Sep 17 00:00:00 2001 From: liwei Date: Fri, 16 Aug 2024 18:38:33 +0800 Subject: [PATCH 3/4] temp --- ui/flutter/android/app/build.gradle | 2 +- ui/flutter/android/app/src/main/AndroidManifest.xml | 5 ++++- ui/flutter/lib/main.dart | 3 +-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/flutter/android/app/build.gradle b/ui/flutter/android/app/build.gradle index cb45fcc13..4ccca1848 100644 --- a/ui/flutter/android/app/build.gradle +++ b/ui/flutter/android/app/build.gradle @@ -48,7 +48,7 @@ android { // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/ui/flutter/android/app/src/main/AndroidManifest.xml b/ui/flutter/android/app/src/main/AndroidManifest.xml index 721bb3037..6e4a54e8d 100644 --- a/ui/flutter/android/app/src/main/AndroidManifest.xml +++ b/ui/flutter/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ package="com.gopeed"> + + + android:foregroundServiceType="dataSync" + android:exported="false" /> diff --git a/ui/flutter/lib/main.dart b/ui/flutter/lib/main.dart index 1d47960fd..06b14f4f8 100644 --- a/ui/flutter/lib/main.dart +++ b/ui/flutter/lib/main.dart @@ -75,8 +75,7 @@ Future init(Args args) async { try { await controller.loadStartConfig(); final startCfg = controller.startConfig.value; - // controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg); - controller.runningPort.value = 9999; + controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg); api.init(startCfg.network, controller.runningAddress(), startCfg.apiToken); } catch (e) { logger.e("libgopeed init fail", e); From bfd92d52582be5d19139b18597cf9873adb377f7 Mon Sep 17 00:00:00 2001 From: liwei Date: Tue, 20 Aug 2024 10:37:11 +0800 Subject: [PATCH 4/4] temp --- .../app/controllers/app_controller.dart | 36 +++++++++++++++++++ ui/flutter/lib/main.dart | 2 ++ 2 files changed, 38 insertions(+) diff --git a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart index f6afe3613..f61a9d279 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -268,6 +268,8 @@ class AppController extends GetxController with WindowListener, TrayListener { return; } + await _requestPermissions(); + FlutterForegroundTask.init( androidNotificationOptions: AndroidNotificationOptions( channelId: 'gopeed_service', @@ -304,6 +306,40 @@ class AppController extends GetxController with WindowListener, TrayListener { } } + Future _requestPermissions() async { + // Android 13+, you need to allow notification permission to display foreground service notification. + // + // iOS: If you need notification, ask for permission. + final NotificationPermission notificationPermissionStatus = + await FlutterForegroundTask.checkNotificationPermission(); + if (notificationPermissionStatus != NotificationPermission.granted) { + await FlutterForegroundTask.requestNotificationPermission(); + } + + if (Platform.isAndroid) { + // "android.permission.SYSTEM_ALERT_WINDOW" permission must be granted for + // onNotificationPressed function to be called. + // + // When the notification is pressed while permission is denied, + // the onNotificationPressed function is not called and the app opens. + // + // If you do not use the onNotificationPressed or launchApp function, + // you do not need to write this code. + if (!await FlutterForegroundTask.canDrawOverlays) { + // This function requires `android.permission.SYSTEM_ALERT_WINDOW` permission. + await FlutterForegroundTask.openSystemAlertWindowSettings(); + } + + // Android 12+, there are restrictions on starting a foreground service. + // + // To restart the service on device reboot or unexpected problem, you need to allow below permission. + if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) { + // This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission. + await FlutterForegroundTask.requestIgnoreBatteryOptimization(); + } + } + } + Future _toCreate(Uri uri) async { final path = (uri.scheme == "magnet" || uri.scheme == "http" || diff --git a/ui/flutter/lib/main.dart b/ui/flutter/lib/main.dart index 06b14f4f8..e0e72f3ff 100644 --- a/ui/flutter/lib/main.dart +++ b/ui/flutter/lib/main.dart @@ -1,6 +1,7 @@ import 'package:args/args.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; @@ -39,6 +40,7 @@ void main(List arguments) async { Future init(Args args) async { WidgetsFlutterBinding.ensureInitialized(); + FlutterForegroundTask.initCommunicationPort(); await Util.initStorageDir(); await Database.instance.init(); if (Util.isDesktop()) {