├── src ├── General │ ├── meta │ │ ├── usestrict.js │ │ ├── botproc.js │ │ ├── updates.xml │ │ ├── manifest.json │ │ ├── metadata.js │ │ └── banner.js │ ├── img │ │ ├── icon.gif │ │ ├── icon128.png │ │ ├── icon16.png │ │ ├── icon48.png │ │ ├── emoji │ │ │ ├── gnu.png │ │ │ ├── osx.png │ │ │ ├── arch.png │ │ │ ├── baka.png │ │ │ ├── centos.png │ │ │ ├── debian.png │ │ │ ├── fedora.png │ │ │ ├── gentoo.png │ │ │ ├── mint.png │ │ │ ├── neko.png │ │ │ ├── pinkie.png │ │ │ ├── plan9.png │ │ │ ├── ponyo.png │ │ │ ├── rabite.png │ │ │ ├── rarity.png │ │ │ ├── rhel.png │ │ │ ├── sage.png │ │ │ ├── sega.png │ │ │ ├── spike.png │ │ │ ├── ubuntu.png │ │ │ ├── yuno.png │ │ │ ├── SS-sage.png │ │ │ ├── freebsd.png │ │ │ ├── openbsd.png │ │ │ ├── opensuse.png │ │ │ ├── rainbow.png │ │ │ ├── sabayon.png │ │ │ ├── sakamoto.png │ │ │ ├── trisquel.png │ │ │ ├── twilight.png │ │ │ ├── windows.png │ │ │ ├── applejack.png │ │ │ ├── crunchbang.png │ │ │ ├── elementary.png │ │ │ ├── fluttershy.png │ │ │ ├── madotsuki.png │ │ │ ├── slackware.png │ │ │ └── appchan-sage.png │ │ ├── links │ │ │ ├── audio.png │ │ │ ├── gfycat.png │ │ │ ├── gist.png │ │ │ ├── image.png │ │ │ ├── video.png │ │ │ ├── vimeo.png │ │ │ ├── liveleak.png │ │ │ ├── pastebin.png │ │ │ ├── vocaroo.png │ │ │ ├── youtube.png │ │ │ ├── soundcloud.png │ │ │ └── installgentoo.png │ │ ├── favicons │ │ │ ├── dead.gif │ │ │ ├── dead.png │ │ │ ├── empty.gif │ │ │ ├── empty.png │ │ │ ├── Metro │ │ │ │ ├── readSFW.png │ │ │ │ ├── readNSFW.png │ │ │ │ ├── unreadSFW.png │ │ │ │ ├── unreadDead.png │ │ │ │ ├── unreadDeadY.png │ │ │ │ ├── unreadNSFW.png │ │ │ │ ├── unreadNSFWY.png │ │ │ │ └── unreadSFWY.png │ │ │ ├── exclamation.png │ │ │ ├── xat- │ │ │ │ ├── unreadDead.png │ │ │ │ ├── unreadNSFW.png │ │ │ │ ├── unreadSFW.png │ │ │ │ ├── unreadSFWY.png │ │ │ │ ├── unreadDeadY.png │ │ │ │ └── unreadNSFWY.png │ │ │ ├── 4chanJS │ │ │ │ ├── unreadDead.png │ │ │ │ ├── unreadNSFW.png │ │ │ │ ├── unreadSFW.png │ │ │ │ ├── unreadSFWY.png │ │ │ │ ├── unreadDeadY.png │ │ │ │ └── unreadNSFWY.png │ │ │ ├── Mayhem │ │ │ │ ├── unreadDead.png │ │ │ │ ├── unreadDeadY.png │ │ │ │ ├── unreadNSFW.png │ │ │ │ ├── unreadNSFWY.png │ │ │ │ ├── unreadSFW.png │ │ │ │ └── unreadSFWY.png │ │ │ ├── Original │ │ │ │ ├── unreadSFW.png │ │ │ │ ├── unreadDead.png │ │ │ │ ├── unreadDeadY.png │ │ │ │ ├── unreadNSFW.png │ │ │ │ ├── unreadNSFWY.png │ │ │ │ └── unreadSFWY.png │ │ │ └── ferongr │ │ │ │ ├── unreadDead.png │ │ │ │ ├── unreadNSFW.png │ │ │ │ ├── unreadSFW.png │ │ │ │ ├── unreadSFWY.png │ │ │ │ ├── unreadDeadY.png │ │ │ │ └── unreadNSFWY.png │ │ ├── icons │ │ │ ├── 4chanSS.png │ │ │ └── oneechan.png │ │ └── changelog │ │ │ ├── 1.1.18.png │ │ │ ├── 1.2.0.png │ │ │ ├── 1.2.28.png │ │ │ ├── 1.2.31.png │ │ │ ├── 1.2.46.png │ │ │ ├── 1.3.6.gif │ │ │ ├── 1.4.1.png │ │ │ ├── 1.6.0.png │ │ │ ├── 2.3.6.png │ │ │ ├── 1.2.28-2.png │ │ │ └── 2.0.2-qr.png │ ├── audio │ │ └── beep.wav │ ├── lib │ │ ├── board.class │ │ ├── set.class │ │ ├── simpledict.class │ │ ├── catalogthread.class │ │ ├── connection.class │ │ ├── classes.coffee │ │ ├── callbacks.class │ │ ├── notice.class │ │ ├── polyfill.coffee │ │ ├── randomaccesslist.class │ │ ├── clone.class │ │ ├── databoard.class │ │ ├── thread.class │ │ └── captcha.class │ ├── html │ │ ├── Features │ │ │ ├── Filters.svg │ │ │ ├── Index-pagelist.html │ │ │ ├── Embed.html │ │ │ ├── Thread-catalog-view.html │ │ │ ├── MascotDialog.html │ │ │ ├── Settings.html │ │ │ ├── Gallery.html │ │ │ ├── Index-navlinks.html │ │ │ ├── QuickReply.html │ │ │ └── Settings-section-Rice.html │ │ ├── Settings │ │ │ ├── Mascot.html │ │ │ ├── Batch-Theme.html │ │ │ ├── Batch-Mascot.html │ │ │ ├── Keybinds.html │ │ │ ├── Filter-select.html │ │ │ ├── Sauce.html │ │ │ ├── Settings.html │ │ │ ├── Deleted-Theme.html │ │ │ ├── Filter-guide.html │ │ │ └── Theme.html │ │ ├── Monitoring │ │ │ └── ThreadWatcher.html │ │ └── Build │ │ │ └── post.html │ ├── Cheats.coffee │ ├── css │ │ ├── padding.nav.css │ │ ├── padding.pages.css │ │ ├── theme.import.css │ │ ├── mascot.css │ │ ├── prettyprint.css │ │ ├── themeoptions.css │ │ ├── dynamic.css │ │ ├── jscolor.css │ │ └── font-awesome.css │ ├── Globals.coffee │ ├── eventPage │ │ └── eventPage.coffee │ ├── BuildTest.coffee │ └── CrossOrigin.coffee ├── Theming │ ├── GlobalMessage.coffee │ ├── color.coffee │ └── Rice.coffee ├── Monitoring │ ├── ThreadExcerpt.coffee │ ├── MarkNewIPs.coffee │ └── ThreadStats.coffee ├── Miscellaneous │ ├── CustomCSS.coffee │ ├── Flash.coffee │ ├── RemoveSpoilers.coffee │ ├── IDHighlight.coffee │ ├── AntiAutoplay.coffee │ ├── IDColor.coffee │ ├── Time.coffee │ ├── AnnouncementHiding.coffee │ ├── FileInfo.coffee │ ├── CatalogLinks.coffee │ ├── Nav.coffee │ ├── Fourchan.coffee │ ├── ExpandComment.coffee │ ├── Banner.coffee │ ├── ExpandThread.coffee │ └── RelativeDates.coffee ├── Menu │ ├── Labels.coffee │ ├── ReportLink.coffee │ ├── Menu.coffee │ ├── DownloadLink.coffee │ ├── ArchiveLink.coffee │ └── DeleteLink.coffee ├── Quotelinks │ ├── QuoteStrikeThrough.coffee │ ├── QuotePreview.coffee │ ├── QuoteMarkers.coffee │ ├── QuoteBacklink.coffee │ ├── Quotify.coffee │ └── QuoteInline.coffee ├── Images │ ├── RevealSpoilers.coffee │ ├── FappeTyme.coffee │ ├── Sauce.coffee │ ├── ImageHover.coffee │ ├── ImageCommon.coffee │ └── ImageLoader.coffee ├── Filtering │ ├── Anonymize.coffee │ └── Recursive.coffee ├── Archive │ ├── archives.json │ └── Redirect.coffee ├── Posting │ ├── QR.persona.coffee │ └── Captcha.fixes.coffee └── Linkification │ └── Linkify.coffee ├── latest.js ├── .gitignore ├── .gitattributes ├── README.md ├── package.json ├── CONTRIBUTING.md ├── LICENSE └── questionable.json /src/General/meta/usestrict.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | -------------------------------------------------------------------------------- /latest.js: -------------------------------------------------------------------------------- 1 | postMessage({version:'2.9.15'},'*') 2 | -------------------------------------------------------------------------------- /src/General/meta/botproc.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript -------------------------------------------------------------------------------- /src/General/img/icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/icon.gif -------------------------------------------------------------------------------- /src/General/audio/beep.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/audio/beep.wav -------------------------------------------------------------------------------- /src/General/img/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/icon128.png -------------------------------------------------------------------------------- /src/General/img/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/icon16.png -------------------------------------------------------------------------------- /src/General/img/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/icon48.png -------------------------------------------------------------------------------- /src/General/img/emoji/gnu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/gnu.png -------------------------------------------------------------------------------- /src/General/img/emoji/osx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/osx.png -------------------------------------------------------------------------------- /src/General/img/emoji/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/arch.png -------------------------------------------------------------------------------- /src/General/img/emoji/baka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/baka.png -------------------------------------------------------------------------------- /src/General/img/emoji/centos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/centos.png -------------------------------------------------------------------------------- /src/General/img/emoji/debian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/debian.png -------------------------------------------------------------------------------- /src/General/img/emoji/fedora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/fedora.png -------------------------------------------------------------------------------- /src/General/img/emoji/gentoo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/gentoo.png -------------------------------------------------------------------------------- /src/General/img/emoji/mint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/mint.png -------------------------------------------------------------------------------- /src/General/img/emoji/neko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/neko.png -------------------------------------------------------------------------------- /src/General/img/emoji/pinkie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/pinkie.png -------------------------------------------------------------------------------- /src/General/img/emoji/plan9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/plan9.png -------------------------------------------------------------------------------- /src/General/img/emoji/ponyo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/ponyo.png -------------------------------------------------------------------------------- /src/General/img/emoji/rabite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/rabite.png -------------------------------------------------------------------------------- /src/General/img/emoji/rarity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/rarity.png -------------------------------------------------------------------------------- /src/General/img/emoji/rhel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/rhel.png -------------------------------------------------------------------------------- /src/General/img/emoji/sage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/sage.png -------------------------------------------------------------------------------- /src/General/img/emoji/sega.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/sega.png -------------------------------------------------------------------------------- /src/General/img/emoji/spike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/spike.png -------------------------------------------------------------------------------- /src/General/img/emoji/ubuntu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/ubuntu.png -------------------------------------------------------------------------------- /src/General/img/emoji/yuno.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/yuno.png -------------------------------------------------------------------------------- /src/General/img/links/audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/audio.png -------------------------------------------------------------------------------- /src/General/img/links/gfycat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/gfycat.png -------------------------------------------------------------------------------- /src/General/img/links/gist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/gist.png -------------------------------------------------------------------------------- /src/General/img/links/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/image.png -------------------------------------------------------------------------------- /src/General/img/links/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/video.png -------------------------------------------------------------------------------- /src/General/img/links/vimeo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/vimeo.png -------------------------------------------------------------------------------- /src/General/img/emoji/SS-sage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/SS-sage.png -------------------------------------------------------------------------------- /src/General/img/emoji/freebsd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/freebsd.png -------------------------------------------------------------------------------- /src/General/img/emoji/openbsd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/openbsd.png -------------------------------------------------------------------------------- /src/General/img/emoji/opensuse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/opensuse.png -------------------------------------------------------------------------------- /src/General/img/emoji/rainbow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/rainbow.png -------------------------------------------------------------------------------- /src/General/img/emoji/sabayon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/sabayon.png -------------------------------------------------------------------------------- /src/General/img/emoji/sakamoto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/sakamoto.png -------------------------------------------------------------------------------- /src/General/img/emoji/trisquel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/trisquel.png -------------------------------------------------------------------------------- /src/General/img/emoji/twilight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/twilight.png -------------------------------------------------------------------------------- /src/General/img/emoji/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/windows.png -------------------------------------------------------------------------------- /src/General/img/favicons/dead.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/dead.gif -------------------------------------------------------------------------------- /src/General/img/favicons/dead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/dead.png -------------------------------------------------------------------------------- /src/General/img/favicons/empty.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/empty.gif -------------------------------------------------------------------------------- /src/General/img/favicons/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/empty.png -------------------------------------------------------------------------------- /src/General/img/icons/4chanSS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/icons/4chanSS.png -------------------------------------------------------------------------------- /src/General/img/icons/oneechan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/icons/oneechan.png -------------------------------------------------------------------------------- /src/General/img/links/liveleak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/liveleak.png -------------------------------------------------------------------------------- /src/General/img/links/pastebin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/pastebin.png -------------------------------------------------------------------------------- /src/General/img/links/vocaroo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/vocaroo.png -------------------------------------------------------------------------------- /src/General/img/links/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/youtube.png -------------------------------------------------------------------------------- /src/General/img/changelog/1.1.18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/changelog/1.1.18.png -------------------------------------------------------------------------------- /src/General/img/changelog/1.2.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/changelog/1.2.0.png -------------------------------------------------------------------------------- /src/General/img/changelog/1.2.28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/changelog/1.2.28.png -------------------------------------------------------------------------------- /src/General/img/changelog/1.2.31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/changelog/1.2.31.png -------------------------------------------------------------------------------- /src/General/img/changelog/1.2.46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/changelog/1.2.46.png -------------------------------------------------------------------------------- /src/General/img/changelog/1.3.6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/changelog/1.3.6.gif -------------------------------------------------------------------------------- /src/General/img/changelog/1.4.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/changelog/1.4.1.png -------------------------------------------------------------------------------- /src/General/img/changelog/1.6.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/changelog/1.6.0.png -------------------------------------------------------------------------------- /src/General/img/changelog/2.3.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/changelog/2.3.6.png -------------------------------------------------------------------------------- /src/General/img/emoji/applejack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/applejack.png -------------------------------------------------------------------------------- /src/General/img/emoji/crunchbang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/crunchbang.png -------------------------------------------------------------------------------- /src/General/img/emoji/elementary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/elementary.png -------------------------------------------------------------------------------- /src/General/img/emoji/fluttershy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/fluttershy.png -------------------------------------------------------------------------------- /src/General/img/emoji/madotsuki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/madotsuki.png -------------------------------------------------------------------------------- /src/General/img/emoji/slackware.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/slackware.png -------------------------------------------------------------------------------- /src/General/img/links/soundcloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/soundcloud.png -------------------------------------------------------------------------------- /src/General/img/changelog/1.2.28-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/changelog/1.2.28-2.png -------------------------------------------------------------------------------- /src/General/img/changelog/2.0.2-qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/changelog/2.0.2-qr.png -------------------------------------------------------------------------------- /src/General/img/emoji/appchan-sage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/emoji/appchan-sage.png -------------------------------------------------------------------------------- /src/General/img/links/installgentoo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/links/installgentoo.png -------------------------------------------------------------------------------- /src/General/img/favicons/Metro/readSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Metro/readSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/exclamation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/exclamation.png -------------------------------------------------------------------------------- /src/General/img/favicons/Metro/readNSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Metro/readNSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/Metro/unreadSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Metro/unreadSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/xat-/unreadDead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/xat-/unreadDead.png -------------------------------------------------------------------------------- /src/General/img/favicons/xat-/unreadNSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/xat-/unreadNSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/xat-/unreadSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/xat-/unreadSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/xat-/unreadSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/xat-/unreadSFWY.png -------------------------------------------------------------------------------- /src/General/img/favicons/4chanJS/unreadDead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/4chanJS/unreadDead.png -------------------------------------------------------------------------------- /src/General/img/favicons/4chanJS/unreadNSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/4chanJS/unreadNSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/4chanJS/unreadSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/4chanJS/unreadSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/4chanJS/unreadSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/4chanJS/unreadSFWY.png -------------------------------------------------------------------------------- /src/General/img/favicons/Mayhem/unreadDead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Mayhem/unreadDead.png -------------------------------------------------------------------------------- /src/General/img/favicons/Mayhem/unreadDeadY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Mayhem/unreadDeadY.png -------------------------------------------------------------------------------- /src/General/img/favicons/Mayhem/unreadNSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Mayhem/unreadNSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/Mayhem/unreadNSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Mayhem/unreadNSFWY.png -------------------------------------------------------------------------------- /src/General/img/favicons/Mayhem/unreadSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Mayhem/unreadSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/Mayhem/unreadSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Mayhem/unreadSFWY.png -------------------------------------------------------------------------------- /src/General/img/favicons/Metro/unreadDead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Metro/unreadDead.png -------------------------------------------------------------------------------- /src/General/img/favicons/Metro/unreadDeadY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Metro/unreadDeadY.png -------------------------------------------------------------------------------- /src/General/img/favicons/Metro/unreadNSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Metro/unreadNSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/Metro/unreadNSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Metro/unreadNSFWY.png -------------------------------------------------------------------------------- /src/General/img/favicons/Metro/unreadSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Metro/unreadSFWY.png -------------------------------------------------------------------------------- /src/General/img/favicons/Original/unreadSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Original/unreadSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/ferongr/unreadDead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/ferongr/unreadDead.png -------------------------------------------------------------------------------- /src/General/img/favicons/ferongr/unreadNSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/ferongr/unreadNSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/ferongr/unreadSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/ferongr/unreadSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/ferongr/unreadSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/ferongr/unreadSFWY.png -------------------------------------------------------------------------------- /src/General/img/favicons/xat-/unreadDeadY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/xat-/unreadDeadY.png -------------------------------------------------------------------------------- /src/General/img/favicons/xat-/unreadNSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/xat-/unreadNSFWY.png -------------------------------------------------------------------------------- /src/General/img/favicons/4chanJS/unreadDeadY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/4chanJS/unreadDeadY.png -------------------------------------------------------------------------------- /src/General/img/favicons/4chanJS/unreadNSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/4chanJS/unreadNSFWY.png -------------------------------------------------------------------------------- /src/General/img/favicons/Original/unreadDead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Original/unreadDead.png -------------------------------------------------------------------------------- /src/General/img/favicons/Original/unreadDeadY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Original/unreadDeadY.png -------------------------------------------------------------------------------- /src/General/img/favicons/Original/unreadNSFW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Original/unreadNSFW.png -------------------------------------------------------------------------------- /src/General/img/favicons/Original/unreadNSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Original/unreadNSFWY.png -------------------------------------------------------------------------------- /src/General/img/favicons/Original/unreadSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/Original/unreadSFWY.png -------------------------------------------------------------------------------- /src/General/img/favicons/ferongr/unreadDeadY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/ferongr/unreadDeadY.png -------------------------------------------------------------------------------- /src/General/img/favicons/ferongr/unreadNSFWY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zixaphir/appchan-x/HEAD/src/General/img/favicons/ferongr/unreadNSFWY.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *~ 3 | *.db 4 | tmp-crx/ 5 | tmp-userscript/ 6 | testbuilds/ 7 | builds/4chan-X.zip 8 | Gruntfile.js 9 | builds/4chan-* 10 | Gruntfile.js 11 | -------------------------------------------------------------------------------- /src/General/lib/board.class: -------------------------------------------------------------------------------- 1 | class Board 2 | toString: -> @ID 3 | 4 | constructor: (@ID) -> 5 | @threads = new SimpleDict() 6 | @posts = new SimpleDict() 7 | 8 | g.boards[@] = @ -------------------------------------------------------------------------------- /src/General/html/Features/Filters.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/General/html/Features/Index-pagelist.html: -------------------------------------------------------------------------------- 1 | 6 |
7 | -------------------------------------------------------------------------------- /src/General/html/Settings/Mascot.html: -------------------------------------------------------------------------------- 1 |
#{name.replace /_/g, ' '}
2 |
-------------------------------------------------------------------------------- /src/General/meta/updates.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Theming/GlobalMessage.coffee: -------------------------------------------------------------------------------- 1 | GlobalMessage = 2 | init: -> 3 | $.asap (-> d.body), -> 4 | $.asap (-> $.id 'delform'), GlobalMessage.ready 5 | 6 | ready: -> 7 | if el = $ "#globalMessage", d.body 8 | for child in el.children 9 | child.cssText = "" 10 | return -------------------------------------------------------------------------------- /src/General/html/Features/Embed.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | \uf061 4 | \uf00d 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/General/html/Settings/Batch-Theme.html: -------------------------------------------------------------------------------- 1 | New Theme 2 | / 3 | Import Theme 4 | 5 | / 6 | Undelete Theme -------------------------------------------------------------------------------- /src/General/html/Monitoring/ThreadWatcher.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Thread Watcher 4 | \uf021 5 | 6 | 7 | \uf107 8 |
9 |
-------------------------------------------------------------------------------- /src/Monitoring/ThreadExcerpt.coffee: -------------------------------------------------------------------------------- 1 | ThreadExcerpt = 2 | init: -> 3 | return if g.VIEW isnt 'thread' 4 | 5 | Thread.callbacks.push 6 | name: 'Thread Excerpt' 7 | cb: @node 8 | node: -> d.title = Get.threadExcerpt @ 9 | disconnect: -> 10 | return if g.VIEW isnt 'thread' 11 | Thread.callbacks.disconnect 'Thread Excerpt' 12 | -------------------------------------------------------------------------------- /src/General/Cheats.coffee: -------------------------------------------------------------------------------- 1 | # I am bad at JavaScript and if you reuse this, so are you. 2 | Array::indexOf = (val, i) -> 3 | i or= 0 4 | len = @length 5 | while i < len 6 | return i if @[i] is val 7 | i++ 8 | return -1 9 | 10 | # Update CoffeeScript's reference to [].indexOf 11 | # Reserved keywords are ignored in embedded javascript. 12 | `__indexOf = [].indexOf` 13 | -------------------------------------------------------------------------------- /src/Miscellaneous/CustomCSS.coffee: -------------------------------------------------------------------------------- 1 | CustomCSS = 2 | init: -> 3 | return unless Conf['Custom CSS'] 4 | @addStyle() 5 | 6 | addStyle: -> 7 | @style = $.addStyle Conf['usercss'], 'CustomCSS' 8 | 9 | rmStyle: -> 10 | if @style 11 | $.rm @style 12 | delete @style 13 | 14 | update: -> 15 | unless @style 16 | return @addStyle() 17 | @style.textContent = Conf['usercss'] -------------------------------------------------------------------------------- /src/General/lib/set.class: -------------------------------------------------------------------------------- 1 | class ShimSet 2 | constructor: -> 3 | @elements = {} 4 | @size = 0 5 | has: (value) -> 6 | value of @elements 7 | add: (value) -> 8 | return if @elements[value] 9 | @elements[value] = true 10 | @size++ 11 | delete: (value) -> 12 | return unless @elements[value] 13 | delete @elements[value] 14 | @size-- 15 | 16 | window.Set = ShimSet unless 'Set' of window 17 | -------------------------------------------------------------------------------- /src/General/html/Features/Thread-catalog-view.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | #{postCount} / #{fileCount} / #{pageCount} 4 | 5 |
6 | #{subject} 7 |
#{comment}
8 | -------------------------------------------------------------------------------- /src/General/html/Features/MascotDialog.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | PROTIP: Shift-Click the Mascot Image field to upload your own images! 4 |
5 | This may have some caveats. 6 |
7 |
8 |
9 | Save Mascot 10 |
11 |
12 | Close 13 |
-------------------------------------------------------------------------------- /src/General/html/Settings/Batch-Mascot.html: -------------------------------------------------------------------------------- 1 | Clear All 2 | / 3 | Select All 4 | / 5 | Add Mascot 6 | / 7 | Import Mascot 8 | / 9 | Undelete Mascots 10 | / 11 | Get More Mascots! -------------------------------------------------------------------------------- /src/General/lib/simpledict.class: -------------------------------------------------------------------------------- 1 | class SimpleDict 2 | constructor: -> 3 | @keys = [] 4 | 5 | push: (key, data) -> 6 | key = "#{key}" 7 | @keys.push key unless @[key] 8 | @[key] = data 9 | 10 | rm: (key) -> 11 | key = "#{key}" 12 | if (i = @keys.indexOf key) isnt -1 13 | @keys.splice i, 1 14 | delete @[key] 15 | 16 | first: -> @[@keys[0]] 17 | 18 | forEach: (fn) -> 19 | fn @[key] for key in [@keys...] 20 | return 21 | -------------------------------------------------------------------------------- /src/General/css/padding.nav.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-bottom: 15px; 3 | padding-top: 15px; 4 | } 5 | .fourchan-ss-navigation.fixed.top-header:not(.autohide) body::before { 6 | top: #{navHeight}px; 7 | } 8 | .fourchan-ss-navigation.fixed.bottom-header:not(.autohide) body::before { 9 | bottom: #{navHeight}px; 10 | } 11 | .top-header:not(.autohide) body { 12 | padding-top: #{navHeight + 1}px; 13 | } 14 | .bottom-header:not(.autohide) body { 15 | padding-bottom: #{navHeight + 15}px; 16 | } -------------------------------------------------------------------------------- /src/General/html/Settings/Keybinds.html: -------------------------------------------------------------------------------- 1 |
Keybinds are disabled.
2 |
Allowed keys: a-z, 0-9, Ctrl, Shift, Alt, Meta, Enter, Esc, Up, Down, Right, Left.
3 |
Press Backspace to disable a keybind.
4 | 5 | 6 |
ActionsKeybinds
-------------------------------------------------------------------------------- /src/Menu/Labels.coffee: -------------------------------------------------------------------------------- 1 | Labels = 2 | init: -> 3 | return unless Conf['Menu'] and g.VIEW in ['index', 'thread'] 4 | 5 | Menu.menu.addEntry 6 | el: $.el 'div', textContent: 'Labels' 7 | order: 60 8 | open: (post, addSubEntry) -> 9 | {labels} = post.origin or post 10 | return false unless labels.length 11 | @subEntries.length = 0 12 | @subEntries = (el: $.el 'div', textContent: label for label in labels) 13 | true 14 | subEntries: [] 15 | 16 | -------------------------------------------------------------------------------- /src/General/lib/catalogthread.class: -------------------------------------------------------------------------------- 1 | class CatalogThread 2 | @callbacks = new Callbacks 'Catalog Thread' 3 | toString: -> @ID 4 | 5 | constructor: (root, @thread) -> 6 | @ID = @thread.ID 7 | @board = @thread.board 8 | @nodes = 9 | root: root 10 | thumb: $ '.thumb', root 11 | icons: $ '.catalog-icons', root 12 | postCount: $ '.post-count', root 13 | fileCount: $ '.file-count', root 14 | pageCount: $ '.page-count', root 15 | comment: $ '.comment', root 16 | @thread.catalogView = @ 17 | -------------------------------------------------------------------------------- /src/Quotelinks/QuoteStrikeThrough.coffee: -------------------------------------------------------------------------------- 1 | QuoteStrikeThrough = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] and 4 | (Conf['Post Hiding'] or Conf['Post Hiding Link'] or Conf['Filter']) 5 | 6 | Post.callbacks.push 7 | name: 'Strike-through Quotes' 8 | cb: @node 9 | 10 | node: -> 11 | return if @isClone 12 | for quotelink in @nodes.quotelinks 13 | {boardID, postID} = Get.postDataFromLink quotelink 14 | if g.posts["#{boardID}.#{postID}"]?.isHidden 15 | $.addClass quotelink, 'filtered' 16 | return 17 | -------------------------------------------------------------------------------- /src/Images/RevealSpoilers.coffee: -------------------------------------------------------------------------------- 1 | RevealSpoilers = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] and Conf['Reveal Spoiler Thumbnails'] 4 | 5 | Post.callbacks.push 6 | name: 'Reveal Spoiler Thumbnails' 7 | cb: @node 8 | 9 | node: -> 10 | return if @isClone or !@file?.isSpoiler 11 | {thumb} = @file 12 | # Remove old width and height. 13 | thumb.removeAttribute 'style' 14 | # Enforce thumbnail size if thumbnail is replaced. 15 | thumb.style.maxHeight = thumb.style.maxWidth = if @isReply then '125px' else '250px' 16 | thumb.src = @file.thumbURL 17 | -------------------------------------------------------------------------------- /src/General/css/padding.pages.css: -------------------------------------------------------------------------------- 1 | .fourchan-ss-navigation.index.pagination-sticky-top body::before, 2 | .fourchan-ss-navigation.index.pagination-top body::before { 3 | top: #{pageHeight}px; 4 | } 5 | .fourchan-ss-navigation.index.pagination-sticky-bottom body::before, 6 | .fourchan-ss-navigation.index.pagination-bottom body::before { 7 | bottom: #{pageHeight}px; 8 | } 9 | .index.pagination-sticky-top body, 10 | .index.pagination-top body { 11 | padding-top: #{pageHeight + 1}px; 12 | } 13 | .index.pagination-sticky-bottom body, 14 | .index.pagination-bottom body { 15 | padding-bottom: #{pageHeight + 15}px; 16 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | builds/* -text 5 | text eol=lf 6 | 7 | # Custom for Visual Studio 8 | *.cs diff=csharp 9 | *.sln merge=union 10 | *.csproj merge=union 11 | *.vbproj merge=union 12 | *.fsproj merge=union 13 | *.dbproj merge=union 14 | 15 | # Standard to msysgit 16 | *.doc diff=astextplain 17 | *.DOC diff=astextplain 18 | *.docx diff=astextplain 19 | *.DOCX diff=astextplain 20 | *.dot diff=astextplain 21 | *.DOT diff=astextplain 22 | *.pdf diff=astextplain 23 | *.PDF diff=astextplain 24 | *.rtf diff=astextplain 25 | *.RTF diff=astextplain 26 | -------------------------------------------------------------------------------- /src/General/Globals.coffee: -------------------------------------------------------------------------------- 1 | 2 | editTheme = {} 3 | editMascot = {} 4 | userNavigation = {} 5 | Conf = {} 6 | c = console 7 | d = document 8 | doc = d.documentElement 9 | g = 10 | VERSION: '<%= meta.version %>' 11 | NAMESPACE: '<%= meta.name.replace(' ', '_') %>.' 12 | NAME: '<%= meta.name %>' 13 | FAQ: '<%= meta.faq %>' 14 | CHANGELOG: '<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md' 15 | boards: {} 16 | 17 | E = do -> 18 | str = {'&': '&', "'": ''', '"': '"', '<': '<', '>': '>'} 19 | r = String::replace 20 | regex = /[&"'<>]/g 21 | fn = (x) -> 22 | str[x] 23 | (text) -> r.call text, regex, fn -------------------------------------------------------------------------------- /src/General/html/Settings/Filter-select.html: -------------------------------------------------------------------------------- 1 | 16 |
-------------------------------------------------------------------------------- /src/General/meta/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= meta.name %>", 3 | "version": "<%= meta.version %>", 4 | "manifest_version": 2, 5 | "description": "<%= description %>", 6 | "icons": { 7 | "16": "icon16.png", 8 | "48": "icon48.png", 9 | "128": "icon128.png" 10 | }, 11 | "content_scripts": [{ 12 | "js": ["script.js"], 13 | "matches": <%= JSON.stringify(meta.matches) %>, 14 | "all_frames": true, 15 | "run_at": "document_start" 16 | }], 17 | "homepage_url": "<%= meta.page %>", 18 | "minimum_chrome_version": "<%= meta.min.chrome %>", 19 | "permissions": [ 20 | "storage", 21 | "http://*/", 22 | "https://*/" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/Miscellaneous/Flash.coffee: -------------------------------------------------------------------------------- 1 | Flash = 2 | init: -> 3 | if g.BOARD.ID is 'f' 4 | $.ready Flash.initReady 5 | 6 | initReady: -> 7 | $.globalEval 'SWFEmbed.init()' 8 | 9 | return unless g.VIEW is 'thread' 10 | 11 | swfName = $ '.fileText > a' 12 | nav = $ '.navLinks.desktop' 13 | swfName = swfName.href.replace /^(.*?)\/f\//g, "" 14 | sauceLink = $.el 'a', 15 | textContent: 'Check Sauce on SWFCHAN' 16 | href: "http://eye.swfchan.com/search/?q=#{swfName}" 17 | target: "_blank" 18 | $.addClass nav, 'swfSauce' 19 | $.rmClass nav, 'navLinks' 20 | $.rmAll nav 21 | $.add nav, [$.tn('['), sauceLink, $.tn(']')] -------------------------------------------------------------------------------- /src/General/html/Settings/Sauce.html: -------------------------------------------------------------------------------- 1 |
Sauce is disabled.
2 |
Lines starting with a # will be ignored.
3 |
You can specify a display text by appending ;text:[text] to the URL.
4 | 11 | 12 | -------------------------------------------------------------------------------- /src/General/html/Features/Settings.html: -------------------------------------------------------------------------------- 1 |
2 | 14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /src/General/css/theme.import.css: -------------------------------------------------------------------------------- 1 | .board { 2 | padding: 1px 2px; 3 | } 4 | .rice { 5 | box-shadow:rgba(""" + mainColor.shiftRGB(32) + """,.3) 0 1px; 6 | } 7 | input[type=password]:hover, 8 | input[type=text]:not([disabled]):hover, 9 | input#fs_search:hover, 10 | input.field:hover, 11 | textarea:hover, 12 | #options input:not([type=checkbox]):hover { 13 | box-shadow:inset rgba(0,0,0,.2) 0 1px 2px; 14 | } 15 | input[type=password]:focus, 16 | input[type=text]:focus, 17 | input#fs_search:focus, 18 | input.field:focus, 19 | textarea:focus, 20 | #options input:focus { 21 | box-shadow:inset rgba(0,0,0,.2) 0 1px 2px; 22 | } 23 | button, 24 | input, 25 | textarea, 26 | .rice { 27 | transition:background .2s,box-shadow .2s; 28 | } 29 | -------------------------------------------------------------------------------- /src/General/lib/connection.class: -------------------------------------------------------------------------------- 1 | class Connection 2 | constructor: (@target, @origin, @cb={}) -> 3 | $.on window, 'message', @onMessage 4 | 5 | targetWindow: -> 6 | if @target instanceof window.HTMLIFrameElement 7 | @target.contentWindow 8 | else 9 | @target 10 | 11 | send: (data) => 12 | @targetWindow().postMessage "#{g.NAMESPACE}#{JSON.stringify data}", @origin 13 | 14 | onMessage: (e) => 15 | return unless e.source is @targetWindow() and 16 | e.origin is @origin and 17 | typeof e.data is 'string' and 18 | e.data[...g.NAMESPACE.length] is g.NAMESPACE 19 | data = JSON.parse e.data[g.NAMESPACE.length..] 20 | for type, value of data 21 | @cb[type]? value 22 | return 23 | -------------------------------------------------------------------------------- /src/Menu/ReportLink.coffee: -------------------------------------------------------------------------------- 1 | ReportLink = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Report Link'] 4 | 5 | a = $.el 'a', 6 | className: 'report-link' 7 | href: 'javascript:;' 8 | textContent: 'Report this post' 9 | $.on a, 'click', ReportLink.report 10 | Menu.menu.addEntry 11 | el: a 12 | order: 10 13 | open: (post) -> 14 | ReportLink.post = post 15 | !post.isDead 16 | report: -> 17 | {post} = ReportLink 18 | url = "//sys.4chan.org/#{post.board}/imgboard.php?mode=report&no=#{post}" 19 | id = Date.now() 20 | set = "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=685,height=285" 21 | window.open url, id, set 22 | -------------------------------------------------------------------------------- /src/Filtering/Anonymize.coffee: -------------------------------------------------------------------------------- 1 | Anonymize = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread', 'archive'] and Conf['Anonymize'] 4 | return @archive() if g.VIEW is 'archive' 5 | 6 | Post.callbacks.push 7 | name: 'Anonymize' 8 | cb: @node 9 | 10 | node: -> 11 | return if @info.capcode or @isClone 12 | {name, tripcode, email} = @nodes 13 | if @info.name isnt 'Anonymous' 14 | name.textContent = 'Anonymous' 15 | if tripcode 16 | $.rm tripcode 17 | delete @nodes.tripcode 18 | if @info.email 19 | $.replace email, name 20 | delete @nodes.email 21 | 22 | archive: -> 23 | $.ready -> 24 | name.textContent = 'Anonymous' for name in $$ '.name' 25 | $.rm trip for trip in $$ '.postertrip' 26 | return 27 | -------------------------------------------------------------------------------- /src/General/lib/classes.coffee: -------------------------------------------------------------------------------- 1 | <%= grunt.file.read('src/General/lib/callbacks.class') %> 2 | <%= grunt.file.read('src/General/lib/board.class') %> 3 | <%= grunt.file.read('src/General/lib/thread.class') %> 4 | <%= grunt.file.read('src/General/lib/catalogthread.class') %> 5 | <%= grunt.file.read('src/General/lib/post.class') %> 6 | <%= grunt.file.read('src/General/lib/clone.class') %> 7 | <%= grunt.file.read('src/General/lib/databoard.class') %> 8 | <%= grunt.file.read('src/General/lib/notice.class') %> 9 | <%= grunt.file.read('src/General/lib/randomaccesslist.class') %> 10 | <%= grunt.file.read('src/General/lib/simpledict.class') %> 11 | <%= grunt.file.read('src/General/lib/set.class') %> 12 | <%= grunt.file.read('src/General/lib/connection.class') %> 13 | <%= grunt.file.read('src/General/lib/captcha.class') %> 14 | -------------------------------------------------------------------------------- /src/General/html/Settings/Settings.html: -------------------------------------------------------------------------------- 1 | 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/General/html/Features/Gallery.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | \uf04b 4 | \uf04d 5 | \uf107 6 | \uf00d 7 | 8 | 9 | 10 | / 11 | 12 |
13 |
14 | 15 |
16 |
17 |
18 |
-------------------------------------------------------------------------------- /src/Miscellaneous/RemoveSpoilers.coffee: -------------------------------------------------------------------------------- 1 | RemoveSpoilers = 2 | init: -> 3 | if Conf['Reveal Spoilers'] 4 | $.addClass doc, 'reveal-spoilers' 5 | 6 | return unless Conf['Remove Spoilers'] 7 | 8 | $.addClass doc, 'remove-spoilers' 9 | 10 | Post.callbacks.push 11 | name: 'Reveal Spoilers' 12 | cb: @node 13 | 14 | CatalogThread.callbacks.push 15 | name: 'Reveal Spoilers' 16 | cb: @node 17 | 18 | if g.VIEW is 'archive' 19 | $.ready -> RemoveSpoilers.unspoiler $.id 'arc-list' 20 | 21 | node: (post) -> 22 | RemoveSpoilers.unspoiler @nodes.comment 23 | 24 | unspoiler: (el) -> 25 | spoilers = $$ 's', el 26 | for spoiler in spoilers 27 | span = $.el 'span', className: 'removed-spoiler' 28 | $.replace spoiler, span 29 | $.add span, [spoiler.childNodes...] 30 | return -------------------------------------------------------------------------------- /src/General/lib/callbacks.class: -------------------------------------------------------------------------------- 1 | class Callbacks 2 | constructor: (@type) -> 3 | @keys = [] 4 | 5 | push: ({name, cb}) -> 6 | if @[name] 7 | @connect name 8 | else 9 | @keys.push name 10 | @[name] = cb 11 | 12 | connect: (name) -> delete @[name].disconnected if @[name].disconnected 13 | disconnect: (name) -> @[name].disconnected = true if @[name] 14 | 15 | execute: (nodes) -> 16 | for name in @keys 17 | feature = @[name] 18 | for node in nodes when not feature.disconnected 19 | try 20 | feature.call node 21 | catch err 22 | errors = [] unless errors 23 | errors.push 24 | message: ['"', name, '" crashed on node ', @type, ' No.', node.ID, ' (', node.board, ').'].join('') 25 | error: err 26 | 27 | Main.handleErrors errors if errors 28 | -------------------------------------------------------------------------------- /src/Miscellaneous/IDHighlight.coffee: -------------------------------------------------------------------------------- 1 | IDHighlight = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] 4 | 5 | Post.callbacks.push 6 | name: 'Highlight by User ID' 7 | cb: @node 8 | 9 | uniqueID: null 10 | 11 | node: -> 12 | $.on @nodes.uniqueID, 'click', IDHighlight.click @ if @nodes.uniqueID 13 | $.on @nodes.capcode, 'click', IDHighlight.click @ if @nodes.capcode 14 | IDHighlight.set @ unless @isClone 15 | 16 | set: (post) -> 17 | match = (post.info.uniqueID or post.info.capcode) is IDHighlight.uniqueID 18 | $[if match then 'addClass' else 'rmClass'] post.nodes.post, 'highlight' 19 | 20 | click: (post) -> -> 21 | uniqueID = post.info.uniqueID or post.info.capcode 22 | IDHighlight.uniqueID = if IDHighlight.uniqueID is uniqueID then null else uniqueID 23 | g.posts.forEach IDHighlight.set 24 | -------------------------------------------------------------------------------- /src/General/css/mascot.css: -------------------------------------------------------------------------------- 1 | #mascot img { 2 | height: #{ 3 | if mascot.height and isNaN parseFloat mascot.height then 4 | mascot.height 5 | else if mascot.height then 6 | parseInt(mascot.height, 10) + 'px' 7 | else 8 | 'auto' 9 | }; 10 | width: #{ 11 | if mascot.width and isNaN parseFloat mascot.width then 12 | mascot.width 13 | else if mascot.width then 14 | parseInt(mascot.width, 10) + 'px' 15 | else 16 | 'auto' 17 | }; 18 | } 19 | #mascot { 20 | margin: #{mascot.vOffset or 0}px #{mascot.hOffset or 0}px; 21 | } 22 | .sidebar-large #mascot { 23 | left: #{if mascot.center then 25 else 0}px; 24 | right: #{if mascot.center then 25 else 0}px; 25 | } 26 | .mascot-position-above-post-form.post-form-style-fixed #mascot { 27 | <%= transform %>: translateY(-#{if QR.nodes then QR.nodes.el.getBoundingClientRect().height else 0}px); 28 | } -------------------------------------------------------------------------------- /src/General/css/prettyprint.css: -------------------------------------------------------------------------------- 1 | (if Style.lightTheme then """ 2 | .prettyprint { 3 | background-color: #e7e7e7; 4 | border: 1px solid #dcdcdc; 5 | } 6 | .com { 7 | color: #dd0000; 8 | } 9 | .str, 10 | .atv { 11 | color: #7fa61b; 12 | } 13 | .pun { 14 | color: #61663a; 15 | } 16 | .tag { 17 | color: #117743; 18 | } 19 | .kwd { 20 | color: #5a6F9e; 21 | } 22 | .typ, 23 | .atn { 24 | color: #9474bd; 25 | } 26 | .lit { 27 | color: #368c72; 28 | }\n 29 | """ else """ 30 | .prettyprint { 31 | background-color: rgba(0,0,0,.1); 32 | border: 1px solid rgba(0,0,0,0.5); 33 | } 34 | .tag { 35 | color: #96562c; 36 | } 37 | .pun { 38 | color: #5b6f2a; 39 | } 40 | .com { 41 | color: #a34443; 42 | } 43 | .str, 44 | .atv { 45 | color: #8ba446; 46 | } 47 | .kwd { 48 | color: #987d3e; 49 | } 50 | .typ, 51 | .atn { 52 | color: #897399; 53 | } 54 | .lit { 55 | color: #558773; 56 | }\n 57 | """ -------------------------------------------------------------------------------- /src/Menu/Menu.coffee: -------------------------------------------------------------------------------- 1 | Menu = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] 4 | 5 | @button = $.el 'a', 6 | className: 'menu-button' 7 | href: 'javascript:;' 8 | 9 | $.extend @button, <%= html('\uf107') %> 10 | 11 | @menu = new UI.Menu 'post' 12 | Post.callbacks.push 13 | name: 'Menu' 14 | cb: @node 15 | 16 | CatalogThread.callbacks.push 17 | name: 'Menu' 18 | cb: @catalogNode 19 | 20 | node: -> 21 | if @isClone 22 | Menu.makeButton @, $('.menu-button', @nodes.info) 23 | return 24 | $.add @nodes.info, Menu.makeButton @ 25 | 26 | catalogNode: -> 27 | $.after @nodes.icons, Menu.makeButton @thread.OP 28 | 29 | makeButton: (post, button) -> 30 | button or= Menu.button.cloneNode true 31 | $.on button, 'click', (e) -> 32 | Menu.menu.toggle e, @, post 33 | button 34 | -------------------------------------------------------------------------------- /src/General/lib/notice.class: -------------------------------------------------------------------------------- 1 | class Notice 2 | constructor: (type, content, @timeout, @onclose) -> 3 | @el = $.el 'div', 4 | <%= html('\uf00d
') %> 5 | @el.style.opacity = 0 6 | @setType type 7 | $.on @el.firstElementChild, 'click', @close 8 | if typeof content is 'string' 9 | content = $.tn content 10 | $.add @el.lastElementChild, content 11 | 12 | $.ready @add 13 | 14 | setType: (type) -> 15 | @el.className = "notification #{type}" 16 | 17 | add: => 18 | if d.hidden 19 | $.on d, 'visibilitychange', @add 20 | return 21 | $.off d, 'visibilitychange', @add 22 | $.add Header.noticesRoot, @el 23 | @el.clientHeight # force reflow 24 | @el.style.opacity = 1 25 | setTimeout @close, @timeout * $.SECOND if @timeout 26 | 27 | close: => 28 | $.off d, 'visibilitychange', @add 29 | $.rm @el 30 | @onclose?() 31 | -------------------------------------------------------------------------------- /src/Menu/DownloadLink.coffee: -------------------------------------------------------------------------------- 1 | DownloadLink = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] 4 | 5 | if Conf['Add Download Attribute to Filename'] 6 | Post.callbacks.push 7 | name: 'DownloadLink' 8 | cb: @node 9 | 10 | return unless Conf['Menu'] and Conf['Download Link'] 11 | 12 | a = $.el 'a', 13 | className: 'download-link' 14 | textContent: 'Download file' 15 | 16 | # Specifying the filename with the download attribute only works for same-origin links. 17 | $.on a, 'click', ImageCommon.download 18 | 19 | Menu.menu.addEntry 20 | el: a 21 | order: 100 22 | open: ({file}) -> 23 | return false unless file 24 | a.href = file.URL 25 | a.download = file.name 26 | true 27 | 28 | node: -> 29 | return unless @file 30 | # Filename formatting really fucks with this. 31 | a = $('.file-info a', @file.text) or @file.text.firstElementChild 32 | a.download = @file.name -------------------------------------------------------------------------------- /src/Miscellaneous/AntiAutoplay.coffee: -------------------------------------------------------------------------------- 1 | AntiAutoplay = 2 | init: -> 3 | return if !Conf['Disable Autoplaying Sounds'] 4 | $.addClass doc, 'anti-autoplay' 5 | @stop audio for audio in $$ 'audio[autoplay]', doc 6 | window.addEventListener 'loadstart', ((e) => @stop e.target), true 7 | Post.callbacks.push 8 | name: 'Disable Autoplaying Sounds' 9 | cb: @node 10 | CatalogThread.callbacks.push 11 | name: 'Disable Autoplaying Sounds' 12 | cb: @node 13 | $.ready => @process d.body 14 | 15 | stop: (audio) -> 16 | return unless audio.autoplay 17 | audio.pause() 18 | audio.autoplay = false 19 | return if audio.controls 20 | audio.controls = true 21 | $.addClass audio, 'controls-added' 22 | 23 | node: -> 24 | AntiAutoplay.process @nodes.root 25 | 26 | process: (root) -> 27 | for iframe in $$ 'iframe[src*="youtube"][src*="autoplay=1"]', root 28 | iframe.src = iframe.src.replace(/\?autoplay=1&?/, '?').replace('&autoplay=1', '') -------------------------------------------------------------------------------- /src/Filtering/Recursive.coffee: -------------------------------------------------------------------------------- 1 | Recursive = 2 | recursives: {} 3 | init: -> 4 | return unless g.VIEW in ['index', 'thread'] 5 | Post.callbacks.push 6 | name: 'Recursive' 7 | cb: @node 8 | 9 | node: -> 10 | return if @isClone or @isFetchedQuote 11 | for quote in @quotes when obj = Recursive.recursives[quote] 12 | for recursive, i in obj.recursives 13 | @[recursive] obj.args[i]... 14 | return 15 | 16 | add: (recursive, post, args...) -> 17 | obj = Recursive.recursives[post.fullID] or= 18 | recursives: [] 19 | args: [] 20 | obj.recursives.push recursive 21 | obj.args.push args 22 | 23 | rm: (recursive, post) -> 24 | return unless obj = Recursive.recursives[post.fullID] 25 | for rec, i in obj.recursives when rec is recursive 26 | obj.recursives.splice i, 1 27 | obj.args.splice i, 1 28 | return 29 | 30 | apply: (recursive, post, args...) -> 31 | {fullID} = post 32 | g.posts.forEach (post) -> 33 | if fullID in post.quotes 34 | post[recursive] args... 35 | -------------------------------------------------------------------------------- /src/General/css/themeoptions.css: -------------------------------------------------------------------------------- 1 | #{if Style.lightTheme then " 2 | .prettyprint { 3 | background-color: #e7e7e7 !important; 4 | border: 1px solid #dcdcdc !important; 5 | } 6 | .com { 7 | color: #dd0000 !important; 8 | } 9 | .str, 10 | .atv { 11 | color: #7fa61b !important; 12 | } 13 | .pun { 14 | color: #61663a !important; 15 | } 16 | .tag { 17 | color: #117743 !important; 18 | } 19 | .kwd { 20 | color: #5a6F9e !important; 21 | } 22 | .typ, 23 | .atn { 24 | color: #9474bd !important; 25 | } 26 | .lit { 27 | color: #368c72 !important; 28 | } 29 | " else " 30 | .prettyprint { 31 | background-color: rgba(0,0,0,.1) !important; 32 | border: 1px solid rgba(0,0,0,0.5) !important; 33 | } 34 | .tag { 35 | color: #96562c !important; 36 | } 37 | .pun { 38 | color: #5b6f2a !important; 39 | } 40 | .com { 41 | color: #a34443 !important; 42 | } 43 | .str, 44 | .atv { 45 | color: #8ba446 !important; 46 | } 47 | .kwd { 48 | color: #987d3e !important; 49 | } 50 | .typ, 51 | .atn { 52 | color: #897399 !important; 53 | } 54 | .lit { 55 | color: #558773 !important; 56 | } 57 | "} -------------------------------------------------------------------------------- /src/General/eventPage/eventPage.coffee: -------------------------------------------------------------------------------- 1 | requestID = 0 2 | 3 | chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> 4 | id = requestID 5 | requestID++ 6 | sendResponse id 7 | 8 | xhr = new XMLHttpRequest() 9 | xhr.open 'GET', request.url, true 10 | xhr.responseType = request.responseType 11 | xhr.addEventListener 'load', -> 12 | if @readyState is @DONE && xhr.status is 200 13 | contentType = @getResponseHeader 'Content-Type' 14 | contentDisposition = @getResponseHeader 'Content-Disposition' 15 | {response} = @ 16 | if request.responseType is 'arraybuffer' 17 | response = [new Uint8Array(response)...] 18 | chrome.tabs.sendMessage sender.tab.id, {id, response, contentType, contentDisposition} 19 | else 20 | chrome.tabs.sendMessage sender.tab.id, {id, error: true} 21 | , false 22 | xhr.addEventListener 'error', -> 23 | chrome.tabs.sendMessage sender.tab.id, {id, error: true} 24 | , false 25 | xhr.addEventListener 'abort', -> 26 | chrome.tabs.sendMessage sender.tab.id, {id, error: true} 27 | , false 28 | xhr.send() 29 | -------------------------------------------------------------------------------- /src/Images/FappeTyme.coffee: -------------------------------------------------------------------------------- 1 | FappeTyme = 2 | init: -> 3 | return unless (Conf['Fappe Tyme'] or Conf['Werk Tyme']) and g.VIEW in ['index', 'thread'] and g.BOARD.ID isnt 'f' 4 | 5 | for type in ["Fappe", "Werk"] when Conf["#{type} Tyme"] 6 | lc = type.toLowerCase() 7 | FappeTyme[lc] = el = $.el 'a', 8 | href: 'javascript:;' 9 | id: "#{lc}Tyme" 10 | title: "#{type} Tyme" 11 | className: 'a-icon' 12 | 13 | if type is 'Werk' 14 | el.textContent = '\uf0b1' 15 | el.className = 'fa' 16 | 17 | $.on el, 'click', FappeTyme.cb.toggle.bind {name: "#{lc}"} 18 | Header.addShortcut el, true 19 | FappeTyme.cb.set lc 20 | 21 | Post.callbacks.push 22 | name: 'Fappe Tyme' 23 | cb: @node 24 | 25 | node: -> 26 | return if @file 27 | $.addClass @nodes.root, "noFile" 28 | 29 | cb: 30 | set: (type) -> $["#{if Conf[type] then 'add' else 'rm'}Class"] doc, "#{type}Tyme" 31 | toggle: -> 32 | Conf[@name] = !Conf[@name] 33 | FappeTyme.cb.set @name 34 | $.cb.checked.call {name: @name, checked: Conf[@name]} 35 | -------------------------------------------------------------------------------- /src/General/meta/metadata.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name <%= meta.name %> 3 | // @version <%= meta.version %> 4 | // @minGMVer <%= meta.min.greasemonkey %> 5 | // @minFFVer <%= meta.min.firefox %> 6 | // @namespace <%= meta.namespace %> 7 | // @description <%= description %> 8 | // @license MIT; <%= meta.repo %>blob/<%= meta.mainBranch %>/LICENSE 9 | <%= 10 | meta.matches.map(function(match) { 11 | return '// @match ' + match; 12 | }).join('\n') 13 | %> 14 | <%= 15 | meta.excludes.map(function(exclude) { 16 | return '// @exclude ' + exclude; 17 | }).join('\n') 18 | %> 19 | // @grant GM_getValue 20 | // @grant GM_setValue 21 | // @grant GM_deleteValue 22 | // @grant GM_listValues 23 | // @grant GM_openInTab 24 | // @grant GM_xmlhttpRequest 25 | // @run-at document-start 26 | // @updateURL <%= meta.repo %>raw/stable/builds/<%= meta.files.metajs %> 27 | // @downloadURL <%= meta.repo %>raw/stable/builds/<%= meta.files.userjs %> 28 | // @icon data:image/png;base64,<%= grunt.file.read('src/General/img/icon48.png', {encoding: 'base64'}) %> 29 | // ==/UserScript== 30 | -------------------------------------------------------------------------------- /src/General/lib/polyfill.coffee: -------------------------------------------------------------------------------- 1 | Polyfill = 2 | init: -> 3 | @notificationPermission() 4 | @toBlob() 5 | @visibility() 6 | notificationPermission: -> 7 | return if !window.Notification or 'permission' of Notification or !window.webkitNotifications 8 | Object.defineProperty Notification, 'permission', 9 | get: -> 10 | switch webkitNotifications.checkPermission() 11 | when 0 12 | 'granted' 13 | when 1 14 | 'default' 15 | when 2 16 | 'denied' 17 | toBlob: -> 18 | HTMLCanvasElement::toBlob or= (cb) -> 19 | data = atob @toDataURL()[22..] 20 | # DataUrl to Binary code from Aeosynth's 4chan X repo 21 | l = data.length 22 | ui8a = new Uint8Array l 23 | for i in [0...l] by 1 24 | ui8a[i] = data.charCodeAt i 25 | cb new Blob [ui8a], type: 'image/png' 26 | visibility: -> 27 | # page visibility API 28 | return if 'visibilityState' of d 29 | Object.defineProperties HTMLDocument.prototype, 30 | visibilityState: 31 | get: -> @webkitVisibilityState 32 | hidden: 33 | get: -> @webkitHidden 34 | $.on d, 'webkitvisibilitychange', -> $.event 'visibilitychange' 35 | -------------------------------------------------------------------------------- /src/General/html/Settings/Deleted-Theme.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | #{name} 5 | 6 | 7 | #{theme['Author']} 8 | 9 | 10 | (SAGE) 11 | 12 | 13 | #{theme['Author Tripcode']} 14 | 15 | 18 | 19 | No.27583594 20 | 21 |
22 |
23 | 24 | >>27582902 25 | 26 |
27 | I forgive you for using VLC to open me. ;__; 28 |
29 |
-------------------------------------------------------------------------------- /src/General/html/Build/post.html: -------------------------------------------------------------------------------- 1 | """#{if isOP then '' else "
>>
"} 2 |
8 | 9 | #{if isOP then fileHTML else ''} 10 | 11 |
12 | 13 | #{' '}#{subject or ''}#{' '} 14 | 15 | #{emailStart} 16 | #{name or ''} 17 | #{tripcode + capcodeStart + emailEnd + capcodeIcon + userID + flag} 18 | #{" "} 19 | #{date}#{' '} 20 | 21 | No. 22 | #{postID} 28 | #{pageIcon + sticky + closed + replyLink} 29 | 30 |
31 | 32 | #{if isOP then '' else fileHTML} 33 | 34 |
#{comment or ''}
#{' '} 35 | 36 |
""" 37 | -------------------------------------------------------------------------------- /src/General/html/Features/Index-navlinks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | \uf05c 4 |   5 | 6 | 7 | 8 | 15 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Get Appchan X [HERE](http://zixaphir.github.io/appchan-x/). 2 | 3 | 1. Make sure both your **browser** and **Appchan X** are up to date. 4 | 2. Disable your other extensions & scripts to identify conflicts. 5 | 3. If your issue persists, open a [new issue](https://github.com/zixaphir/appchan-x/issues) with the following information: 6 | 1. Precise steps to reproduce the problem, with the expected and actual results. 7 | 2. Console errors, if any. 8 | 3. Browser version. 9 | 4. Your exported settings. If your settings contains sensitive information (e.g. personas), edit the text file manually. 10 | 11 | ### If you have any problems, try resetting your Appchan X settings before calling me a faggot (but feel free to do so) 12 | 13 | ## Forking 14 | 15 | ### Get started 16 | 17 | - Get started by reading through the [Help link](https://help.github.com/) on how to fork a Github project. 18 | - Click the "Fork" button on this page. 19 | - Install [node.js](http://nodejs.org/). 20 | - Install [Grunt's CLI](http://gruntjs.com/) with `npm install -g grunt-cli`. 21 | - Clone Appchan X. 22 | - `cd` into it. 23 | - Install/Update dependencies with `npm install`. 24 | 25 | ### Build 26 | 27 | - Build with `grunt`. 28 | - Continuously build with `grunt watch`. 29 | 30 | ### Contribute 31 | 32 | - See (https://github.com/zixaphir/appchan-x/blob/master/CONTRIBUTING.md). 33 | 34 | Note: this is only used to release new versions, ignore as you see fit. 35 | -------------------------------------------------------------------------------- /src/Miscellaneous/IDColor.coffee: -------------------------------------------------------------------------------- 1 | IDColor = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] and Conf['Color User IDs'] 4 | @ids = { 5 | Heaven: [0, 0, 0, '#fff'] 6 | } 7 | 8 | Post.callbacks.push 9 | name: 'Color User IDs' 10 | cb: @node 11 | 12 | node: -> 13 | return if @isClone or !((uid = @info.uniqueID) and (span = $ 'span.hand', @nodes.uniqueID)) 14 | 15 | rgb = IDColor.ids[uid] or IDColor.compute uid 16 | 17 | # Style the damn node. 18 | {style} = span 19 | style.color = rgb[3] 20 | style.backgroundColor = "rgb(#{rgb[0]},#{rgb[1]},#{rgb[2]})" 21 | $.addClass span, 'painted' 22 | 23 | compute: (uid) -> 24 | # Convert chars to integers, bitshift and math to create a larger integer 25 | # Create a nice string of binary 26 | hash = IDColor.hash uid 27 | 28 | # Convert binary string to numerical values with bitshift and '&' truncation. 29 | rgb = [ 30 | (hash >> 24) & 0xFF 31 | (hash >> 16) & 0xFF 32 | (hash >> 8) & 0xFF 33 | ] 34 | 35 | # Weight color luminance values, assign a font color that should be readable. 36 | rgb.push if (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) > 125 37 | '#000' 38 | else 39 | '#fff' 40 | 41 | # Cache. 42 | @ids[uid] = rgb 43 | 44 | hash: (uid) -> 45 | msg = 0 46 | i = 0 47 | while i < 8 48 | msg = (msg << 5) - msg + uid.charCodeAt i++ 49 | msg 50 | -------------------------------------------------------------------------------- /src/Menu/ArchiveLink.coffee: -------------------------------------------------------------------------------- 1 | ArchiveLink = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Archive Link'] 4 | 5 | div = $.el 'div', 6 | textContent: 'Archive' 7 | 8 | entry = 9 | el: div 10 | order: 90 11 | open: ({ID, thread, board}) -> 12 | !!Redirect.to 'thread', {postID: ID, threadID: thread.ID, boardID: board.ID} 13 | subEntries: [] 14 | 15 | for type in [ 16 | ['Post', 'post'] 17 | ['Name', 'name'] 18 | ['Tripcode', 'tripcode'] 19 | ['Unique ID', 'uniqueID'] 20 | ['Subject', 'subject'] 21 | ['Filename', 'filename'] 22 | ['Image MD5', 'MD5'] 23 | ] 24 | # Add a sub entry for each type. 25 | entry.subEntries.push @createSubEntry type[0], type[1] 26 | 27 | Menu.menu.addEntry entry 28 | 29 | createSubEntry: (text, type) -> 30 | el = $.el 'a', 31 | textContent: text 32 | target: '_blank' 33 | 34 | open = if type is 'post' 35 | ({ID, thread, board}) -> 36 | el.href = Redirect.to 'thread', {postID: ID, threadID: thread.ID, boardID: board.ID} 37 | true 38 | else 39 | (post) -> 40 | value = Filter[type] post 41 | # We want to parse the exact same stuff as the filter does already. 42 | return false unless value 43 | el.href = Redirect.to 'search', 44 | boardID: post.board.ID 45 | type: type 46 | value: value 47 | isSearch: true 48 | true 49 | 50 | return { 51 | el: el 52 | open: open 53 | } 54 | -------------------------------------------------------------------------------- /src/General/html/Settings/Filter-guide.html: -------------------------------------------------------------------------------- 1 |
Filter is disabled.
2 |

3 | Use regular expressions, one per line.
4 | Lines starting with a # will be ignored.
5 | For example, /weeaboo/i will filter posts containing the string `weeaboo`, case-insensitive.
6 | MD5 filtering uses exact string matching, not regular expressions. 7 |

8 | 30 | -------------------------------------------------------------------------------- /src/Miscellaneous/Time.coffee: -------------------------------------------------------------------------------- 1 | Time = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] and Conf['Time Formatting'] 4 | 5 | Post.callbacks.push 6 | name: 'Time Formatting' 7 | cb: @node 8 | 9 | node: -> 10 | return if @isClone 11 | @nodes.date.textContent = Time.format Conf['time'], @info.date 12 | format: (formatString, date) -> 13 | formatString.replace /%(.)/g, (s, c) -> 14 | if c of Time.formatters 15 | Time.formatters[c].call(date) 16 | else 17 | s 18 | 19 | day: [ 20 | 'Sunday' 21 | 'Monday' 22 | 'Tuesday' 23 | 'Wednesday' 24 | 'Thursday' 25 | 'Friday' 26 | 'Saturday' 27 | ] 28 | 29 | month: [ 30 | 'January' 31 | 'February' 32 | 'March' 33 | 'April' 34 | 'May' 35 | 'June' 36 | 'July' 37 | 'August' 38 | 'September' 39 | 'October' 40 | 'November' 41 | 'December' 42 | ] 43 | 44 | zeroPad: (n) -> if n < 10 then "0#{n}" else n 45 | 46 | formatters: 47 | a: -> Time.day[@getDay()][...3] 48 | A: -> Time.day[@getDay()] 49 | b: -> Time.month[@getMonth()][...3] 50 | B: -> Time.month[@getMonth()] 51 | d: -> Time.zeroPad @getDate() 52 | e: -> @getDate() 53 | H: -> Time.zeroPad @getHours() 54 | I: -> Time.zeroPad @getHours() % 12 or 12 55 | k: -> @getHours() 56 | l: -> @getHours() % 12 or 12 57 | m: -> Time.zeroPad @getMonth() + 1 58 | M: -> Time.zeroPad @getMinutes() 59 | p: -> if @getHours() < 12 then 'AM' else 'PM' 60 | P: -> if @getHours() < 12 then 'am' else 'pm' 61 | S: -> Time.zeroPad @getSeconds() 62 | y: -> @getFullYear().toString()[2..] 63 | Y: -> @getFullYear() 64 | '%': -> '%' 65 | -------------------------------------------------------------------------------- /src/Miscellaneous/AnnouncementHiding.coffee: -------------------------------------------------------------------------------- 1 | PSAHiding = 2 | init: -> 3 | return unless Conf['Announcement Hiding'] 4 | $.addClass doc, 'hide-announcement' 5 | $.on d, '4chanXInitFinished', @setup 6 | 7 | setup: -> 8 | $.off d, '4chanXInitFinished', PSAHiding.setup 9 | 10 | unless psa = $.id 'globalMessage' 11 | $.rmClass doc, 'hide-announcement' 12 | return 13 | 14 | entry = 15 | el: $.el 'a', 16 | textContent: 'Show announcement' 17 | className: 'show-announcement' 18 | href: 'javascript:;' 19 | order: 50 20 | open: -> psa.hidden 21 | Header.menu.addEntry entry 22 | $.on entry.el, 'click', PSAHiding.toggle 23 | 24 | PSAHiding.btn = btn = $.el 'span', 25 | title: 'Mark announcement as read and hide.' 26 | className: 'hide-announcement' 27 | href: 'javascript:;' 28 | 29 | $.extend btn, <%= html('[Dismiss]') %> 30 | 31 | $.on btn, 'click', PSAHiding.toggle 32 | 33 | $.get 'hiddenPSA', 0, ({hiddenPSA}) -> 34 | PSAHiding.sync hiddenPSA 35 | $.add psa, btn 36 | $.rmClass doc, 'hide-announcement' 37 | 38 | $.sync 'hiddenPSA', PSAHiding.sync 39 | 40 | toggle: (e) -> 41 | if $.hasClass @, 'hide-announcement' 42 | UTC = +$.id('globalMessage').dataset.utc 43 | $.set 'hiddenPSA', UTC 44 | else 45 | $.event 'CloseMenu' 46 | $.delete 'hiddenPSA' 47 | PSAHiding.sync UTC 48 | 49 | sync: (UTC) -> 50 | psa = $.id 'globalMessage' 51 | psa.hidden = PSAHiding.btn.hidden = if UTC and UTC >= +psa.dataset.utc 52 | true 53 | else 54 | false 55 | if (hr = psa.nextElementSibling) and hr.nodeName is 'HR' 56 | hr.hidden = psa.hidden 57 | -------------------------------------------------------------------------------- /src/Monitoring/MarkNewIPs.coffee: -------------------------------------------------------------------------------- 1 | MarkNewIPs = 2 | init: -> 3 | return if g.VIEW isnt 'thread' or !Conf['Mark New IPs'] 4 | Thread.callbacks.push 5 | name: 'Mark New IPs' 6 | cb: @node 7 | 8 | node: -> 9 | MarkNewIPs.ipCount = @ipCount 10 | MarkNewIPs.postIDs = (+x for x in @posts.keys) 11 | $.on d, 'ThreadUpdate', MarkNewIPs.onUpdate 12 | 13 | onUpdate: (e) -> 14 | {ipCount, newPosts} = e.detail 15 | {postIDs} = ThreadUpdater 16 | return unless ipCount? 17 | if newPosts.length 18 | obj = {} 19 | obj[x] = true for x in MarkNewIPs.postIDs 20 | added = 0 21 | added++ for x in postIDs when not (x of obj) 22 | removed = MarkNewIPs.postIDs.length + added - postIDs.length 23 | switch ipCount - MarkNewIPs.ipCount 24 | when added 25 | i = MarkNewIPs.ipCount 26 | for fullID in newPosts 27 | MarkNewIPs.markNew g.posts[fullID], ++i 28 | when -removed 29 | for fullID in newPosts 30 | MarkNewIPs.markOld g.posts[fullID] 31 | MarkNewIPs.ipCount = ipCount 32 | MarkNewIPs.postIDs = postIDs 33 | 34 | markNew: (post, ipCount) -> 35 | suffix = if (ipCount // 10) % 10 is 1 36 | 'th' 37 | else 38 | ['st', 'nd', 'rd'][ipCount % 10 - 1] or 'th' # fuck switches 39 | counter = $.el 'span', 40 | className: 'ip-counter' 41 | textContent: "(#{ipCount})" 42 | post.nodes.nameBlock.title = "This is the #{ipCount}#{suffix} IP in the thread." 43 | $.add post.nodes.nameBlock, [$.tn(' '), counter] 44 | $.addClass post.nodes.root, 'new-ip' 45 | 46 | markOld: (post) -> 47 | post.nodes.nameBlock.title = 'Not the first post from this IP.' 48 | $.addClass post.nodes.root, 'old-ip' 49 | -------------------------------------------------------------------------------- /src/General/css/dynamic.css: -------------------------------------------------------------------------------- 1 | #boardNavDesktopFoot a, 2 | #header-bar a, 3 | #header-bar .shortcut > span, 4 | .deleteform::before, 5 | .field, 6 | .hide-navigation-decorations .pages a, 7 | .notification, 8 | .selectrice, 9 | body, 10 | button, 11 | input, 12 | textarea { 13 | font-size: #{parseInt Conf["Font Size"], 10}px; 14 | } 15 | #boardTitle, 16 | .boardTitle a { 17 | font-size: #{parseInt(Conf["Font Size"], 10) + 10}px; 18 | } 19 | .boardSubtitle, 20 | .boardSubtitle a { 21 | font-size: #{parseInt(Conf["Font Size"], 10) - 1}px; 22 | } 23 | body, 24 | button, 25 | input, 26 | textarea { 27 | font-family: #{Conf["Font"]}; 28 | } 29 | body { 30 | padding: 0 #{parseInt(Conf["Right Thread Padding"], 10) + editSpace["right"]}px 0 #{parseInt(Conf["Left Thread Padding"], 10) + editSpace["left"]}px; 31 | } 32 | .board > .thread { 33 | margin: #{parseInt Conf["Top Thread Padding"], 10}px 0 #{parseInt Conf["Bottom Thread Padding"], 10}px 0; 34 | } 35 | .post, 36 | .summary { 37 | margin-bottom: #{Conf["Post Spacing"]}px; 38 | } 39 | .thread > .threadContainer:last-of-type { 40 | margin-bottom: -#{Conf["Post Spacing"]}px; 41 | } 42 | .thread > .replyContainer > .reply.post { 43 | border-width: #{if Conf['Post Spacing'] is "0" then "1px 1px 0 1px" else '1px'}; 44 | } 45 | #post-preview, 46 | .postMessage { 47 | margin: #{Conf['Vertical Post Padding']}px #{Conf['Horizontal Post Padding']}px; 48 | } 49 | :root:not(fourchan-ss-navigation):not(.pagination-on-side) .pagelist, 50 | :root:not(fourchan-ss-navigation) #header-bar { 51 | margin-left: #{parseInt(Conf["Left Thread Padding"], 10) + editSpace["right"]}px; 52 | margin-right: #{parseInt(Conf["Right Thread Padding"], 10) + editSpace["left"]}px; 53 | } 54 | #mascot { 55 | opacity: #{Conf['Mascot Opacity']}; 56 | } -------------------------------------------------------------------------------- /src/Theming/color.coffee: -------------------------------------------------------------------------------- 1 | class Color 2 | minmax = (base) -> if base < 0 then 0 else if base > 255 then 255 else base 3 | 4 | shortToLong = (hex) -> 5 | longHex = [] 6 | i = 0 7 | while i < 3 8 | longHex.push hex.substr i, 1 9 | i = Math.floor longHex.length / 2 10 | return longHex.join "" 11 | 12 | colorToHex = (color) -> 13 | if color.substr(0, 1) is '#' 14 | if color.length isnt 4 15 | return color[1..] 16 | else 17 | return shortToLong color.substr(1, 3) 18 | 19 | len = color.length 20 | if len in [3, 6] 21 | if /[0-9a-f]{3}/i.test color 22 | return if color.length is 6 then color else shortToLong color 23 | 24 | if digits = color.match /(.*?)rgba?\((\d+), ?(\d+), ?(\d+)(.*?)\)/ 25 | # [R, G, B] to 0xRRGGBB 26 | hex = ( 27 | (parseInt(digits[2], 10) << 16) | 28 | (parseInt(digits[3], 10) << 8) | 29 | (parseInt(digits[4], 10)) 30 | ).toString 16 31 | 32 | while hex.length < 6 33 | hex = "0#{hex}" 34 | return hex 35 | 36 | else 37 | "000000" 38 | 39 | constructor: (value) -> 40 | @raw = colorToHex value 41 | 42 | hex: -> "#" + @raw 43 | rgb: -> @privateRGB().join "," 44 | hover: -> @shiftRGB 16, true 45 | 46 | isLight: -> 47 | rgb = @privateRGB() 48 | (rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114) > 125 49 | 50 | privateRGB: -> 51 | hex = parseInt @raw, 16 52 | return [ 53 | # 0xRRGGBB to [R, G, B] 54 | (hex >> 16) & 0xFF 55 | (hex >> 8) & 0xFF 56 | hex & 0xFF 57 | ] 58 | 59 | shiftRGB: (shift, smart) -> 60 | shift = (if @isLight() then -1 else 1) * Math.abs shift if smart 61 | rgb = [] 62 | rgb.push minmax color + shift for color in @privateRGB() 63 | rgb.join "," -------------------------------------------------------------------------------- /src/General/html/Settings/Theme.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | #{name} 6 | 7 | 8 | #{theme['Author']} 9 | 10 | 11 | (SAGE) 12 | 13 | 14 | #{theme['Author Tripcode']} 15 | 16 | 19 | 20 | No.27583594 21 | 22 | 23 | >>edit 24 | 25 | 26 | >>export 27 | 28 | 29 | >>delete 30 | 31 |
32 |
33 | 34 | >>27582902 35 | 36 |
37 | Post content is right here. 38 |
39 |

40 | Selected 41 |

42 |
-------------------------------------------------------------------------------- /src/General/lib/randomaccesslist.class: -------------------------------------------------------------------------------- 1 | class RandomAccessList 2 | constructor: (items) -> 3 | @length = 0 4 | @push item for item in items if items 5 | 6 | push: (data) -> 7 | {ID} = data 8 | ID or= data.id 9 | return if @[ID] 10 | {last} = @ 11 | @[ID] = item = 12 | prev: last 13 | next: null 14 | data: data 15 | ID: ID 16 | item.prev = last 17 | @last = if last 18 | last.next = item 19 | else 20 | @first = item 21 | @length++ 22 | 23 | before: (root, item) -> 24 | return if item.next is root or item is root 25 | 26 | @rmi item 27 | 28 | {prev} = root 29 | root.prev = item 30 | item.next = root 31 | item.prev = prev 32 | if prev 33 | prev.next = item 34 | else 35 | @first = item 36 | 37 | after: (root, item) -> 38 | return if item.prev is root 39 | 40 | @rmi item 41 | 42 | {next} = root 43 | root.next = item 44 | item.prev = root 45 | item.next = next 46 | if next 47 | next.prev = item 48 | else 49 | @last = item 50 | 51 | prepend: (item) -> 52 | {first} = @ 53 | return if item is first or not @[item.ID] 54 | @rmi item 55 | item.next = first 56 | if first 57 | first.prev = item 58 | else 59 | @last = item 60 | @first = item 61 | delete item.prev 62 | 63 | shift: -> 64 | @rm @first.ID 65 | 66 | order: -> 67 | order = [item = @first] 68 | order.push item while item = item.next 69 | order 70 | 71 | rm: (ID) -> 72 | item = @[ID] 73 | return unless item 74 | delete @[ID] 75 | @length-- 76 | @rmi item 77 | delete item.next 78 | delete item.prev 79 | 80 | rmi: (item) -> 81 | {prev, next} = item 82 | if prev 83 | prev.next = next 84 | else 85 | @first = next 86 | if next 87 | next.prev = prev 88 | else 89 | @last = prev 90 | -------------------------------------------------------------------------------- /src/Miscellaneous/FileInfo.coffee: -------------------------------------------------------------------------------- 1 | FileInfo = 2 | init: -> 3 | return if !Conf['File Info Formatting'] 4 | 5 | Post.callbacks.push 6 | name: 'File Info Formatting' 7 | cb: @node 8 | node: -> 9 | return if !@file or @isClone 10 | @file.text.innerHTML = "#{FileInfo.format Conf['fileInfo'], @}" 11 | format: (formatString, post) -> 12 | formatString.replace /%([A-Za-z])/g, (s, c) -> 13 | if c of FileInfo.formatters 14 | FileInfo.formatters[c].call(post) 15 | else 16 | s 17 | convertUnit: (size, unit) -> 18 | if unit is 'B' 19 | return "#{size.toFixed()} Bytes" 20 | i = 1 + ['KB', 'MB'].indexOf unit 21 | size /= 1024 while i-- 22 | size = if unit is 'MB' 23 | Math.round(size * 100) / 100 24 | else 25 | size.toFixed() 26 | "#{size} #{unit}" 27 | escape: (name) -> 28 | name.replace /<|>/g, (c) -> 29 | c is '<' and '<' or '>' 30 | formatters: 31 | t: -> @file.URL.match(/\d+\..+$/)[0] 32 | T: -> "#{FileInfo.formatters.t.call @}" 33 | l: -> "#{FileInfo.formatters.n.call @}" 34 | L: -> "#{FileInfo.formatters.N.call @}" 35 | n: -> 36 | fullname = @file.name 37 | shortname = Build.shortFilename @file.name, @isReply 38 | if fullname is shortname 39 | FileInfo.escape fullname 40 | else 41 | "#{FileInfo.escape shortname}#{FileInfo.escape fullname}" 42 | N: -> FileInfo.escape @file.name 43 | p: -> if @file.isSpoiler then 'Spoiler, ' else '' 44 | s: -> @file.size 45 | B: -> FileInfo.convertUnit @file.sizeInBytes, 'B' 46 | K: -> FileInfo.convertUnit @file.sizeInBytes, 'KB' 47 | M: -> FileInfo.convertUnit @file.sizeInBytes, 'MB' 48 | r: -> @file.dimensions or 'PDF' 49 | -------------------------------------------------------------------------------- /src/Quotelinks/QuotePreview.coffee: -------------------------------------------------------------------------------- 1 | QuotePreview = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] and Conf['Quote Previewing'] 4 | 5 | if Conf['Comment Expansion'] 6 | ExpandComment.callbacks.push @node 7 | 8 | Post.callbacks.push 9 | name: 'Quote Previewing' 10 | cb: @node 11 | 12 | node: -> 13 | for link in @nodes.quotelinks.concat [@nodes.backlinks...] 14 | $.on link, 'mouseover', QuotePreview.mouseover 15 | return 16 | 17 | mouseover: (e) -> 18 | return if $.hasClass @, 'inlined' 19 | 20 | {boardID, threadID, postID} = Get.postDataFromLink @ 21 | 22 | qp = $.el 'div', 23 | id: 'qp' 24 | className: 'dialog' 25 | 26 | $.add Header.hover, qp 27 | Get.postClone boardID, threadID, postID, qp, Get.contextFromNode @ 28 | 29 | UI.hover 30 | root: @ 31 | el: qp 32 | latestEvent: e 33 | endEvents: 'mouseout click' 34 | cb: QuotePreview.mouseout 35 | asapTest: -> qp.firstElementChild 36 | 37 | return unless origin = g.posts["#{boardID}.#{postID}"] 38 | 39 | if Conf['Quote Highlighting'] 40 | posts = [origin].concat origin.clones 41 | # Remove the clone that's in the qp from the array. 42 | posts.pop() 43 | for post in posts 44 | $.addClass post.nodes.post, 'qphl' 45 | 46 | quoterID = $.x('ancestor::*[@id][1]', @).id.match(/\d+$/)[0] 47 | clone = Get.postFromRoot qp.firstChild 48 | for quote in clone.nodes.quotelinks.concat [clone.nodes.backlinks...] 49 | if quote.hash[2..] is quoterID 50 | $.addClass quote, 'forwardlink' 51 | return 52 | 53 | mouseout: -> 54 | # Stop if it only contains text. 55 | return unless root = @el.firstElementChild 56 | 57 | clone = Get.postFromRoot root 58 | post = clone.origin 59 | post.rmClone root.dataset.clone 60 | 61 | return unless Conf['Quote Highlighting'] 62 | for post in [post].concat post.clones 63 | $.rmClass post.nodes.post, 'qphl' 64 | return 65 | -------------------------------------------------------------------------------- /src/Miscellaneous/CatalogLinks.coffee: -------------------------------------------------------------------------------- 1 | CatalogLinks = 2 | init: -> 3 | return unless Conf['Catalog Links'] 4 | CatalogLinks.el = el = $.el 'label', 5 | id: 'toggleCatalog' 6 | href: 'javascript:;' 7 | innerHTML: " Catalog Links" 8 | 9 | input = $ 'input', el 10 | $.on input, 'change', @toggle 11 | $.sync 'Header catalog links', CatalogLinks.set 12 | 13 | Header.menu.addEntry 14 | el: el 15 | order: 95 16 | 17 | $.asap (-> $.id 'boardNavDesktopFoot' ), -> 18 | # Set links on load. 19 | CatalogLinks.set Conf['Header catalog links'] 20 | 21 | toggle: -> 22 | $.event 'CloseMenu' 23 | $.set 'Header catalog links', @checked 24 | CatalogLinks.set @checked 25 | 26 | set: (useCatalog) -> 27 | path = if useCatalog then 'catalog' else '' 28 | 29 | generateURL = if useCatalog and Conf['External Catalog'] 30 | CatalogLinks.external 31 | else 32 | CatalogLinks.internal 33 | 34 | for a in $$ """#board-list a:not(.catalog), #boardNavDesktopFoot a""" 35 | continue if a.hostname not in ['boards.4chan.org', 'catalog.neet.tv', '4index.gropes.us'] or 36 | !(board = a.pathname.split('/')[1]) or 37 | board in ['f', 'status', '4chan'] or 38 | $.hasClass a, 'external' 39 | 40 | # Href is easier than pathname because then we don't have 41 | # conditions where External Catalog has been disabled between switches. 42 | a.href = generateURL board, path 43 | 44 | CatalogLinks.el.title = "Turn catalog links #{if useCatalog then 'off' else 'on'}." 45 | 46 | internal: (board, path) -> "/#{board}/#{path}" 47 | external: (board) -> 48 | if board in ['a', 'c', 'g', 'co', 'k', 'm', 'o', 'p', 'v', 'vg', 'w', 'cm', '3', 'adv', 'an', 'cgl', 'ck', 'diy', 'fa', 'fit', 'int', 'jp', 'mlp', 'lit', 'mu', 'n', 'po', 'sci', 'toy', 'trv', 'tv', 'vp', 'x', 'q'] 49 | "http://catalog.neet.tv/#{board}" 50 | else 51 | "/#{board}/catalog" 52 | -------------------------------------------------------------------------------- /src/General/html/Features/QuickReply.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 7 |
8 | \uf00d 9 |
10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 | + 23 |
24 |
25 | 26 | No selected file 27 | 28 | 29 | 32 | Spoiler 33 | \uf0c1 34 | Post from URL 35 | + 36 | Dump 37 | \uf00d 38 | Remove File 39 | 40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/Images/Sauce.coffee: -------------------------------------------------------------------------------- 1 | Sauce = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] and Conf['Sauce'] 4 | 5 | links = [] 6 | for link in Conf['sauces'].split '\n' 7 | try 8 | links.push link.trim() if link[0] isnt '#' 9 | catch err 10 | # Don't add random text plz. 11 | return unless links.length 12 | @links = links 13 | @link = $.el 'a', target: '_blank' 14 | Post.callbacks.push 15 | name: 'Sauce' 16 | cb: @node 17 | createSauceLink: (link, post) -> 18 | parts = {} 19 | for part, i in link.split /;(?=(?:text|boards|types):)/ 20 | if i is 0 21 | parts['url'] = part 22 | else 23 | m = part.match /^(\w*):(.*)$/ 24 | parts[m[1]] = m[2] 25 | parts['text'] or= parts['url'].match(/(\w+)\.\w+\//)?[1] or '?' 26 | for key of parts 27 | parts[key] = parts[key].replace /%(T?URL|MD5|board|name|%|semi)/g, (parameter) -> 28 | type = { 29 | '%TURL': post.file.thumbURL 30 | '%URL': post.file.URL 31 | '%MD5': post.file.MD5 32 | '%board': post.board.ID 33 | '%name': post.file.name 34 | '%%': '%' 35 | '%semi': ';' 36 | }[parameter] 37 | if key is 'url' and parameter isnt '%%' and parameter isnt '%semi' 38 | type = JSON.stringify type if /^javascript:/i.test parts['url'] 39 | type = encodeURIComponent type 40 | type 41 | ext = post.file.URL.match(/\.([^\.]*)$/)?[1] or '' 42 | return null unless !parts['boards'] or post.board.ID in parts['boards'].split ',' 43 | return null unless !parts['types'] or ext in parts['types'].split ',' 44 | a = Sauce.link.cloneNode true 45 | a.href = parts['url'] 46 | a.textContent = parts['text'] 47 | a.removeAttribute 'target' if /^javascript:/i.test parts['url'] 48 | a 49 | node: -> 50 | return if @isClone or !@file 51 | nodes = [] 52 | for link in Sauce.links when node = Sauce.createSauceLink link, @ 53 | # \u00A0 is nbsp 54 | nodes.push $.tn('\u00A0'), node 55 | $.add @file.text, nodes 56 | -------------------------------------------------------------------------------- /src/General/css/jscolor.css: -------------------------------------------------------------------------------- 1 | .jscBox { 2 | width: 251px; 3 | height: 155px; 4 | } 5 | .jscBoxB, 6 | .jscPadB, 7 | .jscPadM, 8 | .jscSldB, 9 | .jscSldM, 10 | .jscBtn { 11 | position: absolute; 12 | clear: both; 13 | } 14 | .jscBoxB { 15 | left: 320px; 16 | bottom: 20px; 17 | z-index: 30; 18 | border: 1px solid; 19 | border-color: ThreeDHighlight ThreeDShadow ThreeDShadow ThreeDHighlight; 20 | background: ThreeDFace; 21 | } 22 | .jscPad { 23 | width: 181px; 24 | height: 101px; 25 | background-image: linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255,1)), linear-gradient(to right, rgb(255,0,0), rgb(255,255,0), rgb(0,255,0), rgb(0,255,255), rgb(0,0,255), rgb(255,0,255), rgb(255,0,0)); 26 | background-repeat: no-repeat; 27 | background-position: 0 0; 28 | } 29 | .jscPadB { 30 | left: 10px; 31 | top: 10px; 32 | border: 1px solid; 33 | border-color: ThreeDShadow ThreeDHighlight ThreeDHighlight ThreeDShadow; 34 | } 35 | .jscPadM { 36 | left: 0; 37 | top: 0; 38 | width: 200px; 39 | height: 121px; 40 | cursor: crosshair; 41 | background-image: url('data:image/gif;base64,R0lGODlhDwAPAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAPAA8AAAIklB8Qx53b4otSUWcvyiz4/4AeQJbmKY4p1HHapBlwPL/uVRsFADs='); 42 | background-repeat: no-repeat; 43 | } 44 | .jscSld { 45 | width: 16px; 46 | height: 101px; 47 | background-image: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,1)); 48 | } 49 | .jscSldB { 50 | right: 10px; 51 | top: 10px; 52 | border: 1px solid; 53 | border-color: ThreeDShadow ThreeDHighlight ThreeDHighlight ThreeDShadow; 54 | } 55 | .jscSldM { 56 | right: 0; 57 | top: 0; 58 | width: 36px; 59 | height: 121px; 60 | cursor: pointer; 61 | background-image: url('data:image/gif;base64,R0lGODlhBwALAKECAAAAAP///6g8eKg8eCH5BAEKAAIALAAAAAAHAAsAAAITTIQYcLnsgGxvijrxqdQq6DRJAQA7'); 62 | background-repeat: no-repeat; 63 | } 64 | .jscBtn { 65 | right: 10px; 66 | bottom: 10px; 67 | padding: 0 15px; 68 | height: 18px; 69 | border: 1px solid; 70 | border-color: ThreeDHighlight ThreeDShadow ThreeDShadow ThreeDHighlight; 71 | color: ButtonText; 72 | text-align: center; 73 | cursor: pointer; 74 | } 75 | .jscBtnS { 76 | line-height: 10px; 77 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "appchan-x", 3 | "description": "The most comprehensive 4chan userscript.", 4 | "meta": { 5 | "name": "appchan x", 6 | "version": "2.10.15", 7 | "namespace": "zixaphir", 8 | "repo": "https://github.com/zixaphir/appchan-x/", 9 | "page": "http://zixaphir.github.com/appchan-x/", 10 | "faq": "https://github.com/zixaphir/appchan-x/wiki/Frequently-Asked-Questions", 11 | "recaptchaKey": "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc", 12 | "youtubeAPIKey": "AIzaSyCrvwsT3ub8sDl3S5APhok2eY-OzRcCK5U", 13 | "buildsPath": "builds/", 14 | "mainBranch": "master", 15 | "matches": [ 16 | "*://*.4chan.org/*", 17 | "*://boards.4chan.org/*", 18 | "*://sys.4chan.org/*", 19 | "*://a.4cdn.org/*", 20 | "*://i.4cdn.org/*", 21 | "*://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc" 22 | ], 23 | "excludes": [ 24 | "*://blog.4chan.org/*", 25 | "*://dis.4chan.org/*" 26 | ], 27 | "files": { 28 | "metajs": "appchan-x.meta.js", 29 | "userjs": "appchan-x.user.js" 30 | }, 31 | "min": { 32 | "chrome": "32", 33 | "firefox": "26", 34 | "greasemonkey": "1.14" 35 | } 36 | }, 37 | "devDependencies": { 38 | "font-awesome": "4.2.0", 39 | "grunt": "0.4.5", 40 | "grunt-bump": "~0.0.16", 41 | "grunt-concurrent": "1.0.0", 42 | "grunt-contrib-clean": "0.6.0", 43 | "grunt-contrib-coffee": "0.12.0", 44 | "grunt-contrib-compress": "0.13.0", 45 | "grunt-contrib-concat": "0.5.0", 46 | "grunt-contrib-copy": "0.7.0", 47 | "grunt-contrib-jshint": "0.10.0", 48 | "grunt-contrib-watch": "0.6.1", 49 | "grunt-shell": "1.1.1", 50 | "load-grunt-tasks": "2.0.0" 51 | }, 52 | "repository": { 53 | "type": "git", 54 | "url": "git://github.com/zixaphir/appchan-x.git" 55 | }, 56 | "author": "Zixaphir ", 57 | "contributors": [ 58 | "Nicolas Stepien ", 59 | "James Campos ", 60 | "seaweedchan ", 61 | "ccd0" 62 | ], 63 | "license": "MIT", 64 | "readmeFilename": "README.md", 65 | "engines": { 66 | "node": ">=0.10" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/General/css/font-awesome.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.0.3 3 | * the iconic font designed for Bootstrap 4 | * ------------------------------------------------------------------------------ 5 | * The full suite of pictographic icons, examples, and documentation can be 6 | * found at http://fontawesome.io. Stay up to date on Twitter at 7 | * http://twitter.com/fontawesome. 8 | * 9 | * License 10 | * ------------------------------------------------------------------------------ 11 | * - The Font Awesome font is licensed under SIL OFL 1.1 - 12 | * http://scripts.sil.org/OFL 13 | * - Font Awesome CSS, LESS, and SASS files are licensed under MIT License - 14 | * http://opensource.org/licenses/mit-license.html 15 | * - Font Awesome documentation licensed under CC BY 3.0 - 16 | * http://creativecommons.org/licenses/by/3.0/ 17 | * - Attribution is no longer required in Font Awesome 3.0, but much appreciated: 18 | * "Font Awesome by Dave Gandy - http://fontawesome.io" 19 | * 20 | * Author - Dave Gandy 21 | * ------------------------------------------------------------------------------ 22 | * Email: dave@fontawesome.io 23 | * Twitter: http://twitter.com/davegandy 24 | * Work: Lead Product Designer @ Kyruus - http://kyruus.com 25 | */ 26 | @font-face{font-family: 'FontAwesome';src: url('data:application/font-woff;base64,<%= grunt.file.read('node_modules/font-awesome/fonts/fontawesome-webfont.woff', {encoding: 'base64'}) %>') format('woff');font-weight:normal;font-style:normal;}.fa,.pfa::after,.pfa::before{font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;speak:none;font-size:14px !important;}#shortcuts .fa {color:rgb(130,130,130) !important;}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} 27 | -------------------------------------------------------------------------------- /src/Miscellaneous/Nav.coffee: -------------------------------------------------------------------------------- 1 | Nav = 2 | init: -> 3 | switch g.VIEW 4 | when 'index' 5 | return unless Conf['Index Navigation'] 6 | when 'thread' 7 | return unless Conf['Reply Navigation'] 8 | else 9 | return 10 | 11 | prev = $.el 'a', 12 | href: 'javascript:;' 13 | id: 'navPrev' 14 | next = $.el 'a', 15 | href: 'javascript:;' 16 | id: 'navNext' 17 | 18 | Header.addShortcut prev, true 19 | Header.addShortcut next, true 20 | 21 | $.on prev, 'click', @prev 22 | $.on next, 'click', @next 23 | 24 | prev: -> 25 | if g.VIEW is 'thread' 26 | window.scrollTo 0, 0 27 | else 28 | Nav.scroll -1 29 | 30 | next: -> 31 | if g.VIEW is 'thread' 32 | window.scrollTo 0, d.body.scrollHeight 33 | else 34 | Nav.scroll +1 35 | 36 | getThread: -> 37 | for threadRoot in $$ '.thread' 38 | thread = Get.threadFromRoot threadRoot 39 | continue if thread.isHidden and !thread.stub 40 | if Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height # not scrolled past 41 | return threadRoot 42 | return $ '.board' 43 | 44 | scroll: (delta) -> 45 | d.activeElement?.blur() 46 | thread = Nav.getThread() 47 | axis = if delta is +1 48 | 'following' 49 | else 50 | 'preceding' 51 | if next = $.x "#{axis}-sibling::div[contains(@class,'thread') and not(@hidden)][1]", thread 52 | # Unless we're not at the beginning of the current thread, 53 | # and thus wanting to move to beginning, 54 | # or we're above the first thread and don't want to skip it. 55 | top = Header.getTopOf thread 56 | thread = next if delta is +1 and top < 5 or delta is -1 and top > -5 57 | # Add extra space to the end of the page if necessary so that all threads can be selected by keybinds. 58 | extra = Header.getTopOf(thread) + doc.clientHeight - d.body.getBoundingClientRect().bottom 59 | d.body.style.marginBottom = "#{extra}px" if extra > 0 60 | 61 | Header.scrollTo thread 62 | 63 | if extra > 0 and !Nav.haveExtra 64 | Nav.haveExtra = true 65 | $.on d, 'scroll', Nav.removeExtra 66 | 67 | removeExtra: -> 68 | extra = doc.clientHeight - d.body.getBoundingClientRect().bottom 69 | if extra > 0 70 | d.body.style.marginBottom = "#{extra}px" 71 | else 72 | d.body.style.marginBottom = null 73 | delete Nav.haveExtra 74 | $.off d, 'scroll', Nav.removeExtra 75 | -------------------------------------------------------------------------------- /src/Miscellaneous/Fourchan.coffee: -------------------------------------------------------------------------------- 1 | Fourchan = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] 4 | 5 | id = g.BOARD.ID 6 | if id is 'g' 7 | $.globalEval ''' 8 | window.addEventListener('prettyprint', function(e) { 9 | window.dispatchEvent(new CustomEvent('prettyprint:cb', { 10 | detail: prettyPrintOne(e.detail) 11 | })); 12 | }, false); 13 | ''' 14 | Post.callbacks.push 15 | name: 'Parse /g/ code' 16 | cb: @code 17 | if id is 'sci' 18 | # https://github.com/MayhemYDG/4chan-x/issues/645#issuecomment-13704562 19 | $.globalEval ''' 20 | window.addEventListener('jsmath', function(e) { 21 | if (!jsMath) return; 22 | if (jsMath.loaded) { 23 | // process one post 24 | jsMath.ProcessBeforeShowing(e.target); 25 | } else if (jsMath.Autoload && jsMath.Autoload.checked) { 26 | // load jsMath and process whole document 27 | jsMath.Autoload.Script.Push('ProcessBeforeShowing', [null]); 28 | jsMath.Autoload.LoadJsMath(); 29 | } 30 | }, false); 31 | ''' 32 | Post.callbacks.push 33 | name: 'Parse /sci/ math' 34 | cb: @math 35 | 36 | CatalogThread.callbacks.push 37 | name: 'Parse /sci/ math' 38 | cb: @math 39 | 40 | # Disable 4chan's ID highlighting (replaced by IDHighlight) and reported post hiding. 41 | Main.ready -> 42 | $.globalEval ''' 43 | (function() { 44 | window.clickable_ids = false; 45 | var nodes = document.querySelectorAll('.posteruid, .capcode'); 46 | for (var i = 0; i < nodes.length; i++) { 47 | nodes[i].removeEventListener("click", window.idClick, false); 48 | } 49 | window.removeEventListener("message", Report.onMessage, false); 50 | })(); 51 | ''' 52 | 53 | code: -> 54 | return if @isClone 55 | apply = (e) -> 56 | pre.innerHTML = e.detail 57 | $.addClass pre, 'prettyprinted' 58 | $.on window, 'prettyprint:cb', apply 59 | for pre in $$ '.prettyprint:not(.prettyprinted)', @nodes.comment 60 | $.event 'prettyprint', pre.innerHTML, window 61 | $.off window, 'prettyprint:cb', apply 62 | return 63 | math: -> 64 | return if (@isClone and doc.contains @origin.nodes.root) or !$ '.math', @nodes.comment 65 | $.asap (=> doc.contains @nodes.comment), => 66 | $.event 'jsmath', null, @nodes.comment 67 | -------------------------------------------------------------------------------- /src/Archive/archives.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "uid": 3, 3 | "name": "4plebs", 4 | "domain": "archive.4plebs.org", 5 | "http": true, 6 | "https": true, 7 | "software": "foolfuuka", 8 | "boards": ["adv", "f", "hr", "o", "pol", "s4s", "sp", "tg", "trv", "tv", "x"], 9 | "files": ["adv", "f", "hr", "o", "pol", "s4s", "sp", "tg", "trv", "tv", "x"] 10 | }, { 11 | "uid": 4, 12 | "name": "Nyafuu Archive", 13 | "domain": "archive.nyafuu.org", 14 | "http": true, 15 | "https": true, 16 | "software": "foolfuuka", 17 | "boards": ["c", "e", "w", "wg", "wsr"], 18 | "files": ["c", "e", "w", "wg", "wsr"] 19 | }, { 20 | "uid": 8, 21 | "name": "Rebecca Black Tech", 22 | "domain": "archive.rebeccablacktech.com", 23 | "http": false, 24 | "https": true, 25 | "software": "fuuka", 26 | "boards": ["cgl", "g", "mu", "qa"], 27 | "files": ["cgl", "g", "mu", "qa"] 28 | }, { 29 | "uid": 10, 30 | "name": "warosu", 31 | "domain": "warosu.org", 32 | "http": false, 33 | "https": true, 34 | "software": "fuuka", 35 | "boards": ["3", "biz", "cgl", "ck", "diy", "fa", "g", "ic", "jp", "lit", "sci", "tg", "vr"], 36 | "files": ["3", "biz", "cgl", "ck", "diy", "fa", "g", "ic", "jp", "lit", "sci", "tg", "vr"] 37 | }, { 38 | "uid": 15, 39 | "name": "fgts", 40 | "domain": "fgts.jp", 41 | "http": true, 42 | "https": true, 43 | "software": "foolfuuka", 44 | "boards": ["asp", "b", "cm", "gd", "h", "hc", "hm", "n", "out", "p", "po", "qa", "r", "s", "soc", "toy", "vp", "y"], 45 | "files": ["asp", "b", "cm", "gd", "h", "hc", "hm", "n", "out", "p", "po", "qa", "r", "s", "soc", "toy", "vp", "y"] 46 | }, { 47 | "uid": 23, 48 | "name": "Desustorage", 49 | "domain": "desustorage.org", 50 | "http": true, 51 | "https": true, 52 | "software": "foolfuuka", 53 | "boards": ["a", "aco", "an", "c", "co", "d", "fit", "his", "int", "k", "m", "mlp", "qa", "r9k", "tg", "trash", "vr", "wsg"], 54 | "files": ["a", "aco", "an", "c", "co", "d", "fit", "his", "int", "k", "m", "mlp", "qa", "r9k", "tg", "trash", "vr", "wsg"] 55 | }, { 56 | "uid": 24, 57 | "name": "fireden.net", 58 | "domain": "boards.fireden.net", 59 | "http": false, 60 | "https": true, 61 | "software": "foolfuuka", 62 | "boards": ["a", "cm", "ic", "sci", "tg", "v", "vg", "y"], 63 | "files": ["a", "cm", "ic", "sci", "tg", "v", "vg", "y"] 64 | }, { 65 | "uid": 25, 66 | "name": "arch.b4k.co", 67 | "domain": "arch.b4k.co", 68 | "http": true, 69 | "https": true, 70 | "software": "foolfuuka", 71 | "boards": ["g", "jp", "mlp", "v"], 72 | "files": [] 73 | }] 74 | -------------------------------------------------------------------------------- /src/Miscellaneous/ExpandComment.coffee: -------------------------------------------------------------------------------- 1 | ExpandComment = 2 | init: -> 3 | return if g.VIEW isnt 'index' or !Conf['Comment Expansion'] or Conf['JSON Navigation'] 4 | 5 | @callbacks.push Fourchan.code if g.BOARD.ID is 'g' 6 | @callbacks.push Fourchan.math if g.BOARD.ID is 'sci' 7 | 8 | Post.callbacks.push 9 | name: 'Comment Expansion' 10 | cb: @node 11 | 12 | node: -> 13 | if a = $ '.abbr > a:not([onclick])', @nodes.comment 14 | $.on a, 'click', ExpandComment.cb 15 | 16 | callbacks: [] 17 | 18 | cb: (e) -> 19 | e.preventDefault() 20 | ExpandComment.expand Get.postFromNode @ 21 | 22 | expand: (post) -> 23 | if post.nodes.longComment and !post.nodes.longComment.parentNode 24 | $.replace post.nodes.shortComment, post.nodes.longComment 25 | post.nodes.comment = post.nodes.longComment 26 | return 27 | return unless a = $ '.abbr > a', post.nodes.comment 28 | a.textContent = "Post No.#{post} Loading..." 29 | $.cache "//a.4cdn.org#{a.pathname.split('/').splice(0,4).join('/')}.json", -> ExpandComment.parse @, a, post 30 | 31 | contract: (post) -> 32 | return unless post.nodes.shortComment 33 | a = $ '.abbr > a', post.nodes.shortComment 34 | a.textContent = 'here' 35 | $.replace post.nodes.longComment, post.nodes.shortComment 36 | post.nodes.comment = post.nodes.shortComment 37 | 38 | parse: (req, a, post) -> 39 | {status} = req 40 | unless status in [200, 304] 41 | a.textContent = "Error #{req.statusText} (#{status})" 42 | return 43 | 44 | posts = req.response.posts 45 | if spoilerRange = posts[0].custom_spoiler 46 | Build.spoilerRange[g.BOARD] = spoilerRange 47 | 48 | for postObj in posts 49 | break if postObj.no is post.ID 50 | if postObj.no isnt post.ID 51 | a.textContent = "Post No.#{post} not found." 52 | return 53 | 54 | {comment} = post.nodes 55 | clone = comment.cloneNode false 56 | clone.innerHTML = postObj.com 57 | # Fix pathnames 58 | for quote in $$ '.quotelink', clone 59 | href = quote.getAttribute 'href' 60 | continue if href[0] is '/' # Cross-board quote, or board link 61 | if href[0] is '#' 62 | quote.href = "#{a.pathname.split('/').splice(0,4).join('/')}#{href}" 63 | else 64 | quote.href = "#{a.pathname.split('/').splice(0,3).join('/')}/#{href}" 65 | post.nodes.shortComment = comment 66 | $.replace comment, clone 67 | post.nodes.comment = post.nodes.longComment = clone 68 | post.parseComment() 69 | post.parseQuotes() 70 | 71 | for callback in ExpandComment.callbacks 72 | callback.call post 73 | return 74 | -------------------------------------------------------------------------------- /src/Images/ImageHover.coffee: -------------------------------------------------------------------------------- 1 | ImageHover = 2 | init: -> 3 | return if g.VIEW not in ['index', 'thread'] 4 | if Conf['Image Hover'] 5 | Post.callbacks.push 6 | name: 'Image Hover' 7 | cb: @node 8 | if Conf['Image Hover in Catalog'] 9 | CatalogThread.callbacks.push 10 | name: 'Image Hover' 11 | cb: @catalogNode 12 | 13 | node: -> 14 | return unless @file and (@file.isImage or @file.isVideo) 15 | $.on @file.thumb, 'mouseover', ImageHover.mouseover 16 | 17 | catalogNode: -> 18 | {file} = @thread.OP 19 | return unless file and (file.isImage or file.isVideo) 20 | $.on @nodes.thumb, 'mouseover', ImageHover.mouseover 21 | 22 | mouseover: (e) -> 23 | post = if $.hasClass @, 'thumb' 24 | g.posts[@parentNode.dataset.fullID] 25 | else 26 | Get.postFromNode @ 27 | {file} = post 28 | {isVideo} = file 29 | return if file.isExpanding or file.isExpanded 30 | error = ImageHover.error post 31 | if ImageCommon.cache?.dataset.fullID is post.fullID 32 | el = ImageCommon.popCache() 33 | $.on el, 'error', error 34 | else 35 | el = $.el (if isVideo then 'video' else 'img') 36 | el.dataset.fullID = post.fullID 37 | $.on el, 'error', error 38 | el.src = file.URL 39 | 40 | if Conf['Restart when Opened'] 41 | ImageCommon.rewind el 42 | ImageCommon.rewind @ 43 | el.id = 'ihover' 44 | $.add Header.hover, el 45 | if isVideo 46 | el.loop = true 47 | el.controls = false 48 | el.play() if Conf['Autoplay'] 49 | [width, height] = (+x for x in file.dimensions.split 'x') 50 | {left, right} = @getBoundingClientRect() 51 | padding = 16 52 | maxWidth = Math.max left, doc.clientWidth - right 53 | maxHeight = doc.clientHeight - 16 54 | scale = Math.min 1, maxWidth / width, maxHeight / height 55 | el.style.maxWidth = "#{scale * width}px" 56 | el.style.maxHeight = "#{scale * height}px" 57 | el.style.width = "#{width}px" 58 | el.style.height = "#{height}px" 59 | UI.hover 60 | root: @ 61 | el: el 62 | latestEvent: e 63 | endEvents: 'mouseout click' 64 | asapTest: -> true 65 | height: scale * height + padding 66 | noRemove: true 67 | cb: -> 68 | $.off el, 'error', error 69 | ImageCommon.pushCache el 70 | el.pause() if isVideo 71 | $.rm el 72 | el.removeAttribute 'style' 73 | 74 | error: (post) -> -> 75 | return if ImageCommon.decodeError @, post 76 | ImageCommon.error @, post, 3 * $.SECOND, (URL) => 77 | if URL 78 | @src = URL + if @src is URL then '?' + Date.now() else '' 79 | else 80 | $.rm @ -------------------------------------------------------------------------------- /src/General/BuildTest.coffee: -------------------------------------------------------------------------------- 1 | <% if (tests_enabled) { %> 2 | BuildTest = 3 | init: -> 4 | return if !Conf['Menu'] or g.VIEW not in ['index', 'thread'] 5 | 6 | a = $.el 'a', 7 | textContent: 'Test HTML building' 8 | $.on a, 'click', @testOne 9 | Menu.menu.addEntry 10 | el: a 11 | open: (post) -> 12 | a.dataset.fullID = post.fullID 13 | true 14 | 15 | a2 = $.el 'a', 16 | textContent: 'Test HTML building' 17 | $.on a2, 'click', @testAll 18 | Header.menu.addEntry 19 | el: a2 20 | 21 | firstDiff: (x, y) -> 22 | x2 = x.cloneNode false 23 | y2 = y.cloneNode false 24 | return [x2, y2] unless x2.isEqualNode y2 25 | i = 0 26 | while true 27 | x2 = x.childNodes[i] 28 | y2 = y.childNodes[i] 29 | return [x2, y2] unless x2 and y2 30 | return BuildTest.firstDiff(x2, y2) unless x2.isEqualNode y2 31 | i++ 32 | 33 | testOne: (post) -> 34 | BuildTest.postsRemaining++ 35 | $.cache "//a.4cdn.org/#{post.board.ID}/thread/#{post.thread.ID}.json", -> 36 | {posts} = @response 37 | Build.spoilerRange[post.board.ID] = posts[0].custom_spoiler 38 | for postData in posts 39 | if postData.no is post.ID 40 | t1 = new Date().getTime() 41 | root = Build.postFromObject postData, post.board.ID 42 | t2 = new Date().getTime() 43 | BuildTest.time += t2 - t1 44 | post2 = new Post root, post.thread, post.board 45 | x = post.normalizedOriginal 46 | y = post2.normalizedOriginal 47 | if x.isEqualNode y 48 | c.log "#{post.fullID} correct" 49 | else 50 | c.log "#{post.fullID} differs" 51 | BuildTest.postsFailed++ 52 | [x2, y2] = BuildTest.firstDiff x, y 53 | c.log x2 54 | c.log y2 55 | c.log x.outerHTML 56 | c.log y.outerHTML 57 | BuildTest.postsRemaining-- 58 | BuildTest.report() if BuildTest.postsRemaining is 0 59 | post2.isFetchedQuote = true 60 | Main.callbackNodes Post, [post2] 61 | 62 | testAll: -> 63 | g.posts.forEach (post) -> 64 | unless post.isClone or post.isFetchedQuote or $ '.abbr', post.nodes.comment 65 | BuildTest.testOne post 66 | return 67 | 68 | postsRemaining: 0 69 | postsFailed: 0 70 | time: 0 71 | 72 | report: -> 73 | if BuildTest.postsFailed 74 | new Notice 'warning', "#{BuildTest.postsFailed} post(s) differ (#{BuildTest.time} ms)", 30 75 | else 76 | new Notice 'success', "All correct (#{BuildTest.time} ms)", 5 77 | BuildTest.postsFailed = BuildTest.time = 0 78 | 79 | cb: 80 | testOne: -> 81 | BuildTest.testOne g.posts[@dataset.fullID] 82 | Menu.menu.close() 83 | 84 | testAll: -> 85 | BuildTest.testAll() 86 | Header.menu.close() 87 | <% } %> 88 | -------------------------------------------------------------------------------- /src/General/lib/clone.class: -------------------------------------------------------------------------------- 1 | class Clone extends Post 2 | constructor: (@origin, @context, contractThumb) -> 3 | for key in ['ID', 'fullID', 'board', 'thread', 'info', 'quotes', 'isReply'] 4 | # Copy or point to the origin's key value. 5 | @[key] = origin[key] 6 | 7 | {nodes} = origin 8 | root = if contractThumb 9 | @cloneWithoutVideo nodes.root 10 | else 11 | nodes.root.cloneNode true 12 | post = $ '.post', root 13 | info = $ '.postInfo', post 14 | @nodes = 15 | root: root 16 | post: post 17 | info: info 18 | nameBlock: $ '.nameBlock', info 19 | quote: $ '.postNum > a:nth-of-type(2)', info 20 | comment: $ '.postMessage', post 21 | quotelinks: [] 22 | backlinks: info.getElementsByClassName 'backlink' 23 | 24 | # Remove inlined posts inside of this post. 25 | for inline in $$ '.inline', post 26 | $.rm inline 27 | for inlined in $$ '.inlined', post 28 | $.rmClass inlined, 'inlined' 29 | 30 | root.hidden = post.hidden = false # post hiding 31 | $.rmClass root, 'forwarded' # quote inlining 32 | $.rmClass post, 'highlight' # keybind navigation, ID highlighting 33 | 34 | if nodes.subject 35 | @nodes.subject = $ '.subject', info 36 | if nodes.name 37 | @nodes.name = $ '.name', info 38 | if nodes.email 39 | @nodes.email = $ '.useremail', info 40 | if nodes.tripcode 41 | @nodes.tripcode = $ '.postertrip', info 42 | if nodes.uniqueID 43 | @nodes.uniqueID = $ '.posteruid', info 44 | if nodes.capcode 45 | @nodes.capcode = $ '.capcode.hand', info 46 | if nodes.flag 47 | @nodes.flag = $ '.flag, .countryFlag', info 48 | if nodes.date 49 | @nodes.date = $ '.dateTime', info 50 | 51 | @parseQuotes() 52 | 53 | if origin.file 54 | # Copy values, point to relevant elements. 55 | # See comments in Post's constructor. 56 | @file = {} 57 | for key, val of origin.file 58 | @file[key] = val 59 | file = $ '.file', post 60 | @file.text = file.firstElementChild 61 | @file.thumb = $ '.fileThumb > [data-md5]', file 62 | @file.fullImage = $ '.full-image', file 63 | @file.videoControls = $ '.video-controls', @file.text 64 | 65 | # Contract thumbnails in quote preview 66 | ImageExpand.contract @ if contractThumb 67 | 68 | @isDead = true if origin.isDead 69 | @isClone = true 70 | root.dataset.clone = origin.clones.push(@) - 1 71 | 72 | cloneWithoutVideo: (node) -> 73 | if node.tagName is 'VIDEO' and !node.dataset.md5 # (exception for WebM thumbnails) 74 | [] 75 | else if node.nodeType is Node.ELEMENT_NODE and $ 'video', node 76 | clone = node.cloneNode false 77 | $.add clone, @cloneWithoutVideo child for child in node.childNodes 78 | clone 79 | else 80 | node.cloneNode true 81 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Reporting bugs and suggestions 2 | 3 | Reporting bugs: (note that some of these links refer to Mayhem's 4chan X repo to avoid duplication of information resources. Bugs MUST be filed on [our issue tracker](https://github.com/zixaphir/appchan-x/issues), not Mayhem's) 4 | 5 | 1. Make sure both your **browser** and **Appchan X** are up to date.
6 | Only **Chrome**, **Firefox** and **Opera** are supported.
7 | **SRWare Iron**, **Firefox ESR**, **Pale Moon**, **Waterfox**, and other derivatives are not supported, use them at your own risk. This means that issue reports made with these browsers will be ignored unless you're able to duplicate it on a supported browser. 8 | 9 | 2. Look at the list of [known problems and solutions](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#known-problems). 10 | 3. Disable your other extensions & scripts to identify conflicts. 11 | 4. If your issue persists, open a [new issue](https://github.com/zixaphir/appchan-x/issues) with the following information: 12 | 1. Precise steps to reproduce the problem, with the expected and actual results. 13 | 2. [Console errors](https://github.com/MayhemYDG/4chan-x/wiki/FAQ#console-errors), if any. 14 | 3. Appchan X version, browser variant, browser version, and Greasemonkey version if you are using it. 15 | 4. Your exported settings. If your settings contains sensible information (e.g. personas), edit the text file manually. 16 | 17 | Respect these guidelines: 18 | - Describe the issue clearly, put some effort into it. A one-liner isn't a good enough description. 19 | - If you want to get your suggestion implemented sooner, make it convincing. 20 | - If you want to criticize, make it convincing and constructive. 21 | - Be mature. Act like an idiot and you will be blocked without warning. 22 | 23 | ## Development & Contribution 24 | 25 | ### Get started 26 | 27 | - Install [node.js](http://nodejs.org/). 28 | - Install [Grunt's CLI](http://gruntjs.com/) with `npm install -g grunt-cli`. 29 | - Clone Appchan X. 30 | - `cd` into it. 31 | - Install/Update Appchan X dependencies with `npm install`. 32 | 33 | ### Build 34 | 35 | - Build with `grunt`. 36 | - Continuously build with `grunt watch`. 37 | 38 | ### Release 39 | 40 | - Update the version with `grunt patch`, `grunt minor` or `grunt major`. 41 | - Release with `grunt release`. 42 | 43 | Note: this is only used to release new versions, and is **not** needed or wanted in pull requests. 44 | 45 | ### Contribute 46 | 47 | - Edit the sources. 48 | - If the edits affect regular users, edit the changelog. 49 | - Open a pull request. 50 | 51 | ## Archive Maintenance 52 | 53 | Archivers should direct their archive pull requests (updated boards, changed protocols, etc) to [archives.json](https://github.com/MayhemYDG/archives.json). Appchan does not provide a JSON Interfact to synchronize dynamically as 4chan X does. There is no benefit to updating the archives in this repo and it only creates additional merge overhead. 54 | -------------------------------------------------------------------------------- /src/Quotelinks/QuoteMarkers.coffee: -------------------------------------------------------------------------------- 1 | QuoteMarkers = 2 | init: -> 3 | if Conf['Highlight Own Posts'] 4 | $.addClass doc, 'highlight-own' 5 | 6 | if Conf['Highlight Posts Quoting You'] 7 | $.addClass doc, 'highlight-you' 8 | 9 | Post.callbacks.push 10 | name: 'Quote Markers' 11 | cb: @node 12 | 13 | node: -> 14 | {parseQuotelink} = QuoteMarkers 15 | for quotelink in @nodes.quotelinks 16 | parseQuotelink @, quotelink, !!@isClone 17 | return 18 | 19 | parseQuotelink: (post, quotelink, mayReset, customText) -> 20 | {board, thread} = if post.isClone then post.context else post 21 | markers = [] 22 | {boardID, threadID, postID} = Get.postDataFromLink quotelink 23 | 24 | if QR.db.get {boardID, threadID, postID} 25 | markers.push 'You' if Conf['Mark Quotes of You'] 26 | $.addClass post.nodes.root, 'quotesYou' 27 | if Conf['Double Beep'] 28 | QuoteMarkers.beep = true 29 | 30 | if board.ID is boardID 31 | if Conf['Mark OP Quotes'] and thread.ID is postID 32 | markers.push 'OP' 33 | 34 | if Conf['Mark Cross-thread Quotes'] and (threadID and threadID isnt thread.ID) # threadID is 0 for deadlinks 35 | markers.push 'Cross-thread' 36 | 37 | if $.hasClass quotelink, 'deadlink' 38 | markers.push 'Dead' 39 | 40 | text = if customText 41 | customText 42 | else if boardID is post.board.ID 43 | ">>#{postID}" 44 | else 45 | ">>>/#{boardID}/#{postID}" 46 | if markers.length 47 | quotelink.textContent = "#{text}\u00A0(#{markers.join '|'})" 48 | else if mayReset 49 | quotelink.textContent = text 50 | 51 | cb: 52 | seek: (type) -> 53 | if Conf['Mark Quotes of You'] and post = QuoteMarkers.cb.findPost type 54 | QuoteMarkers.cb.scroll post 55 | 56 | findPost: (type) -> 57 | posts = $$ '.quotesYou' 58 | unless QuoteMarkers.lastRead 59 | unless post = QuoteMarkers.lastRead = posts[0] 60 | new Notice 'warning', 'No posts are currently quoting you, loser.', 20 61 | return 62 | unless Get.postFromRoot(post).isHidden 63 | return post 64 | else 65 | post = QuoteMarkers.lastRead 66 | 67 | len = posts.length - 1 68 | index = i = posts.indexOf post 69 | while true 70 | break if index is ( 71 | i = if type is 'prev' 72 | if i is 0 73 | len 74 | else 75 | i - 1 76 | else if i is len 77 | 0 78 | else 79 | i + 1 80 | ) 81 | post = posts[i] 82 | return post unless Get.postFromRoot(post).isHidden 83 | 84 | scroll: (post) -> 85 | $.rmClass highlight, 'highlight' if highlight = $ '.highlight' 86 | QuoteMarkers.lastRead = post 87 | window.location.hash = "##{post.id}" 88 | Header.scrollTo post 89 | $.addClass $('.post', post), 'highlight' 90 | 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | /* 2 | * appchan x - Version 2.10.15 - 2016-02-27 3 | * 4 | * Licensed under the MIT license. 5 | * https://github.com/zixaphir/appchan-x/blob/master/LICENSE 6 | * 7 | * Appchan X Copyright © 2013-2016 Zixaphir 8 | * http://zixaphir.github.io/appchan-x/ 9 | * 4chan x Copyright © 2009-2011 James Campos 10 | * https://github.com/aeosynth/4chan-x 11 | * 4chan x Copyright © 2012-2016 Nicolas Stepien 12 | * https://4chan-x.just-believe.in/ 13 | * 4chan x Copyright © 2013-2016 Jordan Bates 14 | * http://seaweedchan.github.io/4chan-x/ 15 | * 4chan x Copyright © 2012-2016 ihavenoface 16 | * http://ihavenoface.github.io/4chan-x/ 17 | * 4chan SS Copyright © 2011-2013 Ahodesuka 18 | * https://github.com/ahodesuka/4chan-Style-Script/ 19 | * 20 | * Permission is hereby granted, free of charge, to any person 21 | * obtaining a copy of this software and associated documentation 22 | * files (the "Software"), to deal in the Software without 23 | * restriction, including without limitation the rights to use, 24 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | * copies of the Software, and to permit persons to whom the 26 | * Software is furnished to do so, subject to the following 27 | * conditions: 28 | * 29 | * The above copyright notice and this permission notice shall be 30 | * included in all copies or substantial portions of the Software. 31 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 32 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 33 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 34 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 35 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 36 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 37 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 38 | * OTHER DEALINGS IN THE SOFTWARE. 39 | * 40 | * Contributors: 41 | * aeosynth 42 | * mayhemydg 43 | * noface 44 | * !K.WeEabo0o 45 | * blaise 46 | * that4chanwolf 47 | * desuwa 48 | * seaweed 49 | * e000 50 | * ahodesuka 51 | * Shou 52 | * ferongr 53 | * xat 54 | * Ongpot 55 | * thisisanon 56 | * Anonymous 57 | * Seiba 58 | * herpaderpderp 59 | * WakiMiko 60 | * btmcsweeney 61 | * AppleBloom 62 | * detharonil 63 | * 64 | * All the people who've taken the time to write bug reports. 65 | * 66 | * Thank you. 67 | */ 68 | 69 | /* 70 | * Contains data from external sources: 71 | * 72 | * audio/beep.wav from http://freesound.org/people/pierrecartoons1979/sounds/90112/ 73 | * cc-by-nc-3.0 74 | * 75 | * 4chan/4chan-JS (https://github.com/4chan/4chan-JS) 76 | * Copyright (c) 2012-2013, 4chan LLC 77 | * All rights reserved. 78 | * 79 | * license: https://github.com/4chan/4chan-JS/blob/master/LICENSE 80 | * 81 | * jsColor: (http://jscolor.com/) 82 | * Copyright (c) Jan Odvarko, http://odvarko.cz 83 | * 84 | * license: http://www.gnu.org/copyleft/lesser.html 85 | * 86 | */ -------------------------------------------------------------------------------- /src/Quotelinks/QuoteBacklink.coffee: -------------------------------------------------------------------------------- 1 | QuoteBacklink = 2 | # Backlinks appending need to work for: 3 | # - previous, same, and following posts. 4 | # - existing and yet-to-exist posts. 5 | # - newly fetched posts. 6 | # - clones. 7 | # XXX what about order for fetched posts? 8 | # 9 | # First callback creates a map of quoted -> [quoters], 10 | # and append backlinks to posts that already have containers. 11 | # Second callback creates, fill and append containers. 12 | init: -> 13 | return if !Conf['Quote Backlinks'] 14 | 15 | @frag = $.nodes [$.tn(' '), $.el 'a', className: 'backlink'] 16 | @map = {} 17 | 18 | Post.callbacks.push 19 | name: 'Quote Backlinking Part 1' 20 | cb: @firstNode 21 | 22 | Post.callbacks.push 23 | name: 'Quote Backlinking Part 2' 24 | cb: @secondNode 25 | 26 | firstNode: -> 27 | return if @isClone 28 | addNodes = (post, that) -> 29 | $.add post.nodes.backlinkContainer, QuoteBacklink.buildBacklink post, that 30 | for quoteID in @quotes 31 | (QuoteBacklink.map[quoteID] or= []).push @fullID 32 | continue unless (post = g.posts[quoteID])? and container = post.nodes.backlinkContainer 33 | addNodes post, @ 34 | for post in post.clones 35 | addNodes post, @ 36 | return 37 | 38 | secondNode: -> 39 | # Don't backlink the OP. 40 | return unless @isReply or Conf['OP Backlinks'] 41 | if @isClone 42 | @nodes.backlinkContainer = $ '.backlink-container', @nodes.info 43 | for backlink in @nodes.backlinks 44 | QuoteMarkers.parseQuotelink @, backlink, true, Conf['backlink'].replace(/%id/g, Get.postDataFromLink(backlink).postID) 45 | return 46 | @nodes.backlinkContainer = container = $.el 'span', 47 | className: 'backlink-container' 48 | if map = QuoteBacklink.map[@fullID] 49 | for quoteID in map when post = g.posts[quoteID] # Post hasn't been collected since. 50 | $.add container, QuoteBacklink.buildBacklink @, post 51 | $.add @nodes.info, container 52 | 53 | buildBacklink: (quoted, quoter) -> 54 | $.addClass quoted.nodes.post, 'quoted' 55 | frag = QuoteBacklink.frag.cloneNode true 56 | a = frag.querySelector "a:last-of-type" 57 | a.href = Build.path quoter.board.ID, quoter.thread.ID, quoter.ID 58 | a.textContent = text = Conf['backlink'].replace /%id/g, quoter.ID 59 | if quoter.isDead 60 | $.addClass a, 'deadlink' 61 | if quoter.isHidden 62 | $.addClass a, 'filtered' 63 | QuoteMarkers.parseQuotelink quoted, a, false, text 64 | if Conf['Quote Previewing'] 65 | $.on a, 'mouseover', QuotePreview.mouseover 66 | if Conf['Quote Inlining'] 67 | $.on a, 'click', QuoteInline.toggle 68 | if Conf['Quote Hash Navigation'] 69 | hash = QuoteInline.qiQuote a, quoter.isHidden 70 | if Conf['JSON Navigation'] 71 | if hash 72 | Navigate.singleQuoteLink hash 73 | else unless Conf['Quote Inlining'] 74 | Navigate.singleQuoteLink a 75 | frag 76 | -------------------------------------------------------------------------------- /src/Theming/Rice.coffee: -------------------------------------------------------------------------------- 1 | Rice = 2 | ul: $.el 'ul', id: "selectrice" 3 | init: -> 4 | $.ready @initReady 5 | 6 | Post.callbacks.push 7 | name: 'Rice Checkboxes' 8 | cb: @node 9 | 10 | initReady: -> 11 | Rice.nodes d.body 12 | $.add d.body, Rice.ul 13 | 14 | node: -> 15 | Rice.checkbox $ '.postInfo input', @nodes.post 16 | 17 | nodes: (root) -> 18 | root or= d.body 19 | {process} = Rice 20 | process $$('[type=checkbox]:not(.riced)', root), 'checkbox' 21 | process $$('select:not(.riced)', root), 'select' 22 | 23 | process: (items, type) -> 24 | fn = Rice[type] 25 | fn item for item in items 26 | return 27 | 28 | cleanup: -> 29 | $.off d, 'click scroll blur resize', Rice.cleanup 30 | $.rmAll Rice.ul 31 | return 32 | 33 | checkbox: (input) -> 34 | return if $.hasClass input, 'riced' 35 | $.addClass input, 'riced' 36 | div = $.el 'div', className: 'rice' 37 | div.check = input 38 | $.after input, div 39 | $.on div, 'click', Rice.cb.check 40 | 41 | select: (select) -> 42 | div = $.el 'div', 43 | className: 'selectrice' 44 | innerHTML: "
#{select.options[select.selectedIndex or '0']?.textContent or ''}
" 45 | $.on div, 'click', Rice.cb.select 46 | $.on div, 'keydown', Rice.cb.keybind 47 | 48 | $.after select, div 49 | $.addClass select, 'riced' 50 | 51 | cb: 52 | check: (e)-> 53 | e.preventDefault() 54 | e.stopPropagation() 55 | @check.click() 56 | 57 | option: (e) -> 58 | e.stopPropagation() 59 | e.preventDefault() 60 | 61 | return if @dataset.disabled 62 | 63 | select = Rice.input 64 | container = select.nextElementSibling 65 | 66 | container.firstChild.textContent = @textContent 67 | select.value = @dataset.value 68 | 69 | $.event 'change', null, select 70 | Rice.cleanup() 71 | 72 | select: (e) -> 73 | e.stopPropagation() 74 | e.preventDefault() 75 | 76 | {ul} = Rice 77 | 78 | if ul.children.length > 0 79 | return Rice.cleanup() 80 | 81 | {width, left, bottom, top} = @getBoundingClientRect() 82 | {clientHeight} = d.documentElement 83 | {style} = ul 84 | 85 | style.cssText = "width: #{width}px; left: #{left}px;" + (if clientHeight - bottom < 200 then "bottom: #{clientHeight - top}px" else "top: #{bottom}px") 86 | Rice.input = select = @previousElementSibling 87 | 88 | nodes = [] 89 | for option in select.options 90 | li = $.el 'li', textContent: option.textContent 91 | li.dataset.value = option.value 92 | if option.disabled 93 | li.dataset.disabled = true 94 | $.on li, 'click', Rice.cb.option 95 | nodes.push li 96 | 97 | $.add ul, nodes 98 | 99 | $.on ul, 'click scroll blur', (e) -> 100 | e.stopPropagation() 101 | 102 | $.on d, 'click scroll blur resize', Rice.cleanup 103 | -------------------------------------------------------------------------------- /src/General/meta/banner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * <%= meta.name %> - Version <%= meta.version %> - <%= grunt.template.today('yyyy-mm-dd') %> 3 | * 4 | * Licensed under the MIT license. 5 | * <%= meta.repo %>blob/master/LICENSE 6 | * 7 | * Appchan X Copyright © 2013-<%= grunt.template.today('yyyy') %> Zixaphir 8 | * http://zixaphir.github.io/appchan-x/ 9 | * 4chan x Copyright © 2009-2011 James Campos 10 | * https://github.com/aeosynth/4chan-x 11 | * 4chan x Copyright © 2012-<%= grunt.template.today('yyyy') %> Nicolas Stepien 12 | * https://4chan-x.just-believe.in/ 13 | * 4chan x Copyright © 2013-<%= grunt.template.today('yyyy') %> Jordan Bates 14 | * http://seaweedchan.github.io/4chan-x/ 15 | * 4chan x Copyright © 2012-<%= grunt.template.today('yyyy') %> ihavenoface 16 | * http://ihavenoface.github.io/4chan-x/ 17 | * 4chan SS Copyright © 2011-2013 Ahodesuka 18 | * https://github.com/ahodesuka/4chan-Style-Script/ 19 | * 20 | * Permission is hereby granted, free of charge, to any person 21 | * obtaining a copy of this software and associated documentation 22 | * files (the "Software"), to deal in the Software without 23 | * restriction, including without limitation the rights to use, 24 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | * copies of the Software, and to permit persons to whom the 26 | * Software is furnished to do so, subject to the following 27 | * conditions: 28 | * 29 | * The above copyright notice and this permission notice shall be 30 | * included in all copies or substantial portions of the Software. 31 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 32 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 33 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 34 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 35 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 36 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 37 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 38 | * OTHER DEALINGS IN THE SOFTWARE. 39 | * 40 | * Contributors: 41 | * aeosynth 42 | * mayhemydg 43 | * noface 44 | * !K.WeEabo0o 45 | * blaise 46 | * that4chanwolf 47 | * desuwa 48 | * seaweed 49 | * e000 50 | * ahodesuka 51 | * Shou 52 | * ferongr 53 | * xat 54 | * Ongpot 55 | * thisisanon 56 | * Anonymous 57 | * Seiba 58 | * herpaderpderp 59 | * WakiMiko 60 | * btmcsweeney 61 | * AppleBloom 62 | * detharonil 63 | * 64 | * All the people who've taken the time to write bug reports. 65 | * 66 | * Thank you. 67 | */ 68 | 69 | /* 70 | * Contains data from external sources: 71 | * 72 | * audio/beep.wav from http://freesound.org/people/pierrecartoons1979/sounds/90112/ 73 | * cc-by-nc-3.0 74 | * 75 | * 4chan/4chan-JS (https://github.com/4chan/4chan-JS) 76 | * Copyright (c) 2012-2013, 4chan LLC 77 | * All rights reserved. 78 | * 79 | * license: https://github.com/4chan/4chan-JS/blob/master/LICENSE 80 | * 81 | * jsColor: (http://jscolor.com/) 82 | * Copyright (c) Jan Odvarko, http://odvarko.cz 83 | * 84 | * license: http://www.gnu.org/copyleft/lesser.html 85 | * 86 | */ -------------------------------------------------------------------------------- /src/Miscellaneous/Banner.coffee: -------------------------------------------------------------------------------- 1 | Banner = 2 | banners: `<%= JSON.stringify(grunt.file.readJSON('src/Miscellaneous/banners.json')) %>` 3 | 4 | init: -> 5 | $.asap (-> d.body), -> 6 | $.asap (-> $ 'hr'), Banner.ready 7 | 8 | ready: -> 9 | banner = $ ".boardBanner" 10 | title = $.el "div", 11 | id: "boardTitle" 12 | {children} = banner 13 | nodes = [] 14 | 15 | if g.BOARD.ID isnt 'f' and g.VIEW is 'thread' and Conf['Remove Thread Excerpt'] 16 | Banner.setTitle children[1].textContent 17 | 18 | for child, i in children 19 | if i is 0 20 | $.rm child 21 | img = $.el 'img', 22 | alt: '4chan' 23 | title: 'Click to change' 24 | 25 | $.on img, 'click', Banner.cb.toggle 26 | Banner.cb.toggle.call img 27 | 28 | $.prepend banner, img 29 | 30 | continue 31 | 32 | if Conf['Custom Board Titles'] 33 | Banner.custom(child).title = "Ctrl/\u2318+click to edit board #{if i is 2 34 | 'sub' 35 | else 36 | ''}title" 37 | child.spellcheck = false 38 | 39 | nodes.push child 40 | 41 | $.add title, nodes 42 | $.after banner, title 43 | return 44 | 45 | setTitle: (title) -> 46 | if Unread.title? 47 | Unread.title = title 48 | Unread.update() 49 | else 50 | d.title = title 51 | 52 | cb: 53 | toggle: -> 54 | unless Banner.choices?.length 55 | Banner.choices = Banner.banners.slice() 56 | i = Math.floor(Banner.choices.length * Math.random()) 57 | banner = Banner.choices.splice i, 1 58 | @src = "//s.4cdn.org/image/title/#{banner}" 59 | 60 | click: (e) -> 61 | if e.ctrlKey or e.metaKey 62 | @contentEditable = true 63 | @focus() 64 | 65 | keydown: (e) -> 66 | e.stopPropagation() 67 | return @blur() if !e.shiftKey and e.keyCode is 13 68 | 69 | focus: -> 70 | string = "#{g.BOARD}.#{@className}" 71 | string2 = "#{string}.orig" 72 | 73 | items = {title: @textContent} 74 | items[string] = '' 75 | items[string2] = false 76 | 77 | $.get items, (items) -> 78 | unless items[string2] and items.title is items[string] 79 | $.set string2, items.title 80 | 81 | return 82 | 83 | blur: -> 84 | @contentEditable = false 85 | $.set "#{g.BOARD}.#{@className}", @textContent 86 | 87 | custom: (child) -> 88 | cachedTest = child.textContent 89 | string = "#{g.BOARD}.#{child.className}" 90 | 91 | $.on child, 'click keydown focus blur', (e) -> Banner.cb[e.type].apply @, [e] 92 | 93 | $.get string, cachedTest, (item) -> 94 | return unless title = item[string] 95 | return child.textContent = title if Conf['Persistent Custom Board Titles'] 96 | 97 | string2 = "#{string}.orig" 98 | 99 | $.get string2, cachedTest, (itemb) -> 100 | if cachedTest is itemb[string2] 101 | child.textContent = title 102 | else 103 | $.set string, cachedTest 104 | $.set string2, cachedTest 105 | child 106 | -------------------------------------------------------------------------------- /src/Images/ImageCommon.coffee: -------------------------------------------------------------------------------- 1 | ImageCommon = 2 | rewind: (el) -> 3 | if el.nodeName is 'VIDEO' 4 | el.currentTime = 0 if el.readyState >= el.HAVE_METADATA 5 | else if /\.gif$/.test el.src 6 | $.queueTask -> el.src = el.src 7 | 8 | pushCache: (el) -> 9 | ImageCommon.cache = el 10 | $.on el, 'error', ImageCommon.cacheError 11 | 12 | popCache: -> 13 | el = ImageCommon.cache 14 | $.off el, 'error', ImageCommon.cacheError 15 | delete ImageCommon.cache 16 | el 17 | 18 | cacheError: -> 19 | delete ImageCommon.cache if ImageCommon.cache is @ 20 | 21 | decodeError: (file, post) -> 22 | return false unless file.error?.code is MediaError.MEDIA_ERR_DECODE 23 | unless message = $ '.warning', post.file.thumb.parentNode 24 | message = $.el 'div', className: 'warning' 25 | $.after post.file.thumb, message 26 | message.textContent = 'Error: Corrupt or unplayable video' 27 | return true 28 | 29 | error: (file, post, delay, cb) -> 30 | src = post.file.URL.split '/' 31 | URL = Redirect.to 'file', 32 | boardID: post.board.ID 33 | filename: src[src.length - 1] 34 | unless Conf['404 Redirect'] and URL and Redirect.securityCheck URL 35 | URL = null 36 | 37 | return cb URL if (post.isDead or post.file.isDead) and file.src.split('/')[2] is 'i.4cdn.org' 38 | 39 | timeoutID = setTimeout (-> cb URL), delay if delay? 40 | return if post.isDead or post.file.isDead 41 | redirect = -> 42 | if file.src.split('/')[2] is 'i.4cdn.org' 43 | clearTimeout timeoutID if delay? 44 | cb URL 45 | 46 | <% if (type === 'crx') { %> 47 | $.ajax post.file.URL, 48 | onloadend: -> 49 | if @status is 200 50 | URL = post.file.URL 51 | else 52 | post.kill true if @status is 404 53 | redirect() 54 | , 55 | type: 'head' 56 | <% } else { %> 57 | # XXX CORS for i.4cdn.org WHEN? 58 | $.ajax "//a.4cdn.org/#{post.board}/thread/#{post.thread}.json", onload: -> 59 | post.kill() if @status is 404 60 | return redirect() if @status isnt 200 61 | for postObj in @response.posts 62 | break if postObj.no is post.ID 63 | if postObj.no isnt post.ID 64 | post.kill() 65 | redirect() 66 | else if postObj.filedeleted 67 | post.kill true 68 | redirect() 69 | else 70 | URL = post.file.URL 71 | <% } %> 72 | 73 | # Add controls, but not until the mouse is moved over the video. 74 | addControls: (video) -> 75 | handler = -> 76 | $.off video, 'mouseover', handler 77 | # Hacky workaround for Firefox forever-loading bug for very short videos 78 | t = new Date().getTime() 79 | $.asap (-> chrome? or (video.readyState >= 3 and video.currentTime <= Math.max 0.1, (video.duration - 0.5)) or new Date().getTime() >= t + 1000), -> 80 | video.controls = true 81 | $.on video, 'mouseover', handler 82 | 83 | download: (e) -> 84 | return true if @protocol is 'blob:' 85 | e.preventDefault() 86 | CrossOrigin.file @href, (blob) => 87 | if blob 88 | @href = URL.createObjectURL blob 89 | @click() 90 | else 91 | new Notice 'error', "Could not download #{@href}", 30 92 | -------------------------------------------------------------------------------- /src/General/lib/databoard.class: -------------------------------------------------------------------------------- 1 | class DataBoard 2 | @keys = ['pinnedThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads'] 3 | 4 | constructor: (@key, sync, dontClean) -> 5 | @data = Conf[key] 6 | $.sync key, @onSync 7 | @clean() unless dontClean 8 | return unless sync 9 | # Chrome also fires the onChanged callback on the current tab, 10 | # so we only start syncing when we're ready. 11 | init = => 12 | $.off d, '4chanXInitFinished', init 13 | @sync = sync 14 | $.on d, '4chanXInitFinished', init 15 | 16 | save: -> $.set @key, @data 17 | 18 | delete: ({boardID, threadID, postID}) -> 19 | $.forceSync @key 20 | if postID 21 | return unless @data.boards[boardID]?[threadID] 22 | delete @data.boards[boardID][threadID][postID] 23 | @deleteIfEmpty {boardID, threadID} 24 | else if threadID 25 | return unless @data.boards[boardID] 26 | delete @data.boards[boardID][threadID] 27 | @deleteIfEmpty {boardID} 28 | else 29 | delete @data.boards[boardID] 30 | @save() 31 | 32 | deleteIfEmpty: ({boardID, threadID}) -> 33 | $.forceSync @key 34 | if threadID 35 | unless Object.keys(@data.boards[boardID][threadID]).length 36 | delete @data.boards[boardID][threadID] 37 | @deleteIfEmpty {boardID} 38 | else unless Object.keys(@data.boards[boardID]).length 39 | delete @data.boards[boardID] 40 | 41 | set: ({boardID, threadID, postID, val}) -> 42 | $.forceSync @key 43 | if postID isnt undefined 44 | ((@data.boards[boardID] or= {})[threadID] or= {})[postID] = val 45 | else if threadID isnt undefined 46 | (@data.boards[boardID] or= {})[threadID] = val 47 | else 48 | @data.boards[boardID] = val 49 | @save() 50 | 51 | get: ({boardID, threadID, postID, defaultValue}) -> 52 | if board = @data.boards[boardID] 53 | unless threadID 54 | if postID 55 | for ID, thread in board 56 | if postID of thread 57 | val = thread[postID] 58 | break 59 | else 60 | val = board 61 | else if thread = board[threadID] 62 | val = if postID 63 | thread[postID] 64 | else 65 | thread 66 | val or defaultValue 67 | 68 | forceSync: -> 69 | $.forceSync @key 70 | 71 | clean: -> 72 | $.forceSync @key 73 | for boardID, val of @data.boards 74 | @deleteIfEmpty {boardID} 75 | 76 | now = Date.now() 77 | if (@data.lastChecked or 0) < now - 2 * $.HOUR 78 | @data.lastChecked = now 79 | for boardID of @data.boards 80 | for threadID of @data.boards[boardID] 81 | @ajaxClean boardID, threadID 82 | @save() 83 | 84 | ajaxClean: (boardID, threadID) -> 85 | $.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json", 86 | onloadend: (e) => 87 | if e.target.status is 404 88 | @delete {boardID, threadID} 89 | , 90 | type: 'head' 91 | 92 | onSync: (data) => 93 | data or= boards: {} 94 | $.extend @data, data 95 | @sync?() 96 | 97 | disconnect: -> 98 | $.desync @key 99 | delete @sync 100 | delete @data 101 | -------------------------------------------------------------------------------- /src/Quotelinks/Quotify.coffee: -------------------------------------------------------------------------------- 1 | Quotify = 2 | init: -> 3 | return if g.VIEW not in ['index', 'thread'] or !Conf['Resurrect Quotes'] 4 | 5 | if Conf['Comment Expansion'] 6 | ExpandComment.callbacks.push @node 7 | 8 | Post.callbacks.push 9 | name: 'Resurrect Quotes' 10 | cb: @node 11 | 12 | node: -> 13 | for deadlink in $$ '.deadlink', @nodes.comment 14 | if @isClone 15 | if $.hasClass deadlink, 'quotelink' 16 | @nodes.quotelinks.push deadlink 17 | else 18 | Quotify.parseDeadlink.call @, deadlink 19 | return 20 | 21 | parseDeadlink: (deadlink) -> 22 | if $.hasClass deadlink.parentNode, 'prettyprint' 23 | # Don't quotify deadlinks inside code tags, 24 | # un-`span` them. 25 | # This won't be necessary once 4chan 26 | # stops quotifying inside code tags: 27 | # https://github.com/4chan/4chan-JS/issues/77 28 | Quotify.fixDeadlink deadlink 29 | return 30 | 31 | quote = deadlink.textContent 32 | return unless postID = quote.match(/\d+$/)?[0] 33 | if postID[0] is '0' 34 | # Fix quotelinks that start with a `0`. 35 | Quotify.fixDeadlink deadlink 36 | return 37 | boardID = if m = quote.match /^>>>\/([a-z\d]+)/ 38 | m[1] 39 | else 40 | @board.ID 41 | quoteID = "#{boardID}.#{postID}" 42 | 43 | if post = g.posts[quoteID] 44 | unless post.isDead 45 | # Don't (Dead) when quotifying in an archived post, 46 | # and we know the post still exists. 47 | a = $.el 'a', 48 | href: Build.postURL boardID, post.thread.ID, postID 49 | className: 'quotelink' 50 | textContent: quote 51 | else 52 | # Replace the .deadlink span if we can redirect. 53 | a = $.el 'a', 54 | href: Build.postURL boardID, post.thread.ID, postID 55 | className: 'quotelink deadlink' 56 | target: '_blank' 57 | textContent: "#{quote}\u00A0(Dead)" 58 | $.extend a.dataset, {boardID, threadID: post.thread.ID, postID} 59 | 60 | else 61 | redirect = Redirect.to 'thread', {boardID, threadID: 0, postID} 62 | fetchable = Redirect.to 'post', {boardID, postID} 63 | if redirect or fetchable 64 | # Replace the .deadlink span if we can redirect or fetch the post. 65 | a = $.el 'a', 66 | href: redirect or 'javascript:;' 67 | className: 'deadlink' 68 | target: '_blank' 69 | textContent: "#{quote}\u00A0(Dead)" 70 | if fetchable 71 | # Make it function as a normal quote if we can fetch the post. 72 | $.addClass a, 'quotelink' 73 | $.extend a.dataset, {boardID, postID} 74 | 75 | @quotes.push quoteID unless quoteID in @quotes 76 | 77 | unless a 78 | deadlink.textContent = "#{quote}\u00A0(Dead)" 79 | return 80 | 81 | $.replace deadlink, a 82 | if $.hasClass a, 'quotelink' 83 | @nodes.quotelinks.push a 84 | 85 | fixDeadlink: (deadlink) -> 86 | if not (el = deadlink.previousSibling) or el.nodeName is 'BR' 87 | green = $.el 'span', 88 | className: 'quote' 89 | $.before deadlink, green 90 | $.add green, deadlink 91 | $.replace deadlink, [deadlink.childNodes...] 92 | -------------------------------------------------------------------------------- /src/General/lib/thread.class: -------------------------------------------------------------------------------- 1 | class Thread 2 | @callbacks = new Callbacks 'Thread' 3 | toString: -> @ID 4 | 5 | constructor: (@ID, @board) -> 6 | @fullID = "#{@board}.#{@ID}" 7 | @posts = new SimpleDict() 8 | @isDead = false 9 | @isHidden = false 10 | @isOnTop = false 11 | @isPinned = false 12 | @isSticky = false 13 | @isClosed = false 14 | @isArchived = false 15 | @postLimit = false 16 | @fileLimit = false 17 | @ipCount = undefined 18 | 19 | @OP = null 20 | @catalogView = null 21 | 22 | g.threads.push @fullID, board.threads.push @, @ 23 | 24 | setPage: (pageNum) -> 25 | {info, quote} = @OP.nodes 26 | unless icon = $ '.page-num', info 27 | icon = $.el 'span', className: 'page-num' 28 | $.after quote, [$.tn(' '), icon] 29 | icon.title = "This thread is on page #{pageNum} in the original index." 30 | icon.textContent = "[#{pageNum}]" 31 | @catalogView.nodes.pageCount.textContent = pageNum if @catalogView 32 | 33 | setCount: (type, count, reachedLimit) -> 34 | return unless @catalogView 35 | el = @catalogView.nodes["#{type}Count"] 36 | el.textContent = count 37 | (if reachedLimit then $.addClass else $.rmClass) el, 'warning' 38 | 39 | setStatus: (type, status) -> 40 | name = "is#{type}" 41 | return if @[name] is status 42 | @[name] = status 43 | return unless @OP 44 | 45 | @setIcon 'Sticky', @isSticky 46 | @setIcon 'Closed', @isClosed and !@isArchived 47 | @setIcon 'Archived', @isArchived 48 | 49 | setIcon: (type, status) -> 50 | typeLC = type.toLowerCase() 51 | icon = $ ".#{typeLC}Icon", @OP.nodes.info 52 | return if !!icon is status 53 | 54 | unless status 55 | $.rm icon.previousSibling 56 | $.rm icon 57 | $.rm $ ".#{typeLC}Icon", @catalogView.nodes.icons if @catalogView 58 | return 59 | icon = $.el 'img', 60 | src: "#{Build.staticPath}#{typeLC}#{Build.gifIcon}" 61 | alt: type 62 | title: type 63 | className: "#{typeLC}Icon retina" 64 | 65 | root = if type isnt 'Sticky' and @isSticky 66 | $ '.stickyIcon', @OP.nodes.info 67 | else 68 | $('.page-num', @OP.nodes.info) or @OP.nodes.quote 69 | $.after root, [$.tn(' '), icon] 70 | 71 | return unless @catalogView 72 | (if type is 'Sticky' and @isClosed then $.prepend else $.add) @catalogView.nodes.icons, icon.cloneNode() 73 | 74 | pin: -> 75 | @isPinned = true 76 | $.addClass @catalogView.nodes.root, 'pinned' if @catalogView 77 | 78 | unpin: -> 79 | @isPinned = false 80 | $.rmClass @catalogView.nodes.root, 'pinned' if @catalogView 81 | 82 | hide: -> 83 | return if @isHidden 84 | @isHidden = true 85 | @OP.nodes.root.parentElement.hidden = true unless Conf['JSON Navigation'] 86 | if button = $ '.hide-post-button', @OP.nodes.root 87 | $.replace button, PostHiding.makeButton false 88 | 89 | show: -> 90 | return if !@isHidden 91 | @isHidden = false 92 | if button = $ '.show-post-button', @OP.nodes.root 93 | $.replace button, PostHiding.makeButton true 94 | 95 | kill: -> 96 | @isDead = true 97 | 98 | collect: -> 99 | @posts.forEach (post) -> post.collect() 100 | g.threads.rm @fullID 101 | @board.threads.rm @ 102 | -------------------------------------------------------------------------------- /src/Images/ImageLoader.coffee: -------------------------------------------------------------------------------- 1 | ImageLoader = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] and g.BOARD.ID isnt 'f' 4 | return unless Conf['Image Prefetching'] or 5 | Conf['Replace JPG'] or Conf['Replace PNG'] or Conf['Replace GIF'] or Conf['Replace WEBM'] 6 | 7 | Post.callbacks.push 8 | name: 'Image Replace' 9 | cb: @node 10 | 11 | $.on d, 'PostsInserted', -> 12 | g.posts.forEach ImageLoader.prefetch 13 | 14 | if Conf['Replace WEBM'] 15 | $.on d, 'scroll visibilitychange 4chanXInitFinished PostsInserted', @playVideos 16 | 17 | return unless Conf['Image Prefetching'] 18 | 19 | prefetch = $.el 'label', 20 | <%= html(' Prefetch Images') %> 21 | 22 | @el = prefetch.firstElementChild 23 | $.on @el, 'change', @toggle 24 | 25 | Header.menu.addEntry 26 | el: prefetch 27 | order: 104 28 | 29 | node: -> 30 | return if @isClone or !@file 31 | ImageLoader.replaceVideo @ if Conf['Replace WEBM'] and @file.isVideo 32 | ImageLoader.prefetch @ 33 | 34 | replaceVideo: (post) -> 35 | {file} = post 36 | {thumb} = file 37 | video = $.el 'video', 38 | preload: 'none' 39 | loop: true 40 | poster: thumb.src 41 | textContent: thumb.alt 42 | className: thumb.className 43 | video.dataset.md5 = thumb.dataset.md5 44 | video.style[attr] = thumb.style[attr] for attr in ['height', 'width', 'maxHeight', 'maxWidth'] 45 | video.src = file.URL 46 | $.replace thumb, video 47 | file.thumb = video 48 | file.videoThumb = true 49 | 50 | prefetch: (post) -> 51 | {file} = post 52 | return unless file 53 | {isImage, isVideo, thumb, URL} = file 54 | return if file.isPrefetched or !(isImage or isVideo) or post.isHidden or post.thread.isHidden 55 | type = if (match = URL.match(/\.([^.]+)$/)[1].toUpperCase()) is 'JPEG' then 'JPG' else match 56 | replace = Conf["Replace #{type}"] and !/spoiler/.test thumb.src 57 | return unless replace or Conf['prefetch'] 58 | 59 | pass = false 60 | for item in [post, post.clones...] 61 | if doc.contains item.nodes.root 62 | pass = true 63 | break 64 | return unless pass 65 | 66 | file.isPrefetched = true 67 | if file.videoThumb 68 | clone.file.thumb.preload = 'auto' for clone in post.clones 69 | thumb.preload = 'auto' 70 | # XXX Cloned video elements with poster in Firefox cause momentary display of image loading icon. 71 | if !chrome? 72 | $.on thumb, 'loadeddata', -> @removeAttribute 'poster' 73 | return 74 | 75 | el = $.el if isImage then 'img' else 'video' 76 | if replace and isImage 77 | $.on el, 'load', -> 78 | clone.file.thumb.src = URL for clone in post.clones 79 | return 80 | thumb.src = URL 81 | el.src = URL 82 | 83 | toggle: -> 84 | if Conf['prefetch'] = @checked 85 | g.BOARD.posts.forEach ImageLoader.prefetch 86 | return 87 | 88 | playVideos: -> 89 | # Special case: Quote previews are off screen when inserted into document, but quickly moved on screen. 90 | qpClone = $.id('qp')?.firstElementChild 91 | g.posts.forEach (post) -> 92 | for post in [post, post.clones...] when post.file?.videoThumb 93 | {thumb} = post.file 94 | if Header.isNodeVisible(thumb) or post.nodes.root is qpClone then thumb.play() else thumb.pause() 95 | return 96 | -------------------------------------------------------------------------------- /src/Menu/DeleteLink.coffee: -------------------------------------------------------------------------------- 1 | DeleteLink = 2 | init: -> 3 | return unless g.VIEW in ['index', 'thread'] and Conf['Menu'] and Conf['Delete Link'] 4 | 5 | div = $.el 'div', 6 | className: 'delete-link' 7 | textContent: 'Delete' 8 | postEl = $.el 'a', 9 | className: 'delete-post' 10 | href: 'javascript:;' 11 | fileEl = $.el 'a', 12 | className: 'delete-file' 13 | href: 'javascript:;' 14 | 15 | postEntry = 16 | el: postEl 17 | open: -> 18 | postEl.textContent = 'Post' 19 | $.on postEl, 'click', DeleteLink.delete 20 | true 21 | fileEntry = 22 | el: fileEl 23 | open: ({file}) -> 24 | return false if !file or file.isDead 25 | fileEl.textContent = 'File' 26 | $.on fileEl, 'click', DeleteLink.delete 27 | true 28 | 29 | Menu.menu.addEntry 30 | el: div 31 | order: 40 32 | open: (post) -> 33 | return false if post.isDead 34 | DeleteLink.post = post 35 | node = div.firstChild 36 | node.textContent = 'Delete' 37 | DeleteLink.cooldown.start post, node 38 | true 39 | subEntries: [postEntry, fileEntry] 40 | 41 | delete: -> 42 | {post} = DeleteLink 43 | return if DeleteLink.cooldown.counting is post 44 | 45 | $.off @, 'click', DeleteLink.delete 46 | fileOnly = $.hasClass @, 'delete-file' 47 | @textContent = "Deleting #{if fileOnly then 'file' else 'post'}..." 48 | 49 | form = 50 | mode: 'usrdel' 51 | onlyimgdel: fileOnly 52 | pwd: QR.persona.getPassword() 53 | form[post.ID] = 'delete' 54 | 55 | link = @ 56 | $.ajax $.id('delform').action.replace("/#{g.BOARD}/", "/#{post.board}/"), 57 | responseType: 'document' 58 | withCredentials: true 59 | onload: -> DeleteLink.load link, post, fileOnly, @response 60 | onerror: -> DeleteLink.error link 61 | , 62 | form: $.formData form 63 | load: (link, post, fileOnly, resDoc) -> 64 | if resDoc.title is '4chan - Banned' # Ban/warn check 65 | s = 'Banned!' 66 | else if msg = resDoc.getElementById 'errmsg' # error! 67 | s = msg.textContent 68 | $.on link, 'click', DeleteLink.delete 69 | else 70 | if resDoc.title is 'Updating index...' 71 | # We're 100% sure. 72 | QR.cooldown.delete post 73 | (post.origin or post).kill fileOnly 74 | s = 'Deleted' 75 | link.textContent = s 76 | error: (link) -> 77 | link.textContent = 'Connection error, please retry.' 78 | $.on link, 'click', DeleteLink.delete 79 | 80 | cooldown: 81 | start: (post, node) -> 82 | unless QR.db.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID} 83 | # Only start counting on our posts. 84 | delete DeleteLink.cooldown.counting 85 | return 86 | DeleteLink.cooldown.counting = post 87 | length = 60 88 | seconds = Math.ceil (length * $.SECOND - (Date.now() - post.info.date)) / $.SECOND 89 | DeleteLink.cooldown.count post, seconds, length, node 90 | count: (post, seconds, length, node) -> 91 | return if DeleteLink.cooldown.counting isnt post 92 | unless 0 <= seconds <= length 93 | if DeleteLink.cooldown.counting is post 94 | node.textContent = 'Delete' 95 | delete DeleteLink.cooldown.counting 96 | return 97 | setTimeout DeleteLink.cooldown.count, 1000, post, seconds - 1, length, node 98 | node.textContent = "Delete (#{seconds})" 99 | -------------------------------------------------------------------------------- /src/Archive/Redirect.coffee: -------------------------------------------------------------------------------- 1 | Redirect = 2 | init: -> 3 | o = 4 | thread: {} 5 | post: {} 6 | file: {} 7 | 8 | archives = {} 9 | for data in Redirect.archives 10 | {name, boards, files, software, withCredentials} = data 11 | archives[name] = data 12 | for boardID in boards when !withCredentials 13 | o.thread[boardID] = data unless boardID of o.thread 14 | o.post[boardID] = data unless boardID of o.post or software isnt 'foolfuuka' 15 | o.file[boardID] = data unless boardID of o.file or boardID not in files 16 | 17 | for boardID, record of Conf['selectedArchives'] 18 | for type, id of record when (archive = archives[id]) 19 | boards = if type is 'file' then archive.files else archive.boards 20 | continue unless boardID in boards 21 | o[type][boardID] = archive 22 | 23 | Redirect.data = o 24 | 25 | archives: `<%= JSON.stringify(grunt.file.readJSON('src/Archive/archives.json')) %>` 26 | 27 | to: (dest, data) -> 28 | archive = (if dest in ['search', 'board'] then Redirect.data.thread else Redirect.data[dest])[data.boardID] 29 | return '' unless archive 30 | Redirect[dest] archive, data 31 | 32 | protocol: (archive) -> 33 | protocol = location.protocol 34 | unless archive[protocol[0...-1]] 35 | protocol = if protocol is 'https:' then 'http:' else 'https:' 36 | "#{protocol}//" 37 | 38 | thread: (archive, {boardID, threadID, postID}) -> 39 | # Keep the post number only if the location.hash was sent f.e. 40 | path = if threadID 41 | "#{boardID}/thread/#{threadID}" 42 | else 43 | "#{boardID}/post/#{postID}" 44 | if archive.software is 'foolfuuka' 45 | path += '/' 46 | if threadID and postID 47 | path += if archive.software is 'foolfuuka' 48 | "##{postID}" 49 | else 50 | "#p#{postID}" 51 | "#{Redirect.protocol archive}#{archive.domain}/#{path}" 52 | 53 | post: (archive, {boardID, postID}) -> 54 | # For fuuka-based archives: 55 | # https://github.com/eksopl/fuuka/issues/27 56 | protocol = Redirect.protocol archive 57 | URL = new String "#{protocol}#{archive.domain}/_/api/chan/post/?board=#{boardID}&num=#{postID}" 58 | return '' unless Redirect.securityCheck URL 59 | 60 | URL.archive = archive 61 | URL 62 | 63 | file: (archive, {boardID, filename}) -> 64 | "#{Redirect.protocol archive}#{archive.domain}/#{boardID}/full_image/#{filename}" 65 | 66 | board: (archive, {boardID}) -> 67 | "#{Redirect.protocol archive}#{archive.domain}/#{boardID}/" 68 | 69 | search: (archive, {boardID, type, value}) -> 70 | type = if type is 'name' 71 | 'username' 72 | else if type is 'uniqueID' 73 | 'uid' 74 | else if type is 'MD5' 75 | 'image' 76 | else 77 | type 78 | value = encodeURIComponent value 79 | path = if archive.software is 'foolfuuka' 80 | "#{boardID}/search/#{type}/#{value}" 81 | else 82 | "#{boardID}/?task=search2&search_#{if type is 'image' then 'media_hash' else type}=#{value}" 83 | "#{Redirect.protocol archive}#{archive.domain}/#{path}" 84 | 85 | securityCheck: (URL) -> 86 | /^https:\/\//.test(URL) or 87 | location.protocol is 'http:' or 88 | Conf['Exempt Archives from Encryption'] 89 | 90 | navigate: (URL, alternative) -> 91 | if URL and ( 92 | Redirect.securityCheck(URL) or 93 | confirm "Redirect to #{URL}?\n\nYour connection will not be encrypted." 94 | ) 95 | location.replace URL 96 | else if alternative 97 | location.replace alternative -------------------------------------------------------------------------------- /src/General/CrossOrigin.coffee: -------------------------------------------------------------------------------- 1 | CrossOrigin = do -> 2 | <% if (type === 'crx') { %> 3 | eventPageRequest = do -> 4 | callbacks = [] 5 | chrome.runtime.onMessage.addListener (data) -> 6 | callbacks[data.id] data 7 | delete callbacks[data.id] 8 | (url, responseType, cb) -> 9 | chrome.runtime.sendMessage {url, responseType}, (id) -> 10 | callbacks[id] = cb 11 | <% } %> 12 | 13 | file: do -> 14 | makeBlob = (urlBlob, contentType, contentDisposition, url) -> 15 | name = url.match(/([^\/]+)\/*$/)?[1] 16 | mime = contentType?.match(/[^;]*/)[0] or 'application/octet-stream' 17 | match = 18 | contentDisposition?.match(/\bfilename\s*=\s*"((\\"|[^"])+)"/i)?[1] or 19 | contentType?.match(/\bname\s*=\s*"((\\"|[^"])+)"/i)?[1] 20 | if match 21 | name = match.replace /\\"/g, '"' 22 | blob = new Blob([urlBlob], {type: mime}) 23 | blob.name = name 24 | blob 25 | 26 | (url, cb) -> 27 | <% if (type === 'crx') { %> 28 | if /^https:\/\//.test(url) or location.protocol is 'http:' 29 | $.ajax url, 30 | responseType: 'blob' 31 | onload: -> 32 | return cb null unless @readyState is @DONE and @status is 200 33 | contentType = @getResponseHeader 'Content-Type' 34 | contentDisposition = @getResponseHeader 'Content-Disposition' 35 | cb (makeBlob @response, contentType, contentDisposition, url) 36 | onerror: -> 37 | cb null 38 | else 39 | eventPageRequest url, 'arraybuffer', ({response, contentType, contentDisposition, error}) -> 40 | return cb null if error 41 | cb (makeBlob new Uint8Array(response), contentType, contentDisposition, url) 42 | <% } %> 43 | <% if (type === 'userscript') { %> 44 | GM_xmlhttpRequest 45 | method: "GET" 46 | url: url 47 | overrideMimeType: "text/plain; charset=x-user-defined" 48 | onload: (xhr) -> 49 | r = xhr.responseText 50 | data = new Uint8Array r.length 51 | i = 0 52 | while i < r.length 53 | data[i] = r.charCodeAt i 54 | i++ 55 | contentType = xhr.responseHeaders.match(/Content-Type:\s*(.*)/i)?[1] 56 | contentDisposition = xhr.responseHeaders.match(/Content-Disposition:\s*(.*)/i)?[1] 57 | cb (makeBlob data, contentType, contentDisposition, url) 58 | onerror: -> 59 | cb null 60 | <% } %> 61 | 62 | json: do -> 63 | callbacks = {} 64 | responses = {} 65 | (url, cb) -> 66 | <% if (type === 'crx') { %> 67 | if /^https:\/\//.test(url) or location.protocol is 'http:' 68 | return $.cache url, (-> cb @response), responseType: 'json' 69 | <% } %> 70 | if responses[url] 71 | cb responses[url] 72 | return 73 | if callbacks[url] 74 | callbacks[url].push cb 75 | return 76 | callbacks[url] = [cb] 77 | <% if (type === 'userscript') { %> 78 | GM_xmlhttpRequest 79 | method: "GET" 80 | url: url+'' 81 | onload: (xhr) -> 82 | response = JSON.parse xhr.responseText 83 | cb response for cb in callbacks[url] 84 | delete callbacks[url] 85 | responses[url] = response 86 | onerror: -> 87 | delete callbacks[url] 88 | onabort: -> 89 | delete callbacks[url] 90 | <% } %> 91 | <% if (type === 'crx') { %> 92 | eventPageRequest url, 'json', ({response, error}) -> 93 | if error 94 | delete callbacks[url] 95 | else 96 | cb response for cb in callbacks[url] 97 | delete callbacks[url] 98 | responses[url] = response 99 | <% } %> 100 | -------------------------------------------------------------------------------- /src/General/lib/captcha.class: -------------------------------------------------------------------------------- 1 | class Captcha 2 | # constructor: -> 3 | 4 | lifetime: 2 * $.MINUTE 5 | 6 | init: -> 7 | return if d.cookie.indexOf('pass_enabled=1') >= 0 8 | return unless @isEnabled = !!$ '#g-recaptcha, #captchaContainerAlt' 9 | 10 | @captchas = [] 11 | $.get 'captchas', [], ({captchas}) -> 12 | QR.captcha.sync captchas 13 | $.sync 'captchas', @sync.bind @ 14 | 15 | @impInit() 16 | 17 | # impInit: -> # Implement in instance. 18 | 19 | cb: 20 | focus: -> QR.captcha.setup false, true 21 | # Only used by v1 and noscript versions 22 | load: -> 23 | cache: -> 24 | 25 | needed: -> 26 | captchaCount = @captchas.length 27 | captchaCount++ if QR.req 28 | @postsCount = QR.posts.length 29 | if @postsCount is 1 and !Conf['Auto-load captcha'] 30 | {com, file} = QR.posts[0] 31 | @postsCount = 0 if !com and !file 32 | captchaCount < @postsCount 33 | 34 | # Used only in V2. 35 | onNewPost: -> 36 | onPostChange: -> 37 | 38 | preSetup: -> 39 | {input} = @nodes 40 | input.value = '' 41 | input.placeholder = 'Focus to load reCAPTCHA' 42 | @count() 43 | $.on input, 'focus click', @cb.focus 44 | 45 | setup: (focus, force) -> 46 | return unless @isEnabled and (@needed() or force) 47 | @impSetup focus, force 48 | 49 | # impSetup: -> # Implement in instance. 50 | # postSetup: -> # Implement in instance. 51 | # destroy: -> # Implement in instance. 52 | getOne: -> 53 | @clear() 54 | if captcha = @captchas.shift() 55 | @handleCaptcha captcha 56 | else 57 | @handleNoCaptcha() 58 | 59 | handleCaptcha: (captcha) -> 60 | # Some implementations will change this. 61 | @count() 62 | $.set 'captchas', @captchas 63 | captcha 64 | 65 | handleNoCaptcha: -> # Some implementations will change this. 66 | # save: -> # Implement in instance. 67 | # load: -> # not used in v2, implement in instace. 68 | 69 | sync: (captchas=[]) -> 70 | @captchas = captchas 71 | @count() 72 | @clear() 73 | 74 | clear: -> 75 | return false unless @captchas.length 76 | $.forceSync 'captchas' 77 | now = Date.now() 78 | for captcha, i in @captchas 79 | break if captcha.timeout > now 80 | return unless i 81 | @captchas = @captchas[i..] 82 | @count() 83 | $.set 'captchas', @captchas 84 | return true 85 | 86 | count: -> # Implement as super() or override. 87 | count = if @captchas then @captchas.length else 0 88 | placeholder = @nodes.input.placeholder.replace /\ \(.*\)$/, '' 89 | placeholder += switch count 90 | when 0 91 | if placeholder is 'Verification' then ' (Shift + Enter to cache)' else '' 92 | when 1 93 | ' (1 cached captcha)' 94 | else 95 | " (#{count} cached captchas)" 96 | @nodes.input.placeholder = placeholder 97 | @nodes.input.alt = count # For XTRM RICE. 98 | 99 | # reload: -> # Implement in instance 100 | 101 | keydown: (e) -> 102 | if e.keyCode is 8 and not @nodes.input.value 103 | @cb.load() 104 | else if e.keyCode is 13 and e.shiftKey 105 | @cb.cache() 106 | else 107 | return 108 | e.preventDefault() 109 | 110 | buildV1Nodes: -> 111 | container = $.el 'div', 112 | className: 'captcha-img' 113 | title: 'Reload reCAPTCHA' 114 | 115 | input = $.el 'input', 116 | className: 'captcha-input field' 117 | title: 'Verification' 118 | autocomplete: 'off' 119 | spellcheck: false 120 | 121 | @nodes = {container, input} 122 | 123 | $.on input, 'keydown', @keydown.bind @ 124 | $.on container, 'click', @reload.bind @ 125 | 126 | $.addClass QR.nodes.el, 'has-captcha', 'captcha-v1' 127 | $.after QR.nodes.com.parentNode, [container, input] 128 | -------------------------------------------------------------------------------- /questionable.json: -------------------------------------------------------------------------------- 1 | [{"Mascot":"Akiyama_Mio_sitting","category":"Questionable","image":"//i.minus.com/ibnnAPmolhTfE7.png"},{"Mascot":"Anime_Girl_in_Bondage","category":"Questionable","image":"//i.minus.com/ibbfIrZEoNLmiU.png","center":true},{"Mascot":"Anime_Girl_in_Bondage_2","category":"Questionable","image":"//i.minus.com/iGRED5sHh4RMs.png","center":true},{"Mascot":"Asuka_Langley_Soryu_5","category":"Questionable","image":"//i.minus.com/iJq4VXY1Gw8ZE.png","center":true},{"Mascot":"Ayase_Yue","category":"Questionable","image":"//i.minus.com/ign5fGOZWTx5o.png"},{"Mascot":"Ayase_2","category":"Questionable","image":"//i.minus.com/ibjUbDLSU5pwhK.png","center":true},{"Mascot":"Blue_Rose","category":"Questionable","image":"//i.minus.com/ibiq1joMemfzeM.png","center":true},{"Mascot":"CC2","category":"Questionable","image":"//i.minus.com/iVT3TjJ7lBRpl.png","center":true},{"Mascot":"Cirno","category":"Questionable","image":"//i.minus.com/ibffjW5v0zrSGa.png","center":true},{"Mascot":"Erio_Touwa","category":"Questionable","image":"//i.minus.com/in8bF152Y9qVB.png"},{"Mascot":"Gasai_Yuno_2","category":"Questionable","image":"//i.minus.com/ifyPk7Yeo1JA7.png"},{"Mascot":"Hatsune_Miku","category":"Questionable","image":"//i.minus.com/iHuUwYVywpp3Z.png"},{"Mascot":"Hatsune_Miku_2","category":"Questionable","image":"//i.minus.com/iclhgYeHDD77I.png","center":true},{"Mascot":"Hatsune_Miku_6","category":"Questionable","image":"//i.minus.com/iQzx9fPFgPUNl.png","center":true},{"Mascot":"Hatsune_Miku_7","category":"Questionable","image":"//i.minus.com/iDScshaEZqUuy.png","center":true},{"Mascot":"Horo_3","category":"Questionable","image":"//i.minus.com/ibyT9dlTe1HN5P.png"},{"Mascot":"Horo_4","category":"Questionable","image":"//i.minus.com/ibbMKiznORGJ00.png"},{"Mascot":"Ika_Musume_3","category":"Questionable","image":"//i.minus.com/iby8LyjXffukaI.png","center":true},{"Mascot":"Inori","category":"Questionable","image":"//i.minus.com/ibpHKNPxcFqRxs.png"},{"Mascot":"Inori_2","category":"Questionable","image":"//i.minus.com/ibzM531DBaHYXD.png"},{"Mascot":"Kagamine_Rin","category":"Questionable","image":"//i.minus.com/iVPKJeDXKPKeV.png","center":true},{"Mascot":"Kinomoto_Sakura_2","category":"Questionable","image":"//i.minus.com/ibklztjz3Ua747.png","center":true},{"Mascot":"Kirino_Kosaka_and_Ruri_Goko","category":"Questionable","image":"//i.minus.com/isIzggtfUo4ql.png","center":true},{"Mascot":"Konjiki_no_Yami","category":"Questionable","image":"//i.minus.com/imy7iv5fuym8b.png","position":"bottom"},{"Mascot":"Leonmitchelli","category":"Questionable","image":"//i.minus.com/ibgUFGlOpedfbs.png","center":true},{"Mascot":"Nagato_Yuki_4","category":"Questionable","image":"//i.minus.com/i92tUr90OVZGD.png","center":true},{"Mascot":"Nagato_Yuki_7","category":"Questionable","image":"//i.minus.com/iFQQPEaC3aEV7.png"},{"Mascot":"Nodoka_Miyazaki","category":"Questionable","image":"//i.minus.com/iDX5mImKBzrXK.png"},{"Mascot":"Pixie","category":"Questionable","image":"//i.minus.com/ipRzX1YsTyhgZ.png","center":true},{"Mascot":"Railgun","category":"Questionable","image":"//i.minus.com/iysolfmvz6WKs.png","center":true},{"Mascot":"Saber","category":"Questionable","image":"//i.minus.com/i62cv3csQaqgk.png","center":true},{"Mascot":"Sakurazaki_Setsuna","category":"Questionable","image":"//i.minus.com/iHS6559NMU1tS.png"},{"Mascot":"Seraphim","category":"Questionable","image":"//i.minus.com/ivHaKIFHRpPFP.png","center":true},{"Mascot":"Teletha_Tessa_Testarossa","category":"Questionable","image":"//i.minus.com/iQKrg7Pq7Y6Ed.png"},{"Mascot":"Rukia_Nia_and_Asa","category":"Questionable","image":"//i.minus.com/icECBJR5D5U4S.png"},{"Mascot":"Tifa","category":"Questionable","image":"//i.minus.com/inDzKQ0Wck4ef.png","center":true},{"Mascot":"Udine","category":"Questionable","image":"//i.minus.com/iiycujRmhn6QK.png","position":"bottom"},{"Mascot":"Wanwan","category":"Questionable","image":"//i.minus.com/iTdBWYMCXULLT.png","center":true},{"Mascot":"Yoko_Littner","category":"Questionable","image":"//i.minus.com/i0mtOEsBC9GlY.png"}] -------------------------------------------------------------------------------- /src/General/html/Features/Settings-section-Rice.html: -------------------------------------------------------------------------------- 1 |
2 | Custom Board Navigation is disabled. 3 |
4 |
In the following, board can translate to a board ID (a, b, etc...), the current board (current), or the Twitter link (@).
5 |
Board link: board
6 |
Archive link: board-archive
7 |
Title link: board-title
8 |
Board link (Replace with title when on that board): board-replace
9 |
Full text link: board-full
10 |
Custom text link: board-text:"VIP Board"
11 |
Index mode: board-mode:"type" where type is paged, all threads or catalog
12 |
Index sort: board-sort:"type" where type is bump order, last reply, creation date, reply count or file count
13 |
Combinations are possible: board-text:"VIP Catalog"-mode:"catalog"-sort:"creation date"
14 |
Full board list toggle: toggle-all
15 |
16 | 17 |
18 | Time Formatting is disabled. 19 |
:
20 | 21 |
Day: %a, %A, %d, %e
22 |
Month: %m, %b, %B
23 |
Year: %y, 20%y
24 |
Hour: %k, %H, %l, %I, %p, %P
25 |
Minute: %M
26 |
Second: %S
27 |
28 | 29 |
30 | Quote Backlinks formatting is disabled. 31 |
:
32 |
33 | 34 |
35 | File Info Formatting is disabled. 36 |
:
37 |
Link: %l (truncated), %L (untruncated), %T (Unix timestamp)
38 |
Original file name: %n (truncated), %N (untruncated), %t (Unix timestamp)
39 |
Spoiler indicator: %p
40 |
Size: %B (Bytes), %K (KB), %M (MB), %s (4chan default)
41 |
Resolution: %r (Displays 'PDF' for PDF files)
42 |
43 | 44 |
45 | Unread Tab Icon is disabled. 46 | 52 | 53 |
54 | 55 |
56 | 57 | 58 | 59 | 60 | 61 |
62 | -------------------------------------------------------------------------------- /src/Posting/QR.persona.coffee: -------------------------------------------------------------------------------- 1 | QR.persona = 2 | pwd: '' 3 | always: {} 4 | init: -> 5 | QR.persona.getPassword() 6 | $.get 'QR.personas', Conf['QR.personas'], ({'QR.personas': personas}) -> 7 | types = 8 | name: [] 9 | email: [] 10 | sub: [] 11 | for item in personas.split '\n' 12 | QR.persona.parseItem item.trim(), types 13 | for type, arr of types 14 | QR.persona.loadPersonas type, arr 15 | return 16 | 17 | parseItem: (item, types) -> 18 | return if item[0] is '#' 19 | return unless match = item.match /(name|options|email|subject|password):"(.*)"/i 20 | [match, type, val] = match 21 | 22 | thread = g.threads["#{g.BOARD}.#{g.THREADID}"] 23 | 24 | # Don't mix up item settings with val. 25 | item = item.replace match, '' 26 | 27 | boards = item.match(/boards:([^;]+)/i)?[1].toLowerCase() or 'global' 28 | return if boards isnt 'global' and g.BOARD.ID not in boards.split ',' 29 | 30 | # Thread-specific rules (for anonymous shitposting) 31 | # Matches on subject 32 | threads = item.match(/threads:([^;]+)/i)?[1] or 'any' 33 | if threads isnt 'any' and g.VIEW is 'thread' 34 | #console.log("threads: directive found: #{threads}") 35 | #console.log(thread) 36 | found = false 37 | threadRegex = null 38 | for origthreadRegex in threads.split ',' 39 | if origthreadRegex[0] == '/' 40 | try 41 | # Please, don't write silly regular expressions. 42 | threadRegex = RegExp origthreadRegex 43 | catch err 44 | # I warned you, bro. 45 | new Notice 'warning', [ 46 | $.tn "Invalid regular expression filter: " + origthreadRegex, 47 | $.el 'br' 48 | $.tn err.message 49 | ], 60 50 | continue 51 | if threadRegex.test thread.OP.info.subject 52 | found = true 53 | break 54 | #console.log("Found regex #{origthreadRegex} in #{thread.OP.info.subject}!") 55 | else 56 | #console.log("Did not find regex #{origthreadRegex} in #{thread.OP.info.subject}.") 57 | else 58 | if origthreadRegex in thread.OP.info.subject 59 | found = true 60 | break 61 | #console.log("Found string #{origthreadRegex} in #{thread.OP.info.subject}!") 62 | else 63 | #console.log("Did not find string #{origthreadRegex} in #{thread.OP.info.subject}.") 64 | return if not found 65 | 66 | 67 | if type is 'password' 68 | QR.persona.pwd = val 69 | return 70 | 71 | type = 'email' if type is 'options' 72 | type = 'sub' if type is 'subject' 73 | 74 | if /always/i.test item 75 | QR.persona.always[type] = val 76 | 77 | unless val in types[type] 78 | types[type].push val 79 | 80 | loadPersonas: (type, arr) -> 81 | list = $ "#list-#{type}", QR.nodes.el 82 | for val in arr when val 83 | $.add list, $.el 'option', 84 | textContent: val 85 | return 86 | 87 | getPassword: -> 88 | unless QR.persona.pwd 89 | QR.persona.pwd = if m = d.cookie.match /4chan_pass=([^;]+)/ 90 | decodeURIComponent m[1] 91 | else if input = $.id 'postPassword' 92 | input.value 93 | else 94 | # If we're in a closed thread, #postPassword isn't available. 95 | # And since #delPassword.value is only filled on window.onload 96 | # we'd rather use #postPassword when we can. 97 | $.id('delPassword')?.value or '' 98 | return QR.persona.pwd 99 | 100 | get: (cb) -> 101 | $.get 'QR.persona', {}, ({'QR.persona': persona}) -> 102 | cb persona 103 | 104 | set: (post) -> 105 | $.get 'QR.persona', {}, ({'QR.persona': persona}) -> 106 | persona = 107 | name: post.name 108 | email: if /^sage$/.test post.email then persona.email else post.email 109 | flag: post.flag 110 | $.set 'QR.persona', persona 111 | -------------------------------------------------------------------------------- /src/Quotelinks/QuoteInline.coffee: -------------------------------------------------------------------------------- 1 | QuoteInline = 2 | init: -> 3 | return if !Conf['Quote Inlining'] 4 | 5 | @process = if Conf['Quote Hash Navigation'] 6 | (link, clone) -> 7 | $.on link, 'click', QuoteInline.toggle 8 | return if clone 9 | QuoteInline.qiQuote link, $.hasClass link, 'filtered' 10 | 11 | else 12 | (link) -> 13 | $.on link, 'click', QuoteInline.toggle 14 | 15 | if Conf['Comment Expansion'] 16 | ExpandComment.callbacks.push @node 17 | 18 | Post.callbacks.push 19 | name: 'Quote Inlining' 20 | cb: @node 21 | 22 | node: -> 23 | {process} = QuoteInline 24 | {isClone} = @ 25 | process link, isClone for link in @nodes.quotelinks 26 | process link, isClone for link in @nodes.backlinks 27 | return 28 | 29 | qiQuote: (link, hidden) -> 30 | name = "hashlink" 31 | name += " filtered" if hidden 32 | $.after link, $.el 'a', 33 | className: name 34 | textContent: '#' 35 | href: link.href 36 | 37 | toggle: (e) -> 38 | return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 39 | e.preventDefault() 40 | {boardID, threadID, postID} = Get.postDataFromLink @ 41 | context = Get.contextFromNode @ 42 | if $.hasClass @, 'inlined' 43 | QuoteInline.rm @, boardID, threadID, postID, context 44 | else 45 | return if $.x "ancestor::div[@id='pc#{postID}']", @ 46 | QuoteInline.add @, boardID, threadID, postID, context 47 | @classList.toggle 'inlined' 48 | 49 | findRoot: (quotelink, isBacklink) -> 50 | if isBacklink 51 | quotelink.parentNode.parentNode 52 | else 53 | $.x 'ancestor-or-self::*[parent::blockquote][1]', quotelink 54 | 55 | add: (quotelink, boardID, threadID, postID, context) -> 56 | isBacklink = $.hasClass quotelink, 'backlink' 57 | inline = $.el 'div', 58 | id: "i#{postID}" 59 | className: 'inline' 60 | root = QuoteInline.findRoot(quotelink, isBacklink) 61 | $.after root, inline 62 | 63 | qroot = $.x 'ancestor::*[contains(@class,"postContainer")][1]', root 64 | 65 | $.addClass qroot, 'hasInline' 66 | Get.postClone boardID, threadID, postID, inline, context 67 | 68 | return unless (post = g.posts["#{boardID}.#{postID}"]) and 69 | context.thread is post.thread 70 | 71 | # Hide forward post if it's a backlink of a post in this thread. 72 | # Will only unhide if there's no inlined backlinks of it anymore. 73 | if isBacklink and Conf['Forward Hiding'] 74 | $.addClass post.nodes.root, 'forwarded' 75 | post.forwarded++ or post.forwarded = 1 76 | 77 | # Decrease the unread count if this post 78 | # is in the array of unread posts. 79 | return unless Unread.posts 80 | Unread.readSinglePost post 81 | 82 | rm: (quotelink, boardID, threadID, postID, context) -> 83 | isBacklink = $.hasClass quotelink, 'backlink' 84 | # Select the corresponding inlined quote, and remove it. 85 | root = QuoteInline.findRoot quotelink, isBacklink 86 | root = $.x "following-sibling::div[@id='i#{postID}'][1]", root 87 | qroot = $.x 'ancestor::*[contains(@class,"postContainer")][1]', root 88 | $.rm root 89 | 90 | unless $ '.inline', qroot 91 | $.rmClass qroot, 'hasInline' 92 | 93 | # Stop if it only contains text. 94 | return unless el = root.firstElementChild 95 | 96 | # Dereference clone. 97 | post = g.posts["#{boardID}.#{postID}"] 98 | post.rmClone el.dataset.clone 99 | 100 | # Decrease forward count and unhide. 101 | if Conf['Forward Hiding'] and 102 | isBacklink and 103 | context.thread is g.threads["#{boardID}.#{threadID}"] and 104 | not --post.forwarded 105 | delete post.forwarded 106 | $.rmClass post.nodes.root, 'forwarded' 107 | 108 | # Repeat. 109 | while inlined = $ '.inlined', el 110 | {boardID, threadID, postID} = Get.postDataFromLink inlined 111 | QuoteInline.rm inlined, boardID, threadID, postID, context 112 | $.rmClass inlined, 'inlined' 113 | return 114 | -------------------------------------------------------------------------------- /src/Miscellaneous/ExpandThread.coffee: -------------------------------------------------------------------------------- 1 | ExpandThread = 2 | statuses: {} 3 | init: -> 4 | return if g.VIEW is 'thread' or !Conf['Thread Expansion'] 5 | $.on d, (if Conf['JSON Navigation'] then 'IndexRefresh' else '4chanXInitFinished'), @onIndexRefresh 6 | 7 | setButton: (thread) -> 8 | return unless summary = $.x 'following-sibling::*[contains(@class,"summary")][1]', thread.OP.nodes.root 9 | a = $.el 'a', 10 | textContent: ExpandThread.text '+', summary.textContent.match(/\d+/g)... 11 | href: "res/#{thread.ID}" 12 | className: 'summary' 13 | $.on a, 'click', ExpandThread.cbToggle 14 | $.replace summary, a 15 | 16 | disconnect: -> 17 | @refresh() 18 | $.off d, 'IndexRefresh', @onIndexRefresh 19 | 20 | refresh: (disconnect) -> 21 | return if g.VIEW is 'thread' or !Conf['Thread Expansion'] 22 | for threadID, status of ExpandThread.statuses 23 | status.req?.abort() 24 | delete ExpandThread.statuses[threadID] 25 | return 26 | 27 | onIndexRefresh: -> 28 | ExpandThread.refresh() 29 | g.BOARD.threads.forEach (thread) -> 30 | ExpandThread.setButton thread 31 | 32 | text: (status, posts, files) -> 33 | "#{status} #{posts} post#{if posts > 1 then 's' else ''}" + 34 | (if +files then " and #{files} image repl#{if files > 1 then 'ies' else 'y'}" else "") + 35 | " #{if status is '-' then 'shown' else 'omitted'}." 36 | 37 | cbToggle: (e) -> 38 | return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 39 | e.preventDefault() 40 | ExpandThread.toggle Get.threadFromNode @ 41 | 42 | toggle: (thread) -> 43 | threadRoot = thread.OP.nodes.root.parentNode 44 | return unless a = $ '.summary', threadRoot 45 | if thread.ID of ExpandThread.statuses 46 | ExpandThread.contract thread, a, threadRoot 47 | else 48 | ExpandThread.expand thread, a, threadRoot 49 | expand: (thread, a, threadRoot) -> 50 | ExpandThread.statuses[thread] = status = {} 51 | a.textContent = ExpandThread.text '...', a.textContent.match(/\d+/g)... 52 | status.req = $.cache "//a.4cdn.org/#{thread.board}/thread/#{thread}.json", -> 53 | delete status.req 54 | ExpandThread.parse @, thread, a 55 | contract: (thread, a, threadRoot) -> 56 | status = ExpandThread.statuses[thread] 57 | delete ExpandThread.statuses[thread] 58 | if status.req 59 | status.req.abort() 60 | a.textContent = ExpandThread.text '+', a.textContent.match(/\d+/g)... if a 61 | return 62 | 63 | replies = $$ '.thread > .replyContainer', threadRoot 64 | if Conf['Show Replies'] 65 | num = if thread.isSticky 66 | 1 67 | else switch g.BOARD.ID 68 | # XXX boards config 69 | when 'b', 'vg' then 3 70 | when 't' then 1 71 | else 5 72 | replies = replies[...-num] 73 | postsCount = 0 74 | filesCount = 0 75 | for reply in replies 76 | # rm clones 77 | inlined.click() while inlined = $ '.inlined', reply if Conf['Quote Inlining'] 78 | postsCount++ 79 | filesCount++ if 'file' of Get.postFromRoot reply 80 | $.rm reply 81 | a.textContent = ExpandThread.text '+', postsCount, filesCount 82 | parse: (req, thread, a) -> 83 | if req.status not in [200, 304] 84 | a.textContent = "Error #{req.statusText} (#{req.status})" 85 | return 86 | 87 | Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler 88 | 89 | posts = [] 90 | postsRoot = [] 91 | filesCount = 0 92 | for postData in req.response.posts 93 | continue if postData.no is thread.ID 94 | if post = thread.posts[postData.no] 95 | filesCount++ if 'file' of post 96 | postsRoot.push post.nodes.root 97 | continue 98 | root = Build.postFromObject postData, thread.board.ID 99 | post = new Post root, thread, thread.board 100 | filesCount++ if 'file' of post 101 | posts.push post 102 | postsRoot.push root 103 | Post.callbacks.execute posts 104 | $.after a, postsRoot 105 | 106 | postsCount = postsRoot.length 107 | a.textContent = ExpandThread.text '-', postsRoot.length, filesCount 108 | -------------------------------------------------------------------------------- /src/Posting/Captcha.fixes.coffee: -------------------------------------------------------------------------------- 1 | Captcha.fixes = 2 | imageKeys: '789456123uiojklm'.split('').concat(['Comma', 'Period']) 3 | 4 | css: ''' 5 | .rc-imageselect-target > div:focus { 6 | outline: 2px solid #4a90e2; 7 | } 8 | .rc-imageselect-target td:focus { 9 | box-shadow: inset 0 0 0 2px #4a90e2; 10 | outline: none; 11 | } 12 | .rc-button-default:focus { 13 | box-shadow: inset 0 0 0 2px #0063d6; 14 | } 15 | ''' 16 | 17 | cssNoscript: ''' 18 | .fbc-payload-imageselect { 19 | position: relative; 20 | } 21 | .fbc-payload-imageselect > label { 22 | position: absolute; 23 | display: block; 24 | height: 93.3px; 25 | width: 93.3px; 26 | } 27 | label[data-row="0"] {top: 0px;} 28 | label[data-row="1"] {top: 93.3px;} 29 | label[data-row="2"] {top: 186.6px;} 30 | label[data-col="0"] {left: 0px;} 31 | label[data-col="1"] {left: 93.3px;} 32 | label[data-col="2"] {left: 186.6px;} 33 | ''' 34 | 35 | init: -> 36 | switch location.pathname.split('/')[3] 37 | when 'anchor' then @initMain() 38 | when 'frame' then @initPopup() 39 | when 'fallback' then @initNoscript() 40 | 41 | initMain: -> 42 | $.onExists d.body, '#recaptcha-anchor', true, (checkbox) -> 43 | focus = -> 44 | if d.hasFocus() and d.activeElement isnt checkbox 45 | checkbox.focus() 46 | focus() 47 | $.on window, 'focus', -> 48 | $.queueTask focus 49 | 50 | initPopup: -> 51 | $.addStyle @css 52 | @fixImages() 53 | new MutationObserver(=> @fixImages()).observe d.body, {childList: true, subtree: true} 54 | $.on d, 'keydown', @keybinds.bind(@) 55 | 56 | initNoscript: -> 57 | @noscript = true 58 | @images = $$ '.fbc-payload-imageselect > input' 59 | return unless @images.length is 9 60 | 61 | $.addStyle @cssNoscript 62 | @addLabels() 63 | $.on d, 'keydown', @keybinds.bind(@) 64 | $.on $('.fbc-imageselect-challenge > form'), 'submit', @checkForm.bind(@) 65 | 66 | fixImages: -> 67 | @images = $$ '.rc-imageselect-target > div, .rc-imageselect-target td' 68 | for img in @images 69 | img.tabIndex = 0 70 | @addTooltips @images if @images.length is 9 71 | @complaintLinks() 72 | 73 | complaintLinks: -> 74 | for errmsg in $$ '.rc-imageselect-incorrect-response, .rc-imageselect-error-select-one, .rc-imageselect-error-select-more' 75 | unless $ 'a', errmsg 76 | link = $.el 'a', 77 | href: 'https://www.4chan-x.net/captchas.html' 78 | target: '_blank' 79 | textContent: '[complain]' 80 | $.add errmsg, [$.tn(' '), link] 81 | return 82 | 83 | addLabels: -> 84 | imageSelect = $ '.fbc-payload-imageselect' 85 | labels = for checkbox, i in @images 86 | checkbox.id = "checkbox-#{i}" 87 | label = $.el 'label', 88 | htmlFor: checkbox.id 89 | label.dataset.row = i // 3 90 | label.dataset.col = i % 3 91 | label 92 | $.add imageSelect, labels 93 | @addTooltips labels 94 | 95 | addTooltips: (nodes) -> 96 | for node, i in nodes 97 | node.title = "#{@imageKeys[i]} or #{@imageKeys[i+9][0].toUpperCase()}#{@imageKeys[i+9][1..]}" 98 | return 99 | 100 | checkForm: (e) -> 101 | n = 0 102 | n++ for checkbox in @images when checkbox.checked 103 | e.preventDefault() if n is 0 104 | 105 | keybinds: (e) -> 106 | return unless @images and doc.contains(@images[0]) 107 | n = @images.length 108 | w = Math.round Math.sqrt n 109 | last = n + w - 1 110 | 111 | reload = $ '#recaptcha-reload-button, .fbc-button-reload' 112 | verify = $ '#recaptcha-verify-button, .fbc-button-verify > input' 113 | x = @images.indexOf d.activeElement 114 | if x < 0 115 | x = if d.activeElement is verify then last else n 116 | key = Keybinds.keyCode e 117 | 118 | if !@noscript and key is 'Space' and x < n 119 | @images[x].click() 120 | else if n is 9 and (i = @imageKeys.indexOf key) >= 0 121 | @images[i % 9].click() 122 | verify.focus() 123 | else if dx = {'Up': n, 'Down': w, 'Left': last, 'Right': 1}[key] 124 | x = (x + dx) % (n + w) 125 | if n < x < last 126 | x = if dx is last then n else last 127 | (@images[x] or (if x is n then reload) or (if x is last then verify)).focus() 128 | else 129 | return 130 | 131 | e.preventDefault() 132 | e.stopPropagation() 133 | -------------------------------------------------------------------------------- /src/Linkification/Linkify.coffee: -------------------------------------------------------------------------------- 1 | Linkify = 2 | init: -> 3 | return if g.VIEW not in ['index', 'thread'] or not Conf['Linkify'] 4 | 5 | if Conf['Comment Expansion'] 6 | ExpandComment.callbacks.push @node 7 | 8 | Post.callbacks.push 9 | name: 'Linkify' 10 | cb: @node 11 | 12 | CatalogThread.callbacks.push 13 | name: 'Linkify' 14 | cb: @catalogNode 15 | 16 | Embedding.init() 17 | 18 | node: -> 19 | return Embedding.events @ if @isClone 20 | return unless Linkify.regString.test @info.comment 21 | links = Linkify.process @nodes.comment 22 | Embedding.process link, @ for link in links 23 | return 24 | 25 | catalogNode: -> 26 | return unless Linkify.regString.test @thread.OP.info.comment 27 | Linkify.process @nodes.comment 28 | 29 | process: (node) -> 30 | test = /[^\s'"]+/g 31 | space = /[\s'"]/ 32 | snapshot = $.X './/br|.//text()', node 33 | i = 0 34 | links = [] 35 | while node = snapshot.snapshotItem i++ 36 | {data} = node 37 | continue if !data or node.parentElement.nodeName is "A" 38 | 39 | while result = test.exec data 40 | {index} = result 41 | endNode = node 42 | word = result[0] 43 | # End of node, not necessarily end of space-delimited string 44 | if (length = index + word.length) is data.length 45 | test.lastIndex = 0 46 | 47 | while (saved = snapshot.snapshotItem i++) 48 | if saved.nodeName is 'BR' 49 | break 50 | 51 | endNode = saved 52 | {data} = saved 53 | 54 | if end = space.exec data 55 | # Set our snapshot and regex to start on this node at this position when the loop resumes 56 | word += data[...end.index] 57 | test.lastIndex = length = end.index 58 | i-- 59 | break 60 | else 61 | {length} = data 62 | word += data 63 | 64 | if Linkify.regString.test word 65 | links.push Linkify.makeRange node, endNode, index, length 66 | <%= assert('word is links[links.length-1].toString()') %> 67 | 68 | break unless test.lastIndex and node is endNode 69 | 70 | i = links.length 71 | while i-- 72 | links[i] = Linkify.makeLink links[i] 73 | links 74 | 75 | regString: ///( 76 | # http, magnet, ftp, etc 77 | (https?|mailto|git|magnet|ftp|irc):( 78 | [a-z\d%/?] 79 | ) 80 | | # This should account for virtually all links posted without http: 81 | ([-a-z\d]+[.])+( 82 | aero|asia|biz|cat|com|coop|dance|info|int|jobs|mobi|moe|museum|name|net|org|post|pro|tel|travel|xxx|edu|gov|mil|[a-z]{2} 83 | )([:/]|(?![^\s'"])) 84 | | # IPv4 Addresses 85 | [\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3} 86 | | # E-mails 87 | [-\w\d.@]+@[a-z\d.-]+\.[a-z\d] 88 | )///i 89 | 90 | makeRange: (startNode, endNode, startOffset, endOffset) -> 91 | range = document.createRange() 92 | range.setStart startNode, startOffset 93 | range.setEnd endNode, endOffset 94 | range 95 | 96 | makeLink: (range) -> 97 | text = range.toString() 98 | 99 | # Clean start of range 100 | i = text.search Linkify.regString 101 | 102 | if i > 0 103 | text = text.slice i 104 | i-- while range.startOffset + i >= range.startContainer.data.length 105 | 106 | range.setStart range.startContainer, range.startOffset + i if i 107 | 108 | # Clean end of range 109 | i = 0 110 | while /[)\]}>.,]/.test t = text.charAt text.length - (1 + i) 111 | break unless /[.,]/.test(t) or (text.match /[()\[\]{}<>]/g).length % 2 112 | i++ 113 | 114 | if i 115 | text = text.slice 0, -i 116 | i-- while range.endOffset - i < 0 117 | 118 | if i 119 | range.setEnd range.endContainer, range.endOffset - i 120 | 121 | # Make our link 'valid' if it is formatted incorrectly. 122 | unless /((mailto|magnet):|.+:\/\/)/.test text 123 | text = ( 124 | if /@/.test text 125 | 'mailto:' 126 | else 127 | 'http://' 128 | ) + text 129 | 130 | a = $.el 'a', 131 | className: 'linkify' 132 | rel: 'nofollow noreferrer' 133 | target: '_blank' 134 | href: text 135 | 136 | # Insert the range into the anchor, the anchor into the range's DOM location, and destroy the range. 137 | $.add a, range.extractContents() 138 | range.insertNode a 139 | 140 | a 141 | -------------------------------------------------------------------------------- /src/Miscellaneous/RelativeDates.coffee: -------------------------------------------------------------------------------- 1 | RelativeDates = 2 | INTERVAL: $.MINUTE / 2 3 | init: -> 4 | if ( 5 | g.VIEW in ['index', 'thread'] and Conf['Relative Post Dates'] and !Conf['Relative Date Title'] or 6 | g.VIEW is 'index' and Conf['JSON Navigation'] and g.BOARD.ID isnt 'f' 7 | ) 8 | @flush() 9 | $.on d, 'visibilitychange ThreadUpdate', @flush 10 | 11 | if Conf['Relative Post Dates'] 12 | Post.callbacks.push 13 | name: 'Relative Post Dates' 14 | cb: @node 15 | 16 | node: -> 17 | dateEl = @nodes.date 18 | if Conf['Relative Date Title'] 19 | $.on dateEl, 'mouseover', => RelativeDates.hover @ 20 | return 21 | return if @isClone 22 | 23 | # Show original absolute time as tooltip so users can still know exact times 24 | # Since "Time Formatting" runs its `node` before us, the title tooltip will 25 | # pick up the user-formatted time instead of 4chan time when enabled. 26 | dateEl.title = dateEl.textContent 27 | 28 | RelativeDates.update @ 29 | 30 | # diff is milliseconds from now. 31 | relative: (diff, now, date) -> 32 | unit = if (number = (diff / $.DAY)) >= 1 33 | years = now.getYear() - date.getYear() 34 | months = now.getMonth() - date.getMonth() 35 | days = now.getDate() - date.getDate() 36 | if years > 1 37 | number = years - (months < 0 or months is 0 and days < 0) 38 | 'year' 39 | else if years is 1 and (months > 0 or months is 0 and days >= 0) 40 | number = years 41 | 'year' 42 | else if (months = months + 12*years) > 1 43 | number = months - (days < 0) 44 | 'month' 45 | else if months is 1 and days >= 0 46 | number = months 47 | 'month' 48 | else 49 | 'day' 50 | else if (number = (diff / $.HOUR)) >= 1 51 | 'hour' 52 | else if (number = (diff / $.MINUTE)) >= 1 53 | 'minute' 54 | else 55 | # prevent "-1 seconds ago" 56 | number = Math.max(0, diff) / $.SECOND 57 | 'second' 58 | 59 | rounded = Math.round number 60 | unit += 's' if rounded isnt 1 # pluralize 61 | 62 | "#{rounded} #{unit} ago" 63 | 64 | # Changing all relative dates as soon as possible incurs many annoying 65 | # redraws and scroll stuttering. Thus, sacrifice accuracy for UX/CPU economy, 66 | # and perform redraws when the DOM is otherwise being manipulated (and scroll 67 | # stuttering won't be noticed), falling back to INTERVAL while the page 68 | # is visible. 69 | # 70 | # Each individual dateTime element will add its update() function to the stale list 71 | # when it is to be called. 72 | stale: [] 73 | flush: -> 74 | # No point in changing the dates until the user sees them. 75 | return if d.hidden 76 | 77 | now = new Date() 78 | RelativeDates.update data, now for data in RelativeDates.stale 79 | RelativeDates.stale = [] 80 | 81 | # Reset automatic flush. 82 | clearTimeout RelativeDates.timeout 83 | RelativeDates.timeout = setTimeout RelativeDates.flush, RelativeDates.INTERVAL 84 | 85 | hover: (post) -> 86 | date = post.info.date 87 | now = new Date() 88 | diff = now - date 89 | post.nodes.date.title = RelativeDates.relative diff, now, date 90 | 91 | # `update()`, when called from `flush()`, updates the elements, 92 | # and re-calls `setOwnTimeout()` to re-add `data` to the stale list later. 93 | update: (data, now) -> 94 | isPost = data instanceof Post 95 | date = if isPost 96 | data.info.date 97 | else 98 | new Date +data.dataset.utc 99 | now or= new Date() 100 | diff = now - date 101 | relative = RelativeDates.relative diff, now, date 102 | if isPost 103 | for singlePost in [data].concat data.clones 104 | singlePost.nodes.date.firstChild.textContent = relative 105 | else 106 | data.firstChild.textContent = relative 107 | RelativeDates.setOwnTimeout diff, data 108 | setOwnTimeout: (diff, data) -> 109 | delay = if diff < $.MINUTE 110 | $.SECOND - (diff + $.SECOND / 2) % $.SECOND 111 | else if diff < $.HOUR 112 | $.MINUTE - (diff + $.MINUTE / 2) % $.MINUTE 113 | else if diff < $.DAY 114 | $.HOUR - (diff + $.HOUR / 2) % $.HOUR 115 | else 116 | $.DAY - (diff + $.DAY / 2) % $.DAY 117 | setTimeout RelativeDates.markStale, delay, data 118 | markStale: (data) -> 119 | return if data in RelativeDates.stale # We can call RelativeDates.update() multiple times. 120 | return if data instanceof Post and !g.posts[data.fullID] # collected post. 121 | RelativeDates.stale.push data 122 | -------------------------------------------------------------------------------- /src/Monitoring/ThreadStats.coffee: -------------------------------------------------------------------------------- 1 | ThreadStats = 2 | init: -> 3 | return if g.VIEW isnt 'thread' or !Conf['Thread Stats'] 4 | 5 | html = " 6 | [0 / 7 | 0 8 | #{if Conf['IP Count in Stats'] then '/ ?' else ''} 9 | #{if Conf['Page Count in Stats'] then '/ 0' else ''}] 10 | ".trim() 11 | 12 | title = " 13 | Post Count / File Count 14 | #{if Conf['IP Count in Stats'] then ' / IPs' else ''} 15 | #{if Conf['Page Count in Stats'] then (if g.BOARD.ID is 'f' then ' / Purge Position' else ' / Page Count') else ''} 16 | ".trim() 17 | 18 | if Conf['Updater and Stats in Header'] 19 | @dialog = sc = $.el 'span', 20 | innerHTML: html 21 | id: 'thread-stats' 22 | title: title 23 | Header.addShortcut sc 24 | 25 | else 26 | @dialog = sc = UI.dialog 'thread-stats', 'bottom: 0px; right: 0px;', 27 | innerHTML: "
#{html}
" 28 | $.addClass doc, 'float' 29 | $.ready -> 30 | $.add d.body, sc 31 | 32 | @postCountEl = $ '#post-count', sc 33 | @ipCountEl = $ '#ip-count', sc 34 | @fileCountEl = $ '#file-count', sc 35 | @pageCountEl = $ '#page-count', sc 36 | 37 | Thread.callbacks.push 38 | name: 'Thread Stats' 39 | cb: @node 40 | 41 | node: -> 42 | postCount = 0 43 | fileCount = 0 44 | @posts.forEach (post) -> 45 | postCount++ 46 | fileCount++ if post.file 47 | ThreadStats.lastPost = post.info.date if Conf["Page Count in Stats"] 48 | ThreadStats.thread = @ 49 | ThreadStats.fetchPage() 50 | ThreadStats.update postCount, fileCount, @ipCount 51 | $.on d, 'ThreadUpdate', ThreadStats.onUpdate 52 | 53 | disconnect: -> 54 | return if g.VIEW isnt 'thread' or !Conf['Thread Stats'] 55 | 56 | if Conf['Updater and Stats in Header'] 57 | Header.rmShortcut @dialog 58 | else 59 | $.rm @dialog 60 | 61 | clearTimeout @timeout # a possible race condition might be that this won't clear in time, but the resulting error will prevent issues anyways. 62 | 63 | delete @timeout 64 | delete @thread 65 | delete @postCountEl 66 | delete @fileCountEl 67 | delete @pageCountEl 68 | delete @dialog 69 | 70 | Thread.callbacks.disconnect 'Thread Stats' 71 | $.off d, 'ThreadUpdate', ThreadStats.onUpdate 72 | 73 | onUpdate: (e) -> 74 | return if e.detail[404] 75 | {postCount, fileCount, ipCount, newPosts} = e.detail 76 | ThreadStats.update postCount, fileCount, ipCount 77 | return unless Conf["Page Count in Stats"] 78 | if newPosts.length 79 | ThreadStats.lastPost = g.posts[newPosts[newPosts.length - 1]].info.date 80 | if ThreadStats.lastPost > ThreadStats.lastPageUpdate and ThreadStats.pageCountEl?.textContent isnt '1' 81 | ThreadStats.fetchPage() 82 | 83 | update: (postCount, fileCount, ipCount) -> 84 | {thread, postCountEl, fileCountEl, ipCountEl} = ThreadStats 85 | postCountEl.textContent = postCount 86 | fileCountEl.textContent = fileCount 87 | if ipCount? and Conf["IP Count in Stats"] 88 | ipCountEl.textContent = ipCount 89 | (if thread.postLimit and !thread.isSticky then $.addClass else $.rmClass) postCountEl, 'warning' 90 | (if thread.fileLimit and !thread.isSticky then $.addClass else $.rmClass) fileCountEl, 'warning' 91 | 92 | fetchPage: -> 93 | return unless Conf["Page Count in Stats"] 94 | clearTimeout ThreadStats.timeout 95 | if ThreadStats.thread.isDead 96 | ThreadStats.pageCountEl.textContent = 'Dead' 97 | $.addClass ThreadStats.pageCountEl, 'warning' 98 | return 99 | ThreadStats.timeout = setTimeout ThreadStats.fetchPage, 2 * $.MINUTE 100 | $.ajax "//a.4cdn.org/#{ThreadStats.thread.board}/threads.json", onload: ThreadStats.onThreadsLoad, 101 | whenModified: true 102 | 103 | onThreadsLoad: -> 104 | return unless Conf["Page Count in Stats"] and @status is 200 105 | for page in @response 106 | if g.BOARD.ID is 'f' 107 | purgePos = 1 108 | for thread in page.threads 109 | if thread.no < ThreadStats.thread.ID 110 | purgePos++ 111 | ThreadStats.pageCountEl.textContent = purgePos 112 | else 113 | for thread in page.threads when thread.no is ThreadStats.thread.ID 114 | ThreadStats.pageCountEl.textContent = page.page 115 | (if page.page is @response.length then $.addClass else $.rmClass) ThreadStats.pageCountEl, 'warning' 116 | # Thread data may be stale (modification date given < time of last post). If so, try again on next thread update. 117 | ThreadStats.lastPageUpdate = new Date thread.last_modified * $.SECOND 118 | return 119 | --------------------------------------------------------------------------------