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
399 changes: 399 additions & 0 deletions src/components/ReleaseSourceSettingsModal.tsx

Large diffs are not rendered by default.

228 changes: 178 additions & 50 deletions src/components/ReleaseTimeline.tsx

Large diffs are not rendered by default.

44 changes: 29 additions & 15 deletions src/components/settings/DataManagementPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,13 @@ import type {
SubscriptionChannel,
SearchFilters,
ProxyConfig,
RpcDownloadConfig
RpcDownloadConfig,
ReleaseSourceSettings
} from '../../types';
import {
mergeReleaseSourceSettings,
normalizeReleaseSourceSettings,
} from '../../utils/releaseSources';

interface DataManagementPanelProps {
t: (zh: string, en: string) => string;
Expand Down Expand Up @@ -90,6 +95,7 @@ interface ExportData {
subscriptionLastRefresh?: Record<string, string | null>;
subscriptionChannels?: SubscriptionChannel[];
releaseSubscriptions?: number[];
releaseSourceSettings?: ReleaseSourceSettings;
readReleases?: number[];
searchFilters?: SearchFilters;
hiddenDefaultCategoryIds?: string[];
Expand Down Expand Up @@ -151,6 +157,7 @@ export const DataManagementPanel: React.FC<DataManagementPanelProps> = ({ t }) =
discoveryRepos,
subscriptionRepos,
releaseSubscriptions,
releaseSourceSettings,
readReleases,
language,
setRepositories,
Expand Down Expand Up @@ -408,8 +415,9 @@ export const DataManagementPanel: React.FC<DataManagementPanelProps> = ({ t }) =

const deleteReleaseSubscriptions = async () => {
try {
useAppStore.setState({
useAppStore.setState({
releaseSubscriptions: new Set<number>(),
releaseSourceSettings: normalizeReleaseSourceSettings(null),
readReleases: new Set<number>()
});
addLog(t('删除 Release 订阅与已读', 'Delete release subscriptions & read'), true);
Expand Down Expand Up @@ -506,6 +514,7 @@ export const DataManagementPanel: React.FC<DataManagementPanelProps> = ({ t }) =
}
if (selectedTypes.includes('releaseSubscriptions')) {
exportDataObj.data.releaseSubscriptions = Array.from(store.releaseSubscriptions);
exportDataObj.data.releaseSourceSettings = store.releaseSourceSettings;
exportDataObj.data.readReleases = Array.from(store.readReleases);
}
if (selectedTypes.includes('searchFilters')) {
Expand Down Expand Up @@ -654,12 +663,9 @@ export const DataManagementPanel: React.FC<DataManagementPanelProps> = ({ t }) =
}
}
if (selectedTypes.includes('releaseSubscriptions')) {
if (importedData.releaseSubscriptions) {
useAppStore.setState({ releaseSubscriptions: new Set(importedData.releaseSubscriptions) });
}
if (importedData.readReleases) {
useAppStore.setState({ readReleases: new Set(importedData.readReleases) });
}
useAppStore.setState({ releaseSubscriptions: new Set(importedData.releaseSubscriptions || []) });
store.setReleaseSourceSettings(normalizeReleaseSourceSettings(importedData.releaseSourceSettings || null));
useAppStore.setState({ readReleases: new Set(importedData.readReleases || []) });
}
if (selectedTypes.includes('searchFilters') && importedData.searchFilters) {
useAppStore.setState({ searchFilters: importedData.searchFilters });
Expand Down Expand Up @@ -755,10 +761,18 @@ export const DataManagementPanel: React.FC<DataManagementPanelProps> = ({ t }) =
const newFilters = importedData.assetFilters.filter(f => !existingIds.has(f.id));
useAppStore.setState({ assetFilters: [...store.assetFilters, ...newFilters] });
}
if (selectedTypes.includes('releaseSubscriptions') && importedData.releaseSubscriptions) {
const existingSubs = store.releaseSubscriptions;
const newSubs = new Set([...Array.from(existingSubs), ...importedData.releaseSubscriptions]);
useAppStore.setState({ releaseSubscriptions: newSubs });
if (selectedTypes.includes('releaseSubscriptions')) {
if (importedData.releaseSubscriptions) {
const existingSubs = store.releaseSubscriptions;
const newSubs = new Set([...Array.from(existingSubs), ...importedData.releaseSubscriptions]);
useAppStore.setState({ releaseSubscriptions: newSubs });
}
if (importedData.releaseSourceSettings) {
store.setReleaseSourceSettings(mergeReleaseSourceSettings(
store.releaseSourceSettings,
normalizeReleaseSourceSettings(importedData.releaseSourceSettings)
));
}
}
}

Expand Down Expand Up @@ -1215,9 +1229,9 @@ export const DataManagementPanel: React.FC<DataManagementPanelProps> = ({ t }) =
{
key: 'releaseSubscriptions',
label: t('Release 订阅与已读', 'Release Subscriptions & Read'),
description: t('已订阅 Release 的仓库列表和已读标记。删除后 Release 时间线将不显示订阅状态和已读标记。',
'Subscribed repo list and read marks for releases. Subscription status and read marks lost after deletion.'),
count: releaseSubscriptions.size,
description: t('已订阅 Release 的仓库列表、来源设置和已读标记。删除后 Release 时间线将不显示订阅状态和已读标记。',
'Subscribed repo list, source settings, and read marks for releases. Subscription status and read marks lost after deletion.'),
count: releaseSubscriptions.size + releaseSourceSettings.watchCustomReleaseRepos.length + releaseSourceSettings.customReleaseRepos.length,
icon: <Eye className="w-5 h-5" />,
color: 'text-gray-700 dark:text-text-secondary',
bgColor: 'bg-gray-100 dark:bg-white/[0.04]',
Expand Down
6 changes: 6 additions & 0 deletions src/services/autoSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ export async function syncFromBackend(): Promise<void> {
if (Array.isArray(settings.assetFilters)) {
useAppStore.setState({ assetFilters: settings.assetFilters });
}
if (settings.releaseSourceSettings && typeof settings.releaseSourceSettings === 'object') {
state.setReleaseSourceSettings(settings.releaseSourceSettings as typeof state.releaseSourceSettings);
}
if (typeof settings.collapsedSidebarCategoryCount === 'number' && settings.collapsedSidebarCategoryCount >= 1) {
useAppStore.setState({ collapsedSidebarCategoryCount: settings.collapsedSidebarCategoryCount });
}
Expand Down Expand Up @@ -276,6 +279,7 @@ export async function syncToBackend(): Promise<void> {
categoryOrder: state.categoryOrder,
customCategories: state.customCategories,
assetFilters: state.assetFilters,
releaseSourceSettings: state.releaseSourceSettings,
collapsedSidebarCategoryCount: state.collapsedSidebarCategoryCount,
}),
]);
Expand Down Expand Up @@ -303,6 +307,7 @@ export async function syncToBackend(): Promise<void> {
categoryOrder: state.categoryOrder,
customCategories: state.customCategories,
assetFilters: state.assetFilters,
releaseSourceSettings: state.releaseSourceSettings,
collapsedSidebarCategoryCount: state.collapsedSidebarCategoryCount,
});
}
Expand Down Expand Up @@ -366,6 +371,7 @@ export function startAutoSync(): () => void {
state.categoryOrder !== prevState.categoryOrder ||
state.customCategories !== prevState.customCategories ||
state.assetFilters !== prevState.assetFilters ||
state.releaseSourceSettings !== prevState.releaseSourceSettings ||
state.collapsedSidebarCategoryCount !== prevState.collapsedSidebarCategoryCount;

if (!changed) return;
Expand Down
40 changes: 40 additions & 0 deletions src/services/githubApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,46 @@ export class GitHubApiService {
return allRepos;
}

async getWatchedRepositories(page = 1, perPage = 100, username?: string): Promise<Repository[]> {
const endpoint = username
? `/users/${encodeURIComponent(username)}/subscriptions?page=${page}&per_page=${perPage}`
: `/user/subscriptions?page=${page}&per_page=${perPage}`;
return this.makeRequest<Repository[]>(endpoint);
}

async getAllWatchedRepositories(username?: string): Promise<Repository[]> {
let allRepos: Repository[] = [];
let page = 1;
const perPage = 100;

while (true) {
const repos = await this.getWatchedRepositories(page, perPage, username);
if (repos.length === 0) break;

allRepos = [...allRepos, ...repos];

if (repos.length < perPage) break;
page++;

await new Promise(resolve => setTimeout(resolve, 100));
}

return allRepos;
}

async getAllWatchedRepositoriesForCurrentUser(): Promise<Repository[]> {
const currentUser = await this.getCurrentUser();
const [privateAware, publicProfile] = await Promise.all([
this.getAllWatchedRepositories(),
this.getAllWatchedRepositories(currentUser.login),
]);
const reposByName = new Map<string, Repository>();
[...privateAware, ...publicProfile].forEach(repo => {
reposByName.set(repo.full_name.toLowerCase(), repo);
});
return Array.from(reposByName.values());
}

private decodeContentResponse(response: GitHubContentResponse): string {
if (response.encoding === 'base64' && response.content) {
// 使用 TextDecoder 正确处理 UTF-8 编码,避免中文乱码
Expand Down
37 changes: 36 additions & 1 deletion src/store/useAppStore.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { Repository } from '../types';
import { Repository, defaultReleaseSourceSettings } from '../types';
import { CUSTOM_RELEASE_SOURCE_ID, createCustomReleaseRepository } from '../utils/releaseSources';

let useAppStore: typeof import('./useAppStore').useAppStore;

Expand Down Expand Up @@ -31,6 +32,40 @@ const createRepository = (id: number, overrides: Partial<Repository> = {}): Repo
...overrides,
});

describe('useAppStore release source settings', () => {
beforeEach(() => {
useAppStore.setState({
releaseSourceSettings: defaultReleaseSourceSettings,
releaseSubscriptions: new Set<number>(),
releases: [],
readReleases: new Set<number>(),
});
});

it('keeps the starred release subscription source enabled by default', () => {
expect(useAppStore.getState().releaseSourceSettings.enabledSourceIds).toEqual(['starred-release-subscription']);
});

it('dedupes custom release repositories by full name', () => {
const first = createCustomReleaseRepository('owner/repo', CUSTOM_RELEASE_SOURCE_ID)!;
const duplicate = createCustomReleaseRepository('https://github.com/OWNER/repo', CUSTOM_RELEASE_SOURCE_ID)!;

useAppStore.getState().addReleaseSourceRepository(CUSTOM_RELEASE_SOURCE_ID, first);
useAppStore.getState().addReleaseSourceRepository(CUSTOM_RELEASE_SOURCE_ID, duplicate);

expect(useAppStore.getState().releaseSourceSettings.customReleaseRepos).toHaveLength(1);
});

it('removes custom release repositories by full name', () => {
const repo = createCustomReleaseRepository('owner/repo', CUSTOM_RELEASE_SOURCE_ID)!;

useAppStore.getState().addReleaseSourceRepository(CUSTOM_RELEASE_SOURCE_ID, repo);
useAppStore.getState().removeReleaseSourceRepository(CUSTOM_RELEASE_SOURCE_ID, 'OWNER/repo');

expect(useAppStore.getState().releaseSourceSettings.customReleaseRepos).toHaveLength(0);
});
});

describe('useAppStore repository performance guards', () => {
beforeEach(() => {
useAppStore.setState({
Expand Down
Loading
Loading