Skip to content

Commit 9881868

Browse files
authored
Merge pull request #2 from maro114510/fix/selected-sections
fix: Apply already selected section
2 parents 0dba145 + f63af83 commit 9881868

6 files changed

Lines changed: 309 additions & 78 deletions

File tree

.editorconfig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ charset = utf-8
77
end_of_line = lf
88
insert_final_newline = true
99
trim_trailing_whitespace = true
10+
11+
[*.{js,css,json}]
12+
indent_style = space
13+
indent_size = 2

content.js

Lines changed: 139 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const api = typeof browser !== "undefined" ? browser : chrome;
22
const usesPromises = typeof browser !== "undefined";
33

4+
// =============================================================================
5+
// Constants
6+
// =============================================================================
7+
48
// Feedly Read Later pages can vary by user id and legacy paths.
59
const READ_LATER_PATTERNS = [
610
/https:\/\/feedly\.com\/i\/board\/content\/user\/[^/]+\/tag\/global\.saved/i,
@@ -25,6 +29,10 @@ const READ_LATER_SELECTORS = [
2529
];
2630
const READ_LATER_LABELS = ["read later", "後で読む", "あとで読む"];
2731

32+
// =============================================================================
33+
// Utility Functions
34+
// =============================================================================
35+
2836
function delay(ms) {
2937
return new Promise((resolve) => setTimeout(resolve, ms));
3038
}
@@ -95,68 +103,41 @@ function toOpenableUrl(href) {
95103
return url.toString();
96104
}
97105

98-
async function getSavedEntriesWithUrls(settings) {
99-
const entries = getEntryElements();
100-
const seen = new Set();
101-
const results = [];
102-
const limit =
103-
settings.mode === "count" ? Math.max(settings.count || 1, 1) : Infinity;
106+
// =============================================================================
107+
// Save State Detection
108+
// =============================================================================
104109

105-
for (const entry of entries) {
106-
if (results.length >= limit) {
107-
break;
108-
}
109-
const url = getEntryLink(entry);
110-
if (!url || seen.has(url)) {
111-
continue;
112-
}
113-
114-
entry.scrollIntoView({ block: "center", inline: "center" });
115-
await revealToolbar(entry);
116-
const button = findUnsaveButton(entry);
117-
if (!button) {
118-
continue;
119-
}
120-
121-
seen.add(url);
122-
results.push({ entry, url, button });
110+
function hasAccentClass(element) {
111+
const classAttr = element.getAttribute("class") || "";
112+
if (classAttr.includes("color--accent")) {
113+
return true;
123114
}
124-
125-
return results;
126-
}
127-
128-
function findUnsaveButton(entry) {
129-
// Prefer toolbar bookmark icon when available (hover-only in Feedly UI).
130-
const toolbarButtons = entry.querySelectorAll(TOOLBAR_BUTTON_SELECTOR);
131-
for (const button of toolbarButtons) {
132-
if (isSavedButton(button)) {
133-
return button;
115+
if (element.classList) {
116+
for (const className of element.classList) {
117+
if (className.startsWith("color--accent")) {
118+
return true;
119+
}
134120
}
135121
}
122+
return false;
123+
}
136124

137-
// Primary path: explicit Read Later button in metadata row.
138-
const explicit = entry.querySelector(READ_LATER_SELECTORS.join(","));
139-
if (explicit) {
140-
const button = explicit.closest("a,button") || explicit;
141-
return isSavedButton(button) ? button : null;
125+
function hasSecondaryClass(element) {
126+
const classAttr = element.getAttribute("class") || "";
127+
if (classAttr.includes("color--secondary")) {
128+
return true;
142129
}
143-
144-
// Fallback: look for buttons that contain the Read Later label.
145-
const candidates = entry.querySelectorAll("a[role='button'], button");
146-
for (const candidate of candidates) {
147-
if (!containsReadLaterText(candidate)) {
148-
continue;
149-
}
150-
if (isSavedButton(candidate)) {
151-
return candidate;
130+
if (element.classList) {
131+
for (const className of element.classList) {
132+
if (className.startsWith("color--secondary")) {
133+
return true;
134+
}
152135
}
153136
}
154-
155-
return null;
137+
return false;
156138
}
157139

158140
function isSavedButton(button) {
159-
// Be conservative: only click when we can confirm "saved" state to avoid re-saving.
160141
const svg = button.querySelector("svg");
161142
if (svg && hasSecondaryClass(svg)) {
162143
return false;
@@ -186,39 +167,109 @@ function containsReadLaterText(element) {
186167
return READ_LATER_LABELS.some((label) => text.includes(label));
187168
}
188169

189-
function hasAccentClass(element) {
190-
const classAttr = element.getAttribute("class") || "";
191-
if (classAttr.includes("color--accent")) {
192-
return true;
170+
/**
171+
* Quick pre-check if entry appears to be saved (before DOM operations).
172+
* Returns true if saved, false if not saved, null if cannot determine.
173+
*/
174+
function quickCheckSaved(entry) {
175+
const toolbarButton = entry.querySelector(TOOLBAR_BUTTON_SELECTOR);
176+
if (toolbarButton) {
177+
const svg = toolbarButton.querySelector("svg");
178+
if (hasSecondaryClass(toolbarButton) || (svg && hasSecondaryClass(svg))) {
179+
return false;
180+
}
181+
if (hasAccentClass(toolbarButton) || (svg && hasAccentClass(svg))) {
182+
return true;
183+
}
193184
}
194-
if (element.classList) {
195-
for (const className of element.classList) {
196-
if (className.startsWith("color--accent")) {
197-
return true;
198-
}
185+
186+
const metaButton = entry.querySelector(READ_LATER_SELECTORS.join(","));
187+
if (metaButton) {
188+
const btn = metaButton.closest("a,button") || metaButton;
189+
const svg = btn.querySelector("svg");
190+
if (hasSecondaryClass(btn) || (svg && hasSecondaryClass(svg))) {
191+
return false;
192+
}
193+
if (hasAccentClass(btn) || (svg && hasAccentClass(svg))) {
194+
return true;
199195
}
200196
}
201-
return false;
197+
198+
return null;
202199
}
203200

204-
function hasSecondaryClass(element) {
205-
const classAttr = element.getAttribute("class") || "";
206-
if (classAttr.includes("color--secondary")) {
207-
return true;
201+
// =============================================================================
202+
// Button Detection
203+
// =============================================================================
204+
205+
function findUnsaveButton(entry) {
206+
const toolbarButtons = entry.querySelectorAll(TOOLBAR_BUTTON_SELECTOR);
207+
for (const button of toolbarButtons) {
208+
if (isSavedButton(button)) {
209+
return button;
210+
}
208211
}
209-
if (element.classList) {
210-
for (const className of element.classList) {
211-
if (className.startsWith("color--secondary")) {
212-
return true;
213-
}
212+
213+
const explicit = entry.querySelector(READ_LATER_SELECTORS.join(","));
214+
if (explicit) {
215+
const button = explicit.closest("a,button") || explicit;
216+
return isSavedButton(button) ? button : null;
217+
}
218+
219+
const candidates = entry.querySelectorAll("a[role='button'], button");
220+
for (const candidate of candidates) {
221+
if (!containsReadLaterText(candidate)) {
222+
continue;
223+
}
224+
if (isSavedButton(candidate)) {
225+
return candidate;
214226
}
215227
}
216-
return false;
228+
229+
return null;
230+
}
231+
232+
// =============================================================================
233+
// Entry Processing
234+
// =============================================================================
235+
236+
async function getSavedEntriesWithUrls(settings) {
237+
const entries = getEntryElements();
238+
const seen = new Set();
239+
const results = [];
240+
const limit =
241+
settings.mode === "count" ? Math.max(settings.count || 1, 1) : Infinity;
242+
243+
for (const entry of entries) {
244+
if (results.length >= limit) {
245+
break;
246+
}
247+
const url = getEntryLink(entry);
248+
if (!url || seen.has(url)) {
249+
continue;
250+
}
251+
252+
// Pre-check: skip entries that appear unsaved (before DOM operations)
253+
if (quickCheckSaved(entry) === false) {
254+
continue;
255+
}
256+
257+
entry.scrollIntoView({ block: "center", inline: "center" });
258+
await revealToolbar(entry);
259+
const button = findUnsaveButton(entry);
260+
if (!button) {
261+
continue;
262+
}
263+
264+
seen.add(url);
265+
results.push({ entry, url, button });
266+
}
267+
268+
return results;
217269
}
218270

219271
async function unsaveEntry(entry, knownButton) {
220272
entry.scrollIntoView({ block: "center", inline: "center" });
221-
// Feedly reveals toolbar actions on hover. Simulate hover first.
222273
await revealToolbar(entry);
223274
await delay(120);
224275
const button = knownButton || findUnsaveButton(entry);
@@ -251,6 +302,10 @@ async function revealToolbar(entry) {
251302
await delay(120);
252303
}
253304

305+
// =============================================================================
306+
// Event Dispatching
307+
// =============================================================================
308+
254309
function clickElement(element) {
255310
element.scrollIntoView({ block: "center", inline: "center" });
256311

@@ -292,6 +347,10 @@ function activateAsButton(element) {
292347
element.click();
293348
}
294349

350+
// =============================================================================
351+
// Infinite Scroll Loading
352+
// =============================================================================
353+
295354
async function loadAllEntries({ maxRounds, idleThreshold }) {
296355
let idleRounds = 0;
297356
let lastCount = getEntryElements().length;
@@ -340,6 +399,10 @@ function findScrollContainer(startNode) {
340399
return document.scrollingElement || document.documentElement || document.body;
341400
}
342401

402+
// =============================================================================
403+
// Main Handler
404+
// =============================================================================
405+
343406
async function handleOpen(settings) {
344407
if (!(await waitForReadLaterPage(2000))) {
345408
return { ok: false, error: "This tab is not a Feedly Read Later page." };
@@ -405,6 +468,10 @@ function isRecentlyReadLater() {
405468
return location.origin === "https://feedly.com" && lastReadLaterUrl.length > 0;
406469
}
407470

471+
// =============================================================================
472+
// Message Listener
473+
// =============================================================================
474+
408475
if (!window.__feedlyReadLaterOpenerListenerAdded) {
409476
window.__feedlyReadLaterOpenerListenerAdded = true;
410477
api.runtime.onMessage.addListener((message, sender, sendResponse) => {

manifest.json

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "Feedly Read Later Opener",
4-
"version": "0.1.0",
4+
"version": "0.2.0",
55
"description": "Open Feedly Read Later items in background tabs and unsave them.",
66
"icons": {
77
"16": "icons/icon-16.png",
@@ -19,12 +19,22 @@
1919
"default_popup": "popup.html",
2020
"default_title": "Feedly Read Later Opener"
2121
},
22-
"permissions": ["storage", "tabs", "scripting"],
23-
"host_permissions": ["https://feedly.com/*"],
22+
"permissions": [
23+
"storage",
24+
"tabs",
25+
"scripting"
26+
],
27+
"host_permissions": [
28+
"https://feedly.com/*"
29+
],
2430
"web_accessible_resources": [
2531
{
26-
"resources": ["content.js"],
27-
"matches": ["https://feedly.com/*"]
32+
"resources": [
33+
"content.js"
34+
],
35+
"matches": [
36+
"https://feedly.com/*"
37+
]
2838
}
2939
],
3040
"content_scripts": [
@@ -33,7 +43,9 @@
3343
"https://feedly.com/i/board/content/user/*/tag/global.saved*",
3444
"https://feedly.com/i/read-later*"
3545
],
36-
"js": ["content.js"]
46+
"js": [
47+
"content.js"
48+
]
3749
}
3850
]
3951
}

0 commit comments

Comments
 (0)