View Available RSS Feeds (If Any)
// ==UserScript== // @name RSS+ : Show Site All RSS // @name:zh RSS+ : 显示当前网站所有的 RSS // @name:zh-CN RSS+ : 显示当前网站所有的 RSS // @name:zh-TW RSS+ : 顯示當前網站所有的 RSS // @name:ja RSS+ : 現在のサイトのRSSを表示 // @name:ko RSS+ : 현재 사이트의 RSS 표시 // @name:pt-PT RSS+ : Mostrar todos os RSS do site // @name:pt-BR RSS+ : Mostrar todos os RSS do site // @name:fr RSS+ : Afficher tous les flux RSS du site // @description View Available RSS Feeds (If Any) // @description:zh 显示当前网站的所有 RSS(如果有的话) // @description:zh-CN 显示当前网站的所有 RSS(如果有的话) // @description:zh-TW 顯示當前網站的所有 RSS(如果有的话) // @description:ja サイトのすべてのRSSを表示します (あれば) // @description:ko 웹 사이트의 모든 RSS 를 표시합니다 (있는 경우) // @description:pt-PT Mostra todos os feeds RSS do site (se houver) // @description:pt-BR Mostra todos os feeds RSS do site (se houver) // @description:fr Montre tous les flux RSS du site (s'il y en a) // @license GPL3.0 // @version 1.2.1 // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAgAElEQVR4Xu1dC5hbVbX+18m0w6sCnZxMefkAlOKDV5XmpC23KCAPLyiviw9QQCm0zUkLIg/1WpWnRdqctGCRl1cUBHy0iEVA4dI2JwUrCKJFQRS40MnJlEIRmOnkrPvtTGaYSZOZZO99MplOzvf5gczea6299vmz99l7rX8Rmk/TA00PVPQANX3T9EDTA5U90ARI8+1oemAIDzQBEuDrkbvW2gMhTAYw2TewBxgTDNBOIH+CzzQBEP8fO4l/+sAEYoj/BiZsNoDNADb7wBvinwbxZrCx2Qe/AfF3H/8HYD3yWB8+3xX/3nwC8EATIBqcmktZk/M+9idgMgiTDWAyM/YH9b7wgT+MzUT4qy8Aw1jPwPqQgb+G4+76wHVv4wqaAKlxgr2bpk3gN3pmGIYx3fd5BhFNr1FEvZs/7DP/b8gwHvGN8Y9G5jwsVqTmU6UHmgAZxlFim+SPQ4x8WGQgygyrSt82arOHiWgNw3+UjfyjkTmPbWhUQxvBriZAyszChpT1EcPn4w2DjtsGADHke8aAC+Z7fYNWTIq7TzXCS9lINjQBUpyNwgd1Cx/PoOMBHN1Ik1RHW+4j8Ar00Irmh3+v18c0QF681tq+dRwfTz4dD4IAhjhRaj7i5Iyxgg1e0bWFVux1vvvWWHXKmASIl5w6hcn4EqEAineP1cmvctwvMLCC2L/VTKxdV2WfbabZmAKIAAYM4xwwztlmZrCeAyHcAN+/YSwBZUwApAkMzSgaQ0DZpgHSBIZmYJSKGwNA2SYBknWsg4lwbnMrFTBA+sQTbmDGDyK2+3idNNZNzTYFkOzSmTtRvutCAOJ/29fNi01FwgPipGshh1oXbku39dsMQHKp2BeYWQDjgOb7OqIeeJKIFobj6dtG1ApNykc9QHJLolPBuJCZTtLkk6YYDR4g4p+DsDA8N7NWg7gREzFqAfLajdbELf/GhUyF7VRoxDzYVDyUB/LEWDhuRyzc+cvuxtHoqlEJkFwydjoTXyryLEaH0+lvIKwnYD377Bkh3pRneo2ZXguBN7FvvOajZ9Obm7Z/TYxnh13e2tlAyy5k+DvnQbsQ8c4h4p39PO1CBpksxs1i7PyB0TF+rCemK8KJ9I9Hib39Zo4qgPCdHxqf2/CuqwHMa0xH058I/Ccf/IzIyWgBr5+4ce16WgA/CHt5AYyNE6dO7gFNFrkoBmg/JhwAxkFB6NMgc3F40usX0alPd2uQVRcRowYgnc60qM/+1SAcVhfPVKGECE+zjwwIv9sOLQ9OsFd5VXQLvMlLSw5tG8ehGQbjP0CY2VCAYTxikHFRm70mE7gjNCgYFQDpSMVmG8xi5RjZYELGPwA8xAZcotAqc+7qv2mYg8BFdCyavjdC+ekGeDqYPgHC3oErHVrBGz7RRe3x9HUjbMew6hsaIOKXsNVvuQrgLw87kuAabCai5cRYPrG1aznNWrclOFXBS+ZlU8Zt7Go9gQknMPMJIh8+eK2VNNCNXUbPxXvOfbRz5GwYWnPDAiTnRI9goqvBOGQknMfA/QbRcmzh5dtqbkQhB2YcneAzn0DAUSPhZxD+SMwXhe3MgyOifxilDQmQ4inV/4yAw/7lM986vsX41a5z0k+MgP4RU/nq0thBPb5/ApjOBPCeehvCwNkR27253nqH09dwACl+bywdznCdf2fgSSK+9c1Qy63vmb36VZ2yR5usf103fdcd8j1fYiaRL1PXqASf+aL2ROZ7jeSzhgJILhX9GjOJj/F6PasYuNXc6N4a1FFsvQaiW484QvYmWgIkXwIwQ7f8il8lRN8Lx9MX1UvfcHoaBiC5VOxqZv7acAZr+Tvh1wbo1rZ4+uda5G3jQjpTsZN88JfA+FQ9hkrAzWHbPbseuobT0RAA8ZzocvSSJQT6ENGLDP8KM575QaCKtlHhXip6LsG4lJn3CnqIBPwibLsjHl834gDxHEuw/+0XtMMB+mHLOP+KXc/L/DN4Xduuhlevj763Z4txKcBfCXqUDNwXsd1jgtYzlPwRBYjnWFyHwf/R9/0r2uetbW6nNDq7Y/HUkwxDACXYY3gieiAcT4/MEfRI0v54jiUIl3fXOGdbiWLQFX7XG1dMuvDJfwepZ6zK3rDwgB2N1p0uJRQCRwN7CFgStt14YAqGEDwiK4jnxP4A8JSgBkzAYzBwSXiu+7ugdDTlvuOB3BLrE/BxJQMfC8ovDMQjtrskKPmV5NYdIJ5jrQDwn8ENlK4PtXZdMnHWukLoePOpjwc2Lpuyc76r9UqAzwtKIxOfFolnfhaU/HJy6woQL2UtC5BIocMguqQtnr6lng6sVtfLy6bs0NI1fj8yaD8/708OGcb2YN5BpH+w+CdhByKjkEfP7L8FxptE9CaAN0H0Zt733zJChsgneaantfuZ3WetE39ruKczFTvTZ74SQHswxtGnTDt9bzCyt5ZaN4B4qei3wLQgoIHdmydc0ijky96iKbuR0XoEiA8F0X7M/mSANB+N8otExnowP8MEN/82PTjpwnQ2IP/WJFaQf4cYAiTH1dSxysZs0H9E5qYfqbK5UrO6AMRLxs4B8TIlSyt09pm/3Z7IBAW8qkwWEbLe2y1HkmEcDqbDg/y+GsogBtIhwn2+768Ov7r9KlrwcE9VAwioUXA/ivwi8vi0OT/zx4BM7xcbOEAKUbmgBwIZCPF5I3Xp99q11sTuFj4GMI4B+IjgthTSnnsZoIcAf+X4Hlq58/kjkxOeS1kXc+9qovt5Evnuo835617RLXigvEABUsjn4ND9QYSsE+PEcML9ZZDOKZVdAgpxgTWxnvoVdG0EaOVIgaXTseb5wCIF+yt1vd203c8FILc+K4jnxH4YRLKTz5jWnnDTQTpmoOwBFKYnjyJQVHLPRhDurjcJtedYnwXwU91zxszfiCQyl+uW2ycvsBUkqLB1H7Rvu51+LiiHVADGtskGX2du3dzS2Mc4z4/qnjsifCYcd3+lW66QFwhACgQL8MV3h9Yccg61TqgHreWYI72uI1BevNaauF0L9KbYEv7Bvn90JLH277pBoh0gBWqeV971gG72kbd70LZXwB+am6+PRd7u5gtB+KpuR48KeYxrthtPCyecF+xxcZFDWdSB1/ncY9qu9ohw7QDxHEt8jGnlraIQHRqek35MpzdLZXmONasADMa+QeppeNmEZ8G4xrTdQI7l+8bf4cT2McDP6vQHMa4KJ9xLtMrUKSygXPLPmbZ7u047B8rKJa1PcO+KMVYLd1Zy7X3EuCacCC6erSNpxQzCGp1zS0Sn6yTO1raCCK7c7jcLg9VGB2oA89tsd7FOBw76CE9ZC8FjdDtVrVMJ15hxV/AfB/LkktZnmPALjcLXj98B03RxAWsDSC5pXcmEi3UNlAiXhOPuVbrkDZSz8bqpB/g9xkIeKaqbIAYVoEwSFEgt/oUTZ699Mgg1IlMRTNfrkq1zq6UFIKIEAfskVg8tLOtBho90JGOnG8QLG/DmW9f7EZScDp/pwvaACKg1h6XkyeBpOkov6AFIKnq3xvoc95q2Gwg5gNfcUqmDJ8Atl+dYv9YV4Cjqk4TjGXGxq/QoA6RY2UkXrX1HnnBkEFG5Oce6k4FTlLzV7Fz0AK8w7YygLdX6FKOAxf2ZllB5HR/sSgApnmeLrZUWgjGD6Kwg8jkaEBx5Av7qAy8bxJvBxmYf/AYIvXcDjAkGaCeQP8FnmmAAuzOwv64trKa3eo1pu9M1yeoXU8wn0cWw+CSHWqepXC4rAcRzrG8D+G89TqLrTTs9W4+sd6R4TmwVBKv5yD2vM/NqInoY4PXMvF72xjebnPp+IhK5JaIeSIyBmQB2Gbmh4RnTdrWdWvaNw3Ni12nMTPyOabvfkvWRNEAKpZZRONZVriYrcsiN1u4jdafJeo71LwDvlnWObD8CfptnzoRCtCrovPiOVHRayKfDuLduygjc5VDOtNOmrK/K9RPpu37X+Ac05bi/xcA02RLV0gDRmT5LBo7Q/SLViVJo4PyuJOKV4wxaufMcV+sNcbUv32tLrX23+HwMM4lQ/LrySZm2K/0ulRufIIJgH3oY3wk3mHF3VrV+HNhOalCFYD4y/iCjsLSPoOaJ2Omv65A1YIn2AA7rlFlOFgPPG6Cbx4X4jpECRaUxFsCSp9N88FkEvC9oXwB4wbRdrazwWSd2uTZKIfY/aibWrqvVD3IA0Ue+8Md8178P08lbVSemxicN4BZq7b5F97aw1gkcrr3YrnDX+DN9QJQ10HKYUlknrTbttDaia8G7FWrdUeSeq9eIkVxFagaIztXD9/2TdTIeeo61GsC04V4ahb9vIOIr2tr3uo5OvSuvIKfuXfnOU0KdHS/OZiZB8jYpKAMIuCtsu6fqkl9kcLxbizyJVaR2gGhbPeiHpp3WlogUNAE2M9/S2kJXNNpWqtYXR2y9unr4UqJCoZxAHt0g8ZzYDVq4gCVWkZoAomv1ECzroRb/MF1E0sHekNOfmPjySNy9K5C3aYSEZlPWKcT0dYAPDMQEjTfugjA732M8ooVVvsZVpDaA6Fo9NLKRFGOrAinXxuBb/S7jokbhm9L9Im9YGIsYrf7VBBJFcrQ/PtMZumK3tAU01riKVA0QXasHCL82464W6lERlZvvMe7XFZrQ94Yw8BaYL4okMintb00DCswmo3EQXU0a7rRKhtcRavGP0hUF7KWse7QU8alhFakaIFnHShEwV3V+DaKTdVV2yjnWb3WHrBPh6Tx4Vns8ozWRR9VvQfcvXDiCljHjQzp1iVD5sO1+UofMQqUrZuUP9lrY4qsCyIvXWttv1wJR6Eb1VnqVabvixlf5CeK7Q4CD2D+1zV77F2UDR6GATmfqB5mMO3WDBBq/RzzHEse+SkfJBLzkh1r3ryZGqyqAZFPR/yKmO1TnXFep32KarJ5b1uKgxjo4+uY2KJAQ4wgd6btZxxIXnzepvovVRvpWBRAvaf0EBCUGO1Fq2dzoHqyjmqznWCt1xh01wTH4dQsIJPeZGsqpFavvPq5aorrafJFhAZK71tqDe7dXahxXxOeb8Ywy/WSBfQTQVoSzCY7yv8UBgeRcHWwpXio6H0zXKq4ieWZ//+Eiq4cHiBM9j0HXKRrzrzdbQge/Z/bqV1XkFHireniNLmqeRgHHxqum7Gy8a/u2np58IX6spSWU819/q3PixSNbBEg7SAjPbtdC01R5t/513fRdd+jJPw5ANfbra6btivTris+wANGxndGVY+4lrYW6SN1GAhzZpR+bRPkWkYl3GgNhArUVgyrHVZihLQDlGNxJQA7AHRzqWR6Z89gGlR+aWvpqB4ng3Eqos6R0JKMLDCLpPA/hA1EuImK7Q4YmDQmQYgqkMpNFS4gO3nVO+olaJqa0rbZ7mF7HvMXER9bjKHdD6tD3hTgkqrSKux9dBWVEhaV78pS/f1L80edV/FpNX3EETEwPaLsnqeEeopJ9ry6NHdSTZ7GKKD0hGIdNtNesqiRkSIBkk9GvE9FlKhYwcH9Ewzm4zvwTZraDvgQs1kWxg63HWJiZewjshO2M1lO90jkXl4lE5Ki8C/19a7zNrqQz61i/JWXqJrrWtNMXyAHEsdIEWCpOIaI54Xha6RtG7+rBt0bsTGCBet6S6CHkG3EGBxK+UXki6VY2/JQ5N7iqS1kneou2sBQNq0guFZvNzEvV3k88HY67H64ZIMX9smr1ns3Ug/3D57uiJrr0o2/1oD/lu3BUELFVryyKvrclZMQJHGeg0jeFtA+q6UjAFgalevJ+arf5mX9W06eWNiJ2K9SK+7UEOGpYRYonrH8FMKGWcZS2NeB/qNLlcMUtlpeKHg+m5SqKiei2cDx9uooMrasH4dQgonI9J/YVgL+rOyZMwW8dAH3TtNM/VJBRtmtvFDDu1CJXzyryY2b+goo9TPhyJO6WvXysCJBcKnYZMyulwhqgU9rstFLsjK7VQ+RzRBKZs1QcWa6v50SvAajiHla3vtrk8fdNO6O9lEM2Gb1ZSz6JhlWk04md7IPVUhEYN5kJ98vlfFt5BXGsh9BLKyP3MP4R3q57Ms1at0VOANBbExCiKIpqLcAN40OYoTvZyXOsFXX4CJd1X18/7XUzRNJVdx7i5Ec1M3Hj+B68X6XAqKgwnHt7/HoQ9pZ1lDjyr/QdUhYgegqc8A9NO6OUMeg50c8DdJvswPv6EbEdjusNXfec6DMAfUDVtvr057+ZdmY/nbpyqWicWcepFn/BtDM/UbHNS1o3gnC2iozuLT3v3uOCx14slVEWILlU7OPM/DsVhT7wxXbbVUpk8pzYbQB/XsUOAE+GJ+15iM4c8hGgFFJ0QW93ndQ8Isc9t+ElUadckQiCfmLaaaVviGzKOpsYNyo6qWwdmrIA0XFL6edD+7TPX/0PWaN1ba901xhpAKZGWZeKkpRaWUc0lXdW3mZ5S6Z/AH7+GQXHgJivCycyc6paQTzV7w/CE2bcPVjFYB3bK8Fb1dLafbAuap5s0rqSNNZAUfGPbF9mXBXRVKZMUAr1dI0XkbWKvFvq26xcyvqzSh4LA09FbHer1bDsCqJhC7HYtN35spMo+unYXhHom2E7rRQJ0DeGzpSV8BmBVbtS8VWtfQ3CvLa4m6y1X7n2OSf2De494lZ41LdZOr5DtrR277j7rHVvDhzIVgDJpazJzBCXL9KPT/hMu0Ldal4wsyU3sUvw6u4ubQSA8SG8X8fJVZDEECrjU+mri1CheKKlWn65I9zavZfKiafnWJ8F8FMVnzBwSCmH71YA6UhanzEUa8Z1GfnwnnMfla6F7SWnHg4yfq8yWAArTds9VlEGem/IKRPMJSCt9tn/XchAJzM2MmGjsJcYE4kwMe+jzSDjEwGx03f05Dmq48bdc6zfqHIBM/vHRRJrhRypZ7Mzw3wbPVmpzn2dDPqsOTc9KHN2K4Bkk9YlRLhCWpGG74/OlPVNn/EdaRvES6bpaNdzYt8H+HwVW0qW7Pth0G08LvSgOWtVVaE83rIZu9GW/BHw+Qt6SSqGDtSrdsxajnw15K17TuwJpTAY4gVmPCNKevQ/WwPEsX5EwBnVOqe0HQH/E7bdL8r2F/2yjrVG1L9QkaFje9UbeEgZTbFVYhvyfdWMumJGpbi5f7+KfworlYjdMjiqGuCoZ5tF60w7/VGVMeUc60es8O6C+Q4zkRFbtcoAyTnWWgYOlTWUwV+P2BnpFag3II47ZPUX+2nJf845sVt0ROUKKs4taJmzm73KUxxXofsrzgxzHHqW6igpR6Bbw3ZaObo551j3MaBG75Pv3t2cv66qVbWcH7NO9FICXa7g48dN2x1ElL3VCuIlrddB8tGRPvikdjsjXffaS1mfA0PpZpUYl4YT7pUKjkIxn0PUy1N6dPPUDjRGV2k5Ah+pmk+i4+6MmM4IK1TR7XCiJxqgnytM2Jum7e5YcQUphg+/pKAAQ4UOVyM351gpViSo84mnq2YL6oiz0pVqPJTfdLyYIunKtN3jq5mfSm10FLyphdCtnB0iPdiH8bTKOIzQuHe3zXmkP+Rk0Aqig28qvNENqVD75FKx+5n5SIVBbjJtd1eF/tBxM1sPcPSNUQtIjNB+5tzVf1Pym2O9BuBdsjKI6IFwPC3Sk6UeQQmUm2gplaXwff+o9nlr+3cOpQCZw4QlUtYVOtHfTDutFBTnOdEXANpL1gYCfhW23c/I9hf9vKT1VRCGZLsYWj7daNrpr6jYUGtfz4n9EOCyIdtVyWJcaCbca6pqW6FRNhm9l4gUjtb5RdPOKLF3ek7sGYClg0gZiEdstx8DgwCSTVlXEOMSaScRrTDjaen62S8vm7LDuK7x/5bW39txWCqX4eQrhtr8vQct03R9kA9na9/fxYd7C3oEn7Ds6dbDpu0eXq2+cu08x7oQwPdUZJS7za5FnpeKLQez9HaRCFeG464oMtT7kz9QuSpBNRF9LxxPX1TLgEr0i8q5IkJU4eHjTTtzj6yATmfqnj6MrcKea5CnhRytBn39TVVJ9Qz4e7XZa6W/QT0n+p8AiRwZ6afcbXYtwnKp2NXM/LVa+gxsW/odNHiLpXqsqbhMe0tip8Hn22UHJ/ox+x8Yji1vKPlZJ2YTWCpOSSeTuawPVBjvGZSI2Glp5pLeOu6G0ncMytxm1+IL1e0xEX4Ujrv9hBuDAZKK3s1MJ9Vi0MC2hsFfaZubkY7L1/CxmTdtt0XWftFPZXtlEJ3VFk/foqJftW9nKnamz3yzpBwd26weACFJ/VA93OhcEv2y75N0Lj4Bvwjbbj8GSrdYSjxDrEiKoGF5/HPYdj8iOzmiX9axniKgIg3MULJLjwhV7JDt27n0sL38/JYXZPoz8OeIov9yjvUUS/pP2Ky8TVcklSg9SSsFiBIPlg/6ZLudFhWfpB7VOxAdJHWeE3sZ4N1qHQABj4VtVzoCoVZ9Q7XPOdajDHysdpn0immnlSKoVcncVO9COpzYUQb4t7WPvbcHgTJhO93PBVfyDWI9yYD0LzABU8O2+6iscTnHuokBaeaRainth7LPc6xuSPBaqW4NZH1Wrp/CVnWLabvjVWzJKW7TCbg5bLvS+eU5xzqUgbWyYyglcCgFyPMMvFdWOHxjsjlvjXTqo5eM3g6i02T1q8YVCZb1/A7jN8no1xU9LKO7tI9KdG3oze5dVFjllePXygQM1uITb/G0/WD4olyH5DP4LqZ0i5UjoE1SMnzfn9Q+b610oKFqrXMGlkRsNy5r/6brpu+9pSf/nFx/9bRROb1b91JJVx7XEtpnl9nyXAKqVwUArzDtjPRdWsfiqe2GYaiw3w+KxBgEEC9pdYEgvcS+sbF1+/ctePht2YlWDTNhwpWRAZc8tdqhsjwz8bGReEZUvhrxJ5uKHkNMUslHqttk1ctm1XCT5xfM3G6niV1vKUzCoJPQwQBxLCF4O1nhWzZ277j7gsE5vbXIUq1FwqArInZamg2yI2nFDIJUddttBSA+Y1p7wk3XMm8D22ad2OUE7r+JlpCjlKrw8oIpO4ybqBSN8YZpu/1cv6UAEbH40mx5lci3qnWSl4reDpb/BgGgRBZRrLv+p2rtHdxu29hihVr8A1XqmnuOJcrszZPzYSHf+A4zPjhpqRZZ//f9j+01flyL1DF3Uc9Lpu32xwKWAkSQNUyuxaCBbTlPB0fmyxfKyaWs65lxrqx+QI3NscOJ7WOAn5XRv618pPugfdvttOR3mLhojd4AkHSgJhF+EI6758nMgeiTXRQ7iELyhXUIGHSXVhpq4jI4KmucaqnfXMq6khkXy+oH46dmwpVmYlT5wNtGjnnVD1oUKyIT4apw3JUOmFVP2RhMrle6giiVV2bm/4okMtLU+NnF1sVkQDoTUDXUXY2TWC9rofSPRC+n2CpZJhQOtU6IzHn4DVn9Ocf6JQOflu3PPi6JzHOvku2fTUZPJaKfyfYH4ddm3BXl8grPYIAofgMQaHbYTl8va1xHMnquQSTdn5keiCTkE26E3Uqkea0tu1fLVCLro+H6CQYUdPW8PFy7Sn9X5e/NJmP3E8knvPnM57UnMtJlvnNO7DwGS1c0K61pUxKsqPYNoMpk6CVjp4GUonmfNW1XNh+i8M6opNqSQWeE56Z/LPty6uiXWxI7nX2WJQ1XTr31HEuwt+wrPRamz5qJwdxUtchSZXocOtxd9RtA8RQpm7SOJoLSXUKrkQ+/S4m0LnYOiJfVMikDluP7wxoKlsro7uujEu4OpllmIn2DrP7Xlxza1uWHRLlq6YcZx0QS7n2yAlRP0Qh8WdjOfLPsFkv1GwDMt5mJjHTJNfUwgcKu8VOmnRZlkqWe3KLo/hyiv0h17u00ahOmKM8fDM/PSNPOek7sOIB/reA7aAhX+jGIpMsp+IwL2wekHg/aYil/AwArI4p0n55jiZTbHaSdTPiuGXf/W7p/70fuHwCeIiljlKbcqhO3eSnrO2D0//pK+G8r2p1aZWQd6zcEHFNrv/72zF8xE+/kNJWEmih/A6w3bXd/aeMUT2CEXk0f6kqXXUFyYVXyrQaOLKVLVmGX6ge6jvolnmOp3eWBTokMqKs5OFhRwzfAJnjbvd9+tksWJLlUzGFm6YBDAK+btruzrH7Rr3NJzPJ9lg63KGz0gLvCtnuqih3V9tUADhgGxdrmpt1qdZZr56nT/qTC8bQta8PfnX1bd4EpHQso9PrgI9vtzINlv0FecaZ+sEWReKsHmLqbQk5INhk7k0g6ZbR3XAZPUeWbzSajdxPJpx8XnM387fZEZoHshFfTTyH3Y8Cugn8eSWROrkZfpTYbFkenhowCC770w0xnRRLyKcuvONahLQq5IIUftnHYM3ye+39lASL+o+dYSjnFAJ9j2hnpnOBXnekH9iD/hLSXe3++LzTjahxPOurEBw0SHeAo+Jn4BDOeUWIj8VLRb4FJ6cegBaGDdrVXS8bCFcJcvgKQ9CkcCFkz7rYPfPe25uZ1ok8AdKDsC8rA0ojtzpXtXwSpYMczFGQoRYT26c0mrf8lwmEKdhS70o09CF2qiyurlwMrf4USUVzRMmY8Ekm4/6E6RhWyi6Ju37RdabIHISPrWEsI2KrOYLVjI8Jvw3H36GEAolZZlplXRxKZGdUaVa6d4imS2P93h/Kt7bvOf1gqO7AfIHqqp/aJa7jyB8IwJnw5EndvUpmvYsFVkaQ0Tl6O+ilaNhldRUTTZW1g8NUROzMoFnDrEmxO7CIGS8fCgLHZTLjS/KyFFSRpfQekdFwI1TJwfU5WjS0qnSzBnUVEd8BoeXAgSfJQkyqYSuD3HMHMp+ksoKMau9Znc2cqdpLPfLfsi1kEqlKyW/G9UapMwMSnReKZQXFcZbZY6pc9fr5nn/b5j0mXgBaFa+DTOhWHA1hm2q5C6Hyv9mL49EMAdlG0Z6vuggklz/yboUqwhYiOlWMoGdbaTZynw1XSE/o0eI4lYqdmDatxiAaqp2gdiz62txFqkQ7TL5hm5MmxlJIAABHASURBVPcz5z46iPhuK4BsXDzt3XnDFwU0pR+fcWJ7wv2ltIDeVeQ5EPZWkLElxHTIxET6zwoyCl09xxIJQOJuZFt65pu2q1y1d2My9uE8saCLVdheqZNWa6it+W/Tdncqs+JvPeeq59k6jje9lLUQjK8qvZGMRWbC1VJfUPdWS2lcip11ba2K25prQVAq+Q2os+Grn+iVT1coWyddtUYgAY+GbXeqyjzqCFwE4bVxodAhKiwdfWMo/lKKo9D3qYyrAfo+H2I6XsfKWmCByef/CIbSxazv08nt89IqlaHgpax1YAwqn1aLr4mo7CVlWYDo2FNCsd7c0ws+ND4y8V3/BFAzy+FAxxDR5eF4+hu1OKtSW29RdCZCBbaQ7XXIGwEZbyHPx5rzMw/r0J1LxS5jZmmSjIINhGy4/fW96NSnBWGf1KOBkR8MnB2x3a04jcsCJOdYcxlISVlb7ETAF8O2K5uXUJCSVay422sKvbLFgLX73LTSd1WfL4ogER/to+/J8+G6wPHykth7xvlwZWhaBzlOMQJcyMqlYrOZeanKhFQqu1B+Ben9pVR8Cfhnpp2RZkksDDwZO4GJf6Uy8OKv1A1m3FU6ZRlow6gEiUZwCF94KWsZGOeozg0xfTqcSC9XkeMlrXtA+JSKjEqZlGUBUnCAY20EIF/rj7E5vNvrYZWls9eO2O8BVqp8JOSoVt8tdX7hFzTPT6lUBFaZ0Kr7MjZvCdFHdK2gQq+GarJ9+4yHTDv98arHUqYhO8e05rBJJGltdQJVg9zlpu2WzaOvDJCUdSMY0iTCwjhm/7hIYq0Uw1/f4HKOdQYDP6phsBWa0rotrV2H7T5LntiunGBVmhv1cQ0lQY0GqZzk3jJ5rY8o5Mv0i9WyDVcsdyCMIdCZYTt9a7nxDrGCqF8YAnStaacvUH0JPMcSl4bSJxR9+lWZFyuNw0vGzgfx91XHqbU/0wVmIn2tVpmF70Jl5sQ+k/5o2q5sUlr/sHKp6FJmmq0yznEtoYm7zF79ak0AEUtXJza9zpDn6gXwpGm70oGPfQZnk9E4EUmXBhs48NJ4fxXHDuxbrM8nJmpQsJsu+TXIuQ/g61TqNFbS1eFEjzBA/SWSa7Bpq6bMbEcSGaWDICHUS1l/AUMlSe/3pu1+otJYKq4gBeVO7A6A/0vFEWRwNDw3I12voWDH1dMmYHv/cQD7qNhS7PuHN/JvHfm++U8oBTIOsZoIZhYBFKWATYlxrgLTdSqMIEPpfH7RQbvsFNpegOOjEraVdnkObxkHmxet2awia4MzNRqCoZTkBeLzzXimYpTEkADpcKwzDMX9v2pJgv5VxIl9g8DfVXFof1/GD8yEPL1lNTZ0OtZZfm98UtBVpx41gGVtZc7wq7Gz2jZe0roepEIL+44mBn0zYqcvq1Z3pXbqpRYAPx/ap31+5XIPQ68gqWm7g/3+7CqpATE2++NCB7Ur1JwQeot1wEUarDzn0oAB6AjzrsYfhSNhg45n4ARSiy17B9+MfxCwHD6v0HWvMdRYsnrD/p/tQUtMNTem47rpextb8k8oniKuMm13yHyfIQHSu82yBI3LcdW8DEO0+Y5pu99SlCGOF88zQNKseSX6PR/5I9vtR6Uz2GodzyuLou81DHzJIJLyhYhx833cutv8jIgwqMvT4Rx6oIGQ2FqZOhT64NntdkaaPbPPBs+xvg1Aib2GwF8P25krhhrX8ABJReeDSfU05F8t+daDVBOYxEByjvUgAxU/qmqZRFGPzjfw6cgcV4rRvRZdAyZWxHP1c7/WKEOZ+bAWfW8sntr+dshYzgyluLo+nQT8Lmy7R9RiQ7m2ry6auUtPqEukZb9HSRb7HzUTa4dMqxgWIB2Lpx5gGIbyrywT5kXiblJpQAVqmWnHEvnSxHCl+gVI2jpbD6IFD4tc/MAfz7HEllW2kuzLpu3uEbiRAO485ZTQ4TNeEuweM3XpYzaOiyTWKN2LCVuyKStBDKVQfQZnInamv5ptpTEOCxDR0UtaGZDyr8jjpu0q32UUVpFk9GYmOlPXxJVWNtUlt5wcJXJssc+x3armTHUMGnLMB5lAzLeEExnpCsYDhXmOJfJPDlYZY7UpGVU5O5e05jBhiYpBoi8xnRFOqJM7b0jGPhwCpxU/0AZPYEnhFNWxVuo/GgCiGxwiDTsPik3SkLyWS8ZOZ5Im5+6dFsLb433/gJ0TawVPwJBPVQDZ8D9H7RjatFncQygxp+vag4oRqSfIbO0XBv4csV3pOvHDObuwGjsWV9OuUpugV5CsY/2dNJ0U9o2h2l/ravyi4xu0llrsVQGksK1xopcy6PJqBjFUmzzTpycpRm/2yfcc66cAPqtqU2l/P0+x9vlqLIOjbQVRqYw7jP9vN233czrmaEMydkJIQ3Q3GTgiPNf9XTU2VQ2QF6+19mhtweOkeNxHQLot7k4nEjkqak+RH2qljsC5rSwZ5oZV1vJGXEFyTvS7DNKSVDbYL7SuB6FjVO88hExmUGfKWs1ATNb3vbsrPBi23SOrlVE1QIrbg+8BuLBa4ZXaEeGScFy+zNZAuR2p6DTDp5U6v0f65AfBr9toANHB61t2nsUFscHHtMczUmW1S2XmUtbFzPLl+frkGURntcWrpzetCSAF7l42HgcpBTAKWzcZhjG9be6ap1XBJvpnU7EziRX5fEsMIeAvZBin6rJxwLZQaeXU/Q2SdSxxGnSn7u8OJjorUsOLONR70Llk2od831+tTL1E9Lcw73wA2SurJlevCSCFVSRpLQOpZ5IB+LFpu2foAIiQkUtaVzIpVMgtMaQciZgOWxttBRFj6kzFzvQ1/sAQ46pwQr5SbamfPccSqdvShZn65MkcFtQMkI4lMctQLA3QZzD7ODUyz71Lx4tXAIlj3cWAEkt5wRbGd82EWhGeSmNqRIAUVuFkzCFSKjtRGLLubWl2sXUKGZCunNw/DzUc7Q6cu5oBUlhFNITBF434Qxi7TK9lyRsOTDnHeoAB6XAGAn7RNmnPU+nUuwSBtvanUQHCC2a25CZ2/V4lTF83OIrptGJrpRxiX8vRrjJAtHBWvWPFt0zb/Y7ON9FzrMcknfpqqMWfOXH22id12jNQVqMCpLCKLIkdRj6LMB6Z/O6Ked2yvvQcSwQjiqBE5aeWo11lgOhcRRh4E8D0iO2Ki0htj1Q5YsbnzYQr7lYCexoZIGLQMqdFRFgbjrtRnU4rHh6sJpV6lX0GMX5qJtzPy9gntcUqACQ17aPw/TUaTrQKZ9Ntk/Y8Wve2xnNiHsDhahxTjvq+mn61tml0gBRA4lh3MnBKlWN7wbRdtajaEkXP3zJzuwmbu3/DGthsALyWJ8yYFHefqnI8g5pJA6SwJOtL4AcRkuG4K0iitT6eY20B0DK0UFoXnvRaTJWiqBrDRwNA+JaZ23mbu56q4ui327Td1mrGXUsbjSel4r1SunNTAoiIy8+HutYw8MFaHFC5LZ1j2mnp8m2V5A5XWKUFOGRXzVu8SraMBoD0brViH2fmocIxnjNtV0t250BfeU7sAoCv0fQ+rTbttBI3gBJAxCA0n6G/YTA+2ZZwlSrMlnOul4yeD6KtqXmYZpmJtHxduxpncrQApHerFfsGl+EBYMIvI3H3xBqHPmzzIjOMUq3EwUroU6adVsodUgZI4XvEsVSy5AaPibE2tF33JyfOWvfasB6tsUGxMKc4Uy9sC3QfS1ZjzmgCSHFuRYLTMcWx9fjMlwdRude7btp+6PFX6mPPp+tNO63El1V8R6qZ1qHbZBfHDiOD/1ddUq8EBm6O2K4Sq2PFLc6i6CEIFVaSA8M92IPOd9/SZXc1ckYbQLJLZ+5k+F0vMeM5YlwWViyMVM5HCxbAmDvR+g0Dn6zGh1W0eSHkGzMmzlvzQhVth2yiZQURGrKOtYgAfR/ZzBeYiYxqLnzZwW92Zphd2HJgeEDBeFVHVtt/tAFEjKvTiR3VAzzXbqfVSpxVcFIuFXOY1W/x+8QTeHZYAzGEthVECBLh8Nu3QIQjv7fal2W4dkw4NRLXF4oynL56/H00AiRIv+SS1iVMGJJZpEb995q2q8T0PlCfthVECNWVmjvQwG0NJE2AvDO7OurQlIKHfX9GZN5aEZ6i5dEKkAJIUtGbmfURKgiZBtHJbXG1El1avKVBSBMgvU7MLYl+kX0qy6gu7eYAkty0A2Tjsik793SNu49AWkMPoLkAjPQkKHZsAqSQvn0ig5RqEm49DeqFQMtNrXaA9H7UTYv68O8D1Io7lhocYvqIjuKTiu+4UvexDpDOJdOO9H3/fiUnlnQWady8o3G0ebYaGXbdACIUZZOxM4n0ZvkVBqBYHFTnxMjIGssACah03SYy+GjVCgKV5jKQFaRPmedYWnLYS40Pt3aPp1nrRIzVqHvGKkACAof4Pq0px7zWFyZQgAhjtN6yDxydb0w25615ptYBj3T7sQiQrBO9iEBXafc94Roz7iqTiAxlV+AAyV5r7UvjsBKsp2zB4MHwF0w78xPtjg9Q4FgDiJeK3gumY7W7lOg3ZjytWnVgWLMCB0hhFVlifQo+7hnWGrkGi03bnS/Xtf69xgpAXl82JdzVNT6jqSrYoIki4J/cYhxtzg5+B1EXgBS3WmIpFN8kATy0uiU//j91lFcIwLhBIscCQDoWTz3JMIy7g/IlGTgxPNf9ZVDyB8qtG0CE0qwTvYpAFwUyMMLbedBxk+JpQTzQsM+2DpCszlJ5ZWax3pEVdQWIGK/uwLStfUhfNe10Y5VkHmDktgyQrGP9iABtXGelc1tvcAj9dQdIASSOdRMDWmpFlDixG+CTgyiBrGtJ2pYBkltiXcK+1sDDfrePBDhGDCC93yTqJaZLX9pqas7petFl5WzLACn++NVC+FCVG0cKHCMKkAJIUtY9YGgKTeafmXbmtKo8PoKNtnWAiJJ9ZBg/r4LwoapZGElwjDhAeleS6O8BOrwqb1Vu9IJhGMfqJppWtKls920dIGLQungKfN8/vX3e2tuCmIdqZY7IN0ipcRpKfn3OtN3bqx30SLYbCwAR/lXl+mUgHrFd5bJ/qnPdEADpXUmsh2QqqhLhynDcvVTVEfXqP1YAUpzTR2S4fhn0zYidvqxeczKUnoYBSHG7dQ1AF1TrGEF52T2+++O7z1on6EtHxTOmANJLkCHIPKrm+vWZzmjXUOhV18vQUAARg+pwoicaVSbTkMHRoMKcdTm4zHayoQroBDXOPrlZx5pLQKoKPa+FWvzDgiQOr8KGrZo0HEAKK0ly6hTAEGWexw8xqHNN210mM+iR7DOWVpA+Pw9bbJWw1tRMfq1rjhsSIGJwueutPXgL/RjlCYy1U+3rcuhwcsYiQAof7RXKSzPhR5G4+6Xh/DZSf29YgAiHPLRgZsuHJ3Y7AJ83wEHd+XG016Tz0tmRcpqK3rEKkJxjHcrA2gG+20KE/9ZVzFVlTobq29AAGbBE9xdSIaZPhzXVWQ/KqUPJHasAKWydHUsQCwqCwafQC45fjcQc1KJzVACk6NxZAA42bffcWgbYaG3HMkAKW2fHurMIjvWNNjfl7Bk1ABkNzqzGxrEOkGp81EhtmgCp82w0AVJnhyuqawJE0YG1dm8CpFaPjWz7JkDq7P8mQOrscEV1TYAoOrDW7k2A1OqxkW3fBEid/d8ESJ0drqiuCRBFB9bavQmQWj02su2bAKmz/5sAqbPDFdU1AaLowFq7NwFSq8dGtn0TIHX2fxMgdXa4oromQBQdWGv3rGNlCTBr7SfaM+BFbDci07fZR84DTYDI+U26l2xqcVHhw6btqhJcSNs+Fjs2AVLnWfdS1jIwzpFSS7jBjLsiaLP51MkDTYDUydF9arKOdRYBN8moZeDsiO3eLNO32UfOA02AyPlNqZfnRJcDdHxtQniFaWdOqK1Ps7WqB5oAUfWgZP9aT7NM223OlaSvVbo1na7iPcW+HcnoAoPoW0OJ8Zm/3Z7ILFBU1ewu6YEmQCQdp6ub50Q/T6CPM7AfQPv1yuVnCHiGwb8fbSXmdPmlUeQ0AdIoM9G0oyE90ARIQ05L06hG8UATII0yE007GtID/w/Jiqv1e0ZE2QAAAABJRU5ErkJggg== // @author Wizos // @namespace https://blog.wizos.me // @supportURL [email protected] // @match http://*/* // @match https://*/* // @require https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js // @resource RulerJs https://fastly.jsdelivr.net/gh/wizos/rssplus/Ruler.js // @grant GM_xmlhttpRequest // @grant GM_getResourceText // @grant GM_setClipboard // @grant GM_notification // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant unsafeWindow // @connect rssplus.vercel.app // @connect rssplus.pages.dev // @connect rssfind.val.run // @connect rssplus.deno.dev // @connect rssplus.mdict.workers.dev // @noframes // @run-at document-idle // ==/UserScript== // 2025.10.09_1.2.1 增加适应浅色、深色主题,去掉整体阴影效果。 // 2025.10.06_1.2.0 1.将iframe框架改为shadow DOM,防止主网页的CSS干扰本控件。2.计数按钮支持长按后拖动位置,释放后自动贴边。3.在RSS列表关闭按钮的左侧增加设置按钮,点击后弹出对话框。4.整体保持响应式设计。5.根据按钮位置调整展开方向,始终保证容器在可视区域内。 // 2025.09.23_1.1.5 1.调整匹配 RSS 网址的正则,使其兼容性强、更容易理解;2.所有发现的 RSS 源都进行有效性检测,并处理重定向。 // 2025.06.29_1.1.4 1.由于流量过大,增加 rssplus.pages.dev, rssfind.val.run, rssplus.deno.dev, rssplus.mdict.workers.dev 域名。2.修复无法使用自定义 RSSHub 域名。3.增加 Folo 订阅。 // 2025.04.05_1.1.3 1.修复火狐136版本无法查看 RSS 问题。2. 支持 NewsBlur 和 The Old Reader。3. 全屏时隐藏图标。4. 保持订阅源 URL 大小写。 // 2025.01.16_1.1.2 1.使用 TTP 给元素增加 innerHTML。2、增加识别 feed.json。 // 2024.10.20_1.1.1 1.修复未开启远程规则的问题。2.增加更多的网站适配。 // 2024.10.08_1.0.9 1.增加支持葡萄牙语和法语(感谢Filipe Mota (BlackSpirits)提供的翻译)。2.支持 RSSHub 访问密钥。3.重构代码。 // 2022.10.18_1.0.8 1.修复 TinyTinyRSS 订阅地址错误问题。2.修复 Bug。 // 2022.07.04_1.0.7 支持用Miniflux订阅(感谢Sevichecc提供的代码https://gist.github.com/Sevichecc/f5608c4ad52e71d98f6fcf74110369df) // 2022.07.04_1.0.6 修复火狐上因为GSAP库导致无法使用问题 // 2022.05.20_1.0.5 1.解决 jsdelivr 在中国被墙的问题。2.修复bilibili video页面获取feed标题异常问题。 // 2022.04.18_1.0.4 修复导致网页加载卡顿的问题。 // 2022.04.05_1.0.2 1.受unsafe-eval影响,无法本地执行规则时使用远程规则,改用 rssplus.vercel.app 接口。2.调整 UI,每次获取到新 RSS 都同步到 UI 中。3.精简设置项。4.监听 URL 变化,同步获取新的 RSS。 // 2022.04.02_1.0.0 1.将远程规则放到 GitHub。2.修复订阅 RSS 网址的转义问题。 // 2021.12.17_0.9.2 1.支持设置 FreshRSS 一键订阅。2.支持设置带端口的网址。 // 2021.02.24_0.9.1 1.支持开启/关闭二维码。 // 2021.02.19_0.9.0 1.支持鼠标悬停在订阅链接上时展示其二维码,方便扫码订阅。 // 2021.02.05_0.8.1 1.url参数用base64编码,防止服务端获取url参数时,漏掉query部分的数据。 // 2021.02.03_0.8.0 1.支持小屏幕展示。2.支持设置 TinyTinyRSS 服务的域名。 // 2021.01.05_0.7.3 1.改用 GM_xmlhttpRequest。2.改用 rssfinder.vercel.app 接口。 // 2020.12.16_0.7.2 1.修复 bug。 // 2020.12.06_0.7.1 1.调整搞定。2.优化代码。 // 2020.11.16_0.7.0 1.支持设置 InoReader 服务的域名。2.在打印页面时隐藏。3.修复影响页面样式的问题。 // 2020.09.11_0.6.4 1.支持 RSSHub 服务器为 IP 地址。2.被识别的 RSS 地址不再转换为小写(因为 news.google.com 的小写地址打不开) // 2020.04.28_0.6.3 修复改了脚本name导致无法更新的bug。 // 2020.04.27_0.6.2 修复rsshub domain默认为undefined的bug。 // 2020.04.26_0.6.1 支持设置 RSSHub 服务的域名。 // 2020.03.01_0.6 1.可设置点击"订阅"时打开的rss服务商(feedly,inoreadly)。2.修复火狐浏览器下无法展示的问题。 // 2019.09.29_0.5 增加hexo站点的rss嗅探规则。 // 2019.04.26_0.4.2 1.修复默认圆圈状态下宽度太宽,导致遮挡下层页面事件触发的问题。2.将icon由字体改为svg形式,修复部分站点无法显示icon的问题。3.优化RSS没有title时的默认名称。 // 2018.11.10_0.4.1 关闭发现RSS后的h5通知 // 2018.10.29_0.4 1.在无法链接服务器时也能展示本地的RSS;2.针对开启 Content-Security-Policy 的网站直接展示本地的RSS;3.发现RSS后,进行h5通知 // 2018.10.23_0.4 1.增加识别为 wordpress 站点时,尝试使用/feed后缀;2.增加多语言支持 // 2018.10.16_0.3 1.改为iframe方式显示,兼容性更好;2.改为post方式传递页面地址; // 2018.10.14_0.2 第一版 RSS+ 成型; // 2018.09.16_0.1 在 RSS+Atom Feed Subscribe Button Generator 脚本基础上增加连接后端获取feed的方式; (function() { 'use strict'; // 过滤掉明确不包含 RSS 源的URL if (location.href.match(/(api\.wizos\.me)|(feedly\.com\/i\/subscription)|(inoreader\.com\/feed\/http)/i)) { return; } // 常量定义 const CONSTANTS = { // 国际化文本 I18N: { zh: { noTitle: "无标题", copied: "已复制", copy: "复制", copySucceeded: "复制成功", follow: "订阅", found: "发现 ", feed: "订阅源数量:", clickToView: "点击右下角的数字查看", subscriptionMode: "订阅方式", settingRSShubDomain: "设置 RSSHub 的域名", settingRSShubAccessKey: "设置 RSSHub 的访问密钥", settingInoreaderDomain: "设置 Inoreader 的域名", settingTinytinyrssDomain: "设置 Tiny Tiny RSS 的域名", settingFreshrssDomain: "设置 FreshRSS 的域名", settingMinifluxDomain: "设置 Miniflux 的域名", domainIsWrong: "服务器地址格式有误,请检查", enableQRCode: "启用二维码", enabled: "已启用", disabled: "已禁用", setting: "设置", confirm: "确认", close: "关闭" }, en: { noTitle: "Untitled", copied: "Copied", copy: "Copy", copySucceeded: "Copy succeeded", follow: "Subscribe", found: "Found ", feed: "Number of feeds: ", clickToView: "Click the number in the bottom right to view", subscriptionMode: "Subscription mode", settingRSShubDomain: "Set RSSHub domain", settingRSShubAccessKey: "Set RSSHub access key", settingInoreaderDomain: "Set Inoreader domain", settingTinytinyrssDomain: "Set Tiny Tiny RSS domain", settingFreshrssDomain: "Set FreshRSS domain", settingMinifluxDomain: "Set Miniflux domain", domainIsWrong: "Error in domain name format. Please check", enableQRCode: "Enable QR code", enabled: "Enabled", disabled: "Disabled", setting: "Setting", confirm: "Confirm", close: "Close" } }, // RSS内容类型 FEED_CONTENT_TYPES: [ 'application/rss+xml', 'application/atom+xml', 'application/rdf+xml', 'application/xml', 'application/feed+json', 'application/json', 'text/xml' ], // RSS后缀 FEED_SUFFIXES: [ '/feed', '/rss', '/rss.xml', '/atom.xml', '/feed.xml', '/rss.json', '/atom.json', '/feed.json', '/?feed=rss', '/?feed=rss2', '/blog/feed', '/blog/rss', '/latest/rss', '/news/atom', '/feed/index.xml' ], // 代理列表 PROXY_LIST: [ "https://rssplus.vercel.app/", "https://rssplus.pages.dev/", "https://rssfind.val.run/", "https://rssplus.deno.dev/", "https://rssplus.mdict.workers.dev/" ], // RSS模式 - 借鉴附件中的正则表达式 RSS_PATTERNS: [ // 子域名风格 /(?:https?:\/\/)?(?:feeds?|rss2?|atom2?)\.[a-z0-9-]+\.[a-z]{2,}(?::\d{2,5})?(?:\/[^\s?#]*)?(?:[\s?#][^\s]*)?/i, // 路径/文件名风格(相对/绝对均可) /(^|[\/.])(feeds?|rss2?|atom2?)(?=[\/?#]|\.(json|xml|rdf|jsp|aspx|php)|$)/i, // 有些站点写的比较特殊,会用 index.xml, index.json 作为 rss 源 /(^|[\/.])index(?=\.(json|xml|rdf)([\/?#]|$)|[\/?#])/i, // 查询参数风格 /[?&](feeds?|format)=(rss2?|atom2?|rdf)(?:&|$)/i ], // RSS内容模式 RSS_CONTENT_PATTERNS: [ /<rss\s+version=/i, /<feed\s+xmlns=/i, /<rss\s+xmlns=/i, /{"version":\s*"https:\/\/jsonfeed\.org\/version\/1/i, /<channel>/i ] }; // RSS检测器主类 class RSSPlusDetector { constructor() { this.state = { foundRSS: new Set(), validRSS: new Map(), invalidRSS: new Set(), settings: { subscriptionService: 'feedly', enableQRCode: true, rsshubBase: 'https://rsshub.app', rsshubAccessKey: '', inoreaderDomain: 'https://www.inoreader.com', tinytinyrssDomain: 'https://tt-rss.org', freshrssDomain: 'https://freshrss.org', minifluxDomain: 'https://miniflux.app' }, maxZIndex: 0, isDragging: false, isContainerDragging: false, dragStartX: 0, dragStartY: 0, hasMoved: false }; // 获取当前语言 - 修复i18n问题 const browserLang = navigator.language || navigator.userLanguage || 'en'; this.lang = browserLang.toLowerCase().replace('-', ""); this.t = CONSTANTS.I18N[this.lang] || CONSTANTS.I18N[browserLang.split('-')[0]] || CONSTANTS.I18N.en; this.init(); } init() { this.loadSettings(); this.calculateMaxZIndex(); this.createUI(); this.detectRSS(); this.setupMutationObserver(); this.setupUrlObserver(); } // 计算页面最大z-index calculateMaxZIndex() { let max = 0; document.querySelectorAll('*').forEach(el => { const z = parseInt(window.getComputedStyle(el).zIndex) || 0; if (z > max) max = z; }); this.state.maxZIndex = max; } // 创建UI createUI() { // 创建Shadow DOM this.shadowHost = document.createElement('rss-plus'); this.shadowHost.id = 'rss-plus-shadow-host'; document.body.appendChild(this.shadowHost); this.shadowRoot = this.shadowHost.attachShadow({ mode: 'open' }); // 兼容性更好 // 添加样式 const style = document.createElement('style'); style.textContent = ` * { margin: 0; padding: 0; box-sizing: border-box; } :host { all: initial; /* 1. 让影子根支持两套配色,浏览器会先换好默认 Canvas/CanvasText */ color-scheme: light dark; /* 2. 核心语义变量 —— 全部用 light-dark() 定义 */ --fg: light-dark(#1f2937, #f3f4f6); /* 主文字 */ --bg: light-dark(#ffffff, #111827); /* 主背景 */ --sub-fg: light-dark(#6b7280, #9ca3af); /* 次要文字 */ --accent: light-dark(#4f46e5, #818cf8); /* 主强调色 */ --accent-hover: light-dark(#4338ca, #a5b4fc); /* 强调色-hover */ --border: light-dark(rgba(0,0,0,.08), rgba(255,255,255,.08)); --dash-border: light-dark(#d1d9e0, #737d87); --header-bg: light-dark(rgba(249,250,251,.8), rgba(31,41,55,.8)); --item-hover: light-dark(rgba(249,250,251,.8), rgba(55,65,81,.8)); --input-bg: light-dark(#ffffff, #1f2937); --input-border: light-dark(#d1d5db, #4b5563); --input-focus-ring: light-dark(0 0 0 3px rgba(79,70,229,.1), 0 0 0 3px rgba(165,180,252,.3)); --modal-bg: light-dark(#ffffff, #1f2937); --overlay-bg: light-dark(rgba(0,0,0,.6), rgba(0,0,0,.75)); --btn-secondary-bg: light-dark(#f3f4f6, #374151); --btn-secondary-hover: light-dark(#e5e7eb, #4b5563); --scroll-thumb: light-dark(rgba(156,163,175,.3), rgba(75,85,99,.5)); --scroll-thumb-hover: light-dark(rgba(156,163,175,.5), rgba(75,85,99,.7)); } body { font-family: -system-ui; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* ---------- 通用文字色 ---------- */ body, #rss-counter, #rss-container, #rss-list, .rss-item, .rss-item-title, .rss-item-url, .setting-label, .setting-input, .setting-select, .btn-secondary { color: var(--fg); background-color: transparent; /* 背景由父级统一给 */ } /* ---------- 悬浮球 ---------- */ #rss-counter { position: fixed; bottom: 20px; right: 20px; width: 30px; height: 30px; border-radius: 50%; background: var(--bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 2px solid var(--border); color: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 12px; cursor: pointer; box-shadow: var(--shadow); z-index: ${this.state.maxZIndex + 1}; transition: all .2s ease; user-select: none; } #rss-counter:hover { transform: translateY(-2px); box-shadow: var(--shadow-hover); background: var(--bg); } #rss-counter:active { transform: translateY(0); } /* ---------- 主面板 ---------- border: 1px solid var(--border); box-shadow: var(--shadow); */ #rss-container { position: fixed; width: 380px; max-height: 70vh; background: var(--bg); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-radius: 12px; border: 2px dashed var(--dash-border); z-index: ${this.state.maxZIndex + 1}; display: none; overflow: hidden; } #rss-header { padding: 10px 18px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: var(--header-bg); border-radius: 12px 12px 0 0; } #rss-title { font-size: 15px; } #rss-title-count { font-weight: bold; color: #ef4444; /* 固定醒目红,不随主题变 */ } /* ---------- 按钮组 ---------- */ .rss-buttons { display: flex; gap: 6px; } .rss-btn { width: 32px; height: 32px; border: none; background: transparent; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: background .2s; } .rss-btn:hover { background: var(--item-hover); } /* ---------- 列表 ---------- */ #rss-list { max-height: calc(70vh - 56px); overflow-y: auto; padding: 0; background: var(--bg); } .rss-item { padding: 10px 18px; position: relative; transition: background .2s; border-bottom: 1px solid var(--border); display: flex; align-items: center; min-height: 50px; } .rss-item:last-child { border-bottom: none; } .rss-item:hover { background: var(--item-hover); } .rss-item.invalid { opacity: .6; } .rss-item.invalid .rss-item-title, .rss-item.invalid .rss-item-url { text-decoration: line-through; } .rss-item-content { flex: 1; min-width: 0; margin-right: 10px; } .rss-item-title { font-weight: 500; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .rss-item-url { font-size: 12px; white-space: nowrap; display: block; overflow: hidden; text-overflow: ellipsis; color: var(--sub-fg); } .rss-item-url:hover { color: var(--accent); text-decoration: underline; } .rss-item-actions { display: flex; gap: 6px; flex-shrink: 0; } /* ---------- QR 弹窗 ---------- */ .qr-code { position: fixed; background: var(--bg); border-radius: 8px; box-shadow: var(--shadow); z-index: ${this.state.maxZIndex + 2}; display: none; border: 1px solid var(--border); } /* ---------- 设置模态框 ---------- */ #settings-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--modal-bg); padding: 24px; border-radius: 12px; box-shadow: 0 15px 35px rgba(0,0,0,.2); z-index: ${this.state.maxZIndex + 3}; display: none; width: 420px; border: 1px solid var(--border); } .modal-overlay { position: fixed; inset: 0; background: var(--overlay-bg); z-index: ${this.state.maxZIndex + 2}; display: none; backdrop-filter: blur(4px); } /* ---------- 表单 ---------- */ .setting-group { margin-bottom: 10px; } .setting-label { display: block; margin-bottom: 8px; font-weight: 500; font-size: 14px; } .setting-input, .setting-select { width: 100%; padding: 10px 14px; border: 1px solid var(--input-border); border-radius: 8px; font-size: 14px; background: var(--input-bg); transition: border .2s, box-shadow .2s; } .setting-input:focus, .setting-select:focus { border-color: var(--accent); outline: none; box-shadow: var(--input-focus-ring); } .required-star { color: #e53e3e; margin-right: 4px; } .setting-input.error { border-color: #e53e3e; } .error-tip { color: #e53e3e; font-size: 12px; margin-top: 4px; display: none; } .checkbox-wrapper { display: flex; align-items: center; gap: 10px; } .checkbox-wrapper input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); } /* ---------- 按钮 ---------- */ .modal-buttons { display: flex; gap: 12px; justify-content: flex-end; margin-top: 10px; } .btn-primary { padding: 10px 18px; background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all .2s; box-shadow: 0 2px 4px rgba(59,130,246,.2); } .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 8px rgba(59,130,246,.3); } .btn-secondary { padding: 10px 18px; background: var(--btn-secondary-bg); color: var(--fg); border: none; border-radius: 8px; cursor: pointer; font-weight: 500; transition: all .2s; box-shadow: 0 1px 2px rgba(0,0,0,.05); } .btn-secondary:hover { background: var(--btn-secondary-hover); transform: translateY(-1px); } .domain-setting { display: none; } .domain-setting.active { display: block; } /* ---------- 滚动条 ---------- */ #rss-list::-webkit-scrollbar { width: 6px; } #rss-list::-webkit-scrollbar-track { background: transparent; } #rss-list::-webkit-scrollbar-thumb { background: var(--scroll-thumb); border-radius: 3px; } #rss-list::-webkit-scrollbar-thumb:hover { background: var(--scroll-thumb-hover); } /* ---------- 响应式 ---------- */ @media (max-width: 480px) { #rss-container { width: calc(100vw - 40px); max-width: 380px; } #settings-modal { width: calc(100vw - 40px); max-width: 380px; } } /* ---------- 打印隐藏 ---------- */ @media print { :host { display: none; } } `; this.shadowRoot.appendChild(style); // 创建计数按钮 this.counter = document.createElement('div'); this.counter.id = 'rss-counter'; this.counter.textContent = '0'; this.counter.style.display = 'none'; this.shadowRoot.appendChild(this.counter); // 创建容器 this.container = document.createElement('div'); this.container.id = 'rss-container'; this.container.innerHTML = ` <div id="rss-header"> <div id="rss-title">${this.t.feed}0</div> <div class="rss-buttons"> <button class="rss-btn" id="settings-btn" title="${this.t.setting}"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="3"></circle> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> </svg> </button> <button class="rss-btn" id="close-btn" title="${this.t.close}"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <line x1="18" y1="6" x2="6" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line> </svg> </button> </div> </div> <div id="rss-list"></div> `; this.shadowRoot.appendChild(this.container); this.rssList = this.shadowRoot.getElementById('rss-list'); // 创建二维码容器 this.qrCode = document.createElement('div'); this.qrCode.className = 'qr-code'; this.shadowRoot.appendChild(this.qrCode); // 创建设置模态框 this.modalOverlay = document.createElement('div'); this.modalOverlay.className = 'modal-overlay'; this.shadowRoot.appendChild(this.modalOverlay); this.settingsModal = document.createElement('div'); this.settingsModal.id = 'settings-modal'; this.settingsModal.innerHTML = ` <h3 style="margin-bottom: 10px; font-size: 18px; color: #1f2937;">${this.t.setting}</h3> <div class="setting-group"> <label class="setting-label">${this.t.subscriptionMode}</label> <select class="setting-select" id="subscription-service"> <option value="feedly">Feedly</option> <option value="inoreader">Inoreader</option> <option value="tinytinyrss">Tiny Tiny RSS</option> <option value="freshrss">Fresh RSS</option> <option value="miniflux">Miniflux</option> <option value="folo">Folo</option> <option value="newsblur">NewsBlur</option> <option value="theoldreader">The Old Reader</option> </select> </div> <div class="setting-group domain-setting" id="inoreader-domain-setting"> <label class="setting-label">${this.t.settingInoreaderDomain}</label> <input type="text" class="setting-input" id="inoreader-domain" placeholder="https://www.inoreader.com"> </div> <div class="setting-group domain-setting required-domain" id="tinytinyrss-domain-setting"> <label class="setting-label"><span class="required-star">*</span>${this.t.settingTinytinyrssDomain}</label> <input type="text" class="setting-input validate-url" id="tinytinyrss-domain" placeholder="https://tt-rss.org"> <div class="error-tip"></div> </div> <div class="setting-group domain-setting required-domain" id="freshrss-domain-setting"> <label class="setting-label"><span class="required-star">*</span>${this.t.settingFreshrssDomain}</label> <input type="text" class="setting-input validate-url" id="freshrss-domain" placeholder="https://freshrss.org"> <div class="error-tip"></div> </div> <div class="setting-group domain-setting required-domain" id="miniflux-domain-setting"> <label class="setting-label"><span class="required-star">*</span>${this.t.settingMinifluxDomain}</label> <input type="text" class="setting-input validate-url" id="miniflux-domain" placeholder="https://miniflux.app"> <div class="error-tip"></div> </div> <div class="setting-group"> <label class="setting-label">${this.t.settingRSShubDomain}</label> <input type="text" class="setting-input" id="rsshub-base" placeholder="https://rsshub.app"> </div> <div class="setting-group"> <label class="setting-label">${this.t.settingRSShubAccessKey}</label> <input type="text" class="setting-input" id="rsshub-access-key" placeholder=""> </div> <div class="setting-group"> <div class="checkbox-wrapper"> <input type="checkbox" id="enable-qrcode"> <label for="enable-qrcode">${this.t.enableQRCode}</label> </div> </div> <div class="modal-buttons"> <button class="btn-secondary" id="cancel-settings">${this.t.close}</button> <button class="btn-primary" id="save-settings">${this.t.confirm}</button> </div> `; this.shadowRoot.appendChild(this.settingsModal); // 绑定事件 this.bindEvents(); } // 绑定事件 bindEvents() { // 拖动功能 this.setupDrag(); // 关闭按钮 this.container.querySelector('#close-btn').addEventListener('click', () => { this.hideContainer(); }); // 设置按钮 this.container.querySelector('#settings-btn').addEventListener('click', () => { this.showSettings(); }); // 设置模态框 this.modalOverlay.addEventListener('click', () => { this.hideSettings(); }); this.settingsModal.addEventListener('click', (e) => { e.stopPropagation(); }); this.settingsModal.querySelector('#cancel-settings').addEventListener('click', () => { this.hideSettings(); }); // this.settingsModal.querySelector('#save-settings').addEventListener('click', () => { // this.saveSettingsFromModal(); // }); this.settingsModal.querySelector('#save-settings').addEventListener('click', () => { if (!this.validateRequiredDomains()) return; // 阻断非法输入 this.saveSettingsFromModal(); }); // 订阅服务切换事件 const subscriptionSelect = this.settingsModal.querySelector('#subscription-service'); subscriptionSelect.addEventListener('change', (e) => { this.toggleDomainSettings(e.target.value); this.validateRequiredDomains(); // 实时把非必填的报错清掉 }); } // 切换域名设置显示 toggleDomainSettings(service) { // 隐藏所有域名设置 this.settingsModal.querySelectorAll('.domain-setting').forEach(el => { el.classList.remove('active'); }); // 显示对应的域名设置 const domainSettings = { // 'inoreader': 'inoreader-domain-setting', 'tinytinyrss': 'tinytinyrss-domain-setting', 'freshrss': 'freshrss-domain-setting', 'miniflux': 'miniflux-domain-setting' }; if (domainSettings[service]) { this.settingsModal.querySelector(`#${domainSettings[service]}`).classList.add('active'); } } // 设置计数按钮拖动 setupDrag() { const dragThreshold = 5; // 像素阈值 let pressTimer = null; // 长按定时器 let dragStartedHere = false; // ★ 新增:标记拖动是否由按钮发起 /* ---------- 启动拖动 ---------- */ const startDrag = (x, y) => { dragStartedHere = true; this.state.isDragging = true; this.state.hasMoved = false; this.state.dragStartX = x; this.state.dragStartY = y; const rect = this.counter.getBoundingClientRect(); this.state.initialX = rect.left; this.state.initialY = rect.top; this.counter.style.cursor = 'grabbing'; this.counter.style.transition = 'none'; // console.log("开始拖动:" + dragStartedHere + " - " + handled); }; /* ---------- 移动处理 ---------- */ const moveHandler = (x, y) => { if (!this.state.isDragging) return; const dx = x - this.state.dragStartX; const dy = y - this.state.dragStartY; if (Math.abs(dx) > dragThreshold || Math.abs(dy) > dragThreshold) { this.state.hasMoved = true; } if (this.state.hasMoved) { this.counter.style.left = `${this.state.initialX + dx}px`; this.counter.style.top = `${this.state.initialY + dy}px`; this.counter.style.right = 'auto'; this.counter.style.bottom = 'auto'; } }; /* ---------- 结束处理 ---------- */ const endHandler = (e) => { // console.log("结束处理器:" + dragStartedHere + " - " + handled); clearTimeout(pressTimer); document.removeEventListener('mousemove', onMoveMouse); document.removeEventListener('mouseup', onEndMouse); document.removeEventListener('touchmove', onMoveTouch); document.removeEventListener('touchend', onEndTouch); // if(!dragStartedHere || handled) return; // handled = true; // cleanup(); if (this.state.hasMoved) { this.snapToEdge(); } else { this.showContainer(); } this.state.isDragging = false; this.state.hasMoved = false; dragStartedHere = false; // handled = false; this.counter.style.cursor = 'pointer'; this.counter.style.transition = 'all .2s ease'; }; /* ========== 鼠标事件 ========== */ const onMoveMouse = (e) => moveHandler(e.clientX, e.clientY); const onEndMouse = (e) => { // console.log("鼠标结束"); e.preventDefault(); endHandler(); }; this.counter.addEventListener('mousedown', (e) => { e.preventDefault(); pressTimer = setTimeout(() => { startDrag(e.clientX, e.clientY); document.addEventListener('mousemove', onMoveMouse); }, 200); document.addEventListener('mouseup', onEndMouse); }); /* ========== 触屏事件 ========== */ const onMoveTouch = (e) => { e.preventDefault(); if (e.touches.length > 0) { moveHandler(e.touches[0].clientX, e.touches[0].clientY); } }; const onEndTouch = (e) => { e.preventDefault(); endHandler(); }; this.counter.addEventListener('touchstart', (e) => { e.preventDefault(); const t = e.touches[0]; pressTimer = setTimeout(() => startDrag(t.clientX, t.clientY), 200); document.addEventListener('touchmove', onMoveTouch, { passive: false }); document.addEventListener('touchend', onEndTouch); }); /* ========== 意外中断 ========== */ // this.counter.addEventListener('touchcancel', cleanup); this.counter.addEventListener('touchcancel', () => { // console.log("cleanup" + onMoveMouse + " moved" + onMoveTouch); clearTimeout(pressTimer); document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); this.state.isDragging = false; this.state.hasMoved = false; dragStartedHere = false; this.counter.style.cursor = 'pointer'; this.counter.style.transition = 'all 0.2s ease'; }); } // 修复后的贴边方法 snapToEdge() { const pad = 20; const counterRect = this.counter.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const centerX = counterRect.left + counterRect.width / 2; let newLeft, newTop; // 水平方向贴边 if (centerX < viewportWidth / 2) { newLeft = pad; // 左边贴边 } else { newLeft = viewportWidth - counterRect.width - pad; // 右边贴边 } // 垂直方向处理(保持当前位置,但确保不超出边界) newTop = counterRect.top; // 检查上边界 if (newTop < pad) { newTop = pad; } // 检查下边界 if (newTop + counterRect.height > viewportHeight - pad) { newTop = viewportHeight - counterRect.height - pad; } // 应用新位置 this.counter.style.left = `${newLeft}px`; this.counter.style.top = `${newTop}px`; this.counter.style.right = 'auto'; this.counter.style.bottom = 'auto'; } // 显示容器 showContainer() { const pad = 20; // 获取计数按钮位置 const counterRect = this.counter.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // 判断计数按钮在左侧还是右侧 const isOnLeft = (counterRect.left + (counterRect.width / 2)) < (viewportWidth / 2); const isOnTop = (counterRect.top + counterRect.height / 2) < (viewportHeight / 2); // 重置容器位置 this.container.style.height = ''; // 设置容器位置 if (isOnLeft) { // 在左侧,以左下角为基准展开 this.container.style.left = counterRect.left + 'px'; this.container.style.right = ''; } else { // 在右侧,以右下角为基准展开 this.container.style.left = ''; this.container.style.right = (viewportWidth - counterRect.right) + 'px'; } if (isOnTop){ this.container.style.top = counterRect.top + 'px'; this.container.style.bottom = ''; this.container.style.maxHeight = viewportHeight - counterRect.height - pad; this.rssList.style.maxHeight = viewportHeight - counterRect.bottom - pad - 52; } else { this.container.style.top = ''; this.container.style.bottom = (viewportHeight - counterRect.bottom) + 'px'; this.container.style.maxHeight = counterRect.bottom; this.rssList.style.maxHeight = counterRect.top - pad - 52; } // 显示容器 this.counter.style.display = 'none'; this.container.style.display = 'block'; // 确保容器在可视区域内 // setTimeout(() => { // this.ensureContainerInViewport(); // }, 10); } // 隐藏容器 hideContainer() { this.container.style.display = 'none'; this.counter.style.display = 'flex'; } // 调整容器高度 adjustContainerHeight() { const listContainer = this.container.querySelector('#rss-list'); const headerHeight = this.container.querySelector('#rss-header').offsetHeight; const viewportHeight = window.innerHeight; const containerBottom = parseInt(this.container.style.bottom) || 0; // 计算可用高度 const availableHeight = viewportHeight - containerBottom - headerHeight - 40; // 40为边距 // 获取列表内容高度 const listContentHeight = listContainer.scrollHeight; // 设置容器高度 if (listContentHeight < availableHeight) { this.container.style.height = (listContentHeight + headerHeight) + 'px'; this.container.style.maxHeight = ''; } else { this.container.style.height = ''; this.container.style.maxHeight = availableHeight + 'px'; } } // 加载设置 loadSettings() { // const saved = localStorage.getItem('rssDetectorSettings'); // const saved = GM_getValue("rssDetectorSettings"); // if (saved) { // this.state.settings = { ...this.state.settings, ...JSON.parse(saved) }; // } this.state.settings.subscriptionService = GM_getValue('rss_service', "feedly"); this.state.settings.rsshubBase = GM_getValue("rsshub_domain", "https://rsshub.app"); this.state.settings.rsshubAccessKey = GM_getValue("rsshub_access_key", ""); // this.state.settings.inoreaderDomain = GM_getValue("inoreader_domain", "https://www.inoreader.com"); this.state.settings.tinytinyrssDomain = GM_getValue("tinytinyrss_domain", ""); this.state.settings.freshrssDomain = GM_getValue("freshrss_domain", ""); this.state.settings.minifluxDomain = GM_getValue("miniflux_domain", ""); this.state.settings.enableQRCode = GM_getValue('enable_qr_code', true); } // 保存设置 saveSettings() { // localStorage.setItem('rssDetectorSettings', JSON.stringify(this.state.settings)); // GM_setValue("rssDetectorSettings", JSON.stringify(this.state.settings)); GM_setValue('rss_service', this.state.settings.subscriptionService); GM_setValue("rsshub_domain", this.state.settings.rsshubBase); GM_setValue("rsshub_access_key", this.state.settings.rsshubAccessKey); // GM_setValue("inoreader_domain", this.state.settings.inoreaderDomain); GM_setValue("tinytinyrss_domain", this.state.settings.tinytinyrssDomain); GM_setValue("freshrss_domain", this.state.settings.freshrssDomain); GM_setValue("miniflux_domain", this.state.settings.minifluxDomain); GM_setValue('enable_qr_code', this.state.settings.enableQRCode); } // 显示设置 showSettings() { // 填充当前设置 this.settingsModal.querySelector('#subscription-service').value = this.state.settings.subscriptionService; this.settingsModal.querySelector('#rsshub-base').value = this.state.settings.rsshubBase; this.settingsModal.querySelector('#rsshub-access-key').value = this.state.settings.rsshubAccessKey; // this.settingsModal.querySelector('#inoreader-domain').value = this.state.settings.inoreaderDomain; this.settingsModal.querySelector('#tinytinyrss-domain').value = this.state.settings.tinytinyrssDomain; this.settingsModal.querySelector('#freshrss-domain').value = this.state.settings.freshrssDomain; this.settingsModal.querySelector('#miniflux-domain').value = this.state.settings.minifluxDomain; this.settingsModal.querySelector('#enable-qrcode').checked = this.state.settings.enableQRCode; // 切换域名设置显示 this.toggleDomainSettings(this.state.settings.subscriptionService); this.modalOverlay.style.display = 'block'; this.settingsModal.style.display = 'block'; } // 隐藏设置 hideSettings() { this.modalOverlay.style.display = 'none'; this.settingsModal.style.display = 'none'; } // 保存设置 saveSettingsFromModal() { this.state.settings.subscriptionService = this.settingsModal.querySelector('#subscription-service').value; this.state.settings.rsshubBase = this.settingsModal.querySelector('#rsshub-base').value; this.state.settings.rsshubAccessKey = this.settingsModal.querySelector('#rsshub-access-key').value; // this.state.settings.inoreaderDomain = this.settingsModal.querySelector('#inoreader-domain').value; this.state.settings.tinytinyrssDomain = this.settingsModal.querySelector('#tinytinyrss-domain').value; this.state.settings.freshrssDomain = this.settingsModal.querySelector('#freshrss-domain').value; this.state.settings.minifluxDomain = this.settingsModal.querySelector('#miniflux-domain').value; this.state.settings.enableQRCode = this.settingsModal.querySelector('#enable-qrcode').checked; this.saveSettings(); this.hideSettings(); } // 简单的 URL 合法性检查 isValidUrl(str) { try { const u = new URL(str); return u.protocol === 'http:' || u.protocol === 'https:'; } catch { return false; } } // 统一校验必填 + 格式 validateRequiredDomains() { const required = [ { id: 'tinytinyrss-domain', service: 'tinytinyrss' }, { id: 'freshrss-domain', service: 'freshrss' }, { id: 'miniflux-domain', service: 'miniflux' } ]; let ok = true; required.forEach(({ id, service }) => { const input = this.settingsModal.querySelector(`#${id}`); const tip = input.parentElement.querySelector('.error-tip'); const value = input.value.trim(); // 如果当前选中的服务就是它,则必填 const currentService = this.settingsModal.querySelector('#subscription-service').value; const isRequired = currentService === service; input.classList.remove('error'); tip.style.display = 'none'; if (isRequired) { if (!value) { tip.textContent = '此项为必填'; tip.style.display = 'block'; input.classList.add('error'); ok = false; } else if (!this.isValidUrl(value)) { tip.textContent = '请输入正确的 URL(带 http/https)'; tip.style.display = 'block'; input.classList.add('error'); ok = false; } } }); return ok; } // 1. 查找已声明的RSS - 借鉴用户代码中的逻辑 findFeedsFromMeta() { const links = document.getElementsByTagName("link"); const rssLinks = []; for (const link of links) { const { href, type, title } = link; if (type && (type.match(/.+\/(rss|rdf|atom|feed\+json)/i) || type.match(/^text\/xml$/i))) { rssLinks.push({ url: href, title: title || document.title || this.t.noTitle, reliable: true }); } } return rssLinks; } // 2. 查找页面上链接中带有rss特征 findFeedsFromLinks(links = []) { const rssLinks = []; for (const link of links) { const href = link.href; // 非url的舍去 if(!/^(?:(?:https?:)?\/\/)?(?:[^\/\s:@]+(?::[^\/\s:@]+)?@)?(?:\[[0-9a-f:]+\]|(?:\d{1,3}\.){3}\d{1,3}|(?:[a-z0-9-]+\.)+[a-z0-9-]+)(?::\d{2,5})?(?:\/[^\s?#]*)?(?:\?[^\s#]*)?(?:#\S*)?$/i.test(href) && !/^\/[^\s#]*$/i.test(href) ){ continue; } // 使用附件中的正则表达式 if (CONSTANTS.RSS_PATTERNS.some(pattern => pattern.test(href))) { rssLinks.push({ url: href, title: link.title || link.textContent || document.title || this.t.noTitle, reliable: true }); } } return rssLinks; } // 3. 检查特定平台 checkFeedForPlatform(selector, regex) { const links = document.querySelectorAll(selector); for (const link of links) { if (regex.test(link.href)) { return true; } } return false; } // 4. 根据页面特征猜测可能的RSS源 findFeedsFromGuess() { const origin = location.origin || `${location.protocol}//${location.host}`; const guessedRSS = []; // 检查特定平台 if (this.checkFeedForPlatform("html > head > link", /(wp-content)/i)) { // WordPress CONSTANTS.FEED_SUFFIXES.forEach(suffix => { guessedRSS.push({ url: origin + suffix, title: document.title || this.t.noTitle, reliable: false }); }); } if (this.checkFeedForPlatform("html > body footer a", /(bitcron\.com|typecho\.org|hexo\.io)/i)) { // Blog platforms CONSTANTS.FEED_SUFFIXES.forEach(suffix => { guessedRSS.push({ url: origin + suffix, title: document.title || this.t.noTitle, reliable: false }); }); } if (document.querySelectorAll("a.footer-copyright-halo, head > meta[name=generator][content*='Halo']").length > 0) { // Halo guessedRSS.push({ url: `${origin}/rss.xml`, title: document.title || this.t.noTitle, reliable: false }); } return guessedRSS; } // 5. 请求远程的RSS规则本地执行 async findFeedsFromCloud() { const url = location.href; const res = document.documentElement.outerHTML; try { // 示例:豆瓣 try { return this.findFeedsByEval(url, res); } catch (e) { console.error("无法通过 Eval 执行:", e); return await this.findFeedsFromAPI(url); } } catch (e) { console.error("无法通过远程规则执行:", e); } return []; } findFeedsByEval(url, res) { const rssLinks = []; return function(jsStr, url, res) { if (jsStr.trim().length === 0) { throw new Error("未获取到可执行脚本"); } const Ruler = Function(`"use strict";return (${jsStr})`)(); const list = Ruler.find(url, res); if (!list) { return rssLinks; } list.forEach(element => { rssLinks.push({url: element.link, title: element.title || document.title, reliable: true}); }); return rssLinks; }.call(window, GM_getResourceText('RulerJs'), url, res); } // 6. 请求API获取RSS源 - 注释掉 async findFeedsFromAPI(url) { const proxy = CONSTANTS.PROXY_LIST[Math.floor(Math.random() * CONSTANTS.PROXY_LIST.length)]; try { const response = await fetch(`${proxy}api/find?url=${encodeURIComponent(url)}`, { method: 'get', timeout: 5000 }); if (response.status === 200) { const obj = JSON.parse(await response.text()); if (obj && Array.isArray(obj)) { return obj.map(item => ({ url: item.link, title: item.title || document.title || this.t.noTitle, reliable: true })); } } } catch (err) { console.error('rss 检测 feed 异常:', err); } return []; } // 检测RSS async detectRSS() { const candidates = new Set(); // 1. 查找已声明的RSS(高可信度) const credibleFeeds = this.findFeedsFromMeta(); credibleFeeds.forEach(feed => candidates.add(JSON.stringify(feed))); // 2. 查找页面上链接中带有rss特征(低可信度) const dubiousFeeds = this.findFeedsFromLinks(document.links || document.getElementsByTagName("a")); dubiousFeeds.forEach(feed => candidates.add(JSON.stringify(feed))); // 3. 请求远程的RSS规则本地执行 const cloudFeeds = await this.findFeedsFromCloud(); cloudFeeds.forEach(feed => candidates.add(JSON.stringify(feed))); // 4. 根据页面特征猜测可能的RSS源 const guessedFeeds = this.findFeedsFromGuess(); guessedFeeds.forEach(feed => candidates.add(JSON.stringify(feed))); // 验证RSS const validationPromises = Array.from(candidates).map(async candidate => { const feed = JSON.parse(candidate); await this.validateRSS(feed); // 更新UI this.updateUI(); }); await Promise.all(validationPromises); } // 检查新增链接是否为RSS源 async checkNewLinksForRSS(links) { const dubiousFeeds = this.findFeedsFromLinks(links); // 验证新增的RSS for (const feed of dubiousFeeds) { if (!this.state.foundRSS.has(feed.url)) { await this.validateRSS(feed); // 更新UI this.updateUI(); } } } // 验证RSS有效性 async validateRSS(feedInfo) { const { url, title, reliable } = feedInfo; if (this.state.foundRSS.has(url)) return; this.state.foundRSS.add(url); console.log("准备验证:" + url); try { const result = await this.verifyFeedUrl(url); // console.log("验证结果:" + JSON.stringify(result)); if (result.valid) { // 处理RSSHub URL let finalUrl = result.targetUrl; if (finalUrl.match(/^https*:\/\/rsshub.app/i)) { const rsshubDomain = this.state.settings.rsshubBase; if (rsshubDomain && rsshubDomain !== "") { finalUrl = finalUrl.replace(/^https*:\/\/rsshub.app/i, rsshubDomain); } const rsshubAccessKey = this.state.settings.rsshubAccessKey; if (rsshubAccessKey && rsshubAccessKey !== "") { const uri = new URL(finalUrl); uri.searchParams.set('key', rsshubAccessKey); finalUrl = uri.href; } } // const feedTitle = await this.getFeedTitle(finalUrl); this.state.validRSS.set(finalUrl, { title: title || document.title, originalUrl: url }); } else if (reliable) { // 对于非猜测的源,即使无效也显示但置灰 this.state.invalidRSS.add({ url: url, title: title || document.title }); } } catch (error) { console.error('验证RSS失败:', error); // 对于非猜测的源,即使验证失败也显示但置灰 if (reliable) { this.state.invalidRSS.add({ url: url, title: title || document.title }); } } } // RSS Url有效性校验 async verifyFeedUrl(url){ try { const response = await fetch(url, { method: 'HEAD', timeout: 3000, headers: { 'Referer': location.href, 'Accept-Encoding': 'deflate', "Accept": "application/feed+json, application/json;q=0.9,application/rss+xml;q=0.8, application/atom+xml;q=0.7, application/xml;q=0.6, */*;q=0.1", 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36' } }); const contentType = response.headers.get('content-type') || response.headers.get('Content-Type') || ''; const valid = (response.ok || response.status === 206) && this.isValidContentType(contentType); if (valid) return {valid: valid, targetUrl: response.url}; } catch (e) { // Range 不支持或报错 → 继续走 fallback } // 先尝试 Range 请求 try { const response = await fetch(url, { method: "GET", timeout: 3000, headers: { "Range": "bytes=0-0", 'Referer': location.href, 'Accept-Encoding': 'deflate', "Accept": "application/feed+json, application/json;q=0.9,application/rss+xml;q=0.8, application/atom+xml;q=0.7, application/xml;q=0.6, */*;q=0.1", 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36' } }); const contentType = response.headers.get('content-type') || response.headers.get('Content-Type') || ''; const valid = (response.ok || response.status === 206) && this.isValidContentType(contentType); if (valid) return {valid: valid, targetUrl: response.url}; } catch (e) { // Range 不支持或报错 → 继续走 fallback } // fallback: 普通 GET,但拿到响应头就中止 const controller = new AbortController(); let bodyConsumed = false; try { const fetchPromise = fetch(url, { signal: controller.signal, timeout: 5000, headers: { "Range": "bytes=0-0", 'Referer': location.href, 'Accept-Encoding': 'deflate', "Accept": "application/feed+json, application/json;q=0.9,application/rss+xml;q=0.8, application/atom+xml;q=0.7, application/xml;q=0.6, */*;q=0.1", 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36' } }) .then(async response => { // 拿到响应头就直接 abort if (!bodyConsumed) { controller.abort(); } const contentType = response.headers.get('content-type') || response.headers.get('Content-Type') || ''; return {valid: response.ok && this.isValidContentType(contentType), targetUrl: response.url}; }) .catch(() => { return {valid: false, targetUrl: url} }); // 限时防止卡死(例如无响应) const timeoutPromise = new Promise(resolve => setTimeout(() => { controller.abort(); resolve({valid: false, targetUrl: url} ); }, 5000) // 最多等 5 秒 ); return await Promise.race([fetchPromise, timeoutPromise]); } catch (e) { return { valid: false, targetUrl: url }; } } // 检查内容类型是否有效 isValidContentType(contentType) { return CONSTANTS.FEED_CONTENT_TYPES.some(type => contentType.includes(type)); } // 检查内容是否为RSS // isRSSContent(content) { // return CONSTANTS.RSS_CONTENT_PATTERNS.some(pattern => pattern.test(content)); // } // // // 获取Feed标题 // async getFeedTitle(responseText, defaultValue = null) { // try { // // RSS 2.0 // const rssTitleMatch = responseText.match(/<title>([^<]+)<\/title>/i); // if (rssTitleMatch) { // return rssTitleMatch[1].trim(); // } // // // Atom // const atomTitleMatch = responseText.match(/<title[^>]*>([^<]+)<\/title>/i); // if (atomTitleMatch) { // return atomTitleMatch[1].trim(); // } // // // JSON Feed // try { // const json = JSON.parse(responseText); // if (json.title) { // return json.title; // } // } catch (e) { // // 不是JSON // } // // return defaultValue || document.title || this.t.noTitle; // } catch (error) { // return defaultValue || document.title || this.t.noTitle; // } // } // 更新UI updateUI() { const totalRSS = this.state.validRSS.size + this.state.invalidRSS.size; // 如果没有RSS源,隐藏计数按钮 if (totalRSS === 0) { this.counter.style.display = 'none'; return; } else if (this.counter.style.display === 'none') { this.counter.style.display = 'flex'; } this.updateCounter(); this.updateList(); } // 更新计数器 updateCounter() { this.counter.textContent = this.state.validRSS.size + this.state.invalidRSS.size; } // 更新列表 updateList() { const listContainer = this.container.querySelector('#rss-list'); const titleElement = this.container.querySelector('#rss-title'); const totalSize = this.state.validRSS.size + this.state.invalidRSS.size; titleElement.innerHTML = `${this.t.feed}<span id="rss-title-count">${totalSize}</span>`; listContainer.innerHTML = ''; // 先添加有效的RSS this.state.validRSS.forEach((info, url) => { const item = this.createRSSItem(url, info.title, info.originalUrl, false); listContainer.appendChild(item); }); // 再添加无效的RSS this.state.invalidRSS.forEach(rssInfo => { const item = this.createRSSItem(rssInfo.url, rssInfo.title, rssInfo.url, true); listContainer.appendChild(item); }); // 调整容器高度 this.adjustContainerHeight(); } // 创建RSS列表项 createRSSItem(url, title, originalUrl, isInvalid) { const item = document.createElement('div'); item.className = 'rss-item' + (isInvalid ? ' invalid' : ''); // 创建内容区域(用于显示二维码) const contentArea = document.createElement('div'); contentArea.className = 'rss-item-content'; // 创建标题和链接 const titleDiv = document.createElement('div'); titleDiv.className = 'rss-item-title'; titleDiv.textContent = this.escapeHtml(title || document.title); const linkElement = document.createElement('a'); linkElement.className = 'rss-item-url'; linkElement.href = url; linkElement.target = '_blank'; linkElement.rel = 'noopener noreferrer'; linkElement.textContent = url; // 将标题和链接添加到内容区域 contentArea.appendChild(titleDiv); contentArea.appendChild(linkElement); // 创建按钮区域 const actionsDiv = document.createElement('div'); actionsDiv.className = 'rss-item-actions'; actionsDiv.innerHTML = ` <button class="rss-btn copy-btn" data-url="${url}" title="${this.t.copy}"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> </svg> </button> <button class="rss-btn subscribe-btn" data-url="${url}" title="${this.t.follow}"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path> </svg> </button> `; // 将内容区域和按钮区域添加到项目 item.appendChild(contentArea); item.appendChild(actionsDiv); // 绑定事件 const copyBtn = actionsDiv.querySelector('.copy-btn'); copyBtn.addEventListener('click', () => { this.copyToClipboard(url); }); const subscribeBtn = actionsDiv.querySelector('.subscribe-btn'); subscribeBtn.addEventListener('click', () => { this.subscribeToRSS(url); }); // 二维码显示 - 只在内容区域显示 if (this.state.settings.enableQRCode) { contentArea.addEventListener('mouseenter', (e) => { this.showQRCode(e, url); }); contentArea.addEventListener('mouseleave', () => { this.hideQRCode(); }); contentArea.addEventListener('mousemove', (e) => { this.updateQRCodePosition(e); }); } return item; } // 复制到剪贴板 async copyToClipboard(text) { try { await navigator.clipboard.writeText(text); this.showToast(this.t.copied); } catch (error) { console.error('复制失败:', error); } } // 订阅RSS subscribeToRSS(url) { const serviceUrls = { feedly: `https://feedly.com/i/subscription/feed/${encodeURIComponent(url)}`, inoreader: `${this.state.settings.inoreaderDomain}/?add_feed=${encodeURIComponent(url)}`, tinytinyrss: `${this.state.settings.tinytinyrssDomain}/public.php?op=bookmarklets--subscribe&feed_url=${url}`, freshrss: `${this.state.settings.freshrssDomain}/i/?c=feed&a=add&url_rss=${encodeURIComponent(url)}`, miniflux: `${this.state.settings.minifluxDomain}/bookmarklet?uri=${encodeURIComponent(url)}`, folo: `follow://add?url=${encodeURIComponent(url)}`, newsblur: `http://www.newsblur.com/?url=${encodeURIComponent(url)}`, theoldreader: `https://theoldreader.com/feeds/subscribe?url=${encodeURIComponent(url)}` }; const subscribeUrl = serviceUrls[this.state.settings.subscriptionService]; if (subscribeUrl) { window.open(subscribeUrl, '_blank'); } } // 生成二维码 async generateQRCode(text) { const qr = qrcode(0, 'L'); qr.addData(text); qr.make(); return qr.createDataURL(); } // 显示二维码 async showQRCode(e, url) { try { const dataURL = await this.generateQRCode(url); this.qrCode.innerHTML = `<img src="${dataURL}">`; //this.qrCode.innerHTML = qrCode; this.qrCode.style.display = 'block'; this.updateQRCodePosition(e); } catch (error) { console.error('生成二维码失败:', error); } } // 隐藏二维码 hideQRCode() { this.qrCode.style.display = 'none'; } // 更新二维码位置 updateQRCodePosition(e) { const qrWidth = 90; // 二维码宽度 const qrHeight = 90; // 二维码高度 const padding = 2; // 边距 let x = e.clientX + 2; let y = e.clientY + 2; // 确保不超出右边界 if (x + qrWidth + padding > window.innerWidth) { x = e.clientX - qrWidth - 2; } // 确保不超出下边界 if (y + qrHeight + padding > window.innerHeight) { y = e.clientY - qrHeight - 2; } // 确保不超出左边界 if (x < padding) { x = padding; } // 确保不超出上边界 if (y < padding) { y = padding; } this.qrCode.style.left = x + 'px'; this.qrCode.style.top = y + 'px'; } // 设置MutationObserver防止控件被删除 setupMutationObserver() { const observer = new MutationObserver(async (mutations) => { const newLinks = []; for (const mutation of mutations) { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName === 'A') { newLinks.push(node); } else if (node.querySelectorAll){ node.querySelectorAll('a').forEach(a => newLinks.push(a)); } } }); mutation.removedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName === 'rss-plus') { // 检查控件是否被删除 if (!document.getElementById('rss-plus-shadow-host')) { document.body.appendChild(this.shadowHost); } } } }); // 处理属性变化(href变化) if (mutation.type === 'attributes' && mutation.target.tagName === 'A' && mutation.attributeName === 'href') { newLinks.push(mutation.target); } } if (newLinks.length > 0) { await this.checkNewLinksForRSS(newLinks); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["href"], attributeOldValue: true }); // 定时检查确保控件存在 // setInterval(() => { // if (!document.getElementById('rss-plus-shadow-host')) { // document.body.appendChild(this.shadowHost); // } // }, 1000); } setupUrlObserver() { const _historyWrap = function(type) { const orig = history[type]; const e = new Event(type); return function() { const rv = orig.apply(this, arguments); e.arguments = arguments; window.dispatchEvent(e); return rv; }; }; history.pushState = _historyWrap('pushState'); history.replaceState = _historyWrap('replaceState'); window.addEventListener('pushState', () => { window.dispatchEvent(new Event('locationchange')); }); window.addEventListener('replaceState', () => { window.dispatchEvent(new Event('locationchange')); }); window.addEventListener('popstate', () => { window.dispatchEvent(new Event('locationchange')); }); window.addEventListener('hashchange', function() { window.dispatchEvent(new Event('locationchange')); }, false); window.addEventListener('locationchange', ()=> { this.state.foundRSS.clear(); this.state.validRSS.clear(); this.state.invalidRSS.clear(); this.detectRSS(); }); } // 显示提示 showToast(message) { const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.8); color: white; padding: 12px 24px; border-radius: 24px; font-size: 14px; z-index: ${this.state.maxZIndex + 4}; animation: fadeInOut 2s ease-in-out; `; toast.textContent = message; const style = document.createElement('style'); style.textContent = ` @keyframes fadeInOut { 0% { opacity: 0; transform: translateX(-50%) translateY(10px); } 20% { opacity: 1; transform: translateX(-50%) translateY(0); } 80% { opacity: 1; transform: translateX(-50%) translateY(0); } 100% { opacity: 0; transform: translateX(-50%) translateY(10px); } } `; document.head.appendChild(style); document.body.appendChild(toast); setTimeout(() => { toast.remove(); style.remove(); }, 2000); } // HTML转义 escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } function fetch(input, options = {}) { return new Promise((resolve, reject) => { const url = typeof input === "string" ? input : input.url; const maxDepth = options.maxRedirects || 20; let depth = 0; let aborted = false; let xhr; // 保存当前 GM_xmlhttpRequest // 绑定 signal if (options.signal) { if (options.signal.aborted) { return reject(new DOMException("Aborted", "AbortError")); } options.signal.addEventListener( "abort", () => { aborted = true; if (xhr) xhr.abort(); reject(new DOMException("Aborted", "AbortError")); }, { once: true } ); } const next = (current) => { if (++depth > maxDepth) return reject(new Error("Too many redirects")); xhr = GM_xmlhttpRequest({ url: current, method: options.method || "GET", headers: options.headers || {}, data: options.body, redirect: "manual", responseType: "arraybuffer", timeout: options.timeout || 30000, onload: (r) => { if (aborted) return; const headersObj = parseHeaders(r.responseHeaders || ""); const buffer = r.response; // ArrayBuffer const status = r.status; const isRedirect = status >= 300 && status < 400; const location = headersObj["location"] ? new URL(headersObj["location"], current).href : null; // redirect 处理 if (isRedirect && location) { const mode = options.redirect || "follow"; if (mode === "follow") { return next(location); } else if (mode === "error") { return reject(new TypeError("Redirect disallowed by redirect option")); } else if (mode === "manual") { return resolve(makeResponse(buffer, { status, statusText: r.statusText, headers: headersObj, }, current)); } } // 非 redirect 或无 Location → 正常返回 resolve(makeResponse(buffer, { status, statusText: r.statusText, headers: headersObj, }, current)); }, onerror: (e) => { if (!aborted) reject(e ?? new Error("Network error")); }, ontimeout: () => { if (!aborted) reject(new DOMException("Timeout", "TimeoutError")); }, onabort: () => { if (!aborted) reject(new DOMException("Aborted", "AbortError")); }, }); }; next(url); }); // 构造原生 Response function makeResponse(buffer, init, currentUrl) { const headers = new Headers(init.headers); // 用 ReadableStream 包装 buffer → 模拟 body stream const body = new ReadableStream({ start(controller) { controller.enqueue(new Uint8Array(buffer)); controller.close(); }, }); const resp = new Response(body, { status: init.status, statusText: init.statusText, headers, }); // 覆盖 url 字段 Object.defineProperty(resp, "url", { value: currentUrl }); return resp; } // 解析响应头为对象 function parseHeaders(raw) { const h = {}; raw.split(/\r?\n/).forEach((line) => { const [k, ...v] = line.split(":"); if (k) h[k.trim()] = v.join(":").trim(); }); return h; } } // 启动RSS检测器 if (!window.rssPlusDetectorInstance) { window.rssPlusDetectorInstance = new RSSPlusDetector(); } })();