Compare commits

...
8 Commits
55 changed files with 1176 additions and 395 deletions

No files matched your search

+10 -8
View File
@@ -4,17 +4,17 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
platform: platform:
description: 'Build platform' description: "Build platform"
required: true required: true
default: 'all' default: "all"
test: test:
description: 'Test mode' description: "Test mode"
required: true required: true
default: "false" default: "false"
env: env:
GO_VERSION: "1.23" GO_VERSION: "1.23"
FLUTTER_VERSION: "3.16.8" FLUTTER_VERSION: "3.24.5"
permissions: permissions:
contents: write contents: write
@@ -97,11 +97,13 @@ jobs:
PrivilegesRequired=lowest PrivilegesRequired=lowest
OutputBaseFilename=gopeed OutputBaseFilename=gopeed
SetupIconFile=..\..\assets\icon\icon.ico SetupIconFile=..\..\assets\icon\icon.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
Compression=lzma Compression=lzma
SolidCompression=yes SolidCompression=yes
WizardStyle=modern WizardStyle=modern
LanguageDetectionMethod=uilanguage LanguageDetectionMethod=uilanguage
ShowLanguageDialog=yes ShowLanguageDialog=yes
CloseApplications=force
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -111,7 +113,7 @@ jobs:
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files] [Files]
Source: ".\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs Source: ".\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs;
; NOTE: Don't use "Flags: ignoreversion" on any shared system files ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[UninstallDelete] [UninstallDelete]
@@ -366,7 +368,7 @@ jobs:
- uses: actions/setup-java@v3 - uses: actions/setup-java@v3
with: with:
distribution: "zulu" distribution: "zulu"
java-version: "11" java-version: "17"
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -443,7 +445,7 @@ jobs:
run: | run: |
VERSION=${VERSION:-$(date +%y%m%d%H%M)} # Use current time as version if not specified VERSION=${VERSION:-$(date +%y%m%d%H%M)} # Use current time as version if not specified
cd ui/flutter cd ui/flutter
flutter build web --web-renderer html flutter build web --no-web-resources-cdn
cd ../../ cd ../../
cp -r ui/flutter/build/web cmd/web/dist cp -r ui/flutter/build/web cmd/web/dist
mkdir -p dist/zip mkdir -p dist/zip
@@ -533,7 +535,7 @@ jobs:
- name: Build flutter web - name: Build flutter web
run: | run: |
cd ui/flutter cd ui/flutter
flutter build web --web-renderer html flutter build web --no-web-resources-cdn
cd ../../ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist cp -r ui/flutter/build/web cmd/web/dist
+6 -6
View File
@@ -28,7 +28,7 @@ on:
env: env:
GO_VERSION: "1.23" GO_VERSION: "1.23"
FLUTTER_VERSION: "3.16.8" FLUTTER_VERSION: "3.24.5"
jobs: jobs:
# lint: # lint:
@@ -73,17 +73,17 @@ jobs:
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- if: runner.os == 'windows' - if: runner.os == 'windows-latest'
run: | run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build windows flutter build windows
- if: runner.os == 'macOS' - if: runner.os == 'macos-latest'
run: | run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build macos flutter build macos
- if: runner.os == 'Linux' - if: runner.os == 'ubuntu-latest'
run: | run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
@@ -140,7 +140,7 @@ jobs:
- name: Build - name: Build
run: | run: |
cd ui/flutter cd ui/flutter
flutter build web --web-renderer html flutter build web --no-web-resources-cdn
cd ../../ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist cp -r ui/flutter/build/web cmd/web/dist
@@ -160,7 +160,7 @@ jobs:
- name: Build - name: Build
run: | run: |
cd ui/flutter cd ui/flutter
flutter build web --web-renderer html flutter build web --no-web-resources-cdn
cd ../../ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist cp -r ui/flutter/build/web cmd/web/dist
+6 -1
View File
@@ -71,6 +71,11 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
<td><code>IPA</code></td> <td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
</tr> </tr>
<tr>
<td>Qnap</td>
<td><code>QPKG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">Link</a></td>
</tr>
<tr> <tr>
<td>Web</td> <td>Web</td>
<td></td> <td></td>
@@ -115,7 +120,7 @@ This project is divided into two parts, the front end uses `flutter`, the back e
### Environment ### Environment
1. Golang 1.23+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.24+
### Clone ### Clone
+6 -1
View File
@@ -71,6 +71,11 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
<td><code>IPA</code></td> <td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
</tr> </tr>
<tr>
<td>Qnap</td>
<td><code>QPKG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">Link</a></td>
</tr>
<tr> <tr>
<td>Web</td> <td>Web</td>
<td></td> <td></td>
@@ -103,7 +108,7 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
### 環境 ### 環境
1. Go 言語 1.23+ 1. Go 言語 1.23+
2. Flutter 3.16+ 2. Flutter 3.24+
### クローン ### クローン
+6 -1
View File
@@ -71,6 +71,11 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
<td><code>IPA</code></td> <td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Liên kết</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Liên kết</a></td>
</tr> </tr>
<tr>
<td>Qnap</td>
<td><code>QPKG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">Liên kết</a></td>
</tr>
<tr> <tr>
<td>Web</td> <td>Web</td>
<td></td> <td></td>
@@ -111,7 +116,7 @@ Dự án này được chia thành hai phần, phần giao diện sử dụng `f
### Environment ### Environment
1. Golang 1.23+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.24+
### Clone ### Clone
+6 -1
View File
@@ -71,6 +71,11 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
<td><code>IPA</code></td> <td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
</tr> </tr>
<tr>
<td>Qnap</td>
<td><code>QPKG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">前往</a></td>
</tr>
<tr> <tr>
<td>Web</td> <td>Web</td>
<td></td> <td></td>
@@ -115,7 +120,7 @@ Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、
### 环境要求 ### 环境要求
1. Golang 1.23+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.24+
### 克隆项目 ### 克隆项目
+6 -1
View File
@@ -71,6 +71,11 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
<td><code>IPA</code></td> <td><code>IPA</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
</tr> </tr>
<tr>
<td>Qnap</td>
<td><code>QPKG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">前往</a></td>
</tr>
<tr> <tr>
<td>Web</td> <td>Web</td>
<td></td> <td></td>
@@ -111,7 +116,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
### 開發環境 ### 開發環境
1. Golang 1.23+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.24+
### 克隆項目 ### 克隆項目
+8 -1
View File
@@ -363,7 +363,14 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
f.torrent, err = client.AddMagnet(req.URL) f.torrent, err = client.AddMagnet(req.URL)
} else { } else {
var reader io.Reader var reader io.Reader
if schema == "DATA" { if schema == "FILE" {
fileUrl, _ := url.Parse(req.URL)
filePath := fileUrl.Path[1:]
reader, err = os.Open(filePath)
if err != nil {
return
}
} else if schema == "DATA" {
_, data := util.ParseDataUri(req.URL) _, data := util.ParseDataUri(req.URL)
reader = bytes.NewBuffer(data) reader = bytes.NewBuffer(data)
} else { } else {
+11
View File
@@ -11,6 +11,7 @@ import (
gohttp "net/http" gohttp "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath"
"reflect" "reflect"
"testing" "testing"
) )
@@ -149,6 +150,16 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
} }
}) })
t.Run("Resolve file scheme Torrent", func(t *testing.T) {
file, _ := filepath.Abs("./testdata/test.unclean.torrent")
uri := "file:///" + file
err := fetcher.Resolve(&base.Request{
URL: uri,
})
if err != nil {
t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err)
}
})
} }
func TestFetcherManager_ParseName(t *testing.T) { func TestFetcherManager_ParseName(t *testing.T) {
+6 -2
View File
@@ -107,9 +107,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) { if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
// response 206 status code, support breakpoint continuation // response 206 status code, support breakpoint continuation
res.Range = true res.Range = true
// parse content length from Content-Range header, eg: bytes 0-1000/1001 // parse content length from Content-Range header, eg: bytes 0-1000/1001 or bytes 0-0/*
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange)) contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
if contentTotal != "" { if contentTotal != "" && contentTotal != "*" {
parse, err := strconv.ParseInt(contentTotal, 10, 64) parse, err := strconv.ParseInt(contentTotal, 10, 64)
if err != nil { if err != nil {
return err return err
@@ -427,6 +427,10 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
return return
} }
httpReq.Header = headers httpReq.Header = headers
// Override Host header
if host := headers.Get(base.HttpHeaderHost); host != "" {
httpReq.Host = host
}
return httpReq, nil return httpReq, nil
} }
+16
View File
@@ -11,6 +11,7 @@ import (
"net" "net"
gohttp "net/http" gohttp "net/http"
"net/url" "net/url"
"strings"
"testing" "testing"
"time" "time"
) )
@@ -59,6 +60,21 @@ func TestFetcher_Resolve(t *testing.T) {
} }
} }
func TestFetcher_ResolveWithHostHeader(t *testing.T) {
fetcher := buildFetcher()
err := fetcher.Resolve(&base.Request{
URL: "https://bing.com",
Extra: &http.ReqExtra{
Header: map[string]string{
"Host": "test",
},
},
})
if err == nil || !strings.Contains(err.Error(), "400") {
t.Errorf("Resolve() got = %v, want %v", err, "400")
}
}
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) { func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
listener := startTestServer() listener := startTestServer()
defer listener.Close() defer listener.Close()
+1
View File
@@ -15,6 +15,7 @@ const (
HttpCodeOK = 200 HttpCodeOK = 200
HttpCodePartialContent = 206 HttpCodePartialContent = 206
HttpHeaderHost = "Host"
HttpHeaderRange = "Range" HttpHeaderRange = "Range"
HttpHeaderAcceptRanges = "Accept-Ranges" HttpHeaderAcceptRanges = "Accept-Ranges"
HttpHeaderContentLength = "Content-Length" HttpHeaderContentLength = "Content-Length"
-1
View File
@@ -10,7 +10,6 @@ import (
func ParseSchema(url string) string { func ParseSchema(url string) string {
index := strings.Index(url, ":") index := strings.Index(url, ":")
// if no schema or windows path like C:\a.txt, return FILE
if index == -1 || index == 1 { if index == -1 || index == 1 {
return "" return ""
} }
@@ -80,6 +80,7 @@ class ExtraConfig {
String locale; String locale;
bool lastDeleteTaskKeep; bool lastDeleteTaskKeep;
bool defaultDirectDownload; bool defaultDirectDownload;
bool defaultBtClient;
ExtraConfigBt bt = ExtraConfigBt(); ExtraConfigBt bt = ExtraConfigBt();
@@ -88,6 +89,7 @@ class ExtraConfig {
this.locale = '', this.locale = '',
this.lastDeleteTaskKeep = false, this.lastDeleteTaskKeep = false,
this.defaultDirectDownload = false, this.defaultDirectDownload = false,
this.defaultBtClient = true,
}); });
factory ExtraConfig.fromJson(Map<String, dynamic>? json) => factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
@@ -73,6 +73,7 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
locale: json['locale'] as String? ?? '', locale: json['locale'] as String? ?? '',
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false, lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false, defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
defaultBtClient: json['defaultBtClient'] as bool? ?? true,
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>); )..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) => Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
@@ -81,6 +82,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
'locale': instance.locale, 'locale': instance.locale,
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep, 'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
'defaultDirectDownload': instance.defaultDirectDownload, 'defaultDirectDownload': instance.defaultDirectDownload,
'defaultBtClient': instance.defaultBtClient,
'bt': instance.bt.toJson(), 'bt': instance.bt.toJson(),
}; };
+9 -5
View File
@@ -10,8 +10,8 @@ class Options {
Object? extra; Object? extra;
Options({ Options({
required this.name, this.name = '',
required this.path, this.path = '',
this.selectFiles = const [], this.selectFiles = const [],
this.extra, this.extra,
}); });
@@ -24,12 +24,16 @@ class Options {
@JsonSerializable() @JsonSerializable()
class OptsExtraHttp { class OptsExtraHttp {
int connections = 0; int connections;
bool autoTorrent = false; bool autoTorrent;
OptsExtraHttp(); OptsExtraHttp({
this.connections = 0,
this.autoTorrent = false,
});
factory OptsExtraHttp.fromJson(Map<String, dynamic> json) => factory OptsExtraHttp.fromJson(Map<String, dynamic> json) =>
_$OptsExtraHttpFromJson(json); _$OptsExtraHttpFromJson(json);
Map<String, dynamic> toJson() => _$OptsExtraHttpToJson(this); Map<String, dynamic> toJson() => _$OptsExtraHttpToJson(this);
} }
+7 -6
View File
@@ -7,10 +7,10 @@ part of 'options.dart';
// ************************************************************************** // **************************************************************************
Options _$OptionsFromJson(Map<String, dynamic> json) => Options( Options _$OptionsFromJson(Map<String, dynamic> json) => Options(
name: json['name'] as String, name: json['name'] as String? ?? '',
path: json['path'] as String, path: json['path'] as String? ?? '',
selectFiles: (json['selectFiles'] as List<dynamic>?) selectFiles: (json['selectFiles'] as List<dynamic>?)
?.map((e) => e as int) ?.map((e) => (e as num).toInt())
.toList() ?? .toList() ??
const [], const [],
extra: json['extra'], extra: json['extra'],
@@ -34,9 +34,10 @@ Map<String, dynamic> _$OptionsToJson(Options instance) {
} }
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) => OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
OptsExtraHttp() OptsExtraHttp(
..connections = json['connections'] as int connections: (json['connections'] as num?)?.toInt() ?? 0,
..autoTorrent = json['autoTorrent'] as bool; autoTorrent: json['autoTorrent'] as bool? ?? false,
);
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) => Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
<String, dynamic>{ <String, dynamic>{
+27 -14
View File
@@ -6,9 +6,9 @@ part 'request.g.dart';
class Request { class Request {
String url; String url;
Object? extra; Object? extra;
Map<String, String>? labels = {}; Map<String, String>? labels;
RequestProxy? proxy; RequestProxy? proxy;
bool skipVerifyCert = false; bool skipVerifyCert;
Request({ Request({
required this.url, required this.url,
@@ -26,11 +26,15 @@ class Request {
@JsonSerializable() @JsonSerializable()
class ReqExtraHttp { class ReqExtraHttp {
String method = 'GET'; String method;
Map<String, String> header = {}; Map<String, String> header;
String body = ''; String body;
ReqExtraHttp(); ReqExtraHttp({
this.method = 'GET',
this.header = const {},
this.body = '',
});
factory ReqExtraHttp.fromJson(Map<String, dynamic> json) => factory ReqExtraHttp.fromJson(Map<String, dynamic> json) =>
_$ReqExtraHttpFromJson(json); _$ReqExtraHttpFromJson(json);
@@ -40,9 +44,11 @@ class ReqExtraHttp {
@JsonSerializable() @JsonSerializable()
class ReqExtraBt { class ReqExtraBt {
List<String> trackers = []; List<String> trackers;
ReqExtraBt(); ReqExtraBt({
this.trackers = const [],
});
factory ReqExtraBt.fromJson(Map<String, dynamic> json) => factory ReqExtraBt.fromJson(Map<String, dynamic> json) =>
_$ReqExtraBtFromJson(json); _$ReqExtraBtFromJson(json);
@@ -58,15 +64,22 @@ enum RequestProxyMode {
@JsonSerializable() @JsonSerializable()
class RequestProxy { class RequestProxy {
RequestProxyMode mode = RequestProxyMode.follow; RequestProxyMode mode;
String scheme = 'http'; String scheme;
String host = ''; String host;
String usr = ''; String usr;
String pwd = ''; String pwd;
RequestProxy(); RequestProxy({
this.mode = RequestProxyMode.follow,
this.scheme = 'http',
this.host = '',
this.usr = '',
this.pwd = '',
});
factory RequestProxy.fromJson(Map<String, dynamic> json) => factory RequestProxy.fromJson(Map<String, dynamic> json) =>
_$RequestProxyFromJson(json); _$RequestProxyFromJson(json);
Map<String, dynamic> toJson() => _$RequestProxyToJson(this); Map<String, dynamic> toJson() => _$RequestProxyToJson(this);
} }
+22 -13
View File
@@ -36,10 +36,14 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
return val; return val;
} }
ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp() ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp(
..method = json['method'] as String method: json['method'] as String? ?? 'GET',
..header = Map<String, String>.from(json['header'] as Map) header: (json['header'] as Map<String, dynamic>?)?.map(
..body = json['body'] as String; (k, e) => MapEntry(k, e as String),
) ??
const {},
body: json['body'] as String? ?? '',
);
Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) => Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -48,21 +52,26 @@ Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
'body': instance.body, 'body': instance.body,
}; };
ReqExtraBt _$ReqExtraBtFromJson(Map<String, dynamic> json) => ReqExtraBt() ReqExtraBt _$ReqExtraBtFromJson(Map<String, dynamic> json) => ReqExtraBt(
..trackers = trackers: (json['trackers'] as List<dynamic>?)
(json['trackers'] as List<dynamic>).map((e) => e as String).toList(); ?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) => Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) =>
<String, dynamic>{ <String, dynamic>{
'trackers': instance.trackers, 'trackers': instance.trackers,
}; };
RequestProxy _$RequestProxyFromJson(Map<String, dynamic> json) => RequestProxy() RequestProxy _$RequestProxyFromJson(Map<String, dynamic> json) => RequestProxy(
..mode = $enumDecode(_$RequestProxyModeEnumMap, json['mode']) mode: $enumDecodeNullable(_$RequestProxyModeEnumMap, json['mode']) ??
..scheme = json['scheme'] as String RequestProxyMode.follow,
..host = json['host'] as String scheme: json['scheme'] as String? ?? 'http',
..usr = json['usr'] as String host: json['host'] as String? ?? '',
..pwd = json['pwd'] as String; usr: json['usr'] as String? ?? '',
pwd: json['pwd'] as String? ?? '',
);
Map<String, dynamic> _$RequestProxyToJson(RequestProxy instance) => Map<String, dynamic> _$RequestProxyToJson(RequestProxy instance) =>
<String, dynamic>{ <String, dynamic>{
+1 -1
View File
@@ -11,7 +11,7 @@ Result<T> _$ResultFromJson<T>(
T Function(Object? json) fromJsonT, T Function(Object? json) fromJsonT,
) => ) =>
Result<T>( Result<T>(
code: json['code'] as int, code: (json['code'] as num).toInt(),
msg: json['msg'] as String?, msg: json['msg'] as String?,
data: _$nullableGenericFromJson(json['data'], fromJsonT), data: _$nullableGenericFromJson(json['data'], fromJsonT),
); );
@@ -16,6 +16,7 @@ import 'package:window_manager/window_manager.dart';
import '../../../../api/api.dart'; import '../../../../api/api.dart';
import '../../../../api/model/downloader_config.dart'; import '../../../../api/model/downloader_config.dart';
import '../../../../api/model/request.dart';
import '../../../../core/common/start_config.dart'; import '../../../../core/common/start_config.dart';
import '../../../../core/libgopeed_boot.dart'; import '../../../../core/libgopeed_boot.dart';
import '../../../../database/database.dart'; import '../../../../database/database.dart';
@@ -27,6 +28,9 @@ import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart'; import '../../../../util/package_info.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../routes/app_pages.dart'; import '../../../routes/app_pages.dart';
import '../../create/controllers/create_controller.dart';
import '../../create/dto/create_router_params.dart';
import '../../redirect/views/redirect_view.dart';
const unixSocketPath = 'gopeed.sock'; const unixSocketPath = 'gopeed.sock';
@@ -160,8 +164,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
} }
Future<void> _initDeepLinks() async { Future<void> _initDeepLinks() async {
// currently only support android if (Util.isWeb()) {
if (!Util.isAndroid()) {
return; return;
} }
@@ -169,13 +172,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
// Handle link when app is in warm state (front or background) // Handle link when app is in warm state (front or background)
_linkSubscription = _appLinks.uriLinkStream.listen((uri) async { _linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
await _toCreate(uri); await _handleDeepLink(uri);
}); });
// Check initial link if app was in cold state (terminated) // Check initial link if app was in cold state (terminated)
final uri = await _appLinks.getInitialAppLink(); final uri = await _appLinks.getInitialLink();
if (uri != null) { if (uri != null) {
await _toCreate(uri); await _handleDeepLink(uri);
} }
} }
@@ -304,13 +307,38 @@ class AppController extends GetxController with WindowListener, TrayListener {
} }
} }
Future<void> _toCreate(Uri uri) async { Future<void> _handleDeepLink(Uri uri) async {
final path = (uri.scheme == "magnet" || if (uri.scheme == "gopeed") {
if (uri.path == "/create") {
final params = uri.queryParameters["params"];
if (params?.isNotEmpty == true) {
final paramsJson = String.fromCharCodes(base64Decode(params!));
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
arguments: RedirectArgs(Routes.CREATE,
arguments:
CreateRouterParams.fromJson(jsonDecode(paramsJson))));
return;
}
Get.rootDelegate.offAndToNamed(Routes.CREATE);
return;
}
Get.rootDelegate.offAndToNamed(Routes.HOME);
return;
}
String path;
if (uri.scheme == "magnet" ||
uri.scheme == "http" || uri.scheme == "http" ||
uri.scheme == "https") uri.scheme == "https") {
? uri.toString() path = uri.toString();
: (await toFile(uri.toString())).path; } else if (uri.scheme == "file") {
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path); path = Util.isWindows() ? uri.path.substring(1) : uri.path;
} else {
path = (await toFile(uri.toString())).path;
}
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
arguments: RedirectArgs(Routes.CREATE,
arguments: CreateRouterParams(req: Request(url: path))));
} }
String runningAddress() { String runningAddress() {
@@ -9,7 +9,6 @@ import '../../app/controllers/app_controller.dart';
class CreateController extends GetxController class CreateController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
// final files = [].obs;
final RxList fileInfos = [].obs; final RxList fileInfos = [].obs;
final RxList openedFolders = [].obs; final RxList openedFolders = [].obs;
final selectedIndexes = <int>[].obs; final selectedIndexes = <int>[].obs;
@@ -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;
}
@@ -1,5 +1,6 @@
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart'; import 'dart:convert';
import 'package:checkable_treeview/checkable_treeview.dart';
import 'package:contentsize_tabbarview/contentsize_tabbarview.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -13,11 +14,11 @@ import '../../../../api/model/create_task.dart';
import '../../../../api/model/options.dart'; import '../../../../api/model/options.dart';
import '../../../../api/model/request.dart'; import '../../../../api/model/request.dart';
import '../../../../api/model/resolve_result.dart'; import '../../../../api/model/resolve_result.dart';
import '../../../../api/model/task.dart';
import '../../../../database/database.dart'; import '../../../../database/database.dart';
import '../../../../util/input_formatter.dart'; import '../../../../util/input_formatter.dart';
import '../../../../util/message.dart'; import '../../../../util/message.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../../util/extensions.dart';
import '../../../routes/app_pages.dart'; import '../../../routes/app_pages.dart';
import '../../../views/compact_checkbox.dart'; import '../../../views/compact_checkbox.dart';
import '../../../views/directory_selector.dart'; import '../../../views/directory_selector.dart';
@@ -25,6 +26,7 @@ import '../../../views/file_tree_view.dart';
import '../../app/controllers/app_controller.dart'; import '../../app/controllers/app_controller.dart';
import '../../history/views/history_view.dart'; import '../../history/views/history_view.dart';
import '../controllers/create_controller.dart'; import '../controllers/create_controller.dart';
import '../dto/create_router_params.dart';
class CreateView extends GetView<CreateController> { class CreateView extends GetView<CreateController> {
final _confirmFormKey = GlobalKey<FormState>(); final _confirmFormKey = GlobalKey<FormState>();
@@ -38,11 +40,21 @@ class CreateView extends GetView<CreateController> {
final _proxyPortController = TextEditingController(); final _proxyPortController = TextEditingController();
final _proxyUsrController = TextEditingController(); final _proxyUsrController = TextEditingController();
final _proxyPwdController = TextEditingController(); final _proxyPwdController = TextEditingController();
final _httpUaController = TextEditingController(); final _httpHeaderControllers = [
final _httpCookieController = TextEditingController(); (
final _httpRefererController = TextEditingController(); name: TextEditingController(text: "User-Agent"),
value: TextEditingController()
),
(
name: TextEditingController(text: "Cookie"),
value: TextEditingController()
),
(
name: TextEditingController(text: "Referer"),
value: TextEditingController()
),
];
final _btTrackerController = TextEditingController(); final _btTrackerController = TextEditingController();
final _fileTreeViewKey = GlobalKey<TreeViewState<int>>();
final _availableSchemes = ["http:", "https:", "magnet:"]; final _availableSchemes = ["http:", "https:", "magnet:"];
@@ -63,14 +75,74 @@ class CreateView extends GetView<CreateController> {
_pathController.text = appController.downloaderConfig.value.downloadDir; _pathController.text = appController.downloaderConfig.value.downloadDir;
} }
final String? filePath = Get.rootDelegate.arguments(); final CreateRouterParams? routerParams = Get.rootDelegate.arguments();
if (_urlController.text.isEmpty) { if (routerParams?.req?.url.isNotEmpty ?? false) {
if (filePath?.isNotEmpty ?? false) { // get url from route arguments
// get file path from route arguments final url = routerParams!.req!.url;
_urlController.text = filePath!; _urlController.text = url;
_urlController.selection = TextSelection.fromPosition( _urlController.selection = TextSelection.fromPosition(
TextPosition(offset: _urlController.text.length)); TextPosition(offset: _urlController.text.length));
} else { final uppercaseUrl = url.toUpperCase();
Protocol? protocol;
if (uppercaseUrl.startsWith("HTTP:") ||
uppercaseUrl.startsWith("HTTPS:")) {
protocol = Protocol.http;
}
if (uppercaseUrl.startsWith("MAGNET:") ||
uppercaseUrl.endsWith(".TORRENT")) {
protocol = Protocol.bt;
}
if (protocol != null) {
final extraHandlers = {
Protocol.http: () {
final reqExtra = ReqExtraHttp.fromJson(
jsonDecode(jsonEncode(routerParams.req!.extra)));
_httpHeaderControllers.clear();
reqExtra.header.forEach((key, value) {
_httpHeaderControllers.add(
(
name: TextEditingController(text: key),
value: TextEditingController(text: value),
),
);
});
_skipVerifyCertController.value = routerParams.req!.skipVerifyCert;
},
Protocol.bt: () {
final reqExtra = ReqExtraBt.fromJson(
jsonDecode(jsonEncode(routerParams.req!.extra)));
_btTrackerController.text = reqExtra.trackers.join("\n");
},
};
if (routerParams.req?.extra != null) {
extraHandlers[protocol]?.call();
}
// handle options
if (routerParams.opt != null) {
_renameController.text = routerParams.opt!.name;
_pathController.text = routerParams.opt!.path;
final optionsHandlers = {
Protocol.http: () {
final opt = routerParams.opt!;
_renameController.text = opt.name;
_pathController.text = opt.path;
if (opt.extra != null) {
final optsExtraHttp =
OptsExtraHttp.fromJson(jsonDecode(jsonEncode(opt.extra)));
_connectionsController.text =
optsExtraHttp.connections.toString();
}
},
Protocol.bt: null,
};
if (routerParams.opt?.extra != null) {
optionsHandlers[protocol]?.call();
}
}
}
} else if (_urlController.text.isEmpty) {
// read clipboard // read clipboard
Clipboard.getData('text/plain').then((value) { Clipboard.getData('text/plain').then((value) {
if (value?.text?.isNotEmpty ?? false) { if (value?.text?.isNotEmpty ?? false) {
@@ -89,7 +161,6 @@ class CreateView extends GetView<CreateController> {
} }
}); });
} }
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -439,36 +510,84 @@ class CreateView extends GetView<CreateController> {
) )
], ],
), ),
AutoScaleTabBarView( DefaultTabController(
controller: controller.advancedTabController, length: 2,
child: ContentSizeTabBarView(
controller:
controller.advancedTabController,
children: [ children: [
Column( Column(
children: [ children: [
TextFormField( ..._httpHeaderControllers.map((e) {
controller: _httpUaController, return Row(
decoration: const InputDecoration( children: [
labelText: 'User-Agent', Flexible(
)), child: TextFormField(
TextFormField( controller: e.name,
controller: _httpCookieController, decoration: InputDecoration(
decoration: const InputDecoration( hintText:
labelText: 'Cookie', 'httpHeaderName'.tr,
)), ),
TextFormField( ),
controller: _httpRefererController, ),
decoration: const InputDecoration( const Padding(
labelText: 'Referer', padding: EdgeInsets.only(
)), left: 10)),
Flexible(
child: TextFormField(
controller: e.value,
decoration: InputDecoration(
hintText:
'httpHeaderValue'.tr,
),
),
),
const Padding(
padding: EdgeInsets.only(
left: 10)),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
_httpHeaderControllers.add(
(
name:
TextEditingController(),
value:
TextEditingController(),
),
);
controller.showAdvanced
.update((val) => val);
},
),
IconButton(
icon:
const Icon(Icons.remove),
onPressed: () {
if (_httpHeaderControllers
.length <=
1) {
return;
}
_httpHeaderControllers
.remove(e);
controller.showAdvanced
.update((val) => val);
},
),
],
);
}),
Padding( Padding(
padding: padding:
const EdgeInsets.only(top: 10), const EdgeInsets.only(top: 10),
child: CompactCheckbox( child: CompactCheckbox(
label: 'skipVerifyCert'.tr, label: 'skipVerifyCert'.tr,
value: value: _skipVerifyCertController
_skipVerifyCertController.value, .value,
onChanged: (bool? value) { onChanged: (bool? value) {
_skipVerifyCertController.value = _skipVerifyCertController
value ?? false; .value = value ?? false;
}, },
textStyle: const TextStyle( textStyle: const TextStyle(
color: Colors.grey, color: Colors.grey,
@@ -489,6 +608,7 @@ class CreateView extends GetView<CreateController> {
], ],
) )
], ],
),
) )
], ],
).paddingOnly(top: 16), ).paddingOnly(top: 16),
@@ -647,12 +767,10 @@ class CreateView extends GetView<CreateController> {
if (controller.showAdvanced.value) { if (controller.showAdvanced.value) {
switch (controller.advancedTabController.index) { switch (controller.advancedTabController.index) {
case 0: case 0:
final header = { final header = Map<String, String>.fromEntries(_httpHeaderControllers
"User-Agent": _httpUaController.text, .map((e) => MapEntry(e.name.text, e.value.text)));
"Cookie": _httpCookieController.text, header.removeWhere(
"Referer": _httpRefererController.text, (key, value) => key.trim().isEmpty || value.trim().isEmpty);
};
header.removeWhere((key, value) => value.trim().isEmpty);
if (header.isNotEmpty) { if (header.isNotEmpty) {
reqExtra = ReqExtraHttp()..header = header; reqExtra = ReqExtraHttp()..header = header;
} }
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/redirect_controller.dart';
class RedirectBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<RedirectController>(
() => RedirectController(),
);
}
}
@@ -0,0 +1,3 @@
import 'package:get/get.dart';
class RedirectController extends GetxController {}
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/redirect_controller.dart';
class RedirectArgs {
final String page;
final dynamic arguments;
RedirectArgs(this.page, {this.arguments});
}
class RedirectView extends GetView<RedirectController> {
const RedirectView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final redirectArgs = Get.rootDelegate.arguments() as RedirectArgs;
// Waiting for previous page controller to delete, avoid deleting controller that route page after redirect
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 350));
Get.rootDelegate
.offAndToNamed(redirectArgs.page, arguments: redirectArgs.arguments);
});
return const SizedBox();
}
}
@@ -17,6 +17,7 @@ import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart'; import '../../../../util/log_util.dart';
import '../../../../util/message.dart'; import '../../../../util/message.dart';
import '../../../../util/package_info.dart'; import '../../../../util/package_info.dart';
import '../../../../util/scheme_register/scheme_register.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../views/check_list_view.dart'; import '../../../views/check_list_view.dart';
import '../../../views/directory_selector.dart'; import '../../../views/directory_selector.dart';
@@ -111,7 +112,7 @@ class SettingView extends GetView<SettingController> {
}); });
final buildDefaultDirectDownload = final buildDefaultDirectDownload =
_buildConfigItem('defaultDirectDownload'.tr, () { _buildConfigItem('defaultDirectDownload', () {
return appController.downloaderConfig.value.extra.defaultDirectDownload return appController.downloaderConfig.value.extra.defaultDirectDownload
? 'on'.tr ? 'on'.tr
: 'off'.tr; : 'off'.tr;
@@ -161,7 +162,7 @@ class SettingView extends GetView<SettingController> {
// Currently auto startup only support Windows and Linux // Currently auto startup only support Windows and Linux
final buildAutoStartup = !Util.isWindows() && !Util.isLinux() final buildAutoStartup = !Util.isWindows() && !Util.isLinux()
? () => null ? () => null
: _buildConfigItem('launchAtStartup'.tr, () { : _buildConfigItem('launchAtStartup', () {
return appController.autoStartup.value ? 'on'.tr : 'off'.tr; return appController.autoStartup.value ? 'on'.tr : 'off'.tr;
}, (Key key) { }, (Key key) {
return Container( return Container(
@@ -229,8 +230,9 @@ class SettingView extends GetView<SettingController> {
], ],
); );
}); });
final buildHttpUseServerCtime = _buildConfigItem('useServerCtime'.tr, final buildHttpUseServerCtime = _buildConfigItem(
() => httpConfig.useServerCtime ? 'on'.tr : 'off'.tr, (Key key) { 'useServerCtime', () => httpConfig.useServerCtime ? 'on'.tr : 'off'.tr,
(Key key) {
return Container( return Container(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Switch( child: Switch(
@@ -420,6 +422,37 @@ class SettingView extends GetView<SettingController> {
].where((e) => e != null).map((e) => e!).toList(), ].where((e) => e != null).map((e) => e!).toList(),
); );
}); });
final buildBtDefaultClientConfig = !Util.isWindows()
? () => null
: _buildConfigItem('setAsDefaultBtClient', () {
return appController.downloaderConfig.value.extra.defaultBtClient
? 'on'.tr
: 'off'.tr;
}, (Key key) {
return Container(
alignment: Alignment.centerLeft,
child: Switch(
value:
appController.downloaderConfig.value.extra.defaultBtClient,
onChanged: (bool value) async {
try {
if (value) {
registerDefaultTorrentClient();
} else {
unregisterDefaultTorrentClient();
}
appController.downloaderConfig.update((val) {
val!.extra.defaultBtClient = value;
});
await debounceSave();
} catch (e) {
showErrorMessage(e);
logger.e('register default torrent client fail', e);
}
},
),
);
});
// ui config items start // ui config items start
final buildTheme = _buildConfigItem( final buildTheme = _buildConfigItem(
@@ -947,6 +980,7 @@ class SettingView extends GetView<SettingController> {
buildBtTrackerSubscribeUrls(), buildBtTrackerSubscribeUrls(),
buildBtTrackers(), buildBtTrackers(),
buildBtSeedConfig(), buildBtSeedConfig(),
buildBtDefaultClientConfig(),
]), ]),
)), )),
Text('ui'.tr), Text('ui'.tr),
@@ -1,4 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:gopeed/api/model/task.dart'; import 'package:gopeed/api/model/task.dart';
@@ -6,4 +8,20 @@ class TaskController extends GetxController {
final tabIndex = 0.obs; final tabIndex = 0.obs;
final scaffoldKey = GlobalKey<ScaffoldState>(); final scaffoldKey = GlobalKey<ScaffoldState>();
final selectTask = Rx<Task?>(null); final selectTask = Rx<Task?>(null);
@override
void onInit() {
super.onInit();
if (kIsWeb) {
BrowserContextMenu.disableContextMenu();
}
}
@override
void onClose() {
super.onClose();
if (kIsWeb) {
BrowserContextMenu.enableContextMenu();
}
}
} }
+9 -2
View File
@@ -1,6 +1,4 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:gopeed/app/modules/task/views/task_files_view.dart';
import 'package:gopeed/app/modules/task/views/task_view.dart';
import '../modules/create/bindings/create_binding.dart'; import '../modules/create/bindings/create_binding.dart';
import '../modules/create/views/create_view.dart'; import '../modules/create/views/create_view.dart';
@@ -8,12 +6,16 @@ import '../modules/extension/bindings/extension_binding.dart';
import '../modules/extension/views/extension_view.dart'; import '../modules/extension/views/extension_view.dart';
import '../modules/home/bindings/home_binding.dart'; import '../modules/home/bindings/home_binding.dart';
import '../modules/home/views/home_view.dart'; import '../modules/home/views/home_view.dart';
import '../modules/redirect/bindings/redirect_binding.dart';
import '../modules/redirect/views/redirect_view.dart';
import '../modules/root/bindings/root_binding.dart'; import '../modules/root/bindings/root_binding.dart';
import '../modules/root/views/root_view.dart'; import '../modules/root/views/root_view.dart';
import '../modules/setting/bindings/setting_binding.dart'; import '../modules/setting/bindings/setting_binding.dart';
import '../modules/setting/views/setting_view.dart'; import '../modules/setting/views/setting_view.dart';
import '../modules/task/bindings/task_binding.dart'; import '../modules/task/bindings/task_binding.dart';
import '../modules/task/bindings/task_files_binding.dart'; import '../modules/task/bindings/task_files_binding.dart';
import '../modules/task/views/task_files_view.dart';
import '../modules/task/views/task_view.dart';
part 'app_routes.dart'; part 'app_routes.dart';
@@ -68,6 +70,11 @@ class AppPages {
page: () => CreateView(), page: () => CreateView(),
binding: CreateBinding(), binding: CreateBinding(),
), ),
GetPage(
name: _Paths.REDIRECT,
page: () => const RedirectView(),
binding: RedirectBinding(),
),
]), ]),
]; ];
} }
@@ -3,6 +3,7 @@ part of 'app_pages.dart';
abstract class Routes { abstract class Routes {
Routes._(); Routes._();
static const ROOT = _Paths.ROOT; static const ROOT = _Paths.ROOT;
static const HOME = _Paths.HOME; static const HOME = _Paths.HOME;
static const CREATE = _Paths.CREATE; static const CREATE = _Paths.CREATE;
@@ -10,10 +11,12 @@ abstract class Routes {
static const TASK_FILES = TASK + _Paths.TASK_FILES; static const TASK_FILES = TASK + _Paths.TASK_FILES;
static const EXTENSION = _Paths.HOME + _Paths.EXTENSION; static const EXTENSION = _Paths.HOME + _Paths.EXTENSION;
static const SETTING = _Paths.HOME + _Paths.SETTING; static const SETTING = _Paths.HOME + _Paths.SETTING;
static const REDIRECT = _Paths.REDIRECT;
} }
abstract class _Paths { abstract class _Paths {
_Paths._(); _Paths._();
static const ROOT = '/'; static const ROOT = '/';
static const HOME = '/home'; static const HOME = '/home';
static const CREATE = '/create'; static const CREATE = '/create';
@@ -21,4 +24,5 @@ abstract class _Paths {
static const TASK_FILES = '/files'; static const TASK_FILES = '/files';
static const EXTENSION = '/extension'; static const EXTENSION = '/extension';
static const SETTING = '/setting'; static const SETTING = '/setting';
static const REDIRECT = '/redirect';
} }
@@ -1,4 +1,4 @@
import 'package:contextmenu/contextmenu.dart'; import 'package:contextmenu_plus/contextmenu_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
+58 -54
View File
@@ -7,6 +7,7 @@ import '../../api/model/resource.dart';
import '../../icon/gopeed_icons.dart'; import '../../icon/gopeed_icons.dart';
import '../../util/util.dart'; import '../../util/util.dart';
import 'file_icon.dart'; import 'file_icon.dart';
import 'responsive_builder.dart';
import 'sort_icon_button.dart'; import 'sort_icon_button.dart';
const _toggleSwitchIcons = [ const _toggleSwitchIcons = [
@@ -53,6 +54,52 @@ class _FileTreeViewState extends State<FileTreeView> {
widget.files.length; widget.files.length;
final selectedFileSize = calcSelectedSize(null); final selectedFileSize = calcSelectedSize(null);
final filterRow = InkWell(
onTap: () {},
child: ToggleSwitch(
minHeight: 32,
cornerRadius: 8,
doubleTapDisable: true,
inactiveBgColor: Theme.of(context).dividerColor,
activeBgColor: [Theme.of(context).colorScheme.primary],
initialLabelIndex: toggleSwitchIndex,
icons: _toggleSwitchIcons,
onToggle: (index) {
toggleSwitchIndex = index;
if (index == null) {
key.currentState?.setSelectedValues(List.empty());
return;
}
final iconFileExtArr = iconConfigMap[_toggleSwitchIcons[index]] ?? [];
final selectedFileIndexes = widget.files
.asMap()
.entries
.where((e) => iconFileExtArr.contains(fileExt(e.value.name)))
.map((e) => e.key)
.toList();
key.currentState?.setSelectedValues(selectedFileIndexes);
},
),
);
final countRow = Row(
children: [
Text('fileSelectedCount'.tr),
Text(
selectedFileCount.toString(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(width: 12),
Text('fileSelectedSize'.tr),
Text(
selectedFileCount > 0 && selectedFileSize == 0
? 'unknown'.tr
: Util.fmtByte(selectedFileSize),
style: Theme.of(context).textTheme.bodySmall,
),
],
);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -141,64 +188,21 @@ class _FileTreeViewState extends State<FileTreeView> {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( !ResponsiveBuilder.isNarrow(context)
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Flexible( filterRow,
flex: 4, countRow,
child: InkWell(
onTap: () {},
child: ToggleSwitch(
minHeight: 32,
cornerRadius: 8,
doubleTapDisable: true,
inactiveBgColor: Theme.of(context).dividerColor,
activeBgColor: [Theme.of(context).colorScheme.primary],
initialLabelIndex: toggleSwitchIndex,
icons: _toggleSwitchIcons,
onToggle: (index) {
toggleSwitchIndex = index;
if (index == null) {
key.currentState?.setSelectedValues(List.empty());
return;
}
final iconFileExtArr =
iconConfigMap[_toggleSwitchIcons[index]] ?? [];
final selectedFileIndexes = widget.files
.asMap()
.entries
.where((e) =>
iconFileExtArr.contains(fileExt(e.value.name)))
.map((e) => e.key)
.toList();
key.currentState?.setSelectedValues(selectedFileIndexes);
},
),
),
),
Flexible(
flex: 6,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('fileSelectedCount'.tr),
Text(
selectedFileCount.toString(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(width: 12),
Text('fileSelectedSize'.tr),
Text(
selectedFileCount > 0 && selectedFileSize == 0
? 'unknown'.tr
: Util.fmtByte(selectedFileSize),
style: Theme.of(context).textTheme.bodySmall,
),
], ],
), )
), : Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
filterRow,
const SizedBox(height: 8),
countRow,
], ],
), ),
], ],
@@ -11,7 +11,7 @@ StartConfig _$StartConfigFromJson(Map<String, dynamic> json) => StartConfig()
..address = json['address'] as String ..address = json['address'] as String
..storage = json['storage'] as String ..storage = json['storage'] as String
..storageDir = json['storageDir'] as String ..storageDir = json['storageDir'] as String
..refreshInterval = json['refreshInterval'] as int ..refreshInterval = (json['refreshInterval'] as num).toInt()
..apiToken = json['apiToken'] as String; ..apiToken = json['apiToken'] as String;
Map<String, dynamic> _$StartConfigToJson(StartConfig instance) => Map<String, dynamic> _$StartConfigToJson(StartConfig instance) =>
+3
View File
@@ -105,6 +105,7 @@ const enUS = {
'seedKeep': 'Keep seeding until manually stopped', 'seedKeep': 'Keep seeding until manually stopped',
'seedRatio': 'Seed ratio', 'seedRatio': 'Seed ratio',
'seedTime': 'Seed time (minutes)', 'seedTime': 'Seed time (minutes)',
'setAsDefaultBtClient': 'Set as the default BT client',
'taskDetail': 'Task Detail', 'taskDetail': 'Task Detail',
'taskName': 'Task Name', 'taskName': 'Task Name',
'taskUrl': 'Task URL', 'taskUrl': 'Task URL',
@@ -115,5 +116,7 @@ const enUS = {
'unknown': 'Unknown', 'unknown': 'Unknown',
'fileSelectedCount': 'Files: ', 'fileSelectedCount': 'Files: ',
'fileSelectedSize': 'Size: ', 'fileSelectedSize': 'Size: ',
'httpHeaderName': 'Header Name',
'httpHeaderValue': 'Header Value',
}, },
}; };
+3
View File
@@ -103,6 +103,7 @@ const zhCN = {
'seedKeep': '持续做种直到手动停止', 'seedKeep': '持续做种直到手动停止',
'seedRatio': '做种分享率', 'seedRatio': '做种分享率',
'seedTime': '做种时间(分钟)', 'seedTime': '做种时间(分钟)',
'setAsDefaultBtClient': '设为系统默认 BT 客户端',
'taskDetail': '任务详情', 'taskDetail': '任务详情',
'taskName': '任务名称', 'taskName': '任务名称',
'taskUrl': '任务链接', 'taskUrl': '任务链接',
@@ -113,5 +114,7 @@ const zhCN = {
'unknown': '未知', 'unknown': '未知',
'fileSelectedCount': '文件数:', 'fileSelectedCount': '文件数:',
'fileSelectedSize': '大小:', 'fileSelectedSize': '大小:',
'httpHeaderName': '请求头名称',
'httpHeaderValue': '请求头值',
} }
}; };
+3
View File
@@ -103,6 +103,7 @@ const zhTW = {
'seedKeep': '持續做種直到手動停止', 'seedKeep': '持續做種直到手動停止',
'seedRatio': '做種分享率', 'seedRatio': '做種分享率',
'seedTime': '做種時間(分鐘)', 'seedTime': '做種時間(分鐘)',
'setAsDefaultBtClient': '設為系統預設 BT 客戶端',
'taskDetail': '任務詳情', 'taskDetail': '任務詳情',
'taskName': '任務名稱', 'taskName': '任務名稱',
'taskUrl': '任務連結', 'taskUrl': '任務連結',
@@ -113,5 +114,7 @@ const zhTW = {
'unknown': '未知', 'unknown': '未知',
'fileSelectedCount': '文件數:', 'fileSelectedCount': '文件數:',
'fileSelectedSize': '大小:', 'fileSelectedSize': '大小:',
'httpHeaderName': '標頭名稱',
'httpHeaderValue': '標頭值',
} }
}; };
+10
View File
@@ -14,6 +14,7 @@ import 'util/locale_manager.dart';
import 'util/log_util.dart'; import 'util/log_util.dart';
import 'util/mac_secure_util.dart'; import 'util/mac_secure_util.dart';
import 'util/package_info.dart'; import 'util/package_info.dart';
import 'util/scheme_register/scheme_register.dart';
import 'util/util.dart'; import 'util/util.dart';
class Args { class Args {
@@ -81,6 +82,15 @@ Future<void> init(Args args) async {
logger.e("libgopeed init fail", e); logger.e("libgopeed init fail", e);
} }
try {
registerUrlScheme("gopeed");
if (controller.downloaderConfig.value.extra.defaultBtClient) {
registerDefaultTorrentClient();
}
} catch (e) {
logger.e("register scheme fail", e);
}
try { try {
await controller.loadDownloaderConfig(); await controller.loadDownloaderConfig();
MacSecureUtil.loadBookmark(); MacSecureUtil.loadBookmark();
@@ -0,0 +1,93 @@
import 'dart:io';
import 'package:win32_registry/win32_registry.dart';
import '../../util.dart';
doRegisterUrlScheme(String scheme) {
if (Util.isWindows()) {
final schemeKey = 'Software\\Classes\\$scheme';
final appPath = Platform.resolvedExecutable;
_upsertRegistry(
schemeKey,
'URL Protocol',
'',
);
_upsertRegistry(
'$schemeKey\\shell\\open\\command',
'',
'"$appPath" "%1"',
);
}
}
doUnregisterUrlScheme(String scheme) {
if (Util.isWindows()) {
Registry.currentUser
.deleteKey('Software\\Classes\\$scheme', recursive: true);
}
}
const _torrentRegKey = 'Software\\Classes\\.torrent';
const _torrentRegValue = 'Gopeed_torrent';
const _torrentAppRegKey = 'Software\\Classes\\$_torrentRegValue';
/// Register as the system's default torrent client
/// 1. Register the scheme "magnet"
/// 2. Register the file type ".torrent"
doRegisterDefaultTorrentClient() {
if (Util.isWindows()) {
doRegisterUrlScheme("magnet");
final appPath = Platform.resolvedExecutable;
final iconPath =
'${File(appPath).parent.path}\\data\\flutter_assets\\assets\\tray_icon\\icon.ico';
_upsertRegistry(
_torrentRegKey,
'',
_torrentRegValue,
);
_upsertRegistry(
_torrentAppRegKey,
'',
'Torrent file',
);
_upsertRegistry(
'$_torrentAppRegKey\\DefaultIcon',
'',
iconPath,
);
_upsertRegistry(
'$_torrentAppRegKey\\shell\\open\\command',
'',
'"$appPath" "file:///%1"',
);
}
}
doUnregisterDefaultTorrentClient() {
if (Util.isWindows()) {
doUnregisterUrlScheme("magnet");
Registry.currentUser.deleteKey(_torrentRegKey, recursive: true);
Registry.currentUser.deleteKey(_torrentAppRegKey, recursive: true);
}
}
// Add Windows registry key and value if not exists
_upsertRegistry(String keyPath, String valueName, String value) {
RegistryKey regKey;
try {
regKey = Registry.openPath(RegistryHive.currentUser,
path: keyPath, desiredAccessRights: AccessRights.allAccess);
} catch (e) {
regKey = Registry.currentUser.createKey(keyPath);
}
if (regKey.getValueAsString(valueName) != value) {
regKey
.createValue(RegistryValue(valueName, RegistryValueType.string, value));
}
regKey.close();
}
@@ -0,0 +1,11 @@
import 'scheme_register_stub.dart'
if (dart.library.io) 'entry/scheme_register_native.dart';
registerUrlScheme(String scheme) => doRegisterUrlScheme(scheme);
unregisterUrlScheme(String scheme) => doUnregisterUrlScheme(scheme);
registerDefaultTorrentClient() => doRegisterDefaultTorrentClient();
unregisterDefaultTorrentClient() => doUnregisterDefaultTorrentClient();
@@ -0,0 +1,7 @@
doRegisterUrlScheme(String scheme) => throw UnimplementedError();
doUnregisterUrlScheme(String scheme) => throw UnimplementedError();
doRegisterDefaultTorrentClient() => throw UnimplementedError();
doUnregisterDefaultTorrentClient() => throw UnimplementedError();
@@ -12,3 +12,4 @@ Icon=com.gopeed.Gopeed
Type=Application Type=Application
Categories=Utility;Internet Categories=Utility;Internet
Keywords=Bittorrent;Downloader; Keywords=Bittorrent;Downloader;
MimeType=x-scheme-handler/gopeed;x-scheme-handler/magnet;application/x-bittorrent;
+11 -4
View File
@@ -17,6 +17,13 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate. // Implements GApplication::activate.
static void my_application_activate(GApplication* application) { static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application); MyApplication* self = MY_APPLICATION(application);
GList* windows = gtk_application_get_windows(GTK_APPLICATION(application));
if (windows) {
gtk_window_present(GTK_WINDOW(windows->data));
return;
}
GtkWindow* window = GtkWindow* window =
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
@@ -48,8 +55,7 @@ static void my_application_activate(GApplication* application) {
} }
gtk_window_set_default_size(window, 1280, 720); gtk_window_set_default_size(window, 1280, 720);
//gtk_widget_show(GTK_WIDGET(window)); gtk_widget_show(GTK_WIDGET(window));
gtk_widget_realize(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new(); g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
@@ -59,6 +65,7 @@ static void my_application_activate(GApplication* application) {
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
fl_register_plugins(FL_PLUGIN_REGISTRY(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view));
gtk_widget_hide(GTK_WIDGET(window));
gtk_widget_grab_focus(GTK_WIDGET(view)); gtk_widget_grab_focus(GTK_WIDGET(view));
} }
@@ -79,7 +86,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch
g_application_activate(application); g_application_activate(application);
*exit_status = 0; *exit_status = 0;
return TRUE; return FALSE;
} }
// Implements GObject::dispose. // Implements GObject::dispose.
@@ -100,6 +107,6 @@ static void my_application_init(MyApplication* self) {}
MyApplication* my_application_new() { MyApplication* my_application_new() {
return MY_APPLICATION(g_object_new(my_application_get_type(), return MY_APPLICATION(g_object_new(my_application_get_type(),
"application-id", APPLICATION_ID, "application-id", APPLICATION_ID,
"flags", G_APPLICATION_NON_UNIQUE, "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN,
nullptr)); nullptr));
} }
@@ -14,4 +14,9 @@ categories:
- Network - Network
- Utility - Utility
supported_mime_type:
- x-scheme-handler/gopeed
- x-scheme-handler/magnet
- application/x-bittorrent
startup_notify: true startup_notify: true
@@ -28,4 +28,9 @@ categories:
- Network - Network
- Utility - Utility
supported_mime_type:
- x-scheme-handler/gopeed
- x-scheme-handler/magnet
- application/x-bittorrent
startup_notify: true startup_notify: true
+11 -5
View File
@@ -3,6 +3,8 @@ PODS:
- FlutterMacOS - FlutterMacOS
- desktop_drop (0.0.1): - desktop_drop (0.0.1):
- FlutterMacOS - FlutterMacOS
- device_info_plus (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- macos_secure_bookmarks (0.0.3): - macos_secure_bookmarks (0.0.3):
- FlutterMacOS - FlutterMacOS
@@ -28,6 +30,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
- desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- macos_secure_bookmarks (from `Flutter/ephemeral/.symlinks/plugins/macos_secure_bookmarks/macos`) - macos_secure_bookmarks (from `Flutter/ephemeral/.symlinks/plugins/macos_secure_bookmarks/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
@@ -44,6 +47,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
desktop_drop: desktop_drop:
:path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos
device_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
macos_secure_bookmarks: macos_secure_bookmarks:
@@ -68,15 +73,16 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898
device_info_plus: 74e614483d05c89290d30a4c8feae15d555f7427
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
macos_secure_bookmarks: cff041c4b377fa00941bb901aaec4d27fac5edf2 macos_secure_bookmarks: cff041c4b377fa00941bb901aaec4d27fac5edf2
package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce package_info_plus: f5790acc797bf17c3e959e9d6cf162cc68ff7523
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 share_plus: fd717ef89a2801d3491e737630112b80c310640e
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
@@ -206,7 +206,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 0920; LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1430; LastUpgradeCheck = 1510;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
33CC10EC2044A3C60003C045 = { 33CC10EC2044A3C60003C045 = {
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1430" LastUpgradeVersion = "1510"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
+29 -1
View File
@@ -1,8 +1,22 @@
import Cocoa import Cocoa
import FlutterMacOS import FlutterMacOS
import app_links
@NSApplicationMain @main
class AppDelegate: FlutterAppDelegate { class AppDelegate: FlutterAppDelegate {
public override func application(_ application: NSApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void) -> Bool {
guard let url = AppLinks.shared.getUniversalLink(userActivity) else {
return false
}
AppLinks.shared.handleLink(link: url.absoluteString)
return false // Returning true will stop the propagation to other packages
}
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return false return false
} }
@@ -19,4 +33,18 @@ class AppDelegate: FlutterAppDelegate {
} }
return true return true
} }
override func application(_ sender: NSApplication, openFile filename: String) -> Bool {
let url = URL(fileURLWithPath: filename)
AppLinks.shared.handleLink(link: url.absoluteString)
return false;
}
override func application(_ application: NSApplication, open urls: [URL]) {
// Only handle the first file
if(urls.isEmpty) {
return
}
AppLinks.shared.handleLink(link: urls.first!.absoluteString)
}
} }
Loaded 50 of 55 files, more files were not shown because too many files have changed in this diff. Show more