Fix AniList token persistence and AVIF timing

- Defer AniList setup prompts until app-ready and reuse stored tokens
- Add AVIF lead-in padding so motion stays aligned with sentence audio
This commit is contained in:
2026-03-29 22:07:15 -07:00
parent 54324df3be
commit 55b350c3a2
7 changed files with 268 additions and 5 deletions

View File

@@ -82,7 +82,42 @@ test('refresh handler prefers cached token when not forced', async () => {
assert.equal(loadCalls, 0);
});
test('refresh handler falls back to stored token then opens setup when missing', async () => {
test('refresh handler falls back to stored token without opening setup', async () => {
let cached: string | null = null;
let opened = false;
let openCalls = 0;
const states: Array<{ status: string; source: string }> = [];
const refresh = createRefreshAnilistClientSecretStateHandler({
getResolvedConfig: () => ({ anilist: { accessToken: '' } }),
isAnilistTrackingEnabled: () => true,
getCachedAccessToken: () => cached,
setCachedAccessToken: (token) => {
cached = token;
},
saveStoredToken: () => {},
loadStoredToken: () => ' stored-token ',
setClientSecretState: (state) => {
states.push({ status: state.status, source: state.source });
},
getAnilistSetupPageOpened: () => opened,
setAnilistSetupPageOpened: (next) => {
opened = next;
},
openAnilistSetupWindow: () => {
openCalls += 1;
},
now: () => 400,
});
const token = await refresh({ force: true });
assert.equal(token, 'stored-token');
assert.equal(cached, 'stored-token');
assert.equal(opened, false);
assert.equal(openCalls, 0);
assert.deepEqual(states, [{ status: 'resolved', source: 'stored' }]);
});
test('refresh handler opens setup when missing token and prompting allowed', async () => {
let cached: string | null = null;
let opened = false;
let openCalls = 0;
@@ -111,3 +146,44 @@ test('refresh handler falls back to stored token then opens setup when missing',
assert.equal(cached, null);
assert.equal(openCalls, 1);
});
test('refresh handler skips setup open when missing token and prompting disabled', async () => {
let cached: string | null = null;
let opened = false;
let openCalls = 0;
const states: Array<{ status: string; source: string; message: string | null }> = [];
const refresh = createRefreshAnilistClientSecretStateHandler({
getResolvedConfig: () => ({ anilist: { accessToken: '' } }),
isAnilistTrackingEnabled: () => true,
getCachedAccessToken: () => cached,
setCachedAccessToken: (token) => {
cached = token;
},
saveStoredToken: () => {},
loadStoredToken: () => '',
setClientSecretState: (state) => {
states.push({ status: state.status, source: state.source, message: state.message });
},
getAnilistSetupPageOpened: () => opened,
setAnilistSetupPageOpened: (next) => {
opened = next;
},
openAnilistSetupWindow: () => {
openCalls += 1;
},
now: () => 500,
});
const token = await refresh({ force: true, allowSetupPrompt: false });
assert.equal(token, null);
assert.equal(cached, null);
assert.equal(opened, false);
assert.equal(openCalls, 0);
assert.deepEqual(states, [
{
status: 'error',
source: 'none',
message: 'cannot authenticate without anilist.accessToken',
},
]);
});

View File

@@ -27,7 +27,10 @@ export function createRefreshAnilistClientSecretStateHandler<
openAnilistSetupWindow: () => void;
now: () => number;
}) {
return async (options?: { force?: boolean }): Promise<string | null> => {
return async (options?: {
force?: boolean;
allowSetupPrompt?: boolean;
}): Promise<string | null> => {
const resolved = deps.getResolvedConfig();
const now = deps.now();
if (!deps.isAnilistTrackingEnabled(resolved)) {
@@ -87,7 +90,11 @@ export function createRefreshAnilistClientSecretStateHandler<
resolvedAt: null,
errorAt: now,
});
if (deps.isAnilistTrackingEnabled(resolved) && !deps.getAnilistSetupPageOpened()) {
if (
options?.allowSetupPrompt !== false &&
deps.isAnilistTrackingEnabled(resolved) &&
!deps.getAnilistSetupPageOpened()
) {
deps.openAnilistSetupWindow();
}
return null;