<style>
    #rssView {
        padding: 20px 20px 0 20px;
        height: calc(100% - 20px);
        display: flex;
        flex-direction: column;
    }

    #rssTopBar {
        flex-shrink: 0;
    }

    #rssContentView {
        width: 100%;
        flex-grow: 1;
        overflow: auto;
    }

    #rssFeedFixedHeaderDiv .dynamicTableHeader,
    #rssArticleFixedHeaderDiv .dynamicTableHeader {
        cursor: default;
    }

    .alignRight {
        float: right;
    }

    .unreadArticle {
        color: var(--color-text-blue);
    }

    #rssFetchingDisabled {
        color: var(--color-text-red);
        font-style: italic;
        margin-bottom: 10px;
    }

    #centerRssColumn {
        margin: 0 10px 0 10px;
    }

    #leftRssColumn,
    #centerRssColumn,
    #rightRssColumn {
        float: left;
        /* should be 20 px but due to rounding differences some browsers don't render that properly */
        width: calc((100% - 21px) / 3);
        border: none;
    }

    #rightRssColumn {
        overflow: auto;
        height: 100%;
    }

    #rssFeedTableDiv,
    #rssArticleTableDiv {
        height: calc(100vh - 180px);
    }

    #rssTorrentDetailsName {
        flex-shrink: 0;
        font-weight: bold;
        background-color: var(--color-background-blue);
        padding: 0;
        color: var(--color-text-white);
    }

    #rssTorrentDetailsAuthor,
    #rssTorrentDetailsDate,
    #rssTorrentDetailsLink {
        flex-shrink: 1;
        background-color: var(--color-background-default);
    }

    #rssDetailsView {
        display: flex;
        flex-direction: column;
        height: 100%;
        overflow: auto;
    }

    #rssButtonBar {
        overflow: hidden;
        height: 30px;
    }

    #rssButtonBar button {
        padding: 3px 6px;
    }

    #rssButtonBar input {
        background-color: var(--color-background-default);
        background-image: url("../images/edit-find.svg");
        background-position: 2px;
        background-repeat: no-repeat;
        background-size: 1.5em;
        border: 1px solid var(--color-border-default);
        border-radius: 3px;
        min-width: 170px;
        padding: 2px 2px 2px 25px;
    }

    #rssButtonBar div {
        display: inline-block;
        vertical-align: top;
    }

    #rssButtonBar button img {
        margin: 0 5px -3px 0;
    }

    #rssFilterToolbar {
        float: right;
        margin-right: 5px;
    }

    #rssContentView table {
        width: 100%;
    }

    #rssDescription {
        flex-grow: 2;
        width: 100%;
        border: none;
    }

</style>

<div id="rssView">
    <div id="rssTopBar">
        <div id="rssFetchingDisabled" class="invisible">
            QBT_TR(Fetching of RSS feeds is disabled now! You can enable it in application settings.)QBT_TR[CONTEXT=RSSWidget]
        </div>
        <div id="rssButtonBar">
            <button type="button" id="newSubscriptionButton" onclick="qBittorrent.Rss.addRSSFeed()">
                <img alt="QBT_TR(New subscription)QBT_TR[CONTEXT=RSSWidget]" src="images/list-add.svg" width="16" height="16">QBT_TR(New subscription)QBT_TR[CONTEXT=RSSWidget]
            </button>
            <button type="button" id="markReadButton" onclick="qBittorrent.Rss.markSelectedAsRead()">
                <img alt="QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]" src="images/task-complete.svg" width="16" height="16">QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]
            </button>
            <button type="button" id="updateAllButton" onclick="qBittorrent.Rss.refreshAllFeeds()">
                <img alt="QBT_TR(Update all)QBT_TR[CONTEXT=RSSWidget]" src="images/view-refresh.svg" width="16" height="16">QBT_TR(Update all)QBT_TR[CONTEXT=RSSWidget]
            </button>
            <div id="rssFilterToolbar" class="alignRight">
                <input type="search" id="rssFilterInput" placeholder="QBT_TR(Filter feed items...)QBT_TR[CONTEXT=MainWindow]" aria-label="QBT_TR(Filter feed items...)QBT_TR[CONTEXT=MainWindow]" autocorrect="off" autocapitalize="none">
                <button type="button" id="rssDownloaderButton" onclick="qBittorrent.Rss.openRssDownloader()">
                    <img alt="QBT_TR(RSS Downloader...)QBT_TR[CONTEXT=RSSWidget]" src="images/downloading.svg" width="16" height="16">QBT_TR(RSS Downloader...)QBT_TR[CONTEXT=RSSWidget]
                </button>
            </div>
        </div>
    </div>
    <div id="rssContentView">
        <div id="leftRssColumn">
            <div id="rssFeedFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
                <table class="dynamicTable unselectable">
                    <thead>
                        <tr class="dynamicTableHeader"></tr>
                    </thead>
                </table>
            </div>
            <div id="rssFeedTableDiv" class="dynamicTableDiv">
                <table class="dynamicTable unselectable">
                    <tbody></tbody>
                </table>
            </div>
        </div>
        <div id="centerRssColumn">
            <div id="rssArticleFixedHeaderDiv" class="dynamicTableFixedHeaderDiv">
                <table class="dynamicTable unselectable">
                    <thead>
                        <tr class="dynamicTableHeader"></tr>
                    </thead>
                </table>
            </div>
            <div id="rssArticleTableDiv" class="dynamicTableDiv">
                <table class="dynamicTable unselectable">
                    <tbody></tbody>
                </table>
            </div>
        </div>
        <div id="rightRssColumn">
            <div id="rssDetailsView"></div>
        </div>
    </div>
