├── .eslintrc.json
├── .github
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
├── no-response.yml
├── prettifier.yml
└── stale.yml
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── docs
├── _config.yml
└── index.html
├── icons
├── sox_access_time.svg
├── sox_checked_box.svg
├── sox_chevron_left.svg
├── sox_chevron_right.svg
├── sox_copy.svg
├── sox_export.svg
├── sox_hot.svg
├── sox_import.svg
├── sox_info.svg
├── sox_key.svg
├── sox_launch.svg
├── sox_search.svg
├── sox_settings.svg
├── sox_toggle_off.svg
├── sox_toggle_on.svg
├── sox_top.svg
├── sox_unchecked_box.svg
└── sox_wrench.svg
├── sox.common.info.json
├── sox.common.js
├── sox.css
├── sox.dialog.html
├── sox.dialog.js
├── sox.features.info.json
├── sox.features.js
├── sox.github.js
├── sox.sprites.svg
└── sox.user.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jquery": true,
6 | "greasemonkey": true
7 | },
8 | "extends": "eslint:recommended",
9 | "parserOptions": {
10 | "ecmaVersion": 2018
11 | },
12 | "rules": {
13 | "indent": [
14 | "error",
15 | 2
16 | ],
17 | "linebreak-style": [
18 | "error",
19 | "windows"
20 | ],
21 | "quotes": [
22 | "error",
23 | "single"
24 | ],
25 | "semi": [
26 | "error",
27 | "always"
28 | ],
29 | "comma-dangle": ["error", "always-multiline"],
30 | "no-console": "off",
31 | "prefer-arrow-callback":"warn",
32 | "no-trailing-spaces":"error",
33 | "prefer-const": "warn",
34 | "one-var": ["warn", "never"],
35 | "no-multiple-empty-lines": ["warn", { "max": 1 }],
36 | "arrow-parens": ["warn", "as-needed"]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at shubatse@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you for considering to contribute to SOX!
4 |
5 | We would appreciate any and all contributions, partial or complete, and try to respond to all issues/pull requests as soon as we can!
6 |
7 | Contributing doesn't have to be with code! Fixing a simple typo, or helping us document code/update the wiki, is equally as important! :)
8 |
9 | Please see the [Contributing Wiki](https://github.com/soscripted/sox/wiki/Contributing) for full details on how you can help out with SOX.
10 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a bug report to improve SOX
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behaviour:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **SOX errors logged in console**
21 | Please click 'enable debugging' in the SOX settings dialog; hit F12 on a Stack Exchange site; and copy & paste the contents of the 'console' here
22 |
23 | **Expected behaviour**
24 | A clear and concise description of what you expected to happen.
25 |
26 | **Screenshots/GIFs**
27 | If applicable, please add screenshots/GIFs (you can use something like [LICEcap](https://www.cockos.com/licecap/) to make one!) to help explain the problem.
28 |
29 | **Environment**
30 | If this is not auto-populated, please state your full browser version, SOX version number, and userscript manager (e.g. Tampermonkey)
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for SOX
4 | title: ''
5 | labels: feature request
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the feature you'd like**
11 | A clear and concise description of what you would like to see SOX implement.
12 |
13 | **Use case**
14 | A short example of where this feature could be used
15 |
16 | **Suggested descriptions**
17 | If you have suggestions for a short concise description (to include in the settings dialog), please mention them!
18 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | **Bug/Feature**
2 | Is this a bug fix or does it implement a feature?
3 |
4 | **Related issue**
5 | Please link to the issue that this pull request is for
6 |
7 | **Description of changes**
8 | Please describe (briefly!) the changes made
9 |
--------------------------------------------------------------------------------
/.github/no-response.yml:
--------------------------------------------------------------------------------
1 | # Configuration for probot-no-response - https://github.com/probot/no-response
2 |
3 | # Number of days of inactivity before an Issue is closed for lack of response
4 | daysUntilClose: 5
5 | # Label requiring a response
6 | responseRequiredLabel: more-information-needed
7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable
8 | closeComment: >
9 | This issue has been automatically closed because there has been no response
10 | to our request for more information from the original author. With only the
11 | information that is currently in the issue, we don't have enough information
12 | to take action. Please reach out if you have or find the answers we need so
13 | that we can investigate further.
14 |
--------------------------------------------------------------------------------
/.github/prettifier.yml:
--------------------------------------------------------------------------------
1 | commitMessage: "Prettify Code"
2 |
3 | tabWidth: 2
4 | useTabs: false
5 | semi: true
6 | singleQuote: true
7 | trailingComma: "es5"
8 | bracketSpacing: true
9 | arrowParens: "avoid"
10 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 15
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - contributions welcome
8 | - confirmed
9 | - approved
10 | # Label to use when marking an issue as stale
11 | staleLabel: rejected
12 | # Comment to post when marking an issue as stale. Set to `false` to disable
13 | markComment: >
14 | This issue has been automatically marked as stale because it has not had
15 | recent activity. It will be closed if no further activity occurs. Thank you
16 | for your contributions.
17 | # Comment to post when closing a stale issue. Set to `false` to disable
18 | closeComment: true
19 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.enable": true
3 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 soscripted
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://gitter.im/soscripted/sox?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
2 |
3 | ### SOX v2.8.0
4 |
5 | Stack Overflow Extras (*SOX*) is a project that stemmed from the [Stack Overflow Optional Features (SOOF)](https://github.com/shu8/Stack-Overflow-Optional-Features) project.
6 |
7 | The SOX userscript adds a bunch of **optional** features to all sites in the Stack Exchange network. These can be toggled on or off from an easy to use control panel (see screenshot below).
8 |
9 | Note: This project has no relation to Stack Overflow or Stack Exchange; it is simply a userscript that enhances the sites!
10 |
11 | ## Installation & Requirements
12 |
13 | 1. Install a userscript manager; these are free extensions available for all popular browsers that allow you to manage and install userscripts, along with exposing certain code functions that SOX requires.
14 |
15 | We recommend [Tampermonkey](http://tampermonkey.net/) for Chrome and Firefox.
16 |
17 | Whilst SOX only explicitly supports Chrome and Firefox, it should work on any popular browser that can run userscripts.
18 |
19 | **Note: Greasemonkey 4 and upwards [is not supported with SOX](https://github.com/soscripted/sox/issues/306).**
20 |
21 | **There seems to be [an issue with Tampermonkey on Firefox](https://github.com/Tampermonkey/tampermonkey/issues/477) where userscripts don't seem to run. If this happens, please restart your browser and/or computer before raising an issue on GitHub, as a restart seems to fix this!**
22 |
23 | 2. Install the script. Clicking on 'install' below will make Tampermonkey prompt you automatically to install it.
24 |
25 | - Official Version: [install](https://github.com/soscripted/sox/raw/v2.8.0/sox.user.js) . [view source](https://github.com/soscripted/sox/blob/v2.8.0/sox.user.js)
26 | - Development Version: [install](https://github.com/soscripted/sox/raw/dev/sox.user.js) . [view source](https://github.com/soscripted/sox/blob/dev/sox.user.js)
27 |
28 | 3. Go to any site in the Stack Exchange Network (e.g. [Super User](http://superuser.com/) or [Stack Overflow](http://stackoverflow.com/)) (reload if already opened) and confirm the dialog asking you to get an access token. This will open a new page where you can authorize SOX, and can be closed once your access token is saved. A toggle button (gears icon) will be added to your topbar. Click and review and save the settings
29 |
30 | 
31 |
32 | ## What features are included?
33 |
34 | A full list of all the features is available on the SOX wiki page [here](https://github.com/soscripted/sox/wiki/Features).
35 |
36 | ## Bugs and Feature Requests
37 |
38 | Please post bugs and feature requests as issues on [Github](https://github.com/soscripted/sox), where we can track them easily and push updates quickly. Please **do not** post them as answers on Stack Apps -- they are much harder to manage!
39 |
40 | ## Contribute
41 |
42 | Pull requests to add new features or improve the existing ones, etc. are welcome! Please head to the [Contributing](https://github.com/soscripted/sox/wiki/Contributing) wiki page to get started.
43 |
44 | ## Changes
45 |
46 | Please see the change log [at Stack Apps](http://stackapps.com/a/6358).
47 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-minimal
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SOX by soscripted
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 | SOX
20 | A userscript for the Stack Exchange websites to add a bunch of optional user-selectable features
21 | View the Project on GitHub
22 |
23 |
24 |
25 |
26 |
27 |
28 | SOX
29 |
30 |
31 |
32 |
Your access token
33 |
34 |
35 |
An alert should have appeared saying the token has been saved. If not, please open the console ((FN +) F12) and copy and paste the entire log in an issue at http://github.com/soscripted/sox/issues/new so we can fix this for you! :)
36 |
42 |
43 |
44 |
45 | Stack Overflow Extras (SOX ) is a project that stemmed from the Stack Overflow Optional Features (SOOF) project.
46 |
47 | The SOX userscript adds a bunch of optional features to all sites in the Stack Exchange network. These can be toggled on or off from an easy to use control panel (see screenshot below).
48 |
49 | Note: This project is not related to Stack Overflow or Stack Exchange; it is simply a userscript that enhances the sites!
50 |
51 | Installation & Requirements
52 |
53 |
54 | Install Greasemonkey (for Firefox), Tampermonkey (for Chrome), or NinjaKit for Safari. These are userscript
55 | managers that must be installed in order for this to work, as the script relies on certain GM_*
functions in order to save your settings!
56 |
57 | Install the script. Clicking on the 'install' button below will make your userscript manager prompt you automatically to install it.
58 |
59 |
63 |
64 | Go to any site in the Stack Exchange Network (e.g. Super User or Stack Overflow ). You will automatically be asked to choose and save your settings. A toggle button
65 | (gears icon) will be added to your topbar where you can change these later on:
66 |
67 |
68 |
69 |
70 | What features are included?
71 |
72 | A full list of all the features is available on the SOX wiki page here .
73 |
74 | Bugs and Feature Requests
75 |
76 | Please post bugs and feature requests as issues on Github , where we can track them easily and push updates quickly. Please do not post them as answers on Stack Apps – they are
77 | much harder to manage!
78 |
79 | Changes
80 |
81 | Please see the change log at Stack Apps .
82 |
83 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/icons/sox_access_time.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_checked_box.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_chevron_left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_chevron_right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_copy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_export.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_hot.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_import.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_info.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_key.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_launch.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_search.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/icons/sox_settings.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_toggle_off.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_toggle_on.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_top.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_unchecked_box.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/sox_wrench.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sox.common.info.json:
--------------------------------------------------------------------------------
1 | {
2 | "privileges": {
3 | "beta": {
4 | "access review queues": 350,
5 | "access to moderator tools": 2000,
6 | "approve tag wiki edits": 1500,
7 | "cast close and reopen votes": 500,
8 | "comment everywhere": 50,
9 | "create chat rooms": 100,
10 | "create gallery chat rooms": 1000,
11 | "create posts": 1,
12 | "create tag synonyms": 1250,
13 | "create tags": 150,
14 | "create wiki posts": 10,
15 | "edit community wiki": 100,
16 | "edit questions and answers": 1000,
17 | "established user": 750,
18 | "flag posts": 15,
19 | "participate in meta": 5,
20 | "protect questions": 3500,
21 | "remove new user restrictions": 10,
22 | "set bounties": 75,
23 | "talk in chat": 20,
24 | "trusted user": 4000,
25 | "view close votes": 250,
26 | "vote down": 125,
27 | "vote up": 15
28 | },
29 | "graduated": {
30 | "access review queues": 2000,
31 | "access to moderator tools": 10000,
32 | "approve tag wiki edits": 5000,
33 | "cast close and reopen votes": 3000,
34 | "comment everywhere": 5,
35 | "create chat rooms": 100,
36 | "create gallery chat rooms": 1000,
37 | "create posts": 1,
38 | "create tag synonyms": 2500,
39 | "create tags": 500,
40 | "create wiki posts": 10,
41 | "edit community wiki": 100,
42 | "edit questions and answers": 2000,
43 | "established user": 1000,
44 | "flag posts": 15,
45 | "participate in meta": 5,
46 | "protect questions": 15000,
47 | "reduce ads": 200,
48 | "remove new user restrictions": 10,
49 | "set bounties": 75,
50 | "talk in chat": 20,
51 | "trusted user": 20000,
52 | "view close votes": 250,
53 | "vote down": 125,
54 | "vote up": 15
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/sox.common.js:
--------------------------------------------------------------------------------
1 | /* globals CHAT, StackExchange, jQuery */
2 | (function(sox, $) {
3 | 'use strict';
4 | const SOX_SETTINGS = 'SOXSETTINGS';
5 | const commonInfo = JSON.parse(GM_getResourceText('common'));
6 | const lastVersionInstalled = GM_getValue('SOX-lastVersionInstalled');
7 | var hookAjaxObject = {};
8 |
9 | sox.info = {
10 | version: (typeof GM_info !== 'undefined' ? GM_info.script.version : 'unknown'),
11 | handler: (typeof GM_info !== 'undefined' ? GM_info.scriptHandler : 'unknown'),
12 | apikey: 'lL1S1jr2m*DRwOvXMPp26g((',
13 | debugging: GM_getValue('SOX-debug', false),
14 | lastVersionInstalled: lastVersionInstalled,
15 | };
16 |
17 | sox.NEW_TOPBAR = location.href.indexOf('area51') === -1;
18 |
19 | sox.debug = function() {
20 | if (!sox.info.debugging) return;
21 | for (let arg = 0; arg < arguments.length; ++arg) {
22 | console.debug('SOX:', arguments[arg]);
23 | }
24 | };
25 |
26 | sox.log = function() {
27 | for (let arg = 0; arg < arguments.length; ++arg) {
28 | console.log('SOX:', arguments[arg]);
29 | }
30 | };
31 |
32 | sox.warn = function() {
33 | for (let arg = 0; arg < arguments.length; ++arg) {
34 | console.warn('SOX:', arguments[arg]);
35 | }
36 | };
37 |
38 | sox.error = function() {
39 | for (let arg = 0; arg < arguments.length; ++arg) {
40 | console.error('SOX:', arguments[arg]);
41 | }
42 | };
43 |
44 | sox.loginfo = function() {
45 | for (let arg = 0; arg < arguments.length; ++arg) {
46 | console.info('SOX:', arguments[arg]);
47 | }
48 | };
49 |
50 | let Chat;
51 | let Stack;
52 | if (location.href.indexOf('github.com') === -1) { //need this so it works on FF -- CSP blocks window.eval() it seems
53 | Chat = (typeof window.CHAT === 'undefined' ? window.eval('typeof CHAT != \'undefined\' ? CHAT : undefined') : CHAT);
54 | Stack = (typeof Chat === 'undefined'
55 | ? (typeof StackExchange === 'undefined'
56 | ? window.eval('if (typeof StackExchange != "undefined") StackExchange')
57 | : (StackExchange || window.StackExchange))
58 | : undefined);
59 | }
60 |
61 | sox.Stack = Stack;
62 |
63 | sox.exists = function(path) {
64 | if (!Stack) return false;
65 | const toCheck = path.split('.');
66 |
67 | let cont = true;
68 | let o = Stack;
69 | let i;
70 |
71 | for (i = 0; i < toCheck.length; i++) {
72 | if (!cont) return false;
73 | if (!(toCheck[i] in o)) cont = false;
74 | o = o[toCheck[i]];
75 | }
76 | return cont;
77 | };
78 |
79 | sox.ready = function(func) {
80 | $(() => {
81 | if (Stack) {
82 | if (Stack.ready) {
83 | Stack.ready(func());
84 | } else {
85 | func();
86 | }
87 | } else {
88 | func();
89 | }
90 | });
91 | };
92 |
93 | sox.settings = {
94 | available: GM_getValue(SOX_SETTINGS, -1) != -1,
95 | load: function() {
96 | const settings = GM_getValue(SOX_SETTINGS);
97 | return settings === undefined ? undefined : JSON.parse(settings);
98 | },
99 | save: function(settings) {
100 | // If importing, it will already be a string so there's no need to stringify it
101 | GM_setValue(SOX_SETTINGS, typeof settings === 'string' ? settings : JSON.stringify(settings));
102 | },
103 | reset: function() {
104 | const keys = GM_listValues();
105 | sox.debug(keys);
106 | keys.forEach(key => GM_deleteValue(key));
107 | },
108 | get accessToken() {
109 | const accessToken = GM_getValue('SOX-accessToken', false);
110 | return (accessToken == -2 ? false : accessToken); //if the user was already asked once, the value is set to -2, so make sure this is returned as false
111 | },
112 | writeToConsole: function(hideAccessToken) {
113 | sox.loginfo('logging sox stored values --- ');
114 | const keys = GM_listValues();
115 | for (let i = 0; i < keys.length; i++) {
116 | const key = keys[i];
117 | if (hideAccessToken && key == 'SOX-accessToken') {
118 | sox.loginfo('access token set');
119 | } else {
120 | sox.loginfo(key, GM_getValue(key));
121 | }
122 | }
123 | },
124 | };
125 |
126 | function throttle(fn, countMax, time) {
127 | let counter = 0;
128 |
129 | setInterval(() => {
130 | counter = 0;
131 | }, time);
132 |
133 | return function() {
134 | if (counter < countMax) {
135 | counter++;
136 | fn.apply(this, arguments);
137 | }
138 | };
139 | }
140 |
141 | sox.sprites = {
142 | getSvg: function (name, tooltip, css) {
143 | const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
144 | const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
145 | const title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
146 |
147 | if (tooltip) {
148 | svg.setAttribute('title', tooltip);
149 | title.textContent = tooltip;
150 | svg.appendChild(title);
151 | }
152 |
153 | use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', `#sox_${name}`);
154 | svg.appendChild(use);
155 |
156 | svg.classList.add('sox-sprite');
157 | svg.classList.add(`sox-sprite-${name}`);
158 | return svg;
159 | },
160 | };
161 |
162 | sox.helpers = {
163 | getFromAPI: function (details, callback) {
164 | let {
165 | ids,
166 | useCache = true,
167 | } = details;
168 |
169 | const {
170 | endpoint,
171 | childEndpoint,
172 | sort = 'creation',
173 | order = 'desc',
174 | sitename,
175 | filter,
176 | limit,
177 | page,
178 | featureId,
179 | cacheDuration = 3, // Minutes to cache data for
180 | } = details;
181 | const baseURL = 'https://api.stackexchange.com/2.2/';
182 | const queryParams = [];
183 |
184 | // Cache can only be used if the featureId and IDs (as an array) have been provided
185 | useCache = featureId && useCache && Array.isArray(ids);
186 | const apiCache = JSON.parse(GM_getValue('SOX-apiCache', '{}'));
187 |
188 | if (!(featureId in apiCache)) apiCache[featureId] = {};
189 | const featureCache = apiCache[featureId];
190 |
191 | if (!(endpoint in featureCache)) featureCache[endpoint] = [];
192 | const endpointCache = featureCache[endpoint];
193 |
194 | const endpointToIdFieldNames = {
195 | 'questions': 'question_id',
196 | 'answers': 'answer_id',
197 | 'users': 'user_id',
198 | 'comments': 'comment_id',
199 | };
200 |
201 | if (filter) queryParams.push(`filter=${filter}`);
202 | if (order) queryParams.push(`order=${order}`);
203 | if (limit) queryParams.push(`pagesize=${limit}`);
204 | if (page) queryParams.push(`page=${page}`);
205 | queryParams.push(`sort=${sort}`);
206 | queryParams.push(`site=${sitename}`);
207 | queryParams.push(`key=${sox.info.apikey}`);
208 | queryParams.push(`access_token=${sox.settings.accessToken}`);
209 | const queryString = queryParams.join('&');
210 |
211 | let finalItems = [];
212 | if (useCache) {
213 | // Count backwards so splicing doesn't change indices
214 | for (let i = ids.length; i >= 0; i--) {
215 | const cachedItemIndex = endpointCache.findIndex(item => {
216 | const idFieldName = endpointToIdFieldNames[endpoint];
217 | return item[idFieldName] === +ids[i];
218 | });
219 |
220 | // Cache results for max. cacheDuraction minutes (convert to milliseconds)
221 | const earliestRequestTime = new Date().getTime() - (60 * cacheDuration * 1000);
222 | if (cachedItemIndex !== -1) {
223 | const cachedItem = endpointCache[cachedItemIndex];
224 | if (cachedItem.sox_request_time >= earliestRequestTime) {
225 | // If we have a cached item for this ID, delete it from `ids` so we don't request the API for it
226 | sox.debug(`API: [${featureId}:/${endpoint}/${ids[i]}] Using cached API item`);
227 | finalItems.push(cachedItem);
228 | ids.splice(i, 1);
229 | } else {
230 | // The cached item is now stale (too old); delete it
231 | sox.debug(`API: [${featureId}:/${endpoint}/${ids[i]}] Deleting stale cached item`);
232 | endpointCache.splice(cachedItemIndex, 1);
233 | }
234 | }
235 | }
236 | }
237 |
238 | // IDs are optional for endpoints like /questions
239 | if (ids && Array.isArray(ids)) {
240 | if (ids.length) {
241 | ids = ids.join(';');
242 | } else if (useCache) {
243 | // The cache had details for all IDs; no need to request API at all
244 | sox.debug(`API: [${featureId}:/${endpoint}] API Cache had details for all requested IDs, skipping API request`);
245 | GM_setValue('SOX-apiCache', JSON.stringify(apiCache));
246 | sox.debug('API: Saving new cache', apiCache);
247 | callback(finalItems);
248 | return;
249 | }
250 | }
251 |
252 | const idPath = ids ? `/${ids}` : '';
253 | let queryURL;
254 | if (childEndpoint) {
255 | // e.g. /posts/{ids}/revisions
256 | queryURL = `${baseURL}${endpoint}${idPath}/${childEndpoint}?${queryString}`;
257 | } else {
258 | // e.g. /questions/{ids}
259 | queryURL = `${baseURL}${endpoint}${idPath}?${queryString}`;
260 | }
261 | sox.debug(`API: Sending request to URL: '${queryURL}'`);
262 |
263 | fetch(queryURL).then(apiResponse => apiResponse.json()).then(responseJson => {
264 | if (responseJson.backoff) {
265 | sox.error('SOX Error: BACKOFF: ' + responseJson.backoff);
266 | } else if (responseJson.error_id == 502) {
267 | sox.error('THROTTLE VIOLATION', responseJson);
268 | } else if (responseJson.error_id == 403) {
269 | sox.warn('Access token invalid! Opening window to get new one');
270 | window.open('https://stackexchange.com/oauth/dialog?client_id=7138&scope=no_expiry&redirect_uri=http://soscripted.github.io/sox/');
271 | alert('Your access token is no longer valid. A window has been opened to request a new one.');
272 | } else {
273 | if (useCache) {
274 | responseJson.items.forEach(item => {
275 | item.sox_request_time = new Date().getTime();
276 | finalItems.push(item);
277 | endpointCache.push(item);
278 | });
279 | GM_setValue('SOX-apiCache', JSON.stringify(apiCache));
280 | sox.debug('API: saving new cache', apiCache);
281 | } else {
282 | finalItems = responseJson.items;
283 | }
284 | callback(finalItems);
285 | }
286 | });
287 | },
288 | observe: function (targets, elements, callback) {
289 | sox.debug(`OBSERVE: '${elements}' on target(s)`, targets);
290 | if (!targets || (Array.isArray(targets) && !targets.length)) return;
291 |
292 | const observer = new MutationObserver(throttle(mutations => {
293 | for (let i = 0; i < mutations.length; i++) {
294 | const mutation = mutations[i];
295 | const target = mutation.target;
296 | const addedNodes = mutation.addedNodes;
297 |
298 | if (addedNodes) {
299 | for (let n = 0; n < addedNodes.length; n++) {
300 | if ($(addedNodes[n]).find(elements).length) {
301 | sox.debug('fire: node: ', addedNodes[n]);
302 | callback(target);
303 | return;
304 | }
305 | }
306 | }
307 |
308 | if ($(target).is(elements)) { //TODO: maybe add OR to find subelements for childList events?
309 | callback(target);
310 | sox.debug('fire: target: ', target);
311 | return;
312 | }
313 | }
314 | }, 1500));
315 |
316 | if (Array.isArray(targets)) {
317 | for (let i = 0; i < targets.length; i++) {
318 | const target = targets[i];
319 | if (!target) continue;
320 |
321 | observer.observe(target, {
322 | attributes: true,
323 | childList: true,
324 | characterData: true,
325 | subtree: true,
326 | });
327 | }
328 | } else {
329 | observer.observe(targets, {
330 | attributes: true,
331 | childList: true,
332 | characterData: true,
333 | subtree: true,
334 | });
335 | }
336 | },
337 | newElement: function(type, elementDetails) {
338 | const extras = {};
339 | const allowed = ['text', 'checkbox', 'radio', 'textarea', 'span', 'div', 'a'];
340 |
341 | if (allowed.indexOf(type) != -1) {
342 | if (type == 'text') {
343 | type = 'input';
344 | extras.type = 'input';
345 | } else if (type == 'checkbox') {
346 | type = 'input';
347 | extras.type = 'checkbox';
348 | } else if (type == 'radio') {
349 | type = 'input';
350 | extras.type = 'radio';
351 | } else if (type == 'textarea') {
352 | if (!elementDetails.text) {
353 | elementDetails.text = elementDetails.value;
354 | }
355 | }
356 |
357 | $.each(elementDetails, (k, v) => {
358 | extras[k] = v;
359 | });
360 | return $('<' + type + '/>', extras);
361 | } else {
362 | return false;
363 | }
364 | },
365 | getIDFromAnchor: function(anchor) {
366 | return anchor.href ? sox.helpers.getIDFromLink(anchor.href) : null;
367 | },
368 | getSiteNameFromAnchor: function(anchor) {
369 | return anchor.href ? sox.helpers.getSiteNameFromLink(anchor.href) : null;
370 | },
371 | // answer ID, question ID, user ID, comment ID ("posts/comments/ID" NOT "comment1545_5566")
372 | getIDFromLink: function(link) {
373 | // test cases: https://regex101.com/r/6P9sDX/2
374 | const idMatch = link.match(/\/(\d+)/);
375 | return idMatch ? +idMatch[1] : null;
376 | },
377 | getSiteNameFromLink: function(link) {
378 | const siteRegex = /(((.+)\.)?(((stackexchange|stackoverflow|superuser|serverfault|askubuntu|stackapps))(?=\.com))|mathoverflow\.net)/;
379 | const siteMatch = link.replace(/https?:\/\//, '').match(siteRegex);
380 | return siteMatch ? siteMatch[1] : null;
381 | },
382 | createModal: function (params) {
383 | const closeButtonSvg = `
384 |
385 | `;
386 |
387 | const dialog = document.createElement('aside');
388 | dialog.className = 's-modal js-modal-overlay js-modal-close js-stacks-managed-popup js-fades-with-aria-hidden sox-custom-dialog';
389 | dialog.role = 'dialog';
390 | dialog.ariaHidden = false;
391 | if (params.id) dialog.id = params.id;
392 |
393 | const dialogInnerContainer = document.createElement('div');
394 | dialogInnerContainer.className = 's-modal--dialog js-modal-dialog';
395 | dialogInnerContainer.style.minWidth = '568px'; // top: 227.736px; left: 312.653px;
396 |
397 | // if (params.css) $dialog.css(params.css)
398 | // if (params.css) $dialogInnerContainer.css(params.css);
399 |
400 | const header = document.createElement('h1');
401 | header.className = 's-modal--header fs-headline1 fw-bold mr48 js-first-tabbable sox-custom-dialog-header';
402 | header.innerHTML = params.header;
403 | const mainContent = document.createElement('div');
404 | mainContent.className = 's-modal--body sox-custom-dialog-content';
405 | if (params.html) mainContent.innerHTML = params.html;
406 |
407 | const closeButton = document.createElement('button');
408 | closeButton.className = 's-modal--close s-btn s-btn__muted js-modal-close js-last-tabbable';
409 | closeButton.onclick = () => document.querySelector('.sox-custom-dialog').remove();
410 | closeButton.insertAdjacentHTML('beforeend', closeButtonSvg);
411 |
412 | dialogInnerContainer.appendChild(header);
413 | dialogInnerContainer.appendChild(mainContent);
414 | dialogInnerContainer.appendChild(closeButton);
415 | dialog.appendChild(dialogInnerContainer);
416 |
417 | return dialog;
418 | },
419 | addButtonToHelpMenu: function (params) {
420 | const liElement = document.createElement('li');
421 | const anchor = document.createElement('a');
422 | anchor.href = 'javascript:void(0)';
423 | anchor.id = params.id;
424 | anchor.innerText = `SOX: ${params.linkText}`;
425 | const span = document.createElement('span');
426 | span.className = 'item-summary';
427 | span.innerText = params.summary;
428 |
429 | liElement.addEventListener('click', params.click);
430 | anchor.appendChild(span);
431 | liElement.appendChild(anchor);
432 | document.querySelector('.topbar-dialog.help-dialog.js-help-dialog .modal-content ul').appendChild(liElement);
433 | },
434 | surroundSelectedText: function(textarea, start, end) {
435 | // same wrapper code on either side (`$...$`)
436 | if (typeof end === 'undefined') end = start;
437 |
438 | /*--- Expected behavior:
439 | When there is some text selected: (unwrap it if already wrapped)
440 | "]text[" --> "**]text[**"
441 | "**]text[**" --> "]text["
442 | "]**text**[" --> "**]**text**[**"
443 | "**]**text**[**" --> "]**text**["
444 | When there is no text selected:
445 | "][" --> "**placeholder text**"
446 | "**][**" --> ""
447 | Note that `]` and `[` denote the selected text here.
448 | */
449 |
450 | const selS = textarea.selectionStart < textarea.selectionEnd ? textarea.selectionStart : textarea.selectionEnd;
451 | const selE = textarea.selectionStart > textarea.selectionEnd ? textarea.selectionStart : textarea.selectionEnd;
452 | const value = textarea.value;
453 | const startLen = start.length;
454 | const endLen = end.length;
455 |
456 | let valBefore = value.substring(0, selS);
457 | let valMid = value.substring(selS, selE);
458 | let valAfter = value.substring(selE);
459 | let generatedWrapper;
460 |
461 | // handle trailing spaces
462 | const trimmedSelection = valMid.match(/^(\s*)(\S?(?:.|\n|\r)*\S)(\s*)$/) || ['', '', '', ''];
463 |
464 | // determine if text is currently wrapped
465 | if (valBefore.endsWith(start) && valAfter.startsWith(end)) {
466 | textarea.value = valBefore.substring(0, valBefore.length - startLen) + valMid + valAfter.substring(endLen);
467 | textarea.selectionStart = valBefore.length - startLen;
468 | textarea.selectionEnd = (valBefore + valMid).length - startLen;
469 | textarea.focus();
470 | } else {
471 | valBefore += trimmedSelection[1];
472 | valAfter = trimmedSelection[3] + valAfter;
473 | valMid = trimmedSelection[2];
474 |
475 | generatedWrapper = start + valMid + end;
476 |
477 | textarea.value = valBefore + generatedWrapper + valAfter;
478 | textarea.selectionStart = valBefore.length + start.length;
479 | textarea.selectionEnd = (valBefore + generatedWrapper).length - end.length;
480 | textarea.focus();
481 | }
482 |
483 | sox.Stack.MarkdownEditor.refreshAllPreviews();
484 | },
485 | getCssProperty: function(element, propertyValue) {
486 | return window.getComputedStyle(element).getPropertyValue(propertyValue);
487 | },
488 | runAjaxHooks: function() {
489 | let originalOpen = XMLHttpRequest.prototype.open;
490 | XMLHttpRequest.prototype.open = function() {
491 | this.addEventListener('load', function() {
492 | for (const key in hookAjaxObject) {
493 | if (this.responseURL.match(new RegExp(key))) hookAjaxObject[key](); // if the URL matches the regex, then execute the respective function
494 | }
495 | });
496 | originalOpen.apply(this, arguments);
497 | }
498 | },
499 | addAjaxListener: function(regexToMatch, functionToExecute) {
500 | if (!regexToMatch) { // all information has been inserted in hookAjaxObject
501 | sox.helpers.runAjaxHooks();
502 | return;
503 | }
504 | hookAjaxObject[regexToMatch] = functionToExecute;
505 | },
506 | };
507 |
508 | sox.site = {
509 | types: {
510 | main: 'main',
511 | meta: 'meta',
512 | chat: 'chat',
513 | beta: 'beta',
514 | },
515 | id: (sox.exists('options.site.id') ? Stack.options.site.id : undefined),
516 | currentApiParameter: sox.helpers.getSiteNameFromLink(location.href),
517 | get name() {
518 | if (Chat) {
519 | return document.querySelector('#footer-logo a').title;
520 | } else { //using StackExchange object doesn't give correct name (eg. `Biology` is called `Biology Stack Exchange` in the object)
521 | return document.querySelector('.js-topbar-dialog-corral .modal-content.current-site-container .current-site-link div').title;
522 | }
523 | },
524 |
525 | get type() {
526 | if (Chat) {
527 | return this.types.chat;
528 | } else if (Stack) {
529 | if (sox.exists('options.site') && Stack.options.site.isMetaSite) {
530 | return this.types.meta;
531 | } else {
532 | // check if site is in beta or graduated
533 | if (document.querySelector('.beta-title')) {
534 | return this.types.beta;
535 | } else {
536 | return this.types.main;
537 | }
538 | }
539 | }
540 | return null;
541 | },
542 | get icon() {
543 | return 'favicon-' + document.querySelector('.current-site a:not([href*=\'meta\']) .site-icon').className.split('favicon-')[1];
544 | },
545 | url: location.hostname, // e.g. "meta.stackexchange.com"
546 | href: location.href, // e.g. "https://meta.stackexchange.com/questions/blah/blah"
547 | };
548 |
549 | sox.location = {
550 | // location helpers
551 | on: function(location) {
552 | return window.location.href.indexOf(location) > -1 ? true : false;
553 | },
554 | get onUserProfile() {
555 | return this.on('/users/');
556 | },
557 | get onQuestion() {
558 | return this.on('/questions/');
559 | },
560 | matchWithPattern: function(pattern, urlToMatchWith) { //commented version @ https://jsfiddle.net/shub01/t90kx2dv/
561 | if (pattern == 'SE1.0') { //SE.com && Area51.SE.com special checking
562 | if (urlToMatchWith) {
563 | if (urlToMatchWith.match(/https?:\/\/stackexchange\.com\/?/)
564 | || (sox.location.matchWithPattern('*://area51.stackexchange.com/*') && sox.site.href.indexOf('.meta.') === -1)) return true;
565 | } else {
566 | if (location.href.match(/https?:\/\/stackexchange\.com\/?/) ||
567 | (sox.location.matchWithPattern('*://area51.stackexchange.com/*') && sox.site.href.indexOf('.meta.') === -1)) return true;
568 | }
569 | return false;
570 | }
571 | let currentSiteScheme; let currentSiteHost; let currentSitePath;
572 | if (urlToMatchWith) {
573 | const split = urlToMatchWith.split('/');
574 | currentSiteScheme = split[0];
575 | currentSiteHost = split[2];
576 | currentSitePath = '/' + split.slice(-(split.length - 3)).join('/');
577 | } else {
578 | currentSiteScheme = location.protocol;
579 | currentSiteHost = location.hostname;
580 | currentSitePath = location.pathname;
581 | }
582 |
583 | const matchSplit = pattern.split('/');
584 | let matchScheme = matchSplit[0];
585 | let matchHost = matchSplit[2];
586 | let matchPath = matchSplit.slice(-(matchSplit.length - 3)).join('/');
587 |
588 | matchScheme = matchScheme.replace(/\*/g, '.*');
589 | matchHost = matchHost.replace(/\./g, '\\.').replace(/\*\\\./g, '.*.?').replace(/\\\.\*/g, '.*').replace(/\*$/g, '.*');
590 | matchPath = '^/' + matchPath.replace(/\//g, '\\/').replace(/\*/g, '.*');
591 |
592 | if (currentSiteScheme.match(new RegExp(matchScheme)) && currentSiteHost.match(new RegExp(matchHost)) && currentSitePath.match(new RegExp(matchPath))) {
593 | return true;
594 | }
595 | return false;
596 | },
597 | };
598 |
599 | sox.user = {
600 | get id() {
601 | if (sox.site.type == sox.site.types.chat) {
602 | return Chat ? Chat.RoomUsers.current().id : undefined;
603 | } else {
604 | return sox.exists('options.user.userId') ? Stack.options.user.userId : undefined;
605 | }
606 | },
607 | get rep() {
608 | if (sox.site.type == sox.site.types.chat) {
609 | return Chat.RoomUsers.current().reputation;
610 | } else {
611 | return sox.exists('options.user.rep') ? Stack.options.user.rep : undefined;
612 | }
613 | },
614 | get name() {
615 | if (sox.site.type == sox.site.types.chat) {
616 | return Chat.RoomUsers.current().name;
617 | } else {
618 | const username = document.querySelector('.s-topbar--item.s-user-card .s-avatar');
619 | return (username ? username.title : '');
620 | }
621 | },
622 | get loggedIn() {
623 | return sox.exists('options.user.isRegistered') ? Stack.options.user.isRegistered : undefined;
624 | },
625 | hasPrivilege: function(privilege) {
626 | if (this.loggedIn) {
627 | const rep = (sox.site.type == 'beta' ? commonInfo.privileges.beta[privilege] : commonInfo.privileges.graduated[privilege]);
628 | return this.rep > rep;
629 | }
630 | return false;
631 | },
632 | };
633 |
634 | })(window.sox = window.sox || {}, jQuery);
635 |
--------------------------------------------------------------------------------
/sox.css:
--------------------------------------------------------------------------------
1 | /* sprites */
2 | .sox-sprite {
3 | width: 15px;
4 | height: 15px;
5 | }
6 |
7 | /* -------- sox settings dialog ------- */
8 |
9 | #sox-settings-dialog {
10 | top: 47px;
11 | right: 208px;
12 | min-height: calc(100vh - 90px);
13 | width: 550px;
14 | display: none;
15 | }
16 |
17 | #sox-settings-dialog.new-topbar {
18 | min-height: calc(100vh - 75px);
19 | }
20 |
21 | #sox-settings-dialog .modal-content {
22 | max-height: none;
23 | min-height: inherit;
24 | }
25 |
26 | #sox-settings-dialog .header h3 {
27 | width: 100%;
28 | }
29 |
30 | #sox-settings-dialog-version {
31 | float: right;
32 | font-size: 14px;
33 | }
34 |
35 | #sox-settings-dialog #search-container {
36 | min-height: 0;
37 | width: 570px;
38 | left: 10px;
39 | }
40 |
41 | #sox-settings-dialog #search-container .sox-sprite-search {
42 | width: 20px;
43 | height: 20px;
44 | top: 5px;
45 | }
46 |
47 | #sox-settings-dialog:not(.new-topbar) #search-container .sox-sprite-search {
48 | position: relative;
49 | left: -485px;
50 | }
51 |
52 | #sox-settings-dialog.new-topbar #search-container .sox-sprite-search {
53 | position: relative;
54 | left: -485px;
55 | }
56 |
57 | #sox-settings-dialog #search-container #search {
58 | max-width: 90%;
59 | }
60 |
61 | #sox-settings-dialog-features {
62 | padding: 0 10px 10px 10px;
63 | overflow-y: scroll;
64 | height: calc(100vh - 250px);
65 | }
66 |
67 | #sox-settings-dialog-features .modal-content div {
68 | margin-bottom: 3px;
69 | padding: 2px;
70 | }
71 |
72 | #sox-settings-dialog-features .modal-content div:hover {
73 | background-color: var(--black-075);
74 | }
75 |
76 | #sox-settings-dialog-features .modal-content label>input {
77 | margin-right: 5px;
78 | }
79 |
80 | #sox-settings-dialog-actions {
81 | height: 40px;
82 | padding: 0 10px;
83 | }
84 |
85 | #sox-settings-dialog-save {
86 | margin-left: 5px;
87 | float: right;
88 | }
89 |
90 | #sox-settings-dialog-actions .action {
91 | float: right;
92 | padding: 13px 11px;
93 | }
94 |
95 | #sox-settings-dialog .sox-new-version-item {
96 | padding: 5px;
97 | }
98 |
99 | .sox-settings-button {
100 | background-image: none !important;
101 | font-size: 14px;
102 | cursor: pointer;
103 | text-align: center;
104 | }
105 |
106 | .sox-settings-button:hover, .sox-settings-button.is-selected {
107 | color: #9fa6ad;
108 | background-color: var(--theme-topbar-item-background-hover);
109 | }
110 |
111 | #sox-settings-dialog-check-toggle>i {
112 | padding: 14px 0;
113 | }
114 |
115 | #sox-settings-dialog-features textarea.featureSetting {
116 | min-width: 80%;
117 | }
118 |
119 | .network-items .fa:before {
120 | /*https://github.com/soscripted/sox/pull/127*/
121 | color: #9fa6ad;
122 | }
123 |
124 | .sox-feature-info,
125 | .sox-feature-settings {
126 | background-color: var(--black-025);
127 | border-radius: 9px;
128 | margin-left: 20px;
129 | padding: 8px;
130 | width: 90%;
131 | line-height: 1.5;
132 | padding: 5px !important;
133 | }
134 |
135 | .sox-feature .sox-sprite-info,
136 | .sox-feature .sox-sprite-wrench {
137 | font-size: 12px;
138 | cursor: pointer;
139 | }
140 |
141 | .sox-feature .sox-sprite-wrench {
142 | margin-left: 5px;
143 | transform: rotate(90deg);
144 | }
145 |
146 | .sox-feature .sox-sprite-info {
147 | float: right;
148 | width: 17px;
149 | height: 17px;
150 | fill: gray;
151 | }
152 |
153 | #sox-settings-dialog-features-packs {
154 | margin-bottom: 5px;
155 | }
156 |
157 | .sox-settings-dialog-feature-packs-list {
158 | padding: 10px;
159 | margin: 5px;
160 | }
161 |
162 | .sox-settings-dialog-feature-packs-list li.sox-settings-dialog-feature-pack {
163 | display: inline;
164 | margin: 5px;
165 | padding: 10px;
166 | border-radius: 9px;
167 | background-color: lightgrey;
168 | color: #3c4146; /* dark mode compatibility */
169 | cursor: pointer;
170 | }
171 |
172 | .sox-settings-dialog-feature-packs-list li.sox-settings-dialog-feature-pack.clear-feature-pack-selection {
173 | background-color: inherit;
174 | color: inherit;
175 | }
176 |
177 | #sox-settings-dialog .sox-feature.feature-fade-out, #sox-settings-dialog .sox-feature.disabled-feature {
178 | opacity: 0.5;
179 | }
180 |
181 | #sox-settings-dialog.dark-mode .sox-settings-dialog-feature-pack:hover {
182 | background-color: #3b9de2 !important;
183 | }
184 |
185 | #sox-settings-dialog.dark-mode .sox-settings-dialog-feature-pack,
186 | #sox-settings-dialog.dark-mode #sox-settings-dialog-features .modal-content div:hover {
187 | background-color: black !important;
188 | }
189 |
190 | #sox-settings-dialog.dark-mode .sox-feature-settings,
191 | #sox-settings-dialog.dark-mode .sox-feature-info {
192 | background-color: black;
193 | }
194 |
195 | #sox-settings-dialog.dark-mode .sox-sprite {
196 | fill: #ccc;
197 | }
198 |
199 | /* -------- sox specific features' CSS ------- */
200 |
201 | #sox-scrollToTop {
202 | position: fixed;
203 | right: 0;
204 | bottom: 0;
205 | background-color: rgba(200, 200, 200, .7);
206 | text-align: center;
207 | cursor: pointer;
208 | }
209 |
210 | #sox-scrollToTop .sox-sprite-top {
211 | width: 45px;
212 | height: 45px;
213 | }
214 |
215 | /*Main centered divs, SE-style:*/
216 |
217 | .sox-centered {
218 | width: 400px;
219 | z-index: 1001;
220 | top: 102px;
221 | left: 615.5px;
222 | display: inline-block;
223 | margin-top: -95.5px;
224 | margin-left: -216px;
225 | overflow: auto;
226 | height: 90%;
227 | }
228 |
229 | /*Specifically for quickCommentShortcuts -- the div's too wide! https://github.com/shu8/Stack-Overflow-Optional-Features/issues/36*/
230 |
231 | #quickCommentShortcuts.sox-centered {
232 | width: 75%;
233 | height: 90%;
234 | z-index: 1001;
235 | top: 20%;
236 | left: 14%;
237 | display: inline-block;
238 | overflow: auto;
239 | }
240 |
241 | /*standOutDupeCloseMigrated signs */
242 |
243 | .standOutDupeCloseMigrated-duplicate,
244 | .standOutDupeCloseMigrated-closed,
245 | .standOutDupeCloseMigrated-migrated,
246 | .standOutDupeCloseMigrated-onhold {
247 | color: #FFF;
248 | padding: 2px;
249 | border-radius: 4px;
250 | font-size: 12px;
251 | display: inline-block;
252 | }
253 |
254 | .standOutDupeCloseMigrated-duplicate {
255 | background: #FA0;
256 | }
257 |
258 | .standOutDupeCloseMigrated-closed {
259 | background: #F00;
260 | }
261 |
262 | .standOutDupeCloseMigrated-migrated {
263 | background: #19F;
264 | }
265 |
266 | .standOutDupeCloseMigrated-onhold {
267 | background: #808080;
268 | }
269 |
270 | /*metaNewQuestionAlert for the mod diamond and dialog */
271 |
272 | #metaNewQuestionAlertDialog {
273 | top: 50px;
274 | right: 242px;
275 | width: 377px;
276 | }
277 |
278 | #metaNewQuestionAlertDialogList:empty::after {
279 | content: "No new meta questions at this time.";
280 | }
281 |
282 | /*addHotText for the 'this question is hot' banner */
283 |
284 | .sox-hot .sox-sprite-hot {
285 | float: left;
286 | width: 35px;
287 | height: 35px;
288 | margin-right: 3px;
289 | fill: red;
290 | }
291 |
292 | .sox-hot.question-list .sox-sprite-hot {
293 | width: 20px;
294 | height: 20px;
295 | }
296 |
297 | /*quickCommentShortcutsMain: */
298 |
299 | .quickCommentShortcutsReminder {
300 | height: 40%;
301 | width: 13%;
302 | left: 0;
303 | top: 10%;
304 | position: fixed;
305 | background-color: gray;
306 | color: white;
307 | text-align: center;
308 | overflow: auto;
309 | }
310 |
311 | /*Side by Side Editing (SBS, addSBSBtn, startSBS): https://github.com/szego/SE-Answers_scripts/blob/side-by-side/editing-and-toggling/side-by-side-editing.user.js:*/
312 |
313 | #sidebar.sbs-on {
314 | display: none !important;
315 | }
316 |
317 | #content.sbs-on {
318 | width: 1360px !important;
319 | }
320 |
321 | .draft-saved.sbs-on {
322 | margin-left: 35px !important;
323 | }
324 |
325 | .draft-discarded.sbs-on {
326 | margin-left: 35px !important;
327 | }
328 |
329 | .draft-saved.sbs-on.sbs-newq {
330 | margin-left: 40px !important;
331 | height: 15px !important;
332 | float: left !important;
333 | }
334 |
335 | .draft-discarded.sbs-on.sbs-newq {
336 | margin-left: 40px !important;
337 | height: 15px !important;
338 | float: left !important;
339 | }
340 |
341 | .votecell.sbs-on {
342 | display: none !important;
343 | }
344 |
345 | .hide-preview.sbs-on {
346 | margin-left: 35px !important;
347 | }
348 |
349 | .post-editor.sbs-on {
350 | width: 1360px !important;
351 | }
352 |
353 | .sbs-on-left-side {
354 | width: 660px;
355 | }
356 |
357 | .wmd-button-bar.sbs-on {
358 | float: none !important;
359 | }
360 |
361 | .wmd-container.sbs-on {
362 | float: left !important;
363 | }
364 |
365 | .wmd-preview.sbs-on {
366 | width: 685px;
367 | clear: none !important;
368 | float: right !important;
369 | }
370 |
371 | .wmd-preview.sbs-on.sbs-newq {
372 | margin-top: 10px !important;
373 | }
374 |
375 | .tag-editor-p.sbs-on.sbs-newq {
376 | float: left !important;
377 | }
378 |
379 | .form-item.sbs-on.sbs-newq {
380 | float: left !important;
381 | }
382 |
383 | .post-form.sbs-on {
384 | /*https://github.com/soscripted/sox/issues/247*/
385 | width: 1335px;
386 | }
387 |
388 | .edit-comment.sbs-on {
389 | clear: both;
390 | position: initial;
391 | }
392 |
393 | /*linkedPostsInline for the displayed post text; https://github.com/shu8/Stack-Overflow-Optional-Features/issues/48 */
394 |
395 | .linkedPostsInline-loaded-body-sox {
396 | background-color: var(--blue-050);
397 | border-radius: 20px;
398 | overflow-wrap: break-word;
399 | width: auto;
400 | padding: 10px;
401 | -webkit-box-shadow: inset 1px 1px 4px 1px rgba(0, 0, 0, 0.5);
402 | -moz-box-shadow: inset 1px 1px 4px 1px rgba(0, 0, 0, 0.5);
403 | box-shadow: inset 1px 1px 4px 1px rgba(0, 0, 0, 0.5);
404 | }
405 |
406 | .linkedPostsInline-loaded-body-sox .post-text {
407 | width: auto;
408 | font-size: 90%;
409 | }
410 |
411 | /*findAndReplace*/
412 |
413 | .findReplaceToolbar {
414 | display: block;
415 | background-color: transparent;
416 | padding: 0 12px;
417 | border: 1px solid #c8ccd0;
418 | border-bottom: 0;
419 | border-top: 0;
420 | }
421 |
422 | #findReplace {
423 | cursor: pointer;
424 | }
425 |
426 | .findReplaceToolbar.findReplace>input[type='text'] {
427 | height: 10px;
428 | width: 25%;
429 | }
430 |
431 | .findReplaceToolbar.findReplace>input[type='button'] {
432 | width: 10%;
433 | height: 100%;
434 | }
435 |
436 | /*chatEasyAccess - for the links (b elements)*/
437 |
438 | .chatEasyAccess b {
439 | cursor: pointer;
440 | }
441 |
442 | /*highlightQuestions -- for the tags*/
443 |
444 | .sox-tagged-interesting {
445 | position: relative;
446 | }
447 |
448 | .sox-tagged-interesting:before {
449 | content: '';
450 | display: block;
451 | position: absolute;
452 | top: 0;
453 | left: 0;
454 | width: 2px;
455 | height: 100%;
456 | background: black;
457 | }
458 |
459 | /*commentReplies -- for the reply buttons*/
460 |
461 | .soxReplyLink {
462 | cursor: pointer;
463 | float: right;
464 | }
465 |
466 | /*flagPercentageBar -- for the percentage bar itself*/
467 |
468 | #sox-flagPercentProgressBar {
469 | background: var(--black-025);
470 | height: 10px;
471 | width: 250px;
472 | margin: 6px 10px 10px 0;
473 | padding: 0px;
474 | margin: auto;
475 | }
476 |
477 | #sox-flagPercentProgressBar:after {
478 | content: '';
479 | display: block;
480 | height: 100%;
481 | }
482 |
483 | #sox-flagPercentHelpful {
484 | margin-bottom: 5px;
485 | text-align: center;
486 | }
487 |
488 | .sox-flagPercentProgressBar-container {
489 | background-color: var(--black-025);
490 | }
491 |
492 | /*sox-copyCode -- for the button and textarea (completely hide it)*/
493 |
494 | .sox-copyCodeButton {
495 | float: right;
496 | position: sticky;
497 | top: 0;
498 | /* z-index for compatibility with https://github.com/SmartManoj/SmartUserScripts/blob/master/SO_Lines.user.js */
499 | z-index: 50;
500 | display: none;
501 | /* relative position prevents code from being in front of button */
502 | position: relative;
503 | background-color: var(--highlight-bg);
504 | margin-left: -15px;
505 | }
506 |
507 | .sox-copyCodeTextarea {
508 | position: fixed;
509 | top: 0;
510 | left: 0;
511 | height: 1px;
512 | width: 1px;
513 | padding: 0;
514 | border: 0;
515 | outline: 0;
516 | box-shadow: none;
517 | background: transparent;
518 | overflow: hidden;
519 | }
520 |
521 | /*openLinksInNewTab -- for the external link sign*/
522 |
523 | .sox-openLinksInNewTab-externalLink {
524 | display: inline !important;
525 | margin-left: 3px;
526 | }
527 |
528 | /*colorAnswerer -- for the username*/
529 |
530 | .sox-answerer {
531 | color: var(--blue-700);
532 | background-color: var(--bc-medium);
533 | padding: 1px 5px !important;
534 | border-radius: 3px;
535 | }
536 |
537 | /*hotNetworkQuestionsFiltering -- for the questions to hide*/
538 |
539 | .sox-hot-network-question-filter-hide {
540 | display: none !important;
541 | }
542 |
543 | /*addAuthorNameToInboxNotifications -- for the author span*/
544 |
545 | .sox-notification-author {
546 | color: #848d95;
547 | }
548 |
549 | /*hotNetworkQuestionsFiltering -- for the tags icon and tags list shown after hovering over it*/
550 |
551 | .getQuestionTags {
552 | margin-left: 5px;
553 | }
554 |
555 | .sox-hnq-question-tags-tooltip {
556 | display: block;
557 | margin-top: 5px;
558 | margin-left: 5px;
559 | background-color: #eeeefe;
560 | border: 1px solid darkgrey;
561 | font-size: 11px;
562 | padding: 2px;
563 | white-space: normal;
564 | }
565 |
566 | /* onlyShowCommentActionsOnHover -- the function just adds this CSS to avoid having to do it in JS*/
567 | /* thanks @Makyen */
568 |
569 | .sox-onlyShowCommentActionsOnHover:not(:hover) .comment-up-off,
570 | .sox-onlyShowCommentActionsOnHover:not(:hover) .js-comment-flag:not(.fc-red-500) {
571 | visibility: hidden;
572 | }
573 |
574 | /* autoShowCommentImages -- for the image displayed */
575 | .sox-autoShowCommentImages-image {
576 | padding: 2px;
577 | margin: 2px;
578 | border: 1px dotted black;
579 | display: block;
580 | }
581 |
582 | /* editComment -- for the dialog and checkboxes shown when editing */
583 | .sox-editComment-currentValues {
584 | border: 1px solid black;
585 | padding: 12px;
586 | }
587 |
588 | .sox-editComment-deleteDialogButton,
589 | .sox-editComment-editDialogButton {
590 | min-height: 2.2em !important;
591 | padding: 5px !important;
592 | font-size: 12px;
593 | margin-left: 5px;
594 | }
595 |
596 | .sox-editComment-reason {
597 | display: inline-block !important;
598 | background-color: inherit;
599 | color: inherit;
600 | margin-top: 7px;
601 | padding: 3px;
602 | }
603 |
604 | .sox-editComment-reason:hover {
605 | background-color: gray;
606 | color: white;
607 | }
608 |
609 | #currentValues section {
610 | display: inline-block !important;
611 | padding: 0px 10px;
612 | }
613 |
614 | /* customMagicLinks -- for the settings table in the dialog */
615 | .sox-customMagicLinks-settings-table {
616 | margin-bottom: 10px;
617 | }
618 |
619 | .sox-customMagicLinks-settings-table td,
620 | .sox-customMagicLinks-settings-table th {
621 | border: 1px solid #ddd;
622 | padding: 8px;
623 | }
624 |
625 | .sox-customMagicLinks-settings-table th {
626 | font-weight: bold;
627 | }
628 |
629 | /* linkedToFrom -- for the >/< chevrons */
630 | .sox-linkedToFrom-chevron {
631 | width: 22px;
632 | height: 22px;
633 | float: right;
634 | }
635 |
636 | /* quickAuthorInfo -- for the user details */
637 | .sox-quickAuthorInfo-details {
638 | color: #848d95;
639 | font-size: 11px;
640 | padding-top: 38px
641 | }
642 |
643 | .sox-quickAuthorInfo-details .sox-sprite-access_time {
644 | margin-right: 5px;
645 | }
646 |
647 | .sox-quickAuthorInfo-unregistered {
648 | margin-left: 5px;
649 | font-size: smaller;
650 | }
651 |
652 | .sox-last-seen {
653 | position: relative;
654 | bottom: 2.5px;
655 | }
656 |
657 | /* markEmployees -- for the SE icon */
658 | .sox-markEmployees-logo {
659 | margin-left: 5px;
660 | height: 15px;
661 | width: 15px;
662 | }
663 |
664 | /* openImagesAsModals -- for the small '(source)' link in header */
665 | .sox-openImagesAsModals-sourceLink {
666 | font-size: 18px;
667 | margin-left: 10px;
668 | font-weight: bold;
669 | font-style: italic;
670 | }
671 |
672 | /* sox-scrollChatRoomsList -- for the user popup and sidebar scrollbar */
673 | .sox-scrollChatRoomsList-user-popup {
674 | max-height: calc(100vh - 90px);
675 | }
676 |
677 | .sox-scrollChatRoomsList-user-popup ul.no-bullets {
678 | max-height: calc(100vh - 500px);
679 | overflow-y: auto;
680 | }
681 |
682 | .sox-scrollChatRoomsList-sidebar {
683 | max-height: calc(40vh);
684 | overflow: auto;
685 | }
686 |
687 | .sox-scrollChatRoomsList-sidebar > #my-rooms {
688 | height: 100%;
689 | overflow: auto;
690 | }
691 |
692 | /* tabularReviewerStats -- for the table with user's stats */
693 | .sox-tabularReviewerStats-table tr,
694 | .sox-tabularReviewerStats-table th,
695 | .sox-tabularReviewerStats-table td:not(:first-child) {
696 | border: 1px solid #cccccc;
697 | border-collapse: collapse;
698 | }
699 |
700 | .sox-tabularReviewerStats-table th, .sox-tabularReviewerStats-table td {
701 | padding: 4px;
702 | }
703 |
704 | /* stickyVoteButtons -- for the vote buttons on the left of the post */
705 | .sox-stickyVoteButtons {
706 | z-index: 2;
707 | }
708 |
709 | .sox-stickyVoteButtons > .js-voting-container {
710 | position: -webkit-sticky;
711 | position: sticky;
712 | }
713 |
714 | .sox-stickyVoteButtons .s-popover {
715 | width: max-content;
716 | }
717 |
718 | /* displayName -- for the CSS of the username */
719 | .sox-displayName {
720 | color: color(--white);
721 | padding-right: 12px;
722 | font-size: 13px;
723 | }
724 |
725 | /* flagPercentages -- for the percentages */
726 | .sox-percentage-span {
727 | margin-left: 5px;
728 | color: #999;
729 | font-size: 12px;
730 | }
731 |
732 | /* topAnswers -- for the scores of the answers */
733 | #sox-top-answers {
734 | padding-bottom: 10px;
735 | border-bottom: 1px solid #eaebec;
736 | }
737 |
738 | /* warnNotLoggedIn -- for the div warning the user */
739 | #loggedInReminder {
740 | position: fixed;
741 | right: 0;
742 | bottom: 50px;
743 | background-color: rgba(200, 200, 200, 1);
744 | width: 200px;
745 | text-align: center;
746 | padding: 5px;
747 | color: black;
748 | font-weight: bold;
749 | }
750 |
751 | /* disableVoteButtons -- for the CSS of the disabled voting buttons */
752 | .sox-disabled-button {
753 | cursor: default;
754 | opacity: 0.5;
755 | pointer-events: none;
756 | }
757 |
758 | /* addTagsToHNQs -- for the tags' span */
759 | .sox-addTagsToHNQs-span {
760 | color: grey;
761 | display: block;
762 | margin-bottom: 10px;
763 | }
764 |
--------------------------------------------------------------------------------
/sox.dialog.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
14 |
15 |
16 |
19 |
20 | Major UI tweaks
21 | Key features
22 | Power User features
23 | clear
24 |
25 |
26 |
27 |
45 |
46 |
--------------------------------------------------------------------------------
/sox.dialog.js:
--------------------------------------------------------------------------------
1 | (function(sox, $) {
2 | 'use strict';
3 |
4 | sox.dialog = {
5 | init: function(options) {
6 | if (!$('.s-topbar').length) return;
7 | sox.debug('initializing SOX dialog');
8 |
9 | const version = options.version;
10 | const features = options.features;
11 | const settings = options.settings;
12 | const lastVersionInstalled = options.lastVersionInstalled;
13 | const html = GM_getResourceText('dialog');
14 | const $soxSettingsDialog = $(html);
15 | const $soxSettingsDialogFeatures = $soxSettingsDialog.find('#sox-settings-dialog-features');
16 | const $soxSettingsDialogVersion = $soxSettingsDialog.find('#sox-settings-dialog-version');
17 | const $soxSettingsSave = $soxSettingsDialog.find('#sox-settings-dialog-save');
18 | const $soxSettingsReset = $soxSettingsDialog.find('#sox-settings-dialog-reset');
19 | const $soxSettingsDebugging = $soxSettingsDialog.find('#sox-settings-dialog-debugging');
20 | const $soxSettingsNewAccessTokenButton = $soxSettingsDialog.find('#sox-settings-dialog-access-token');
21 | const $soxSettingsToggle = $soxSettingsDialog.find('#sox-settings-dialog-check-toggle');
22 | const $soxSettingsClose = $soxSettingsDialog.find('#sox-settings-dialog-close');
23 | const $searchBox = $soxSettingsDialog.find('#search');
24 | const $importSettingsButton = $soxSettingsDialog.find('#sox-settings-import');
25 | const $exportSettingsButton = $soxSettingsDialog.find('#sox-settings-export');
26 | const $featurePackButtons = $soxSettingsDialog.find('.sox-settings-dialog-feature-pack');
27 |
28 | // Array of HTML strings that will be displayed as `li` items if the user has installed a new version.
29 | const changes = [
30 | "Fix bugs in various features due to SE layout changes",
31 | "Add feature to add answer count to question header",
32 | "Behind-the-scenes performance improvements (e.g., reduce usage of jQuery for efficiency - thanks @double-beep!)",
33 | 'Deprecate "align badges by their class on user profile pages" feature (now natively implemented!)',
34 | 'Deprecate "differentiate spoilers from empty blockquotes" (now native!)',
35 | ];
36 |
37 | function addCategory(name) {
38 | const $div = $('
', {
39 | 'class': 'header category',
40 | 'id': 'header-for-' + name,
41 | });
42 |
43 | const $h3 = $(' ', {
44 | text: name.toLowerCase(),
45 | });
46 |
47 | const $content = $('
', {
48 | id: name,
49 | 'class': 'modal-content features',
50 | });
51 | $div.append($h3);
52 |
53 | if (!$soxSettingsDialogFeatures.find('div#header-for-' + name).length) {
54 | $soxSettingsDialogFeatures.append($div);
55 | $div.after($content);
56 | }
57 | }
58 |
59 | function addFeature(category, name, description, featureSettings, extendedDescription, metaLink, featurePacks, usesApi) {
60 | const blockFeatureSelection = usesApi && !sox.settings.accessToken;
61 |
62 | const $div = $('
', {
63 | 'class': 'sox-feature ' + (featurePacks.length ? featurePacks.join(' ') : '') + (blockFeatureSelection ? ' disabled-feature' : ''),
64 | 'title': blockFeatureSelection ? 'You must get an access token to enable this feature (click the key button at the bottom of the SOX dialog)' : '',
65 | });
66 |
67 | const $info = $(sox.sprites.getSvg('info')).hover(function() {
68 | if (extendedDescription && !$(this).parent().find('.sox-feature-info').length) {
69 | $(this).parent().append($('
', {
70 | 'class': 'sox-feature-info',
71 | 'html': extendedDescription + (metaLink ? ' [meta] ' : ''),
72 | }));
73 | }
74 | });
75 |
76 | const $label = $(' ');
77 | const $input = $(' ', {
78 | id: name,
79 | 'class': 's-checkbox',
80 | type: 'checkbox',
81 | }).prop('disabled', blockFeatureSelection);
82 |
83 | $div.on('mouseleave', function() {
84 | $(this).find('.sox-feature-info').remove();
85 | }).append($label).append(extendedDescription ? $info : '');
86 | $label.append($input);
87 | $input.after(description);
88 | $soxSettingsDialogFeatures.find('#' + category).append($div);
89 |
90 | if (featureSettings) {
91 | const $settingsDiv = $('
', {
92 | id: 'feature-settings-' + name,
93 | 'class': 'sox-feature-settings',
94 | style: 'display: none; margin-top: 5px;',
95 | });
96 |
97 | const $settingsToggle = $(sox.sprites.getSvg('wrench', 'Edit this feature\'s settings')).click(e => {
98 | e.preventDefault(); //don't uncheck the checkbox
99 |
100 | const $settingsPanel = $('#feature-settings-' + name);
101 |
102 | if ($settingsPanel.is(':visible')) {
103 | $settingsPanel.fadeOut();
104 | } else {
105 | $settingsPanel.fadeIn();
106 | }
107 | });
108 |
109 | const optionalSettings = GM_getValue('SOX-' + name + '-settings', -1);
110 |
111 | for (let i = 0; i < featureSettings.length; i++) {
112 | const currentSetting = featureSettings[i];
113 | $settingsDiv
114 | .append(sox.helpers.newElement(currentSetting.type, { //use newElement helper so the type can be things like 'checkbox' or 'radio'
115 | id: currentSetting.id,
116 | 'class': currentSetting.type === 'textarea' ? 'featureSetting s-input' : 'featureSetting s-checkbox',
117 | 'style': 'margin-right: 5px',
118 | 'checked': (currentSetting.type === 'checkbox' ? JSON.parse(optionalSettings)[currentSetting.id] : false),
119 | value: (optionalSettings === -1 ? '' : JSON.parse(optionalSettings)[currentSetting.id]),
120 | }))
121 | .append(currentSetting.desc)
122 | .append(' ');
123 | }
124 |
125 | const $saveFeatureSettings = $(' ', {
126 | id: 'saveSettings-' + name,
127 | 'class': 'action s-btn s-btn__secondary s-btn__sm',
128 | text: 'Save Settings',
129 | click: function(e) {
130 | e.preventDefault(); //don't uncheck the checkbox
131 | const settingsToSave = {};
132 | $('.sox-feature.disabled-feature input[type="checkbox"]').prop('checked', false); //uncheck any features that somehow were checked (they shouldn't be able to through the UI) but should be disabled (user doesn't have access token)
133 | $(this).parent().find('.featureSetting').each(function() {
134 | settingsToSave[$(this).attr('id')] = ($(this).is(':checkbox') ? $(this).is(':checked') : $(this).val());
135 | });
136 | GM_setValue('SOX-' + name + '-settings', JSON.stringify(settingsToSave));
137 | sox.settings.writeToConsole(true);
138 | alert('Saved!');
139 | },
140 | });
141 |
142 | $settingsDiv.append($saveFeatureSettings);
143 |
144 | const $feature = $soxSettingsDialogFeatures.find('input#' + name).parent();
145 | $feature.append($settingsToggle);
146 |
147 | if ($div.has('.sox-sprite-info').length) {
148 | $info.after($settingsDiv);
149 | } else {
150 | $feature.append($settingsDiv);
151 | }
152 | }
153 | }
154 |
155 | // display sox version number in the dialog
156 | if (version != 'unknown' && version !== null) {
157 | $soxSettingsDialogVersion.text(' v' + (version ? version.toLowerCase() : ''));
158 | } else {
159 | $soxSettingsDialogVersion.text('');
160 | }
161 |
162 | if (version !== lastVersionInstalled) {
163 | GM_setValue('SOX-lastVersionInstalled', version);
164 | const $newVersionDetailsContainer = $('
', {
165 | 'class': 'sox-new-version-details',
166 | });
167 |
168 | const $newVersionHeader = $('
', {
169 | 'class': 'header category',
170 | 'html': 'new in this version ',
171 | });
172 |
173 | const $changes = $('');
174 |
175 | for (let i = 0; i < changes.length; i++) {
176 | $changes.append($(' ', {
177 | 'html': changes[i], //this array is defined near the top of the file
178 | 'class': 'sox-new-version-item',
179 | }));
180 | }
181 |
182 | const $newVersionInfoContainer = $('
', {
183 | 'class': 'modal-content',
184 | 'html': $changes,
185 | });
186 |
187 | $soxSettingsDialogFeatures.append($newVersionDetailsContainer.append($newVersionHeader).append($newVersionInfoContainer));
188 | }
189 |
190 | if (sox.info.debugging) $soxSettingsDebugging.text('Disable debugging');
191 |
192 | // wire up event handlers
193 | $soxSettingsClose.on('click', () => {
194 | $soxSettingsDialog.hide();
195 | });
196 |
197 | $soxSettingsReset.on('click', () => {
198 | if (confirm('Are you sure you want to reset SOX?')) {
199 | sox.settings.reset();
200 | location.reload(); // reload page to reflect changed settings
201 | }
202 | });
203 |
204 | $soxSettingsDebugging.on('click', () => {
205 | const currentState = sox.info.debugging;
206 | if (typeof currentState === 'undefined') {
207 | GM_setValue('SOX-debug', true);
208 | $soxSettingsDebugging.text('Disable debugging');
209 | } else {
210 | GM_setValue('SOX-debug', !currentState);
211 | $soxSettingsDebugging.text(currentState ? 'Enable debugging' : 'Disable debugging');
212 | }
213 | location.reload();
214 | });
215 |
216 | $soxSettingsNewAccessTokenButton.on('click', () => {
217 | window.open('https://stackexchange.com/oauth/dialog?client_id=7138&scope=no_expiry&redirect_uri=http://soscripted.github.io/sox/');
218 | sox.loginfo('To get a new access token, please go to the following URL', 'https://stackexchange.com/oauth/dialog?client_id=7138&scope=no_expiry&redirect_uri=http://soscripted.github.io/sox/');
219 | });
220 |
221 | $soxSettingsToggle.on('click', function() {
222 | const $icon = $(this).find('i');
223 |
224 | const checked = $icon.hasClass('fas');
225 |
226 | if (checked) {
227 | $icon.removeClass('fas').addClass('far');
228 | } else {
229 | $icon.removeClass('far').addClass('fas');
230 | }
231 |
232 | $soxSettingsDialogFeatures.find('input').prop('checked', !checked);
233 | });
234 |
235 | $soxSettingsSave.on('click', () => {
236 | const settings = [];
237 | $soxSettingsDialogFeatures.find('input[type=checkbox]:checked').not('.featureSetting').each(function() { //NOT the per-feature featureSetting checkboxes, because they are saved in THEIR OWN setting object!
238 | const x = $(this).closest('.modal-content').attr('id') + '-' + $(this).attr('id');
239 | settings.push(x); //Add the function's ID (also the checkbox's ID) to the array
240 | });
241 |
242 | sox.settings.save(settings);
243 | location.reload(); // reload page to reflect changed settings
244 | });
245 |
246 | $importSettingsButton.on('click', () => {
247 | const settingsToImport = window.prompt('Please paste the settings exactly as they were given to you');
248 | if (!settingsToImport) return;
249 | sox.settings.save(settingsToImport);
250 | location.reload();
251 | });
252 |
253 | $exportSettingsButton.on('click', () => {
254 | window.prompt('Your settings are below. Press Ctrl/Cmd + C to copy.', JSON.stringify(sox.settings.load()));
255 | });
256 |
257 | $searchBox.on('keyup keydown', function() { //search box
258 | if ($(this).val() !== '') {
259 | const searchQuery = $(this).val();
260 | $('.sox-new-version-details').hide();
261 | $('#sox-settings-dialog .sox-feature').each(function() {
262 | if ($(this).find('label').text().toLowerCase().indexOf(searchQuery) == -1) {
263 | $(this).hide();
264 | } else {
265 | $(this).show();
266 | }
267 | });
268 | } else {
269 | $('.category, .features, #sox-settings-dialog .sox-feature, .sox-new-version-details').fadeIn();
270 | }
271 | });
272 |
273 | $featurePackButtons.click(function() {
274 | $('#sox-settings-dialog .sox-feature').removeClass('feature-fade-out');
275 | if ($(this).is('.clear-feature-pack-selection')) return;
276 | $('#sox-settings-dialog .sox-feature').not('.' + $(this).attr('data-feature-pack-id')).addClass('feature-fade-out');
277 | });
278 |
279 | // create sox settings button
280 | const $soxSettingsButton = $(' ', {
281 | id: 'soxSettingsButton',
282 | 'class': 'sox-settings-button s-topbar--item',
283 | title: 'Change SOX settings',
284 | href: '#',
285 | click: function(e) {
286 | e.preventDefault();
287 | $('#sox-settings-dialog').toggle();
288 | if ($soxSettingsDialog.is(':visible')) {
289 | $(this).addClass('is-selected');
290 | $soxSettingsDialog.find('#search').focus();
291 | $soxSettingsDialog.css('right', 'calc(95vw - ' + $(e.target).offset().left + 'px)');
292 | } else {
293 | $(this).removeClass('is-selected');
294 | }
295 | },
296 | });
297 |
298 | // Very basic 'dark theme' support. See https://github.com/soscripted/sox/issues/406
299 | // Not sure what the best way to detect dark mode is; the following just checks to
300 | // see if 's text color is #ccc (grey/rgb(204,204,204)). Might need changing
301 | // in future
302 | if ($('body').css('color') === 'rgb(204, 204, 204)') {
303 | sox.debug('Dark mode detected, tweaking SOX CSS');
304 | $soxSettingsDialog.addClass('dark-mode');
305 | }
306 |
307 | const $icon = $(sox.sprites.getSvg('settings', 'Change your SOX settings')).css({
308 | fill: $('.top-bar .-secondary .-link').css('color'),
309 | width: '25px',
310 | height: '25px',
311 | }).addClass('svg-icon iconInbox');
312 |
313 | //close dialog if clicked outside it
314 | $(document).click(e => { //close dialog if clicked outside it
315 | const $target = $(e.target);
316 | const isToggle = $target.is('#soxSettingsButton, #sox-settings-dialog');
317 | const isChild = $target.parents('#soxSettingsButton, #sox-settings-dialog').is('#soxSettingsButton, #sox-settings-dialog');
318 |
319 | if (!isToggle && !isChild) {
320 | $soxSettingsDialog.hide();
321 | $soxSettingsButton.removeClass('is-selected');
322 | }
323 | });
324 |
325 | //close dialog if one of the links on the topbar is clicked
326 | $('.s-topbar--content .s-topbar--item').not('.sox-settings-button').click(() => {
327 | $soxSettingsDialog.hide();
328 | $soxSettingsButton.removeClass('is-selected');
329 | });
330 |
331 | // load features into dialog
332 | sox.debug('injecting features into dialog');
333 | for (const category in features.categories) {
334 | addCategory(category);
335 | for (const feature in features.categories[category]) {
336 | const currentFeature = features.categories[category][feature];
337 | addFeature(
338 | category,
339 | currentFeature.name,
340 | currentFeature.desc,
341 | (currentFeature.settings ? currentFeature.settings : false), //add the settings panel for this feautre if indicated in the JSON
342 | (currentFeature.extended_description ? currentFeature.extended_description : false), //add the extra description on hover if the feature has the extended description
343 | (currentFeature.meta ? currentFeature.meta : false), //add the meta link to the extra description on hover
344 | (currentFeature.feature_packs ? currentFeature.feature_packs : []),
345 | (currentFeature.usesApi ? currentFeature.usesApi : false)
346 | );
347 | }
348 | }
349 |
350 | if (settings) {
351 | for (let i = 0; i < settings.length; ++i) {
352 | $soxSettingsDialogFeatures.find('#' + settings[i].split('-')[1]).prop('checked', true);
353 | }
354 | } else { // no settings found, mark all inputs as checked and display settings dialog
355 | //note: the .not() is to make sure any disabled features (user doesn't have access token) aren't selected by default
356 | $soxSettingsDialogFeatures.find('input').not('.sox-feature.disabled-feature input').prop('checked', true);
357 | $soxSettingsButton.addClass('topbar-icon-on');
358 | $soxSettingsDialog.show();
359 | }
360 |
361 | // add dialog to corral and sox button to topbar
362 | $soxSettingsButton.append($icon);
363 | const $loggedInTopbarTarget = $('.s-topbar--item.s-user-card');
364 | const $loggedOutTopbarTarget = $(".s-topbar--item.js-help-button");
365 | if ($loggedInTopbarTarget.length) {
366 | $loggedInTopbarTarget.parent().after($(' ').append($soxSettingsButton));
367 | } else {
368 | $loggedOutTopbarTarget.parent().before($(' ').append($soxSettingsButton));
369 | }
370 |
371 | $soxSettingsDialog.css({
372 | 'top': $('.top-bar').height(),
373 | 'right': $('.-container').outerWidth() - $('#soxSettingsButton').parent().position().left - $('#soxSettingsButton').outerWidth(),
374 | });
375 |
376 | //only add dialog if button was added successfully
377 | if ($('#soxSettingsButton').length) $('.js-topbar-dialog-corral').append($soxSettingsDialog);
378 | },
379 | };
380 | })(window.sox = window.sox || {}, jQuery);
381 |
--------------------------------------------------------------------------------
/sox.features.info.json:
--------------------------------------------------------------------------------
1 | {
2 | "categories": {
3 | "Appearance": [{
4 | "name": "addAuthorNameToInboxNotifications",
5 | "desc": "Add the author's name to notifications in the inbox",
6 | "extended_description": "If you receive a comment/answer/suggested edit notification, this feature will add the name of the author of the comment/answer/edit to the notification",
7 | "meta": "",
8 | "match": "",
9 | "exclude": "",
10 | "settings": [{
11 | "id": "addNameBeforeMessageOrAtTop",
12 | "type": "checkbox",
13 | "desc": "Show name before contents (e.g. [name]: message) rather than at top (e.g. 'comment by [name]')?"
14 | }],
15 | "feature_packs": ["power_user"],
16 | "usesApi": true
17 | }, {
18 | "name": "answerTagsSearch",
19 | "desc": "Show tags for the question an answer belongs to on search pages (for better context)",
20 | "extended_description": "By default, any search results that are answers do not show its question's tags. This feature adds the question's tags underneath the result.",
21 | "meta": "https://meta.stackexchange.com/q/197874",
22 | "match": "*://*.com/search*",
23 | "exclude": "SE1.0",
24 | "feature_packs": ["power_user"],
25 | "usesApi": true
26 | }, {
27 | "name": "colorAnswerer",
28 | "desc": "Color answerer's comments",
29 | "extended_description": "Highlight the username of a commenter if they have posted an answer on that page.",
30 | "meta": "https://meta.stackexchange.com/q/19574",
31 | "match": "*://*/questions/*",
32 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
33 | "feature_packs": ["major_ui", "key_feature", "power_user"]
34 | }, {
35 | "name": "displayName",
36 | "desc": "Add display name before gravatar in topbar",
37 | "meta": "https://meta.stackexchange.com/q/209992",
38 | "match": "",
39 | "exclude": "*://chat.*.com/*,SE1.0"
40 | }, {
41 | "name": "dragBounty",
42 | "desc": "Make bounty box draggable",
43 | "meta": "https://meta.stackexchange.com/q/170125",
44 | "match": "*://*/questions/*",
45 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
46 | }, {
47 | "name": "highlightQuestions",
48 | "desc": "Change highlighting for questions with favourite tags",
49 | "extended_description": "Changes the favourite tag question highlighting to be a more subtle, coloured left-border",
50 | "meta": "https://meta.stackexchange.com/q/238591",
51 | "match": "",
52 | "exclude": "*://chat.*.com/*,*://*/questions/ask",
53 | "feature_packs": ["major_ui", "key_feature", "power_user"]
54 | }, {
55 | "name": "isQuestionHot",
56 | "desc": "Add a label on questions which are hot-network questions",
57 | "extended_description": "If the question you are currently viewing is HOT, a flame icon is added next to the title",
58 | "meta": "https://meta.stackexchange.com/q/245390",
59 | "match": "*://*/questions/*,*://*/search*,*://*/?",
60 | "exclude": "SE1.0,*://*/questions/ask,*://meta.*/*",
61 | "usesApi": true
62 | }, {
63 | "name": "localTimestamps",
64 | "desc": "Convert timestamps to your local time",
65 | "extended_description": "Displays timestamps based on your time zone. You can also display time in 12-hour format by checking the option.",
66 | "meta": "",
67 | "match": "",
68 | "exclude": "SE1.0",
69 | "settings": [{
70 | "id": "twelveHours",
71 | "type": "checkbox",
72 | "desc": "Use 12-hour format?"
73 | }]
74 | }, {
75 | "name": "metaChatBlogStackExchangeButton",
76 | "desc": "Show meta and chat buttons on hover of a site under the site switcher button",
77 | "meta": "https://meta.stackexchange.com/q/256183",
78 | "match": "",
79 | "exclude": "*://chat.*.com/*"
80 | }, {
81 | "name": "metaNewQuestionAlert",
82 | "desc": "Add a mod diamond to the topbar to alert you of new questions on the site's child-meta",
83 | "meta": "https://meta.stackexchange.com/q/256318",
84 | "match": "",
85 | "exclude": "*://chat.*.com/*,SE1.0",
86 | "feature_packs": ["power_user"],
87 | "usesApi": true
88 | }, {
89 | "name": "scrollToTop",
90 | "desc": "Add Scroll To Top button",
91 | "extended_description": "This feature adds an up arrow at the bottom-right of your screen, that will jump to the top of the page when clicked",
92 | "meta": "",
93 | "match": "",
94 | "exclude": "*://chat.*.com/*"
95 | }, {
96 | "name": "standOutDupeCloseMigrated",
97 | "desc": "Add highlighted tags to closed/on hold/duplicate/migrated questions on question lists and review",
98 | "extended_description": "Adds a coloured box at the end of a title (that replaces the standard [duplicate], etc.) in question lists to more easily tell what the state of a question is",
99 | "meta": "https://meta.stackexchange.com/q/257021",
100 | "match": "*://*.*/,*://*.*/questions*,*://*.*/review*",
101 | "exclude": "*://chat.*.com/*,SE1.0",
102 | "feature_packs": ["major_ui, key_feature", "power_user"],
103 | "usesApi": true
104 | }, {
105 | "name": "tabularReviewerStats",
106 | "desc": "Display reviewer stats on /review/suggested-edits in table form",
107 | "meta": "https://meta.stackexchange.com/q/276946",
108 | "match": "*://*.com/review/suggested-edits/*",
109 | "exclude": "",
110 | "feature_packs": ["power_user"]
111 | }, {
112 | "name": "topAnswers",
113 | "desc": "Improve answer visibility by listing top answers",
114 | "meta": "",
115 | "match": "*://*/questions/*",
116 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
117 | }, {
118 | "name": "unspoil",
119 | "desc": "Add a link to the bottom of a post to reveal all spoilers in a post",
120 | "meta": "https://meta.stackexchange.com/q/249808",
121 | "match": "*://*/questions*",
122 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
123 | }, {
124 | "name": "addTimelineAndRevisionLinks",
125 | "desc": "Add a revision link to the bottom of each post for quick access",
126 | "meta": "",
127 | "match": "*://*/questions/*,*://*/review*",
128 | "exclude": "*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
129 | "feature_packs": ["power_user"]
130 | }, {
131 | "name": "showTagWikiLinkOnTagPopup",
132 | "desc": "Add a link to the tag wiki page on the popup that appears when hovering over a tag",
133 | "meta": "",
134 | "match": "*://*/questions*,*://*/review*,*://*/tags*",
135 | "exclude": "",
136 | "feature_packs": ["power_user"]
137 | }, {
138 | "name": "hideWelcomeBackMessage",
139 | "desc": "Hide the 'welcome back...don't forget to vote' message when visiting a site after a while",
140 | "meta": "",
141 | "match": "*://*/questions*",
142 | "exclude": "*://*/questions/ask"
143 | }, {
144 | "name": "openImagesAsModals",
145 | "desc": "Open imgur images as larger modals on click",
146 | "extended_description": "When clicking on an image that is hosted on Stack Exchange's imgur account, a modal will open showing a larger version of the image. If you still want to open the image in a new tab, there is a 'source' link in the modal.",
147 | "meta": "",
148 | "match": "*://*/questions/*,*://*/review/*",
149 | "exclude": "*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
150 | }, {
151 | "name": "addAnswerCountToQuestionHeader",
152 | "desc": "Add the post's answer count under the question title",
153 | "extended_description": "Add the number of answers the current question has underneath the question header, next to the existing active dates and view count.",
154 | "meta": "",
155 | "match": "*://*/questions/*",
156 | "exclude": "*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
157 | }],
158 | "Comments": [{
159 | "name": "autoShowCommentImages",
160 | "desc": "View linked images (to imgur) in comments inline",
161 | "extended_description": "This feature will automatically detect comments with links to imgur and will display them inline",
162 | "meta": "",
163 | "match": "*://*/questions/*,*://*/review*",
164 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
165 | "feature_packs": ["major_ui, key_feature", "power_user"]
166 | }, {
167 | "name": "commentReplies",
168 | "desc": "Add reply links to comments for quick replying (without having to type someone's username)",
169 | "meta": "https://meta.stackexchange.com/q/74778",
170 | "match": "*://*/questions/*,*://*/review*",
171 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
172 | "feature_packs": ["power_user"]
173 | }, {
174 | "name": "commentShortcuts",
175 | "desc": "Use Ctrl+I,B,K (to italicise, bolden and add code backticks) in comments",
176 | "meta": "https://meta.stackexchange.com/q/14756",
177 | "match": "*://*/questions/*",
178 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
179 | }, {
180 | "name": "confirmNavigateAway",
181 | "desc": "Add a confirmation dialog when navigating away on pages whilst still typing a comment",
182 | "meta": "https://meta.stackexchange.com/q/252205",
183 | "match": "*://*/questions/*",
184 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
185 | }, {
186 | "name": "copyCommentsLink",
187 | "desc": "Copy 'show x more comments' link to the top",
188 | "meta": "https://meta.stackexchange.com/q/55020",
189 | "match": "*://*/questions/*",
190 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
191 | }, {
192 | "name": "moveBounty",
193 | "desc": "Move the 'start a bounty' link before the comments",
194 | "meta": "https://meta.stackexchange.com/q/234095",
195 | "match": "*://*/questions/*",
196 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
197 | }, {
198 | "name": "showCommentScores",
199 | "desc": "Show your comment and comment replies scores in your profile tabs",
200 | "extended_description": "Adds a button next to comments in your profile's responses/activity tabs that when clicked, show you the score of your comment",
201 | "meta": "https://meta.stackexchange.com/q/38285",
202 | "match": "*://*/users/*",
203 | "exclude": "SE1.0,*://chat.*.com/*",
204 | "usesApi": true
205 | }, {
206 | "name": "hiddenCommentsIndicator",
207 | "desc": "Add a darker border underneath comments if there are some hidden after it",
208 | "meta": "https://meta.stackoverflow.com/q/296582",
209 | "match": "*://*/questions/*,*://*/review*",
210 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
211 | "feature_packs": ["power_user"]
212 | }, {
213 | "name": "onlyShowCommentActionsOnHover",
214 | "desc": "Only show the comment actions (flag/upvote) when hovering over a comment",
215 | "meta": "https://meta.stackexchange.com/q/312794",
216 | "match": "*://*/questions/*,*://*/review*",
217 | "exclude": "*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
218 | }, {
219 | "name": "copyCommentMarkdown",
220 | "desc": "Add a button to copy the markdown of a comment next to them",
221 | "meta": "",
222 | "match": "*://*/questions/*,*://*/review*",
223 | "exclude": "*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
224 | "usesApi": true,
225 | "feature_packs": ["power_user"]
226 | }],
227 | "Editing": [{
228 | "name": "addSBSBtn",
229 | "desc": "Add a button to the editor toolbar to start side-by-side editing with the markdown editor and preview",
230 | "extended_description": "An 'SBS' button is added to the right of the markdown editor toolbar. Clicking it will make the markdown and the preview appear side-by-side",
231 | "meta": "https://meta.stackexchange.com/q/253112",
232 | "match": "*://*/questions/*",
233 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
234 | "settings": [{
235 | "id": "sbsByDefault",
236 | "type": "checkbox",
237 | "desc": "Automatically enable SBS on all posts?"
238 | }],
239 | "feature_packs": ["key_feature", "power_user"]
240 | }, {
241 | "name": "editComment",
242 | "desc": "Pre-defined edit comment options (checkboxes)",
243 | "extended_description": "Adds checkboxes to add canned messages for edit revisions when editing. You can make *your own* canned responses by clicking the 'Edit Reasons' button that is added to the Help dropdown menu in the topbar",
244 | "meta": "https://meta.stackexchange.com/q/190461",
245 | "match": "*://*/questions/*",
246 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
247 | "feature_packs": ["power_user"]
248 | }, {
249 | "name": "editReasonTooltip",
250 | "desc": "Add a tooltip to posts showing the last revision's comment",
251 | "extended_description": "When a post is edited, the editor is displayed underneath the post. This feature will show what the last revision's comment was as a tooltip when you hover over 'edited' underneath a post in 'edited [date] at [time]'",
252 | "meta": "https://meta.stackexchange.com/q/2315",
253 | "match": "*://*/questions/*,*://*/review*",
254 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
255 | "feature_packs": ["power_user"],
256 | "usesApi": true
257 | }, {
258 | "name": "findAndReplace",
259 | "desc": "Add a find & replace feature to the markdown editor",
260 | "extended_description": "",
261 | "meta": "",
262 | "match": "*://*/questions/*,*://*/review*",
263 | "exclude": "SE1.0,*://*/questions/tagged*,*://*/questions/greatest-hits*"
264 | }, {
265 | "name": "kbdAndBullets",
266 | "desc": "Add KBD and list buttons to editor toolbar",
267 | "extended_description": "Adds a kbd and bullet icon to the markdown editor toolbar that lets you surround the selected text with KBD tags or listify's the current selection",
268 | "meta": "https://meta.stackexchange.com/q/102841",
269 | "match": "*://*/questions/*,*://*/review*",
270 | "exclude": "SE1.0,*://*/questions/tagged*,*://*/questions/greatest-hits*"
271 | }, {
272 | "name": "inlineEditorEverywhere",
273 | "desc": "Inline editor regardless of reputation",
274 | "extended_description": "Enables the inline editor on all sites, even if you don't have 2k rep there yet. Note: this feature may not work on Firefox.",
275 | "meta": "",
276 | "match": "*://*/questions/*",
277 | "exclude": "*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
278 | }],
279 | "Flags": [{
280 | "name": "flagOutcomeTime",
281 | "desc": "Show the flag outcome time when viewing your Flag History",
282 | "meta": "",
283 | "match": "*://*/users/*",
284 | "exclude": "*://chat.*.com/*,SE1.0"
285 | }, {
286 | "name": "flagPercentages",
287 | "desc": "Show flagging percentages for each type in the Flag Summary",
288 | "meta": "",
289 | "match": "*://*/users/flag-summary/*",
290 | "exclude": "*://chat.*.com/*,SE1.0",
291 | "feature_packs": ["power_user"]
292 | }, {
293 | "name": "flagPercentageBar",
294 | "desc": "Show the total percentage of helpful flags as a coloured bar on the Flag Summary Page",
295 | "meta": "https://meta.stackoverflow.com/q/310881",
296 | "match": "*://*/users/flag-summary/*",
297 | "exclude": "*://chat.*.com/*,SE1.0",
298 | "feature_packs": ["power_user"]
299 | }],
300 | "Sidebar": [{
301 | "name": "hideCommunityBulletin",
302 | "desc": "Hide the Community Bulletin module",
303 | "meta": "",
304 | "match": "",
305 | "exclude": "*://chat.*.com/*,*://*/users/*"
306 | }, {
307 | "name": "hideJustHotMetaPosts",
308 | "desc": "Hide just the 'Hot Meta Posts' sections in the Community Bulletin",
309 | "meta": "",
310 | "match": "",
311 | "exclude": "*://chat.*.com/*"
312 | }, {
313 | "name": "hideHireMe",
314 | "desc": "Hide the Looking for a Job module",
315 | "meta": "",
316 | "match": "",
317 | "exclude": "*://chat.*.com/*"
318 | }, {
319 | "name": "hideChatSidebar",
320 | "desc": "Hide the Chat module",
321 | "meta": "",
322 | "match": "",
323 | "exclude": "*://chat.*.com/*"
324 | }, {
325 | "name": "hideLoveThisSite",
326 | "desc": "Hide the 'Love this site?' module",
327 | "meta": "",
328 | "match": "",
329 | "exclude": "*://chat.*.com/*,*://*/review*"
330 | }, {
331 | "name": "linkedToFrom",
332 | "desc": "Add an arrow to linked posts in the sidebar to show whether they are linked to or linked from",
333 | "meta": "https://meta.stackexchange.com/q/276235",
334 | "match": "*://*/questions/*",
335 | "exclude": "*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*,SE1.0",
336 | "usesApi": true
337 | }, {
338 | "name": "hotNetworkQuestionsFiltering",
339 | "desc": "Filter the hot network questions by their attributes",
340 | "meta": "",
341 | "match": "",
342 | "exclude": "*://chat.*.com/*,SE1.0",
343 | "settings": [{
344 | "id": "wordsToBlock",
345 | "type": "textarea",
346 | "desc": "Words to block from titles (comma-separated)"
347 | }, {
348 | "id": "sitesToBlock",
349 | "type": "textarea",
350 | "desc": "Sites to block (comma-separated match patterns)"
351 | }, {
352 | "id": "titlesToHideRegex",
353 | "type": "textarea",
354 | "desc": "Titles to hide (comma-separated regexes)"
355 | }],
356 | "feature_packs": ["key_feature"],
357 | "usesApi": true
358 | }, {
359 | "name": "addTagsToHNQs",
360 | "desc": "Show HNQ tags on hover in the sidebar",
361 | "extended_description": "When hovering over a question title in the Hot Network Questions sidebar, the question's tags from the originating site will be shown",
362 | "meta": "",
363 | "match": "*://*/*",
364 | "exclude": "SE1.0",
365 | "usesApi": true
366 | }],
367 | "Chat": [{
368 | "name": "chatEasyAccess",
369 | "desc": "Add buttons to user profiles to change user write access directly from a chat room",
370 | "meta": "https://meta.stackexchange.com/q/203480",
371 | "match": "*://chat.*.com/*",
372 | "exclude": ""
373 | }, {
374 | "name": "replyToOwnChatMessages",
375 | "desc": "Add reply signs to your own chat messages",
376 | "meta": "",
377 | "match": "*://chat.*.com/rooms/*",
378 | "exclude": ""
379 | }, {
380 | "name": "renameChat",
381 | "desc": "Prepend 'Chat - ' to chat tabs' titles",
382 | "meta": "https://meta.stackexchange.com/q/246289",
383 | "match": "*://chat.*.com/*",
384 | "exclude": ""
385 | }, {
386 | "name": "scrollChatRoomsList",
387 | "desc": "Enable scrolling for room list in usercards and sidebar when there are many rooms",
388 | "extended_desc": "Enable scrolling in the room list within usercards and sidebar when there's a significant amount of rooms in the list, rather than the list overflowing and hiding some content.",
389 | "meta": "https://meta.stackexchange.com/q/183479",
390 | "match": "*://chat.*.com/rooms/*",
391 | "exclude": ""
392 | }],
393 | "Voting": [{
394 | "name": "betterCSS",
395 | "desc": "Add extra CSS for animation actions on voting and favourite buttons",
396 | "extended_description": "Pulse effect when hovering over the upvote/downvote/favourite buttons. Currently only implemented natively on Android.SE.",
397 | "meta": "https://meta.stackexchange.com/q/252685",
398 | "match": "*://*/questions/*,*://*/review*",
399 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
400 | "feature_packs": ["major_ui"]
401 | }, {
402 | "name": "stickyVoteButtons",
403 | "desc": "Make vote buttons next to posts sticky whilst scrolling on that post",
404 | "meta": "https://meta.stackexchange.com/a/35047",
405 | "match": "*://*/questions/*,*://*/review*",
406 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
407 | "feature_packs": ["major_ui, key_feature", "power_user"]
408 | }, {
409 | "name": "disableVoteButtons",
410 | "desc": "Disable vote buttons on your own posts and deleted posts, which you cannot vote on",
411 | "meta": "",
412 | "match": "*://*/questions/*",
413 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
414 | }],
415 | "Extras": [{
416 | "name": "linkedPostsInline",
417 | "desc": "Display linked posts inline by clicking on an arrow",
418 | "extended_description": "Adds a button next to Stack Exchange posts that expand to show the post inline",
419 | "meta": "",
420 | "match": "*://*/questions/*,*://*/review*",
421 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
422 | "usesApi": true
423 | }, {
424 | "name": "parseCrossSiteLinks",
425 | "desc": "Parse titles to links cross-SE-sites",
426 | "extended_description": "Detects links to other questions on SE sites and converts them to their title",
427 | "meta": "https://meta.stackexchange.com/q/251183",
428 | "match": "*://*/questions/*,*://*/review*",
429 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
430 | "usesApi": true
431 | }, {
432 | "name": "quickAuthorInfo",
433 | "desc": "Show when the post's author was last seen and whether they are registered",
434 | "meta": "",
435 | "match": "*://*/questions*,*://*/review*",
436 | "exclude": "SE1.0,*://*/questions/ask,*://chat.*.com/*",
437 | "feature_packs": ["power_user"],
438 | "usesApi": true
439 | }, {
440 | "name": "shareLinksPrivacy",
441 | "desc": "Remove your user ID from the 'share' link",
442 | "extended_description": "When you click 'share' under a post, this displays a referral link, which incorporates your user ID. This option strips your user ID from the displayed link, preventing inadvertent privacy leaks when disseminating the link via copy-paste. The social-media sharing buttons are unaffected, since the lack of privacy is obvious.",
443 | "meta": "https://meta.stackexchange.com/q/74274",
444 | "match": "*://*/questions/*,*://*/review*",
445 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*",
446 | "feature_packs": ["power_user"]
447 | }, {
448 | "name": "shareLinksMarkdown",
449 | "desc": "Change 'share' link to format of [post-name](url)",
450 | "extended_description": "When you click 'share' under a post, this will convert the URL given to a markdown-friendly version, with the post name as the link text. This feature also automatically copies the converted string to your clipboard",
451 | "meta": "https://meta.stackexchange.com/q/126544",
452 | "match": "*://*/questions/*",
453 | "exclude": "SE1.0,*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
454 | }, {
455 | "name": "sortByBountyAmount",
456 | "desc": "Add an option to filter bounties by their amount",
457 | "meta": "https://meta.stackexchange.com/q/7753",
458 | "match": "*://*/questions",
459 | "exclude": "*://*/users/*,*://chat.*.com/*,SE1.0,*://*/questions/*,*://*/review*"
460 | }, {
461 | "name": "warnNotLoggedIn",
462 | "desc": "Warn you when you are not logged in",
463 | "meta": "",
464 | "match": "",
465 | "exclude": "*://chat.*.com/*,SE1.0"
466 | }, {
467 | "name": "hideCertainQuestions",
468 | "desc": "Hide on hold, closed, or duplicate questions or deleted answers",
469 | "meta": "",
470 | "match": "",
471 | "exclude": "*://chat.*.com/*",
472 | "settings": [{
473 | "id": "duplicate",
474 | "type": "checkbox",
475 | "desc": "Hide duplicate questions?"
476 | }, {
477 | "id": "closed",
478 | "type": "checkbox",
479 | "desc": "Hide closed questions?"
480 | }, {
481 | "id": "migrated",
482 | "type": "checkbox",
483 | "desc": "Hide migrated questions?"
484 | }, {
485 | "id": "onHold",
486 | "type": "checkbox",
487 | "desc": "Hide on hold questions?"
488 | }, {
489 | "id": "deletedAnswers",
490 | "type": "checkbox",
491 | "desc": "Hide deleted answers?"
492 | }]
493 | }, {
494 | "name": "showMetaReviewCount",
495 | "desc": "Add how many reviews are available on Meta to the main site review page",
496 | "meta": "",
497 | "match": "*://*/review$",
498 | "exclude": "*://meta.*.com/*"
499 | }, {
500 | "name": "copyCode",
501 | "desc": "Add a button to code in posts to let you copy it",
502 | "meta": "",
503 | "match": "*://*/questions/*,*://*/review*",
504 | "exclude": "*://*/questions/ask,*://*/questions/tagged*,*://*/questions/greatest-hits*"
505 | }, {
506 | "name": "openLinksInNewTab",
507 | "desc": "Open any links to the specified sites in a new tab by default",
508 | "extended_description": "You need to set the sites yourself by clicking the button next to this feature",
509 | "meta": "",
510 | "match": "*://*/questions*",
511 | "exclude": "",
512 | "settings": [{
513 | "id": "linksToOpenInNewTab",
514 | "type": "textarea",
515 | "desc": "Links to open in new tab (comma-separated match patterns)"
516 | }]
517 | }, {
518 | "name": "addOnTopicLinkToSiteSwitcher",
519 | "desc": "Replace the 'help' link with an 'on-topic' link in the site switcher dropdown",
520 | "meta": "",
521 | "match": "",
522 | "exclude": "*://chat.*.com/*,SE1.0"
523 | }, {
524 | "name": "customMagicLinks",
525 | "desc": "Add custom magic links to comments and posts",
526 | "extended_description": "Allows you to create your own magic links ('[text]' auto-converts to a link of your choice). Links can use the placeholders $BASEURL, $METABASEURL, $QUESTIONID, and $ANSWERID which are auto-replaced with the respective data. You can set the reasons by clicking the 'Magic Links' button added to the 'Help' dropdown at the top-right of the page. See https://stackapps.com/q/6650 for more details.",
527 | "meta": "",
528 | "match": "",
529 | "exclude": "*://chat.*.com/*,SE1.0,*://*/questions/ask",
530 | "feature_packs": ["power_user"]
531 | }]
532 | }
533 | }
534 |
--------------------------------------------------------------------------------
/sox.github.js:
--------------------------------------------------------------------------------
1 | (function(sox, $) {
2 | 'use strict';
3 |
4 | sox.github = {
5 | init: function(version, handler) {
6 | // auto-inject version number and environment information into GitHub issues
7 | function inject() {
8 | if (!sox.location.on('github.com/soscripted/sox') || sox.location.on('feature_request')) return;
9 | const issue = document.querySelector('#issue_body');
10 | if (!issue) return;
11 | const environmentText = `**Environment**
12 | SOX version: ${version}
13 | Platform: ${handler}`;
14 |
15 | let issueText = issue.value;
16 | issueText = issueText.replace(/..Environment..\n.*?Tampermonkey\)/, environmentText); // inject environment details
17 | issueText += '\n---\n\n### Features Enabled \n\n ' + JSON.stringify(sox.settings.load());
18 | issue.value = issueText;
19 | }
20 |
21 | $(document).on('pjax:complete', inject);
22 | inject();
23 | },
24 | };
25 | })(window.sox = window.sox || {}, jQuery);
26 |
--------------------------------------------------------------------------------
/sox.sprites.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/sox.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Stack Overflow Extras (SOX)
3 | // @namespace https://github.com/soscripted/sox
4 | // @homepage https://github.com/soscripted/sox
5 | // @homepageURL https://github.com/soscripted/sox
6 | // @version 2.8.1 DEV
7 | // @description Extra optional features for Stack Overflow and Stack Exchange sites
8 | // @contributor ᴉʞuǝ (https://stackoverflow.com/users/1454538/, https://github.com/mezmi)
9 | // @contributor ᔕᖺᘎᕊ (https://stackexchange.com/users/4337810/, https://github.com/shu8)
10 | // @contributor Sir-Cumference (https://stackexchange.com/users/4119142/, https://github.com/Sir-Cumference)
11 | // @contributor GaurangTandon (https://github.com/GaurangTandon)
12 | // @contributor double-beep (https://stackexchange.com/users/14688437/double-beep, https://github.com/double-beep)
13 | // @updateURL https://cdn.jsdelivr.net/gh/soscripted/sox@dev/sox.user.js
14 |
15 | // @match https://*.stackoverflow.com/*
16 | // @match https://*.stackexchange.com/*
17 | // @match https://*.superuser.com/*
18 | // @match https://*.serverfault.com/*
19 | // @match https://*.askubuntu.com/*
20 | // @match https://*.stackapps.com/*
21 | // @match https://*.mathoverflow.net/*
22 | // @match *://github.com/soscripted/*
23 | // @match *://soscripted.github.io/sox/*
24 |
25 | // @exclude *://data.stackexchange.com/*
26 | // @exclude *://api.stackexchange.com/*
27 | // @exclude *://stackoverflow.com/c/*
28 |
29 | // @require https://code.jquery.com/jquery-3.3.1.min.js
30 | // @require https://code.jquery.com/ui/1.12.1/jquery-ui.min.js
31 | // @require https://api.stackexchange.com/js/2.0/all.js
32 | // @require https://cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.5.3/jquery.timeago.min.js
33 |
34 | // @require sox.common.js
35 | // @require sox.github.js
36 | // @require sox.dialog.js
37 | // @require sox.features.js
38 |
39 | // @resource css sox.css
40 | // @resource dialog sox.dialog.html
41 | // @resource featuresJSON sox.features.info.json
42 | // @resource common sox.common.info.json
43 | // @resource sprites sox.sprites.svg
44 |
45 | // @grant GM_setValue
46 | // @grant GM_getValue
47 | // @grant GM_deleteValue
48 | // @grant GM_listValues
49 | // @grant GM_getResourceText
50 | // @grant GM_addStyle
51 | // @grant GM_info
52 | // @grant GM_setClipboard
53 | // ==/UserScript==
54 | /*jshint loopfunc: true*/
55 | (function(sox, $) {
56 | 'use strict';
57 |
58 | function runFeatures(settings, featureInfo) {
59 | // Execute features
60 | performance.mark('allFeatures-start');
61 | for (let i = 0; i < settings.length; ++i) {
62 | const category = settings[i].split('-')[0];
63 | const featureId = settings[i].split('-')[1];
64 |
65 | if (!(category in featureInfo.categories)) { //if we ever rename a category
66 | sox.loginfo('Deleting feature "' + settings[i] + '" (category rename?)');
67 | settings.splice(i, 1);
68 | sox.settings.save(settings);
69 | continue;
70 | }
71 |
72 | const feature = featureInfo.categories[category].filter(obj => {
73 | return obj.name == featureId;
74 | })[0];
75 |
76 | let runFeature = true;
77 | try {
78 | //NOTE: there is no else if() because it is possible to have both match and exclude patterns..
79 | //which could have minor exceptions making it neccessary to check both
80 | if (feature.match !== '') {
81 | const sites = feature.match.split(',');
82 |
83 | for (let pattern = 0; pattern < sites.length; pattern++) {
84 | if (!sox.location.matchWithPattern(sites[pattern])) {
85 | runFeature = false; //none of the patterns match the current site.. yet.
86 | } else {
87 | runFeature = true;
88 | break; //if it does match, then stop looping; we want the feature to run
89 | }
90 | }
91 | }
92 | if (feature.exclude !== '') {
93 | const sites = feature.exclude.split(',');
94 |
95 | for (let pattern = 0; pattern < sites.length; pattern++) {
96 | if (sox.location.matchWithPattern(sites[pattern])) { //if current site is in list, DON'T run feature
97 | runFeature = false; //don't run feature
98 | break; //no need to keep on looping
99 | }
100 | }
101 | }
102 | if (runFeature) {
103 | sox.debug('running ' + featureId);
104 | performance.mark(`${featureId}-start`);
105 | if (feature.settings) {
106 | const settingsToPass = GM_getValue('SOX-' + featureId + '-settings') ? JSON.parse(GM_getValue('SOX-' + featureId + '-settings')) : {};
107 | sox.features[featureId](settingsToPass); //run the feature if match and exclude conditions are met, pass on settings object
108 | } else {
109 | sox.features[featureId](); //run the feature if match and exclude conditions are met
110 | }
111 | performance.mark(`${featureId}-end`);
112 | performance.measure(featureId, `${featureId}-start`, `${featureId}-end`);
113 | }
114 | } catch (err) {
115 | if (!sox.features[featureId] || !feature) { //remove deprecated/'corrupt' feature IDs from saved settings
116 | sox.loginfo('Deleting feature "' + settings[i] + '" (feature not found)');
117 | settings.splice(i, 1);
118 | sox.settings.save(settings);
119 | $('#sox-settings-dialog-features').find('#' + settings[i].split('-')[1]).parent().parent().remove();
120 | } else {
121 | $('#sox-settings-dialog-features').find('#' + settings[i].split('-')[1]).parent().css('color', 'red').attr('title', 'There was an error loading this feature. Please raise an issue on GitHub.');
122 | sox.error('There was an error loading the feature "' + settings[i] + '". Please raise an issue on GitHub, and copy the following error log:\n' + err);
123 | }
124 | }
125 | }
126 | performance.mark('allFeatures-end');
127 | performance.measure('allFeatures', 'allFeatures-start', 'allFeatures-end');
128 | sox.debug('Performance Data', performance.getEntriesByType('measure'));
129 | }
130 |
131 | function init() {
132 | if (sox.location.on('github.com/soscripted')) {
133 | try {
134 | sox.github.init(sox.info.version, sox.info.handler);
135 | } catch (e) {
136 | throw ('SOX: There was an error while attempting to initialize the sox.github.js file, please report this on GitHub.\n' + e);
137 | }
138 | return;
139 | }
140 |
141 | if (sox.location.on('soscripted.github.io/sox/#access_token')) { //save access token
142 | try {
143 | const access_token = window.location.href.split('=')[1].split('&')[0];
144 | sox.loginfo('ACCESS TOKEN: ', access_token);
145 | GM_setValue('SOX-accessToken', access_token);
146 | alert('Access token successfully saved! You can close this window :)');
147 | } catch (e) {
148 | throw ('SOX: There was an error saving your access token');
149 | }
150 | return;
151 | }
152 | if (sox.info.debugging) {
153 | sox.debug('DEBUGGING SOX VERSION ' + sox.info.version);
154 | sox.debug('----------------saved variables---------------------');
155 | sox.settings.writeToConsole(true); //true => hide access token
156 | sox.debug('----------------end saved variables---------------------');
157 | }
158 |
159 | const spritesDiv = $('
', { html: GM_getResourceText('sprites') });
160 | $('head').append(spritesDiv);
161 |
162 | GM_addStyle(GM_getResourceText('css'));
163 |
164 | const settings = sox.settings.load();
165 | //returns undefined if not set
166 |
167 | const featureInfo = JSON.parse(GM_getResourceText('featuresJSON'));
168 |
169 | try {
170 | sox.debug('SOX object', sox);
171 | sox.dialog.init({
172 | version: sox.info.version,
173 | features: featureInfo,
174 | settings: settings,
175 | lastVersionInstalled: sox.info.lastVersionInstalled,
176 | });
177 | } catch (e) {
178 | throw ('SOX: There was an error while attempting to initialize the SOX Settings Dialog, please report this on GitHub.\n' + e);
179 | }
180 |
181 | window.onload = () => {
182 | if (sox.settings.available) {
183 | if (document.hasFocus && document.hasFocus()) {
184 | runFeatures(settings, featureInfo);
185 | } else {
186 | window.addEventListener('focus', () => runFeatures(settings, featureInfo), { once: true });
187 | }
188 | }
189 | }
190 |
191 | //custom events....
192 | sox.helpers.observe([...document.getElementsByClassName('post-layout')], '.new_comment, .comment, .comments, .comment-text', node => {
193 | sox.debug('sox-new-comment event triggered');
194 | $(document).trigger('sox-new-comment', [node]);
195 | });
196 |
197 | sox.helpers.observe(document.body, 'textarea[id^="wmd-input"]', node => {
198 | sox.debug('sox-edit-window event triggered');
199 | $(document).trigger('sox-edit-window', [node]);
200 | });
201 |
202 | sox.helpers.observe(document.body, '.reviewable-post, .review-content', node => {
203 | sox.debug('sox-new-review-post-appeared event triggered');
204 | $(document).trigger('sox-new-review-post-appeared', [node]);
205 | });
206 |
207 | const chatBody = document.getElementById('chat-body');
208 | if (chatBody) {
209 | sox.helpers.observe(chatBody, '.user-popup', node => {
210 | sox.debug('sox-chat-user-popup event triggered');
211 | $(document).trigger('sox-chat-user-popup', [node]);
212 | });
213 | }
214 |
215 | if (GM_getValue('SOX-accessToken', -1) == -1) { //set access token
216 | //This was originally a series of IIFEs appended to the head which used the SE API JS SDK but
217 | //it was very uncertain and often caused issues, especially in FF
218 | //it now uses a Github page to show the access token
219 | //and detects that page and saves it automatically.
220 | //this seems to be a much cleaner and easier-to-debug method!
221 | GM_setValue('SOX-accessToken', -2); //once we ask the user once, don't ask them again: set the value to -2 so this IF never evaluates to true
222 | const askUserToAuthorise = window.confirm('To get the most out of SOX, you should get an access token! Please press "OK" to continue and follow the instructions in the window that opens. NOTE: this message will not appear again; if you choose not to, you can click the key at the bottom of the settings dialog at anytime to get one.');
223 | if (askUserToAuthorise) window.open('https://stackexchange.com/oauth/dialog?client_id=7138&scope=no_expiry&redirect_uri=http://soscripted.github.io/sox/');
224 | sox.warn('Please go to the following URL to get your access token for certain SOX features', 'https://stackexchange.com/oauth/dialog?client_id=7138&scope=no_expiry&redirect_uri=http://soscripted.github.io/sox/');
225 | }
226 | }
227 | sox.ready(init);
228 | })(window.sox = window.sox || {}, jQuery);
229 |
--------------------------------------------------------------------------------