Line data Source code
1 : #include "NewsItem.h"
2 : #include "NewsList.h"
3 : #include "../utilities/FangLogging.h"
4 : #include "../utilities/ErrorHandling.h"
5 :
6 : // NewsSlot implementation
7 2514 : NewsSlot::NewsSlot(NewsItem* newsItem)
8 2514 : : position(newsItem->getDbID(), newsItem->getTimestamp())
9 2514 : , item(newsItem)
10 2514 : {}
11 :
12 : // NewsList implementation
13 345 : NewsList::NewsList(FangObject *parent)
14 345 : : FangObject{parent}
15 345 : {}
16 :
17 379 : NewsList::~NewsList()
18 : {
19 326 : clear();
20 379 : }
21 :
22 355 : void NewsList::clear()
23 : {
24 : // Delete each loaded news item.
25 2869 : for (NewsSlot& slot : slotList) {
26 2514 : if (slot.item) {
27 2514 : slot.item->deleteLater();
28 : }
29 : }
30 :
31 355 : slotList.clear();
32 355 : loadedSet.clear();
33 355 : displayStart = 0;
34 355 : displayEnd = 0;
35 355 : }
36 :
37 605 : qsizetype NewsList::size() const
38 : {
39 605 : return displayEnd - displayStart;
40 : }
41 :
42 78 : bool NewsList::isEmpty() const
43 : {
44 78 : return size() == 0;
45 : }
46 :
47 37 : NewsItem *NewsList::first() const
48 : {
49 37 : if (isEmpty()) {
50 1 : return nullptr;
51 : }
52 36 : return slotList.at(displayStart).item;
53 : }
54 :
55 26 : NewsItem *NewsList::last() const
56 : {
57 26 : if (isEmpty()) {
58 1 : return nullptr;
59 : }
60 25 : return slotList.at(displayEnd - 1).item;
61 : }
62 :
63 225 : NewsItem *NewsList::at(qsizetype i) const
64 : {
65 225 : qsizetype fullIndex = displayStart + i;
66 225 : if (fullIndex < displayStart || fullIndex >= displayEnd) {
67 2 : qCCritical(logModel) << "NewsList::at: Index" << i << "out of display window bounds [0," << size() << ")";
68 1 : return nullptr;
69 : }
70 224 : return slotList.at(fullIndex).item;
71 : }
72 :
73 26 : bool NewsList::contains(NewsItem* value) const
74 : {
75 26 : if (!loadedSet.contains(value)) {
76 2 : return false;
77 : }
78 : // Check if it's in the display window
79 61 : for (qsizetype i = displayStart; i < displayEnd; ++i) {
80 55 : if (slotList.at(i).item == value) {
81 18 : return true;
82 : }
83 : }
84 6 : return false;
85 : }
86 :
87 8 : qsizetype NewsList::indexOf(const NewsItem *value, qsizetype from) const
88 : {
89 : // Search within display window only
90 109 : for (qsizetype i = displayStart + from; i < displayEnd; ++i) {
91 106 : if (slotList.at(i).item == value) {
92 5 : return i - displayStart; // Return index relative to display window
93 : }
94 : }
95 3 : return -1;
96 : }
97 :
98 200 : qsizetype NewsList::fullSize() const
99 : {
100 200 : return slotList.size();
101 : }
102 :
103 166 : NewsItem* NewsList::fullAt(qsizetype i) const
104 : {
105 166 : if (i < 0 || i >= slotList.size()) {
106 0 : return nullptr;
107 : }
108 166 : return slotList.at(i).item; // May be nullptr if unloaded
109 : }
110 :
111 2570 : bool NewsList::containsID(qint64 id) const
112 : {
113 2570 : return fullIndexForItemID(id) >= 0;
114 : }
115 :
116 3109 : qsizetype NewsList::fullIndexForItemID(qint64 id) const
117 : {
118 89641 : for (qsizetype i = 0; i < slotList.size(); ++i) {
119 87099 : if (slotList.at(i).id() == id) {
120 567 : return i;
121 : }
122 : }
123 2542 : return -1;
124 : }
125 :
126 13 : bool NewsList::canPageUp() const
127 : {
128 13 : return displayStart > 0;
129 : }
130 :
131 20 : bool NewsList::canPageDown() const
132 : {
133 20 : return displayEnd < slotList.size();
134 : }
135 :
136 13 : qsizetype NewsList::pageUp(qsizetype count)
137 : {
138 13 : qsizetype available = displayStart; // Items before display window
139 13 : qsizetype toAdd = qMin(count, available);
140 :
141 13 : if (toAdd > 0) {
142 : // Check if any items in the range need reloading
143 11 : QList<qint64> toReload;
144 93 : for (qsizetype i = displayStart - toAdd; i < displayStart; ++i) {
145 82 : if (!slotList[i].isLoaded()) {
146 0 : toReload.append(slotList[i].id());
147 : }
148 : }
149 :
150 : // Reload if needed
151 11 : if (!toReload.isEmpty()) {
152 0 : FANG_CHECK(reloadCallback, "NewsList::pageUp: Reload callback not set");
153 0 : if (reloadCallback) {
154 0 : QList<NewsItem*> reloaded = reloadCallback(toReload);
155 0 : populateSlots(reloaded);
156 :
157 : // Verify all items were reloaded
158 0 : for (qsizetype i = displayStart - toAdd; i < displayStart; ++i) {
159 0 : FANG_CHECK(slotList[i].isLoaded(),
160 : "NewsList::pageUp: Item not loaded after reload callback");
161 : }
162 0 : }
163 : }
164 :
165 11 : displayStart -= toAdd;
166 22 : qCDebug(logModel) << "NewsList::pageUp: Expanded window by" << toAdd
167 11 : << "items, new window [" << displayStart << "," << displayEnd << ")";
168 :
169 : // Last step: Verify integrity.
170 11 : verifyDisplayWindowIntegrity();
171 11 : }
172 :
173 13 : return toAdd;
174 : }
175 :
176 13 : qsizetype NewsList::pageDown(qsizetype count)
177 : {
178 13 : qsizetype available = slotList.size() - displayEnd; // Items after display window
179 13 : qsizetype toAdd = qMin(count, available);
180 :
181 13 : if (toAdd > 0) {
182 : // Check if any items in the range need reloading
183 11 : QList<qint64> toReload;
184 73 : for (qsizetype i = displayEnd; i < displayEnd + toAdd; ++i) {
185 62 : if (!slotList[i].isLoaded()) {
186 0 : toReload.append(slotList[i].id());
187 : }
188 : }
189 :
190 : // Reload if needed
191 11 : if (!toReload.isEmpty()) {
192 0 : FANG_CHECK(reloadCallback, "NewsList::pageDown: Reload callback not set");
193 0 : if (reloadCallback) {
194 0 : QList<NewsItem*> reloaded = reloadCallback(toReload);
195 0 : populateSlots(reloaded);
196 :
197 : // Verify all items were reloaded
198 0 : for (qsizetype i = displayEnd; i < displayEnd + toAdd; ++i) {
199 0 : FANG_CHECK(slotList[i].isLoaded(),
200 : "NewsList::pageDown: Item not loaded after reload callback");
201 : }
202 0 : }
203 : }
204 :
205 11 : displayEnd += toAdd;
206 22 : qCDebug(logModel) << "NewsList::pageDown: Expanded window by" << toAdd
207 11 : << "items, new window [" << displayStart << "," << displayEnd << ")";
208 :
209 : // Last step: Verify integrity.
210 11 : verifyDisplayWindowIntegrity();
211 11 : }
212 :
213 13 : return toAdd;
214 : }
215 :
216 2312 : void NewsList::append(NewsItem *value)
217 : {
218 : // Check for duplicate by ID.
219 2312 : if (containsID(value->getDbID())) {
220 13 : qsizetype existingIndex = fullIndexForItemID(value->getDbID());
221 13 : NewsItem* existingItem = existingIndex >= 0 ? slotList.at(existingIndex).item : nullptr;
222 26 : qCCritical(logModel) << "NewsList::append: DUPLICATE ID" << value->getDbID()
223 13 : << "- new item ptr:" << (void*)value
224 13 : << "existing item ptr:" << (void*)existingItem
225 13 : << "at index:" << existingIndex
226 13 : << "display window: [" << displayStart << "," << displayEnd << ")"
227 13 : << "fullSize:" << slotList.size();
228 13 : return;
229 : }
230 :
231 2299 : loadedSet.insert(value);
232 2299 : slotList.append(NewsSlot(value));
233 : // Expand display window to include the new item
234 2299 : displayEnd = slotList.size();
235 :
236 : // Check if we need to unload distant items
237 2299 : checkMemoryBounds();
238 : }
239 :
240 217 : void NewsList::prepend(NewsItem *value)
241 : {
242 : // Check for duplicate by ID.
243 217 : if (containsID(value->getDbID())) {
244 2 : qsizetype existingIndex = fullIndexForItemID(value->getDbID());
245 2 : NewsItem* existingItem = existingIndex >= 0 ? slotList.at(existingIndex).item : nullptr;
246 4 : qCCritical(logModel) << "NewsList::prepend: DUPLICATE ID" << value->getDbID()
247 2 : << "- new item ptr:" << (void*)value
248 2 : << "existing item ptr:" << (void*)existingItem
249 2 : << "at index:" << existingIndex
250 2 : << "display window: [" << displayStart << "," << displayEnd << ")"
251 2 : << "fullSize:" << slotList.size();
252 2 : return;
253 : }
254 :
255 215 : loadedSet.insert(value);
256 215 : slotList.prepend(NewsSlot(value));
257 : // Shift display window indices to account for the new item at the front
258 : // and keep the same items visible (plus the new one)
259 215 : displayEnd++;
260 : // displayStart stays at 0 to include the new item
261 :
262 : // Check if we need to unload distant items
263 215 : checkMemoryBounds();
264 : }
265 :
266 67 : void NewsList::shrinkDisplayWindow(bool fromStart, qsizetype numberToRemove)
267 : {
268 67 : qsizetype toRemove = qMin(numberToRemove, size());
269 :
270 67 : if (fromStart) {
271 39 : displayStart += toRemove;
272 : } else {
273 28 : displayEnd -= toRemove;
274 : }
275 :
276 134 : qCDebug(logModel) << "NewsList::shrinkDisplayWindow: Removed" << toRemove
277 67 : << "from" << (fromStart ? "start" : "end")
278 67 : << ", new window [" << displayStart << "," << displayEnd << ")"
279 67 : << ", full list size" << slotList.size();
280 67 : }
281 :
282 17 : void NewsList::removeAndDelete(bool fromStart, qsizetype numberToRemove)
283 : {
284 : // Now just shrinks the display window - items stay in memory for paging
285 17 : shrinkDisplayWindow(fromStart, numberToRemove);
286 17 : }
287 :
288 71 : NewsItem *NewsList::newsItemForID(const qint64 id) const
289 : {
290 71 : if (id < 0) {
291 : // Invalid ID.
292 0 : qCCritical(logModel) << "NewsList::newsItemForID: Invalid news ID" << id;
293 0 : return nullptr;
294 : }
295 :
296 71 : qsizetype index = indexForItemID(id);
297 71 : return index < 0 ? nullptr : at(index);
298 : }
299 :
300 156 : NewsItem *NewsList::fullNewsItemForID(const qint64 id) const
301 : {
302 156 : if (id < 0) {
303 0 : qCCritical(logModel) << "NewsList::fullNewsItemForID: Invalid news ID" << id;
304 0 : return nullptr;
305 : }
306 :
307 156 : qsizetype index = fullIndexForItemID(id);
308 156 : return index < 0 ? nullptr : slotList.at(index).item; // May be nullptr if unloaded
309 : }
310 :
311 96 : qsizetype NewsList::indexForItemID(const qint64 id) const
312 : {
313 : // Search within display window only
314 1997 : for (qsizetype i = displayStart; i < displayEnd; ++i) {
315 1958 : if (slotList.at(i).id() == id) {
316 57 : return i - displayStart; // Return index relative to display window
317 : }
318 : }
319 39 : return -1;
320 : }
321 :
322 4 : void NewsList::setReloadCallback(ReloadCallback callback)
323 : {
324 4 : reloadCallback = callback;
325 4 : }
326 :
327 111 : NewsPosition NewsList::positionAt(qsizetype i) const
328 : {
329 111 : if (i < 0 || i >= slotList.size()) {
330 6 : return NewsPosition(); // Invalid
331 : }
332 105 : return slotList.at(i).position;
333 : }
334 :
335 2531 : qsizetype NewsList::loadedCount() const
336 : {
337 2531 : return loadedSet.size();
338 : }
339 :
340 2514 : void NewsList::checkMemoryBounds()
341 : {
342 : // Keep at most 2x the display window size loaded in memory
343 2514 : qsizetype windowSize = displayEnd - displayStart;
344 2514 : qsizetype maxLoaded = windowSize * 2;
345 :
346 : // Ensure minimum threshold to avoid thrashing
347 2514 : if (maxLoaded < 30) {
348 870 : maxLoaded = 30;
349 : }
350 :
351 2514 : qsizetype currentLoaded = loadedCount();
352 2514 : if (currentLoaded > maxLoaded) {
353 0 : unloadDistantItems(currentLoaded - maxLoaded);
354 : }
355 2514 : }
356 :
357 0 : void NewsList::unloadDistantItems(qsizetype count)
358 : {
359 0 : qsizetype unloaded = 0;
360 :
361 : // Prefer unloading from the start if user has scrolled down
362 0 : if (displayStart > 0) {
363 : // Unload from the beginning (oldest items, furthest from display)
364 0 : for (qsizetype i = 0; i < displayStart && unloaded < count; ++i) {
365 0 : if (slotList[i].isLoaded()) {
366 0 : loadedSet.remove(slotList[i].item);
367 0 : slotList[i].item->deleteLater();
368 0 : slotList[i].item = nullptr;
369 0 : unloaded++;
370 : }
371 : }
372 : }
373 :
374 : // If we still need to unload more, unload from the end
375 0 : if (unloaded < count) {
376 0 : for (qsizetype i = slotList.size() - 1; i >= displayEnd && unloaded < count; --i) {
377 0 : if (slotList[i].isLoaded()) {
378 0 : loadedSet.remove(slotList[i].item);
379 0 : slotList[i].item->deleteLater();
380 0 : slotList[i].item = nullptr;
381 0 : unloaded++;
382 : }
383 : }
384 : }
385 :
386 0 : if (unloaded > 0) {
387 0 : qCDebug(logModel) << "NewsList::unloadDistantItems: Unloaded" << unloaded
388 0 : << "items, loaded count now" << loadedCount();
389 : }
390 0 : }
391 :
392 0 : void NewsList::populateSlots(const QList<NewsItem*>& items)
393 : {
394 : // Match reloaded items to their slotList by ID
395 0 : for (NewsItem* item : items) {
396 0 : qint64 id = item->getDbID();
397 0 : qsizetype index = fullIndexForItemID(id);
398 :
399 0 : if (index >= 0 && !slotList[index].isLoaded()) {
400 0 : slotList[index].item = item;
401 0 : loadedSet.insert(item);
402 0 : qCDebug(logModel) << "NewsList::populateSlots: Reloaded item ID" << id
403 0 : << "at index" << index;
404 0 : } else if (index < 0) {
405 0 : qCWarning(logModel) << "NewsList::populateSlots: Item ID" << id
406 0 : << "not found in slotList, deleting";
407 0 : item->deleteLater();
408 : } else {
409 0 : qCWarning(logModel) << "NewsList::populateSlots: Item ID" << id
410 0 : << "already loaded, deleting duplicate";
411 0 : item->deleteLater();
412 : }
413 : }
414 0 : }
415 :
416 22 : bool NewsList::verifyDisplayWindowIntegrity() const
417 : {
418 22 : bool allLoaded = true;
419 525 : for (qsizetype i = displayStart; i < displayEnd; ++i) {
420 503 : if (!slotList[i].isLoaded()) {
421 0 : qCCritical(logModel) << "NewsList::verifyDisplayWindowIntegrity: Unloaded item at index"
422 0 : << i << "(ID:" << slotList[i].id() << ") within display window ["
423 0 : << displayStart << "," << displayEnd << ")";
424 0 : allLoaded = false;
425 : }
426 : }
427 22 : return allLoaded;
428 : }
|