Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src-tauri/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# will have compiled files and executables
/target/

# Local-only Cargo target dir used when running `cargo test --target-dir target-test`
# to keep test artifacts separate from `cargo tauri dev/build` output.
/target-test/

# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"AutoCheckUpdate": "Notify when a new version is available",
"RunAfterLogin": "Launch game after login",
"MinimizeToTaskbar": "Minimize to system tray",
"DisableHardwareAcceleration": "Disable hardware acceleration (lower GPU usage)",
"DisableHardwareAcceleration": "Disable hardware acceleration",
"MsgRestartForHardwareAccelTitle": "Restart required",
"MsgRestartForHardwareAccel": "Hardware acceleration settings have changed. Fully exit Beanfun and start it again for the main window to use WPF software rendering.",
"Game": "Game",
Expand Down
2 changes: 1 addition & 1 deletion src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"AutoCheckUpdate": "有新版本时提示更新",
"RunAfterLogin": "登录完成后运行游戏",
"MinimizeToTaskbar": "最小化到通知中心",
"DisableHardwareAcceleration": "关闭硬件加速(降低 GPU 使用)",
"DisableHardwareAcceleration": "关闭硬件加速",
"MsgRestartForHardwareAccelTitle": "需要重新启动",
"MsgRestartForHardwareAccel": "已变更硬件加速相关设置。请完全关闭 Beanfun 后再打开,主窗口才会套用 WPF 软件渲染。",
"Game": "游戏",
Expand Down
2 changes: 1 addition & 1 deletion src/locales/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"AutoCheckUpdate": "有新版本時提示更新",
"RunAfterLogin": "登入完成後開啟遊戲",
"MinimizeToTaskbar": "最小化到通知中心",
"DisableHardwareAcceleration": "關閉硬體加速(降低 GPU 使用)",
"DisableHardwareAcceleration": "關閉硬體加速",
"MsgRestartForHardwareAccelTitle": "需要重新啟動",
"MsgRestartForHardwareAccel": "已變更硬體加速相關設定。請完全關閉 Beanfun 後再開啟,主視窗才會套用 WPF 軟體渲染。",
"Game": "遊戲",
Expand Down
48 changes: 44 additions & 4 deletions src/pages/AccountList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,46 @@ function selectRow(a: ServiceAccount): void {
account.selectedSid = a.sid
}

/**
* Double-click on a row → copy the row's service-account ID (`sid`)
* to the clipboard. Mirrors WPF `lstViewAccount_MouseDoubleClick`
* (`AccountList.xaml.cs`), whose only side effect was
* `Clipboard.SetText(selected.sid)` — paired with the WPF tooltip
* resource `DoubleClickCopy` ("雙擊複製帳號 / Double-click to copy
* account") which is still shipped via the `DoubleClickCopy` i18n
* key today. Banned rows fall through silently (same guard as
* {@link selectRow}) so the SPA mirrors WPF's "disabled rows ignore
* input" behaviour.
*
* Selection is intentionally **not** armed here: WPF treats the
* double-click as a one-shot copy, not a row-arm; mutating
* `selectedSid` would also flip the OTP / Start Game UI on top of
* the copy, which the WPF user never saw. Single-click + Enter is
* the ergonomic path for OTP fetch (issue #239 fixes the missing
* keyboard route too).
*
* The SPA bypasses the {@link clipboardWriteOtp} helper because that
* helper's success / failure toasts use the OTP-specific
* `GetOtpSuccessAndCopy` string. A generic `CopyFinished` /
* `CopyFailed` toast pair matches the WPF tooltip wording and keeps
* the OTP helper single-purpose.
*/
async function handleRowDblClick(a: ServiceAccount): Promise<void> {
if (!a.is_enable) return
try {
await navigator.clipboard.writeText(a.sid)
ElMessage.success(t('CopyFinished'))
} catch {
/*
* `navigator.clipboard.writeText` only rejects on permission /
* trust-boundary denial inside our Tauri webview — surface it
* so the user knows the buffer wasn't updated (otherwise
* they'd silently paste stale content into the launcher).
*/
ElMessage.error(t('CopyFailed'))
}
}

/* --------------- logout --------------- */