</div>

<ul id="rssFeedMenu" class="contextMenu">
    <li><a href="#update"><img src="images/view-refresh.svg" alt="QBT_TR(Update)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Update)QBT_TR[CONTEXT=RSSWidget]</a></li>
    <li><a href="#markRead"><img src="images/task-complete.svg" alt="QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Mark items read)QBT_TR[CONTEXT=RSSWidget]</a></li>
    <li class="separator"><a href="#rename"><img src="images/edit-rename.svg" alt="QBT_TR(Rename...)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Rename...)QBT_TR[CONTEXT=RSSWidget]</a></li>
    <li><a href="#edit"><img src="images/edit-rename.svg" alt="QBT_TR(Edit feed URL...)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Edit feed URL...)QBT_TR[CONTEXT=RSSWidget]</a></li>
    <li><a href="#delete"><img src="images/edit-clear.svg" alt="QBT_TR(Delete)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Delete)QBT_TR[CONTEXT=RSSWidget]</a></li>

    <li class="separator"><a href="#newSubscription"><img src="images/list-add.svg" alt="QBT_TR(New subscription...)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(New subscription...)QBT_TR[CONTEXT=RSSWidget]</a></li>
    <li><a href="#newFolder"><img src="images/folder-new.svg" alt="QBT_TR(New folder...)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(New folder...)QBT_TR[CONTEXT=RSSWidget]</a></li>
    <li class="separator"><a href="#updateAll"><img src="images/view-refresh.svg" alt="QBT_TR(Update all feeds)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Update all feeds)QBT_TR[CONTEXT=RSSWidget]</a></li>

    <li class="separator"><a href="#copyFeedURL" id="CopyFeedURL"><img src="images/edit-copy.svg" alt="QBT_TR(Copy feed URL)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Copy feed URL)QBT_TR[CONTEXT=RSSWidget]</a></li>
</ul>

<ul id="rssArticleMenu" class="contextMenu">
    <li><a href="#Download"><img src="images/downloading.svg" alt="QBT_TR(Download torrent)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Download torrent)QBT_TR[CONTEXT=RSSWidget]</a></li>
    <li><a href="#OpenNews"><img src="images/application-url.svg" alt="QBT_TR(Open news URL)QBT_TR[CONTEXT=RSSWidget]"> QBT_TR(Open news URL)QBT_TR[CONTEXT=RSSWidget]</a></li>
</ul>

