diff --git a/index.html b/index.html
index d4c9a94..5f45b79 100644
--- a/index.html
+++ b/index.html
@@ -77,7 +77,54 @@
})();
-
+
+
diff --git a/public/manifest.json b/public/manifest.json
index 2a337e7..9fe7284 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -98,9 +98,8 @@
}
],
"share_target": {
- "action": "/share",
- "method": "POST",
- "enctype": "multipart/form-data",
+ "action": "/",
+ "method": "GET",
"params": {
"title": "title",
"text": "text",
diff --git a/src/components/AddFeedDialog/AddFeedDialog.tsx b/src/components/AddFeedDialog/AddFeedDialog.tsx
index cb2c8d0..99a9176 100644
--- a/src/components/AddFeedDialog/AddFeedDialog.tsx
+++ b/src/components/AddFeedDialog/AddFeedDialog.tsx
@@ -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';
@@ -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('');
@@ -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('');
diff --git a/src/pages/FeedsPage.tsx b/src/pages/FeedsPage.tsx
index 4a06c91..0cd2be5 100644
--- a/src/pages/FeedsPage.tsx
+++ b/src/pages/FeedsPage.tsx
@@ -38,8 +38,29 @@ export function FeedsPage() {
const [isRefreshing, setIsRefreshing] = useState(false);
const [articleCounts, setArticleCounts] = useState>({});
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;
@@ -327,7 +348,11 @@ export function FeedsPage() {
{/* Add Feed Dialog */}
-
+ { closeAddFeedDialog(); setSharedFeedUrl(''); }}
+ initialUrl={sharedFeedUrl}
+ />
);
}
\ No newline at end of file
diff --git a/tests/e2e/ci/favorites.spec.ts b/tests/e2e/ci/favorites.spec.ts
index 5c1e348..fc83627 100644
--- a/tests/e2e/ci/favorites.spec.ts
+++ b/tests/e2e/ci/favorites.spec.ts
@@ -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');
diff --git a/tests/e2e/ci/feedSubscription.spec.ts b/tests/e2e/ci/feedSubscription.spec.ts
index b7c0bd7..64af4d5 100644
--- a/tests/e2e/ci/feedSubscription.spec.ts
+++ b/tests/e2e/ci/feedSubscription.spec.ts
@@ -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|订阅源/);
});
diff --git a/tests/e2e/ci/history.spec.ts b/tests/e2e/ci/history.spec.ts
index bbf868a..cfc2e18 100644
--- a/tests/e2e/ci/history.spec.ts
+++ b/tests/e2e/ci/history.spec.ts
@@ -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
@@ -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);
});