mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-11 10:54:08 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abefe428ca | ||
|
|
9e9bcd4b01 | ||
|
|
2f378f7238 | ||
|
|
5bd57608d2 |
No files matched your search
@@ -798,5 +798,5 @@ jobs:
|
|||||||
linux/arm64
|
linux/arm64
|
||||||
linux/arm/v7
|
linux/arm/v7
|
||||||
tags: |
|
tags: |
|
||||||
liwei2633/gopeed:latest
|
${{ !contains(env.VERSION, 'beta') && 'liwei2633/gopeed:latest' || '' }}
|
||||||
liwei2633/gopeed:v${{ env.VERSION }}
|
liwei2633/gopeed:v${{ env.VERSION }}
|
||||||
@@ -27,6 +27,10 @@ type ScriptData struct {
|
|||||||
// ScriptPayload contains the task data
|
// ScriptPayload contains the task data
|
||||||
type ScriptPayload struct {
|
type ScriptPayload struct {
|
||||||
Task *Task
|
Task *Task
|
||||||
|
// TaskName is computed before Task is cloned. The clone has no fetcher
|
||||||
|
// manager, so for an unresolved task without Options.Name its Name()
|
||||||
|
// would return "unknown".
|
||||||
|
TaskName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// getScriptPaths extracts script paths from config
|
// getScriptPaths extracts script paths from config
|
||||||
@@ -103,7 +107,7 @@ func (d *Downloader) executeScriptAtPath(scriptPath string, data *ScriptData) er
|
|||||||
cmd.Env = append(os.Environ(),
|
cmd.Env = append(os.Environ(),
|
||||||
fmt.Sprintf("GOPEED_EVENT=%s", data.Event),
|
fmt.Sprintf("GOPEED_EVENT=%s", data.Event),
|
||||||
fmt.Sprintf("GOPEED_TASK_ID=%s", data.Payload.Task.ID),
|
fmt.Sprintf("GOPEED_TASK_ID=%s", data.Payload.Task.ID),
|
||||||
fmt.Sprintf("GOPEED_TASK_NAME=%s", data.Payload.Task.Name()),
|
fmt.Sprintf("GOPEED_TASK_NAME=%s", data.Payload.TaskName),
|
||||||
fmt.Sprintf("GOPEED_TASK_STATUS=%s", data.Payload.Task.Status),
|
fmt.Sprintf("GOPEED_TASK_STATUS=%s", data.Payload.Task.Status),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -137,7 +141,8 @@ func (d *Downloader) triggerScripts(event ScriptEvent, task *Task, err error) {
|
|||||||
Event: event,
|
Event: event,
|
||||||
Time: time.Now().UnixMilli(),
|
Time: time.Now().UnixMilli(),
|
||||||
Payload: &ScriptPayload{
|
Payload: &ScriptPayload{
|
||||||
Task: task.clone(),
|
Task: task.clone(),
|
||||||
|
TaskName: task.Name(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package download
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -126,6 +128,46 @@ func TestScript_ExecuteScriptAtPath_NonExistentFile(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestScript_TaskNameOfUnresolvedTask(t *testing.T) {
|
||||||
|
scriptPath := getTestScriptPath(t, envDumpScriptName())
|
||||||
|
ensureScriptExecutable(t, scriptPath)
|
||||||
|
outputFile := filepath.Join(t.TempDir(), "env_output.txt")
|
||||||
|
|
||||||
|
setupScriptTest(t, func(downloader *Downloader) {
|
||||||
|
cfg, _ := downloader.GetConfig()
|
||||||
|
cfg.Script = &base.ScriptConfig{
|
||||||
|
Enable: true,
|
||||||
|
Paths: []string{scriptPath},
|
||||||
|
}
|
||||||
|
downloader.PutConfig(cfg)
|
||||||
|
|
||||||
|
// Without Options.Name and a resource, the name comes from the fetcher
|
||||||
|
// manager, which the cloned task in the script data does not have.
|
||||||
|
url := "http://127.0.0.1/error.bin"
|
||||||
|
fm, err := downloader.parseFm(url)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
task := NewTask()
|
||||||
|
task.Meta = &fetcher.FetcherMeta{
|
||||||
|
Req: &base.Request{URL: url},
|
||||||
|
Opts: &base.Options{Path: t.TempDir()},
|
||||||
|
}
|
||||||
|
task.fetcherManager = fm
|
||||||
|
if name := task.Name(); name != "error.bin" {
|
||||||
|
t.Fatalf("Expected task name error.bin, got %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Setenv("GOPEED_TEST_OUTPUT_FILE", outputFile)
|
||||||
|
downloader.triggerScripts(ScriptEventDownloadDone, task, nil)
|
||||||
|
|
||||||
|
output := waitForFileContains(t, outputFile, "GOPEED_TASK_PATH=", 3*time.Second)
|
||||||
|
if !strings.Contains(output, "GOPEED_TASK_NAME=error.bin") {
|
||||||
|
t.Errorf("Expected GOPEED_TASK_NAME=error.bin in output, got: %s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func createDownloadDoneTask(t *testing.T, downloadDir, fileName string) (*Task, string) {
|
func createDownloadDoneTask(t *testing.T, downloadDir, fileName string) (*Task, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
content := []byte("downloaded file")
|
content := []byte("downloaded file")
|
||||||
@@ -171,6 +213,31 @@ func waitForFile(t *testing.T, path string, timeout time.Duration) {
|
|||||||
t.Fatalf("Timeout waiting for file: %s", path)
|
t.Fatalf("Timeout waiting for file: %s", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitForFileContains waits until the file contains want and returns its content.
|
||||||
|
// Test scripts write the file line by line, so waiting for the file to exist is not enough.
|
||||||
|
func waitForFileContains(t *testing.T, path, want string, timeout time.Duration) string {
|
||||||
|
t.Helper()
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if content, err := os.ReadFile(path); err == nil && strings.Contains(string(content), want) {
|
||||||
|
return string(content)
|
||||||
|
}
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
content, _ := os.ReadFile(path)
|
||||||
|
t.Fatalf("Timeout waiting for %q in file %s, got: %q", want, path, content)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// envDumpScriptName returns the test script that writes the GOPEED_* variables
|
||||||
|
// into the file named by GOPEED_TEST_OUTPUT_FILE.
|
||||||
|
func envDumpScriptName() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return "env_dump.bat"
|
||||||
|
}
|
||||||
|
return "env_dump.sh"
|
||||||
|
}
|
||||||
|
|
||||||
func getTestScriptPath(t *testing.T, name string) string {
|
func getTestScriptPath(t *testing.T, name string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
path := filepath.Join("testdata", "scripts", name)
|
path := filepath.Join("testdata", "scripts", name)
|
||||||
|
|||||||
@@ -91,13 +91,8 @@ func TestScript_EnvironmentVariables(t *testing.T) {
|
|||||||
t.Setenv("GOPEED_TEST_OUTPUT_FILE", outputFile)
|
t.Setenv("GOPEED_TEST_OUTPUT_FILE", outputFile)
|
||||||
downloader.triggerScripts(ScriptEventDownloadDone, task, nil)
|
downloader.triggerScripts(ScriptEventDownloadDone, task, nil)
|
||||||
|
|
||||||
waitForFile(t, outputFile, 3*time.Second)
|
// GOPEED_TASK_PATH is the last line the script writes.
|
||||||
content, err := os.ReadFile(outputFile)
|
output := waitForFileContains(t, outputFile, "GOPEED_TASK_PATH=", 3*time.Second)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read output file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output := string(content)
|
|
||||||
if !strings.Contains(output, "GOPEED_EVENT=DOWNLOAD_DONE") {
|
if !strings.Contains(output, "GOPEED_EVENT=DOWNLOAD_DONE") {
|
||||||
t.Errorf("Expected GOPEED_EVENT in output, got: %s", output)
|
t.Errorf("Expected GOPEED_EVENT in output, got: %s", output)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,13 +87,8 @@ func TestScript_EnvironmentVariables(t *testing.T) {
|
|||||||
t.Setenv("GOPEED_TEST_OUTPUT_FILE", outputFile)
|
t.Setenv("GOPEED_TEST_OUTPUT_FILE", outputFile)
|
||||||
downloader.triggerScripts(ScriptEventDownloadDone, task, nil)
|
downloader.triggerScripts(ScriptEventDownloadDone, task, nil)
|
||||||
|
|
||||||
waitForFile(t, outputFile, 5*time.Second)
|
// GOPEED_TASK_PATH is the last line the script writes.
|
||||||
content, err := os.ReadFile(outputFile)
|
output := waitForFileContains(t, outputFile, "GOPEED_TASK_PATH=", 5*time.Second)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to read output file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output := string(content)
|
|
||||||
if !strings.Contains(output, "GOPEED_EVENT=DOWNLOAD_DONE") {
|
if !strings.Contains(output, "GOPEED_EVENT=DOWNLOAD_DONE") {
|
||||||
t.Errorf("Expected GOPEED_EVENT in output, got: %s", output)
|
t.Errorf("Expected GOPEED_EVENT in output, got: %s", output)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform;
|
||||||
import 'package:flutter/material.dart' show Colors;
|
import 'package:flutter/material.dart' show Colors;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
import '../../shared/theme/app_palette.dart';
|
||||||
import '../../shared/widgets/window/desktop_window_header.dart';
|
import '../../shared/widgets/window/desktop_window_header.dart';
|
||||||
import 'app_window_chrome.dart';
|
import 'app_window_chrome.dart';
|
||||||
|
|
||||||
@@ -16,6 +18,15 @@ class AppWindowFrame extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget framedChild = child;
|
Widget framedChild = child;
|
||||||
|
|
||||||
|
// System insets apply to mobile devices at every responsive breakpoint.
|
||||||
|
// SafeArea also consumes the padding so page-level SafeAreas do not add it twice.
|
||||||
|
if (defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS) {
|
||||||
|
framedChild = ColoredBox(
|
||||||
|
color: AppPalette.of(context).bg,
|
||||||
|
child: SafeArea(child: framedChild),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (AppWindowChrome.usesCustomChrome) {
|
if (AppWindowChrome.usesCustomChrome) {
|
||||||
framedChild = Stack(
|
framedChild = Stack(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show TargetPlatform, debugDefaultTargetPlatformOverride;
|
||||||
import 'package:flutter/gestures.dart' show kSecondaryMouseButton;
|
import 'package:flutter/gestures.dart' show kSecondaryMouseButton;
|
||||||
import 'package:flutter/material.dart' show Icons;
|
import 'package:flutter/material.dart' show Icons;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@@ -8,6 +9,9 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:gopeed/api/model/task.dart' as api;
|
import 'package:gopeed/api/model/task.dart' as api;
|
||||||
import 'package:gopeed/features/home/presentation/pages/home_page.dart';
|
import 'package:gopeed/features/home/presentation/pages/home_page.dart';
|
||||||
|
import 'package:gopeed/features/home/presentation/widgets/primary_rail.dart';
|
||||||
|
import 'package:gopeed/features/home/presentation/widgets/tasks_top_bar.dart';
|
||||||
|
import 'package:gopeed/core/window/app_window_frame.dart';
|
||||||
import 'package:gopeed/features/tasks/application/tasks_controller.dart';
|
import 'package:gopeed/features/tasks/application/tasks_controller.dart';
|
||||||
import 'package:gopeed/features/tasks/domain/task_record.dart';
|
import 'package:gopeed/features/tasks/domain/task_record.dart';
|
||||||
import 'package:gopeed/features/tasks/presentation/pages/task_details_page.dart';
|
import 'package:gopeed/features/tasks/presentation/pages/task_details_page.dart';
|
||||||
@@ -162,6 +166,7 @@ Future<({GroupTasks controller, GoRouter router})> showHome(WidgetTester tester,
|
|||||||
child: shad.ShadcnApp.router(
|
child: shad.ShadcnApp.router(
|
||||||
theme: AppTheme.light(),
|
theme: AppTheme.light(),
|
||||||
materialTheme: AppTheme.materialLight(),
|
materialTheme: AppTheme.materialLight(),
|
||||||
|
builder: (context, child) => AppWindowFrame(child: child!),
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -171,6 +176,50 @@ Future<({GroupTasks controller, GoRouter router})> showHome(WidgetTester tester,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
for (final platform in [TargetPlatform.android, TargetPlatform.iOS]) {
|
||||||
|
testWidgets('$platform keeps headers inside system insets across rotation', (tester) async {
|
||||||
|
debugDefaultTargetPlatformOverride = platform;
|
||||||
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||||
|
addTearDown(tester.view.resetPadding);
|
||||||
|
addTearDown(tester.view.resetViewPadding);
|
||||||
|
await showHome(tester, 390);
|
||||||
|
|
||||||
|
for (final size in [const Size(390, 844), const Size(700, 1100), const Size(1100, 700), const Size(700, 1100)]) {
|
||||||
|
tester.view.physicalSize = size;
|
||||||
|
tester.view.padding = FakeViewPadding.zero;
|
||||||
|
tester.view.viewPadding = FakeViewPadding.zero;
|
||||||
|
await settle(tester);
|
||||||
|
final header = size.width < 720
|
||||||
|
? find
|
||||||
|
.ancestor(
|
||||||
|
of: find.byKey(const ValueKey('tasks-mobile-create-button')),
|
||||||
|
matching: find.byType(SafeArea),
|
||||||
|
)
|
||||||
|
.first
|
||||||
|
: find.byType(TasksTopBar);
|
||||||
|
final originalHeaderPosition = tester.getTopLeft(header);
|
||||||
|
|
||||||
|
const insets = FakeViewPadding(top: 24, left: 16, right: 12, bottom: 20);
|
||||||
|
tester.view.padding = insets;
|
||||||
|
tester.view.viewPadding = insets;
|
||||||
|
await settle(tester);
|
||||||
|
|
||||||
|
expect(tester.getRect(find.byType(HomePage)), Rect.fromLTRB(16, 24, size.width - 12, size.height - 20));
|
||||||
|
// Nested page SafeAreas must consume the inset only once.
|
||||||
|
expect(tester.getTopLeft(header), originalHeaderPosition + const Offset(16, 24));
|
||||||
|
expect(MediaQuery.paddingOf(tester.element(header)), EdgeInsets.zero);
|
||||||
|
if (size.width >= 720) {
|
||||||
|
expect(tester.getTopLeft(find.byType(PrimaryRail)), const Offset(16, 24));
|
||||||
|
} else {
|
||||||
|
await tester.tap(find.byKey(const ValueKey('tasks-mobile-batch-button')));
|
||||||
|
await settle(tester);
|
||||||
|
}
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
}
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (final width in [390.0, 700.0, 1100.0]) {
|
for (final width in [390.0, 700.0, 1100.0]) {
|
||||||
testWidgets('group expands actual member tasks and keeps a multi-file BT as one card at $width', (tester) async {
|
testWidgets('group expands actual member tasks and keeps a multi-file BT as one card at $width', (tester) async {
|
||||||
final app = await showHome(tester, width);
|
final app = await showHome(tester, width);
|
||||||
|
|||||||
Reference in new issue
Block a user