<script>
    "use strict";

    window.qBittorrent ??= {};
    window.qBittorrent.Rss ??= (() => {
        const exports = () => {
            return {
                init: init,
                unload: unload,
                load: load,
                showRssFeed: showRssFeed,
                showDetails: showDetails,
                updateRssFeedList: updateRssFeedList,
                refreshAllFeeds: refreshAllFeeds,
                moveItem: moveItem,
                addRSSFeed: addRSSFeed,
                markSelectedAsRead: markSelectedAsRead,
                openRssDownloader: openRssDownloader,
                rssFeedTable: rssFeedTable
            };
        };

        const serverSyncRssDataInterval = 1500;
        let feedData = {};
        let pathByFeedId = new Map();
        let feedRefreshTimer = -1;
        const rssFeedTable = new window.qBittorrent.DynamicTable.RssFeedTable();
        const rssArticleTable = new window.qBittorrent.DynamicTable.RssArticleTable();

        const init = () => {
            const pref = window.parent.qBittorrent.Cache.preferences.get();

            if (!pref.rss_processing_enabled)
                document.getElementById("rssFetchingDisabled").classList.remove("invisible");

            const rssFeedContextMenu = new window.qBittorrent.ContextMenu.RssFeedContextMenu({
                targets: "#rssFeedTableDiv tbody tr",
                menu: "rssFeedMenu",
                actions: {
                    update: (el) => {
                        const feedsToUpdate = new Set();
                        for (const rowId of rssFeedTable.selectedRows) {
                            const selectedPath = rssFeedTable.getRow(rowId).full_data.dataPath;
                            for (const row of rssFeedTable.getRowValues()) {
                                if ((row.full_data.dataPath.slice(0, selectedPath.length) === selectedPath) && (row.full_data.dataUid !== ""))
                                    feedsToUpdate.add(row);
                            }
                        }
                        for (const feed of feedsToUpdate)
                            refreshFeed(feed.full_data.dataUid);
                    },
                    markRead: markSelectedAsRead,
                    rename: (el) => {
                        moveItem(rssFeedTable.getRow(rssFeedTable.selectedRows[0]).full_data.dataPath);
                    },
                    edit: (el) => {
                        const data = rssFeedTable.getRow(rssFeedTable.selectedRows[0]).full_data;
                        editUrl(data.dataPath, data.dataUrl);
                    },
                    delete: (el) => {
                        const selectedDatapaths = rssFeedTable.selectedRows
                            .filter((rowID) => rowID !== "0")
                            .map((rowID) => rssFeedTable.getRow(rowID).full_data.dataPath);
                        // filter children
                        const reducedDatapaths = selectedDatapaths.filter((path) =>
                            selectedDatapaths.filter((innerPath) => path.slice(0, innerPath.length) === innerPath).length === 1
                        );
                        removeItem(reducedDatapaths);
                    },
                    newSubscription: addRSSFeed,
                    newFolder: addFolder,
                    updateAll: refreshAllFeeds
                },
                offsets: {
                    x: 0,
                    y: -60
                }
            });

            rssFeedContextMenu.addTarget(document.getElementById("rssFeedTableDiv"));
            // deselect feed when clicking on empty part of table
            document.getElementById("rssFeedTableDiv").addEventListener("click", (e) => {
                rssFeedTable.deselectAll();
                rssFeedTable.deselectRow();
            });
            document.getElementById("rssFeedTableDiv").addEventListener("contextmenu", (e) => {
                if (e.target.nodeName === "DIV") {
                    rssFeedTable.deselectAll();
                    rssFeedTable.deselectRow();
                    rssFeedContextMenu.updateMenuItems();
                }
            });
            document.getElementById("rssFilterInput").addEventListener("input", (event) => {
                const rowId = rssFeedTable.selectedRows[0];
                let path = "";
                for (const row of rssFeedTable.getRowValues()) {
                    if (row.rowId === rowId) {
                        path = row.full_data.dataPath;
                        break;
                    }
                }
                qBittorrent.Rss.showRssFeed(path);
            });

            document.getElementById("CopyFeedURL").addEventListener("click", async (event) => {
                let joined = "";
                for (const rowID of rssFeedTable.selectedRows) {
                    const row = rssFeedTable.getRow(rowID);
                    if (row.full_data.dataUid !== "")
                        joined += `${row.full_data.dataUrl}\n`;
                }
                await clipboardCopy(joined.slice(0, -1));
            });

            rssFeedTable.setup("rssFeedTableDiv", "rssFeedFixedHeaderDiv", rssFeedContextMenu);

            const rssArticleContextMenu = new window.qBittorrent.ContextMenu.RssArticleContextMenu({
                targets: "#rssArticleTableDiv tbody tr",
                menu: "rssArticleMenu",
                actions: {
                    Download: (el) => {
                        for (const rowID of rssArticleTable.selectedRows) {
                            const { name, torrentURL } = rssArticleTable.getRow(rowID).full_data;
                            window.qBittorrent.Client.createAddTorrentWindow(name, torrentURL);
                        }
                    },
                    OpenNews: (el) => {
                        for (const rowID of rssArticleTable.selectedRows)
                            window.open(rssArticleTable.getRow(rowID).full_data.link);
                    }
                },
                offsets: {
                    x: 0,
                    y: -60
                }
            });
            rssArticleTable.setup("rssArticleTableDiv", "rssArticleFixedHeaderDiv", rssArticleContextMenu);
            updateRssFeedList();
            load();
        };

        const unload = () => {
            clearTimeout(feedRefreshTimer);
            feedRefreshTimer = -1;
        };

        const load = () => {
            feedRefreshTimer = setTimeout(() => {
                updateRssFeedList();
                load();
            }, serverSyncRssDataInterval);
        };

        const addRSSFeed = () => {
            let path = "";
            if (rssFeedTable.selectedRows.length !== 0) {
                const row = rssFeedTable.getRow(rssFeedTable.selectedRows[0]);
                if (row.full_data.dataUid === "") {
                    path = row.full_data.dataPath;
                }
                else {
                    const lastIndex = row.full_data.dataPath.lastIndexOf("\\");
                    if (lastIndex !== -1)
                        path = row.full_data.dataPath.slice(0, lastIndex);
                }
            }

            new MochaUI.Window({
                id: "newFeed",
                icon: "images/qbittorrent-tray.svg",
                title: "QBT_TR(Please type a RSS feed URL)QBT_TR[CONTEXT=RSSWidget]",
                loadMethod: "iframe",
                contentURL: `newfeed.html?v=${CACHEID}&path=${encodeURIComponent(path)}`,
                scrollbars: false,
                resizable: false,
                maximizable: false,
                width: window.qBittorrent.Dialog.limitWidthToViewport(350),
                height: 100
            });
        };

        const addFolder = () => {
            let path = "";
            if (rssFeedTable.selectedRows.length !== 0) {
                const row = rssFeedTable.getRow(rssFeedTable.selectedRows[0]);
                if (row.full_data.dataUid === "") {
                    path = row.full_data.dataPath;
                }
                else {
                    const lastIndex = row.full_data.dataPath.lastIndexOf("\\");
                    if (lastIndex !== -1)
                        path = row.full_data.dataPath.slice(0, lastIndex);
                }
            }

            new MochaUI.Window({
                id: "newFolder",
                icon: "images/qbittorrent-tray.svg",
                title: "QBT_TR(Please choose a folder name)QBT_TR[CONTEXT=RSSWidget]",
                loadMethod: "iframe",
                contentURL: `newfolder.html?v=${CACHEID}&path=${encodeURIComponent(path)}`,
                scrollbars: false,
                resizable: false,
                maximizable: false,
                width: window.qBittorrent.Dialog.limitWidthToViewport(350),
                height: 100
            });
        };

        const showRssFeed = (path) => {
            rssArticleTable.clear();

            const childFeeds = new Set();
            for (const row of rssFeedTable.getRowValues()) {
                if ((row.full_data.dataPath.slice(0, path.length) === path) && (row.full_data.dataUid !== ""))
                    childFeeds.add(row.full_data.dataUid);
            }

            let visibleArticles = [];
            for (const feedEntry in feedData) {
                if (childFeeds.has(feedEntry)) {
                    visibleArticles.append(feedData[feedEntry]
                        .map((a) => {
                            a.feedUid = feedEntry;
                            return a;
                        }));
                }
            }
            // filter read articles if "Unread" feed is selected
            if (path === "")
                visibleArticles = visibleArticles.filter((a) => !a.isRead);

            const rssFilterInput = document.getElementById("rssFilterInput");
            if (rssFilterInput.value.length > 0) {
                const lowerFilter = rssFilterInput.value.toLowerCase();
                visibleArticles = visibleArticles.filter((a) => a.title.toLowerCase().includes(lowerFilter));
            }

            let rowID = -1;
            visibleArticles.sort((e1, e2) => new Date(e2.date) - new Date(e1.date))
                .each((torrentEntry) => {
                    rssArticleTable.updateRowData({
                        rowId: ++rowID,
                        name: torrentEntry.title,
                        link: torrentEntry.link,
                        torrentURL: torrentEntry.torrentURL,
                        feedUid: torrentEntry.feedUid,
                        dataId: torrentEntry.id,
                        isRead: torrentEntry.isRead
                    });
                });

            clearDetails();
            rssArticleTable.updateTable(false);
        };

        const clearDetails = () => {
            for (const el of [...document.getElementById("rssDetailsView").children])
                el.remove();
        };

        const showDetails = (feedUid, articleID) => {
            markArticleAsRead(pathByFeedId.get(feedUid), articleID);
            clearDetails();

            const article = feedData[feedUid].find((article) => (article.id === articleID));
            if (article === undefined)
                return;

            const detailsView = document.getElementById("rssDetailsView");

            const articleTitle = article.title;
            if (articleTitle !== undefined) {
                const torrentName = document.createElement("p");
                torrentName.textContent = articleTitle;
                torrentName.id = "rssTorrentDetailsName";

                detailsView.append(torrentName);
            }

            const articleDate = article.date;
            if (articleDate !== undefined) {
                const torrentDate = document.createElement("div");
                torrentDate.id = "rssTorrentDetailsDate";

                const torrentDateDesc = document.createElement("b");
                torrentDateDesc.textContent = "QBT_TR(Date: )QBT_TR[CONTEXT=RSSWidget]";
                torrentDate.append(torrentDateDesc);

                const torrentDateData = document.createElement("span");
                torrentDateData.textContent = window.qBittorrent.Misc.formatDate(new Date(articleDate));
                torrentDate.append(torrentDateData);

                detailsView.append(torrentDate);
            }

            const articleAuthor = article.author;
            if (articleAuthor !== undefined) {
                const divElement = document.createElement("div");
                divElement.id = "rssTorrentDetailsAuthor";

                const header = document.createElement("b");
                header.textContent = "QBT_TR(Author: )QBT_TR[CONTEXT=RSSWidget]";
                divElement.append(header);

                const span = document.createElement("span");
                span.textContent = articleAuthor;
                divElement.append(span);

                detailsView.append(divElement);
            }

            const articleLink = article.link;
            if (articleLink !== undefined) {
                const divElement = document.createElement("div");
                divElement.id = "rssTorrentDetailsLink";

                const anchorElement = document.createElement("a");
                anchorElement.href = articleLink;
                anchorElement.textContent = "QBT_TR(Open link)QBT_TR[CONTEXT=RSSWidget]";
                anchorElement.target = "_blank";
                anchorElement.style = "font-weight: bold;";
                divElement.append(anchorElement);

                detailsView.append(divElement);
            }

            const articleDescription = article.description;
            if (articleDescription !== undefined) {
                const rootColor = document.documentElement.classList.contains("dark") ? "class='dark'" : "";

                // Place in iframe with sandbox attribute to prevent js execution
                const iframeElement = document.createElement("iframe");
                iframeElement.id = "rssDescription";
                iframeElement.sandbox = "allow-same-origin"; // allowed to get parent css
                iframeElement.srcdoc = `<html ${rootColor}><head><meta charset="utf-8"><meta name="referrer" content="same-origin"><link rel="stylesheet" type="text/css" href="css/style.css?v=${CACHEID}"></head><body>${articleDescription}</body></html>`;

                detailsView.append(iframeElement);
            }
        };

        const updateRssFeedList = () => {
            const url = new URL("api/v2/rss/items", window.location);
            url.search = new URLSearchParams({
                withData: true
            });
            fetch(url, {
                    method: "GET",
                    cache: "no-store"
                })
                .then(async (response) => {
                    if (!response.ok)
                        return;

                    const responseJSON = await response.json();

                    // flatten folder structure
                    const flattenedResp = [];
                    const recFlatten = (current, name = "", depth = 0, fullName = "") => {
                        for (const child in current) {
                            if (!Object.hasOwn(current, child))
                                continue;

                            const currentFullName = fullName ? `${fullName}\\${child}` : child;
                            if (current[child].uid !== undefined) {
                                current[child].name = child;
                                current[child].isFolder = false;
                                current[child].depth = depth;
                                current[child].fullName = currentFullName;
                                flattenedResp.push(current[child]);
                            }
                            else {
                                flattenedResp.push({
                                    name: child,
                                    isFolder: true,
                                    depth: depth,
                                    fullName: currentFullName
                                });
                                recFlatten(current[child], child, depth + 1, currentFullName);
                            }
                        }
                    };
                    recFlatten(responseJSON);

                    // check if rows matches flattened response
                    const rssFeedRows = [...rssFeedTable.getRowValues()];
                    let match = false;
                    // subtract 'unread' row
                    if ((rssFeedRows.length - 1) === flattenedResp.length) {
                        match = true;
                        for (const [i, newData] of flattenedResp.entries()) {
                            const existingData = rssFeedRows[i + 1].full_data;

                            if (newData.fullName !== existingData.dataPath) {
                                match = false;
                                break;
                            }
                            // only non-folders have these properties
                            if (!newData.isFolder && ((newData.uid !== existingData.dataUid) || (newData.url !== existingData.dataUrl))) {
                                match = false;
                                break;
                            }
                        }
                    }

                    if (match) {
                        // partial refresh
                        // update status
                        let statusDiffers = false;
                        for (const [i, item] of flattenedResp.entries()) {
                            const oldStatus = rssFeedRows[i + 1].full_data.status;
                            let status = "default";
                            if (item.hasError)
                                status = "hasError";
                            if (item.isLoading)
                                status = "isLoading";
                            if (item.isFolder)
                                status = "isFolder";

                            if (oldStatus !== status) {
                                statusDiffers = true;
                                rssFeedTable.updateRowData({
                                    rowId: i + 1,
                                    status: status
                                });
                            }
                        }

                        // get currently opened feed
                        let openedFeedPath = undefined;
                        if (rssFeedTable.selectedRows.length !== 0) {
                            const lastSelectedRow = rssFeedTable.selectedRows.at(-1);
                            openedFeedPath = rssFeedTable.getRow(lastSelectedRow).full_data.dataPath;
                        }

                        // check if list of articles differs
                        let needsUpdate = false;
                        flattenedResp.filter((r) => !r.isFolder)
                            .each((r) => {
                                let articlesDiffer = true;
                                if (r.articles.length === feedData[r.uid].length) {
                                    articlesDiffer = false;
                                    for (let i = 0; i < r.articles.length; ++i) {
                                        if (feedData[r.uid][i].id !== r.articles[i].id)
                                            articlesDiffer = true;
                                    }
                                }

                                if (articlesDiffer) {
                                    // update unread count
                                    const oldUnread = feedData[r.uid].map((art) => !art.isRead).filter(Boolean).length;
                                    const newUnread = r.articles.map((art) => !art.isRead).filter(Boolean).length;
                                    const unreadDifference = newUnread - oldUnread;

                                    // find all parents (and self) and add unread difference
                                    for (const row of rssFeedTable.getRowValues()) {
                                        if (r.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
                                            row.full_data.unread += unreadDifference;
                                    }

                                    needsUpdate = true;

                                    // update data
                                    feedData[r.uid] = r.articles;

                                    // if feed that is open changed, reload
                                    if ((openedFeedPath !== undefined) && (r.fullName.slice(0, openedFeedPath.length) === openedFeedPath))
                                        showRssFeed(r.fullName);
                                }
                                else {
                                    // calculate read difference and update feed data
                                    let readDifference = 0;
                                    let readChanged = false;
                                    for (let i = 0; i < r.articles.length; ++i) {
                                        const oldRead = feedData[r.uid][i].isRead ? 1 : 0;
                                        const newRead = r.articles[i].isRead ? 1 : 0;
                                        feedData[r.uid][i].isRead = r.articles[i].isRead;
                                        readDifference += oldRead - newRead;
                                        if (readDifference !== 0)
                                            readChanged = true;
                                    }

                                    // if read on article changed
                                    if (readChanged) {
                                        needsUpdate = true;
                                        // find all items that contain this rss feed and add read difference
                                        for (const row of rssFeedTable.getRowValues()) {
                                            if (r.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
                                                row.full_data.unread += readDifference;
                                        }

                                        // if feed that is opened changed update dynamically
                                        if ((openedFeedPath !== undefined) && (r.fullName.slice(0, openedFeedPath.length) === openedFeedPath)) {
                                            for (let i = 0; i < r.articles.length; ++i) {
                                                for (const row of rssArticleTable.getRowValues()) {
                                                    if ((row.full_data.feedUid === r.uid) && (row.full_data.dataId === r.articles[i].id)) {
                                                        row.full_data.isRead = r.articles[i].isRead;
                                                        break;
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            });
                        if (statusDiffers)
                            rssFeedTable.updateTable(true);
                        if (needsUpdate) {
                            rssFeedTable.updateTable(true);
                            rssArticleTable.updateTable(true);
                        }

                    }
                    else {
                        // full refresh
                        rssFeedTable.clear();
                        rssArticleTable.clear();
                        feedData = {};
                        pathByFeedId = new Map();

                        // Unread entry at top
                        rssFeedTable.updateRowData({
                            rowId: 0,
                            name: "QBT_TR(Unread)QBT_TR[CONTEXT=FeedListWidget]",
                            unread: 0,
                            status: "unread",
                            indentation: 0,
                            dataUid: "",
                            dataUrl: "",
                            dataPath: ""
                        });

                        let rowCount = 1;
                        for (const dataEntry of flattenedResp) {
                            if (dataEntry.isFolder) {
                                rssFeedTable.updateRowData({
                                    rowId: rowCount,
                                    name: dataEntry.name,
                                    unread: 0,
                                    status: "isFolder",
                                    indentation: dataEntry.depth,
                                    dataUid: "",
                                    dataUrl: "",
                                    dataPath: dataEntry.fullName
                                });
                            }
                            else {
                                let status = "default";
                                if (dataEntry.hasError)
                                    status = "hasError";
                                if (dataEntry.isLoading)
                                    status = "isLoading";

                                rssFeedTable.updateRowData({
                                    rowId: rowCount,
                                    name: dataEntry.name,
                                    unread: 0,
                                    status: status,
                                    indentation: dataEntry.depth,
                                    dataUid: dataEntry.uid,
                                    dataUrl: dataEntry.url,
                                    dataPath: dataEntry.fullName
                                });

                                // calculate number of unread
                                const numberOfUnread = dataEntry.articles.map((art) => !art.isRead).filter(Boolean).length;
                                // find all items that contain this rss feed and add unread count
                                for (const row of rssFeedTable.getRowValues()) {
                                    if (dataEntry.fullName.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
                                        row.full_data.unread += numberOfUnread;
                                }

                                pathByFeedId.set(dataEntry.uid, dataEntry.fullName);
                                feedData[dataEntry.uid] = dataEntry.articles;
                            }
                            ++rowCount;
                        }
                        rssFeedTable.updateTable(false);
                    }
                });
        };

        const refreshFeed = (feedUid) => {
            // set icon to loading
            for (const row of rssFeedTable.getRowValues()) {
                if (row.full_data.dataUid === feedUid)
                    row.full_data.status = "isLoading";
            }
            rssFeedTable.updateTable(true);

            fetch("api/v2/rss/refreshItem", {
                    method: "POST",
                    body: new URLSearchParams({
                        itemPath: pathByFeedId.get(feedUid)
                    })
                })
                .then(async (response) => {
                    if (!response.ok) {
                        if (response.status === 409)
                            alert(await response.text());
                        return;
                    }
                });
        };

        const refreshAllFeeds = () => {
            for (const feedEntry in feedData) {
                if (!Object.hasOwn(feedData, feedEntry))
                    continue;
                refreshFeed(feedEntry);
            }
        };

        const moveItem = (oldPath) => {
            new MochaUI.Window({
                id: "renamePage",
                icon: "images/qbittorrent-tray.svg",
                title: "QBT_TR(Please choose a new name for this RSS feed)QBT_TR[CONTEXT=RSSWidget]",
                loadMethod: "iframe",
                contentURL: `rename_feed.html?v=${CACHEID}&oldPath=${encodeURIComponent(oldPath)}`,
                scrollbars: false,
                resizable: false,
                maximizable: false,
                width: window.qBittorrent.Dialog.limitWidthToViewport(350),
                height: 100
            });
        };

        const editUrl = (path, url) => {
            const contentURL = new URL("editfeedurl.html", window.location);
            contentURL.search = new URLSearchParams({
                v: "${CACHEID}",
                path: path,
                url: url
            });
            new MochaUI.Window({
                id: "editFeedURL",
                icon: "images/qbittorrent-tray.svg",
                title: "QBT_TR(Please type a RSS feed URL)QBT_TR[CONTEXT=RSSWidget]",
                loadMethod: "iframe",
                contentURL: contentURL.toString(),
                scrollbars: false,
                resizable: false,
                maximizable: false,
                width: window.qBittorrent.Dialog.limitWidthToViewport(350),
                height: 100
            });
        };

        const removeItem = (paths) => {
            const encodedPaths = paths.map((path) => encodeURIComponent(path));
            new MochaUI.Window({
                id: "confirmFeedDeletionPage",
                icon: "images/qbittorrent-tray.svg",
                title: "QBT_TR(Deletion confirmation)QBT_TR[CONTEXT=RSSWidget]",
                loadMethod: "iframe",
                contentURL: `confirmfeeddeletion.html?v=${CACHEID}&paths=${encodeURIComponent(encodedPaths.join("|"))}`,
                scrollbars: false,
                resizable: false,
                maximizable: false,
                width: window.qBittorrent.Dialog.limitWidthToViewport(350),
                height: 70
            });
        };

        const markItemAsRead = (path) => {
            // feed data mark as read
            for (const feedID in feedData) {
                if (pathByFeedId.get(feedID).slice(0, path.length) === path)
                    feedData[feedID].each((el) => el.isRead = true);
            }

            // mark rows as read
            for (const row of rssArticleTable.getRowValues())
                row.full_data.isRead = true;

            // find all children and set unread count to 0
            for (const row of rssFeedTable.getRowValues()) {
                if ((row.full_data.dataPath.slice(0, path.length) === path) && (path !== row.full_data.dataPath))
                    row.full_data.unread = 0;
            }

            // find selected row
            let prevUnreadCount;
            for (const row of rssFeedTable.getRowValues()) {
                if (row.full_data.dataPath === path) {
                    prevUnreadCount = row.full_data.unread;
                    break;
                }
            }

            // find all parents (and self) and subtract previous unread count
            for (const row of rssFeedTable.getRowValues()) {
                if (path.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
                    row.full_data.unread -= prevUnreadCount;
            }

            rssArticleTable.updateTable(false);
            rssFeedTable.updateTable(true);

            // send request
            fetch("api/v2/rss/markAsRead", {
                    method: "POST",
                    body: new URLSearchParams({
                        itemPath: path
                    })
                })
                .then(async (response) => {
                    if (!response.ok) {
                        if (response.status === 409)
                            alert(await response.text());
                        return;
                    }
                });
        };

        const markArticleAsRead = (path, id) => {
            // find row
            let uid;
            for (const row of rssFeedTable.getRowValues()) {
                if (row.full_data.dataPath === path) {
                    uid = row.full_data.dataUid;
                    break;
                }
            }

            // update feed data
            let prevReadState = true;
            feedData[uid].each((article) => {
                if (article.id === id) {
                    prevReadState = article.isRead;
                    article.isRead = true;
                }
            });

            if (!prevReadState) {
                // find all items that contain this feed and subtract 1
                for (const row of rssFeedTable.getRowValues()) {
                    if (path.slice(0, row.full_data.dataPath.length) === row.full_data.dataPath)
                        row.full_data.unread -= 1;
                }

                rssFeedTable.updateTable(true);

                fetch("api/v2/rss/markAsRead", {
                        method: "POST",
                        body: new URLSearchParams({
                            itemPath: path,
                            articleId: id
                        })
                    })
                    .then(async (response) => {
                        if (!response.ok) {
                            if (response.status === 409)
                                alert(await response.text());
                            return;
                        }
                    });
            }
        };

        const markSelectedAsRead = () => {
            const selectedDatapaths = rssFeedTable.selectedRows
                .map((sRow) => rssFeedTable.getRow(sRow).full_data.dataPath);
            // filter children
            const reducedDatapaths = selectedDatapaths.filter((path) =>
                selectedDatapaths.filter((innerPath) => path.slice(0, innerPath.length) === innerPath).length === 1
            );
            reducedDatapaths.each((path) => markItemAsRead(path));
        };

        const openRssDownloader = () => {
            const id = "rssdownloaderpage";
            new MochaUI.Window({
                id: id,
                icon: "images/qbittorrent-tray.svg",
                title: "QBT_TR(Rss Downloader)QBT_TR[CONTEXT=AutomatedRssDownloader]",
                loadMethod: "xhr",
                contentURL: "views/rssDownloader.html?v=${CACHEID}",
                maximizable: false,
                width: loadWindowWidth(id, 800, false),
                height: loadWindowHeight(id, 650, false),
                onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
                    saveWindowSize(id);
                }),
                resizeLimit: {
                    x: [800, 2500],
                    y: [500, 2000]
                }
            });
        };

        return exports();
    })();
    Object.freeze(window.qBittorrent.Rss);
</script>
