From f00124c1c0cf3ec59cac5e53bfe5d01d0e8e0c0e Mon Sep 17 00:00:00 2001 From: admin_gitea Date: Sun, 28 Jun 2026 08:56:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E9=80=82=E9=85=8D=E6=80=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unified-management/internal/db/models.go | 1 + .../unified-management/internal/db/schema.go | 73 ++++++++-- .../unified-management/internal/db/store.go | 5 + .../internal/db/store_test.go | 45 ++++++ .../internal/web/admin_system_routes.go | 6 +- .../web/admin/src/views/SystemView.vue | 8 +- .../Feedback/FeedbackSubmissionService.cs | 2 +- .../Media/RemoteMediaCatalog.cs | 2 + .../Media/RemoteMediaResolver.cs | 13 +- src/YMhut.Box.Tests/FeedbackServiceTests.cs | 6 + .../RemoteMediaCatalogTests.cs | 1 + .../RemoteMediaResolverTests.cs | 41 ++++++ src/box-winUI/Assets/LockScreenLogo.png | Bin 2130 -> 2204 bytes src/box-winUI/Assets/Square150x150Logo.png | Bin 4895 -> 5029 bytes src/box-winUI/Assets/Square44x44Logo.png | Bin 1404 -> 1440 bytes src/box-winUI/Assets/StoreLogo.png | Bin 1556 -> 1596 bytes src/box-winUI/Assets/Wide310x150Logo.png | Bin 6569 -> 6546 bytes src/box-winUI/Assets/tool-results/result.css | 52 ++++++- src/box-winUI/Assets/tool-results/result.js | 135 +++++++++++++----- src/box-winUI/Views/Tools/AdaptiveToolPage.cs | 46 +++++- src/box-winUI/Views/Tools/RandomCinemaPage.cs | 55 ++++++- 21 files changed, 428 insertions(+), 63 deletions(-) diff --git a/server/unified-management/internal/db/models.go b/server/unified-management/internal/db/models.go index ee5d2b4..54bee2b 100644 --- a/server/unified-management/internal/db/models.go +++ b/server/unified-management/internal/db/models.go @@ -22,6 +22,7 @@ type adminRow struct { type DatabaseStatus struct { ActiveProvider string `json:"activeProvider"` ConfigProvider string `json:"configProvider"` + SchemaVersion string `json:"schemaVersion"` SQLiteReady bool `json:"sqliteReady"` RemoteReady bool `json:"remoteReady"` FailoverActive bool `json:"failoverActive"` diff --git a/server/unified-management/internal/db/schema.go b/server/unified-management/internal/db/schema.go index 4ed41ba..b1f8345 100644 --- a/server/unified-management/internal/db/schema.go +++ b/server/unified-management/internal/db/schema.go @@ -22,6 +22,9 @@ func (s *Store) migrate(conn *sql.DB, d dialect) error { return err } } + if err := createSchemaIndexes(conn, d); err != nil { + return err + } return s.recordSchemaVersion(conn, d) } @@ -281,21 +284,67 @@ func schemaStatements(d dialect) []string { created_at %s NOT NULL, finished_at %s NOT NULL DEFAULT '' )`, d.idType(), keyText, keyText, keyText, longText, shortText, shortText, shortText), - `CREATE INDEX IF NOT EXISTS idx_feedback_tickets_activity ON feedback_tickets(last_activity_at)`, - `CREATE INDEX IF NOT EXISTS idx_feedback_comments_code ON feedback_comments(feedback_code)`, - `CREATE INDEX IF NOT EXISTS idx_feedback_attachments_code ON feedback_attachments(feedback_code)`, - `CREATE INDEX IF NOT EXISTS idx_feedback_events_code ON feedback_events(feedback_code)`, - `CREATE INDEX IF NOT EXISTS idx_mail_records_code ON mail_records(feedback_code)`, - `CREATE INDEX IF NOT EXISTS idx_endpoint_call_logs_source ON endpoint_call_logs(source_id)`, - `CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at)`, - `CREATE INDEX IF NOT EXISTS idx_audit_logs_type ON audit_logs(type)`, - `CREATE INDEX IF NOT EXISTS idx_audit_logs_target ON audit_logs(target)`, - `CREATE INDEX IF NOT EXISTS idx_legacy_json_revisions_name ON legacy_json_revisions(name, id)`, - `CREATE INDEX IF NOT EXISTS idx_release_notices_version ON release_notices(version)`, - `CREATE INDEX IF NOT EXISTS idx_release_notice_revisions_version ON release_notice_revisions(version, id)`, } } +type schemaIndex struct { + name string + table string + columns string +} + +func schemaIndexes() []schemaIndex { + return []schemaIndex{ + {name: "idx_feedback_tickets_activity", table: "feedback_tickets", columns: "last_activity_at"}, + {name: "idx_feedback_comments_code", table: "feedback_comments", columns: "feedback_code"}, + {name: "idx_feedback_attachments_code", table: "feedback_attachments", columns: "feedback_code"}, + {name: "idx_feedback_events_code", table: "feedback_events", columns: "feedback_code"}, + {name: "idx_mail_records_code", table: "mail_records", columns: "feedback_code"}, + {name: "idx_endpoint_call_logs_source", table: "endpoint_call_logs", columns: "source_id"}, + {name: "idx_audit_logs_created", table: "audit_logs", columns: "created_at"}, + {name: "idx_audit_logs_type", table: "audit_logs", columns: "type"}, + {name: "idx_audit_logs_target", table: "audit_logs", columns: "target"}, + {name: "idx_legacy_json_revisions_name", table: "legacy_json_revisions", columns: "name, id"}, + {name: "idx_release_notices_version", table: "release_notices", columns: "version"}, + {name: "idx_release_notice_revisions_version", table: "release_notice_revisions", columns: "version, id"}, + } +} + +func createSchemaIndexes(conn *sql.DB, d dialect) error { + for _, index := range schemaIndexes() { + if d.name == "mysql" { + exists, err := mysqlIndexExists(conn, index) + if err != nil { + return err + } + if exists { + continue + } + } + if _, err := conn.Exec(d.rebind(createIndexStatement(d, index))); err != nil { + return err + } + } + return nil +} + +func mysqlIndexExists(conn *sql.DB, index schemaIndex) (bool, error) { + var count int + err := conn.QueryRow(`SELECT COUNT(1) + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = ? + AND index_name = ?`, index.table, index.name).Scan(&count) + return count > 0, err +} + +func createIndexStatement(d dialect, index schemaIndex) string { + if d.name == "mysql" { + return fmt.Sprintf("CREATE INDEX %s ON %s(%s)", d.quoteIdent(index.name), d.quoteIdent(index.table), index.columns) + } + return fmt.Sprintf("CREATE INDEX IF NOT EXISTS %s ON %s(%s)", d.quoteIdent(index.name), d.quoteIdent(index.table), index.columns) +} + func (s *Store) recordSchemaVersion(conn *sql.DB, d dialect) error { columns := []string{"version", "applied_at", "description"} _, err := conn.Exec(d.rebind(d.upsert("schema_migrations", columns, []string{"version"})), diff --git a/server/unified-management/internal/db/store.go b/server/unified-management/internal/db/store.go index 4e29184..8a17753 100644 --- a/server/unified-management/internal/db/store.go +++ b/server/unified-management/internal/db/store.go @@ -66,6 +66,7 @@ func Open(cfg *config.Config) (*Store, error) { status: DatabaseStatus{ ActiveProvider: "sqlite", ConfigProvider: cfg.Database.Provider, + SchemaVersion: CurrentSchemaVersion, SQLiteReady: true, LastRecoveredAt: Now(), }, @@ -165,6 +166,7 @@ func (s *Store) ReconfigureDatabase(cfg *config.Config) error { s.remoteDB = remote s.remoteDialect = remoteDialect s.status.ConfigProvider = cfg.Database.Provider + s.status.SchemaVersion = CurrentSchemaVersion s.status.SQLiteReady = true s.status.RemoteReady = remote != nil s.status.LastError = "" @@ -269,6 +271,7 @@ func (s *Store) openRemote() error { s.dialect = remoteDialect s.status.ActiveProvider = "mysql" s.status.ConfigProvider = "mysql" + s.status.SchemaVersion = CurrentSchemaVersion s.status.RemoteReady = true s.status.FailoverActive = false s.status.LastError = "" @@ -304,6 +307,7 @@ func (s *Store) checkRemote() { } s.status.ActiveProvider = "mysql" s.status.RemoteReady = true + s.status.SchemaVersion = CurrentSchemaVersion s.status.FailoverActive = false s.status.LastError = "" s.status.LastRecoveredAt = Now() @@ -320,6 +324,7 @@ func (s *Store) markFailover(err error) { s.dialect = s.localDialect s.status.ActiveProvider = "sqlite" s.status.ConfigProvider = s.cfg.Database.Provider + s.status.SchemaVersion = CurrentSchemaVersion s.status.RemoteReady = false s.status.FailoverActive = !strings.EqualFold(s.cfg.Database.Provider, "sqlite") s.status.LastError = err.Error() diff --git a/server/unified-management/internal/db/store_test.go b/server/unified-management/internal/db/store_test.go index 3478ae3..4f5b961 100644 --- a/server/unified-management/internal/db/store_test.go +++ b/server/unified-management/internal/db/store_test.go @@ -138,6 +138,51 @@ func TestOpenRecordsCurrentSchemaVersion(t *testing.T) { } } +func TestSchemaIndexStatementsAreDialectAware(t *testing.T) { + mysql := dialectFor("mysql") + sqlite := dialectFor("sqlite") + for _, statement := range schemaStatements(mysql) { + if strings.Contains(strings.ToUpper(statement), "CREATE INDEX IF NOT EXISTS") { + t.Fatalf("mysql schema statement contains unsupported index syntax: %s", statement) + } + } + + index := schemaIndexes()[0] + mysqlStatement := createIndexStatement(mysql, index) + if strings.Contains(strings.ToUpper(mysqlStatement), "IF NOT EXISTS") { + t.Fatalf("mysql index statement contains unsupported syntax: %s", mysqlStatement) + } + if !strings.Contains(createIndexStatement(sqlite, index), "IF NOT EXISTS") { + t.Fatalf("sqlite index statement should remain idempotent") + } +} + +func TestMigrateCreatesSQLiteIndexesIdempotently(t *testing.T) { + root := t.TempDir() + conn, err := sql.Open("sqlite", filepath.Join(root, "indexes.sqlite")) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + store := &Store{} + d := dialectFor("sqlite") + if err := store.migrate(conn, d); err != nil { + t.Fatal(err) + } + if err := store.migrate(conn, d); err != nil { + t.Fatal(err) + } + + var name string + if err := conn.QueryRow(`SELECT name FROM sqlite_master WHERE type = 'index' AND name = ?`, "idx_feedback_tickets_activity").Scan(&name); err != nil { + t.Fatal(err) + } + if name != "idx_feedback_tickets_activity" { + t.Fatalf("unexpected index name %q", name) + } +} + func TestChangeAdminPasswordPersistsWhenRemoteSyncFails(t *testing.T) { root := t.TempDir() path := filepath.Join(root, "unified.sqlite") diff --git a/server/unified-management/internal/web/admin_system_routes.go b/server/unified-management/internal/web/admin_system_routes.go index 91f324a..9e7f01c 100644 --- a/server/unified-management/internal/web/admin_system_routes.go +++ b/server/unified-management/internal/web/admin_system_routes.go @@ -271,12 +271,16 @@ func (r *router) handleMigrationStatus(w http.ResponseWriter, req *http.Request) "fileAssets": []map[string]string{ {"name": "downloads", "path": r.cfg.DownloadsDir, "description": "发布包和下载文件"}, {"name": "update public", "path": r.cfg.UpdatePublicDir, "description": "旧客户端兼容 JSON 生成物"}, - {"name": "feedback packages", "path": filepath.Join(r.cfg.StorageDir, "feedback-packages"), "description": "反馈附件包"}, + {"name": "feedback packages", "path": filepath.Join(r.cfg.StorageDir, "feedback"), "description": "反馈加密包和解密后的本地包"}, }, "sqlitePath": r.store.Path(), "mysql": config.SafeDatabase(r.cfg.BaseDir, r.cfg.Database), + "schemaVersion": status.SchemaVersion, "lastSyncAt": status.LastSyncAt, "lastSyncError": status.LastSyncError, + "lastError": status.LastError, + "failoverActive": status.FailoverActive, + "remoteReady": status.RemoteReady, "activeProvider": status.ActiveProvider, }, }) diff --git a/server/unified-management/web/admin/src/views/SystemView.vue b/server/unified-management/web/admin/src/views/SystemView.vue index 641650f..5f6163a 100644 --- a/server/unified-management/web/admin/src/views/SystemView.vue +++ b/server/unified-management/web/admin/src/views/SystemView.vue @@ -30,9 +30,12 @@ const tabs = [
配置类型{{ ctx.database?.configProvider || "-" }} + Schema{{ ctx.database?.schemaVersion || "-" }} + 活动数据库{{ ctx.database?.activeProvider || "-" }} SQLite{{ ctx.database?.sqliteReady ? "ready" : "missing" }} MySQL{{ ctx.database?.remoteReady ? "ready" : "offline" }} Failover{{ ctx.database?.failoverActive ? "active" : "standby" }} + 恢复时间{{ ctx.database?.lastRecoveredAt || "-" }} 最后同步{{ ctx.database?.lastSyncAt || "-" }} 最近错误{{ ctx.database?.lastSyncError || ctx.database?.lastError || "-" }}
@@ -98,9 +101,12 @@ const tabs = [
策略{{ ctx.migrationStatus?.strategy || "-" }} SQLite 文件{{ ctx.migrationStatus?.sqlitePath || "-" }} + Schema{{ ctx.migrationStatus?.schemaVersion || "-" }} 活动数据库{{ ctx.migrationStatus?.activeProvider || "-" }} + MySQL{{ ctx.migrationStatus?.remoteReady ? "ready" : "offline" }} + Failover{{ ctx.migrationStatus?.failoverActive ? "active" : "standby" }} 最后同步{{ ctx.migrationStatus?.lastSyncAt || "-" }} - 同步错误{{ ctx.migrationStatus?.lastSyncError || "-" }} + 同步错误{{ ctx.migrationStatus?.lastSyncError || ctx.migrationStatus?.lastError || "-" }}
diff --git a/src/YMhut.Box.Core/Feedback/FeedbackSubmissionService.cs b/src/YMhut.Box.Core/Feedback/FeedbackSubmissionService.cs index 332ff5b..a67b31d 100644 --- a/src/YMhut.Box.Core/Feedback/FeedbackSubmissionService.cs +++ b/src/YMhut.Box.Core/Feedback/FeedbackSubmissionService.cs @@ -7,7 +7,7 @@ namespace YMhut.Box.Core.Feedback; public sealed class FeedbackSubmissionService(HttpClient? httpClient = null) : IFeedbackSubmissionService { - public const string Endpoint = "https://mail-smtp.ymhut.cn/"; + public const string Endpoint = "https://update.ymhut.cn/"; public const string ClientSignatureKey = "ymhut-box-feedback-client-v1"; private readonly HttpClient _httpClient = httpClient ?? new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; diff --git a/src/YMhut.Box.Core/Media/RemoteMediaCatalog.cs b/src/YMhut.Box.Core/Media/RemoteMediaCatalog.cs index dabb831..16e98f2 100644 --- a/src/YMhut.Box.Core/Media/RemoteMediaCatalog.cs +++ b/src/YMhut.Box.Core/Media/RemoteMediaCatalog.cs @@ -72,6 +72,8 @@ public sealed record RemoteMediaSource( { public string EffectiveApiUrl => string.IsNullOrWhiteSpace(ResolvedUrl) ? ApiUrl : ResolvedUrl; + public string RefreshApiUrl => string.IsNullOrWhiteSpace(ApiUrl) ? EffectiveApiUrl : ApiUrl; + public bool IsAvailable => Uri.TryCreate(EffectiveApiUrl, UriKind.Absolute, out _); public string DisplayName => RemoteMediaCatalogNames.SourceName(Id, Name); diff --git a/src/YMhut.Box.Core/Media/RemoteMediaResolver.cs b/src/YMhut.Box.Core/Media/RemoteMediaResolver.cs index 8da94b7..b2cf789 100644 --- a/src/YMhut.Box.Core/Media/RemoteMediaResolver.cs +++ b/src/YMhut.Box.Core/Media/RemoteMediaResolver.cs @@ -30,6 +30,7 @@ public sealed class RemoteMediaResolver : IRemoteMediaResolver { private const int MaxMediaRedirects = 8; private const long MaxTextProbeLength = 2 * 1024 * 1024; + private static long CacheBustSequence; private static readonly Regex AbsoluteUrlRegex = new(@"https?://[^\s""'<>\\]+", RegexOptions.IgnoreCase | RegexOptions.Compiled); private readonly Func? _handlerFactory; @@ -161,6 +162,14 @@ public sealed class RemoteMediaResolver : IRemoteMediaResolver AddHeader(client.DefaultRequestHeaders.UserAgent, "YMhutBox/2.0"); client.DefaultRequestHeaders.Accept.ParseAdd("image/*, video/*, audio/*, application/json, text/plain, text/html, application/octet-stream, */*"); client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("zh-CN,zh;q=0.9,en;q=0.8"); + client.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue + { + NoCache = true, + NoStore = true, + MaxAge = TimeSpan.Zero + }; + client.DefaultRequestHeaders.Pragma.ParseAdd("no-cache"); + client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.UnixEpoch; return client; } @@ -180,7 +189,9 @@ public sealed class RemoteMediaResolver : IRemoteMediaResolver } var separator = url.Contains('?', StringComparison.Ordinal) ? '&' : '?'; - return $"{url}{separator}_={DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; + var sequence = global::System.Threading.Interlocked.Increment(ref CacheBustSequence); + var token = $"{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}-{sequence}"; + return $"{url}{separator}_={Uri.EscapeDataString(token)}"; } private static RemoteMediaResolution FromUriOnly(Uri uri, RemoteMediaKind expectedKind) diff --git a/src/YMhut.Box.Tests/FeedbackServiceTests.cs b/src/YMhut.Box.Tests/FeedbackServiceTests.cs index 3362a71..828c7f2 100644 --- a/src/YMhut.Box.Tests/FeedbackServiceTests.cs +++ b/src/YMhut.Box.Tests/FeedbackServiceTests.cs @@ -168,6 +168,12 @@ public sealed class FeedbackServiceTests Assert.AreEqual(signature, FeedbackSubmissionService.Sign("1760000000", "abc123", new string('a', 64), "{\"ok\":true}")); } + [TestMethod] + public void FeedbackSubmissionUsesUnifiedManagementEndpoint() + { + Assert.AreEqual("https://update.ymhut.cn/", FeedbackSubmissionService.Endpoint); + } + [TestMethod] public void FeedbackStatusResponseParsesExtendedServerFields() { diff --git a/src/YMhut.Box.Tests/RemoteMediaCatalogTests.cs b/src/YMhut.Box.Tests/RemoteMediaCatalogTests.cs index 2b98651..e16792b 100644 --- a/src/YMhut.Box.Tests/RemoteMediaCatalogTests.cs +++ b/src/YMhut.Box.Tests/RemoteMediaCatalogTests.cs @@ -109,6 +109,7 @@ public sealed class RemoteMediaCatalogTests Assert.AreEqual("data.cover", source.ResolvedKey); Assert.AreEqual("image", source.MediaType); Assert.AreEqual(source.ResolvedUrl, source.EffectiveApiUrl); + Assert.AreEqual(source.ApiUrl, source.RefreshApiUrl); Assert.IsTrue(source.IsAvailable); } diff --git a/src/YMhut.Box.Tests/RemoteMediaResolverTests.cs b/src/YMhut.Box.Tests/RemoteMediaResolverTests.cs index 5cfaa25..41f7193 100644 --- a/src/YMhut.Box.Tests/RemoteMediaResolverTests.cs +++ b/src/YMhut.Box.Tests/RemoteMediaResolverTests.cs @@ -121,6 +121,29 @@ public sealed class RemoteMediaResolverTests Assert.AreEqual(".mp3", result.SuggestedExtension); } + [TestMethod] + public async Task CacheBustsEachRefreshAndDisablesHttpCaches() + { + var observed = new List(); + var resolver = CreateResolver(request => + { + observed.Add(ObservedRequest.From(request)); + return Text(HttpStatusCode.OK, string.Empty, "image/jpeg"); + }); + + await resolver.ResolveMediaAsync("https://example.test/random", RemoteMediaKind.Image); + await resolver.ResolveMediaAsync("https://example.test/random", RemoteMediaKind.Image); + + var randomRequests = observed.Where(item => item.Uri.AbsolutePath == "/random").ToArray(); + Assert.IsTrue(randomRequests.Length >= 4); + Assert.AreNotEqual(randomRequests[0].Uri.Query, randomRequests[2].Uri.Query); + Assert.IsTrue(randomRequests.All(item => item.NoCache)); + Assert.IsTrue(randomRequests.All(item => item.NoStore)); + Assert.IsTrue(randomRequests.All(item => item.MaxAge == TimeSpan.Zero)); + Assert.IsTrue(randomRequests.All(item => item.PragmaNoCache)); + Assert.IsTrue(randomRequests.All(item => item.IfModifiedSince == DateTimeOffset.UnixEpoch)); + } + private static RemoteMediaResolver CreateResolver(Func responseFactory) { return new RemoteMediaResolver(() => new StubHttpHandler(responseFactory)); @@ -150,4 +173,22 @@ public sealed class RemoteMediaResolverTests return Task.FromResult(response); } } + + private sealed record ObservedRequest( + Uri Uri, + bool NoCache, + bool NoStore, + TimeSpan? MaxAge, + bool PragmaNoCache, + DateTimeOffset? IfModifiedSince) + { + public static ObservedRequest From(HttpRequestMessage request) + => new( + request.RequestUri!, + request.Headers.CacheControl?.NoCache == true, + request.Headers.CacheControl?.NoStore == true, + request.Headers.CacheControl?.MaxAge, + request.Headers.Pragma.Any(value => string.Equals(value.Name, "no-cache", StringComparison.OrdinalIgnoreCase)), + request.Headers.IfModifiedSince); + } } diff --git a/src/box-winUI/Assets/LockScreenLogo.png b/src/box-winUI/Assets/LockScreenLogo.png index c0e57c6d0b8ceb4a768a4357b6e2f158b1dbeeaa..f56993ce525eb866cca1cb2704a3abd213b8aa5c 100644 GIT binary patch delta 2142 zcmV-k2%-1V5S$T^ReuOENkl|4 z1Q~m_YkFpEgE2W+B8p@X0wluIdwP0+_`swOlK222Bfww~!sKXjHona{RkwDkrhC@A zD?B^9^pUQnr|<2Ws#~Y-tYkxrwOzQdRNhaqe&HU&j z&9_(W-w@Meqog5oARi{v^E|l#av@|Uq#-{iEqOQ@kp0#AZ-P_~K*QflE`k$UkPK1} zqyeFmTJlF?q@_p+o2lBlF?3XCPl%$A!bovP<4b&dyiXk|q!a@@;KU z4H-eFG-MGPeVAG$NliAv_uC4rn#E5!Q>`WtjLdb7Q=x;J@)PdwsunRa`L_+g-kwgZ z%iGE9d=<5r0Pt*KyzmXBlTx`-EsDrwV+^NnB8FKIKqB{oib>WE5>|Dn`>xS3qYB7#s5J(Gbk*|@FHdKqD!3ka` zYmB@#zB5!N+d#@<%Vlb@Y)sGB0c$=lu*S%#YB3IB-nFr1hp~ z8p`A^skYD2$TtPWzbmlC$Te!wMkXg>SuhXwi+^qA@_RE4c^`5Yp4nF|9cua3xc}2& z*|eo|QaM5``WT$%7uX`-E}7TzguM~&+yWYKJhk^A&%I6mJVzwztf+=z~3AkOlG)S?fO=F_7F zvw!lWeqFY*`eXAI(a7ftPN5^o9ts%{P6*j>w~JS|We_I6Cn|%iGxC^f`wR@2Mm{QG z%k65xVRlNvDQ**LA)dV%aw)DKLRm}fHUl@ZXk*Jn+~Qsh(UP4Io0deZal>(MtM4le z$ay7f`3()&K`j^`UWhiJEIY9BPGvQFyh0ds-e;Z4pytZIt~*2V-7TZ5zB7{=SVx`NgUo zLl}9Y5KvQjR<&&~qZr$p&dxDlL4SgeQt)^-WEg)VEpeogNw&Ake8-rQSWt^q3oun2q>N2II9t*nGW4Xd@1GE=(zJH-c&Qgma zn59C7*=ww$nn>j#xSeT&g(!z-S$y%}t0bM9<~HsG2YEe1U1mnT?_$Ox^lCg=>x7Xf zqnJ>ZVsQ)QKjj<^acmg+WPqm*ooqCiMfVhL&GUN6xSpaG-BFW6ZEt$kaF{gZ4+Pu& z)WTt!?2P+AB$wK97hy5^4}aB^!!fwcG8^PEq2&FZfkPrpy|yx#jByAI2S&FXyi={C z5EDAtR+A%>*~oB44%a(FgY%-WTwZ5#nreFh#*xkn@cd}FsL5_%LwgZOaYO1>wHO_E z4$g9Yhq-#qRc#ND$rX`f?2tn5js$6bb@WOI8{_6c|5S_7fkWJ9Gk+Jfd$=7DQkD&3 z&xh-x;b5AqigM-`CTA_gl>y9;Mhhd4x{L&|pYpV7dI0X5gpOf@_+H&e;fB<&g*4lc z59}KfqXDy?cMP3w$!~*=VSkTj@}aHRXyA`vebDsIY0A6MmA_b2hE%3Tv5(i8q^j)^>M}24 zjlGutVRohlBLw3XgK_8_4vD5LhVEZ#)g3k21sxS1ow(GOagaO%NT5YLw`PwCY-Dr*GtQq7%ms2 zsBD;WeTBH0%_%)ukjV}BoQKbEt>eh7mfhW(tfj=@i-{ao;T(sKa!&sR`bY8X49GdS zo&|ZzI?paiAl}|Nru;XSJ9skCz~ZjcIcyXw#>P;19a5Z)$l#n>uu&c&L)POnem>Hz zTwiv`FkbU4Du0zZTG|-F$og>3W7aug+U+K^v5u5a?BQ8AWO6*1^+|y}LrJ^rKHB%q zq>ec7vF$|;7xp$Q>S^wKxGA5)kyqG*{AGd1x(&G#^1F4AWf~-O2EX$@^-T=dOs476 zeFU_Uz{)-B}itisl-bWA4$Gr!jJ8e|&$GA1n^;am9bDO0$$Z`nrliwTi7Gw^t zx6>#39Q^OV*^s{q+}WEcJHx?;fh{M%8Iy56X?(tNR-rh USk3&JCoph1HH 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 delta 4854 zcma)=`#%$o_s8EUdb=fZX>$u15yD)fi#v-j+uRy+$(;}}v81`rCArOzv}t4R_j~Rl za=#?EDVd5f@h18D{0HCjI6t2AdY;$gaejIcWeP!QFbrRFFyzmCPNsVZtRLg< zP0ajyRzcyCedbzBbzH7BKBs<|Kk4x`Z)HID@JYdv*80eH5ZkbcmFm;i*F(t8S8wUX zUldQK#pvCO_+;|oqIR^hwsz;U&KtYB)UCzfzm8GCTe?RJ{U?E2f6MA-%G6FGe(UZN zB7e_y9Dl}i9CN-86zTteHC&nX0uKPm$I%KhAP7nRRd#Cx8#rE8AzU)+`SjkZ2zqRb zen(r#D?6M^LkR0f%j-vf?lOf5Sd`2Cx8Ea1KIw3PN%W^!E)y)5$z*MhMLP$KC|^+P zE&f?)HhtPPFEF>Y4Q%#N=fK)-qik7!MF~AW{OT%eQPR(qSMz6avf%3smqy*SS^fg2 zNX#K@r1c^LUs**lu+|5_JPF`&7NNMkFq6Z~k+MsHzjZ4r**Trk!R35T|H>@$ZXj;f zn8nVJ5gV@j8T@;W2rRAiJP}i}JPZ_V?Gho}*R!BfE;8Of^4^QwAZym>#rDVYH4K<2 zsy*{!Z7Ji_81FHY{J&X1@wam13ZWuevHxU1@)`(RJqcN&o^ zvei_vJ$rGp`$Kjg`NKR10$odM_53V(BlLPo^m;z)rOaTt)?-CZ6fqc&vMezT6S|vs zO7ZwaFkGJ3W8hzfwz(Js9lqaK77uLe;eR#{2DW2RXF9}#eEq(%PFdc*M3I~NUZ%~V zW4|iw1J5^tt@c%ljhewk&l#gx7q1iyxj6pe+B4R<3Hw^6uF^UuQn?F*!CUlwwU)BQ z4=yAHl4s_D&m*W)5KKb<3zmkQRsr1e0-5eYqBn5P=8J~GQ+P6HD$h*Rr)kE%*X_We zrh%`x)1Azry2s)x%1^%L48RuND&>T3xHHg+)|s*845 z(wqY$m{;5aRtxIx)=$YN4Y#+e-~I{JElr5m*Y{7Rey28L8gH(J1yLSeb*Y>CQ3sYf zi6C=$5cvY(kbn>r#l_8{=l%b^dX$=F_LbJjuf6OQk<~2529^)CqGGc2Vn56Sz*?WFHd#PGT;Xm@&Qv1Bm-a>E8vOO z&yl4)e=jm>PlYEd*Y$)0>iDZ*9|pr3nb^uO1zeq?8B7y0*hjZ2>U{EXk<#-3BIrQ0 zl@}+e2`|@PSsjKidS7!b<)0qOZv0|Xp+*Iq>A#a=T%SnInsAtM3v1Z8`)(AY(SPaG zZ>?9&&cmiQ*hiKNJlLuaHe46iNpb`)Yk1s?_<-E!C{gyswjYZWP=CZnWRlT9{XZjo zCmvm3gWhH4d=PaPAj{8A?LqElnzD`Mjv^ipo> z_bjGCEF<%V&j=ng_Mi?y*^{A#;ww@m3w^ZH)<^Ci48Ah7N$LlByG1IoaL%Lr7U|=Qrb*fb&?u_N7<1%y}^Je`E7{A==O` z`&d0MsKUj-Q!mu-^@`bBnP{!-%YbqoUKwzCyIp67n4-xSPgEm-IMF7&vzRO}P zad-$Su>V|($ajKK%S67NG>+L=Z7zbU{>kG~Jp68AXxvajlp_Q>e|{~3$ZJh>&t$M4 z5G|eK1|5>6z}t$Uu_I|#*BY#=2ytktuzPr}s)S1G?84eT*V0jL-{n#Al|CqZOQU={ zqH!c`X#9|u@frAfUrSv;^y|0NFXUH8?6dPa-2lLUQZYO9a5C`jQCd_N!7=}p=Lctq zE%f*f+KlBUn?2WqDB*fY>qfF^$~rzG%3Ju2{CRXK7w68T!{a@U+l6L_xhU&?N8Wtq zt8?x33LWVfp|c`IQ!ghVy-Dr8Ofa;kbR`w4peK}iL*vIgY$5TkNh|^D5H_7VDQ7JDZ3|pDltOz7 zNpj~D#c&7BUN&~ZLD8zcL)dx&IfjoB2ync9}{_^C{^ zq5$onOMS-!+viD_jY!dNd%j7N6N?@R!IPr?KH#{-Xd!b*tP)A*6VPjWKNQmDyCpRs zQ6<-IfV399(cF{*)roF%gFi8YvTHiqh>ex!-!|)WbxJ|ek#C88}WhBnel*j4nYFI|}PV}mERV&(z(giTc z6P(wc3iPJ9@cOff1wxC{DGg`@I!sa$(Sgs;R51XgdnAHUnTuNU176|gYt z*&rpEt*keazux#jXSXJ33|G=$nv8PhME;y> zs82)7|3(`dokdgKL6~jmrPS{{E}u^}X_@gJYJEZ8wjUv5W%8|itxu9lo?zHMd07pV z+^5fx6JwnF+v6i~vBX@)ZBw2A){=+5Vw<6Z(cJn^rYiTWGkvW5EMS^$(7k7AG#^rt zdcXyQfY3p&&BwdGov!-6wgOO(8mZ}$xCY7Ys#BGUg)OeK(C^q7_MsS3!i?y6>X7L& z^5gUdFOE8LAI3DKWqvEbY}rR|_c6EO{F=pMm5*%>aFt_~AL0Tpx6rsEWM%i(n2j;1 zxis=rq#iu7ib1lZ|7fgRxt);$_}ilFpRpz8swCR=tKYoit}YF>QFL~2JKZS1Zt+67 zm?lCy`^NX5)GL=k#cqUP$MLXWFn1*cNW^uqh=Kl7NjlR{*WJLZoJCb!2kGUNn;&bB zmI{&NzGaID<+`vVb^0zXe5vIQ-I3Mx~}?;yGSdihN%m^0vYuY(nn z)MJCqjnB^WPX=y>P`Ps4wahzc1y_Ar4k}|aCYq|+o?%E9$UI68&%7_Yb#|@okt9>! z*smVHkD7XS9DmSa)IEOn+waPz!y@J*)#>m_ZG@34;pPtA9mJzImOcp}Cr;bawD#^IeSywLH>~T{7bVb>FHyl5Ezi9E^SoNYO96H9w1js%3yKFXO zW%sT?l5OI#tE|dNte*;{_9CNwn^KU1Sr7^Y9?vVZ)m^=$w0^>5ZqG@rDhVt7YiNv7 z%O=I$=lVr<)#BkWewtggt}SHxsZo8EWa$#e+{3du=6U8e&Jd_??6F2PN~x^MBNUNOpslIW^G_Emtn9;fH_a0CjPjC#2|0!DzW|b zE0rhpvRvRa>>gLox9!k3tMw4f3O0!%jB@w+?gzGcaU)> zD$MuGTh=uCl9CHZ_R(Wnd>V#E!4n-{3>tEpLsjZLk=})1pdI^K*gEFZ4C?r`U|1CT z`;SiK8}r4x@G6@YVyrjWx!ZQylsjT4!jMLNFdiP>RJ8A_a=jqHW0>eCkB=9cPfZNcH)VC5Q`_U?4Fs54`&UmfRib=fg?5E`mBQdw6QSmo!I zXO0K4L!x8bIMg0>KC^RN88jkd=DSjetK<;)Yxr| z)g}h-R=1wX;zAyXATG3B+u%GJBnRF)zZI0b+PBoB1qwh2pRCa#G zd<{z~dE<#kdC=+=R87xUcxQesKZ_1wAO_)5V3p}tPXcXcpE;!^N7}VrcME$WTWPLX zIawgnOmDiS&h$KsOKP!9c5$o=^iluAF}GJ>?$2XiHFAM9GzI)@pz7Bm zdY#oG#^CAnAG0!|`i;e_3V=m<0bYVDX3c!__;Y1mk?-h325-#PnW-V+sj9L0(ly*^1L)^&ZeTc=^j<684*+X0N<}(_}f4m7V zCZi+I_Y+IjAEkEjeKz7qHcp>+sM;84Ofs9HDHlb&ZpsT;Q@=LmD86q;7yJ_~l1 z9?3{jqas{&wI_!E{)c+dt;qZEEhfpr*hNHn_STZF>B`n8k-73uxnJ?WO<&S_i;^k^ zus*;qcGy^hNc{c7waHLf_vMA&rg0+&|EGn)00f=aHDSoT!n=g^fsSSMPw8?&8U@#d zF7`|YgU~YO3O~}%d3czyWxkpF3lXf@q9u!MEjQ(#>VpVgnb$#kIVlXC#CPfy{^1RAuPY%h+yd6FO%7t(b#c;Oj zywY?!tO&xYh_eB?xyuoP0s8+e042-WrAID&p@;8|y&M3D0!?D2+=8F+`RJ#ZR~t*n u;F{o-o$~IqvsvQ7EDQgyq=*1mKRFYpS#6Z*c0T%_(=s)*Feuk^e*S;Jk&HV4 diff --git a/src/box-winUI/Assets/Square44x44Logo.png b/src/box-winUI/Assets/Square44x44Logo.png index 3a37741b52bb72da55b4435c9de448d496455eb7..a9c7f8f1664b8bb42d76948fbb48f9292a0c4118 100644 GIT binary patch delta 1371 zcmV-h1*H1?3ZM&+R(~}~L_t(oN9|Zyh#XZEE%yzVVBBye!3ESGH&782{m}eq6459` zKQu}bXS!bXG8s)~A}CQIAR!V0g7~5Mp`!i>iijdAs6nG>#6(Sy0b|q{qgmARJm=Q! zo|@^IBusbD7t7+x|jl#aY|Cd?0=+!60i3p8P0dlL5K|f zNfo~&5674+<#>dniai`_xsYT1u7dA{hH_Y@=GHdC}t zw9g1`x9>|h0>^-CafLrIg&2rOknkoWCbAXm;P<6hJndy-N-*Poms z1l(`xW*myyQh&VHJvn-CCW-!Gi_v{4*0^U1pqLE!-a^2(9_!sR5+M#6*<9saOu%#8 zGszgOZWeEM&q(ye6G(EaC1kGhGwzv0)MK}_#2g(`l=`b=@f}*-( z#qK~nuIZqt#M4N0M?>X+r`*#)%#Me0qHj#uT-~oY`61?8OvS2Hv5IvhHpE!{f&Ij* z{5;G2Lcq#`%GF((eGTh;+y1KaA?N^iBP_m3REkqSTL-*wO>Wd(dVe~(6u$xAn&K%=r<&UuCz_fcDJbH)Pe-)+iGa-BkzsW~<*ms>INt1SN5Cyc zM(S!pRSF@))23`*vUVD=6qob9TlxJnBQ3Q<{5@I3*sj^>x4WQnu=lhbfgP6!+vM0{ zNu~G=*kGh=F{36|qE)<{X$Mk|=MtVbviZ9mID^L?GncGfA&7v%WU@Bih$Sb#iQeHFdjqlLq=8$%5am}iwNx4IOXKo*WrIDk%7j2ZrOZ*b}Q_=+{QRGb`OXjDR`zjGnca#>MWLAW}ToP*=YgDwyG( zkqCIXp=239k@SgXUl>o~f;Z+{kMqn{gnxD;;A^WgnkU1P?wKG0UbpSiNb!nWn<15} z_@_}@Q@rRNHTpA>G~Vdnni24TEjCZSb*&{|hA$g6R=?Hu1tp^nkovJ*KUnq${fts# zY95uymQFr3I(m^3W2}-@gxb!GB}|L6s=yHH!S(JPANtQCWvSF0B3uur2_eHOvlOe~=cG5D?Vj;b!gVCN z%Lp5B{K@ysac?s+ywu2(4DYygg2>C-uQst1sNy>+GK;Dm&E!%aF_Ve5f^qv?LU#kb zY?P|lYtL!1`Lk>IuHpaMdT}a6Sbv>|8;67`#|7?f0VT|{kkG^;ppoH6lHI`TZu2eY z2zX({c9wW@)mX!&T)jqTjO!3T;k_T25T+_eo~-Yh2yH=$rAOPWl2un*L!ULNCRCH7 z#4Dl~mr~h}&2K9Ewf@S$rRlLdTomAthBh@Qd z#QWA8K`l^fi?n{FRC%<|Wr%eQieH_0Rb0SDOmpl0p#?38m+`rIe13k?hZ|ZeVO@05 dMF){2{{up>Oo=>`=+Q4iWnnNb#~ z52m3ubI#u9GER*%28k93A&Lk=^icFrQN093gb^h)4Q0`Vv@&9#8LQDu9m{XUTKk+m z=j_SL%s>Cw!~en0{BzdYXRrOO>;8#aXrYA`CLamTryO%AMSny&7Ldi2q00XU7M+w~ zke+0wE@pa)nKBGhhRu}Vj8OgfBF1{^VWym!5;3&+n=FR@_tHiS*S8O z2-&s#V5p+#;Ik;ffF`74d1I*3e3Y?UvK!+IA`Z1i5TmQY@;4K(O3+U!exw|4P=b5?dV_Tp zmWQYf(<*Oo0xTXXSS{Gaqs8-EDMvr8EDjN_kpR9GIMCvaxC&BVZ9wi+Ysx=dfB z&n=!;W?jkJtA@q6ku2XwDLzx9g>A!s&@!G%xpuz;6_!o7)x^MHiyEw(<7Z7OiC=?n z)hInou(>2!T9h;KKwQLggqPG<_pY7P&8OAYHGd3=QE{=jx5|pR(_qlYY=WD@rDX7- z8Y{7Eu(Wt~9c4JA?{js0LAb0j7`*8Q@*wVd_;j-9S7YTs7Wafp$)Hzd&fxj}d5pK! zD`TnU#{|n)Q;L5)y9UqH<@lt+;L|mk3_2G3^q_L62)B%^mw9e63Z+CG)eP%GhA=mVdfSJ~3Vo#U8?PhQEDnjq!??wP6Oue0NR1 z3WFtjSeJZ#jU}JOm%hfzUF}GaWRwO{w(YX991U`flEie~bFi__Cxec>ND||}N>&vz zc3SlM)|_LQ+Hhy6HVX2aMT`O6Lkup%X)R}F9_Yh@@N1%sKCVV7^mIff|4ZGWL! z4U%XQPOgr-`DEOXEEhMsEBvq2&XSe9R)fRA;#Hp~7Vn4RvB8zK z-~8<&!}mJh#+Xvf5i=>}HDvik;!3i#V>V?tsO!qfEyJeTZ*A?kfKqI5;l`}QB)oX! zCF4bedA==+0rIi7Cd!#qkcpE__?Vj9pU{-tm5xAhgjm-bAE+pb<1e_%lH&UO`g{ zsYjXKv(GvA&eWI=8VU7?*AF5RLePfr!y^$91Px6@LJ%Zm8ZDJ3RO`{Hb$s92Yu{IA zbVl!;xtjYWEBBoJ?S0OA?X~wl*LBfF7hQC*^I|||x=iM{K7W~w-+iq0e}qhWTv^_B zxomcoqHtwMF2j&}+%2-d_3ChV8FaToaY_!!OlZ(jNLfCEp|h-ahr-L%?lve(13Z<6 z(4Zv{8pAg7seA&XcDGR-245TuPj7I2rf|P1ycWhi19?M@qL5=`y!6}14uhBFZqO8E z1aevB7R#Z=a(~`j>?$&%(PEmVFuK1u(AW+^UK|ERH-g78cWB`1-o2tVe;EX6zJ!lR1-L>!R_eONt|n9e*2%ENw7;$}5duZnTbRrg5{dV$GMV8V$38Xkvp5B(O9Wi1o~ti4D^r^){yhu zf?`i&Iet3Ye^WX2O6T1%8kZM5@HEuG^l7I&uTmOPpP~fHiuLUqK4Wvw=G=#6S-ac_sZcg z?04mLYa`#Hs_mv++re{1DnEqM3CU$E6gyEl=KT=bs5A|s;$AkP|AIFn;Kr6$V=LOv&?`Jf2aW}sDDIWuEg&QSCZo}Zn+xIaWursJ35X9 zLKjiA1;ameu28CdVA#EUqc5a)Lpq$M9FSnluU4tYyrUp7EI4LfW_Z!n9q)3VSlI!JqV;aHqii+^Je%lfbRcrJ)nQR6( z-IyBa99Bink+7Nksdb-^7HUeY)d#CbW!OC=dl=h@6~!*-`?;n^oU}-F-DSC-9JBX6ZQ3I%0h{iHwMn@t&sz6M`OuldM>LQsnYFb8wox?gU2x<_r zp(XHgMO85xG@i2PWo4KLkdW&ujTs-jJg@Hz)$cWH-9aSS`$R)1B{{*EvB9#MV?P$l zsTw7B7`uI>@}Nc|RJRrxk``xbu7A@A8pH`b9+Zl+aR%Aiuwti@8T;?>@|GGAQfu@t zG84w4VqZihd@KlitkvV*)Rl^B)Bs%lwjD$Xr$c&H*A|Fg%PUZJ+YfkYaXi{X`DI@^E|Ig0ZGQp6cinX% zXCc9}!wCDsFtP%91ovO*StQ5yy?I6UlIj>O-_`M((Ec>!T3pZ9XJzt>D$jR>16Uij zDT$ncoZlGo7DAIG$AFe2mw9;EQ&9M0 z$QQU@h5N^Y0plnHxm<+)FIyUhJsi`jEbl?CvtE-7=M2uO{&yMoZ)X0000o{ delta 1488 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 literal 6569 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 K2vno#`0_vAgPV8& diff --git a/src/box-winUI/Assets/tool-results/result.css b/src/box-winUI/Assets/tool-results/result.css index fdc4f9f..8fd28a5 100644 --- a/src/box-winUI/Assets/tool-results/result.css +++ b/src/box-winUI/Assets/tool-results/result.css @@ -33,6 +33,12 @@ html[data-theme="dark"] { * { box-sizing: border-box; } +*, +*::before, +*::after { + min-width: 0; +} + .drag-light *, .drag-light *::before, .drag-light *::after { @@ -63,6 +69,9 @@ button { padding: 7px 12px; cursor: pointer; transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease; + max-width: 100%; + white-space: normal; + overflow-wrap: anywhere; } button:hover, @@ -85,6 +94,8 @@ button.compact { .result-page { display: grid; gap: 14px; + max-width: 100%; + overflow: hidden; } .result-chrome, @@ -104,6 +115,7 @@ button.compact { gap: 12px; align-items: center; padding: 14px 16px; + min-width: 0; } .result-chrome .identity { @@ -134,6 +146,7 @@ button.compact { margin: 0; font-size: clamp(1.05rem, 1.25vw, 1.45rem); line-height: 1.18; + overflow-wrap: anywhere; } .result-chrome .meta, @@ -146,6 +159,7 @@ button.compact { flex-wrap: wrap; gap: 8px; justify-content: flex-end; + min-width: 0; } .result-chrome .metrics { @@ -156,6 +170,7 @@ button.compact { .blocks { display: grid; gap: 12px; + min-width: 0; } .details-drawer { @@ -307,6 +322,7 @@ h2 { .block { padding: 14px; overflow: hidden; + min-width: 0; } .side-card { @@ -328,6 +344,17 @@ h2 { min-width: 0; } +.block-title h2, +.paragraph, +.muted, +.meta, +.item-body, +.kv, +.metric, +.badge { + overflow-wrap: anywhere; +} + .block-icon { width: 20px; height: 20px; @@ -347,6 +374,8 @@ h2 { background: var(--accent-soft); font-size: .74rem; font-weight: 800; + max-width: 100%; + white-space: normal; } .kv-grid { @@ -378,6 +407,7 @@ h2 { .list-item strong { display: block; overflow-wrap: anywhere; + word-break: break-word; } .metric-grid .primary-kv { @@ -394,6 +424,9 @@ h2 { .table-wrap { overflow: auto; + max-width: 100%; + max-height: min(58vh, 560px); + scrollbar-gutter: stable; } table { @@ -408,6 +441,10 @@ th { border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; + min-width: 120px; + max-width: 520px; + overflow-wrap: anywhere; + word-break: break-word; } th { @@ -432,6 +469,7 @@ th { gap: 10px; align-items: start; padding: 10px; + max-width: 100%; } .ranked-item { @@ -483,6 +521,9 @@ th { .rank-core { position: relative; z-index: 1; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; } .rank-1 { @@ -510,6 +551,7 @@ th { .item-body { min-width: 0; + max-width: 100%; } .item-body strong { @@ -542,21 +584,29 @@ th { } .code-wrap { - max-height: 420px; + max-height: min(62vh, 560px); overflow: auto; background: var(--panel-strong); + max-width: 100%; + scrollbar-gutter: stable; } pre { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; + word-break: break-word; padding: 12px; font-family: "Cascadia Mono", Consolas, monospace; font-size: .84rem; line-height: 1.55; } +.limit-note { + padding: 8px 12px 10px; + font-size: .78rem; +} + .media-frame { display: grid; place-items: center; diff --git a/src/box-winUI/Assets/tool-results/result.js b/src/box-winUI/Assets/tool-results/result.js index caed7a4..1826e2c 100644 --- a/src/box-winUI/Assets/tool-results/result.js +++ b/src/box-winUI/Assets/tool-results/result.js @@ -42,6 +42,19 @@ const labels = { } }; +const LIMITS = { + keyValues: 80, + tableRows: 160, + tableColumns: 8, + tableCellChars: 600, + listItems: 180, + titleChars: 220, + subtitleChars: 900, + badgeChars: 28, + codeChars: 52000, + rawChars: 90000 +}; + window.chrome?.webview?.addEventListener('message', event => receive(event.data)); window.addEventListener('message', event => { @@ -82,6 +95,15 @@ function text(value) { return value === null || value === undefined ? '' : String(value); } +function clampText(value, max = 0) { + const output = text(value); + return max > 0 && output.length > max ? `${output.slice(0, max)}...` : output; +} + +function previewText(value, policy, max) { + return clampText(redact(value, policy), max); +} + function el(tag, className, content) { const node = document.createElement(tag); if (className) node.className = className; @@ -147,35 +169,58 @@ function metrics(data) { return root; } +const blockRenderers = { + keyvalue: (block, data, kind) => kvGrid(block.pairs || [], data, kind), + metric: (block, data, kind) => kvGrid(block.pairs || [], data, kind), + table: (block, data) => table(block.rows || [], data), + linechart: (block, data) => table(block.rows || [], data), + rankedlist: (block, data, kind) => list(block.items || [], data, kind), + newslist: (block, data, kind) => list(block.items || [], data, kind), + cardlist: (block, data, kind) => list(block.items || [], data, kind), + text: (block, data) => statusList(block.items || [], data, block.text), + diff: (block, data) => statusList(block.items || [], data, block.text), + status: (block, data) => statusList(block.items || [], data, block.text), + timeline: (block, data) => statusList(block.items || [], data, block.text), + code: (block, data) => code(redact(block.text || '', data.privacyPolicy)), + json: (block, data) => code(redact(block.text || '', data.privacyPolicy)), + jsontree: (block, data) => code(redact(block.text || '', data.privacyPolicy)), + raw: (block, data) => code(redact(block.text || '', data.privacyPolicy), LIMITS.rawChars), + file: fileBlock, + link: linkBlock, + image: mediaBlock, + media: mediaBlock, + color: colorBlock +}; + function renderBlock(block, data) { const kind = (block.kind || '').toLowerCase(); const root = el('article', `block kind-${kind || 'text'}`); const head = el('div', 'block-head'); const title = el('div', 'block-title'); - title.append(blockIcon(kind), el('h2', '', block.title || kindLabel(kind))); + title.append(blockIcon(kind), el('h2', '', clampText(block.title || kindLabel(kind), LIMITS.titleChars))); head.append(title, el('span', 'badge', kindLabel(kind))); root.append(head); - if (kind === 'keyvalue' || kind === 'metric') root.append(kvGrid(block.pairs || [], data, kind)); - else if (kind === 'table' || kind === 'linechart') root.append(table(block.rows || [], data)); - else if (['rankedlist', 'newslist', 'cardlist'].includes(kind)) root.append(list(block.items || [], data, kind)); - else if (['text', 'diff', 'status', 'timeline'].includes(kind)) root.append(statusList(block.items || [], data, block.text)); - else if (['code', 'json', 'jsontree', 'raw'].includes(kind)) root.append(code(redact(block.text || '', data.privacyPolicy))); - else if (kind === 'file') root.append(fileBlock(block, data)); - else if (kind === 'link') root.append(linkBlock(block, data)); - else if (kind === 'image' || kind === 'media') root.append(mediaBlock(block, data)); - else if (kind === 'color') root.append(colorBlock(block)); - else root.append(el('p', 'paragraph', redact(block.text || '', data.privacyPolicy))); + const renderer = blockRenderers[kind] || paragraphBlock; + root.append(renderer(block, data, kind)); return root; } +function paragraphBlock(block, data) { + return el('p', 'paragraph', previewText(block.text || '', data.privacyPolicy, LIMITS.subtitleChars)); +} + function kvGrid(pairs, data, kind) { const grid = el('div', `kv-grid ${kind === 'metric' ? 'metric-grid' : ''}`); - for (const [index, pair] of pairs.slice(0, 80).entries()) { + for (const [index, pair] of pairs.slice(0, LIMITS.keyValues).entries()) { const item = el('div', `kv ${index === 0 && kind === 'metric' ? 'primary-kv' : ''}`); - item.append(el('span', '', pair.key || ''), el('strong', '', redact(pair.value || '', data.privacyPolicy))); + item.append( + el('span', '', clampText(pair.key || '', LIMITS.titleChars)), + el('strong', '', previewText(pair.value || '', data.privacyPolicy, LIMITS.subtitleChars)) + ); grid.append(item); } + appendLimitNote(grid, pairs.length, LIMITS.keyValues); if (!pairs.length) grid.append(el('p', 'muted', t('emptyText'))); return grid; } @@ -183,10 +228,12 @@ function kvGrid(pairs, data, kind) { function table(rows, data) { const wrap = el('div', 'table-wrap'); const tableNode = el('table'); - for (const [rowIndex, row] of rows.slice(0, 180).entries()) { + for (const [rowIndex, row] of rows.slice(0, LIMITS.tableRows).entries()) { const tr = el('tr'); const cells = Array.isArray(row) ? row : Object.values(row || {}); - for (const cell of cells.slice(0, 8)) tr.append(el(rowIndex === 0 ? 'th' : 'td', '', redact(cell, data.privacyPolicy))); + for (const cell of cells.slice(0, LIMITS.tableColumns)) { + tr.append(el(rowIndex === 0 ? 'th' : 'td', '', previewText(cell, data.privacyPolicy, LIMITS.tableCellChars))); + } tableNode.append(tr); } if (!rows.length) { @@ -195,22 +242,24 @@ function table(rows, data) { tableNode.append(tr); } wrap.append(tableNode); + appendLimitNote(wrap, rows.length, LIMITS.tableRows); return wrap; } function list(items, data, kind) { const root = el('div', `list ${kind === 'rankedlist' ? 'ranked-list' : 'card-list'} ${kind}`); - for (const [index, item] of items.slice(0, 220).entries()) { + for (const [index, item] of items.slice(0, LIMITS.listItems).entries()) { const normalized = normalizeListItem(item, index); const row = el('div', `list-item ${kind === 'rankedlist' ? 'ranked-item' : ''}`); row.append(rankBadge(normalized.rank, normalized.leading, kind)); const body = el('div', 'item-body'); - body.append(el('strong', '', redact(normalized.title, data.privacyPolicy))); - if (normalized.subtitle) body.append(el('span', 'item-subtitle', redact(normalized.subtitle, data.privacyPolicy))); + body.append(el('strong', '', previewText(normalized.title, data.privacyPolicy, LIMITS.titleChars))); + if (normalized.subtitle) body.append(el('span', 'item-subtitle', previewText(normalized.subtitle, data.privacyPolicy, LIMITS.subtitleChars))); row.append(body); makeCardLink(row, normalized.uri, normalized.title); root.append(row); } + appendLimitNote(root, items.length, LIMITS.listItems); if (!items.length) root.append(el('p', 'muted', t('emptyText'))); return root; } @@ -218,24 +267,33 @@ function list(items, data, kind) { function statusList(items, data, fallbackText) { if (!items.length) return code(redact(fallbackText || '', data.privacyPolicy)); const root = el('div', 'list status-list'); - for (const [index, item] of items.slice(0, 220).entries()) { + for (const [index, item] of items.slice(0, LIMITS.listItems).entries()) { const normalized = normalizeListItem(item, index); const row = el('div', `list-item status-${normalized.status || 'info'}`); - row.append(el('span', 'badge', normalized.leading)); + row.append(el('span', 'badge', clampText(normalized.leading, LIMITS.badgeChars))); const body = el('div', 'item-body'); - body.append(el('strong', '', redact(normalized.title, data.privacyPolicy))); - if (normalized.subtitle) body.append(el('span', 'item-subtitle', redact(normalized.subtitle, data.privacyPolicy))); + body.append(el('strong', '', previewText(normalized.title, data.privacyPolicy, LIMITS.titleChars))); + if (normalized.subtitle) body.append(el('span', 'item-subtitle', previewText(normalized.subtitle, data.privacyPolicy, LIMITS.subtitleChars))); row.append(body); if (normalized.uri) row.append(linkActions(normalized.uri)); root.append(row); } + appendLimitNote(root, items.length, LIMITS.listItems); return root; } +function appendLimitNote(root, total, visible) { + if (total <= visible) return; + const hidden = total - visible; + root.append(el('p', 'muted limit-note', isEnglish() + ? `${hidden} more items are hidden in the preview. Copy raw output for the full result.` + : `预览已隐藏 ${hidden} 条;复制原文可获取完整结果。`)); +} + function normalizeListItem(item, index) { - let title = text(item?.title || item?.text || t('emptyTitle')); - let subtitle = text(item?.subtitle || ''); - const leading = text(item?.leading || item?.status || index + 1); + let title = clampText(item?.title || item?.text || t('emptyTitle'), LIMITS.titleChars * 2); + let subtitle = clampText(item?.subtitle || '', LIMITS.subtitleChars * 2); + const leading = clampText(item?.leading || item?.status || index + 1, LIMITS.badgeChars); const rank = Number.parseInt(leading, 10) || index + 1; const parts = title.split(/\s+\/\s+/).filter(Boolean); if (parts.length > 1) { @@ -254,7 +312,7 @@ function normalizeListItem(item, index) { function rankBadge(rank, label, kind) { const badge = el('span', `rank-badge rank-${rank <= 3 && kind === 'rankedlist' ? rank : 'other'} ${kind !== 'rankedlist' ? 'plain' : ''}`); - badge.append(el('span', 'rank-core', rank <= 3 && kind === 'rankedlist' ? String(rank) : label)); + badge.append(el('span', 'rank-core', rank <= 3 && kind === 'rankedlist' ? String(rank) : clampText(label, LIMITS.badgeChars))); return badge; } @@ -284,11 +342,20 @@ function makeCardLink(row, uri, label) { }); } -function code(value) { +function code(value, max = LIMITS.codeChars) { const wrap = el('div', 'code-wrap'); const pre = el('pre'); - pre.textContent = value; + const source = text(value); + const truncated = source.length > max; + pre.textContent = truncated + ? `${source.slice(0, max)}\n...(preview truncated; copy raw output for the full result)` + : source; wrap.append(pre); + if (truncated) { + wrap.append(el('p', 'muted limit-note', isEnglish() + ? 'Preview truncated. Copy raw output for the full result.' + : '预览已截断;复制原文可获取完整结果。')); + } return wrap; } @@ -296,8 +363,8 @@ function fileBlock(block, data) { const row = el('div', 'list-item file-card'); row.append(el('span', 'badge', 'FILE')); const body = el('div', 'item-body'); - body.append(el('strong', '', block.text || block.path || t('file'))); - body.append(el('span', '', redact(block.path || '', data.privacyPolicy))); + body.append(el('strong', '', previewText(block.text || block.path || t('file'), data.privacyPolicy, LIMITS.titleChars))); + body.append(el('span', '', previewText(block.path || '', data.privacyPolicy, LIMITS.subtitleChars))); row.append(body, button(t('openFolder'), () => post('openFolder', block.path))); return row; } @@ -306,8 +373,8 @@ function linkBlock(block, data) { const row = el('div', 'list-item link-card'); row.append(el('span', 'badge', 'URL')); const body = el('div', 'item-body'); - body.append(el('strong', '', redact(block.text || block.uri || t('link'), data.privacyPolicy))); - body.append(el('span', '', redact(block.uri || '', data.privacyPolicy))); + body.append(el('strong', '', previewText(block.text || block.uri || t('link'), data.privacyPolicy, LIMITS.titleChars))); + body.append(el('span', '', previewText(block.uri || '', data.privacyPolicy, LIMITS.subtitleChars))); row.append(body, linkActions(block.uri)); return row; } @@ -359,7 +426,7 @@ function rawCard(data) { event.stopPropagation?.(); post('copy', data.resultDocument?.rawText || ''); }, 'ghost compact')); - root.append(head, code(redact(data.resultDocument?.rawText || '', data.privacyPolicy))); + root.append(head, code(redact(data.resultDocument?.rawText || '', data.privacyPolicy), LIMITS.rawChars)); return root; } @@ -368,7 +435,7 @@ function emptyBlock(data) { root.append(el('h2', '', t('emptyTitle'))); root.append(el('p', 'muted', t('emptyText'))); const raw = data.resultDocument?.rawText || ''; - if (raw) root.append(code(redact(raw, data.privacyPolicy))); + if (raw) root.append(code(redact(raw, data.privacyPolicy), LIMITS.rawChars)); return root; } diff --git a/src/box-winUI/Views/Tools/AdaptiveToolPage.cs b/src/box-winUI/Views/Tools/AdaptiveToolPage.cs index 2d700ff..1eeed9f 100644 --- a/src/box-winUI/Views/Tools/AdaptiveToolPage.cs +++ b/src/box-winUI/Views/Tools/AdaptiveToolPage.cs @@ -165,6 +165,7 @@ public abstract partial class AdaptiveToolPage : ToolPageBase private bool _toolPageWebViewFailed; private bool _useToolPageWebView; private string _lastRawOutput = string.Empty; + private ToolResultWebPayload? _pendingResultWebPayload; private ToolResultDocument? _lastToolPageDocument; private ToolResultRunState? _lastToolPageRunState; private CancellationTokenSource? _runCts; @@ -1997,6 +1998,12 @@ public abstract partial class AdaptiveToolPage : ToolPageBase resourceFolder = Path.Combine(AppContext.BaseDirectory, "Assets", "tool-results"); } + if (!Directory.Exists(resourceFolder)) + { + _resultWebViewFailed = true; + return; + } + var environment = await _webViewEnvironmentFactory.CreateAsync("ToolResults").ConfigureAwait(true); await _resultWebView.EnsureCoreWebView2Async(environment); _resultWebView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false; @@ -2013,15 +2020,27 @@ public abstract partial class AdaptiveToolPage : ToolPageBase { if (args.IsSuccess) { + _resultWebViewReady = true; + _resultWebViewFailed = false; + var hasResultPayload = _pendingResultWebPayload is not null || !string.IsNullOrWhiteSpace(_lastRawOutput); SendPerformanceMode(); + SendPendingResultWebPayload(); + if (hasResultPayload) + { + SetResultViewMode(_showRawResult); + } + return; } + + _resultWebViewReady = false; + _resultWebViewFailed = true; + SetResultViewMode(_showRawResult); }; _resultWebView.Source = new Uri("https://tool-results.ymhut.local/index.html"); - _resultWebViewReady = true; - SendPerformanceMode(); } catch (Exception exception) { + _resultWebViewReady = false; _resultWebViewFailed = true; CrashLog.Write(exception); } @@ -2029,16 +2048,22 @@ public abstract partial class AdaptiveToolPage : ToolPageBase private bool TryRenderResultWebView(ToolResultDocument document, long durationMs) { - if (Module is null || _resultWebViewFailed || !_resultWebViewReady || _resultWebView.CoreWebView2 is null) + if (Module is null || _resultWebViewFailed) { return false; } try { - _resultCards.Children.Clear(); var payload = _toolResultWebBridge.CreatePayload(Module, document, durationMs); - _resultWebView.CoreWebView2.PostWebMessageAsJson(_toolResultWebBridge.Serialize(payload)); + _pendingResultWebPayload = payload; + if (!_resultWebViewReady || _resultWebView.CoreWebView2 is null) + { + return false; + } + + _resultCards.Children.Clear(); + SendPendingResultWebPayload(); SendPerformanceMode(); return true; } @@ -2050,6 +2075,17 @@ public abstract partial class AdaptiveToolPage : ToolPageBase } } + private void SendPendingResultWebPayload() + { + if (_pendingResultWebPayload is null || !_resultWebViewReady || _resultWebView.CoreWebView2 is null) + { + return; + } + + _resultWebView.CoreWebView2.PostWebMessageAsJson(_toolResultWebBridge.Serialize(_pendingResultWebPayload)); + _pendingResultWebPayload = null; + } + private void RenderResultDocument(ToolResultDocument document, long durationMs = 0) { if (TryRenderResultWebView(document, durationMs)) diff --git a/src/box-winUI/Views/Tools/RandomCinemaPage.cs b/src/box-winUI/Views/Tools/RandomCinemaPage.cs index b583387..81cec95 100644 --- a/src/box-winUI/Views/Tools/RandomCinemaPage.cs +++ b/src/box-winUI/Views/Tools/RandomCinemaPage.cs @@ -150,7 +150,7 @@ public class RandomCinemaPage : ToolPageBase private Grid BuildHeader(IToolModule module) { var back = ModernUi.IconButton("\uE72B", AppLocalizer.T("返回工具箱", "Back to toolbox"), () => _goBack?.Invoke()); - var refresh = ModernUi.PillButton(AppLocalizer.T("重新加载配置", "Reload sources"), "\uE895", async () => await LoadRemoteConfigAsync(forceRefresh: true), primary: true); + var refresh = ModernUi.PillButton(AppLocalizer.T("刷新源列表", "Refresh sources"), "\uE895", async () => await LoadRemoteConfigAsync(forceRefresh: true), primary: true); var grid = new Grid { @@ -651,7 +651,7 @@ public class RandomCinemaPage : ToolPageBase host.Children.Add(loading); var actions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 10 }; - actions.Children.Add(ModernUi.PillButton(AppLocalizer.T("重新加载", "Reload"), "\uE895", async () => await RenderMediaPageAsync(category, source), primary: true)); + actions.Children.Add(ModernUi.PillButton(AppLocalizer.T("重新获取", "Fetch new media"), "\uE895", async () => await RenderMediaPageAsync(category, source), primary: true)); actions.Children.Add(ModernUi.PillButton(AppLocalizer.T("全屏", "Fullscreen"), "\uE740", async () => await ShowFullscreenAsync())); var saveButton = ModernUi.PillButton(AppLocalizer.T("另存为", "Save as"), "\uE74E", async () => await SaveMediaAsync()); saveButton.IsEnabled = source.Downloadable; @@ -703,7 +703,7 @@ public class RandomCinemaPage : ToolPageBase host.Children.Add(media); _activeMediaHost = host; _activeMediaView = media; - ToastService.Show(AppLocalizer.T("媒体已重新加载", "Media reloaded"), ToastKind.Success); + ToastService.Show(AppLocalizer.T("媒体已重新获取", "Media refreshed"), ToastKind.Success); } catch (Exception exception) { @@ -729,8 +729,7 @@ public class RandomCinemaPage : ToolPageBase private async Task BuildImageViewerAsync(RemoteMediaSource source, IProgress? progress) { - var resolution = await _mediaResolver.ResolveMediaAsync(source.EffectiveApiUrl, RemoteMediaKind.Image, progress: progress); - EnsureExpectedMedia(resolution, RemoteMediaKind.Image); + var resolution = await ResolveFreshMediaAsync(source, RemoteMediaKind.Image, progress); _currentMediaUri = resolution.Uri; _currentExtension = resolution.SuggestedExtension; progress?.Report(45); @@ -750,8 +749,7 @@ public class RandomCinemaPage : ToolPageBase private async Task BuildVideoViewerAsync(RemoteMediaCategory category, RemoteMediaSource source, IProgress? progress) { - var resolution = await _mediaResolver.ResolveMediaAsync(source.EffectiveApiUrl, RemoteMediaKind.Video, progress: progress); - EnsureExpectedMedia(resolution, RemoteMediaKind.Video); + var resolution = await ResolveFreshMediaAsync(source, RemoteMediaKind.Video, progress); _currentMediaUri = resolution.Uri; _currentExtension = resolution.SuggestedExtension; progress?.Report(90); @@ -774,6 +772,49 @@ public class RandomCinemaPage : ToolPageBase return MediaStage(WrapPlayableMedia(media, category, source)); } + private async Task ResolveFreshMediaAsync( + RemoteMediaSource source, + RemoteMediaKind expectedKind, + IProgress? progress) + { + var primaryUrl = source.RefreshApiUrl; + var fallbackUrl = source.ResolvedUrl; + Exception? primaryError = null; + + if (!string.IsNullOrWhiteSpace(primaryUrl)) + { + try + { + var resolution = await _mediaResolver.ResolveMediaAsync(primaryUrl, expectedKind, cacheBust: true, progress: progress); + EnsureExpectedMedia(resolution, expectedKind); + return resolution; + } + catch (Exception exception) when (ShouldTryResolvedFallback(primaryUrl, fallbackUrl)) + { + primaryError = exception; + progress?.Report(35); + } + } + + if (ShouldTryResolvedFallback(primaryUrl, fallbackUrl)) + { + var resolution = await _mediaResolver.ResolveMediaAsync(fallbackUrl, expectedKind, cacheBust: true, progress: progress); + EnsureExpectedMedia(resolution, expectedKind); + return resolution; + } + + if (primaryError is not null) + { + throw new InvalidOperationException(primaryError.Message, primaryError); + } + + throw new InvalidOperationException(AppLocalizer.T("远程媒体源没有可用地址。", "The remote media source does not have a usable URL.")); + } + + private static bool ShouldTryResolvedFallback(string? primaryUrl, string? fallbackUrl) + => !string.IsNullOrWhiteSpace(fallbackUrl) && + !string.Equals(primaryUrl?.Trim(), fallbackUrl.Trim(), StringComparison.OrdinalIgnoreCase); + private static MediaPlayer CreateRemoteMediaPlayer(RemoteMediaResolution resolution) { return new MediaPlayer