Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5ac13cfdc | ||
|
|
073e1413ab | ||
|
|
04b9aa18ec | ||
|
|
08fe4eef4e | ||
|
|
8f9bac9e6b |
No files matched your search
@@ -63,7 +63,7 @@ Connect Gopeed to an AI agent and manage downloads with natural language. For ex
|
|||||||
|
|
||||||
Gopeed 2.0.0 is currently in public beta, introducing a redesigned interface, a native communication architecture that connects desktop and mobile clients directly to the Go core through FFI, a more consistent cross-platform experience, improved task management, more flexible API support, and MCP-based AI agent integration. Some features may still be incomplete or unstable, so please try it and report any issues you encounter.
|
Gopeed 2.0.0 is currently in public beta, introducing a redesigned interface, a native communication architecture that connects desktop and mobile clients directly to the Go core through FFI, a more consistent cross-platform experience, improved task management, more flexible API support, and MCP-based AI agent integration. Some features may still be incomplete or unstable, so please try it and report any issues you encounter.
|
||||||
|
|
||||||
- [Download Gopeed 2.0.0 Beta 1](https://github.com/GopeedLab/gopeed/releases/tag/v2.0.0-beta.1)
|
- [Download Gopeed 2.0.0 Beta 2](https://github.com/GopeedLab/gopeed/releases/tag/v2.0.0-beta.2)
|
||||||
|
|
||||||
Once the features and stability meet our release standards, we will publish the official Gopeed 2.0.0 release. Beta users will be able to upgrade directly to the final release, while existing stable users will not be automatically moved onto the beta channel.
|
Once the features and stability meet our release standards, we will publish the official Gopeed 2.0.0 release. Beta users will be able to upgrade directly to the final release, while existing stable users will not be automatically moved onto the beta channel.
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Gopeed を AI Agent と連携すると、自然言語でダウンロードを管
|
|||||||
|
|
||||||
Gopeed 2.0.0 は現在パブリックベータ版です。刷新されたインターフェース、デスクトップおよびモバイルから FFI で Go コアへ直接接続するネイティブ通信アーキテクチャ、より統一されたクロスプラットフォーム体験、強化されたタスク管理、より柔軟な API、MCP による AI Agent 連携機能を導入しています。一部の機能はまだ不完全または不安定な場合がありますので、ぜひお試しいただき、問題があればフィードバックをお寄せください。
|
Gopeed 2.0.0 は現在パブリックベータ版です。刷新されたインターフェース、デスクトップおよびモバイルから FFI で Go コアへ直接接続するネイティブ通信アーキテクチャ、より統一されたクロスプラットフォーム体験、強化されたタスク管理、より柔軟な API、MCP による AI Agent 連携機能を導入しています。一部の機能はまだ不完全または不安定な場合がありますので、ぜひお試しいただき、問題があればフィードバックをお寄せください。
|
||||||
|
|
||||||
- [Gopeed 2.0.0 Beta 1 をダウンロード](https://github.com/GopeedLab/gopeed/releases/tag/v2.0.0-beta.1)
|
- [Gopeed 2.0.0 Beta 2 をダウンロード](https://github.com/GopeedLab/gopeed/releases/tag/v2.0.0-beta.2)
|
||||||
|
|
||||||
機能と安定性が正式リリースの基準に達した時点で、Gopeed 2.0.0 正式版を公開します。ベータ版のユーザーは正式版へ直接アップデートできます。既存の安定版ユーザーが自動的にベータチャンネルへ移行することはありません。
|
機能と安定性が正式リリースの基準に達した時点で、Gopeed 2.0.0 正式版を公開します。ベータ版のユーザーは正式版へ直接アップデートできます。既存の安定版ユーザーが自動的にベータチャンネルへ移行することはありません。
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Sau khi kết nối Gopeed với AI Agent, bạn có thể quản lý tải xu
|
|||||||
|
|
||||||
Gopeed 2.0.0 hiện đang trong giai đoạn beta công khai, với giao diện được thiết kế lại, kiến trúc giao tiếp native kết nối trực tiếp ứng dụng desktop và di động với lõi Go qua FFI, trải nghiệm đa nền tảng nhất quán hơn, khả năng quản lý tác vụ được cải thiện, API linh hoạt hơn và khả năng tích hợp AI Agent qua MCP. Một số tính năng có thể vẫn chưa hoàn thiện hoặc chưa ổn định, vì vậy hãy dùng thử và gửi phản hồi nếu bạn gặp vấn đề.
|
Gopeed 2.0.0 hiện đang trong giai đoạn beta công khai, với giao diện được thiết kế lại, kiến trúc giao tiếp native kết nối trực tiếp ứng dụng desktop và di động với lõi Go qua FFI, trải nghiệm đa nền tảng nhất quán hơn, khả năng quản lý tác vụ được cải thiện, API linh hoạt hơn và khả năng tích hợp AI Agent qua MCP. Một số tính năng có thể vẫn chưa hoàn thiện hoặc chưa ổn định, vì vậy hãy dùng thử và gửi phản hồi nếu bạn gặp vấn đề.
|
||||||
|
|
||||||
- [Tải Gopeed 2.0.0 Beta 1](https://github.com/GopeedLab/gopeed/releases/tag/v2.0.0-beta.1)
|
- [Tải Gopeed 2.0.0 Beta 2](https://github.com/GopeedLab/gopeed/releases/tag/v2.0.0-beta.2)
|
||||||
|
|
||||||
Khi các tính năng và độ ổn định đáp ứng tiêu chuẩn phát hành, chúng tôi sẽ phát hành Gopeed 2.0.0 chính thức. Người dùng bản beta có thể nâng cấp trực tiếp lên bản chính thức, trong khi người dùng bản ổn định hiện tại sẽ không tự động được chuyển sang kênh beta.
|
Khi các tính năng và độ ổn định đáp ứng tiêu chuẩn phát hành, chúng tôi sẽ phát hành Gopeed 2.0.0 chính thức. Người dùng bản beta có thể nâng cấp trực tiếp lên bản chính thức, trong khi người dùng bản ổn định hiện tại sẽ không tự động được chuyển sang kênh beta.
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Gopeed(全称 **Go Speed**,也叫“够快下载器”)是一款使用 Go
|
|||||||
|
|
||||||
Gopeed 2.0.0 目前处于公开 Beta 测试阶段,引入了全新界面、通过 FFI 直连 Go 核心的原生通信架构、更一致的跨平台体验、更完善的任务管理、更灵活的 API,以及用于 AI Agent 接入的 MCP 能力。部分功能仍可能不够完善或稳定,欢迎提前体验并向我们反馈问题。
|
Gopeed 2.0.0 目前处于公开 Beta 测试阶段,引入了全新界面、通过 FFI 直连 Go 核心的原生通信架构、更一致的跨平台体验、更完善的任务管理、更灵活的 API,以及用于 AI Agent 接入的 MCP 能力。部分功能仍可能不够完善或稳定,欢迎提前体验并向我们反馈问题。
|
||||||
|
|
||||||
- [下载 Gopeed 2.0.0 Beta 1](https://github.com/GopeedLab/gopeed/releases/tag/v2.0.0-beta.1)
|
- [下载 Gopeed 2.0.0 Beta 2](https://github.com/GopeedLab/gopeed/releases/tag/v2.0.0-beta.2)
|
||||||
|
|
||||||
当功能完整度和稳定性达到正式发布标准后,我们会发布 Gopeed 2.0.0 正式版。已安装 Beta 版本的用户可以直接升级到最终正式版,现有稳定版用户则不会被自动切换到 Beta 渠道。
|
当功能完整度和稳定性达到正式发布标准后,我们会发布 Gopeed 2.0.0 正式版。已安装 Beta 版本的用户可以直接升级到最终正式版,现有稳定版用户则不会被自动切换到 Beta 渠道。
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ Gopeed(**Go Speed** 的縮寫)是一款以 Go 與 Flutter 開發的高速、
|
|||||||
|
|
||||||
Gopeed 2.0.0 目前處於公開 Beta 測試階段,引入了全新介面、透過 FFI 直連 Go 核心的原生通訊架構、更一致的跨平台體驗、更完善的任務管理、更靈活的 API,以及透過 MCP 串接 AI Agent 的能力。部分功能仍可能不夠完善或穩定,歡迎搶先體驗並向我們回報問題。
|
Gopeed 2.0.0 目前處於公開 Beta 測試階段,引入了全新介面、透過 FFI 直連 Go 核心的原生通訊架構、更一致的跨平台體驗、更完善的任務管理、更靈活的 API,以及透過 MCP 串接 AI Agent 的能力。部分功能仍可能不夠完善或穩定,歡迎搶先體驗並向我們回報問題。
|
||||||
|
|
||||||
- [下載 Gopeed 2.0.0 Beta 1](https://github.com/GopeedLab/gopeed/releases/tag/v2.0.0-beta.1)
|
- [下載 Gopeed 2.0.0 Beta 2](https://github.com/GopeedLab/gopeed/releases/tag/v2.0.0-beta.2)
|
||||||
|
|
||||||
當功能完整度與穩定性達到正式發布標準後,我們會發布 Gopeed 2.0.0 正式版。已安裝 Beta 版本的使用者可以直接升級至最終正式版,現有穩定版使用者則不會被自動切換至 Beta 頻道。
|
當功能完整度與穩定性達到正式發布標準後,我們會發布 Gopeed 2.0.0 正式版。已安裝 Beta 版本的使用者可以直接升級至最終正式版,現有穩定版使用者則不會被自動切換至 Beta 頻道。
|
||||||
|
|
||||||
|
|||||||
@@ -459,6 +459,7 @@ func (f *Fetcher) Resolve(req *base.Request, opts *base.Options) error {
|
|||||||
if file.Name == "" || file.Name == "/" || file.Name == "." {
|
if file.Name == "" || file.Name == "/" || file.Name == "." {
|
||||||
file.Name = httpReq.URL.Hostname()
|
file.Name = httpReq.URL.Hostname()
|
||||||
}
|
}
|
||||||
|
file.Name = appendFilenameExtension(file.Name, resp.Header.Get("Content-Type"))
|
||||||
|
|
||||||
res.Files = append(res.Files, file)
|
res.Files = append(res.Files, file)
|
||||||
f.meta.Res = res
|
f.meta.Res = res
|
||||||
|
|||||||
@@ -221,6 +221,30 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ResolveFilenameFromContentType(t *testing.T) {
|
||||||
|
server := httptest.NewServer(gohttp.HandlerFunc(func(w gohttp.ResponseWriter, r *gohttp.Request) {
|
||||||
|
w.Header().Set("Content-Type", "image/webp")
|
||||||
|
w.WriteHeader(gohttp.StatusOK)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
fetcher := buildFetcher()
|
||||||
|
defer fetcher.Pause()
|
||||||
|
|
||||||
|
const opaqueName = "OIP-C.u5nEXTXND3PJF0gNTgw5YgHaFP"
|
||||||
|
err := fetcher.Resolve(&base.Request{URL: server.URL + "/" + opaqueName}, &base.Options{
|
||||||
|
Name: test.DownloadName,
|
||||||
|
Path: test.Dir,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := fetcher.meta.Res.Files[0].Name; got != opaqueName+".webp" {
|
||||||
|
t.Fatalf("resolved filename = %q, want %q", got, opaqueName+".webp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFetcher_ResolveWithHostHeader(t *testing.T) {
|
func TestFetcher_ResolveWithHostHeader(t *testing.T) {
|
||||||
listener := test.StartTestHostHeaderServer()
|
listener := test.StartTestHostHeaderServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|||||||
@@ -136,3 +136,52 @@ func TestUnescapeHTMLEntities(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAppendFilenameExtension(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
filename string
|
||||||
|
contentType string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "adds extension from content type",
|
||||||
|
filename: "download",
|
||||||
|
contentType: "image/png",
|
||||||
|
want: "download.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "parses MIME parameters",
|
||||||
|
filename: "download",
|
||||||
|
contentType: "image/png; charset=binary",
|
||||||
|
want: "download.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preserves existing extension",
|
||||||
|
filename: "download.zip",
|
||||||
|
contentType: "application/zip",
|
||||||
|
want: "download.zip",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignores invalid content type",
|
||||||
|
filename: "download",
|
||||||
|
contentType: "not a content type",
|
||||||
|
want: "download",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignores unknown content type",
|
||||||
|
filename: "download",
|
||||||
|
contentType: "application/x-gopeed-unknown",
|
||||||
|
want: "download",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := appendFilenameExtension(tt.filename, tt.contentType)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("appendFilenameExtension() = %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@@ -314,6 +315,28 @@ func parseFilename(contentDisposition string) string {
|
|||||||
return parseFilenameFallback(contentDisposition)
|
return parseFilenameFallback(contentDisposition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// appendFilenameExtension adds a MIME-derived extension when the resolved
|
||||||
|
// filename has no usable extension. Some image hosts use opaque path segments
|
||||||
|
// that look like extensions, even though the response Content-Type is the
|
||||||
|
// only reliable source for the file type.
|
||||||
|
func appendFilenameExtension(filename, contentType string) string {
|
||||||
|
mediaType, _, err := mime.ParseMediaType(contentType)
|
||||||
|
if err != nil {
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions, err := mime.ExtensionsByType(mediaType)
|
||||||
|
if err != nil || len(extensions) == 0 {
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := path.Ext(filename)
|
||||||
|
if len(ext) > 1 && len(ext) <= 20 {
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
return filename + extensions[0]
|
||||||
|
}
|
||||||
|
|
||||||
// parseFilenameExtended handles RFC 5987 extended notation (filename*=)
|
// parseFilenameExtended handles RFC 5987 extended notation (filename*=)
|
||||||
// Format: filename*=charset'language'value (e.g., UTF-8”%E6%B5%8B%E8%AF%95.zip)
|
// Format: filename*=charset'language'value (e.g., UTF-8”%E6%B5%8B%E8%AF%95.zip)
|
||||||
func parseFilenameExtended(cd string) string {
|
func parseFilenameExtended(cd string) string {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: Design or revise Gopeed file-classification icons as one cohesive S
|
|||||||
|
|
||||||
Maintain Gopeed's file-classification visual language and its deterministic SVG → TTF → Flutter `IconData` pipeline.
|
Maintain Gopeed's file-classification visual language and its deterministic SVG → TTF → Flutter `IconData` pipeline.
|
||||||
|
|
||||||
The canonical editable materials live in `design/file-category-icons/`, outside the Flutter project. Read its `README.md` before changing any source. Do not add that directory to Flutter assets.
|
The canonical editable materials live in `design/file-category-icons/` as tooling-only sources inside the Flutter project. Read its `README.md` before changing any source. Do not add that directory to Flutter assets.
|
||||||
|
|
||||||
## Classification grammar
|
## Classification grammar
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ Use [assets/manifest.example.json](assets/manifest.example.json) and [assets/fol
|
|||||||
## Invariants
|
## Invariants
|
||||||
|
|
||||||
- Existing codepoints are API: never renumber or reuse one unless the user explicitly approves a breaking migration.
|
- Existing codepoints are API: never renumber or reuse one unless the user explicitly approves a breaking migration.
|
||||||
- `design/file-category-icons/` is the sole editable source of truth. Flutter keeps only the generated runtime TTF and Dart constants.
|
- `design/file-category-icons/` is the sole editable source of truth. The packaged Flutter runtime uses only the generated TTF and Dart constants.
|
||||||
- Never add the design-material directory to `pubspec.yaml`; it must remain outside the packaged Flutter asset graph.
|
- Never add the design-material directory to `pubspec.yaml`; it must remain outside the packaged Flutter asset graph.
|
||||||
- The manifest is the source of truth. Do not hand-edit generated Dart constants or patch the binary font.
|
- The manifest is the source of truth. Do not hand-edit generated Dart constants or patch the binary font.
|
||||||
- Keep icon names lowercase `snake_case`; the generator emits lowerCamelCase Dart fields.
|
- Keep icon names lowercase `snake_case`; the generator emits lowerCamelCase Dart fields.
|
||||||
|
Before Width: | Height: | Size: 818 B After Width: | Height: | Size: 818 B |
@@ -1,6 +1,6 @@
|
|||||||
# 文件分类图标源物料
|
# 文件分类图标源物料
|
||||||
|
|
||||||
此目录是 Gopeed 文件分类图标的唯一源物料目录,位于 Flutter 工程之外,不得加入 `ui/flutter/pubspec.yaml` 的 `assets` 配置。
|
此目录是 Gopeed 文件分类图标的唯一源物料目录。它位于 Flutter 工程内,但只用于设计和生成流程,不得加入 `pubspec.yaml` 的 `assets` 配置。
|
||||||
|
|
||||||
## 维护限制
|
## 维护限制
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
|
|
||||||
## 生成产物
|
## 生成产物
|
||||||
|
|
||||||
- Flutter 字体:`ui/flutter/assets/fonts/GopeedIcons.ttf`
|
- Flutter 字体:`assets/fonts/GopeedIcons.ttf`
|
||||||
- Flutter 常量:`ui/flutter/lib/core/icons/gopeed_icons.dart`
|
- Flutter 常量:`lib/core/icons/gopeed_icons.dart`
|
||||||
|
|
||||||
所有修改都应遵循:
|
所有修改都应遵循:
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"fontFamily": "GopeedIcons",
|
"fontFamily": "GopeedIcons",
|
||||||
"fontOutput": "../../ui/flutter/assets/fonts/GopeedIcons.ttf",
|
"fontOutput": "../../assets/fonts/GopeedIcons.ttf",
|
||||||
"dartOutput": "../../ui/flutter/lib/core/icons/gopeed_icons.dart",
|
"dartOutput": "../../lib/core/icons/gopeed_icons.dart",
|
||||||
"previewOutput": "preview/gopeed_icons_preview.png",
|
"previewOutput": "preview/gopeed_icons_preview.png",
|
||||||
"dartClass": "GopeedIcons",
|
"dartClass": "GopeedIcons",
|
||||||
"codepointStart": "0xE900",
|
"codepointStart": "0xE900",
|
||||||
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 526 B After Width: | Height: | Size: 526 B |
|
Before Width: | Height: | Size: 387 B After Width: | Height: | Size: 387 B |
|
Before Width: | Height: | Size: 342 B After Width: | Height: | Size: 342 B |
|
Before Width: | Height: | Size: 323 B After Width: | Height: | Size: 323 B |
|
Before Width: | Height: | Size: 597 B After Width: | Height: | Size: 597 B |
|
Before Width: | Height: | Size: 495 B After Width: | Height: | Size: 495 B |
|
Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 293 B |
|
Before Width: | Height: | Size: 355 B After Width: | Height: | Size: 355 B |
|
Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 293 B |
|
Before Width: | Height: | Size: 395 B After Width: | Height: | Size: 395 B |
|
Before Width: | Height: | Size: 256 B After Width: | Height: | Size: 256 B |
|
Before Width: | Height: | Size: 262 B After Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 479 B After Width: | Height: | Size: 479 B |
|
Before Width: | Height: | Size: 357 B After Width: | Height: | Size: 357 B |
|
Before Width: | Height: | Size: 315 B After Width: | Height: | Size: 315 B |
|
Before Width: | Height: | Size: 231 B After Width: | Height: | Size: 231 B |
|
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B |
|
Before Width: | Height: | Size: 467 B After Width: | Height: | Size: 467 B |
|
Before Width: | Height: | Size: 166 B After Width: | Height: | Size: 166 B |
|
Before Width: | Height: | Size: 472 B After Width: | Height: | Size: 472 B |
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
|
Before Width: | Height: | Size: 386 B After Width: | Height: | Size: 386 B |
|
Before Width: | Height: | Size: 723 B After Width: | Height: | Size: 723 B |
@@ -1,6 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart' show Divider, Icons, Scrollbar, ScrollbarOrientation;
|
import 'package:flutter/material.dart' show Divider, Icons, Scrollbar, ScrollbarOrientation;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
@@ -1376,12 +1375,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
|||||||
title: index == null ? context.l10n.addScript : context.l10n.editScript,
|
title: index == null ? context.l10n.addScript : context.l10n.editScript,
|
||||||
fieldLabel: context.l10n.scriptPath,
|
fieldLabel: context.l10n.scriptPath,
|
||||||
initialValue: index == null ? '' : paths[index],
|
initialValue: index == null ? '' : paths[index],
|
||||||
pickPath: Util.isDesktop()
|
pickFile: true,
|
||||||
? () async {
|
|
||||||
final file = await FilePicker.pickFile();
|
|
||||||
return file?.path;
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
if (value == null || !mounted) return;
|
if (value == null || !mounted) return;
|
||||||
_mutateConfig((config) {
|
_mutateConfig((config) {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ Future<String?> showTextSettingDialog(
|
|||||||
required String fieldLabel,
|
required String fieldLabel,
|
||||||
String initialValue = '',
|
String initialValue = '',
|
||||||
bool requireHttpUrl = false,
|
bool requireHttpUrl = false,
|
||||||
Future<String?> Function()? pickPath,
|
bool pickFile = false,
|
||||||
Future<void> Function(String value)? onTest,
|
Future<void> Function(String value)? onTest,
|
||||||
}) async {
|
}) async {
|
||||||
final controller = TextEditingController(text: initialValue);
|
final controller = TextEditingController(text: initialValue);
|
||||||
@@ -189,16 +189,12 @@ Future<String?> showTextSettingDialog(
|
|||||||
children: [
|
children: [
|
||||||
Text(fieldLabel, style: TextStyle(color: palette.textSecondary, fontSize: 12)),
|
Text(fieldLabel, style: TextStyle(color: palette.textSecondary, fontSize: 12)),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
if (pickPath == null)
|
if (!pickFile)
|
||||||
AppTextField(controller: controller)
|
AppTextField(controller: controller)
|
||||||
else
|
else
|
||||||
AppPathPickerField(
|
AppPathPickerField.file(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
desktopWidth: AppDesignTokens.settingsFormControlWidth,
|
desktopWidth: AppDesignTokens.settingsFormControlWidth,
|
||||||
onPick: () async {
|
|
||||||
final path = await pickPath();
|
|
||||||
if (path != null && path.isNotEmpty) controller.text = path;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
if (validationMessage != null || testMessage != null) ...[
|
if (validationMessage != null || testMessage != null) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import '../../../../shared/theme/app_design_tokens.dart';
|
|||||||
import '../../../../shared/theme/app_palette.dart';
|
import '../../../../shared/theme/app_palette.dart';
|
||||||
import '../../../../shared/widgets/app_choice_segmented_control.dart';
|
import '../../../../shared/widgets/app_choice_segmented_control.dart';
|
||||||
import '../../../../shared/widgets/app_http_headers_editor.dart';
|
import '../../../../shared/widgets/app_http_headers_editor.dart';
|
||||||
import '../../../../shared/widgets/app_loading_button.dart';
|
|
||||||
import '../../../../shared/widgets/app_number_input.dart';
|
import '../../../../shared/widgets/app_number_input.dart';
|
||||||
import '../../../../shared/widgets/app_path_picker_field.dart';
|
import '../../../../shared/widgets/app_path_picker_field.dart';
|
||||||
import '../../../../shared/widgets/app_text_field.dart';
|
import '../../../../shared/widgets/app_text_field.dart';
|
||||||
@@ -38,8 +37,8 @@ import '../../../../shared/widgets/app_toast.dart';
|
|||||||
import '../../../../l10n/l10n.dart';
|
import '../../../../l10n/l10n.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../application/pending_create_task.dart';
|
import '../../application/pending_create_task.dart';
|
||||||
|
import '../widgets/create_task_action_buttons.dart';
|
||||||
import '../widgets/resolve_file_tree.dart';
|
import '../widgets/resolve_file_tree.dart';
|
||||||
import '../widgets/resolve_task_actions.dart';
|
|
||||||
|
|
||||||
class CreateTaskWindowPage extends ConsumerStatefulWidget {
|
class CreateTaskWindowPage extends ConsumerStatefulWidget {
|
||||||
const CreateTaskWindowPage({super.key, this.windowController, this.initialTask});
|
const CreateTaskWindowPage({super.key, this.windowController, this.initialTask});
|
||||||
@@ -696,17 +695,14 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SecondaryButton(
|
CreateTaskActionButtons(
|
||||||
onPressed: _creating ? null : _closeWindow,
|
submitting: _creating,
|
||||||
child: SizedBox(width: 68, child: Center(child: Text(context.l10n.cancel))),
|
onCancel: _closeWindow,
|
||||||
),
|
onSubmit: _confirm,
|
||||||
const SizedBox(width: 12),
|
cancelLabel: context.l10n.cancel,
|
||||||
AppLoadingButton(
|
submitLabel: context.l10n.confirm,
|
||||||
key: const ValueKey('create-task-confirm-button'),
|
cancelButtonKey: const ValueKey('create-task-cancel-button'),
|
||||||
onPressed: _confirm,
|
submitButtonKey: const ValueKey('create-task-confirm-button'),
|
||||||
loading: _creating,
|
|
||||||
variant: AppLoadingButtonVariant.primary,
|
|
||||||
child: SizedBox(width: 68, child: Center(child: Text(context.l10n.confirm))),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1159,10 +1155,14 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
flutter.Flexible(
|
flutter.Flexible(
|
||||||
child: ResolveTaskActions(
|
child: CreateTaskActionButtons(
|
||||||
submitting: submitting,
|
submitting: submitting,
|
||||||
onCancel: () => closeOverlay(dialogContext, false),
|
onCancel: () => closeOverlay(dialogContext, false),
|
||||||
onCreate: submit,
|
onSubmit: submit,
|
||||||
|
cancelLabel: dialogContext.l10n.cancel,
|
||||||
|
submitLabel: dialogContext.l10n.createAction,
|
||||||
|
cancelButtonKey: const ValueKey('resolve-cancel-button'),
|
||||||
|
submitButtonKey: const ValueKey('resolve-create-button'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1205,7 +1205,7 @@ class _CreateTaskWindowPageState extends ConsumerState<CreateTaskWindowPage> {
|
|||||||
width: contentWidth,
|
width: contentWidth,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: Text(dialogContext.l10n.createHistory)),
|
Expanded(child: Text(dialogContext.l10n.historyLinks)),
|
||||||
AppTooltip(
|
AppTooltip(
|
||||||
message: dialogContext.l10n.clearHistory,
|
message: dialogContext.l10n.clearHistory,
|
||||||
child: GhostButton(
|
child: GhostButton(
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/widgets.dart' as flutter;
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
|
||||||
|
import '../../../../shared/theme/app_design_tokens.dart';
|
||||||
|
import '../../../../shared/widgets/app_loading_button.dart';
|
||||||
|
|
||||||
|
class CreateTaskActionButtons extends StatelessWidget {
|
||||||
|
const CreateTaskActionButtons({
|
||||||
|
super.key,
|
||||||
|
required this.submitting,
|
||||||
|
required this.onCancel,
|
||||||
|
required this.onSubmit,
|
||||||
|
required this.cancelLabel,
|
||||||
|
required this.submitLabel,
|
||||||
|
this.cancelButtonKey,
|
||||||
|
this.submitButtonKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool submitting;
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
final VoidCallback onSubmit;
|
||||||
|
final String cancelLabel;
|
||||||
|
final String submitLabel;
|
||||||
|
final Key? cancelButtonKey;
|
||||||
|
final Key? submitButtonKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return flutter.Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SecondaryButton(
|
||||||
|
key: cancelButtonKey,
|
||||||
|
onPressed: submitting ? null : onCancel,
|
||||||
|
child: SizedBox(
|
||||||
|
width: _actionContentWidth,
|
||||||
|
child: Center(child: Text(cancelLabel)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: AppDesignTokens.space12),
|
||||||
|
AppLoadingButton(
|
||||||
|
key: submitButtonKey,
|
||||||
|
onPressed: onSubmit,
|
||||||
|
loading: submitting,
|
||||||
|
variant: AppLoadingButtonVariant.primary,
|
||||||
|
child: SizedBox(
|
||||||
|
width: _actionContentWidth,
|
||||||
|
child: Center(child: Text(submitLabel)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _actionContentWidth = 68.0;
|
||||||
@@ -189,42 +189,48 @@ class _TreeFooter extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final palette = AppPalette.of(context);
|
final palette = AppPalette.of(context);
|
||||||
final filters = _FileTypeFilterGroup(
|
final filters = KeyedSubtree(
|
||||||
|
key: const ValueKey('resolve-tree-filters'),
|
||||||
|
child: _FileTypeFilterGroup(
|
||||||
videoActive: activeTypeFilters.contains(_FileTypeFilter.video),
|
videoActive: activeTypeFilters.contains(_FileTypeFilter.video),
|
||||||
audioActive: activeTypeFilters.contains(_FileTypeFilter.audio),
|
audioActive: activeTypeFilters.contains(_FileTypeFilter.audio),
|
||||||
imageActive: activeTypeFilters.contains(_FileTypeFilter.image),
|
imageActive: activeTypeFilters.contains(_FileTypeFilter.image),
|
||||||
onSelectVideo: onSelectVideo,
|
onSelectVideo: onSelectVideo,
|
||||||
onSelectAudio: onSelectAudio,
|
onSelectAudio: onSelectAudio,
|
||||||
onSelectImage: onSelectImage,
|
onSelectImage: onSelectImage,
|
||||||
);
|
|
||||||
final stats = Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
context.l10n.selectedCount(selectedCount, totalCount),
|
|
||||||
style: TextStyle(color: palette.textSecondary, fontSize: 12, fontWeight: FontWeight.w600),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
);
|
||||||
Text(
|
final statsStyle = TextStyle(color: palette.textSecondary, fontSize: 12, fontWeight: FontWeight.w600);
|
||||||
unknownSize ? context.l10n.unknownSize : Util.fmtByte(selectedSize),
|
final selectedLabel = context.l10n.selectedCount(selectedCount, totalCount);
|
||||||
style: TextStyle(color: palette.textSecondary, fontSize: 12, fontWeight: FontWeight.w600),
|
final sizeLabel = unknownSize ? context.l10n.unknownSize : Util.fmtByte(selectedSize);
|
||||||
|
final baseline = MediaQuery.textScalerOf(context).scale(statsStyle.fontSize!);
|
||||||
|
final stats = Wrap(
|
||||||
|
key: const ValueKey('resolve-tree-selection-stats'),
|
||||||
|
alignment: WrapAlignment.end,
|
||||||
|
spacing: AppDesignTokens.space8,
|
||||||
|
runSpacing: AppDesignTokens.space4,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.start,
|
||||||
|
children: [
|
||||||
|
Baseline(
|
||||||
|
baseline: baseline,
|
||||||
|
baselineType: TextBaseline.alphabetic,
|
||||||
|
child: Text(key: const ValueKey('resolve-tree-selected-count'), selectedLabel, style: statsStyle),
|
||||||
|
),
|
||||||
|
Baseline(
|
||||||
|
baseline: baseline,
|
||||||
|
baselineType: TextBaseline.alphabetic,
|
||||||
|
child: Text(key: const ValueKey('resolve-tree-selected-size'), sizeLabel, style: statsStyle),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
return OverflowBar(
|
||||||
if (constraints.maxWidth < 360) {
|
key: const ValueKey('resolve-tree-footer'),
|
||||||
return Column(
|
spacing: AppDesignTokens.space8,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
alignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
overflowAlignment: OverflowBarAlignment.end,
|
||||||
Align(alignment: Alignment.centerLeft, child: filters),
|
overflowSpacing: AppDesignTokens.space4,
|
||||||
const SizedBox(height: 4),
|
children: [filters, stats],
|
||||||
Align(alignment: Alignment.centerRight, child: stats),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Row(crossAxisAlignment: CrossAxisAlignment.center, children: [filters, const Spacer(), stats]);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,7 +296,11 @@ class _FilterDivider extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(width: 1, height: 18, child: ColoredBox(color: color));
|
return SizedBox(
|
||||||
|
width: _fileTypeFilterDividerWidth,
|
||||||
|
height: 18,
|
||||||
|
child: ColoredBox(color: color),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +315,7 @@ class _FilterButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final palette = AppPalette.of(context);
|
final palette = AppPalette.of(context);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 40,
|
width: _fileTypeFilterButtonWidth,
|
||||||
height: 28,
|
height: 28,
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration: BoxDecoration(color: active ? palette.filterActiveBg : null),
|
decoration: BoxDecoration(color: active ? palette.filterActiveBg : null),
|
||||||
@@ -340,3 +350,5 @@ const _audioExts = <String>{'mp3', 'flac', 'wav', 'aac', 'm4a', 'ogg', 'ape'};
|
|||||||
const _imageExts = <String>{'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'};
|
const _imageExts = <String>{'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'};
|
||||||
const _resolveTreeRowHeight = 36.0;
|
const _resolveTreeRowHeight = 36.0;
|
||||||
const _resolveFileNameStyle = TextStyle(fontSize: 14, height: 1.25);
|
const _resolveFileNameStyle = TextStyle(fontSize: 14, height: 1.25);
|
||||||
|
const _fileTypeFilterButtonWidth = 40.0;
|
||||||
|
const _fileTypeFilterDividerWidth = 1.0;
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
|
||||||
import 'package:flutter/widgets.dart' as flutter;
|
|
||||||
|
|
||||||
import '../../../../l10n/l10n.dart';
|
|
||||||
import '../../../../shared/theme/app_design_tokens.dart';
|
|
||||||
import '../../../../shared/widgets/app_loading_button.dart';
|
|
||||||
|
|
||||||
class ResolveTaskActions extends StatelessWidget {
|
|
||||||
const ResolveTaskActions({super.key, required this.submitting, required this.onCancel, required this.onCreate});
|
|
||||||
|
|
||||||
final bool submitting;
|
|
||||||
final VoidCallback onCancel;
|
|
||||||
final VoidCallback onCreate;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
// Size both actions from the wider translated label, including while the
|
|
||||||
// create button replaces its label with a loader.
|
|
||||||
return IntrinsicWidth(
|
|
||||||
child: flutter.Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
flutter.Expanded(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(minWidth: AppDesignTokens.dialogActionMinWidth),
|
|
||||||
child: SecondaryButton(
|
|
||||||
key: const ValueKey('resolve-cancel-button'),
|
|
||||||
onPressed: submitting ? null : onCancel,
|
|
||||||
child: Text(context.l10n.cancel, maxLines: 1, softWrap: false),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: AppDesignTokens.space8),
|
|
||||||
flutter.Expanded(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(minWidth: AppDesignTokens.dialogActionMinWidth),
|
|
||||||
child: AppLoadingButton(
|
|
||||||
key: const ValueKey('resolve-create-button'),
|
|
||||||
onPressed: onCreate,
|
|
||||||
loading: submitting,
|
|
||||||
variant: AppLoadingButtonVariant.primary,
|
|
||||||
child: Text(context.l10n.createAction, maxLines: 1, softWrap: false),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count}",
|
"resolvedFileCount": "{count}",
|
||||||
"resourceHasNoFiles": "ولا توجد في هذا المورد قائمة ملف قابلة للاختيار",
|
"resourceHasNoFiles": "ولا توجد في هذا المورد قائمة ملف قابلة للاختيار",
|
||||||
"noFiles": "لا توجد ملفات",
|
"noFiles": "لا توجد ملفات",
|
||||||
"createHistory": "تاريخ الخلق",
|
"historyLinks": "روابط السجل",
|
||||||
"noHistory": "لا تاريخ",
|
"noHistory": "لا تاريخ",
|
||||||
"directDownloadDescription": "حل المشاكل وخلق المهام على الفور",
|
"directDownloadDescription": "حل المشاكل وخلق المهام على الفور",
|
||||||
"taskProgress": "التقدم المحرز",
|
"taskProgress": "التقدم المحرز",
|
||||||
|
|||||||
@@ -367,7 +367,7 @@
|
|||||||
"resolvedFileCount": "{count} fayl tapıldı",
|
"resolvedFileCount": "{count} fayl tapıldı",
|
||||||
"resourceHasNoFiles": "Bu resursda seçilə bilən fayl siyahısı yoxdur",
|
"resourceHasNoFiles": "Bu resursda seçilə bilən fayl siyahısı yoxdur",
|
||||||
"noFiles": "Fayl yoxdur",
|
"noFiles": "Fayl yoxdur",
|
||||||
"createHistory": "Tarixçə Yarat",
|
"historyLinks": "Tarixçə keçidləri",
|
||||||
"noHistory": "Tarixçə yoxdur",
|
"noHistory": "Tarixçə yoxdur",
|
||||||
"directDownloadDescription": "Təhlili keç və tapşırıqları dərhal yarat",
|
"directDownloadDescription": "Təhlili keç və tapşırıqları dərhal yarat",
|
||||||
"taskProgress": "Tapşırığın irəliləyişi",
|
"taskProgress": "Tapşırığın irəliləyişi",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count}",
|
"resolvedFileCount": "{count}",
|
||||||
"resourceHasNoFiles": "El recurs no té cap llista de fitxers per seleccionar",
|
"resourceHasNoFiles": "El recurs no té cap llista de fitxers per seleccionar",
|
||||||
"noFiles": "Sense fitxers",
|
"noFiles": "Sense fitxers",
|
||||||
"createHistory": "Crea un historial",
|
"historyLinks": "Enllaços de l'historial",
|
||||||
"noHistory": "Sense historial",
|
"noHistory": "Sense historial",
|
||||||
"directDownloadDescription": "Omet la resolució i crea les tasques immediatament",
|
"directDownloadDescription": "Omet la resolució i crea les tasques immediatament",
|
||||||
"taskProgress": "Progrés de la tasca",
|
"taskProgress": "Progrés de la tasca",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count}-Dateien behoben",
|
"resolvedFileCount": "{count}-Dateien behoben",
|
||||||
"resourceHasNoFiles": "Die Ressource verfügt über keine auswählbare Dateiliste",
|
"resourceHasNoFiles": "Die Ressource verfügt über keine auswählbare Dateiliste",
|
||||||
"noFiles": "Keine Dateien",
|
"noFiles": "Keine Dateien",
|
||||||
"createHistory": "Verlauf erstellen",
|
"historyLinks": "Verlaufslinks",
|
||||||
"noHistory": "Keine Historie",
|
"noHistory": "Keine Historie",
|
||||||
"directDownloadDescription": "Überspringen Sie die Lösung und erstellen Sie sofort Aufgaben",
|
"directDownloadDescription": "Überspringen Sie die Lösung und erstellen Sie sofort Aufgaben",
|
||||||
"taskProgress": "Aufgabenfortschritt",
|
"taskProgress": "Aufgabenfortschritt",
|
||||||
|
|||||||
@@ -367,7 +367,7 @@
|
|||||||
"resolvedFileCount": "{count, plural, =1{1 file resolved} other{{count} files resolved}}",
|
"resolvedFileCount": "{count, plural, =1{1 file resolved} other{{count} files resolved}}",
|
||||||
"resourceHasNoFiles": "The resource has no selectable file list",
|
"resourceHasNoFiles": "The resource has no selectable file list",
|
||||||
"noFiles": "No files",
|
"noFiles": "No files",
|
||||||
"createHistory": "Create History",
|
"historyLinks": "History Links",
|
||||||
"noHistory": "No history",
|
"noHistory": "No history",
|
||||||
"directDownloadDescription": "Skip resolving and create tasks immediately",
|
"directDownloadDescription": "Skip resolving and create tasks immediately",
|
||||||
"taskProgress": "Task progress",
|
"taskProgress": "Task progress",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} resueltos",
|
"resolvedFileCount": "{count} resueltos",
|
||||||
"resourceHasNoFiles": "El recurso no tiene lista de archivos seleccionable",
|
"resourceHasNoFiles": "El recurso no tiene lista de archivos seleccionable",
|
||||||
"noFiles": "No hay archivos",
|
"noFiles": "No hay archivos",
|
||||||
"createHistory": "Crear historia",
|
"historyLinks": "Enlaces del historial",
|
||||||
"noHistory": "No hay historia",
|
"noHistory": "No hay historia",
|
||||||
"directDownloadDescription": "Saltar a resolver y crear tareas inmediatamente",
|
"directDownloadDescription": "Saltar a resolver y crear tareas inmediatamente",
|
||||||
"taskProgress": "Progresos en la tarea",
|
"taskProgress": "Progresos en la tarea",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} فایل شناسایی شد",
|
"resolvedFileCount": "{count} فایل شناسایی شد",
|
||||||
"resourceHasNoFiles": "منبع هیچ لیست فایل قابل انتخاب ندارد",
|
"resourceHasNoFiles": "منبع هیچ لیست فایل قابل انتخاب ندارد",
|
||||||
"noFiles": "فایل ها",
|
"noFiles": "فایل ها",
|
||||||
"createHistory": "ایجاد تاریخ",
|
"historyLinks": "پیوندهای تاریخچه",
|
||||||
"noHistory": "تاریخ",
|
"noHistory": "تاریخ",
|
||||||
"directDownloadDescription": "پیاده سازی و ایجاد وظایف بلافاصله",
|
"directDownloadDescription": "پیاده سازی و ایجاد وظایف بلافاصله",
|
||||||
"taskProgress": "پیشرفت شغلی",
|
"taskProgress": "پیشرفت شغلی",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} résolus",
|
"resolvedFileCount": "{count} résolus",
|
||||||
"resourceHasNoFiles": "La ressource n'a pas de liste de fichiers sélectionnable",
|
"resourceHasNoFiles": "La ressource n'a pas de liste de fichiers sélectionnable",
|
||||||
"noFiles": "Aucun fichier",
|
"noFiles": "Aucun fichier",
|
||||||
"createHistory": "Créer l'historique",
|
"historyLinks": "Liens de l'historique",
|
||||||
"noHistory": "Pas d'histoire",
|
"noHistory": "Pas d'histoire",
|
||||||
"directDownloadDescription": "Sautez la résolution et créez immédiatement des tâches",
|
"directDownloadDescription": "Sautez la résolution et créez immédiatement des tâches",
|
||||||
"taskProgress": "Progrès accomplis dans la tâche",
|
"taskProgress": "Progrès accomplis dans la tâche",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} fájlok megoldva",
|
"resolvedFileCount": "{count} fájlok megoldva",
|
||||||
"resourceHasNoFiles": "Az erőforrásnak nincs kiválasztható fájllistája",
|
"resourceHasNoFiles": "Az erőforrásnak nincs kiválasztható fájllistája",
|
||||||
"noFiles": "Nincsenek fájlok",
|
"noFiles": "Nincsenek fájlok",
|
||||||
"createHistory": "Készítsen előzményeket",
|
"historyLinks": "Előzményhivatkozások",
|
||||||
"noHistory": "Nincs előzmény",
|
"noHistory": "Nincs előzmény",
|
||||||
"directDownloadDescription": "Hagyja ki a megoldást, és azonnal hozzon létre feladatokat",
|
"directDownloadDescription": "Hagyja ki a megoldást, és azonnal hozzon létre feladatokat",
|
||||||
"taskProgress": "A feladat előrehaladása",
|
"taskProgress": "A feladat előrehaladása",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} terselesaikan",
|
"resolvedFileCount": "{count} terselesaikan",
|
||||||
"resourceHasNoFiles": "Sumber daya tidak memiliki daftar berkas yang dapat dipilih",
|
"resourceHasNoFiles": "Sumber daya tidak memiliki daftar berkas yang dapat dipilih",
|
||||||
"noFiles": "Tidak ada berkas",
|
"noFiles": "Tidak ada berkas",
|
||||||
"createHistory": "Buat Riwayat",
|
"historyLinks": "Tautan Riwayat",
|
||||||
"noHistory": "Tidak ada sejarah",
|
"noHistory": "Tidak ada sejarah",
|
||||||
"directDownloadDescription": "Lewatkan pemecahan dan buat tugas segera",
|
"directDownloadDescription": "Lewatkan pemecahan dan buat tugas segera",
|
||||||
"taskProgress": "Kemajuan tugas",
|
"taskProgress": "Kemajuan tugas",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} risolti",
|
"resolvedFileCount": "{count} risolti",
|
||||||
"resourceHasNoFiles": "La risorsa non ha un elenco di file selezionabile",
|
"resourceHasNoFiles": "La risorsa non ha un elenco di file selezionabile",
|
||||||
"noFiles": "Nessun file",
|
"noFiles": "Nessun file",
|
||||||
"createHistory": "Creare la storia",
|
"historyLinks": "Link della cronologia",
|
||||||
"noHistory": "Nessuna storia",
|
"noHistory": "Nessuna storia",
|
||||||
"directDownloadDescription": "Salta la risoluzione e crea immediatamente i compiti",
|
"directDownloadDescription": "Salta la risoluzione e crea immediatamente i compiti",
|
||||||
"taskProgress": "Prospettive di attività",
|
"taskProgress": "Prospettive di attività",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} ファイルが解決しました",
|
"resolvedFileCount": "{count} ファイルが解決しました",
|
||||||
"resourceHasNoFiles": "リソースには、選択可能なファイルリストはありません",
|
"resourceHasNoFiles": "リソースには、選択可能なファイルリストはありません",
|
||||||
"noFiles": "ファイルがない場合",
|
"noFiles": "ファイルがない場合",
|
||||||
"createHistory": "歴史を創造する",
|
"historyLinks": "履歴リンク",
|
||||||
"noHistory": "歴史なし",
|
"noHistory": "歴史なし",
|
||||||
"directDownloadDescription": "解決をスキップし、すぐにタスクを作成",
|
"directDownloadDescription": "解決をスキップし、すぐにタスクを作成",
|
||||||
"taskProgress": "タスクの進捗",
|
"taskProgress": "タスクの進捗",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} 파일 해결",
|
"resolvedFileCount": "{count} 파일 해결",
|
||||||
"resourceHasNoFiles": "리소스에는 선택 가능한 파일 목록이 없습니다",
|
"resourceHasNoFiles": "리소스에는 선택 가능한 파일 목록이 없습니다",
|
||||||
"noFiles": "파일 없음",
|
"noFiles": "파일 없음",
|
||||||
"createHistory": "회사연혁",
|
"historyLinks": "기록 링크",
|
||||||
"noHistory": "의 역사",
|
"noHistory": "의 역사",
|
||||||
"directDownloadDescription": "재해를 드리며 즉시 업무를 만듭니다",
|
"directDownloadDescription": "재해를 드리며 즉시 업무를 만듭니다",
|
||||||
"taskProgress": "작업 진행",
|
"taskProgress": "작업 진행",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} plików rozwiązanych",
|
"resolvedFileCount": "{count} plików rozwiązanych",
|
||||||
"resourceHasNoFiles": "Zasób nie ma listy plików do wyboru",
|
"resourceHasNoFiles": "Zasób nie ma listy plików do wyboru",
|
||||||
"noFiles": "Brak plików",
|
"noFiles": "Brak plików",
|
||||||
"createHistory": "Utwórz historię",
|
"historyLinks": "Linki historii",
|
||||||
"noHistory": "Brak historii",
|
"noHistory": "Brak historii",
|
||||||
"directDownloadDescription": "Pomiń rozwiązywanie i tworzyć zadania natychmiast",
|
"directDownloadDescription": "Pomiń rozwiązywanie i tworzyć zadania natychmiast",
|
||||||
"taskProgress": "Postępy w realizacji zadań",
|
"taskProgress": "Postępy w realizacji zadań",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} ficheiros resolvidos",
|
"resolvedFileCount": "{count} ficheiros resolvidos",
|
||||||
"resourceHasNoFiles": "O recurso não tem lista de arquivos selecionáveis",
|
"resourceHasNoFiles": "O recurso não tem lista de arquivos selecionáveis",
|
||||||
"noFiles": "Sem arquivos",
|
"noFiles": "Sem arquivos",
|
||||||
"createHistory": "Criar Histórico",
|
"historyLinks": "Links do histórico",
|
||||||
"noHistory": "Sem histórico",
|
"noHistory": "Sem histórico",
|
||||||
"directDownloadDescription": "Ignorar resolver e criar tarefas imediatamente",
|
"directDownloadDescription": "Ignorar resolver e criar tarefas imediatamente",
|
||||||
"taskProgress": "Progresso da tarefa",
|
"taskProgress": "Progresso da tarefa",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} файлов, разрешенных",
|
"resolvedFileCount": "{count} файлов, разрешенных",
|
||||||
"resourceHasNoFiles": "Ресурс не имеет выбираемого списка файлов",
|
"resourceHasNoFiles": "Ресурс не имеет выбираемого списка файлов",
|
||||||
"noFiles": "Нет файлов",
|
"noFiles": "Нет файлов",
|
||||||
"createHistory": "Создаем историю",
|
"historyLinks": "Ссылки в истории",
|
||||||
"noHistory": "Нет истории",
|
"noHistory": "Нет истории",
|
||||||
"directDownloadDescription": "Немедленно пропустите решение и создайте задачи",
|
"directDownloadDescription": "Немедленно пропустите решение и создайте задачи",
|
||||||
"taskProgress": "Прогресс в решении задач",
|
"taskProgress": "Прогресс в решении задач",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} கோப்புகள் தீர்க்கப்பட்டன",
|
"resolvedFileCount": "{count} கோப்புகள் தீர்க்கப்பட்டன",
|
||||||
"resourceHasNoFiles": "வளத்தில் தேர்ந்தெடுக்கக்கூடிய கோப்பு பட்டியல் இல்லை",
|
"resourceHasNoFiles": "வளத்தில் தேர்ந்தெடுக்கக்கூடிய கோப்பு பட்டியல் இல்லை",
|
||||||
"noFiles": "(% 1 மொத்தம்) Items (Folders, Files), Size",
|
"noFiles": "(% 1 மொத்தம்) Items (Folders, Files), Size",
|
||||||
"createHistory": "வரலாற்றை உருவாக்கு",
|
"historyLinks": "வரலாற்று இணைப்புகள்",
|
||||||
"noHistory": "& வரலாறà¯à®±à®¿à®²à¯ à®à®£à¯à®à¯à®ªà®¿à®à®¿... Unlimited (number of lines)",
|
"noHistory": "& வரலாறà¯à®±à®¿à®²à¯ à®à®£à¯à®à¯à®ªà®¿à®à®¿... Unlimited (number of lines)",
|
||||||
"directDownloadDescription": "தீர்க்கப்படுவதைத் தவிர்த்து, உடனடியாகப் பணிகளை உருவாக்கவும்",
|
"directDownloadDescription": "தீர்க்கப்படுவதைத் தவிர்த்து, உடனடியாகப் பணிகளை உருவாக்கவும்",
|
||||||
"taskProgress": "பணி முன்னேற்றம்",
|
"taskProgress": "பணி முன்னேற்றம்",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} dosyaları çözüldü",
|
"resolvedFileCount": "{count} dosyaları çözüldü",
|
||||||
"resourceHasNoFiles": "Kaynak, uygun olmayan dosya listesine sahiptir",
|
"resourceHasNoFiles": "Kaynak, uygun olmayan dosya listesine sahiptir",
|
||||||
"noFiles": "Hiçbir dosya yok",
|
"noFiles": "Hiçbir dosya yok",
|
||||||
"createHistory": "Create History Create History",
|
"historyLinks": "Geçmiş Bağlantıları",
|
||||||
"noHistory": "Geçmiş yok",
|
"noHistory": "Geçmiş yok",
|
||||||
"directDownloadDescription": "Hemen çözümle ve görevleri oluşturun",
|
"directDownloadDescription": "Hemen çözümle ve görevleri oluşturun",
|
||||||
"taskProgress": "Görev ilerlemesi",
|
"taskProgress": "Görev ilerlemesi",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} файли вирішені",
|
"resolvedFileCount": "{count} файли вирішені",
|
||||||
"resourceHasNoFiles": "Ресурс не має вибраного списку файлів",
|
"resourceHasNoFiles": "Ресурс не має вибраного списку файлів",
|
||||||
"noFiles": "Немає файлів",
|
"noFiles": "Немає файлів",
|
||||||
"createHistory": "Історія",
|
"historyLinks": "Посилання історії",
|
||||||
"noHistory": "Історія",
|
"noHistory": "Історія",
|
||||||
"directDownloadDescription": "Вирішити і створювати завдання відразу",
|
"directDownloadDescription": "Вирішити і створювати завдання відразу",
|
||||||
"taskProgress": "Завдання",
|
"taskProgress": "Завдання",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "{count} tập tin được giải quyết",
|
"resolvedFileCount": "{count} tập tin được giải quyết",
|
||||||
"resourceHasNoFiles": "Không thể chọn danh sách tập tin",
|
"resourceHasNoFiles": "Không thể chọn danh sách tập tin",
|
||||||
"noFiles": "Không có tập tin",
|
"noFiles": "Không có tập tin",
|
||||||
"createHistory": "Tạo ra lịch sử",
|
"historyLinks": "Liên kết lịch sử",
|
||||||
"noHistory": "Không có lịch sử",
|
"noHistory": "Không có lịch sử",
|
||||||
"directDownloadDescription": "Bỏ qua việc giải quyết và tạo tác vụ ngay lập tức",
|
"directDownloadDescription": "Bỏ qua việc giải quyết và tạo tác vụ ngay lập tức",
|
||||||
"taskProgress": "Tiến trình tác vụ",
|
"taskProgress": "Tiến trình tác vụ",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "已解析 {count} 个文件",
|
"resolvedFileCount": "已解析 {count} 个文件",
|
||||||
"resourceHasNoFiles": "该资源没有可选择的文件列表",
|
"resourceHasNoFiles": "该资源没有可选择的文件列表",
|
||||||
"noFiles": "没有文件",
|
"noFiles": "没有文件",
|
||||||
"createHistory": "创建历史",
|
"historyLinks": "历史链接",
|
||||||
"noHistory": "暂无历史记录",
|
"noHistory": "暂无历史记录",
|
||||||
"directDownloadDescription": "跳过解析并立即创建任务",
|
"directDownloadDescription": "跳过解析并立即创建任务",
|
||||||
"taskProgress": "任务进度",
|
"taskProgress": "任务进度",
|
||||||
|
|||||||
@@ -353,7 +353,7 @@
|
|||||||
"resolvedFileCount": "已解析 {count} 個檔案",
|
"resolvedFileCount": "已解析 {count} 個檔案",
|
||||||
"resourceHasNoFiles": "該資源沒有可選擇的檔案列表",
|
"resourceHasNoFiles": "該資源沒有可選擇的檔案列表",
|
||||||
"noFiles": "沒有檔案",
|
"noFiles": "沒有檔案",
|
||||||
"createHistory": "建立歷史記錄",
|
"historyLinks": "歷史連結",
|
||||||
"noHistory": "暫無歷史記錄",
|
"noHistory": "暫無歷史記錄",
|
||||||
"directDownloadDescription": "跳過解析並立即建立任務",
|
"directDownloadDescription": "跳過解析並立即建立任務",
|
||||||
"taskProgress": "任務進度",
|
"taskProgress": "任務進度",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@@ -17,6 +18,11 @@ import 'util/util.dart';
|
|||||||
|
|
||||||
Future<void> main(List<String> args) async {
|
Future<void> main(List<String> args) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
if (Util.isMacos()) {
|
||||||
|
// Gopeed is not sandboxed. Configure each engine before the main/child
|
||||||
|
// window split so native file and directory dialogs work in both windows.
|
||||||
|
await FilePicker.skipEntitlementsChecks();
|
||||||
|
}
|
||||||
if (Util.isAndroid()) {
|
if (Util.isAndroid()) {
|
||||||
FlutterForegroundTask.initCommunicationPort();
|
FlutterForegroundTask.initCommunicationPort();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart' show Icons;
|
import 'package:flutter/material.dart' show Icons;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||||
|
|
||||||
import '../../core/utils/breakpoints.dart';
|
import '../../core/utils/breakpoints.dart';
|
||||||
|
import '../../util/util.dart';
|
||||||
import '../services/download_directory_picker.dart';
|
import '../services/download_directory_picker.dart';
|
||||||
import 'app_text_field.dart';
|
import 'app_text_field.dart';
|
||||||
import 'app_tooltip.dart';
|
import 'app_tooltip.dart';
|
||||||
@@ -24,7 +26,28 @@ class AppPathPickerField extends StatefulWidget {
|
|||||||
this.padding,
|
this.padding,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.pickerButtonStyle = AppPathPickerButtonStyle.ghost,
|
this.pickerButtonStyle = AppPathPickerButtonStyle.ghost,
|
||||||
}) : platformDownloadDirectory = false,
|
}) : platformFile = false,
|
||||||
|
platformDownloadDirectory = false,
|
||||||
|
onDirectoryPicked = null,
|
||||||
|
allowAndroidEditing = false;
|
||||||
|
|
||||||
|
/// Selects a native desktop file path; Web and mobile accept manual paths only.
|
||||||
|
const AppPathPickerField.file({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
this.fieldKey,
|
||||||
|
this.pickerKey,
|
||||||
|
this.hintText,
|
||||||
|
this.desktopWidth,
|
||||||
|
this.filled,
|
||||||
|
this.border,
|
||||||
|
this.borderRadius,
|
||||||
|
this.padding,
|
||||||
|
this.onChanged,
|
||||||
|
this.pickerButtonStyle = AppPathPickerButtonStyle.outline,
|
||||||
|
}) : onPick = null,
|
||||||
|
platformFile = true,
|
||||||
|
platformDownloadDirectory = false,
|
||||||
onDirectoryPicked = null,
|
onDirectoryPicked = null,
|
||||||
allowAndroidEditing = false;
|
allowAndroidEditing = false;
|
||||||
|
|
||||||
@@ -44,6 +67,7 @@ class AppPathPickerField extends StatefulWidget {
|
|||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.pickerButtonStyle = AppPathPickerButtonStyle.outline,
|
this.pickerButtonStyle = AppPathPickerButtonStyle.outline,
|
||||||
}) : onPick = null,
|
}) : onPick = null,
|
||||||
|
platformFile = false,
|
||||||
platformDownloadDirectory = true;
|
platformDownloadDirectory = true;
|
||||||
|
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
@@ -58,6 +82,7 @@ class AppPathPickerField extends StatefulWidget {
|
|||||||
final EdgeInsetsGeometry? padding;
|
final EdgeInsetsGeometry? padding;
|
||||||
final ValueChanged<String>? onChanged;
|
final ValueChanged<String>? onChanged;
|
||||||
final AppPathPickerButtonStyle pickerButtonStyle;
|
final AppPathPickerButtonStyle pickerButtonStyle;
|
||||||
|
final bool platformFile;
|
||||||
final bool platformDownloadDirectory;
|
final bool platformDownloadDirectory;
|
||||||
final ValueChanged<String>? onDirectoryPicked;
|
final ValueChanged<String>? onDirectoryPicked;
|
||||||
final bool allowAndroidEditing;
|
final bool allowAndroidEditing;
|
||||||
@@ -97,6 +122,16 @@ class _AppPathPickerFieldState extends State<AppPathPickerField> {
|
|||||||
setState(() => _tooltipPath = path);
|
setState(() => _tooltipPath = path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _pickFile() async {
|
||||||
|
if (!Util.isDesktop()) return;
|
||||||
|
final file = await FilePicker.pickFile();
|
||||||
|
if (!mounted) return;
|
||||||
|
final path = file?.path;
|
||||||
|
if (path == null || path.isEmpty || path == widget.controller.text) return;
|
||||||
|
widget.controller.text = path;
|
||||||
|
widget.onChanged?.call(path);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _pickPlatformDirectory() async {
|
Future<void> _pickPlatformDirectory() async {
|
||||||
final path = await DownloadDirectoryPicker.pick(context, currentPath: widget.controller.text.trim());
|
final path = await DownloadDirectoryPicker.pick(context, currentPath: widget.controller.text.trim());
|
||||||
if (!mounted || path == null || path.isEmpty || path == widget.controller.text) return;
|
if (!mounted || path == null || path.isEmpty || path == widget.controller.text) return;
|
||||||
@@ -110,6 +145,8 @@ class _AppPathPickerFieldState extends State<AppPathPickerField> {
|
|||||||
final desktop = MediaQuery.sizeOf(context).width >= Breakpoints.mobile;
|
final desktop = MediaQuery.sizeOf(context).width >= Breakpoints.mobile;
|
||||||
final onPick = widget.platformDownloadDirectory
|
final onPick = widget.platformDownloadDirectory
|
||||||
? (DownloadDirectoryPicker.canPick ? _pickPlatformDirectory : null)
|
? (DownloadDirectoryPicker.canPick ? _pickPlatformDirectory : null)
|
||||||
|
: widget.platformFile
|
||||||
|
? (Util.isDesktop() ? _pickFile : null)
|
||||||
: widget.onPick;
|
: widget.onPick;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: desktop ? widget.desktopWidth : double.infinity,
|
width: desktop ? widget.desktopWidth : double.infinity,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 2.0.0-beta.1+1
|
version: 2.0.0-beta.2+2
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.4
|
sdk: ^3.11.4
|
||||||
|
|||||||
@@ -499,11 +499,12 @@ void main() {
|
|||||||
|
|
||||||
expect(_fieldText(tester, 'create-task-rename-input'), isEmpty);
|
expect(_fieldText(tester, 'create-task-rename-input'), isEmpty);
|
||||||
final createButton = tester.widget<AppLoadingButton>(find.byKey(const ValueKey('resolve-create-button')));
|
final createButton = tester.widget<AppLoadingButton>(find.byKey(const ValueKey('resolve-create-button')));
|
||||||
expect(createButton.child, isA<Text>());
|
expect(createButton.child, isA<SizedBox>());
|
||||||
final createText = createButton.child as Text;
|
expect((createButton.child as SizedBox).width, 68);
|
||||||
expect(createText.data, 'Create');
|
expect(
|
||||||
expect(createText.maxLines, 1);
|
find.descendant(of: find.byKey(const ValueKey('resolve-create-button')), matching: find.text('Create')),
|
||||||
expect(createText.softWrap, isFalse);
|
findsOneWidget,
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
tester.getSize(find.byKey(const ValueKey('resolve-cancel-button'))),
|
tester.getSize(find.byKey(const ValueKey('resolve-cancel-button'))),
|
||||||
tester.getSize(find.byKey(const ValueKey('resolve-create-button'))),
|
tester.getSize(find.byKey(const ValueKey('resolve-create-button'))),
|
||||||
@@ -555,6 +556,7 @@ void main() {
|
|||||||
|
|
||||||
await tester.tap(find.byIcon(Icons.history));
|
await tester.tap(find.byIcon(Icons.history));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('History Links'), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
tester.getSize(find.byKey(const ValueKey('create-history-title-bar'))).width,
|
tester.getSize(find.byKey(const ValueKey('create-history-title-bar'))).width,
|
||||||
tester.getSize(find.byKey(const ValueKey('create-history-content'))).width,
|
tester.getSize(find.byKey(const ValueKey('create-history-content'))).width,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:gopeed/features/tasks/presentation/widgets/resolve_task_actions.dart';
|
import 'package:gopeed/features/tasks/presentation/widgets/create_task_action_buttons.dart';
|
||||||
import 'package:gopeed/l10n/l10n.dart';
|
import 'package:gopeed/l10n/l10n.dart';
|
||||||
import 'package:gopeed/shared/theme/app_component_themes.dart';
|
import 'package:gopeed/shared/theme/app_component_themes.dart';
|
||||||
import 'package:gopeed/shared/theme/app_theme.dart';
|
import 'package:gopeed/shared/theme/app_theme.dart';
|
||||||
@@ -36,10 +36,14 @@ void main() {
|
|||||||
content: SizedBox(width: width < 760 ? width - 32 : 720, height: 100),
|
content: SizedBox(width: width < 760 ? width - 32 : 720, height: 100),
|
||||||
actions: [
|
actions: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ResolveTaskActions(
|
child: CreateTaskActionButtons(
|
||||||
submitting: submitting,
|
submitting: submitting,
|
||||||
onCancel: () => cancelled = true,
|
onCancel: () => cancelled = true,
|
||||||
onCreate: () => setState(() => submitting = true),
|
onSubmit: () => setState(() => submitting = true),
|
||||||
|
cancelLabel: context.l10n.cancel,
|
||||||
|
submitLabel: context.l10n.createAction,
|
||||||
|
cancelButtonKey: const ValueKey('resolve-cancel-button'),
|
||||||
|
submitButtonKey: const ValueKey('resolve-create-button'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -58,6 +62,9 @@ void main() {
|
|||||||
final createRect = tester.getRect(create);
|
final createRect = tester.getRect(create);
|
||||||
expect(cancelRect.size, createRect.size);
|
expect(cancelRect.size, createRect.size);
|
||||||
expect(cancelRect.center.dy, createRect.center.dy);
|
expect(cancelRect.center.dy, createRect.center.dy);
|
||||||
|
expect(createRect.left - cancelRect.right, 12);
|
||||||
|
expect((tester.widget<shad.SecondaryButton>(cancel).child as SizedBox).width, 68);
|
||||||
|
expect((tester.widget<AppLoadingButton>(create).child as SizedBox).width, 68);
|
||||||
expect(cancelRect.left, greaterThanOrEqualTo(0));
|
expect(cancelRect.left, greaterThanOrEqualTo(0));
|
||||||
expect(createRect.right, lessThanOrEqualTo(width));
|
expect(createRect.right, lessThanOrEqualTo(width));
|
||||||
expect(find.text(appLocalizationsFor(language).createAction), findsOneWidget);
|
expect(find.text(appLocalizationsFor(language).createAction), findsOneWidget);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:gopeed/shared/theme/app_component_themes.dart';
|
||||||
|
import 'package:gopeed/shared/theme/app_theme.dart';
|
||||||
|
import 'package:gopeed/shared/widgets/app_path_picker_field.dart';
|
||||||
|
import 'package:gopeed/shared/widgets/app_text_field.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart' as shad;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('file paths remain editable and Web hides native file selection', (tester) async {
|
||||||
|
final controller = TextEditingController(text: '/scripts/old.sh');
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
shad.ShadcnApp(
|
||||||
|
theme: AppTheme.light(),
|
||||||
|
materialTheme: AppTheme.materialLight(),
|
||||||
|
home: AppComponentThemes(
|
||||||
|
child: Center(
|
||||||
|
child: AppPathPickerField.file(
|
||||||
|
controller: controller,
|
||||||
|
desktopWidth: 320,
|
||||||
|
pickerKey: const ValueKey('file-picker'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(const ValueKey('file-picker')), kIsWeb ? findsNothing : findsOneWidget);
|
||||||
|
expect(tester.widget<AppTextField>(find.byType(AppTextField)).readOnly, isFalse);
|
||||||
|
await tester.enterText(find.byType(EditableText), '/scripts/custom.sh');
|
||||||
|
await tester.pump();
|
||||||
|
expect(controller.text, '/scripts/custom.sh');
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2669,9 +2669,9 @@ void main() {
|
|||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: ResolveFileTree(
|
child: ResolveFileTree(
|
||||||
files: [
|
files: [
|
||||||
FileInfo(path: 'videos', name: 'clip.mp4', size: 1024),
|
FileInfo(path: 'videos', name: 'clip.mp4', size: 0),
|
||||||
FileInfo(path: 'docs', name: 'notes.txt', size: 128),
|
FileInfo(path: 'docs', name: 'notes.txt', size: 0),
|
||||||
FileInfo(path: 'docs', name: 'manual.pdf', size: 256),
|
FileInfo(path: 'docs', name: 'manual.pdf', size: 0),
|
||||||
],
|
],
|
||||||
initialSelection: const [0, 1, 2],
|
initialSelection: const [0, 1, 2],
|
||||||
onSelectionChanged: (values) => selected = values,
|
onSelectionChanged: (values) => selected = values,
|
||||||
@@ -2708,7 +2708,26 @@ void main() {
|
|||||||
expect(find.byKey(const ValueKey('resolve-tree-expand-toggle')), findsOneWidget);
|
expect(find.byKey(const ValueKey('resolve-tree-expand-toggle')), findsOneWidget);
|
||||||
expect(find.byType(shad.OutlinedContainer), findsOneWidget);
|
expect(find.byType(shad.OutlinedContainer), findsOneWidget);
|
||||||
expect(find.text('clip.mp4'), findsOneWidget);
|
expect(find.text('clip.mp4'), findsOneWidget);
|
||||||
|
expect(find.text('Unknown size'), findsOneWidget);
|
||||||
expect(tester.widget<Text>(find.text('clip.mp4')).style?.color, AppPalette.light.textPrimary);
|
expect(tester.widget<Text>(find.text('clip.mp4')).style?.color, AppPalette.light.textPrimary);
|
||||||
|
final baselines = tester
|
||||||
|
.widgetList<Baseline>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(const ValueKey('resolve-tree-selection-stats')),
|
||||||
|
matching: find.byType(Baseline),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
expect(baselines, hasLength(2));
|
||||||
|
expect(baselines.first.baseline, baselines.last.baseline);
|
||||||
|
expect(baselines.every((widget) => widget.baselineType == TextBaseline.alphabetic), isTrue);
|
||||||
|
final filtersRect = tester.getRect(find.byKey(const ValueKey('resolve-tree-filters')));
|
||||||
|
final statsRect = tester.getRect(find.byKey(const ValueKey('resolve-tree-selection-stats')));
|
||||||
|
final selectedCountRect = tester.getRect(find.byKey(const ValueKey('resolve-tree-selected-count')));
|
||||||
|
final selectedSizeRect = tester.getRect(find.byKey(const ValueKey('resolve-tree-selected-size')));
|
||||||
|
expect(statsRect.center.dy, closeTo(filtersRect.center.dy, 0.01));
|
||||||
|
expect(selectedCountRect.center.dy, closeTo(selectedSizeRect.center.dy, 0.01));
|
||||||
|
expect(selectedSizeRect.left, greaterThan(selectedCountRect.right));
|
||||||
|
|
||||||
await tester.tap(find.byKey(const ValueKey('resolve-tree-expand-toggle')));
|
await tester.tap(find.byKey(const ValueKey('resolve-tree-expand-toggle')));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
@@ -2770,8 +2789,16 @@ void main() {
|
|||||||
expect(find.text('clip.mp4'), findsOneWidget);
|
expect(find.text('clip.mp4'), findsOneWidget);
|
||||||
expect(selected, [0, 1, 2]);
|
expect(selected, [0, 1, 2]);
|
||||||
|
|
||||||
tester.view.physicalSize = const Size(360, 620);
|
tester.view.physicalSize = const Size(280, 620);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
expect(
|
||||||
|
tester.getRect(find.byKey(const ValueKey('resolve-tree-selection-stats'))).top,
|
||||||
|
greaterThanOrEqualTo(tester.getRect(find.byKey(const ValueKey('resolve-tree-filters'))).bottom),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester.getRect(find.byKey(const ValueKey('resolve-tree-selected-size'))).top,
|
||||||
|
greaterThanOrEqualTo(tester.getRect(find.byKey(const ValueKey('resolve-tree-selected-count'))).bottom),
|
||||||
|
);
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||