async function handleLogout(): Promise<void> {
Expand Down Expand Up @@ -2243,6 +2283,7 @@ onBeforeUnmount(() => {
}"
:data-test="`account-row-${a.sid}`"
@click="selectRow(a)"
@dblclick="handleRowDblClick(a)"
>
<span
class="account-list__row-grip"
Expand All @@ -2253,9 +2294,8 @@ onBeforeUnmount(() => {
<span class="account-list__row-num">{{ idx + 1 }}</span>
<div class="account-list__row-info">
<p class="account-list__row-name">{{ a.sname }}</p>
<p class="account-list__row-sub">
<template v-if="a.is_enable">ID: {{ a.sid }}</template>
<template v-else>{{ t('accountList.statusBanned') }}</template>
<p v-if="!a.is_enable" class="account-list__row-sub">
{{ t('accountList.statusBanned') }}
</p>
</div>
<el-dropdown
Expand Down Expand Up @@ -2880,7 +2920,7 @@ onBeforeUnmount(() => {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.625rem 0.75rem;
padding: 0.4375rem 0.75rem;
border-radius: var(--bf-radius-card);
cursor: pointer;
position: relative;
Expand Down
2 changes: 1 addition & 1 deletion src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ export const routes: RouteRecordRaw[] = [
meta: {
titleKey: 'titleBar.settings',
titleIcon: 'settings',
windowWidth: 880,
windowWidth: 660,
windowHeight: 680,
},
/*
Expand Down
61 changes: 58 additions & 3 deletions tests/unit/pages/AccountList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,12 +853,12 @@ describe('AccountList page', () => {
const rows = wrapper.findAll('.account-list__row')
expect(rows).toHaveLength(3)
expect(rows[0].text()).toContain('Main Toon')
expect(rows[0].text()).toContain('sid-1')
expect(rows[1].text()).toContain('Mule Account')
expect(rows[2].text()).toContain('Suspended User')
/*
* The banned row swaps the ID line for the localized
* "Disabled" copy — proves the conditional branch fires.
* The banned row shows the localized "Disabled" copy as a
* subtitle — proves the conditional branch fires. Enabled
* rows show only the display name (no ID subtitle).
*/
expect(rows[2].text()).toContain(i18nMessages['zh-TW'].accountList.statusBanned)
expect(rows[2].classes()).toContain('account-list__row--banned')
Expand Down Expand Up @@ -917,6 +917,61 @@ describe('AccountList page', () => {
expect(account.selectedSid).toBe('sid-1')
})

it('double-clicking an enabled row copies sid to clipboard (WPF parity, issue #239)', async () => {
/*
* WPF `lstViewAccount_MouseDoubleClick` only called
* `Clipboard.SetText(selected.sid)` — no row-arm, no OTP fetch.
* Mirror that here: the dblclick must hit `clipboard.writeText`
* with the row's sid and surface a generic `CopyFinished` toast,
* without touching `account.selectedSid` or `commands.getOtp`
* (those belong to the single-click + Enter path; auto-select
* from PR #245 is what arms the initial selection on mount).
*/
vi.mocked(commands.getAccounts).mockReturnValueOnce(ok(POPULATED_LIST))
const clipboard = installClipboardMock()

const ctx = buildHarness()
const wrapper = await ctx.mountIt()
await flushPromises()

const account = useAccountStore()
/*
* Snapshot whatever the auto-select from PR #245 armed on
* mount; the dblclick on sid-2 must NOT mutate it (otherwise
* the OTP / Start Game UI would silently flip to point at the
* just-copied row, which the WPF user never saw).
*/
const selectedBeforeDblClick = account.selectedSid

await wrapper.get('[data-test="account-row-sid-2"]').trigger('dblclick')
await flushPromises()

expect(clipboard.writeText).toHaveBeenCalledTimes(1)
expect(clipboard.writeText).toHaveBeenCalledWith('sid-2')
expect(ElMessage.success).toHaveBeenCalledWith(i18nMessages['zh-TW'].CopyFinished)
expect(commands.getOtp).not.toHaveBeenCalled()
expect(account.selectedSid).toBe(selectedBeforeDblClick)
})

it('double-clicking a banned row is a no-op (mirrors the single-click guard)', async () => {
vi.mocked(commands.getAccounts).mockReturnValueOnce(ok(POPULATED_LIST))
const clipboard = installClipboardMock()

const ctx = buildHarness()
const wrapper = await ctx.mountIt()
await flushPromises()

const account = useAccountStore()
const selectedBeforeDblClick = account.selectedSid

await wrapper.get('[data-test="account-row-sid-3"]').trigger('dblclick')
await flushPromises()

expect(clipboard.writeText).not.toHaveBeenCalled()
expect(commands.getOtp).not.toHaveBeenCalled()
expect(account.selectedSid).toBe(selectedBeforeDblClick)
})

it('logout: confirm → auth.logout → account.clearSessionData → /login', async () => {
vi.mocked(commands.getAccounts).mockReturnValueOnce(ok(POPULATED_LIST))
vi.mocked(commands.logout).mockReturnValueOnce(ok(null))
Expand Down