LCOV - code coverage report
Current view: top level - src/models - NewsList.cpp (source / functions) Coverage Total Hit
Test: coverage.info.cleaned Lines: 73.1 % 234 171
Test Date: 2026-03-23 10:19:47 Functions: 94.1 % 34 32

            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              : }
        

Generated by: LCOV version 2.0-1