11const api = typeof browser !== "undefined" ? browser : chrome ;
22const usesPromises = typeof browser !== "undefined" ;
33
4+ // =============================================================================
5+ // Constants
6+ // =============================================================================
7+
48// Feedly Read Later pages can vary by user id and legacy paths.
59const READ_LATER_PATTERNS = [
610 / h t t p s : \/ \/ f e e d l y \. c o m \/ i \/ b o a r d \/ c o n t e n t \/ u s e r \/ [ ^ / ] + \/ t a g \/ g l o b a l \. s a v e d / i,
@@ -25,6 +29,10 @@ const READ_LATER_SELECTORS = [
2529] ;
2630const READ_LATER_LABELS = [ "read later" , "後で読む" , "あとで読む" ] ;
2731
32+ // =============================================================================
33+ // Utility Functions
34+ // =============================================================================
35+
2836function 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
158140function 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
219271async 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+
254309function 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+
295354async 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+
343406async 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+
408475if ( ! window . __feedlyReadLaterOpenerListenerAdded ) {
409476 window . __feedlyReadLaterOpenerListenerAdded = true ;
410477 api . runtime . onMessage . addListener ( ( message , sender , sendResponse ) => {
0 commit comments