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

    #rssContentView {
        display: table;
        width: 100%;
        height: calc(100% - 30px);
        vertical-align: top;
    }

    #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;
    }

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

    #rssTorrentDetailsName {
        background-color: var(--color-background-blue);
        padding: 0;
        color: var(--color-text-white);
    }

    #rssTorrentDetailsDate {
        background-color: var(--color-background-default);
    }

    #rssDetailsView {
        height: calc(100vh - 135px);
        overflow: auto;
    }

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

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

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

    #rssContentView table {
        width: 100%;
    }

    #rssDescription {
        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>
            <button type="button" id="rssDownloaderButton" class="alignRight" 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 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">
                    <thead>
                        <tr class="dynamicTableHeader"></tr>
                    </thead>
                    <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">
                    <thead>
                        <tr class="dynamicTableHeader"></tr>
                    </thead>
                    <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)
                $("rssFetchingDisabled").removeClass("invisible");

            // recalculate heights
            const nonPageHeight = $("rssTopBar").getBoundingClientRect().height
                + $("desktopHeader").getBoundingClientRect().height
                + $("desktopFooterWrapper").getBoundingClientRect().height + 20;
            $("rssDetailsView").style.height = "calc(100vh - " + nonPageHeight + "px)";

            const nonTableHeight = nonPageHeight + $("rssFeedFixedHeaderDiv").getBoundingClientRect().height;
            $("rssFeedTableDiv").style.height = "calc(100vh - " + nonTableHeight + "px)";
            $("rssArticleTableDiv").style.height = "calc(100vh - " + nonTableHeight + "px)";

            $("rssContentView").style.height = "calc(100% - " + $("rssTopBar").getBoundingClientRect().height + "px)";

            const rssFeedContextMenu = new window.qBittorrent.ContextMenu.RssFeedContextMenu({
                targets: ".rssFeedContextMenuTarget",
                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);
                            }
                        };
                        feedsToUpdate.forEach((feed) => 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($("rssFeedTableDiv"));
            // deselect feed when clicking on empty part of table
            $("rssFeedTableDiv").addEventListener("click", (e) => {
                rssFeedTable.deselectAll();
                rssFeedTable.deselectRow();
            });
            $("rssFeedTableDiv").addEventListener("contextmenu", (e) => {
                if (e.target.nodeName === "DIV") {
                    rssFeedTable.deselectAll();
                    rssFeedTable.deselectRow();
                    rssFeedContextMenu.updateMenuItems();
                }
            });

            new ClipboardJS("#CopyFeedURL", {
                text: () => {
                    let joined = "";
                    for (const rowID of rssFeedTable.selectedRows) {
                        const row = rssFeedTable.getRow(rowID);
                        if (row.full_data.dataUid !== "")
                            joined += `${row.full_data.dataUrl}\n`;
                    }
                    return joined.slice(0, -1);
                }
            });
            rssFeedTable.setup("rssFeedTableDiv", "rssFeedFixedHeaderDiv", rssFeedContextMenu);

            const rssArticleContextMenu = new window.qBittorrent.ContextMenu.RssArticleContextMenu({
                targets: ".rssArticleElement",
                menu: "rssArticleMenu",
                actions: {
                    Download: (el) => {
                        let dlString = "";
                        for (const rowID of rssArticleTable.selectedRows)
                            dlString += `${rssArticleTable.getRow(rowID).full_data.torrentURL}\n`;
                        showDownloadPage([dlString]);
                    },
                    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?path=" + encodeURIComponent(path),
                scrollbars: false,
                resizable: false,
                maximizable: false,
                width: 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?path=" + encodeURIComponent(path),
                scrollbars: false,
                resizable: false,
                maximizable: false,
                width: 350,
                height: 100
            });
        };

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

            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);

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

            $("rssDetailsView").getChildren().each(c => c.destroy());
            rssArticleTable.updateTable(false);
        };

        const showDetails = (feedUid, articleID) => {
            markArticleAsRead(pathByFeedId.get(feedUid), articleID);
            $("rssDetailsView").getChildren().each(c => c.destroy());
            const article = feedData[feedUid].filter((article) => article.id === articleID)[0];
            if (article) {
                $("rssDetailsView").append((() => {
                    const torrentName = document.createElement("p");
                    torrentName.textContent = article.title;
                    torrentName.id = "rssTorrentDetailsName";
                    return torrentName;
                })());
                $("rssDetailsView").append((() => {
                    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 = new Date(article.date).toLocaleString();
                    torrentDate.append(torrentDateData);

                    return torrentDate;
                })());
                // Place in iframe with sandbox attribute to prevent js execution
                const torrentDescription = document.createRange().createContextualFragment('<iframe sandbox id="rssDescription"></iframe>');
                $("rssDetailsView").append(torrentDescription);
                document.getElementById("rssDescription").srcdoc = '<html><head><link rel="stylesheet" type="text/css" href="css/style.css"></head><body>' + article.description + "</body></html>";

                // calculate height to fill screen
                document.getElementById("rssDescription").style.height =
                    "calc(100% - " + document.getElementById("rssTorrentDetailsName").offsetHeight + "px - "
                    + document.getElementById("rssTorrentDetailsDate").offsetHeight + "px - 5px)";
            }
        };

        const updateRssFeedList = () => {
            new Request.JSON({
                url: "api/v2/rss/items",
                method: "get",
                noCache: true,
                data: {
                    withData: true
                },
                onSuccess: (response) => {
                    // 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(response);

                    // 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 (let i = 0; i < flattenedResp.length; ++i) {
                            const newData = flattenedResp[i];
                            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 (let i = 0; i < flattenedResp.length; ++i) {
                            const oldStatus = rssFeedRows[i + 1].full_data.status;
                            let status = "default";
                            if (flattenedResp[i].hasError)
                                status = "hasError";
                            if (flattenedResp[i].isLoading)
                                status = "isLoading";
                            if (flattenedResp[i].isFolder)
                                status = "isFolder";

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

                        // 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((v) => v).length;
                                    const newUnread = r.articles.map((art) => !art.isRead).filter((v) => v).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 (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((v) => v).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);
                        rssFeedTable.updateIcons();
                    }
                }
            }).send();
        };

        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.updateIcons();

            new Request({
                url: "api/v2/rss/refreshItem",
                method: "post",
                data: {
                    itemPath: pathByFeedId.get(feedUid)
                },
                onFailure: (response) => {
                    if (response.status === 409)
                        alert(response.responseText);
                }
            }).send();
        };

        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?oldPath=" + encodeURIComponent(oldPath),
                scrollbars: false,
                resizable: false,
                maximizable: false,
                width: 350,
                height: 100
            });
        };

        const editUrl = (path, 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: new URI("editfeedurl.html").setData("path", path).setData("url", url).toString(),
                scrollbars: false,
                resizable: false,
                maximizable: false,
                width: 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?paths=" + encodeURIComponent(encodedPaths.join("|")),
                scrollbars: false,
                resizable: false,
                maximizable: false,
                width: 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
            new Request({
                url: "api/v2/rss/markAsRead",
                method: "post",
                data: {
                    itemPath: path
                },
                onFailure: (response) => {
                    if (response.status === 409)
                        alert(response.responseText);
                }
            }).send();
        };

        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);

                new Request({
                    url: "api/v2/rss/markAsRead",
                    method: "post",
                    data: {
                        itemPath: path,
                        articleId: id
                    },
                    onFailure: (response) => {
                        if (response.status === 409)
                            alert(response.responseText);
                    }
                }).send();
            }
        };

        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",
                maximizable: false,
                width: loadWindowWidth(id, 800),
                height: loadWindowHeight(id, 650),
                onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
                    saveWindowSize(id);
                }),
                resizeLimit: {
                    "x": [800, 2500],
                    "y": [500, 2000]
                }
            });
        };

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