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
49 changes: 48 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,54 @@
})();
</script>

<!-- Critical CSS for loading screen and initial layout -->
<!-- Critical CSS for loading screen - prevents transparent/white flash on PWA launch -->
<style>
#app-loading {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background-color: #1f1f23;
color: #e5e5e5;
transition: opacity 0.3s ease;
}
html.light #app-loading {
background-color: #ffffff;
color: #1a1a1a;
}
#app-loading.hide {
opacity: 0;
pointer-events: none;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.loading-spinner {
width: 36px;
height: 36px;
border: 3px solid rgba(255, 255, 255, 0.15);
border-top-color: #e5e5e5;
border-radius: 50%;
animation: app-spin 0.75s linear infinite;
}
html.light .loading-spinner {
border-color: rgba(0, 0, 0, 0.1);
border-top-color: #1a1a1a;
}
.loading-text {
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
opacity: 0.6;
}
@keyframes app-spin {
to { transform: rotate(360deg); }
}
</style>
</head>

<body>
Expand Down
5 changes: 2 additions & 3 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,8 @@
}
],
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"action": "/",
"method": "GET",
"params": {
"title": "title",
"text": "text",
Expand Down
12 changes: 10 additions & 2 deletions src/components/AddFeedDialog/AddFeedDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Modal dialog for adding new RSS feed subscriptions
*/

import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useStore } from '../../hooks/useStore';
import { useOfflineDetection } from '../../hooks/useOfflineDetection';
Expand All @@ -12,9 +12,10 @@ import { useToast } from '../../hooks/useToast';
interface AddFeedDialogProps {
isOpen: boolean;
onClose: () => void;
initialUrl?: string;
}

export function AddFeedDialog({ isOpen, onClose }: AddFeedDialogProps) {
export function AddFeedDialog({ isOpen, onClose, initialUrl }: AddFeedDialogProps) {
const { t } = useTranslation('feed');
const [url, setUrl] = useState('');
const [categoryId, setCategoryId] = useState<string>('');
Expand All @@ -30,6 +31,13 @@ export function AddFeedDialog({ isOpen, onClose }: AddFeedDialogProps) {
loadCategories();
});

// Populate URL field when dialog opens with an initial URL (e.g. from share target)
useEffect(() => {
if (isOpen && initialUrl) {
setUrl(initialUrl);
}
}, [isOpen, initialUrl]);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
Expand Down
27 changes: 26 additions & 1 deletion src/pages/FeedsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,29 @@ export function FeedsPage() {
const [isRefreshing, setIsRefreshing] = useState(false);
const [articleCounts, setArticleCounts] = useState<Record<string, { total: number; unread: number; starred: number }>>({});
const [isBottomBarVisible, setIsBottomBarVisible] = useState(true);
const [sharedFeedUrl, setSharedFeedUrl] = useState('');
const lastScrollY = useRef(0);

// Handle PWA shortcuts (?action=add-feed) and Web Share Target (?url=... or ?text=...)
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const action = params.get('action');
const sharedUrl = params.get('url') || params.get('text');

if (action === 'add-feed') {
openAddFeedDialog();
} else if (sharedUrl) {
// If the param is plain text with an embedded URL, extract it
let feedUrl = sharedUrl;
if (!/^https?:\/\//i.test(feedUrl)) {
const match = sharedUrl.match(/https?:\/\/[^\s]+/i);
feedUrl = match ? match[0] : sharedUrl;
}
setSharedFeedUrl(feedUrl);
openAddFeedDialog();
}
}, [openAddFeedDialog]);

// Load feeds, categories and start auto-refresh on mount
useEffect(() => {
let cancelled = false;
Expand Down Expand Up @@ -327,7 +348,11 @@ export function FeedsPage() {
</div>

{/* Add Feed Dialog */}
<AddFeedDialog isOpen={isAddFeedDialogOpen} onClose={closeAddFeedDialog} />
<AddFeedDialog
isOpen={isAddFeedDialogOpen}
onClose={() => { closeAddFeedDialog(); setSharedFeedUrl(''); }}
initialUrl={sharedFeedUrl}
/>
</div>
);
}
14 changes: 11 additions & 3 deletions tests/e2e/ci/favorites.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,21 @@ test.describe('Favorites Functionality', () => {
await articleLink.click();
await page.waitForLoadState('networkidle');

// Click the favorite button in the action bar
const favoriteButton = page.locator('button').filter({ hasText: /Favorite|收藏|favorite/ }).first();
// If the article is already favorited (from a previous test run), unfavorite it first
// so we can reliably test the favorite→favorited transition
const alreadyFavoritedBtn = page.locator('button').filter({ hasText: /^Favorited$|^已收藏$/ }).first();
if (await alreadyFavoritedBtn.isVisible({ timeout: 3_000 }).catch(() => false)) {
await alreadyFavoritedBtn.click();
await expect(page.locator('button').filter({ hasText: /^Favorite$|^收藏$/ }).first()).toBeVisible({ timeout: 10_000 });
}

// Click the favorite button in the action bar (exact match to avoid matching "Favorited")
const favoriteButton = page.locator('button').filter({ hasText: /^Favorite$|^收藏$/ }).first();
await expect(favoriteButton).toBeVisible({ timeout: 10_000 });
await favoriteButton.click();

// Wait for the favorited state to appear (text changes to "Favorited" / "已收藏")
await expect(page.locator('button').filter({ hasText: /Favorited|已收藏|favorited/ }).first()).toBeVisible({ timeout: 5_000 });
await expect(page.locator('button').filter({ hasText: /^Favorited$|^已收藏$/ }).first()).toBeVisible({ timeout: 10_000 });

// Navigate to favorites page
await page.goto('/#/favorites');
Expand Down
7 changes: 3 additions & 4 deletions tests/e2e/ci/feedSubscription.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,10 @@ test.describe('RSS Feed Subscription', () => {
// Navigate back to feeds list
const backToFeedsButton = page.locator('button').filter({ hasText: /Back to Feeds|返回订阅列表/ }).first();
await backToFeedsButton.click();
await page.waitForLoadState('networkidle');

// Verify we're back at feeds list
const mainHeading = page.locator('h1').first();
await expect(mainHeading).toBeVisible({ timeout: 10_000 });
// Verify we're back at feeds list (wait for h1 matching Feeds/订阅源 directly)
const mainHeading = page.locator('h1').filter({ hasText: /Feeds|订阅源/ }).first();
await expect(mainHeading).toBeVisible({ timeout: 15_000 });
const text = await mainHeading.textContent();
expect(text).toMatch(/Feeds|订阅源/);
});
Expand Down
4 changes: 3 additions & 1 deletion tests/e2e/ci/history.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ test.describe('Reading History', () => {

// Navigate to history page
await page.goto('/#/history');
// Wait for the loading spinner to disappear before checking content
await page.waitForSelector('.animate-spin', { state: 'hidden', timeout: 10_000 }).catch(() => {});
await page.waitForLoadState('networkidle');

// Verify history page shows the read article
Expand All @@ -74,7 +76,7 @@ test.describe('Reading History', () => {

// Should have at least one article in history (the one we just read)
const historyArticles = page.locator('a[href*="/articles/"]');
await expect(historyArticles.first()).toBeVisible({ timeout: 10_000 });
await expect(historyArticles.first()).toBeVisible({ timeout: 15_000 });
const count = await historyArticles.count();
expect(count).toBeGreaterThan(0);
});
Expand Down
Loading