From 2bb3610da72bfd74752ebe5114f61ce2a4390266 Mon Sep 17 00:00:00 2001 From: truewhile <779943132@qq.com> Date: Fri, 11 Sep 2026 15:14:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=B9=81=E4=BD=93=E8=BD=AC?= =?UTF-8?q?=E7=AE=80=E4=BD=93=EF=BC=8C=E7=AE=80=E4=BD=93=E8=BD=AC=E7=B9=81?= =?UTF-8?q?=E4=BD=93=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/model/user.go | 3 ++ internal/service/profile.go | 22 ++++++--- internal/service/profile_subtitle_test.go | 60 +++++++++++++++++++++++ web/package-lock.json | 7 +++ web/package.json | 1 + web/src/api/profile.ts | 3 ++ web/src/components/PlayerControls.tsx | 39 ++++++++++++++- web/src/pages/PlayerPage.tsx | 59 ++++++++++++++++++++++ web/src/pages/PlayerVideoStage.tsx | 53 ++++++++++++++++++-- web/src/types/auth.ts | 1 + web/src/utils/subtitleChinese.ts | 30 ++++++++++++ 11 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 internal/service/profile_subtitle_test.go create mode 100644 web/src/utils/subtitleChinese.ts diff --git a/internal/model/user.go b/internal/model/user.go index 2149e8a..1c7be74 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -28,6 +28,9 @@ type User struct { // PinnedLibraryIDs 存储用户置顶的媒体库 ID 列表(JSON 字符串),顺序即置顶优先级。 PinnedLibraryIDs string `gorm:"type:text" json:"-"` PinnedLibraryList []string `gorm:"-" json:"pinned_library_ids,omitempty"` + // SubtitleChineseMode 是网页播放器外挂字幕的简繁转换偏好: + // original / simplified / traditional。 + SubtitleChineseMode string `gorm:"size:16;not null;default:original" json:"subtitle_chinese_mode"` // ExpiredAt is the account expiry time. Nil means the account never // expires. When set and in the past, the account is treated as expired // (login blocked) until an admin or a redemption code renews it. diff --git a/internal/service/profile.go b/internal/service/profile.go index 865d12b..c5127de 100644 --- a/internal/service/profile.go +++ b/internal/service/profile.go @@ -27,12 +27,13 @@ func NewProfileService(log *zap.Logger, repo *repository.Container) *ProfileServ // ProfileUpdate is the patch object accepted by UpdateProfile. Empty // fields are ignored so the same payload can be reused across screens. type ProfileUpdate struct { - Username *string `json:"username,omitempty"` - Nickname *string `json:"nickname,omitempty"` - Email *string `json:"email,omitempty"` - AvatarURL *string `json:"avatar_url,omitempty"` - HideAdult *bool `json:"hide_adult,omitempty"` - Password string `json:"password,omitempty"` + Username *string `json:"username,omitempty"` + Nickname *string `json:"nickname,omitempty"` + Email *string `json:"email,omitempty"` + AvatarURL *string `json:"avatar_url,omitempty"` + HideAdult *bool `json:"hide_adult,omitempty"` + SubtitleChineseMode *string `json:"subtitle_chinese_mode,omitempty"` + Password string `json:"password,omitempty"` } // UpdateProfile applies a non-credential patch to the user. @@ -73,6 +74,15 @@ func (p *ProfileService) UpdateProfile(ctx context.Context, userID string, patch if patch.HideAdult != nil { updates["hide_adult"] = *patch.HideAdult } + if patch.SubtitleChineseMode != nil { + mode := strings.ToLower(strings.TrimSpace(*patch.SubtitleChineseMode)) + switch mode { + case "original", "simplified", "traditional": + updates["subtitle_chinese_mode"] = mode + default: + return nil, errors.New("subtitle_chinese_mode must be original, simplified, or traditional") + } + } if len(updates) > 0 { if err := p.repo.DB.Model(&model.User{}).Where("id = ?", userID). Updates(updates).Error; err != nil { diff --git a/internal/service/profile_subtitle_test.go b/internal/service/profile_subtitle_test.go new file mode 100644 index 0000000..0d9d519 --- /dev/null +++ b/internal/service/profile_subtitle_test.go @@ -0,0 +1,60 @@ +package service + +import ( + "testing" + + "github.com/glebarez/sqlite" + "go.uber.org/zap" + "gorm.io/gorm" + + "github.com/truewhile/MeBox/internal/model" + "github.com/truewhile/MeBox/internal/repository" +) + +func TestProfileSubtitleChineseModePersistsAndValidates(t *testing.T) { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + t.Fatal(err) + } + if err := db.AutoMigrate(&model.User{}); err != nil { + t.Fatal(err) + } + + repos := repository.New(db) + svc := NewProfileService(zap.NewNop(), repos) + user := &model.User{ + Username: "subtitle-viewer", + PasswordHash: "hash", + Role: "user", + IsActive: true, + } + if err := repos.User.Create(t.Context(), user); err != nil { + t.Fatal(err) + } + + traditional := "traditional" + updated, err := svc.UpdateProfile(t.Context(), user.ID, ProfileUpdate{ + SubtitleChineseMode: &traditional, + }) + if err != nil { + t.Fatal(err) + } + if updated.SubtitleChineseMode != traditional { + t.Fatalf("subtitle mode = %q, want %q", updated.SubtitleChineseMode, traditional) + } + + invalid := "automatic" + if _, err := svc.UpdateProfile(t.Context(), user.ID, ProfileUpdate{ + SubtitleChineseMode: &invalid, + }); err == nil { + t.Fatal("invalid subtitle mode should be rejected") + } + + reloaded, err := repos.User.FindByID(t.Context(), user.ID) + if err != nil { + t.Fatal(err) + } + if reloaded.SubtitleChineseMode != traditional { + t.Fatalf("invalid update changed subtitle mode to %q", reloaded.SubtitleChineseMode) + } +} diff --git a/web/package-lock.json b/web/package-lock.json index 3785c2f..97fa839 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,6 +14,7 @@ "framer-motion": "^12.38.0", "hls.js": "^1.5.11", "lucide-react": "^0.379.0", + "opencc-js": "^1.4.2", "qrcode": "^1.5.4", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -3427,6 +3428,12 @@ "node": ">= 6" } }, + "node_modules/opencc-js": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/opencc-js/-/opencc-js-1.4.2.tgz", + "integrity": "sha512-EdcspgdROiCCZDM+jZR6Pa670xW+EW2QhctPBQ8gRjg6lrx4IHEl+2s+kt5izk/TkxVRgdWuFgaGkaEdIUfdaw==", + "license": "MIT AND Apache-2.0" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", diff --git a/web/package.json b/web/package.json index 76d7f9c..ffd6799 100644 --- a/web/package.json +++ b/web/package.json @@ -16,6 +16,7 @@ "framer-motion": "^12.38.0", "hls.js": "^1.5.11", "lucide-react": "^0.379.0", + "opencc-js": "^1.4.2", "qrcode": "^1.5.4", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/web/src/api/profile.ts b/web/src/api/profile.ts index f754be3..dcfa6ce 100644 --- a/web/src/api/profile.ts +++ b/web/src/api/profile.ts @@ -2,12 +2,15 @@ import { api } from './client' import type { User } from '../types' export const profileAPI = { + get: () => api.get('/me').then((r) => r.data), + update: (patch: { username?: string nickname?: string email?: string avatar_url?: string hide_adult?: boolean + subtitle_chinese_mode?: 'original' | 'simplified' | 'traditional' password?: string }) => api.patch('/me', patch).then((r) => r.data), diff --git a/web/src/components/PlayerControls.tsx b/web/src/components/PlayerControls.tsx index 9521718..e8139d9 100644 --- a/web/src/components/PlayerControls.tsx +++ b/web/src/components/PlayerControls.tsx @@ -15,6 +15,7 @@ import { VolumeX, } from 'lucide-react' import type { SubtitleTrack } from '../api/subtitles' +import type { SubtitleChineseMode } from '../utils/subtitleChinese' // PlayerControls — custom bottom control bar replacing the native