diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..39b18dbc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.js] +quote_type = double + +[*.css] +indent_size = 2 + +[*.html] +indent_size = 2 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..7c6b5bc7 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,11 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Documentation +docs/ + +# Third-party libraries +src/scripts/libs/ \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..1e5283dd --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,172 @@ +module.exports = { + env: { + browser: true, + es2021: true, + amd: true, // Add AMD environment for RequireJS + }, + extends: ["eslint:recommended"], + parserOptions: { + ecmaVersion: 2021, + sourceType: "module", + }, + globals: { + // Application globals + app: true, + bg: true, + tabID: true, + + // WebExtension API globals + browser: "readonly", // Firefox WebExtensions API + chrome: "readonly", // Chrome Extensions API + + // Module system + define: false, + require: false, + requirejs: false, + + // Browser APIs + requestAnimationFrame: true, + URL: true, + HTMLCollection: true, + fetch: "readonly", + localStorage: "readonly", + sessionStorage: "readonly", + indexedDB: "readonly", + Notification: "readonly", + WebSocket: "readonly", + + // WebExtension specific globals + Blob: "readonly", + FileReader: "readonly", + + // Firefox specific + InstallTrigger: "readonly", + + // Extension messaging + runtime: "readonly", + + // Common WebExtension APIs + tabs: "readonly", + storage: "readonly", + contextMenus: "readonly", + webRequest: "readonly", + permissions: "readonly", + i18n: "readonly", + + // Background process globals + sourceToFocus: true, + Source: true, + Item: true, + Folder: true, + settings: true, + info: true, + sources: true, + items: true, + folders: true, + loaded: true, + toolbars: true, + loader: true, + valueToBoolean: true, + getBoolean: true, + getElementBoolean: true, + getElementSetting: true, + fetchAll: true, + fetchOne: true, + reloadExt: true, + appStarted: true, + parsedData: true, + + // Libraries + $: true, + jQuery: true, + _: true, + Backbone: true, + BB: true, + + // AMD modules that might be used globally + RSSParser: true, + Favicon: true, + Animation: true, + Locale: true, + dateUtils: true, + escapeHtml: true, + stripTags: true, + + // Models and collections + Settings: true, + Info: true, + Sources: true, + Items: true, + Folders: true, + Toolbars: true, + Loader: true, + Special: true, + + // Views and layouts + Layout: true, + ArticlesLayout: true, + ContentLayout: true, + FeedsLayout: true, + + // Allow console for debugging + console: true, + }, + rules: { + // Disable all formatting rules + indent: "off", + "linebreak-style": "off", + quotes: "off", + semi: "off", + "comma-dangle": "off", + "max-len": "off", + "brace-style": "off", + "space-before-function-paren": "off", + "space-before-blocks": "off", + "keyword-spacing": "off", + "object-curly-spacing": "off", + "array-bracket-spacing": "off", + "computed-property-spacing": "off", + "space-in-parens": "off", + "comma-spacing": "off", + "no-trailing-spaces": "off", + "eol-last": "off", + + // Keep logical/semantic rules + "no-unused-vars": "warn", + curly: "error", + eqeqeq: "error", + "no-empty": "error", + + // Browser-specific rules + "no-alert": "off", // Allow alert, confirm, and prompt + "no-console": "off", // Allow console for extension debugging + + // Rules for messy codebase + "no-undef": "warn", // Warn instead of error for undefined variables + "no-global-assign": "warn", // Warn instead of error for global reassignments + "no-useless-escape": "warn", // Warn instead of error for unnecessary escape characters + }, + // Allow globals to be declared via comments + noInlineConfig: false, + overrides: [ + { + // For AMD modules + files: ["src/scripts/**/*.js"], + env: { + amd: true, + }, + globals: { + define: "readonly", + require: "readonly", + }, + }, + { + // For background process + files: ["src/scripts/bgprocess/**/*.js"], + globals: { + browser: "readonly", + chrome: "readonly", + }, + }, + ], +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..08852963 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Dependencies +node_modules/ +npm-debug.log +yarn-error.log +yarn-debug.log +package-lock.json + +# Build output +dist/ +*.zip + +# IDE and editor files +.idea/ +.vscode/ +*.sublime-project +*.sublime-workspace +.DS_Store +Thumbs.db + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Documentation +docs/ + +# Temporary files +.tmp/ +.temp/ diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 00000000..04afcfb2 --- /dev/null +++ b/.jshintignore @@ -0,0 +1,3 @@ +node_modules/* +dist/* +docs/* diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000..9a13b998 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,39 @@ +{ + "curly": true, + "eqnull": true, + "eqeqeq": true, + "immed": true, + "newcap": true, + "noarg": true, + "sub": true, + "undef": true, + "browser": true, + "devel": true, + "boss": true, + "forin": true, + "noempty": true, + "unused": true, + "trailing": true, + "supernew": true, + "onevar": false, + "funcscope": true, + "maxdepth": 5, + "quotmark": "single", + "-W030": true, + "-W117": true, + "-W089": true, + "esversion": 8, + "futurehostile": true, + "globals": { + "app": true, + "bg": true, + "tabID": true, + "chrome": false, + "define": false, + "require": false, + "requirejs": false, + "requestAnimationFrame": true, + "URL": true, + "HTMLCollection": true + } +} diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 00000000..dfe5229a --- /dev/null +++ b/BUILD.md @@ -0,0 +1,97 @@ +# Smart-RSS Build System + +This document explains the build system for Smart-RSS. + +## Overview + +The build system has been simplified from a Grunt-based system to a plain Node.js script. The script provides the same functionality as the previous Grunt-based system but with a simpler implementation. + +## Requirements + +- Node.js (v12 or higher recommended) +- npm + +## Installation + +```bash +npm install +``` + +## Usage + +You can use the build system in two ways: + +### 1. Using npm scripts + +```bash +# Copy files from src to dist and strip comments +npm run prepare + +# Prepare and create zip package +npm run package + +# Bump version, commit, prepare, and create zip package (patch version) +npm run release + +# Bump minor version, commit, prepare, and create zip package +npm run release:minor + +# Bump major version, commit, prepare, and create zip package +npm run release:major + +# Watch for changes in src directory +npm run watch + +# Bump version number (patch by default) +npm run bump-version +``` + +### 2. Using the build script directly + +```bash +# Copy files from src to dist and strip comments +node build.js prepare + +# Prepare and create zip package +node build.js package + +# Bump version, commit, prepare, and create zip package +node build.js release [patch|minor|major] + +# Watch for changes in src directory +node build.js watch + +# Bump version number +node build.js bump-version [patch|minor|major] +``` + +## Features + +- **prepare**: Copies files from src to dist and strips comments from JS files +- **package**: Prepares files and creates a zip package +- **release**: Bumps version, commits changes, prepares files, and creates a zip package +- **watch**: Watches for changes in the src directory and automatically runs prepare +- **bump-version**: Bumps the version number in manifest.json + +## Migrating from Grunt + +The new build system provides the same functionality as the previous Grunt-based system. If you were using Grunt tasks, here's how they map to the new system: + +| Grunt Task | New Command | +| --------------------- | -------------------------------------------------------- | +| `grunt prepare` | `npm run prepare` or `node build.js prepare` | +| `grunt package` | `npm run package` or `node build.js package` | +| `grunt release` | `npm run release` or `node build.js release` | +| `grunt release:minor` | `npm run release:minor` or `node build.js release minor` | +| `grunt release:major` | `npm run release:major` or `node build.js release major` | +| `grunt watch` | `npm run watch` or `node build.js watch` | +| `grunt bump-version` | `npm run bump-version` or `node build.js bump-version` | + +## Why the Change? + +The new build system: + +1. Reduces dependencies (no Grunt and related plugins) +2. Simplifies the build process +3. Makes it easier to understand and modify +4. Provides the same functionality in a more straightforward way diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index b7296a21..00000000 --- a/Gruntfile.js +++ /dev/null @@ -1,177 +0,0 @@ -module.exports = function(grunt) { - - // Project configuration. - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - - - jshint: { - options: { - curly: false, // true: force { } - eqnull: true, // true: enable something == null - eqeqeq: false, // true: force === - immed: true, // true: immidiatly invocated fns has to be in () - newcap: true, // true: construcotr has to have firt letter uppercased - noarg: true, // true: no arguments.caller and arguments.callee - sub: true, // true: no warning about a['something'] if a.something can be used - undef: true, // true: can't use undeclared vars - browser: true, // true: set window object and other stuff as globals - devel: true, // true: set alert,confirm,console,... as globals - boss: true, // true: allow assigments in conditions and return statements - forin: true, // true: hasOwnProperty has to be in all for..in cycles - noempty: true, // true: no empty blocks - unused: true, // true: warn about unused vars - trailing: true, // true: no trailing whitespaces - supernew: true, // true: enable 'new Constructor' instead of 'new Constructor()' - onevar: false, // true: only one var per fn - funcscope: false, // false: no 'var' in blocks - maxdepth: 5, // max nesting depth - quotmark: 'single', // single: force ' - '-W041': true, // don't warn about something == false/true - '-W117': true, // don't warn about not defined vars until I refactorize bg.js - globals: { - app: true, - bg: true, - tabID: true, - chrome: false, - define: false, - require: false, - - /* browser globals not recognized by browser or devel options */ - requestAnimationFrame: true, - URL: true, - HTMLCollection: true - } - }, - all: ['scripts/app/**/*.js', 'scripts/bgprocess/**/*.js'] - }, - - requirejs: { - app: { - options: { - name: '../main', - baseUrl: 'scripts/app', - generateSourceMaps: true, - preserveLicenseComments: false, - optimize: 'uglify2', - waitSeconds: 0, - paths: { - jquery: '../libs/jquery.min', - underscore: '../libs/underscore.min', - backbone: '../libs/backbone.min', - text: '../text', - i18n: '../i18n', - domReady: '../domReady' - }, - shim: { - jquery: { - exports: '$' - }, - backbone: { - deps: ['underscore', 'jquery'], - exports: 'Backbone' - }, - underscore: { - exports: '_' - } - }, - excludeShallow: ['modules/Locale', 'jquery', 'underscore', 'backbone'], - out: 'scripts/main-compiled.js' - } - }, - bg: { - options: { - name: '../bgprocess', - baseUrl: 'scripts/bgprocess', - generateSourceMaps: true, - preserveLicenseComments: false, - optimize: 'uglify2', - waitSeconds: 0, - paths: { - jquery: '../libs/jquery.min', - underscore: '../libs/underscore.min', - backbone: '../libs/backbone.min', - text: '../text', - i18n: '../i18n', - md5: '../libs/md5', - domReady: '../domReady', - backboneDB: '../libs/backbone.indexDB' - }, - shim: { - jquery: { - exports: '$' - }, - backboneDB: { - deps: ['backbone'] - }, - backbone: { - deps: ['underscore', 'jquery'], - exports: 'Backbone' - }, - underscore: { - exports: '_' - }, - md5: { - exports: 'CryptoJS' - } - }, - excludeShallow: ['jquery', 'underscore', 'backbone', 'backboneDB'], - out: 'scripts/bgprocess-compiled.js' - } - } - }, - - stylus: { - compile: { - options: { - compress: false, - //imports: ['nib'] - }, - files: { - //'styles/options-compiled.css': 'options.styl', // 1:1 compile - 'styles/main-compiled.css': [ - 'styles/global.styl', - 'styles/feeds.styl', - 'styles/articles.styl', - 'styles/content.styl' - ] - } - } - }, - watch: { - scripts: { - files: ['styles/*.styl'], - tasks: ['stylus'], - options: { - spawn: false, - interrupt: true, - events: ['all'] - }, - }, - }, - yuidoc: { - compile: { - name: '<%= pkg.name %>', - description: '<%= pkg.description %>', - version: '<%= pkg.version %>', - url: '<%= pkg.homepage %>', - options: { - paths: ['scripts'], - /*themedir: 'path/to/custom/theme/',*/ - outdir: 'docs/' - } - } - } - }); - - - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-requirejs'); - grunt.loadNpmTasks('grunt-contrib-stylus'); - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-contrib-yuidoc'); - - // Default task(s). - grunt.registerTask('default', ['jshint']); - grunt.registerTask('rjs', ['requirejs:app', 'requirejs:bg']); -}; \ No newline at end of file diff --git a/README.md b/README.md index ca326978..03b66d3a 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,99 @@ -# Smart RSS extension - -Translations are in scripts/nls/*.js - -For technical bug reports use issues here on GitHub - -For bugs from user perspective use commments on: -http://blog.martinkadlec.eu/post/501-smart-rss-final-v10 - -## For developers - -If you are interested in improving Smart RSS then there are few tips to get started. - -First of all you will need several command line tools: - -- Git (obviously) - http://msysgit.github.io -- Node.JS & npm - http://nodejs.org -- Grunt-cli - http://gruntjs.com/getting-started (npm install -g grunt-cli) -- Mocha - http://visionmedia.github.io/mocha/ (npm install -g mocha) - for in-app integration tests I load mocha from cdn, but if you want to create out-of-app unit tests you will want to install this. - -To setup your Smart RSS project open your console, go to your projects folders and type: -``` -git clone git@github.com:BS-Harou/Smart-RSS.git smartrss -cd smartrss -npm install -``` - -You should also create .gitignore file including at least: -``` -node_modules/* -docs/* -*.map -*-compiled.js -*.sublime-* -``` -(\*.sublime-\* only if you use sublime text projects) - -To check for jshint errors: -``` -grunt jshint -``` - -To compile stylus files to css: -``` -grunt stylus -``` - -You can also use watch task that will automatically compile your stylus files on change: -``` -grunt watch -``` - -To generate source documentaion in ./docs run: -``` -grunt yuidoc -``` - -In dev. builds you can run integration tests by pressing shift+insert directly in smart rss tab. - -When you are done editing the code you should compile all the js files: -``` -grunt rjs -``` - - -and then switch to "publish" branch (that will make index.html (bgprocess) and rss.tml (app) use the compiled files instead of loading all the files separately) and rebase it. -``` -git checkout publish -git rebase master -``` - -Then you can use Opera to make extenion out of it. Opera will automatically ignore all files and folders beggining with "." like ".git", but you might want to remove some other files too (like sublime text projects files). You have to do this manually or use some script that will do this for you. I have a script to do this but it is not yet ready to get published. In future it might work like this: - -``` -grunt build -``` - - -Then don't forget to switch back to master banch -``` -git checkout master -``` \ No newline at end of file +# Smart RSS extension + +## Now officially unmaintained, I _may_ fix some critical issue if any is found within few following weeks, but then I'll archive this repo. Feel free to fork and continue development as you wish + +Originally developed for Opera 15+ by BS-Harou (Martin Kadlec) + +Translations are in scripts/nls/\*.js + +For technical bug reports use issues here on GitHub + +## For users + +Extension is available in following repositories: + +#### AMO: https://addons.mozilla.org/firefox/addon/smart-rss-reader/ + +~#### Chrome Web Store: https://chrome.google.com/webstore/detail/eggggihfcaabljfpjiiaohloefmgejic/~ + +If you encounter issue with a specific feed for best results please back up and include current state of that feed in your report, this will be helpful in case the feed changes before I get to check it, thanks in advance + + +## For developers + +If you are interested in improving Smart RSS then there are few tips to get started. + +First of all you will need several command line tools: + +- Git +- Node.JS (v12 or higher recommended) & npm + +To setup your Smart RSS project open your console, go to your projects folders and type: + +``` +git clone git@github.com:zakius/Smart-RSS.git smartrss +cd smartrss +npm install +``` + +Sometimes you may encounter texts ending with `*` or `!` in app, first ones are fallbacks to English text when used locale lacks the needed one and the latter are actual keys displayed when even English text is missing, feel free to submit PR's to fill them. If you change wording or punctuation somewhere please comment that line (using GitHub interface) with reasoning like common conventions or special punctuation rules in given language. + +### Code Quality + +The project uses a clear separation of concerns for code quality: + +- **ESLint** checks for logical and semantic issues (potential bugs, unused variables, etc.) +- **EditorConfig** handles all formatting concerns (indentation, line endings, quotes, etc.) + +This separation ensures that ESLint focuses on code correctness while EditorConfig manages consistent formatting across different editors and IDEs. + +To lint your code: + +``` +npm run lint +``` + +This will check your code for: + +- Syntax errors and potential bugs +- Logical issues and best practices +- Browser extension-specific concerns + +### Build System + +The build system has been simplified to a plain Node.js script. You can use npm scripts to run the build tasks: + +```bash +# Copy files from src to dist and strip comments +npm run prepare + +# Prepare and create zip package +npm run package + +# Bump version, commit, prepare, and create zip package (patch version) +npm run release + +# Bump minor version, commit, prepare, and create zip package +npm run release:minor + +# Bump major version, commit, prepare, and create zip package +npm run release:major + +# Watch for changes in src directory +npm run watch + +# Bump version number (patch by default) +npm run bump-version +``` + +Or you can use the build script directly: + +```bash +node build.js prepare +node build.js package +node build.js release [patch|minor|major] +node build.js watch +node build.js bump-version [patch|minor|major] +``` + +For more details about the build system, see [BUILD.md](BUILD.md). diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000..c4192631 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/build.js b/build.js new file mode 100755 index 00000000..062dd5bc --- /dev/null +++ b/build.js @@ -0,0 +1,228 @@ +#!/usr/bin/env node + +const { join, dirname, relative } = require("path"); +const { + readdirSync, + lstatSync, + copyFileSync, + mkdirSync, + existsSync, + readFileSync, + writeFileSync, +} = require("fs"); +const { execSync } = require("child_process"); +const semver = require("semver"); +const AdmZip = require("adm-zip"); + +// Utility functions +const scan = (dir) => { + const filesList = []; + readdirSync(dir).forEach((file) => { + if (file[0] === ".") { + return; + } + const filePath = join(dir, file); + + if (lstatSync(filePath).isDirectory()) { + filesList.push(...scan(filePath)); + return; + } + filesList.push(filePath); + }); + return filesList; +}; + +const ensureDirectoryExists = (dirPath) => { + if (!existsSync(dirPath)) { + mkdirSync(dirPath, { recursive: true }); + } +}; + +// Main functions +const bumpVersion = (level = "patch") => { + console.log(`Bumping version (${level})...`); + const manifestPath = join(__dirname, "src/manifest.json"); + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + manifest.version = semver.inc(manifest.version, level); + writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); + console.log(`Version bumped to ${manifest.version}`); + return manifest.version; +}; + +const commit = (level = "patch") => { + console.log("Committing changes..."); + try { + execSync("git add *", { stdio: "inherit" }); + execSync(`git commit -m "auto version bump: ${level}"`, { + stdio: "inherit", + }); + console.log("Changes committed"); + return true; + } catch (error) { + console.error("Git commit failed:", error.message); + return false; + } +}; + +const copyFiles = () => { + console.log("Copying files from src to dist..."); + const srcDir = join(__dirname, "src"); + const distDir = join(__dirname, "dist"); + + // Ensure dist directory exists + ensureDirectoryExists(distDir); + + // Get all files from src + const srcFiles = scan(srcDir); + + // Copy each file to dist, preserving directory structure + srcFiles.forEach((srcFile) => { + const relativePath = relative(srcDir, srcFile); + const destFile = join(distDir, relativePath); + const destDir = dirname(destFile); + + ensureDirectoryExists(destDir); + copyFileSync(srcFile, destFile); + }); + + console.log("Files copied"); +}; + +const stripComments = () => { + console.log("Stripping comments from JS files..."); + const root = join(__dirname, "dist"); + const filesList = scan(root); + + const multilineComment = /^[\t\s]*\/\*\*?[^!][\s\S]*?\*\/[\r\n]/gm; + const specialComments = /^[\t\s]*\/\*!\*?[^!][\s\S]*?\*\/[\r\n]/gm; + const singleLineComment = /^[\t\s]*(\/\/)[^\n\r]*[\n\r]/gm; + + filesList.forEach((filePath) => { + if (!filePath.endsWith(".js")) { + return; + } + const contents = readFileSync(filePath, "utf8") + .replace(multilineComment, "") + .replace(singleLineComment, "") + .replace(specialComments, ""); + + writeFileSync(filePath, contents); + }); + + console.log("Comments stripped"); +}; + +const zipPackage = () => { + console.log("Creating zip package..."); + const root = join(__dirname, "dist"); + const manifestPath = join(root, "manifest.json"); + const filesList = scan(root); + const version = JSON.parse(readFileSync(manifestPath, "utf8")).version; + + const zipFile = new AdmZip(); + + filesList.forEach((file) => { + // Get the directory relative to the root, or empty string if it's at the root + const relativePath = + dirname(file) === root ? "" : relative(root, dirname(file)); + zipFile.addLocalFile(file, relativePath); + }); + + const zipPath = join(__dirname, "dist", `SmartRSS_v${version}.zip`); + zipFile.writeZip(zipPath); + console.log(`Zip package created: ${zipPath}`); +}; + +const watch = () => { + console.log("Watching for changes in src directory..."); + const chokidar = require("chokidar"); + + chokidar + .watch(join(__dirname, "src"), { + ignored: /(^|[\/\\])\../, + persistent: true, + }) + .on("change", (path) => { + console.log(`File ${path} has been changed`); + prepare(); + }); + + console.log("Watching for changes. Press Ctrl+C to stop."); +}; + +// Combined tasks +const prepare = () => { + copyFiles(); + stripComments(); +}; + +const packageTask = () => { + prepare(); + zipPackage(); +}; + +const release = (level = "patch") => { + if (!["major", "minor", "patch"].includes(level)) { + console.error("Wrong update level, aborting"); + return false; + } + + bumpVersion(level); + commit(level); + copyFiles(); + stripComments(); + zipPackage(); +}; + +// Command line interface +const printUsage = () => { + console.log(` +Usage: node build.js [command] [options] + +Commands: + prepare Copy files from src to dist and strip comments + package Prepare and create zip package + release [level] Bump version, commit, prepare, and create zip package + level can be: patch, minor, major (default: patch) + watch Watch for changes in src directory + bump-version [level] Bump version number + level can be: patch, minor, major (default: patch) + +Examples: + node build.js prepare + node build.js release minor + node build.js watch +`); +}; + +// Main +const args = process.argv.slice(2); +const command = args[0]; +const option = args[1]; + +if (!command) { + printUsage(); + process.exit(0); +} + +switch (command) { + case "prepare": + prepare(); + break; + case "package": + packageTask(); + break; + case "release": + release(option || "patch"); + break; + case "watch": + watch(); + break; + case "bump-version": + bumpVersion(option || "patch"); + break; + default: + console.error(`Unknown command: ${command}`); + printUsage(); + process.exit(1); +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..9daeafb9 --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +test diff --git a/index.html b/index.html deleted file mode 100644 index 1500c6ba..00000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Smart RSS - background process - - - - - - - - \ No newline at end of file diff --git a/manifest.json b/manifest.json deleted file mode 100644 index 14a2fa92..00000000 --- a/manifest.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "Smart RSS", - "developer": { - "name": "BS-Harou (Martin Kadlec)", - "url": "http://blog.martinkadlec.eu/" - }, - "description": "RSS Reader for Opera 15+!", - "manifest_version": 2, - "version": "2.0", - "background": { - "page": "index.html" - }, - "web_accessible_resources": ["libs/jquery.min.js", "libs/underscore.min.js", "libs/backbone.min.js", "libs/backbone.indexDB.js", "images/icon16_v2.png"], - "permissions": ["https://*/*", "storage", "unlimitedStorage", "alarms", "tabs", "contextMenus", "http://*/*"], - "content_security_policy": "script-src 'self' 'unsafe-eval' https://cdnjs.cloudflare.com/ajax/libs/mocha/ https://raw.github.com/chaijs/chai/master/; object-src 'unsafe-eval';", - "browser_action": { - "default_title": "Smart RSS", - "default_icon": { - "19": "images/reload_anim_1.png" - } - }, - "options_page": "options.html", - "icons": { - "19": "images/icon19-arrow-orange.png", - "48": "images/48-inverted-round.png", - "64": "images/64-inverted-round.png", - "96": "images/96-inverted-round.png", - "128": "images/128-inverted-round.png" - }, - "commands": { - "_execute_browser_action": { - "suggested_key": { - "windows": "Ctrl+Shift+R", - "mac": "Command+Shift+R", - "chromeos": "Ctrl+Shift+R", - "linux": "Ctrl+Shift+R" - } - } - } -} \ No newline at end of file diff --git a/options.html b/options.html deleted file mode 100644 index 2a034ff4..00000000 --- a/options.html +++ /dev/null @@ -1,286 +0,0 @@ - - - - - Smart RSS - Options - - - - - - -
-
- - -
- - -
-

Localization

-
- -
- -
- -
- -
- -
-
- -
-

Appearance

-
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
-
- -
-

Behaviour

-
- -
-
- -
-
- -
-
- -
-
- -
- -
- -
- -
- -
- -
- -
-
- -
-

Sound notifications

-
- -
- -
- -
- -
- -
- -
- -
-
- -
-

Export

-
- -
-
- -
-
- -
-

Import

-
- -
-
- -
-
- -
-

Clear data

-
- -
-
- -
-

Version

-

-
- -
-

Bug reports and feedback

-

If you found some bug or have some nice suggestion or you want to follow up on development, head over to following link :

- Martin Kadlec (BS-Harou) | Blog. -
-
- -
- -
- - - \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..0666f008 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2416 @@ +{ + "name": "smartrss", + "version": "2.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "smartrss", + "version": "2.0.0", + "license": "MIT", + "devDependencies": { + "@types/firefox-webext-browser": "^120.0.4", + "adm-zip": "^0.5.16", + "chokidar": "^3.5.3", + "eslint": "^8.57.1", + "md5-file": "^5.0.0", + "semver": "^7.7.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/firefox-webext-browser": { + "version": "120.0.4", + "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-120.0.4.tgz", + "integrity": "sha512-lBrpf08xhiZBigrtdQfUaqX1UauwZ+skbFiL8u2Tdra/rklkKadYmIzTwkNZSWtuZ7OKpFqbE2HHfDoFqvZf6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/md5-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", + "integrity": "sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw==", + "dev": true, + "license": "MIT", + "bin": { + "md5-file": "cli.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.4.3" + } + }, + "@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@types/firefox-webext-browser": { + "version": "120.0.4", + "resolved": "https://registry.npmjs.org/@types/firefox-webext-browser/-/firefox-webext-browser-120.0.4.tgz", + "integrity": "sha512-lBrpf08xhiZBigrtdQfUaqX1UauwZ+skbFiL8u2Tdra/rklkKadYmIzTwkNZSWtuZ7OKpFqbE2HHfDoFqvZf6w==", + "dev": true + }, + "@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true + }, + "acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + } + } + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true + }, + "import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "md5-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", + "integrity": "sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json index 2b533bbe..41b6f5c2 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,41 @@ { - "name": "smartrss", - "version": "2.0.0", - "description": "RSS Reader for Opera 15+", - "homepage": "http://blog.martinkadlec.eu/post/501-smart-rss-final-v10", - "dependencies": {}, - "devDependencies": { - "grunt": ">=0.4.1", - "grunt-contrib-jshint": ">=0.6.4", - "grunt-contrib-requirejs": ">=0.4.1", - "grunt-contrib-stylus": ">=0.8.0", - "grunt-contrib-watch": ">=0.5.3", - "grunt-contrib-yuidoc": "~0.5.0", - "ncp": "~0.4.2" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git://github.com/BS-Harou/Smart-RSS.git" - }, - "author": "Martin Kadlec", - "license": "BSD-2-Clause", - "bugs": { - "url": "https://github.com/BS-Harou/Smart-RSS/issues" - }, - "engine": "node >= 0.10.0" + "name": "smartrss", + "version": "2.0.0", + "description": "RSS Reader", + "homepage": "https://github.com/zakius/Smart-RSS", + "devDependencies": { + "@types/firefox-webext-browser": "^120.0.4", + "adm-zip": "^0.5.16", + "chokidar": "^3.5.3", + "eslint": "^8.57.1", + "md5-file": "^5.0.0", + "semver": "^7.7.1" + }, + "scripts": { + "prepare": "node build.js prepare", + "package": "node build.js package", + "release": "node build.js release", + "release:minor": "node build.js release minor", + "release:major": "node build.js release major", + "watch": "node build.js watch", + "bump-version": "node build.js bump-version", + "lint": "eslint src/" + }, + "repository": { + "type": "git", + "url": "git://github.com/zakius/Smart-RSS.git" + }, + "author": "zakius", + "license": "MIT", + "bugs": { + "url": "https://github.com/zakius/Smart-RSS/issues" + }, + "engines": { + "node": ">=12.0.0" + }, + "browserslist": [ + "last 2 Firefox versions", + "last 2 Chrome versions" + ], + "private": true } diff --git a/rss.html b/rss.html deleted file mode 100644 index 176cdb2e..00000000 --- a/rss.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Smart RSS - - - - - - -
-
-
-
-
-
-
-
- - \ No newline at end of file diff --git a/rss_content.html b/rss_content.html deleted file mode 100644 index 03022066..00000000 --- a/rss_content.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - -
-
- - - - diff --git a/scripts/app/app.js b/scripts/app/app.js deleted file mode 100644 index 084a33e7..00000000 --- a/scripts/app/app.js +++ /dev/null @@ -1,186 +0,0 @@ -/** - * @module App - */ -define([ - 'controllers/comm', - 'layouts/Layout', 'jquery', 'domReady!', 'collections/Actions', 'layouts/FeedsLayout', 'layouts/ArticlesLayout', - 'layouts/ContentLayout', 'staticdb/shortcuts', 'modules/Locale', 'views/ReportView', 'preps/all' -], -function (comm, Layout, $, doc, Actions, FeedsLayout, ArticlesLayout, ContentLayout, shortcuts, Locale, ReportView) { - - document.documentElement.style.fontSize = bg.settings.get('uiFontSize') + '%'; - - var templates = $('script[type="text/template"]'); - templates.each(function(i, el) { - el.innerHTML = Locale.translateHTML(el.innerHTML); - }); - - document.addEventListener('contextmenu', function(e) { - if (!e.target.matchesSelector('#region-content header, #region-content header *')) { - e.preventDefault(); - } - }); - - var app = window.app = new (Layout.extend({ - el: 'body', - fixURL: function(url) { - if (url.search(/[a-z]+:\/\//) == -1) { - url = 'http://' + url; - } - return url; - }, - events: { - 'mousedown': 'handleMouseDown', - 'click #panel-toggle': 'handleClickToggle' - }, - initialize: function() { - this.actions = new Actions(); - - window.addEventListener('blur', function(e) { - this.hideContextMenus(); - //$('.focused').removeClass('focused'); - - if (e.target instanceof window.Window) { - comm.trigger('stop-blur'); - } - - }.bind(this)); - - bg.settings.on('change:layout', this.handleLayoutChange, this); - bg.settings.on('change:panelToggled', this.handleToggleChange, this); - bg.sources.on('clear-events', this.handleClearEvents, this); - - if (bg.settings.get('enablePanelToggle')) { - $('#panel-toggle').css('display', 'block'); - } - - if (bg.settings.get('thickFrameBorders')) { - this.$el.addClass('thick-borders'); - } - }, - handleClearEvents: function(id) { - if (window == null || id == tabID) { - bg.settings.off('change:layout', this.handleLayoutChange, this); - bg.settings.off('change:panelToggled', this.handleToggleChange, this); - bg.sources.off('clear-events', this.handleClearEvents, this); - } - }, - handleLayoutChange: function() { - if (bg.settings.get('layout') == 'vertical') { - this.layoutToVertical(); - this.articles.enableResizing(bg.settings.get('layout'), bg.settings.get('posC')); - } else { - this.layoutToHorizontal(); - this.articles.enableResizing(bg.settings.get('layout'), bg.settings.get('posB')); - } - }, - layoutToVertical: function() { - $('.regions .regions').addClass('vertical'); - }, - layoutToHorizontal: function() { - $('.regions .regions').removeClass('vertical'); - }, - - /** - * Saves the panel toggle state (panel visible/hidden) - * @method handleClickToggle - */ - handleClickToggle: function() { - bg.settings.save('panelToggled', !bg.settings.get('panelToggled')); - }, - - /** - * Shows/hides the panel - * @method handleToggleChange - */ - handleToggleChange: function() { - this.feeds.$el.toggleClass('hidden', !bg.settings.get('panelToggled')); - $('#panel-toggle').toggleClass('toggled', bg.settings.get('panelToggled')); - - if (!bg.settings.get('panelToggled')) { - this.feeds.disableResizing(); - } else { - this.feeds.enableResizing('horizontal', bg.settings.get('posA')); - } - }, - handleMouseDown: function(e) { - if (!e.target.matchesSelector('.context-menu, .context-menu *, .overlay, .overlay *')) { - this.hideContextMenus(); - } - }, - hideContextMenus: function() { - comm.trigger('hide-overlays', { blur: true }); - }, - focusLayout: function(e) { - this.setFocus(e.currentTarget.getAttribute('name')); - }, - start: function() { - this.attach('feeds', new FeedsLayout); - this.attach('articles', new ArticlesLayout); - this.attach('content', new ContentLayout); - - - this.handleToggleChange(); - - this.trigger('start'); - this.trigger('start:after'); - - setTimeout(function(that) { - $('body').removeClass('loading'); - that.setFocus('articles'); - that.handleLayoutChange(); - - }, 0, this); - }, - report: function() { - var report = new ReportView(); - document.body.appendChild(report.render().el); - }, - handleKeyDown: function(e) { - var ac = document.activeElement; - if (ac && (ac.tagName == 'INPUT' || ac.tagName == 'TEXTAREA')) { - return; - } - - var str = ''; - if (e.ctrlKey) str += 'ctrl+'; - if (e.shiftKey) str += 'shift+'; - - if (e.keyCode > 46 && e.keyCode < 91) { - str += String.fromCharCode(e.keyCode).toLowerCase(); - } else if (e.keyCode in shortcuts.keys) { - str += shortcuts.keys[e.keyCode]; - } else { - return; - } - - var focus = document.activeElement.getAttribute('name'); - - if (focus && focus in shortcuts) { - if (str in shortcuts[focus]) { - app.actions.execute( shortcuts[focus][str], e); - e.preventDefault(); - return; - } - } - - if (str in shortcuts.global) { - app.actions.execute( shortcuts.global[str], e); - e.preventDefault(); - } - } - })); - - // Prevent context-menu when alt is pressed - document.addEventListener('keyup', function(e) { - if (e.keyCode == 18) { - e.preventDefault(); - } - }); - - - document.addEventListener('keydown', app.handleKeyDown); - - - return app; -}); \ No newline at end of file diff --git a/scripts/app/collections/Actions.js b/scripts/app/collections/Actions.js deleted file mode 100644 index f3657d1b..00000000 --- a/scripts/app/collections/Actions.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @module App - * @submodule collections/Actions - */ -define(['backbone', 'models/Action', 'staticdb/actions'], function (BB, Action, db) { - - /** - * Collection of executable actions. Actions are usually executed by shorcuts, buttons or context menus. - * @class Actions - * @constructor - * @extends Backbone.Collection - */ - var Actions = BB.Collection.extend({ - model: Action, - - /** - * @method initialize - */ - initialize: function() { - Object.keys(db).forEach(function(region) { - Object.keys(db[region]).forEach(function(name) { - var c = db[region][name]; - this.add({ name: region + ':' + name, fn: c.fn, icon: c.icon, title: c.title }); - }, this); - }, this); - }, - - /** - * Executes given action - * @method execute - * @param action {string|models/Action} - */ - execute: function(action) { - if (typeof action == 'string') action = this.get(action); - if (!action) { - console.log('Action "' + action + '" does not exists'); - return false; - } - var args = [].slice.call(arguments); - args.shift(); - action.get('fn').apply(app, args); - return true; - } - }); - - return Actions; -}); \ No newline at end of file diff --git a/scripts/app/collections/Groups.js b/scripts/app/collections/Groups.js deleted file mode 100644 index 7dca4419..00000000 --- a/scripts/app/collections/Groups.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @module App - * @submodule collections/Groups - */ -define(['backbone', 'models/Group'], function(BB, Group) { - - /** - * Collection of date groups - * @class Groups - * @constructor - * @extends Backbone.Collection - */ - var Groups = BB.Collection.extend({ - model: Group - }); - - return Groups; -}); \ No newline at end of file diff --git a/scripts/app/collections/MenuCollection.js b/scripts/app/collections/MenuCollection.js deleted file mode 100644 index 09fb3fae..00000000 --- a/scripts/app/collections/MenuCollection.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @module App - * @submodule collections/MenuCollection - */ -define(['backbone', 'models/MenuItem'], function(BB, MenuItem) { - - /** - * Each ContextMenu has its own MenuCollection instance - * @class MenuCollection - * @constructor - * @extends Backbone.Collection - */ - var MenuCollection = BB.Collection.extend({ - model: MenuItem - }); - - return MenuCollection; -}); \ No newline at end of file diff --git a/scripts/app/collections/ToolbarItems.js b/scripts/app/collections/ToolbarItems.js deleted file mode 100644 index 4a659810..00000000 --- a/scripts/app/collections/ToolbarItems.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @module App - * @submodule collections/ToolbarItems - */ -define(['backbone', 'models/ToolbarButton'], function (BB, ToolbarButton) { - - /** - * Each ToolbarView has its own ToolbarItems instance - * @class ToolbarItems - * @constructor - * @extends Backbone.Collection - */ - var ToolbarItems = BB.Collection.extend({ - model: ToolbarButton, - - comparator: function(tbItem1, tbItem2) { - if (!tbItem1.view || !tbItem2.view) return 0; - var r1 = tbItem1.view.el.getBoundingClientRect(); - var r2 = tbItem2.view.el.getBoundingClientRect(); - - return r1.left > r2.left ? 1 : -1; - } - }); - - return ToolbarItems; -}); \ No newline at end of file diff --git a/scripts/app/factories/ToolbarItemsFactory.js b/scripts/app/factories/ToolbarItemsFactory.js deleted file mode 100644 index f8c0300e..00000000 --- a/scripts/app/factories/ToolbarItemsFactory.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Factory for making instances of toolbar items - * @module App - * @submodule factories/ToolbarItemsFactory - */ -define([ - 'views/ToolbarButtonView', 'views/ToolbarDynamicSpaceView', 'views/ToolbarSearchView' -], -function (ToolbarButtonView, ToolbarDynamicSpaceView, ToolbarSearchView) { - - var ToolbarItemsFactory = { - /** - * Returns instance of toolbar item - * @method create - * @param name {string} - * @param itemModel {Object} - * @returns ToolbarDynamicSpaceView|ToolbarSearchView|ToolbarButtonView - */ - create: function(name, itemModel) { - if (name == 'dynamicSpace') { - return new ToolbarDynamicSpaceView({ model: itemModel }); - } else if (name == 'search') { - return new ToolbarSearchView({ model: itemModel }); - } else { - return new ToolbarButtonView({ model: itemModel }); - } - } - }; - - - return ToolbarItemsFactory; -}); \ No newline at end of file diff --git a/scripts/app/helpers/escapeHtml.js b/scripts/app/helpers/escapeHtml.js deleted file mode 100644 index cb1c1f6d..00000000 --- a/scripts/app/helpers/escapeHtml.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Escapes following characters: &, <, >, ", ' - * @module App - * @submodule helpers/escapeHtml - * @param string {String} String with html to be escaped - */ -define([], function() { - var entityMap = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''' - }; - - var escapeHtml = function(str) { - str = String(str).replace(/[&<>"']/gm, function (s) { - return entityMap[s]; - }); - str = str.replace(/\s/, function(f) { - if (f == ' ') return ' '; - return ''; - }); - return str; - }; - - return escapeHtml; -}); \ No newline at end of file diff --git a/scripts/app/helpers/formatDate.js b/scripts/app/helpers/formatDate.js deleted file mode 100644 index dfefd97f..00000000 --- a/scripts/app/helpers/formatDate.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Returns formatted date string - * @module App - * @submodule helpers/formatDate - * @param date {Integer|Date} Date to be formated - * @param formatString {String} String consisting of special characters - * @example formatDate(new Date, 'YYYY-MM-DD hh:mm'); - */ -define([], function() { - var formatDate = (function() { - var that; - var addZero = function(num) { - if (num < 10) num = '0' + num; - return num; - }; - var na = function(n, z) { - return n % z; - }; - var getDOY = function() { - var dt = new Date(that); - dt.setHours(0, 0, 0); - var onejan = new Date(dt.getFullYear(), 0, 1); - return Math.ceil((dt - onejan) / 86400000); - }; - var getWOY = function() { - var dt = new Date(that); - dt.setHours(0, 0, 0); - dt.setDate(dt.getDate() + 4 - (dt.getDay() || 7)); - var onejan = new Date(dt.getFullYear(), 0, 1); - return Math.ceil((((dt - onejan) / 86400000) + onejan.getDay() + 1) / 7); - }; - var dateVal = function(all, found) { - switch (found) { - case 'DD': - return addZero(that.getDate()); - case 'D': - return that.getDate(); - case 'MM': - return addZero(that.getMonth() + 1); - case 'M': - return that.getMonth() + 1; - case 'YYYY': - return that.getFullYear(); - case 'YY': - return that.getFullYear().toString().substr(2, 2); - case 'hh': - return addZero(that.getHours()); - case 'h': - return that.getHours(); - case 'HH': - return addZero(na(that.getHours(), 12)); - case 'H': - return na(that.getHours(), 12); - case 'mm': - return addZero(that.getMinutes()); - case 'm': - return that.getMinutes(); - case 'ss': - return addZero(that.getSeconds()); - case 's': - return that.getSeconds(); - case 'u': - return that.getMilliseconds(); - case 'U': - return that.getTime(); - case 'T': - return that.getTime() - that.getTimezoneOffset() * 60000; - case 'W': - return that.getDay(); - case 'y': - return getDOY(); - case 'w': - return getWOY(); - case 'G': - return that.getTimezoneOffset(); - case 'a': - return that.getHours() > 12 ? 'PM' : 'AM'; - default: - return ''; - } - }; - return function(date, str) { - if (!(date instanceof Date)) date = new Date(date); - that = date; - str = str.replace(/(DD|D|MM|M|YYYY|YY|hh|h|HH|H|mm|m|ss|s|u|U|W|y|w|G|a|T)/g, dateVal); - return str; - }; - }()); - - return formatDate; -}); \ No newline at end of file diff --git a/scripts/app/helpers/getWOY.js b/scripts/app/helpers/getWOY.js deleted file mode 100644 index 39984822..00000000 --- a/scripts/app/helpers/getWOY.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Get week of year - * @module App - * @submodule helpers/getWOY - * @param date {string|Date} Get the week based on this date - */ -define([], function() { - var getWOY = function(pdate) { - pdate = new Date(pdate); - pdate.setHours(0, 0, 0); - pdate.setDate(pdate.getDate() + 4 - (pdate.getDay() || 7)); - var onejan = new Date(pdate.getFullYear(), 0, 1); - return Math.ceil((((pdate - onejan) / 86400000) + onejan.getDay() + 1) / 7); - }; - return getWOY; -}); \ No newline at end of file diff --git a/scripts/app/helpers/unixutc.js b/scripts/app/helpers/unixutc.js deleted file mode 100644 index 11a8b209..00000000 --- a/scripts/app/helpers/unixutc.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Get unix time with added/substracted UTC hours - * @module App - * @submodule helpers/unixutc - * @param date {string|Date} Get the week based on this date - */ -define([], function() { - var _unixutcoff = (new Date).getTimezoneOffset() * 60000; - var unixutc = function(date) { - return date.getTime() - _unixutcoff; - }; - return unixutc; -}); \ No newline at end of file diff --git a/scripts/app/instances/contextMenus.js b/scripts/app/instances/contextMenus.js deleted file mode 100644 index 5f1caece..00000000 --- a/scripts/app/instances/contextMenus.js +++ /dev/null @@ -1,243 +0,0 @@ -define([ - 'backbone', 'views/ContextMenu', 'modules/Locale', 'views/feedList' -], -function(BB, ContextMenu, Locale) { - var sourceContextMenu = new ContextMenu([ - { - title: Locale.c.UPDATE, - icon: 'reload.png', - action: function() { - app.actions.execute('feeds:update'); - //bg.downloadOne(sourceContextMenu.currentSource); - } - }, - { - title: Locale.c.MARK_ALL_AS_READ, - icon: 'read.png', - action: function() { - app.actions.execute('feeds:mark'); - } - }, - { - title: Locale.c.DELETE, - icon: 'delete.png', - action: function() { - app.actions.execute('feeds:delete'); - } - }, - { - title: 'Refetch', /**** Localization needed****/ - icon: 'save.png', - action: function() { - app.actions.execute('feeds:refetch'); - } - }, - { - title: Locale.c.PROPERTIES, - icon: 'properties.png', - action: function() { - app.actions.execute('feeds:showProperties'); - } - } - ]); - - var trashContextMenu = new ContextMenu([ - { - title: Locale.c.MARK_ALL_AS_READ, - icon: 'read.png', - action: function() { - bg.items.where({ trashed: true, deleted: false }).forEach(function(item) { - if (item.get('unread') == true) { - item.save({ - unread: false, - visited: true - }); - } - }); - } - }, - { - title: Locale.c.EMPTY_TRASH, - icon: 'delete.png', - action: function() { - if (confirm(Locale.c.REALLY_EMPTY_TRASH)) { - bg.items.where({ trashed: true, deleted: false }).forEach(function(item) { - item.markAsDeleted(); - }); - } - } - } - ]); - - var allFeedsContextMenu = new ContextMenu([ - { - title: Locale.c.UPDATE_ALL, - icon: 'reload.png', - action: function() { - app.actions.execute('feeds:updateAll'); - } - }, - { - title: Locale.c.MARK_ALL_AS_READ, - icon: 'read.png', - action: function() { - if (confirm(Locale.c.MARK_ALL_QUESTION)) { - bg.items.forEach(function(item) { - item.save({ unread: false, visited: true }); - }); - } - } - }, - { - title: Locale.c.DELETE_ALL_ARTICLES, - icon: 'delete.png', - action: function() { - if (confirm(Locale.c.DELETE_ALL_Q)) { - bg.items.forEach(function(item) { - if (item.get('deleted') == true) return; - item.markAsDeleted(); - }); - } - } - } - ]); - - var folderContextMenu = new ContextMenu([ - { - title: Locale.c.UPDATE, - icon: 'reload.png', - action: function() { - app.actions.execute('feeds:update'); - } - }, - { - title: Locale.c.MARK_ALL_AS_READ, - icon: 'read.png', - action: function() { - app.actions.execute('feeds:mark'); - } - }, - { - title: Locale.c.DELETE, - icon: 'delete.png', - action: function() { - app.actions.execute('feeds:delete'); - } - }, - { - title: Locale.c.PROPERTIES, - icon: 'properties.png', - action: function() { - app.actions.execute('feeds:showProperties'); - } - } - /*{ - title: Locale.c.RENAME, - action: function() { - var feedList = require('views/feedList'); - var newTitle = prompt(Locale.c.FOLDER_NAME + ': ', feedList.selectedItems[0].model.get('title')); - if (!newTitle) return; - - feedList.selectedItems[0].model.save({ title: newTitle }); - } - }*/ - ]); - - var itemsContextMenu = new ContextMenu([ - { - title: Locale.c.NEXT_UNREAD + ' (H)', - icon: 'forward.png', - action: function() { - app.actions.execute('articles:nextUnread'); - } - }, - { - title: Locale.c.PREV_UNREAD + ' (Y)', - icon: 'back.png', - action: function() { - app.actions.execute('articles:prevUnread'); - } - }, - { - title: Locale.c.MARK_AS_READ + ' (K)', - icon: 'read.png', - action: function() { - app.actions.execute('articles:mark'); - } - }, - { - title: Locale.c.MARK_AND_NEXT_UNREAD + ' (G)', - icon: 'find_next.png', - action: function() { - app.actions.execute('articles:markAndNextUnread'); - } - }, - { - title: Locale.c.MARK_AND_PREV_UNREAD + ' (T)', - icon: 'find_previous.png', - action: function() { - app.actions.execute('articles:markAndPrevUnread'); - } - }, - { - title: Locale.c.FULL_ARTICLE, - icon: 'full_article.png', - action: function(e) { - app.actions.execute('articles:fullArticle', e); - } - }, - { - title: Locale.c.PIN + ' (P)', - icon: 'pinsource_context.png', - action: function() { - app.actions.execute('articles:pin'); - } - }, - { - title: Locale.c.DELETE + ' (D)', - icon: 'delete.png', - action: function(e) { - app.actions.execute('articles:delete', e); - } - }, - { - title: Locale.c.UNDELETE + ' (N)', - id: 'context-undelete', - icon: 'undelete.png', - action: function() { - app.actions.execute('articles:undelete'); - } - } - ]); - - var contextMenus = new (BB.View.extend({ - list: {}, - initialize: function() { - this.list = { - source: sourceContextMenu, - trash: trashContextMenu, - folder: folderContextMenu, - allFeeds: allFeedsContextMenu, - items: itemsContextMenu - }; - }, - get: function(name) { - if (name in this.list) { - return this.list[name]; - } - return null; - }, - hideAll: function() { - Object.keys(this.list).forEach(function(item) { - this.list[item].hide(); - }, this); - }, - areActive: function() { - return Object.keys(this.list).some(function(item) { - return !!this.list[item].el.parentNode; - }, this); - } - })); - - return contextMenus; -}); \ No newline at end of file diff --git a/scripts/app/instances/specials.js b/scripts/app/instances/specials.js deleted file mode 100644 index d296b873..00000000 --- a/scripts/app/instances/specials.js +++ /dev/null @@ -1,52 +0,0 @@ -define([ - 'backbone', 'models/Special', 'instances/contextMenus', 'modules/Locale', 'views/feedList' -], -function(BB, Special, contextMenus, Locale) { - - var specials = { - trash: new Special({ - title: Locale.c.TRASH, - icon: 'trashsource.png', - filter: { trashed: true, deleted: false }, - position: 'bottom', - name: 'trash', - onReady: function() { - this.contextMenu = contextMenus.get('trash'); - this.el.addEventListener('dragover', function(e) { - e.preventDefault(); - }); - this.el.addEventListener('drop', function(e) { - e.preventDefault(); - var ids = JSON.parse(e.dataTransfer.getData('text/plain') || '[]') || []; - ids.forEach(function(id) { - var item = bg.items.findWhere({ id: id }); - if (item && !item.get('trashed')) { - item.save({ - trashed: true - }); - } - }); - }); - } - }), - allFeeds: new Special({ - title: Locale.c.ALL_FEEDS, - icon: 'icon16_v2.png', - filter: { trashed: false }, - position: 'top', - name: 'all-feeds', - onReady: function() { - this.contextMenu = contextMenus.get('allFeeds'); - } - }), - pinned: new Special({ - title: Locale.c.PINNED, - icon: 'pinsource.png', - filter: { trashed: false, pinned: true }, - position: 'bottom', - name: 'pinned' - }) - }; - - return specials; -}); \ No newline at end of file diff --git a/scripts/app/layouts/ArticlesLayout.js b/scripts/app/layouts/ArticlesLayout.js deleted file mode 100644 index 59561cf9..00000000 --- a/scripts/app/layouts/ArticlesLayout.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @module App - * @submodule layouts/ArticlesLayout - */ -define([ - 'jquery', 'layouts/Layout', 'views/ToolbarView', 'views/articleList', - 'mixins/resizable', 'controllers/comm', 'domReady!' -], -function ($, Layout, ToolbarView, articleList, resizable, comm) { - - var toolbar = bg.toolbars.findWhere({ region: 'articles' }); - - /** - * Articles layout view - * @class ArticlesLayout - * @constructor - * @extends Layout - */ - var ArticlesLayout = Layout.extend({ - el: '#region-articles', - events: { - 'keydown': 'handleKeyDown', - 'mousedown': 'handleMouseDown' - }, - initialize: function() { - this.el.view = this; - - this.on('attach', function() { - this.attach('toolbar', new ToolbarView({ model: toolbar }) ); - this.attach('articleList', articleList ); - }); - - this.$el.on('focus', function() { - $(this).addClass('focused'); - clearTimeout(blurTimeout); - }); - - var focus = true; - var blurTimeout; - - comm.on('stop-blur', function() { - focus = false; - }); - - this.$el.on('blur', function(e) { - blurTimeout = setTimeout(function() { - if (focus && !e.relatedTarget) { - this.focus(); - return; - } - $(this).removeClass('focused'); - focus = true; - }.bind(this), 0); - }); - - - this.on('resize:after', this.handleResizeAfter); - this.on('resize', this.handleResize); - this.on('resize:enabled', this.handleResize); - - }, - /** - * Saves the new layout size - * @triggered after resize - * @method handleResizeAfter - */ - handleResizeAfter: function() { - if (bg.settings.get('layout') == 'horizontal') { - var wid = this.el.offsetWidth; - bg.settings.save({ posB: wid }); - } else { - var hei = this.el.offsetHeight; - bg.settings.save({ posC:hei }); - } - }, - /** - * Changes layout to one/two line according to width - * @triggered while resizing - * @method handleResize - */ - handleResize: function() { - if (bg.settings.get('lines') == 'auto') { - var oneRem = parseFloat(getComputedStyle(document.documentElement).fontSize); - if (this.el.offsetWidth > 37 * oneRem) { - this.articleList.$el.addClass('lines-one-line'); - } else { - this.articleList.$el.removeClass('lines-one-line'); - } - } - } - - }); - - ArticlesLayout = ArticlesLayout.extend(resizable); - - return ArticlesLayout; -}); \ No newline at end of file diff --git a/scripts/app/layouts/ContentLayout.js b/scripts/app/layouts/ContentLayout.js deleted file mode 100644 index 1b04060e..00000000 --- a/scripts/app/layouts/ContentLayout.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @module App - * @submodule layouts/ContentLayout - */ -define([ - 'jquery', 'layouts/Layout', 'views/ToolbarView', 'views/contentView', 'views/SandboxView', - 'views/OverlayView', 'views/LogView', 'controllers/comm', 'domReady!' -], -function ($, Layout, ToolbarView, contentView, SandboxView, OverlayView, LogView, comm) { - - var toolbar = bg.toolbars.findWhere({ region: 'content' }); - - /** - * Content layout view - * @class ContentLayout - * @constructor - * @extends Layout - */ - var ContentLayout = Layout.extend({ - - /** - * View element - * @property el - * @default #region-content - * @type HTMLElement - */ - el: '#region-content', - - /** - * @method initialize - */ - initialize: function() { - this.on('attach', function() { - - this.attach('toolbar', new ToolbarView({ model: toolbar }) ); - this.attach('content', contentView ); - this.attach('sandbox', new SandboxView() ); - this.attach('log', new LogView() ); - this.attach('overlay', new OverlayView() ); - - this.listenTo(comm, 'hide-overlays', this.hideOverlay); - }); - - this.$el.on('focus', function() { - $(this).addClass('focused'); - clearTimeout(blurTimeout); - }); - - var focus = true; - var blurTimeout; - - comm.on('stop-blur', function() { - focus = false; - }); - - this.$el.on('blur', function(e) { - blurTimeout = setTimeout(function() { - if (focus && !e.relatedTarget) { - this.focus(); - return; - } - $(this).removeClass('focused'); - focus = true; - }.bind(this), 0); - }); - - }, - - /** - * Hides config overlay - * @method hideOverlay - */ - hideOverlay: function() { - if (this.overlay.isVisible()) { - this.overlay.hide(); - } - } - - }); - - return ContentLayout; -}); \ No newline at end of file diff --git a/scripts/app/layouts/FeedsLayout.js b/scripts/app/layouts/FeedsLayout.js deleted file mode 100644 index 7750f3c0..00000000 --- a/scripts/app/layouts/FeedsLayout.js +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @module App - * @submodule layouts/FeedsLayout - */ -define([ - 'jquery', 'layouts/Layout', 'views/ToolbarView', 'views/feedList', - 'instances/contextMenus', 'views/Properties', 'mixins/resizable', 'views/IndicatorView', - 'controllers/comm', 'domReady!' -], -function ($, Layout, ToolbarView, feedList, contextMenus, Properties, resizable, IndicatorView, comm) { - - var toolbar = bg.toolbars.findWhere({ region: 'feeds' }); - - /** - * Feeds layout view - * @class FeedsLayout - * @constructor - * @extends Layout - */ - var FeedsLayout = Layout.extend({ - /** - * View element - * @property el - * @default #region-feeds - * @type HTMLElement - */ - el: '#region-feeds', - - /** - * @method initialize - */ - initialize: function() { - - this.on('attach', function() { - this.attach('toolbar', new ToolbarView({ model: toolbar }) ); - this.attach('properties', new Properties); - this.attach('feedList', feedList); - this.attach('indicator', new IndicatorView); - }); - - this.el.view = this; - - this.$el.on('focus', function() { - $(this).addClass('focused'); - clearTimeout(blurTimeout); - }); - - var focus = true; - var blurTimeout; - - comm.on('stop-blur', function() { - focus = false; - }); - - this.$el.on('blur', function(e) { - blurTimeout = setTimeout(function() { - if (focus && !e.relatedTarget) { - this.focus(); - return; - } - $(this).removeClass('focused'); - focus = true; - }.bind(this), 0); - }); - - this.on('resize:after', this.handleResize); - //window.addEventListener('resize', this.handleResize.bind(this)); - - this.enableResizing('horizontal', bg.settings.get('posA')); - }, - - /** - * Saves layout size - * @method handleResize - */ - handleResize: function() { - if (bg.settings.get('panelToggled')) { - var wid = this.el.offsetWidth; - bg.settings.save({ posA: wid }); - } - } - }); - - FeedsLayout = FeedsLayout.extend(resizable); - - return FeedsLayout; -}); \ No newline at end of file diff --git a/scripts/app/layouts/Layout.js b/scripts/app/layouts/Layout.js deleted file mode 100644 index 297998a2..00000000 --- a/scripts/app/layouts/Layout.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @module App - * @submodule layouts/Layout - */ -define(['backbone'], function(BB) { - - /** - * Layout abstract class - * @class Layout - * @constructor - * @extends Backbone.View - */ - var Layout = BB.View.extend({ - - /** - * Gives focus to layout region element - * @method setFocus - * @param name {String} Name of the region - */ - setFocus: function(name) { - if (!name || !this[name]) return; - this[name].el.focus(); - }, - - /** - * Appends new region to layout. - * If existing name is used, the old region is replaced with the new region - * and 'close' event is triggered on the old region - * @method attach - * @param name {String} Name of the region - * @param view {Backbone.View} Backbone view to be the attached region - */ - attach: function(name, view) { - var old = this[name]; - - this[name] = view; - if (!view.el.parentNode) { - if (old && old instanceof BB.View) { - old.$el.replaceWith(view.el); - old.trigger('close'); - } else { - this.$el.append(view.el); - } - } - view.trigger('attach'); - if (!this.focus) this.setFocus(name); - } - }); - - return Layout; -}); \ No newline at end of file diff --git a/scripts/app/mixins/resizable.js b/scripts/app/mixins/resizable.js deleted file mode 100644 index 512e0c67..00000000 --- a/scripts/app/mixins/resizable.js +++ /dev/null @@ -1,138 +0,0 @@ -define(['jquery'], function($) { - - var els = []; - - var resizeWidth = bg.settings.get('thickFrameBorders') ? 10 : 6; - - function handleMouseDown(e) { - this.resizing = true; - e.preventDefault(); - $('iframe').css('pointer-events', 'none'); - this.trigger('resize:before'); - } - - function handleMouseMove(e) { - if (this.resizing) { - var toLeft = bg.settings.get('thickFrameBorders') ? 5 : 1; - e.preventDefault(); - if (this.layout == 'vertical') { - setPosition.call(this, e.clientY); - this.$el.css('flex-basis', Math.abs(e.clientY - this.el.offsetTop + toLeft) ); - } else { - setPosition.call(this, e.clientX); - this.$el.css('flex-basis', Math.abs(e.clientX - this.el.offsetLeft + toLeft) ); - } - - this.trigger('resize'); - } - } - - function handleMouseUp() { - if (!this.resizing) return; - $('iframe').css('pointer-events', 'auto'); - this.resizing = false; - for (var i=0; i= this.el.offsetHeight) { - return false; - } - return true; - }, - handleSelectableMouseDown: function(e) { - //e.currentTarget.view.handleMouseDown(e); - var item = e.currentTarget.view; - if (this.selectedItems.length > 1 && item.$el.hasClass('selected') && !e.ctrlKey && !e.shiftKey) { - this.selectFlag = true; - return; - } - // used to be just { shiftKey: e.shiftKey, ctrlKey: e.ctrlKey } instead of 'e' - this.select(item, e); - }, - handleSelectableMouseUp: function(e) { - var item = e.currentTarget.view; - if (e.which == 1 && this.selectedItems.length > 1 && this.selectFlag) { - this.select(item, e); - this.selectFlag = false; - } - } - -}; -}); \ No newline at end of file diff --git a/scripts/app/models/Action.js b/scripts/app/models/Action.js deleted file mode 100644 index b33a15a8..00000000 --- a/scripts/app/models/Action.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @module App - * @submodule models/Action - */ -define(['backbone'], function (BB) { - - /** - * Executable action. Actions are usually executed by shorcuts, buttons or context menus. - * @class Action - * @constructor - * @extends Backbone.Model - */ - var Action = BB.Model.extend({ - /** - * @property idAttribute - * @type String - * @default name - */ - idAttribute: 'name', - defaults: { - /** - * @attribute name - * @type String - * @default global:default - */ - name: 'global:default', - - /** - * Function to be called when action is executed - * @attribute fn - * @type function - */ - fn: function() { - return function() {}; - }, - - /** - * @attribute icon - * @type String - * @default unknown.png - */ - icon: 'unknown.png', - - /** - * @attribute title - * @type String - * @default '' - */ - title: '' - } - }); - - return Action; -}); \ No newline at end of file diff --git a/scripts/app/models/Group.js b/scripts/app/models/Group.js deleted file mode 100644 index 3ebcab0a..00000000 --- a/scripts/app/models/Group.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * @module App - * @submodule models/Group - */ -define(['backbone', 'helpers/unixutc', 'helpers/getWOY', 'modules/Locale'], function(BB, unixutc, getWOY, Locale) { - - /** - * Date group model - * @class Group - * @constructor - * @extends Backbone.Model - */ - var Group = BB.Model.extend({ - defaults: { - /** - * Title of the date group (Today, Yesterday, 2012, ...) - * @attribute title - * @type String - * @default '' - */ - title: '', - - /** - * End date of date group (yesterdays date is midnight between yesterday and today) in unix time - * @attribute date - * @type Integer - * @default 0 - */ - date: 0 - }, - idAttribute: 'date' - }); - - /** - * Gets date group attributes of given date - * @method getGroup - * @static - * @param date {Integer|Date} - * @return {Object} Object contaning title & date attributes - */ - Group.getGroup = (function() { - var days = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY']; - var months = ['JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER']; - var dc = null; - var todayMidnight = null; - var dct = null; - - - return function(date) { - var dt = new Date(date); - dc = dc || new Date(); - - - var dtt = parseInt(unixutc(dt) / 86400000, 10); - dct = dct || parseInt(unixutc(dc) / 86400000, 10); - - if (!todayMidnight) { - todayMidnight = new Date(dc); - todayMidnight.setHours(0,0,0,0); - setTimeout(function() { - todayMidnight = null; - dc = null; - dct = null; - }, 10000); - } - - var itemMidnight = new Date(dt); - itemMidnight.setHours(0,0,0,0); - - var group; - var dtwoy, dcwoy; - - if (dtt >= dct) { - group = { - title: Locale.c.TODAY.toUpperCase(), - date: todayMidnight.getTime() + 86400000 * 5000 // 5000 = make sure "today" is the first element in list - }; - } else if (dtt + 1 == dct) { - group = { - title: Locale.c.YESTERDAY.toUpperCase(), - date: todayMidnight.getTime() - }; - } else if ((dtwoy = getWOY(dt)) == (dcwoy = getWOY(dc)) && dtt + 7 >= dct) { - group = { - title: Locale.c[days[dt.getDay()]].toUpperCase(), - date: itemMidnight.getTime() + 86400000 - }; - } else if (dtwoy + 1 == dcwoy && dtt + 14 >= dct) { - group = { - title: Locale.c.LAST_WEEK.toUpperCase(), - date: todayMidnight.getTime() - 86400000 * ( ((todayMidnight.getDay() || 7) - 1) || 1) - }; - } else if (dt.getMonth() == dc.getMonth() && dt.getFullYear() == dc.getFullYear()) { - group = { - title: Locale.c.EARLIER_THIS_MONTH.toUpperCase(), - date: todayMidnight.getTime() - 86400000 * ((todayMidnight.getDay() || 7) - 1) - 7 * 86400000 - }; - } else if (dt.getFullYear() == dc.getFullYear() ) { - group = { - title: Locale.c[months[dt.getMonth()]].toUpperCase(), - date: (new Date(dt.getFullYear(), dt.getMonth() + 1, 1)).getTime() - }; - } else { - group = { - title: dt.getFullYear(), - date: (new Date(dt.getFullYear() + 1, 0, 1)).getTime() - }; - } - - return group; - - }; - })(); - - return Group; -}); \ No newline at end of file diff --git a/scripts/app/models/MenuItem.js b/scripts/app/models/MenuItem.js deleted file mode 100644 index 605ff880..00000000 --- a/scripts/app/models/MenuItem.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @module App - * @submodule models/MenuItem - */ -define(['backbone'], function(BB) { - - /** - * Context menu item - * @class MenuItem - * @constructor - * @extends Backbone.Model - */ - var MenuItem = BB.Model.extend({ - defaults: { - - /** - * @attribute title - * @type String - * @default '' - */ - 'title': '', - - /** - * Function to be called when user selects this item - * @attribute action - * @type function - * @default null - */ - 'action': null - } - }); - - return MenuItem; -}); \ No newline at end of file diff --git a/scripts/app/models/Special.js b/scripts/app/models/Special.js deleted file mode 100644 index 7d89eb69..00000000 --- a/scripts/app/models/Special.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @module App - * @submodule models/Special - */ -define(['backbone'], function(BB) { - /** - * Model for special items in feed list like all-feeds, pinned and trash - * @class Special - * @constructor - * @extends Backbone.Model - */ - var Special = BB.Model.extend({ - defaults: { - /** - * Visible title of special. Chnages with localization. - * @attribute title - * @type String - * @default All feeds - */ - title: 'All feeds', - - /** - * @attribute icon - * @type String - * @default icon16_v2.png - */ - icon: 'icon16_v2.png', - - /** - * Name of the special. It is always the same for one special. - * @attribute name - * @type String - * @default '' - */ - name: '', - - /** - * Filter used in 'where' function of items collection - * @attribute filter - * @type Object - * @default {} - * @example { unread: true, trashed: false } - */ - filter: {}, - - /** - * Should the special be above or below feed sources? - * @attribute position - * @type String - * @default top - */ - position: 'top', - - /** - * Function to be called when specials view is initialized - * @attribute onReady - * @type function - * @default null - */ - onReady: null - } - }); - - return Special; -}); \ No newline at end of file diff --git a/scripts/app/models/ToolbarButton.js b/scripts/app/models/ToolbarButton.js deleted file mode 100644 index 90622e53..00000000 --- a/scripts/app/models/ToolbarButton.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @module App - * @submodule models/ToolbarButton - */ -define(['backbone'], function (BB) { - - /** - * Button model for toolbars - * @class ToolbarButton - * @constructor - * @extends Backbone.Model - */ - var ToolbarButton = BB.Model.extend({ - defaults: { - - /** - * @attribute actionName - * @type String - * @default global:default - */ - actionName: 'global:default', - - /** - * Is button aligned to left or right? - * @attribute position - * @type String - * @default left - */ - position: 'left' - } - }); - - return ToolbarButton; -}); \ No newline at end of file diff --git a/scripts/app/modules/Locale.js b/scripts/app/modules/Locale.js deleted file mode 100644 index fec5a5d0..00000000 --- a/scripts/app/modules/Locale.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @module App - * @submodule modules/Locale - */ -var nl = bg.settings.get('lang') || 'en'; -define(['../../nls/' + nl, '../../nls/en'], function (lang, en) { - - /** - * String localization - * @class Locale - * @constructor - * @extends Object - */ - var Locale = { - get c() { - return lang || en; - }, - translate: function(str) { - str = String(str); - if (lang) return lang[str]; - if (en) return en[str]; - return str; - }, - translateHTML: function(content) { - return String(content).replace(/\{\{(\w+)\}\}/gm, function(all, str) { - return lang[str] || en[str] || str; - }); - } - }; - - return Locale; - -}); \ No newline at end of file diff --git a/scripts/app/preps/all.js b/scripts/app/preps/all.js deleted file mode 100644 index 0d465c33..00000000 --- a/scripts/app/preps/all.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Loads all preps as dependencies - * @module App - * @submodule preps/extendNative - */ -define(['preps/extendNative'], function() { - return true; -}); \ No newline at end of file diff --git a/scripts/app/preps/extendNative.js b/scripts/app/preps/extendNative.js deleted file mode 100644 index 742db561..00000000 --- a/scripts/app/preps/extendNative.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Extends prototypes of native objects with various methods. - * Removes prefixes for some nonstandard fucntions. - * @module App - * @submodule preps/extendNative - */ -define([], function() { - - /** - * Set or get last array item - * @method last - * @extends Array - * @param value {Any} Value to set - optional - * @return {Any} Last item of array, null if array is empty - */ - Array.prototype.last = function(val) { - if (!this.length) return null; - if (val) this[this.length - 1] = val; - return this[this.length - 1]; - }; - - /** - * Set or get first array item - * @method last - * @extends Array - * @param value {Any} Value to set - optional - * @return {Any} First item of array, null if array is empty - */ - Array.prototype.first = function(val) { - if (!this.length) return null; - if (val) this[0] = val; - return this[0]; - }; - - /** - * Get index of element in HTMLCollection (used by eg. Element#children) - * @method indexOf - * @extends HTMLCollection - * @param element {HTMLElement} Element fo find index of - * @return {Any} First item of array, null if array is empty - */ - HTMLCollection.prototype.indexOf = Array.prototype.indexOf; - - window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame; - - if (!Element.prototype.hasOwnProperty('matchesSelector')) { - Element.prototype.matchesSelector = Element.prototype.webkitMatchesSelector; - } - - /** - * Git first next sibling that matches given selector - * @method findNext - * @extends Element - * @param query {String} CSS selector - * @return {HTMLELement|null} Found element - */ - Element.prototype.findNext = function(query) { - var cur = this; - while (cur = cur.nextElementSibling) { - if (cur.matchesSelector(query)) { - return cur; - } - } - return null; - }; - - /** - * Git first previous sibling that matches given selector - * @method findPrev - * @extends Element - * @param query {String} CSS selector - * @return {HTMLELement|null} Found element - */ - Element.prototype.findPrev = function(query) { - var cur = this; - while (cur = cur.previousElementSibling) { - if (cur.matchesSelector(query)) { - return cur; - } - } - return null; - }; - - /** - * Escapes regexp characters in string - * @method escape - * @extends RegExp - * @static - * @param text {String} String to be escaped - * @return {String} Escaped string - */ - RegExp.escape = function(str) { - return String(str).replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); - }; -}); \ No newline at end of file diff --git a/scripts/app/staticdb/actions.js b/scripts/app/staticdb/actions.js deleted file mode 100644 index 26b59c54..00000000 --- a/scripts/app/staticdb/actions.js +++ /dev/null @@ -1,723 +0,0 @@ -define(['jquery', 'underscore', 'helpers/stripTags', 'modules/Locale', 'controllers/comm'], function($, _, stripTags, Locale, comm) { - -return { - global: { - default: { - title: 'Unknown', - fn: function() { - alert('no action'); - } - }, - hideOverlays: { - title: 'Hide Overlays', - fn: function() { - comm.trigger('hide-overlays'); - } - }, - runTests: { - title: 'Run tests (dev dependencies needed)', - fn: function() { - require(['../runtests']); - } - }, - openOptions: { - title: 'Options', - icon: 'options.png', - fn: function() { - window.open('options.html'); - } - }, - report: { - title: 'Report a problem', - icon: 'report.png', - fn: function() { - app.report(); - } - } - }, - feeds: { - updateAll: { - icon: 'reload.png', - title: Locale.c.UPDATE_ALL, - fn: function() { - bg.loader.downloadAll(true); - } - }, - update: { - icon: 'reload.png', - title: Locale.c.UPDATE, - fn: function() { - var s = require('views/feedList').selectedItems; - if (s.length) { - bg.loader.download(_.pluck(s, 'model')); - } - } - }, - stopUpdate: { - icon: 'stop.png', - title: 'Stop updating feeds', - fn: function() { - bg.loader.abortDownloading(); - } - }, - mark: { - icon: 'read.png', - title: Locale.c.MARK_ALL_AS_READ, - fn: function() { - var s = require('views/feedList').getSelectedFeeds(); - if (!s.length) return; - - bg.items.forEach(function(item) { - if (item.get('unread') == true && s.indexOf(item.getSource()) >= 0) { - item.save({ - unread: false, - visited: true - }); - } - }); - - s.forEach(function(source) { - if (source.get('hasNew')) { - source.save({ hasNew: false }); - } - }); - - } - }, - refetch: { - title: 'Refetch', /****localization needed****/ - fn: function() { - var s = require('views/feedList').getSelectedFeeds(); - if (!s.length) return; - - s.forEach(function(source) { - bg.items.where({ sourceID: source.get('id') }).forEach(function(item) { - item.destroy(); - }); - }); - - app.actions.execute('feeds:update'); - - } - }, - delete: { - icon: 'delete.png', - title: Locale.c.DELETE, - fn: function() { - if (!confirm(Locale.c.REALLY_DELETE)) return; - - var feeds = require('views/feedList').getSelectedFeeds(); - var folders = require('views/feedList').getSelectedFolders(); - - feeds.forEach(function(feed) { - feed.destroy(); - }); - - folders.forEach(function(folder) { - folder.destroy(); - }); - } - }, - showProperties: { - icon: 'properties.png', - title: Locale.c.PROPERTIES, - fn: function() { - var properties = app.feeds.properties; - - var feedList = require('views/feedList'); - - var feeds = feedList.getSelectedFeeds(); - var folders = feedList.getSelectedFolders(); - - if (feedList.selectedItems.length == 1 && folders.length == 1) { - properties.show(folders[0]); - } else if (!folders.length && feeds.length == 1) { - properties.show(feeds[0]); - } else if (feeds.length > 0) { - properties.show(feeds); - } - - } - }, - addSource: { - icon: 'add.png', - title: Locale.c.ADD_RSS_SOURCE, - fn: function() { - var url = (prompt(Locale.c.RSS_FEED_URL) || '').trim(); - if (!url) return; - - var folderID = 0; - var list = require('views/feedList'); - if (list.selectedItems.length && list.selectedItems[0].$el.hasClass('folder')) { - var fid = list.selectedItems[0].model.get('id'); - // make sure source is not added to folder which is not in db - if (bg.folders.get(fid)) { - folderID = fid; - } - } - - url = app.fixURL(url); - var duplicate = bg.sources.findWhere({ url: url }); - - if (!duplicate) { - var newFeed = bg.sources.create({ - title: url, - url: url, - updateEvery: 180, - folderID: folderID - }, { wait: true }); - app.trigger('focus-feed', newFeed.get('id')); - } else { - app.trigger('focus-feed', duplicate.get('id')); - } - } - }, - addFolder: { - icon: 'add_folder.png', - title: Locale.c.NEW_FOLDER, - fn: function() { - var title = (prompt(Locale.c.FOLDER_NAME + ': ') || '').trim(); - if (!title) return; - - bg.folders.create({ - title: title - }, { wait: true }); - } - }, - focus: { - title: 'Focus feeds', - fn: function() { - app.setFocus('feeds'); - } - }, - selectNext: { - title: 'Select next', - fn: function(e) { - require('views/feedList').selectNext(e); - } - }, - selectPrevious: { - title: 'Select previous', - fn: function(e) { - require('views/feedList').selectPrev(e); - } - }, - closeFolders: { - title: 'Close folders', - fn: function(e) { - var folders = $('.folder.opened'); - if (!folders.length) return; - folders.each(function(i, folder) { - if (folder.view) { - folder.view.handleClickArrow(e); - } - }); - } - }, - openFolders: { - title: 'Open folders', - fn: function(e) { - var folders = $('.folder:not(.opened)'); - if (!folders.length) return; - folders.each(function(i, folder) { - if (folder.view) { - folder.view.handleClickArrow(e); - } - }); - } - }, - toggleFolder: { - title: 'Toggle folder', - fn: function(e) { - e = e || {}; - var cs = require('views/feedList').selectedItems; - if (cs.length && cs[0].$el.hasClass('folder')) { - cs[0].handleClickArrow(e); - } - } - }, - showArticles: { - title: 'Show articles', - fn: function(e) { - e = e || {}; - var t = e.target || {}; - var feedList = require('views/feedList'); - var feeds = feedList.getSelectedFeeds(); - var ids = _.pluck(feeds, 'id'); - var special = $('.special.selected').get(0); - if (special) special = special.view.model; - - app.trigger('select:' + feedList.el.id, { - action: 'new-select', - feeds: ids, - // _.extend is important, because otherwise it would be sent by reference - filter: special ? _.extend({}, special.get('filter')) : null, - name: special ? special.get('name') : null, - unreadOnly: !!e.altKey || t.className == 'source-counter' - }); - - - if (special && special.get('name') == 'all-feeds') { - bg.sources.forEach(function(source) { - if (source.get('hasNew')) { - source.save({ hasNew: false }); - } - }); - - } else if (ids.length) { - bg.sources.forEach(function(source) { - if (source.get('hasNew') && ids.indexOf(source.id) >= 0) { - source.save({ hasNew: false }); - } - }); - } - } - }, - showAndFocusArticles: { - title: 'Show and focus articles', - fn: function(e) { - e = e || {}; - var cs = require('views/feedList').selectedItems; - if (cs.length) { - app.actions.execute('feeds:showArticles', e); - app.actions.execute('articles:focus'); - } - } - } - }, - articles: { - mark: { - icon: 'read.png', - title: Locale.c.MARK_AS_READ, - fn: function() { - require('views/articleList').changeUnreadState(); - } - }, - update: { - icon: 'reload.png', - title: Locale.c.UPDATE, - fn: function() { - var list = require('views/articleList'); - if (list.currentData.feeds.length) { - list.currentData.feeds.forEach(function(id) { - bg.loader.downloadOne(bg.sources.get(id)); - }); - } else { - bg.loader.downloadAll(true); // true = force - } - } - }, - delete: { - icon: 'delete.png', - title: Locale.c.DELETE, - fn: function(e) { - var list = require('views/articleList'); - if (list.currentData.name == 'trash' || e.shiftKey) { - list.destroyBatch(list.selectedItems, list.removeItemCompletely); - } else { - list.destroyBatch(list.selectedItems, list.removeItem); - } - } - }, - undelete: { - icon: 'undelete.png', - title: Locale.c.UNDELETE, - fn: function() { - var articleList = require('views/articleList'); - if (!articleList.selectedItems || !articleList.selectedItems.length || articleList.currentData.name != 'trash') return; - articleList.destroyBatch(articleList.selectedItems, articleList.undeleteItem); - } - }, - selectNext: { - fn: function(e) { - require('views/articleList').selectNext(e); - } - }, - selectPrevious: { - fn: function(e) { - require('views/articleList').selectPrev(e); - } - }, - search: { - title: Locale.c.SEARCH_TIP, - fn: function(e) { - e = e || { currentTarget: $('input[type=search]').get(0) }; - var str = e.currentTarget.value || ''; - var list = require('views/articleList'); - if (str == '') { - $('.date-group').css('display', 'block'); - } else { - $('.date-group').css('display', 'none'); - } - - var searchInContent = false; - if (str[0] && str[0] == ':') { - str = str.replace(/^:/, '', str); - searchInContent = true; - } - var rg = new RegExp(RegExp.escape(str), 'i'); - list.views.some(function(view) { - if (!view.model) return true; - if (rg.test(view.model.get('title')) || rg.test(view.model.get('author')) || (searchInContent && rg.test(view.model.get('content')) )) { - view.$el.removeClass('invisible'); - } else { - view.$el.addClass('invisible'); - } - }); - - list.redraw(); - - list.restartSelection(); - } - }, - focusSearch: { - title: 'Focus Search', - fn: function() { - $('input[type=search]').focus(); - } - }, - focus: { - title: 'Focus Articles', - fn: function() { - app.setFocus('articles'); - } - }, - fullArticle: { - title: Locale.c.FULL_ARTICLE, - icon: 'full_article.png', - fn: function(e) { - var articleList = app.articles.articleList; - if (!articleList.selectedItems || !articleList.selectedItems.length) return; - if (articleList.selectedItems.length > 10 && bg.settings.get('askOnOpening')) { - if (!confirm('Do you really want to open ' + articleList.selectedItems.length + ' articles?')) { - return; - } - } - articleList.selectedItems.forEach(function(item) { - chrome.tabs.create({ url: stripTags(item.model.get('url')), active: !e.shiftKey }); - }); - } - }, - oneFullArticle: { - title: 'One full article', - fn: function(e) { - e = e || {}; - var articleList = app.articles.articleList; - var view; - if ('currentTarget' in e) { - view = e.currentTarget.view; - } else { - if (!articleList.selectedItems || !articleList.selectedItems.length) return; - view = articleList.selectedItems[0]; - } - if (view.model) { - chrome.tabs.create({ url: stripTags(view.model.get('url')), active: !e.shiftKey }); - } - } - }, - markAndNextUnread: { - title: Locale.c.MARK_AND_NEXT_UNREAD, - icon: 'find_next.png', - fn: function() { - require('views/articleList').changeUnreadState({ onlyToRead: true }); - require('views/articleList').selectNext({ selectUnread: true }); - } - }, - markAndPrevUnread: { - title: Locale.c.MARK_AND_PREV_UNREAD, - icon: 'find_previous.png', - fn: function() { - require('views/articleList').changeUnreadState({ onlyToRead: true }); - require('views/articleList').selectPrev({ selectUnread: true }); - } - }, - nextUnread: { - title: Locale.c.NEXT_UNREAD, - icon: 'forward.png', - fn: function() { - require('views/articleList').selectNext({ selectUnread: true }); - } - }, - prevUnread: { - title: Locale.c.PREV_UNREAD, - icon: 'back.png', - fn: function() { - require('views/articleList').selectPrev({ selectUnread: true }); - } - }, - markAllAsRead: { - title: Locale.c.MARK_ALL_AS_READ, - icon: 'read.png', - fn: function() { - var articleList = require('views/articleList'); - var f = articleList.currentData.feeds; - var filter = articleList.currentData.filter; - if (f.length) { - (filter ? bg.items.where(articleList.currentData.filter) : bg.items).forEach(function(item) { - if (item.get('unread') == true && f.indexOf(item.get('sourceID')) >= 0) { - item.save({ unread: false, visited: true }); - } - }); - } else if (articleList.currentData.name == 'all-feeds') { - if (confirm(Locale.c.MARK_ALL_QUESTION)) { - bg.items.forEach(function(item) { - if (item.get('unread') == true) { - item.save({ unread: false, visited: true }); - } - }); - } - } else if (articleList.currentData.filter) { - bg.items.where(articleList.specialFilter).forEach(function(item) { - item.save({ unread: false, visited: true }); - }); - } - } - }, - selectAll: { - title: 'Select All', - fn: function() { - var articleList = require('views/articleList'); - articleList.$el.find('.selected').removeClass('selected'); - articleList.selectedItems = []; - - articleList.$el.find('.item:not(.invisible)').each(function(i, item) { - item.view.$el.addClass('selected'); - articleList.selectedItems.push(item.view); - }); - - articleList.$el.find('.last-selected').removeClass('last-selected'); - articleList.$el.find('.item:not(.invisible):last').addClass('last-selected'); - } - }, - pin: { - title: Locale.c.PIN, - icon: 'pinsource_context.png', - fn: function() { - var articleList = require('views/articleList'); - if (!articleList.selectedItems || !articleList.selectedItems.length) return; - var val = !articleList.selectedItems[0].model.get('pinned'); - articleList.selectedItems.forEach(function(item) { - item.model.save({ pinned: val }); - }); - } - }, - spaceThrough: { - title: 'Space Through', - fn: function() { - var articleList = require('views/articleList'); - if (!articleList.selectedItems || !articleList.selectedItems.length) return; - app.trigger('space-pressed'); - } - }, - pageUp: { - title: 'Page up', - fn: function() { - var el = require('views/articleList').el; - el.scrollByPages(-1); - } - }, - pageDown: { - title: 'Page down', - fn: function() { - var el = require('views/articleList').el; - el.scrollByPages(1); - } - }, - scrollToBottom: { - title: 'Scroll to bottom', - fn: function() { - var el = require('views/articleList').el; - el.scrollTop = el.scrollHeight; - } - }, - scrollToTop: { - title: 'Scroll to top', - fn: function() { - var el = require('views/articleList').el; - el.scrollTop = 0; - } - }, - download: { - title: Locale.c.DOWNLOAD, - icon: 'save.png', - fn: function() { - var contentView = require('views/contentView'); - var articleList = require('views/articleList'); - if (!articleList.selectedItems.length) { - app.actions.execute('content:download'); - return; - } - var tpl = contentView.downloadTemplate; - - var list = {}; - list.articles = articleList.selectedItems.map(function(itemView) { - var attrs = Object.create(itemView.model.attributes); - attrs.date = contentView.getFormatedDate(attrs.date); - return attrs; - }); - - var blob = new Blob([ tpl(list) ], { type: 'text\/html' }); - var reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onload = function() { - window.open(this.result.replace('data:text/html;', 'data:text/html;charset=utf-8;')); - }; - /*var url = URL.createObjectURL(blob); - window.open(url); - setTimeout(function() { - URL.revokeObjectURL(url); - }, 30000);*/ - } - } - }, - content: { - download: { - title: Locale.c.DOWNLOAD, - icon: 'save.png', - fn: function() { - var contentView = require('views/contentView'); - if (!contentView.model) return; - var tpl = contentView.downloadTemplate; - var attrs = Object.create(contentView.model.attributes); - attrs.date = contentView.getFormatedDate(attrs.date); - var list = { articles: [attrs] }; - var blob = new Blob([ tpl(list) ], { type: 'text\/html' }); - var reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onload = function() { - window.open(this.result.replace('data:text/html;', 'data:text/html;charset=utf-8;')); - }; - /*var url = URL.createObjectURL(blob); - window.open(url); - setTimeout(function() { - URL.revokeObjectURL(url); - }, 30000);*/ - } - }, - print: { - title: Locale.c.PRINT, - icon: 'print.png', - fn: function() { - var contentView = require('views/contentView'); - if (!contentView.model) return; - window.print(); - } - }, - mark: { - title: Locale.c.MARK_AS_READ, - icon: 'read.png', - fn: function() { - var contentView = require('views/contentView'); - if (!contentView.model) return; - contentView.model.save({ - unread: !contentView.model.get('unread'), - visited: true - }); - } - }, - delete: { - title: Locale.c.DELETE, - icon: 'delete.png', - fn: function(e) { - var contentView = require('views/contentView'); - if (!contentView.model) return; - - askRmPinned = bg.settings.get('askRmPinned') - if (e.shiftKey) { - if (contentView.model.get('pinned') && askRmPinned && askRmPinned != 'none') { - var conf = confirm(Locale.c.PIN_QUESTION_A + contentView.model.escape('title') + Locale.c.PIN_QUESTION_B); - if (!conf) { - return; - } - } - - contentView.model.markAsDeleted(); - } else { - if (contentView.model.get('pinned') && askRmPinned == 'all') { - var conf = confirm(Locale.c.PIN_QUESTION_A + contentView.model.escape('title') + Locale.c.PIN_QUESTION_B); - if (!conf) { - return; - } - } - - contentView.model.save({ - trashed: true, - visited: true - }); - } - } - }, - showConfig: { - title: Locale.c.SETTINGS, - icon: 'config.png', - fn: function() { - app.content.overlay.show(); - } - }, - focus: { - title: 'Focus Article', - fn: function() { - app.setFocus('content'); - } - }, - focusSandbox: { - title: 'Focus Article', - fn: function() { - app.content.sandbox.el.focus(); - } - }, - scrollDown: { - title: 'Scroll down', - fn: function() { - var cw = $('iframe').get(0).contentWindow; - cw.scrollBy(0, 40); - } - }, - scrollUp: { - title: 'Scroll up', - fn: function() { - var cw = $('iframe').get(0).contentWindow; - cw.scrollBy(0, -40); - } - }, - spaceThrough: { - title: 'Space trough', - fn: function() { - require('views/contentView').handleSpace(); - } - }, - pageUp: { - title: 'Page up', - fn: function() { - var cw = $('iframe').get(0).contentWindow; - var d = $('iframe').get(0).contentWindow.document; - cw.scrollBy(0, -d.documentElement.clientHeight * 0.85); - } - }, - pageDown: { - title: 'Page down', - fn: function() { - var cw = $('iframe').get(0).contentWindow; - var d = $('iframe').get(0).contentWindow.document; - cw.scrollBy(0, d.documentElement.clientHeight * 0.85); - } - }, - scrollToBottom: { - title: 'Scroll to bottom', - fn: function() { - var cw = $('iframe').get(0).contentWindow; - var d = $('iframe').get(0).contentWindow.document; - cw.scrollTo(0, d.documentElement.offsetHeight); - } - }, - scrollToTop: { - title: 'Scroll to top', - fn: function() { - var cw = $('iframe').get(0).contentWindow; - cw.scrollTo(0, 0); - } - } - } - -}; // end actions object -}); // end define function \ No newline at end of file diff --git a/scripts/app/staticdb/shortcuts.js b/scripts/app/staticdb/shortcuts.js deleted file mode 100644 index 94ec8d85..00000000 --- a/scripts/app/staticdb/shortcuts.js +++ /dev/null @@ -1,118 +0,0 @@ -define({ - global: { - 'shift+1': 'feeds:focus', - 'shift+2': 'articles:focus', - 'shift+3': 'content:focus', - 'shift+4': 'content:focusSandbox', - 'esc': 'global:hideOverlays', - 'shift+insert': 'global:runTests' - }, - feeds: { - 'up': 'feeds:selectPrevious', - 'down': 'feeds:selectNext', - 'u': 'feeds:selectPrevious', - 'j': 'feeds:selectNext', - - 'ctrl+left': 'feeds:closeFolders', - 'ctrl+right': 'feeds:openFolders', - 'left': 'feeds:toggleFolder', - 'right': 'feeds:showArticles', - 'enter': 'feeds:showAndFocusArticles', - - 'shift+j': 'feeds:selectNext', - 'shift+down': 'feeds:selectNext', - 'shift+u': 'feeds:selectPrevious', - 'shift+up': 'feeds:selectPrevious', - }, - articles: { - 'd': 'articles:delete', - 'del': 'articles:delete', - 'shift+d': 'articles:delete', - 'shift+del': 'articles:delete', - 'ctrl+f': 'articles:focusSearch', - 'shift+enter': 'articles:fullArticle', - 'enter': 'articles:fullArticle', - 'k': 'articles:mark', - 'j': 'articles:selectNext', - 'down': 'articles:selectNext', - 'u': 'articles:selectPrevious', - 'up': 'articles:selectPrevious', - - 'shift+j': 'articles:selectNext', - 'shift+down': 'articles:selectNext', - 'shift+u': 'articles:selectPrevious', - 'shift+up': 'articles:selectPrevious', - - 'g': 'articles:markAndNextUnread', - 't': 'articles:markAndPrevUnread', - 'h': 'articles:nextUnread', - 'y': 'articles:prevUnread', - 'z': 'articles:prevUnread', - - 'ctrl+shift+a': 'articles:markAllAsRead', - 'ctrl+a': 'articles:selectAll', - 'p': 'articles:pin', - 'n': 'articles:undelete', - 'space': 'articles:spaceThrough', - 'r': 'articles:update', - - 'pgup': 'articles:pageUp', - 'pgdown': 'articles:pageDown', - 'end': 'articles:scrollToBottom', - 'home': 'articles:scrollToTop' - }, - content: { - 'up': 'content:scrollUp', - 'down': 'content:scrollDown', - 'space': 'content:spaceThrough', - 'pgup': 'content:pageUp', - 'pgdown': 'content:pageDown', - 'end': 'content:scrollToBottom', - 'home': 'content:scrollToTop', - 'del': 'content:delete', - 'd': 'content:delete', - 'k': 'content:mark', - - 'g': 'articles:markAndNextUnread', - 't': 'articles:markAndPrevUnread', - 'h': 'articles:nextUnread', - 'y': 'articles:prevUnread', - 'z': 'articles:prevUnread', - 'j': 'articles:selectNext', - 'u': 'articles:selectPrevious' - }, - sandbox: { - 'del': 'content:delete', - 'd': 'content:delete', - 'k': 'content:mark', - 'space': 'content:spaceThrough', - - 'g': 'articles:markAndNextUnread', - 't': 'articles:markAndPrevUnread', - 'h': 'articles:nextUnread', - 'y': 'articles:prevUnread', - 'z': 'articles:prevUnread', - 'j': 'articles:selectNext', - 'u': 'articles:selectPrevious' - }, - keys: { - 8: 'backspace', - 9: 'tab', - 13: 'enter', - //16: 'shift', - //17: 'ctrl', - 20: 'capslock', - 27: 'esc', - 32: 'space', - 33: 'pgup', - 34: 'pgdown', - 35: 'end', - 36: 'home', - 37: 'left', - 38: 'up', - 39: 'right', - 40: 'down', - 45: 'insert', - 46: 'del' - } -}); diff --git a/scripts/app/templates/download.html b/scripts/app/templates/download.html deleted file mode 100644 index 3a5c2da5..00000000 --- a/scripts/app/templates/download.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - <%- (articles[0] ? articles[0].title : 'Feed') %> - - - - <% for (var i=0; i -
-
- -
- - - - - - -
<%- articles[i].date %>|<%- articles[i].author %>
-
-
-
<%= articles[i].content %>
-
- <% } %> - - - diff --git a/scripts/app/templates/folder.html b/scripts/app/templates/folder.html deleted file mode 100644 index 8819e4ea..00000000 --- a/scripts/app/templates/folder.html +++ /dev/null @@ -1,6 +0,0 @@ -
- -
<%- title %>
-<% if (count > 0) { %> -
<%- count %>
-<% } %> \ No newline at end of file diff --git a/scripts/app/templates/header.html b/scripts/app/templates/header.html deleted file mode 100644 index db1d009d..00000000 --- a/scripts/app/templates/header.html +++ /dev/null @@ -1,10 +0,0 @@ -

- <% if (titleIsLink) { %><% } %> - <%= title %> - <% if (titleIsLink) { %><% } %> -

-
-

<%- author %>

-

<%= date %>

-

>

-
\ No newline at end of file diff --git a/scripts/app/templates/item.html b/scripts/app/templates/item.html deleted file mode 100644 index c8e23b31..00000000 --- a/scripts/app/templates/item.html +++ /dev/null @@ -1,4 +0,0 @@ -
<%= title %>
-
-
<%- author %>
-
<%- date %>
\ No newline at end of file diff --git a/scripts/app/templates/log.html b/scripts/app/templates/log.html deleted file mode 100644 index a3b9ecf9..00000000 --- a/scripts/app/templates/log.html +++ /dev/null @@ -1,2 +0,0 @@ - -
\ No newline at end of file diff --git a/scripts/app/templates/overlay.html b/scripts/app/templates/overlay.html deleted file mode 100644 index 99edf209..00000000 --- a/scripts/app/templates/overlay.html +++ /dev/null @@ -1,34 +0,0 @@ - -
- - -
- - - - - - - - -{{OPTIONS}} \ No newline at end of file diff --git a/scripts/app/templates/properties.html b/scripts/app/templates/properties.html deleted file mode 100644 index 367f7f9e..00000000 --- a/scripts/app/templates/properties.html +++ /dev/null @@ -1,52 +0,0 @@ -<% if (typeof title != 'undefined') { %> - -<% } %> - -<% if (typeof url != 'undefined') { %> - -<% } %> - - - -<% if (typeof url != 'undefined') { %> -
{{MORE}}
-
- - - -<% } %> - - - -<% if (typeof url != 'undefined') { %> -
-<% } %> - - \ No newline at end of file diff --git a/scripts/app/templates/report.html b/scripts/app/templates/report.html deleted file mode 100644 index 637bebaa..00000000 --- a/scripts/app/templates/report.html +++ /dev/null @@ -1,19 +0,0 @@ -
- Report a problem - - - - - - - - - -
- -
- -
- -
-
\ No newline at end of file diff --git a/scripts/app/templates/source.html b/scripts/app/templates/source.html deleted file mode 100644 index ebbb181b..00000000 --- a/scripts/app/templates/source.html +++ /dev/null @@ -1,10 +0,0 @@ -<% if (typeof isLoading == 'undefined' || !isLoading) { %> - -<% } else { %> - -<% } %> - -
<%- title %>
-<% if (count > 0) { %> -
<%- count %>
-<% } %> \ No newline at end of file diff --git a/scripts/app/templates/special.html b/scripts/app/templates/special.html deleted file mode 100644 index c46bbe87..00000000 --- a/scripts/app/templates/special.html +++ /dev/null @@ -1,5 +0,0 @@ - -
<%- title %>
-<% if (typeof count != 'undefined' && count > 0) { %> -
<%- count %>
-<% } %> \ No newline at end of file diff --git a/scripts/app/views/ContextMenu.js b/scripts/app/views/ContextMenu.js deleted file mode 100644 index 6f0fbb93..00000000 --- a/scripts/app/views/ContextMenu.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * @module App - * @submodule views/ContextMenu - */ -define([ - 'backbone', 'jquery', 'collections/MenuCollection', 'views/MenuItemView', 'controllers/comm' -], -function(BB, $, MenuCollection, MenuItemView, comm) { - - /** - * Context menu view - * @class ContextMenu - * @constructor - * @extends Backbone.View - */ - var ContextMenu = BB.View.extend({ - - /** - * Tag name of content view element - * @property tagName - * @default 'div' - * @type String - */ - tagName: 'div', - - /** - * Class name of content view element - * @property className - * @default 'context-menu' - * @type String - */ - className: 'context-menu', - - /** - * Backbone collection of all context menu items - * @property menuCollection - * @default 'context-menu' - * @type collections/MenuCollection - */ - menuCollection: null, - - /** - * Adds one context menu item - * @method addItem - * @param item {models/MenuItem} New menu item - */ - addItem: function(item) { - var v = new MenuItemView({ - model: item - }); - v.contextMenu = this; - this.$el.append(v.render().$el); - }, - - /** - * Adds multiple context menu items - * @method addItems - * @param items {Array|MenuCollection} List of models to add - */ - addItems: function(items) { - items.forEach(function(item) { - this.addItem(item); - }, this); - }, - - /** - * Renders the context menu (nothing is happening in the render method right now) - * @method render - * @chainable - */ - render: function() { - return this; - }, - - /** - * Hides the context menu - * @method hide - * @triggered when 'hide-overlays' comm message is sent - */ - hide: function() { - if (this.$el.css('display') == 'block') { - this.$el.css('display', 'none'); - } - }, - - /** - * Called when new instance is created - * @method initialize - * @param mc {collections/MenuCollection} Menu collection for this context menu - */ - initialize: function(mc) { - this.el.view = this; - this.menuCollection = new MenuCollection(mc); - this.addItems(this.menuCollection); - $('body').append(this.render().$el); - - this.listenTo(comm, 'hide-overlays', this.hide); - }, - - /** - * Displays the context menu and moves it to given position - * @method show - * @param x {Number} x-coordinate - * @param y {Number} y-coordinate - */ - show: function(x, y) { - if (x + this.$el.width() + 4 > document.body.offsetWidth) { - x = document.body.offsetWidth - this.$el.width() - 8; - } - if (y + this.$el.height() + 4 > document.body.offsetHeight) { - y = document.body.offsetHeight - this.$el.height() - 8; - } - this.$el.css('top', y + 'px'); - this.$el.css('left', x + 'px'); - this.$el.css('display', 'block'); - } - }); - - return ContextMenu; -}); \ No newline at end of file diff --git a/scripts/app/views/FolderView.js b/scripts/app/views/FolderView.js deleted file mode 100644 index f018221b..00000000 --- a/scripts/app/views/FolderView.js +++ /dev/null @@ -1,217 +0,0 @@ -/** - * @module App - * @submodule views/FolderView - */ -define([ - 'backbone', 'jquery', 'underscore', 'views/TopView', 'instances/contextMenus', 'text!templates/folder.html' -], -function(BB, $, _, TopView, contextMenus, tplFolder) { - - /** - * View for Folder in feed list - * @class FolderView - * @constructor - * @extends views/TopView - */ - var FolderView = TopView.extend({ - - /** - * Set CSS classnames - * @property className - * @default 'list-item folder' - * @type String - */ - className: 'list-item folder', - - /** - * Folder view template - * @property template - * @default ./templates/folder.html - * @type Function - */ - template: _.template(tplFolder), - - /** - * Reference to view/feedList instance. It should be replaced with require('views/feedList') - * @property list - * @default null - * @type Backbone.View - */ - list: null, - events: { - 'dblclick': 'handleDoubleClick', - /*'mouseup': 'handleMouseUp', - 'click': 'handleMouseDown',*/ - 'click .folder-arrow': 'handleClickArrow' - }, - - /** - * Opens/closes folder by calling handleClickArrow method - * @method handleDoubleClick - * @triggered on double click on the folder - * @param event {MouseEvent} - */ - handleDoubleClick: function(e) { - this.handleClickArrow(e); - }, - - /** - * Shows context menu for folder - * @method showContextMenu - * @triggered on right mouse click - * @param event {MouseEvent} - */ - showContextMenu: function(e) { - if (!this.$el.hasClass('selected')) { - this.list.select(this, e); - } - contextMenus.get('folder').currentSource = this.model; - contextMenus.get('folder').show(e.clientX, e.clientY); - }, - - /** - * Initializations (*constructor*) - * @method initialize - * @param opt {Object} I don't use it, but it is automatically passed by Backbone - * @param list {Backbone.View} Reference to feedList - */ - initialize: function(opt, list) { - this.list = list; - this.el.view = this; - - this.model.on('destroy', this.handleModelDestroy, this); - this.model.on('change', this.render, this); - this.model.on('change:title', this.handleChangeTitle, this); - bg.sources.on('clear-events', this.handleClearEvents, this); - - this.el.dataset.id = this.model.get('id'); - }, - - /** - * Places folder to its right place after renaming - * @method handleChangeTitle - * @triggered when title of folder is changed - */ - handleChangeTitle: function() { - var folderViews = $('.folder').toArray(); - if (folderViews.length) { - this.list.insertBefore(this.render(), folderViews); - } else if ($('.special:first').length) { - this.render().$el.insertAfter($('.special:first')); - } - - var that = this; - - var feedsInFolder = $('[data-in-folder="' + this.model.get('id') + '"'); - - feedsInFolder.each(function(i, el) { - el.parentNode.removeChild(el); - }); - - feedsInFolder.each(function(i, el) { - that.list.placeSource(el.view); - }); - - }, - - /** - * If the tab is closed, it will remove all events binded to bgprocess - * @method handleClearEvents - * @triggered when bgprocesses triggers clear-events event - * @param id {Number} ID of closed tab - */ - handleClearEvents: function(id) { - if (window == null || id == tabID) { - this.clearEvents(); - } - }, - - /** - * Removes all events binded to bgprocess - * @method clearEvents - */ - clearEvents: function() { - this.model.off('destroy', this.handleModelDestroy, this); - this.model.off('change', this.render, this); - this.model.off('change:title', this.handleChangeTitle, this); - bg.sources.off('clear-events', this.handleClearEvents, this); - }, - - /** - * If the folder model is removed from DB/Backbone then remove it from DOM as well - * @method handleModelDestroy - * @triggered When model is removed from DB/Backbone - * @param id {Number} ID of closed tab - */ - handleModelDestroy: function() { - this.list.destroySource(this); - }, - - /** - * If user clicks on folder arrow then show/hide its content - * @method handleClickArrow - * @triggered Left click on folder arrow - * @param e {MouseEvent} - */ - handleClickArrow: function(e) { - this.model.save('opened', !this.model.get('opened')); - $('.source[data-in-folder=' + this.model.get('id') + ']').toggleClass('invisible', !this.model.get('opened')); - e.stopPropagation(); - }, - - /** - * Reference to requestAnimationFrame frame. It is used to prevent multiple render calls in one frame - * @property renderInterval - * @type String|Number - */ - renderInterval: 'first-time', - - /** - * Sets renderInterval to render folder view - * @method render - */ - render: function() { - if (this.renderInterval == 'first-time') return this.realRender(); - if (this.renderInterval) return this; - - var that = this; - this.renderInterval = requestAnimationFrame(function() { - that.realRender(); - }); - return this; - }, - - /** - * Renders folder view - * @method realRender - */ - realRender: function() { - this.$el.toggleClass('has-unread', !!this.model.get('count')); - - var data = Object.create(this.model.attributes); - this.$el.toggleClass('opened', this.model.get('opened')); - this.$el.html(this.template(data)); - - this.setTitle(this.model.get('count'), this.model.get('countAll')); - - this.renderInterval = null; - - return this; - }, - - /** - * Data to send to middle column (list of articles) when folder is selected - * @method render - * @param e {MouseEvent} - */ - getSelectData: function(e) { - return { - action: 'new-folder-select', - value: this.model.id, - unreadOnly: !!e.altKey - }; - } - }); - - return FolderView; -}); \ No newline at end of file diff --git a/scripts/app/views/GroupView.js b/scripts/app/views/GroupView.js deleted file mode 100644 index ee43175a..00000000 --- a/scripts/app/views/GroupView.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @module App - * @submodule views/GroupView - */ -define(['backbone'], function(BB) { - - /** - * View for Date Groups in list of articles - * @class GroupView - * @constructor - * @extends Backbone.View - */ - var GroupView = BB.View.extend({ - - /** - * Tag name of date group element - * @property tagName - * @default 'div' - * @type String - */ - tagName: 'div', - - /** - * Class name of date group element - * @property className - * @default 'date-group' - * @type String - */ - className: 'date-group', - - /** - * Initializations (*constructor*) - * @method initialize - * @param model {models/Group} Date group model - * @param groups {Backbone.View} Reference to collection of groups - */ - initialize: function(model, groups) { - this.el.view = this; - this.listenTo(groups, 'reset', this.handleReset); - this.listenTo(groups, 'remove', this.handleRemove); - }, - - /** - * Renders date group view - * @method render - */ - render: function() { - this.$el.html(this.model.get('title')); - return this; - }, - - /** - * If date group model is removed from collection of groups remove the DOM object - * @method handleRemove - * @triggered when any date group is removed from list of groups - * @param model {models/Group} Model removed from list of groups - */ - handleRemove: function(model) { - if (model == this.model) { - this.handleReset(); - } - }, - - /** - * If the reset model (that removes all models from collection) is called, removed DOM object of this date group - * and stop listening to any events of group collection. - * @method handleRemove - * @triggered when on reset - * @param model {models/Group} Model removed from list of groups - */ - handleReset: function() { - this.stopListening(); - this.$el.remove(); - } - }); - - return GroupView; -}); \ No newline at end of file diff --git a/scripts/app/views/IndicatorView.js b/scripts/app/views/IndicatorView.js deleted file mode 100644 index f52c8149..00000000 --- a/scripts/app/views/IndicatorView.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @module App - * @submodule views/IndicatorView - */ -define(['backbone', 'modules/Locale', 'text!templates/indicator.html'], function(BB, Locale, tplIndicator) { - - /** - * Feeds update indicator view - * @class IndicatorView - * @constructor - * @extends Backbone.View - */ - var IndicatorView = BB.View.extend({ - /** - * Indicator element id - * @property id - * @default indicator - */ - id: 'indicator', - - /** - * Article item view template - * @property template - * @default ./templates/item.html - * @type Function - */ - template: _.template(tplIndicator), - - events: { - 'click #indicator-stop': 'handleButtonStop' - }, - - /** - * @method initialize - */ - initialize: function() { - this.$el.html(this.template()); - bg.loader.on('change:loading', this.handleLoadingChange, this); - bg.loader.on('change:loaded', this.render, this); - bg.loader.on('change:maxSources', this.render, this); - bg.sources.on('clear-events', this.handleClearEvents, this); - - this.handleLoadingChange(); - }, - - /** - * Clears bg events it listens to - * @method handleClearEvents - * @param id {Integer} ID of the closed tab - */ - handleClearEvents: function(id) { - if (window == null || id == tabID) { - bg.loader.off('change:loading', this.handleLoadingChange, this); - bg.loader.off('change:loaded', this.render, this); - bg.loader.off('change:maxSources', this.render, this); - bg.sources.off('clear-events', this.handleClearEvents, this); - } - }, - - /** - * Stops updating feeds - * @method handleButtonStop - * @triggered when user clicks on stop button - */ - handleButtonStop: function() { - app.actions.execute('feeds:stopUpdate'); - }, - - /** - * Hides/shows indicator according to loading flag - * @method handleLoadingChange - */ - handleLoadingChange: function() { - var that = this; - if (bg.loader.get('loading') == true) { - this.render(); - this.$el.addClass('indicator-visible'); - } else { - setTimeout(function() { - that.$el.removeClass('indicator-visible'); - }, 500); - } - }, - - /** - * Renders the indicator (gradient/text) - * @method render - * @chainable - */ - render: function() { - var l = bg.loader; - if (l.get('maxSources') == 0) return; - var perc = Math.round(l.get('loaded') * 100 / l.get('maxSources')); - this.$el.find('#indicator-progress').css('background', 'linear-gradient(to right, #c5c5c5 ' + perc + '%, #eee ' + perc + '%)'); - this.$el.find('#indicator-progress').html(Locale.c.UPDATING_FEEDS + ' (' + l.get('loaded') + '/' + l.get('maxSources') + ')'); - return this; - } - }); - - return IndicatorView; -}); \ No newline at end of file diff --git a/scripts/app/views/ItemView.js b/scripts/app/views/ItemView.js deleted file mode 100644 index 01ed2eca..00000000 --- a/scripts/app/views/ItemView.js +++ /dev/null @@ -1,269 +0,0 @@ -/** - * @module App - * @submodule views/ItemView - */ -define([ - 'backbone', 'jquery', 'underscore', 'helpers/formatDate', 'instances/contextMenus', 'helpers/stripTags', 'text!templates/item.html' -], function(BB, $, _, formatDate, contextMenus, stripTags, tplItem) { - - /** - * View of one article item in article list - * @class ItemView - * @constructor - * @extends Backbone.View - */ - var ItemView = BB.View.extend({ - - /** - * Tag name of article item element - * @property tagName - * @default 'div' - * @type String - */ - tagName: 'div', - - /** - * Class name of article item element - * @property className - * @default 'item' - * @type String - */ - className: 'item', - - /** - * Article item view template - * @property template - * @default ./templates/item.html - * @type Function - */ - template: _.template(tplItem), - - /** - * Reference to view/articleList instance. It should be replaced with require('views/articleList') - * @property list - * @default null - * @type Backbone.View - */ - list: null, - - /** - * Initializations (*constructor*) - * @method initialize - * @param opt {Object} I don't use it, but it is automatically passed by Backbone - * @param list {Backbone.View} Reference to articleList - */ - initialize: function(opt, list) { - this.list = list; - this.el.setAttribute('draggable', 'true'); - this.el.view = this; - this.setEvents(); - }, - - /** - * Set events that are binded to bgprocess - * @method setEvents - */ - setEvents: function() { - this.model.on('change', this.handleModelChange, this); - this.model.on('destroy', this.handleModelDestroy, this); - bg.sources.on('clear-events', this.handleClearEvents, this); - }, - - /** - * Swaps models of view. - * It reuses already created DOM when changing selected feeds/folders. - * @method swapModel - * @param newModel {Item} Item model to be used - */ - swapModel: function(newModel) { - if (this.model == newModel) { - this.prerender(); - return; - } - if (this.model) { - this.clearEvents(); - } - this.model = newModel; - this.setEvents(); - this.prerender(); - }, - - /** - * Indiciates whether the item was prerendered (true) or already fully-rendered (false). - * When prerendered, only the classNames are set without any text content. - * Prerendering is used for not-visible items in the list. - * @property prerendered - * @default false - * @type Boolean - */ - prerendered: false, - - /** - * Prerenders view. (More info on prerenderer property). - * @method prerender - */ - prerender: function() { - this.prerendered = true; - this.list.viewsToRender.push(this); - this.el.className = this.model.get('unread') ? 'item unread' : 'item'; - }, - - /** - * Removes item content without removing the actuall DOM and Backbone view. - * When changing selected feed with _m_ items to another feed with _n_ items where n= parseInt(formatDate(Date.now(), 'T') / 86400000, 10)) { - date = formatDate(new Date(date), timeFormat); - } else if ((new Date(date)).getFullYear() == (new Date()).getFullYear() ) { - date = formatDate(new Date(date), pickedFormat.replace(/\/?YYYY(?!-)/, '')); - } else { - date = formatDate(new Date(date), pickedFormat); - } - } - - return date; - }, - - /** - * Shows context menu on right click - * @method handleMouseUp - * @triggered on mouse up + condition for right click only - * @param event {MouseEvent} - */ - handleMouseUp: function(e) { - if (e.which == 3) { - this.showContextMenu(e); - } - }, - - /** - * Shows context menu for article item - * @method showContextMenu - * @param event {MouseEvent} - */ - showContextMenu: function(e) { - if (!this.$el.hasClass('selected')) { - this.list.select(this, e); - } - contextMenus.get('items').currentSource = this.model; - contextMenus.get('items').show(e.clientX, e.clientY); - }, - - /** - * When model is changed rerender it or remove it from DOM (depending on what is changed) - * @method handleModelChange - * @triggered when model is changed - */ - handleModelChange: function() { - if (this.model.get('deleted') || (this.list.currentData.name != 'trash' && this.model.get('trashed')) ) { - this.list.destroyItem(this); - } else { - this.render(); - } - }, - - /** - * When model is removed from DB/Backbone remove it from DOM as well - * @method handleModelDestroy - * @triggered when model is destroyed - */ - handleModelDestroy: function() { - this.list.destroyItem(this); - }, - - /** - * Changes pin state (true/false) - * @method when user clicked on pin button in article item - * @triggered when model is destroyed - */ - handleClickPin: function(e) { - e.stopPropagation(); - this.model.save({ pinned: !this.model.get('pinned') }); - } - }); - - return ItemView; -}); \ No newline at end of file diff --git a/scripts/app/views/LogView.js b/scripts/app/views/LogView.js deleted file mode 100644 index 764d73a0..00000000 --- a/scripts/app/views/LogView.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @module App - * @submodule views/LogView - */ -define([ - 'backbone', 'underscore', 'jquery', 'helpers/formatDate', 'text!templates/log.html' -], function(BB, _, $, formatDate, tplLog) { - - /** - * View in bottom right corner used for bgprocess error logs and integration tests - * @class LogView - * @constructor - * @extends Backbone.View - */ - var LogView = BB.View.extend({ - - /** - * Tag name of the view - * @property tagName - * @default 'footer' - * @type String - */ - tagName: 'footer', - events: { - 'click #button-hide-log': 'hide' - }, - - template: _.template(tplLog), - - /** - * Initializations of events and template. - * Underscore template function has to be created in constructor as the HTML is not yet avalable on Protoype creation. - * @method initialize - */ - initialize: function() { - this.$el.html(this.template({})); - - bg.logs.on('add', this.addItem, this); - bg.sources.on('clear-events', this.handleClearEvents, this); - }, - - /** - * If the tab is closed, it will remove all events binded to bgprocess - * @method handleClearEvents - * @triggered when bgprocesses triggers clear-events event - * @param id {Number} ID of closed tab - */ - handleClearEvents: function(id) { - if (window == null || id == tabID) { - bg.logs.off('add', this.addItem, this); - bg.sources.off('clear-events', this.handleClearEvents, this); - } - }, - - /** - * Adds DOM element for the newly added model - * @method addItem - * @triggered when new model is added to log collection - * @param model {Backbone.Model} - */ - addItem: function(model) { - this.show(); - $('
' + formatDate(new Date, 'hh:mm:ss') + ': ' + model.get('message') + '
').insertAfter(this.$el.find('#button-hide-log')); - }, - - /** - * Show the Log view element (display: block) - * @method show - */ - show: function() { - this.$el.css('display', 'block'); - }, - - /** - * Hide the Log view element (display: none) - * @method hide - */ - hide: function() { - this.$el.css('display', 'none'); - } - }); - - return LogView; -}); \ No newline at end of file diff --git a/scripts/app/views/MenuItemView.js b/scripts/app/views/MenuItemView.js deleted file mode 100644 index 415e1c50..00000000 --- a/scripts/app/views/MenuItemView.js +++ /dev/null @@ -1,31 +0,0 @@ -define(['backbone'], function(BB) { - var MenuItemView = BB.View.extend({ - tagName: 'div', - className: 'context-menu-item', - contextMenu: null, - events: { - 'click': 'handleClick' - }, - initialize: function() { - if (this.model.id) { - this.el.id = this.model.id; - } - }, - render: function() { - if (this.model.get('icon')) { - this.$el.css('background', 'url(/images/' + this.model.get('icon') + ') no-repeat left center'); - } - this.$el.html(this.model.get('title')); - return this; - }, - handleClick: function(e) { - var action = this.model.get('action'); - if (action && typeof action == 'function') { - action(e, app.feeds.feedList); - this.contextMenu.hide(); - } - } - }); - - return MenuItemView; -}); \ No newline at end of file diff --git a/scripts/app/views/OverlayView.js b/scripts/app/views/OverlayView.js deleted file mode 100644 index bc6c916d..00000000 --- a/scripts/app/views/OverlayView.js +++ /dev/null @@ -1,59 +0,0 @@ -define([ - 'backbone', 'underscore', 'jquery', 'text!templates/overlay.html', 'modules/Locale' -], -function(BB, _, $, tplOverlay, Locale) { - var OverlayView = BB.View.extend({ - tagName: 'div', - className: 'overlay', - template: _.template(Locale.translateHTML(tplOverlay)), - events: { - 'click #config-layout input[type=image]': 'handleLayoutChange', - 'change select': 'handleSelectChange', - }, - initialize: function() { - - window.addEventListener('blur', this.hide.bind(this)); - window.addEventListener('resize', this.hide.bind(this)); - }, - render: function() { - this.$el.html(this.template({})); - var layout = bg.settings.get('layout'); - if (layout == 'vertical') { - $('#config-layout input[value=horizontal]').attr('src', '/images/layout_horizontal.png'); - $('#config-layout input[value=vertical]').attr('src', '/images/layout_vertical_selected.png'); - } else { - $('#config-layout input[value=horizontal]').attr('src', '/images/layout_horizontal_selected.png'); - $('#config-layout input[value=vertical]').attr('src', '/images/layout_vertical.png'); - } - this.$el.find('#config-lines').val(bg.settings.get('lines')); - this.$el.find('#config-sort-order').val(bg.settings.get('sortOrder')); - this.$el.find('#config-sort-order2').val(bg.settings.get('sortOrder2')); - this.$el.find('#config-sort-by').val(bg.settings.get('sortBy')); - this.$el.find('#config-sort-by2').val(bg.settings.get('sortBy2')); - return this; - }, - handleSelectChange: function(e) { - bg.settings.save(e.currentTarget.dataset.name, e.currentTarget.value); - }, - handleLayoutChange: function(e) { - var layout = e.currentTarget.value; - bg.settings.save('layout', layout); - this.hide(); - }, - hide: function() { - this.$el.css('display', 'none'); - }, - show: function() { - var config = $('[data-action="content:showConfig"]'); - if (config.length) { - this.el.style.top = config.offset().top + config.height() + 5 + 'px'; - } - this.render().$el.css('display', 'block'); - }, - isVisible: function() { - return this.$el.css('display') == 'block'; - } - }); - -return OverlayView; -}); \ No newline at end of file diff --git a/scripts/app/views/Properties.js b/scripts/app/views/Properties.js deleted file mode 100644 index 5b48b559..00000000 --- a/scripts/app/views/Properties.js +++ /dev/null @@ -1,161 +0,0 @@ -define([ - 'backbone', 'jquery', 'underscore', 'text!templates/properties.html', 'modules/Locale' -], -function(BB, $, _, tplProperties, Locale) { - - var Properties = BB.View.extend({ - id: 'properties', - current: null, - template: _.template(Locale.translateHTML(tplProperties)), - events: { - 'click button' : 'handleClick', - 'keydown button' : 'handleKeyDown', - 'click #advanced-switch' : 'handleSwitchClick', - }, - handleClick: function(e) { - var t = e.currentTarget; - if (t.id == 'prop-cancel') { - this.hide(); - } else if (t.id == 'prop-ok') { - this.saveData(); - } - }, - saveData: function() { - if (!this.current) { - this.hide(); - return; - } - - var updateEvery, autoremove; - - if (this.current instanceof bg.Source) { - /* encrypt the password */ - this.current.setPass($('#prop-password').val()); - - this.current.save({ - title: $('#prop-title').val(), - url: app.fixURL($('#prop-url').val()), - username: $('#prop-username').val(), - updateEvery: parseFloat($('#prop-update-every').val()), - autoremove: parseFloat($('#prop-autoremove').val(), 10), - }); - } else if (this.current instanceof bg.Folder) { - this.current.save({ - title: $('#prop-title').val() - }); - - var sourcesInFolder = bg.sources.where({ folderID: this.current.id }); - - updateEvery = parseFloat($('#prop-update-every').val()); - if (updateEvery >= 0) { - sourcesInFolder.forEach(function(source) { - source.save({ updateEvery: updateEvery }); - }); - } - - autoremove = parseFloat($('#prop-autoremove').val()); - if (autoremove >= 0) { - sourcesInFolder.forEach(function(source) { - source.save({ autoremove: autoremove }); - }); - } - } else if (Array.isArray(this.current)) { - updateEvery = parseFloat($('#prop-update-every').val()); - if (updateEvery >= 0) { - this.current.forEach(function(source) { - source.save({ updateEvery: updateEvery }); - }); - } - - autoremove = parseFloat($('#prop-autoremove').val()); - if (autoremove >= 0) { - this.current.forEach(function(source) { - source.save({ autoremove: autoremove }); - }); - } - } - - this.hide(); - - }, - handleKeyDown: function(e) { - if (e.keyCode == 13) { - this.handleClick(e); - } - }, - render: function() { - if (!this.current) return; - - if (this.current instanceof bg.Source) { - /* decrypt password */ - var attrs = this.current.toJSON(); - attrs.password = this.current.getPass(); - - this.$el.html(this.template(attrs)); - - if (this.current.get('updateEvery')) { - $('#prop-update-every').val(this.current.get('updateEvery')); - } - - if (this.current.get('autoremove')) { - $('#prop-autoremove').val(this.current.get('autoremove')); - } - } else { - var isFolder = this.current instanceof bg.Folder; - var listOfSources = isFolder ? bg.sources.where({ folderID: this.current.id }) : this.current; - - var params = { updateEveryDiffers: 0, autoremoveDiffers: 0, firstUpdate: 0, firstAutoremove: 0 }; - - /** - * Test if all selected feeds has the same properteies or if tehy are mixed - */ - - if (listOfSources.length) { - params.firstUpdate = listOfSources[0].get('updateEvery'); - params.updateEveryDiffers = listOfSources.some(function(c) { - if (params.firstUpdate != c.get('updateEvery')) return true; - }); - - params.firstAutoremove = listOfSources[0].get('autoremove'); - params.autoremoveDiffers = listOfSources.some(function(c) { - if (params.firstAutoremove != c.get('autoremove')) return true; - }); - } - - /** - * Create HTML - */ - - if (isFolder) { - this.$el.html(this.template( _.extend(params, this.current.attributes) )); - } else { - this.$el.html(this.template( params )); - } - - /** - * Set '); - this.$el.replaceWith(newEl); - this.$el = newEl; - this.$el.attr('placeholder', Locale.c.SEARCH); - this.$el.attr('tabindex', -1); - this.el = this.$el.get(0); - - this.el.dataset.action = this.model.get('actionName'); - this.el.title = action.get('title'); - - - this.el.setAttribute('draggable', 'true'); - - this.el.view = this; - } - }); - - return ToolbarSearchView; -}); \ No newline at end of file diff --git a/scripts/app/views/ToolbarView.js b/scripts/app/views/ToolbarView.js deleted file mode 100644 index c4cbb024..00000000 --- a/scripts/app/views/ToolbarView.js +++ /dev/null @@ -1,201 +0,0 @@ -define([ - 'backbone', 'collections/ToolbarItems', 'jquery', 'factories/ToolbarItemsFactory', 'underscore' -], -function (BB, ToolbarItems, $, ToolbarItemsFactory, _) { - var ToolbarView = BB.View.extend({ - tagName: 'div', - className: 'toolbar', - items: null, - doNotRegenerate: false, - events: { - 'click .button': 'handleButtonClick', - 'input input[type=search]': 'handleButtonClick', - /**** replace with "drop" to implement dnd between toolbars ****/ - 'dragend': 'handleDragEnd', - 'dragover': 'handleDragOver' - }, - initialize: function() { - this.el.view = this; - this.items = new ToolbarItems(); - - this.listenTo(this.items, 'add', this.addToolbarItem); - this.listenTo(this.model, 'change', this.handleChange); - bg.sources.on('clear-events', this.handleClearEvents, this); - - - this.model.get('actions').forEach(this.createToolbarItem, this); - this.hideItems('articles:undelete'); - }, - - /** - * If the tab is closed, it will remove all events binded to bgprocess - * @method handleClearEvents - * @triggered when bgprocesses triggers clear-events event - * @param id {Number} ID of closed tab - */ - handleClearEvents: function(id) { - if (window == null || id == tabID) { - this.stopListening(); - bg.sources.off('clear-events', this.handleClearEvents, this); - } - }, - - /** - * Regenerates DOM of items according to new chnages - * @triggered when some change to Toolbar model happens - * @method handleChange - */ - handleChange: function() { - if (!this.doNotRegenerate) { - this.$el.find('> *').remove(); - this.items.reset(); - - this.model.get('actions').forEach(this.createToolbarItem, this); - - if (app.articles.articleList.currentData.name == 'trash') { - this.hideItems('articles:update'); - } else { - this.hideItems('articles:undelete'); - } - } - }, - /** - * List of hidden toolbar items. The reason for storing hidden items is so that we can show all hiden items when customizing ui. - * @param hiddenItems - * @type Array - */ - hiddenItems: [], - - /** - * Hides items from toolbar (e.g. update action while in trash) - * @method hideItems - * @chainable - */ - hideItems: function(action) { - var list = this.$el.find('> [data-action="' + action + '"]').hide().toArray(); - this.hiddenItems = _.uniq(this.hiddenItems.concat(list)); - - return this; - }, - - /** - * Shows again hidden items from toolbar (e.g. update action while going away from trash) - * @method showItems - * @chainable - */ - showItems: function(action) { - this.hiddenItems = this.hiddenItems.filter(function(item) { - if (item.dataset.action == action) { - $(item).show(); - return false; - } - return true; - }); - - return this; - }, - - - - /** - * Called for every Toolbar action when ToolbarView is initalized - * @method createToolbarItem - * @param action {String} Name of the action - */ - createToolbarItem: function(action) { - if (action == '!dynamicSpace') { - this.items.add({ type: 'dynamicSpace' }); - return null; - } - this.items.add({ actionName: action, type: 'button' }); - }, - handleButtonClick: function(e) { - var button = e.currentTarget.view.model; - app.actions.execute(button.get('actionName'), e); - }, - render: function() { - return this; - }, - - /** - * Called when new model was added to _items_ collection - * @method addToolbarItem - * @param toolbarItem {ToolbarButton} Model added to the collection - */ - addToolbarItem: function(toolbarItem) { - var view; - if (toolbarItem.get('actionName') == 'articles:search') { - view = ToolbarItemsFactory.create('search', toolbarItem); - } else if (toolbarItem.get('type') != 'dynamicSpace') { - view = ToolbarItemsFactory.create('button', toolbarItem); - } else { - view = ToolbarItemsFactory.create('dynamicSpace', toolbarItem); - } - - this.$el.append(view.render().el); - toolbarItem.view = view; - }, - - handleDragEnd: function(e) { - e.stopPropagation(); - var t = e.originalEvent.target; - - // toolbarItems are sorted by left position - this.items.sort(); - var moved = this.items.some(function(item) { - var r = item.view.el.getBoundingClientRect(); - - // if the toolbarItem is hidden (e.g. undelete button) - if (r.left == 0) return false; - - if (r.left + r.width/2 > e.originalEvent.clientX) { - if (item.view.el == t) return true; - - $(t).insertBefore(item.view.$el); - return true; - } - }); - - if (!moved) { - this.$el.append(t); - } - this.items.sort(); - this.saveToDB(); - - this.hiddenItems.forEach(function(item) { - $(item).hide(); - }); - }, - saveToDB: function() { - this.doNotRegenerate = true; - var list = this.items.pluck('actionName'); - list = list.map(function(action, i) { - if (action == 'global:default') { - if (this.items.at(i).get('type') == 'dynamicSpace') { - return '!dynamicSpace'; - } - } - return action; - }, this); - - this.model.set('actions', list); - this.model.save(); - this.doNotRegenerate = false; - }, - - /** - * Shows all hidden items during drag - * @triggered when user drags item over to the toolbar (which happens immidiatelly) - * @method handleDragStart - */ - handleDragOver: function(e) { - e.preventDefault(); - e.stopPropagation(); - this.hiddenItems.forEach(function(item) { - $(item).show(); - }); - } - }); - - return ToolbarView; -}); \ No newline at end of file diff --git a/scripts/app/views/TopView.js b/scripts/app/views/TopView.js deleted file mode 100644 index 8713a120..00000000 --- a/scripts/app/views/TopView.js +++ /dev/null @@ -1,30 +0,0 @@ -define([ - 'backbone', 'jquery', 'underscore', 'modules/Locale', 'text!templates/source.html', 'views/feedList' -], function(BB, $, _, Locale, tplSource) { - var TopView = BB.View.extend({ - tagName: 'div', - className: 'list-item', - template: _.template(tplSource), - handleMouseUp: function(e) { - if (e.which == 3) { - this.showContextMenu(e); - } - }, - getSelectData: function(e) { - return { - action: 'new-select', - // _.extend is important, because otherwise it would be sent by reference - value: this.model.id || _.extend({}, this.model.get('filter')), - name: this.model.get('name'), - unreadOnly: !!e.altKey - }; - }, - setTitle: function(unread, total) { - this.$el.attr('title', - this.model.get('title') + ' (' + unread + ' ' + Locale.c.UNREAD + ', ' + total + ' ' + Locale.c.TOTAL + ')' - ); - } - }); - - return TopView; -}); \ No newline at end of file diff --git a/scripts/app/views/articleList.js b/scripts/app/views/articleList.js deleted file mode 100644 index b023b694..00000000 --- a/scripts/app/views/articleList.js +++ /dev/null @@ -1,821 +0,0 @@ -/** - * @module App - * @submodule views/articleList - */ -define([ - 'backbone', 'underscore', 'jquery', 'collections/Groups', 'models/Group', 'views/GroupView', - 'views/ItemView', 'mixins/selectable', 'modules/Locale' -], -function (BB, _, $, Groups, Group, GroupView, ItemView, selectable, Locale) { - - function isScrolledIntoView(elem) { - if (!screen) { - bg.sources.trigger('clear-events', -1); - return false; - } - - var docViewTop = 0; - var docViewBottom = screen.height; - - var rect = elem.getBoundingClientRect(); - var elemTop = rect.top; - var elemBottom = elemTop + rect.height; - - return (elemBottom >= docViewTop) && (elemTop <= docViewBottom); - /* && (elemBottom <= docViewBottom) && (elemTop >= docViewTop) ;*/ - } - - var groups = new Groups(); - - - /** - * List of articles - * @class ArticleListView - * @constructor - * @extends Backbone.View - */ - var ArticleListView = BB.View.extend({ - /** - * Height of one article item (changes with layout and rems) - * @property _itemHeight - * @default 0 - * @type Number - */ - _itemHeight: 0, - - /** - * Tag name of article list element - * @property tagName - * @default 'div' - * @type String - */ - tagName: 'div', - - /** - * ID of article list - * @property id - * @default 'article-list' - * @type String - */ - id: 'article-list', - - /** - * Class of article views - * @property itemClass - * @default 'item' - * @type string - */ - itemClass: 'item', - - /** - * Unordered list of all article views - * @property views - * @default [] - * @type Array - */ - views: [], - - /** - * Order list of yet rendered article views - * @property viewsToRender - * @default [] - * @type Array - */ - viewsToRender: [], - - - /** - * Data received from feedList about current selection (feed ids, name of special, filter, unreadOnly) - * @property currentData - * @default { feeds: [], name: 'all-feeds', filter: { trashed: false}, unreadOnly: false } - * @type Object - */ - currentData: { - feeds: [], - name: 'all-feeds', - filter: { trashed: false }, - unreadOnly: false - }, - - /** - * Flag to prevent focusing more items in one tick - * @property noFocus - * @default false - * @type Boolean - */ - noFocus: false, - - /** - * All article views - unattached views - * @property reuseIndex - * @default 0 - * @type Integer - */ - reuseIndex: 0, - - events: { - 'dragstart .item': 'handleDragStart', - 'mousedown .item': 'handleMouseDown', - 'mouseup .item': 'handleMouseUp', - 'dblclick .item': 'handleItemDblClick', - 'mousedown .item-pin,.item-pinned': 'handleClickPin', - }, - - /** - * Opens articles url in new tab - * @method handleItemDblClick - * @triggered on double click on article - */ - handleItemDblClick: function() { - app.actions.execute('articles:oneFullArticle'); - }, - - /** - * Selects article - * @method handleMouseDown - * @triggered on mouse down on article - * @param event {MouseEvent} - */ - handleMouseDown: function(e) { - this.handleSelectableMouseDown(e); - }, - - /** - * Changes pin state - * @method handleClickPin - * @triggered on click on pin button - * @param event {MouseEvent} - */ - handleClickPin: function(e) { - e.currentTarget.parentNode.view.handleClickPin(e); - }, - - /** - * Calls neccesary slect methods - * @method handleMouseUp - * @triggered on mouse up on article - * @param event {MouseEvent} - */ - handleMouseUp: function(e) { - e.currentTarget.view.handleMouseUp(e); - this.handleSelectableMouseUp(e); - }, - - /** - * Called when new instance is created - * @method initialize - */ - initialize: function() { - - this.$el.addClass('lines-' + bg.settings.get('lines')); - bg.items.on('reset', this.addItems, this); - bg.items.on('add', this.addItem, this); - bg.items.on('sort', this.handleSort, this); - bg.items.on('render-screen', this.handleRenderScreen, this); - bg.settings.on('change:lines', this.handleChangeLines, this); - bg.settings.on('change:layout', this.handleChangeLayout, this); - bg.sources.on('destroy', this.handleSourcesDestroy, this); - bg.sources.on('clear-events', this.handleClearEvents, this); - - groups.on('add', this.addGroup, this); - - this.on('attach', this.handleAttached, this); - this.on('pick', this.handlePick, this); - - - this.$el.on('scroll', this.handleScroll.bind(this)); - - }, - - /** - * Sends msg to show selected article - * @method handlePick - * @triggered when one article is selected - * @param view {views/ItemView} - */ - handlePick: function(view) { - if (!view.model.collection) { - // This shouldn't usually happen - // It might happen when source is deleted and created in the same tick - return; - } - - app.trigger('select:' + this.el.id, { action: 'new-select', value: view.model.id }); - - if (view.model.get('unread') && bg.settings.get('readOnVisit')) { - view.model.save({ - visited: true, - unread: false - }); - } else if (!view.model.get('visited')) { - view.model.save('visited', true); - } - }, - - /** - * Sets comm event listeners - * @method handleAttached - * @triggered when article list is attached to DOM - */ - handleAttached: function() { - - app.on('select:feed-list', function(data) { - this.el.scrollTop = 0; - this.unreadOnly = data.unreadOnly; - - if (data.action == 'new-select') { - this.handleNewSelected(data); - } - }, this); - - app.on('give-me-next', function() { - if (this.selectedItems[0] && this.selectedItems[0].model.get('unread') == true) { - this.selectedItems[0].model.save({ unread: false }); - } - this.selectNext({ selectUnread: true }); - }, this); - - if (bg.sourceToFocus) { - setTimeout(function() { - app.trigger('focus-feed', bg.sourceToFocus); - bg.sourceToFocus = null; - }, 0); - } else { - this.loadAllFeeds(); - } - }, - - /** - * Loads all untrashed feeds - * @method loadAllFeeds - * @chainable - */ - loadAllFeeds: function() { - var that = this; - setTimeout(function() { - app.trigger('select-all-feeds'); - - var unread = bg.items.where({ trashed: false, unread: true }); - if (unread.length) { - that.addItems(unread); - } else { - that.addItems(bg.items.where({ trashed: false })); - } - }, 0); - - return this; - }, - - /** - * Renders unrendered articles in view by calling handleScroll - * @method handleRenderScreen - * @triggered when new items arr added or when source is destroyed - */ - handleRenderScreen: function() { - this.redraw(); - if ($('input[type=search]').val()) { - app.actions.execute('articles:search'); - } - }, - - /** - * Calls redraw when user scrolls in list - * @method handleScroll - * @triggered when list is scrolled (and is called from many other places) - */ - handleScroll: function() { - this.redraw(); - }, - - /** - * Renders unrendered articles in view - * @method redraw - */ - redraw: function() { - var start = -1; - var count = 0; - for (var i=0,j=this.viewsToRender.length; i= 0 && count % 10 != 0) || isScrolledIntoView(this.viewsToRender[i].el)) { - this.viewsToRender[i].render(); - count++; - if (start == -1) start = i; - } else if (start >= 0) { - break; - } - } - - - if (start >= 0 && count > 0) { - this.viewsToRender.splice(start, count); - } - }, - - /** - * Unbinds all listeners to bg process - * @method handleClearEvents - * @triggered when tab is closed/refershed - * @param id {Integer} id of the closed tab - */ - handleClearEvents: function(id) { - if (window == null || id == tabID) { - bg.items.off('reset', this.addItems, this); - bg.items.off('add', this.addItem, this); - bg.items.off('sort', this.handleSort, this); - bg.items.off('render-screen', this.handleRenderScreen, this); - bg.settings.off('change:lines', this.handleChangeLines, this); - bg.settings.off('change:layout', this.handleChangeLayout, this); - - bg.sources.off('destroy', this.handleSourcesDestroy, this); - - bg.sources.off('clear-events', this.handleClearEvents, this); - } - }, - - /** - * Sets new article item height and rerenders list - * @method handleChangeLayout - * @triggered when layout setting is changed - */ - handleChangeLayout: function() { - var that = this; - requestAnimationFrame(function() { - that.setItemHeight(); - that.handleScroll(); - }); - }, - - /** - * Clears searchbox and sorts the list - * @method handleSort - * @triggered when sort setting is changed - */ - handleSort: function() { - $('#input-search').val(''); - - this.handleNewSelected(this.currentData); - - }, - - /** - * Adds or removes neccesary one-line/twoline classes for given lines settings - * @method handleChangeLines - * @triggered when lines setting is changed - * @param settings {Settings} bg.Settings - */ - handleChangeLines: function(settings) { - this.$el.removeClass('lines-auto'); - this.$el.removeClass('lines-one-line'); - this.$el.removeClass('lines-two-lines'); - // this.$el.removeClass('lines-' + settings.previous('lines')); // does not work for some reason - this.$el.addClass('lines-' + settings.get('lines')); - }, - - /** - * Stores ids of dragged items - * @method handleDragStart - * @triggered on drag start - * @param event {DragEvent} - */ - handleDragStart: function(e) { - var ids = this.selectedItems.map(function(view) { - return view.model.id; - }); - - e.originalEvent.dataTransfer.setData('text/plain', JSON.stringify(ids)); - }, - - /** - * Selects new item when the last selected is deleted - * @method selectAfterDelete - * @param view {views/ItemView} - */ - selectAfterDelete: function(view) { - if (view == this.selectedItems[0]) { - var last = this.$el.find('.item:not(.invisible):last').get(0); - if (last && view == last.view) { - this.selectPrev({ currentIsRemoved: true }); - } else { - this.selectNext({ currentIsRemoved: true }); - } - } else { - // if first item is the last item to be deleted, selecting it will trigger error - rAF to get around it - requestAnimationFrame(function() { - this.selectFirst(); - }.bind(this)); - } - }, - - /** - * Tests whether newly fetched item should be added to current list. - * (If the item's feed is selected) - * @method inCurrentData - * @return Boolean - * @param item {Item} bg.Item - */ - inCurrentData: function(item) { - var f = this.currentData.feeds; - if (!f.length) { - if (!this.currentData.filter) { - return true; - } else if (item.query(this.currentData.filter)) { - return true; - } - } else if (f.indexOf(item.get('sourceID')) >= 0) { - return true; - } - - return false; - }, - - /** - * Adds new article item to the list - * @method addItem - * @param item {Item} bg.Item - * @param noManualSort {Boolean} true when adding items in a batch in right order - */ - addItem: function(item, noManualSort) { - - //Don't add newly fetched items to middle column, when they shouldn't be - if (noManualSort !== true && !this.inCurrentData(item)) { - return false; - } - - - - var after = null; - if (noManualSort !== true) { - $.makeArray($('#article-list .item, #article-list .date-group')).some(function(itemEl) { - if (bg.items.comparator(itemEl.view.model, item) === 1) { - after = itemEl; - return true; - } - }); - } - - var view; - - if (!after) { - if (this.reuseIndex >= this.views.length) { - view = new ItemView({ model: item }, this); - if (!this._itemHeight) { - view.render(); - } else { - view.$el.css('height', this._itemHeight + 'px'); - view.prerender(); - } - this.$el.append(view.$el); - this.views.push(view); - } else { - view = this.views[this.reuseIndex]; - view.swapModel(item); - } - - if (!this.selectedItems.length) this.select(view); - } else { - view = new ItemView({ model: item }, this); - view.render().$el.insertBefore($(after)); - - // weee, this is definitelly not working 100% right :D or is it? - var indexElement = after.view instanceof ItemView ? after : after.nextElementSibling; - var index = indexElement ? this.views.indexOf(indexElement.view) : -1; - if (index == -1) index = this.reuseIndex; - - this.views.splice(index, 0, view); - } - - if (!this._itemHeight) { - this._itemHeight = view.el.getBoundingClientRect().height; - } - - - if (!bg.settings.get('disableDateGroups') && bg.settings.get('sortBy') == 'date') { - var group = Group.getGroup(item.get('date')); - if (!groups.findWhere({ title: group.title })) { - groups.add(new Group(group), { before: view.el }); - } - } - - this.reuseIndex++; - }, - - /** - * Adds new date group to the list - * @method addGroup - * @param model {models/Group} group create by groups.create - * @param col {collections/Groups} - * @param opt {Object} options { before: insertBeforeItem } - */ - addGroup: function(model, col, opt) { - var before = opt.before; - var view = new GroupView({ model: model }, groups); - - - view.render().$el.insertBefore(before); - }, - - /** - * Gets the height of one article items and stores it. - * @method setItemHeight - */ - setItemHeight: function() { - var firstItem = this.$el.find('.item:not(.invisible):first'); - if (firstItem.length) { - this._itemHeight = firstItem.get(0).getBoundingClientRect().height; - } - }, - - /** - * Removes everything from lists and adds new collectino of articles - * @method setItemHeight - * @param items {Backbone.Collection} bg.Items - */ - addItems: function(items) { - - groups.reset(); - - - /** - * Select removal - */ - this.selectedItems = []; - this.viewsToRender = []; - this.$el.find('.selected').removeClass('.selected'); - this.$el.find('.last-selected').removeClass('.last-selected'); - this.selectPivot = null; - /* --- */ - - //var st = Date.now(); - - this.setItemHeight(); - - this.reuseIndex = 0; - - - - items.forEach(function(item) { - this.addItem(item, true); - }, this); - - for (var i=this.reuseIndex, j = this.views.length; i < j; i++) { - if (!this.views[i].model) break; - this.views[i].unplugModel(); - } - - this.redraw(); - - if ($('input[type=search]').val()) { - app.actions.execute('articles:search'); - } - - //alert(Date.now() - st); - - }, - - /** - * Called every time when new feed is selected and before it is rendered - * @method clearOnSelect - */ - clearOnSelect: function() { - // Smart RSS used to reset search on feed select change. It instead keeps the fitler now. - //$('input[type=search]').val(''); - - // if prev selected was trash, hide undelete buttons - if (this.currentData.name == 'trash') { - /*$('[data-action="articles:update"]').css('display', 'block'); - $('[data-action="articles:undelete"]').css('display', 'none');*/ - app.articles.toolbar.showItems('articles:update'); - app.articles.toolbar.hideItems('articles:undelete'); - $('#context-undelete').css('display', 'none'); - } - - this.currentData = { - feeds: [], - name: 'all-feeds', - filter: { trashed: false }, - unreadOnly: false - }; - - }, - - /** - * Called every time when new feed is selected. Gets the right data from store. - * @method handleNewSelected - * @param data {Object} data object received from feed list - */ - handleNewSelected: function(data) { - this.clearOnSelect(); - this.currentData = data; - - var searchIn = null; - if (data.filter) { - searchIn = bg.items.where(data.filter); - } else { - searchIn = bg.items.where({ trashed: false }); - } - - // if newly selected is trash - if (this.currentData.name == 'trash') { - app.articles.toolbar.hideItems('articles:update').showItems('articles:undelete'); - $('#context-undelete').css('display', 'block'); - } - - var items = searchIn.filter(function(item) { - if (!item.get('unread') && this.unreadOnly) return false; - return data.name || data.feeds.indexOf(item.get('sourceID')) >= 0; - }, this); - - this.addItems( items ); - }, - - - /** - * If current feed is removed, select all feeds - * @triggered when any source is destroyed - * @method handleSourcesDestroy - * @param source {Source} Destroyed source - */ - handleSourcesDestroy: function(source) { - - var that = this; - var d = this.currentData; - var index = d.feeds.indexOf(source.id); - - if (index >= 0) { - d.feeds.splice(index, 1); - } - - if (!d.feeds.length && !d.filter) { - - this.clearOnSelect(); - - if (document.querySelector('.item')) { - this.once('items-destroyed', function() { - that.loadAllFeeds(); - }, this); - } else { - this.loadAllFeeds(); - } - } - - }, - - /** - * Moves item from trash back to its original source - * @method undeleteItem - * @param view {views/ItemView} Undeleted article view - */ - undeleteItem: function(view) { - view.model.save({ - 'trashed': false - }); - this.destroyItem(view); - }, - - /** - * Moves item to trash - * @method removeItem - * @param view {views/ItemView} Removed article view - */ - removeItem: function(view) { - askRmPinned = bg.settings.get('askRmPinned') - if (view.model.get('pinned') && askRmPinned == 'all') { - var conf = confirm(Locale.c.PIN_QUESTION_A + view.model.escape('title') + Locale.c.PIN_QUESTION_B); - if (!conf) { - return; - } - } - view.model.save({ trashed: true, visited: true }); - //this.destroyItem(view); - }, - - /** - * Removes item from both source and trash leaving only info it has been already fetched and deleted - * @method removeItemCompletely - * @param view {views/ItemView} Removed article view - */ - removeItemCompletely: function(view) { - askRmPinned = bg.settings.get('askRmPinned') - if (view.model.get('pinned') && askRmPinned && askRmPinned != 'none') { - var conf = confirm(Locale.c.PIN_QUESTION_A + view.model.escape('title') + Locale.c.PIN_QUESTION_B); - if (!conf) { - return; - } - } - view.model.markAsDeleted(); - //this.destroyItem(view); - }, - - /** - * Calls undeleteItem/removeItem/removeItemCompletely in a batch for several items - * @method destroyBatch - * @param arr {Array} List of views - * @param fn {Function} Function to be called on each view - */ - destroyBatch: function(arr, fn) { - for (var i=0, j=arr.length; i= 0) this.selectedItems.splice(io, 1); - io = this.views.indexOf(view); - if (io >= 0) this.views.splice(io, 1); - io = this.viewsToRender.indexOf(view); - if (io >= 0) this.viewsToRender.splice(io, 1); - - this.reuseIndex--; - if (this.reuseIndex < 0) { - this.reuseIndex = 0; - console.log('reuse index under zero'); - } - }, - - /** - * Toggles unread state of selected items (with onlyToRead option) - * @method changeUnreadState - * @param opt {Object} Options { onlyToRead: bool } - */ - changeUnreadState: function(opt) { - opt = opt || {}; - var val = this.selectedItems.length && !opt.onlyToRead ? !this.selectedItems[0].model.get('unread') : false; - this.selectedItems.forEach(function(item) { - if (!opt.onlyToRead || item.model.get('unread') == true) { - item.model.save({ unread: val, visited: true }); - } - - }, this); - } - }); - - ArticleListView = ArticleListView.extend(selectable); - - return new ArticleListView(); -}); \ No newline at end of file diff --git a/scripts/app/views/contentView.js b/scripts/app/views/contentView.js deleted file mode 100644 index ab6b3dae..00000000 --- a/scripts/app/views/contentView.js +++ /dev/null @@ -1,248 +0,0 @@ -/** - * @module App - * @submodule views/contentView - */ -define([ - 'backbone', 'jquery', 'underscore', 'helpers/formatDate', 'helpers/escapeHtml', 'helpers/stripTags', 'text!templates/download.html', - 'text!templates/header.html' -], -function(BB, $, _, formatDate, escapeHtml, stripTags, tplDownload, tplHeader) { - - /** - * Full view of one article (right column) - * @class ContentView - * @constructor - * @extends Backbone.View - */ - var ContentView = BB.View.extend({ - - /** - * Tag name of content view element - * @property tagName - * @default 'header' - * @type String - */ - tagName: 'header', - - /** - * Content view template - * @property template - * @default ./templates/header.html - * @type Function - */ - template: _.template(tplHeader), - - /** - * Template for downlaoding an article - * @property downloadTemplate - * @default ./templates/download.html - * @type Function - */ - downloadTemplate: _.template(tplDownload), - - - events: { - 'mousedown': 'handleMouseDown', - 'click .pin-button': 'handlePinClick', - 'keydown': 'handleKeyDown' - }, - - /** - * Changes pin state - * @method handlePinClick - * @triggered on click on pin button - * @param event {MouseEvent} - */ - handlePinClick: function(e) { - $(e.currentTarget).toggleClass('pinned'); - this.model.save({ - pinned: $(e.currentTarget).hasClass('pinned') - }); - }, - - /** - * Called when new instance is created - * @method initialize - */ - initialize: function() { - - this.on('attach', this.handleAttached); - - bg.items.on('change:pinned', this.handleItemsPin, this); - bg.sources.on('clear-events', this.handleClearEvents, this); - }, - - /** - * Sets comm event listeners - * @method handleAttached - * @triggered when content view is attached to DOM - */ - handleAttached: function() { - //this.template = _.template($('#template-header').html()); - - //window.addEventListener('message', function(e) { - app.on('select:article-list', function(data) { - this.handleNewSelected(bg.items.findWhere({ id: data.value })); - }, this); - - app.on('space-pressed', function() { - this.handleSpace(); - }, this); - - app.on('no-items:article-list', function() { - if (this.renderTimeout) { - clearTimeout(this.renderTimeout); - } - this.model = null; - this.hide(); - }, this); - - }, - - /** - * Next page in article or next unread article - * @method handleSpace - * @triggered when space is pressed in middle column - */ - handleSpace: function() { - var cw = $('iframe').get(0).contentWindow; - var d = $('iframe').get(0).contentWindow.document; - if (d.documentElement.clientHeight + $(d.body).scrollTop() >= d.body.offsetHeight ) { - app.trigger('give-me-next'); - } else { - cw.scrollBy(0, d.documentElement.clientHeight * 0.85); - } - }, - - /** - * Unbinds all listeners to bg process - * @method handleClearEvents - * @triggered when tab is closed/refershed - * @param id {Integer} id of the closed tab - */ - handleClearEvents: function(id) { - if (window == null || id == tabID) { - bg.items.off('change:pinned', this.handleItemsPin, this); - bg.sources.off('clear-events', this.handleClearEvents, this); - } - }, - - /** - * Sets the pin button state - * @method handleItemsPin - * @triggered when the pin state of the article is changed - * @param model {Item} article that had its pin state changed - */ - handleItemsPin: function(model) { - if (model == this.model) { - this.$el.find('.pin-button').toggleClass('pinned', this.model.get('pinned')); - } - }, - - /** - * Gets formated date (according to settings) from given unix time - * @method getFormatedDate - * @param unixtime {Number} - */ - getFormatedDate: function(unixtime) { - var dateFormats = { normal: 'DD.MM.YYYY', iso: 'YYYY-MM-DD', us: 'MM/DD/YYYY' }; - var pickedFormat = dateFormats[bg.settings.get('dateType') || 'normal'] || dateFormats['normal']; - - var timeFormat = bg.settings.get('hoursFormat') == '12h' ? 'H:mm a' : 'hh:mm:ss'; - - return formatDate(new Date(unixtime), pickedFormat + ' ' + timeFormat); - }, - - /** - * Rendering of article is delayed with timeout for 50ms to spped up quick select changed in article list. - * This property contains descriptor for that timeout. - * @property renderTimeout - * @default null - * @type Integer - */ - renderTimeout: null, - - /** - * Renders articles content asynchronously - * @method render - * @chainable - */ - render: function() { - clearTimeout(this.renderTimeout); - - this.renderTimeout = setTimeout(function(that) { - - if (!that.model) return; - that.show(); - - var data = Object.create(that.model.attributes); - data.date = that.getFormatedDate(that.model.get('date')); - data.title = stripTags(data.title).trim() || '<no title>'; - data.url = escapeHtml(data.url); - data.titleIsLink = bg.settings.get('titleIsLink'); - - var source = that.model.getSource(); - var content = that.model.get('content'); - - - that.$el.html(that.template(data)); - - // first load might be too soon - var sandbox = app.content.sandbox; - var fr = sandbox.el; - - if (sandbox.loaded) { - fr.contentWindow.scrollTo(0, 0); - fr.contentDocument.documentElement.style.fontSize = bg.settings.get('articleFontSize') + '%'; - fr.contentDocument.querySelector('base').href = source.get('base') || source.get('url'); - fr.contentDocument.querySelector('#smart-rss-content').innerHTML = content; - fr.contentDocument.querySelector('#smart-rss-url').href = that.model.get('url'); - } else { - sandbox.on('load', function() { - fr.contentWindow.scrollTo(0, 0); - fr.contentDocument.documentElement.style.fontSize = bg.settings.get('articleFontSize') + '%'; - fr.contentDocument.querySelector('base').href = source ? source.get('base') || source.get('url') : '#'; - fr.contentDocument.querySelector('#smart-rss-content').innerHTML = content; - fr.contentDocument.querySelector('#smart-rss-url').href = that.model.get('url'); - }); - } - }, 50, this); - - return this; - }, - - /** - * Replaces old article model with newly selected one - * @method handleNewSelected - * @param model {Item} The new article model - */ - handleNewSelected: function(model) { - if (model == this.model) return; - this.model = model; - if (!this.model) { - // should not happen but happens - this.hide(); - } else { - this.render(); - } - }, - - /** - * Hides contents (header, iframe) - * @method hide - */ - hide: function() { - $('header,iframe').css('display', 'none'); - }, - - /** - * Show contents (header, iframe) - * @method hide - */ - show: function() { - $('header,iframe').css('display', 'block'); - }, - }); - - return new ContentView(); -}); \ No newline at end of file diff --git a/scripts/app/views/feedList.js b/scripts/app/views/feedList.js deleted file mode 100644 index 9233f82d..00000000 --- a/scripts/app/views/feedList.js +++ /dev/null @@ -1,495 +0,0 @@ -/** - * @module App - * @submodule views/feedList - */ -define([ - 'backbone', 'jquery', 'underscore', 'views/SourceView', 'views/FolderView', 'views/SpecialView', 'models/Special', - 'instances/contextMenus', 'mixins/selectable', 'instances/specials' -], -function (BB, $, _, SourceView, FolderView, SpecialView, Special, contextMenus, selectable, specials) { - - /** - * List of feeds (in left column) - * @class FeedListView - * @constructor - * @extends Backbone.View - */ - var FeedListView = BB.View.extend({ - - /** - * Tag name of the list - * @property tagName - * @default 'div' - * @type String - */ - tagName: 'div', - - /** - * Class of feed list views - * @property itemClass - * @default 'list-item' - * @type String - */ - itemClass: 'list-item', - - /** - * ID of feed list - * @property id - * @default 'feed-list' - * @type String - */ - id: 'feed-list', - - events: { - 'dragstart .source': 'handleDragStart', - 'drop': 'handleDrop', - 'drop [data-in-folder]': 'handleDrop', - 'drop .folder': 'handleDrop', - 'dragover': 'handleDragOver', - 'dragover .folder,[data-in-folder]': 'handleDragOver', - 'dragleave .folder,[data-in-folder]': 'handleDragLeave', - 'mousedown .list-item': 'handleMouseDown', - 'mouseup .list-item': 'handleMouseUp' - }, - - /** - * Called when new instance is created - * @method initialize - */ - initialize: function() { - - this.el.view = this; - - this.on('attach', this.handleAttach); - - bg.sources.on('reset', this.addSources, this); - bg.sources.on('add', this.addSource, this); - bg.sources.on('change:folderID', this.handleChangeFolder, this); - bg.folders.on('add', this.addFolder, this); - bg.sources.on('clear-events', this.handleClearEvents, this); - - this.on('pick', this.handlePick); - - }, - - /** - * Sets comm event listeners and inserts feeds - * @method handleAttached - * @triggered when feed list is attached to DOM - */ - handleAttach: function() { - app.on('select-all-feeds', function() { - var allFeeds = $('.special:first').get(0); - if (!allFeeds) return; - this.select(allFeeds.view); - }, this); - - app.on('select-folder', function(id) { - var folder = $('.folder[data-id=' + id + ']').get(0); - if (!folder) return; - this.select(folder.view); - }, this); - - app.on('focus-feed', function(id) { - var feed = $('.list-item[data-id=' + id + ']').get(0); - if (!feed) return; - this.select(feed.view); - app.actions.execute('feeds:showAndFocusArticles'); - }, this); - - this.insertFeeds(); - }, - - /** - * Adds folders specials and sources - * @method insertFeeds - * @@chainable - */ - insertFeeds: function() { - this.addFolders(bg.folders); - - this.addSpecial(specials.allFeeds); - this.addSpecial(specials.pinned); - this.addSpecial(specials.trash); - - this.addSources(bg.sources); - - return this; - }, - - /** - * If one list-item was selected by left mouse button, show its articles. - * @triggered by selectable mixin. - * @method handlePick - * @param view {TopView} Picked source, folder or special - * @param event {Event} Mouse or key event - */ - handlePick: function(view, e) { - if (e.type == 'mousedown' && e.which == 1) { - //view.showSourceItems(e); - app.actions.execute('feeds:showAndFocusArticles', e); - } - }, - - /** - * Selectable mixin bindings. The selectable mixing will trigger "pick" event when items are selected. - * @method handleMouseDown - * @triggered on mouse down - * @param event {Event} Mouse event - */ - handleMouseDown: function(e) { - //e.currentTarget.view.handleMouseDown(e); - this.handleSelectableMouseDown(e); - }, - - /** - * Selectable mixin bindings, item bindings - * @method handleMouseUp - * @triggered on mouse up - * @param event {Event} Mouse event - */ - handleMouseUp: function(e) { - e.currentTarget.view.handleMouseUp(e); - this.handleSelectableMouseUp(e); - }, - - /** - * Add class to drop region elements - * @method handleMouseDown - * @triggered on drag over - * @param event {DragEvent} Drag event - */ - handleDragOver: function(e) { - var f = e.currentTarget.dataset.inFolder; - if (f) { - $('.folder[data-id=' + f + ']').addClass('drag-over'); - } else if ($(e.currentTarget).hasClass('folder')) { - $(e.currentTarget).addClass('drag-over'); - } - e.preventDefault(); - }, - - /** - * Remove class from drop region elements - * @method handleDragLeave - * @triggered on drag leave - * @param event {DragEvent} Drag event - */ - handleDragLeave: function(e) { - var f = e.currentTarget.dataset.inFolder; - if (f) { - $('.folder[data-id=' + f + ']').removeClass('drag-over'); - } else if ($(e.currentTarget).hasClass('folder')) { - $(e.currentTarget).removeClass('drag-over'); - } - }, - - /** - * Handle drop event (move feeds between folders) - * @method handleDrop - * @triggered on drop - * @param event {DragEvent} Drag event - */ - handleDrop: function(e) { - - var oe = e.originalEvent; - e.preventDefault(); - - $('.drag-over').removeClass('drag-over'); - - var ids = JSON.parse( oe.dataTransfer.getData('dnd-sources') ) ; - if (!ids || !ids.length) return; - - - for (var i=0; i= 0) { - this.selectedItems.splice(io, 1); - } - }, - - /** - * Get array of selected feeds (including feeds in selected folders) - * @method getSelectedFeeds - * @param arr {Array} List of selected items - */ - getSelectedFeeds: function(arr) { - var si = arr || _.pluck(this.selectedItems, 'model'); - var rt = []; - for (var i=0; i 0) { - chrome.alarms.create('source-' + source.get('id'), { - delayInMinutes: source.get('updateEvery'), - periodInMinutes: source.get('updateEvery') - }); - } - loader.downloadOne(source); - }); - - sources.on('change:updateEvery reset-alarm', function(source) { - if (source.get('updateEvery') > 0) { - chrome.alarms.create('source-' + source.get('id'), { - delayInMinutes: source.get('updateEvery'), - periodInMinutes: source.get('updateEvery') - }); - } else { - chrome.alarms.clear('source-' + source.get('id')); - } - }); - - chrome.alarms.onAlarm.addListener(function(alarm) { - var sourceID = alarm.name.replace('source-', ''); - if (sourceID) { - var source = sources.findWhere({ - id: sourceID - }); - if (source) { - if (!loader.downloadOne(source)) { - setTimeout(loader.downloadOne, 30000, source); - } - } else { - console.log('No source with ID: ' + sourceID); - chrome.alarms.clear(alarm.name); - } - - } - - }); - - sources.on('change:url', function(source) { - loader.downloadOne(source); - }); - - sources.on('change:title', function(source) { - // if url was changed as well change:url listener will download the source - if (!source.get('title')) { - loader.downloadOne(source); - } - - sources.sort(); - }); - - sources.on('change:hasNew', animation.handleIconChange); - settings.on('change:icon', animation.handleIconChange); - - info.setEvents(sources); - - - /** - * Init - */ - - - setTimeout(loader.downloadAll, 30000); - appStarted.resolve(); - - /** - * Set icon - */ - - animation.stop(); - - - /** - * onclick:button -> open RSS - */ - chrome.browserAction.onClicked.addListener(function() { - openRSS(true); - }); - - }); - }); - - /** - * Messages - */ - - chrome.runtime.onMessageExternal.addListener(function(message) { - // if.sender.id != blahblah -> return; - if (!message.hasOwnProperty('action')) { - return; - } - - if (message.action == 'new-rss' && message.value) { - message.value = message.value.replace(/^feed:/i, 'http:'); - - var duplicate = sources.findWhere({ url: message.value }); - if (!duplicate) { - var s = sources.create({ - title: message.value, - url: message.value, - updateEvery: 180 - }, { wait: true }); - openRSS(false, s.get('id')); - } else { - duplicate.trigger('change'); - openRSS(false, duplicate.get('id')); - } - - } - }); - - function openRSS(closeIfActive, focusSource) { - var url = chrome.extension.getURL('rss.html'); - chrome.tabs.query({ - url: url - }, function(tabs) { - if (tabs[0]) { - if (tabs[0].active && closeIfActive) { - chrome.tabs.remove(tabs[0].id); - } else { - chrome.tabs.update(tabs[0].id, { - active: true - }); - if (focusSource) { - window.sourceToFocus = focusSource; - } - } - } else { - window.sourceToFocus = focusSource; - chrome.tabs.create({ - 'url': url - }, function() {}); - } - }); - } - -}); \ No newline at end of file diff --git a/scripts/bgprocess/collections/Folders.js b/scripts/bgprocess/collections/Folders.js deleted file mode 100644 index e066eb73..00000000 --- a/scripts/bgprocess/collections/Folders.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @module BgProcess - * @submodule collections/Folders - */ -define(['backbone', 'models/Folder', 'preps/indexeddb'], function (BB, Folder) { - - /** - * Collection of feed folders - * @class Folders - * @constructor - * @extends Backbone.Collection - */ - var Folders = BB.Collection.extend({ - model: Folder, - localStorage: new Backbone.LocalStorage('folders-backbone'), - comparator: function(a, b) { - var t1 = (a.get('title') || '').trim().toLowerCase(); - var t2 = (b.get('title') || '').trim().toLowerCase(); - return t1 < t2 ? -1 : 1; - } - }); - - return Folders; - -}); \ No newline at end of file diff --git a/scripts/bgprocess/collections/Items.js b/scripts/bgprocess/collections/Items.js deleted file mode 100644 index 752ec42f..00000000 --- a/scripts/bgprocess/collections/Items.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @module BgProcess - * @submodule collections/Items - */ -define(['backbone', 'models/Item', 'preps/indexeddb'], function (BB, Item) { - - function getS(val) { - return String(val).toLowerCase(); - } - - /** - * Collection of feed modules - * @class Items - * @constructor - * @extends Backbone.Collection - */ - var Items = BB.Collection.extend({ - model: Item, - batch: false, - localStorage: new Backbone.LocalStorage('items-backbone'), - comparator: function(a, b, sorting) { - var val; - sortBy = sorting || settings.get('sortBy'); - - - if (sortBy == 'title') { - if (!sorting && getS(a.get('title')) == getS(b.get('title')) ) return this.comparator(a, b, settings.get('sortBy2') || true); - val = getS(a.get('title')) <= getS(b.get('title')) ? 1 : -1; - } else if (sortBy == 'author') { - if (!sorting && getS(a.get('author')) == getS(b.get('author')) ) return this.comparator(a, b, settings.get('sortBy2') || true); - val = getS(a.get('author')) <= getS(b.get('author')) ? 1 : -1; - } else { - if (!sorting && a.get('date') == b.get('date') ) return this.comparator(a, b, settings.get('sortBy2') || true); - val = a.get('date') <= b.get('date') ? 1 : -1; - } - - if (!sorting && settings.get('sortOrder') == 'asc') val = -val; - if (sorting && settings.get('sortOrder2') == 'asc') val = -val; - return val; - }, - initialize: function() { - //settings.on('change:sortOrder', this.sort, this); - this.listenTo(settings, 'change:sortOrder', this.sort); - this.listenTo(settings, 'change:sortOrder2', this.sort); - this.listenTo(settings, 'change:sortBy', this.sort); - this.listenTo(settings, 'change:sortBy2', this.sort); - } - }); - - return Items; - -}); \ No newline at end of file diff --git a/scripts/bgprocess/collections/Logs.js b/scripts/bgprocess/collections/Logs.js deleted file mode 100644 index ca4f6bc4..00000000 --- a/scripts/bgprocess/collections/Logs.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @module BgProcess - * @submodule collections/Logs - */ -define(['backbone', 'models/Log'], function (BB, Log) { - - /** - * Collection of error log modules - * @class Logs - * @constructor - * @extends Backbone.Collection - */ - var Logs = Backbone.Collection.extend({ - model: Log, - initialze: function() { - - }, - startLogging: function() { - window.onerror = function(a, b, c) { - var file = b.replace(/chrome\-extension:\/\/[^\/]+\//, ''); - var msg = a.toString() + ' (Line: ' + c.toString() + ', File: ' + file + ')'; - logs.add({ - message: msg - }); - }; - } - }); - - - return Logs; - -}); \ No newline at end of file diff --git a/scripts/bgprocess/collections/Sources.js b/scripts/bgprocess/collections/Sources.js deleted file mode 100644 index 980686b8..00000000 --- a/scripts/bgprocess/collections/Sources.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @module BgProcess - * @submodule collections/Sources - */ -define(['backbone', 'models/Source', 'preps/indexeddb'], function (BB, Source) { - - /** - * Collection of feed modules - * @class Sources - * @constructor - * @extends Backbone.Collection - */ - var Sources = BB.Collection.extend({ - model: Source, - localStorage: new Backbone.LocalStorage('sources-backbone'), - comparator: function(a, b) { - var t1 = (a.get('title') || '').trim().toLowerCase(); - var t2 = (b.get('title') || '').trim().toLowerCase(); - return t1 < t2 ? -1 : 1; - } - }); - - return Sources; - -}); \ No newline at end of file diff --git a/scripts/bgprocess/collections/Toolbars.js b/scripts/bgprocess/collections/Toolbars.js deleted file mode 100644 index 7ed892f9..00000000 --- a/scripts/bgprocess/collections/Toolbars.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @module BgProcess - * @submodule collections/Toolbars - */ -define([ - 'backbone', 'models/Toolbar', 'staticdb/defaultToolbarItems', 'preps/indexeddb' -], -function (BB, Toolbar, defaultToolbarItems) { - - function getDataByRegion(data, region) { - if (!Array.isArray(data)) return null; - - for (var i=0; i= parsedData[i].version) { - parsedData[i] = fromdb; - } - } - - return parsedData; - } - }); - - return Toolbars; - -}); \ No newline at end of file diff --git a/scripts/bgprocess/models/Folder.js b/scripts/bgprocess/models/Folder.js deleted file mode 100644 index 8a39a84c..00000000 --- a/scripts/bgprocess/models/Folder.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @module BgProcess - * @submodule models/Folder - */ -define(['backbone'], function (BB) { - - /** - * Model for feed folders - * @class Folder - * @constructor - * @extends Backbone.Model - */ - var Folder = BB.Model.extend({ - defaults: { - title: '', - opened: false, - count: 0, // unread - countAll: 0 - } - }); - - return Folder; - -}); \ No newline at end of file diff --git a/scripts/bgprocess/models/Info.js b/scripts/bgprocess/models/Info.js deleted file mode 100644 index df37cec7..00000000 --- a/scripts/bgprocess/models/Info.js +++ /dev/null @@ -1,281 +0,0 @@ -/** - * @module BgProcess - * @submodule models/Info - */ -define(['backbone', 'modules/Animation'], function (BB, animation) { - - var handleAllCountChange = function(model) { - if (settings.get('badgeMode') == 'disabled') { - if (model == settings) chrome.browserAction.setBadgeText({ text: '' }); - return; - } - - if (model == settings) { - if (settings.get('badgeMode') == 'unread') { - info.off('change:allCountUnvisited', handleAllCountChange); - info.on('change:allCountUnread', handleAllCountChange); - } else { - info.off('change:allCountUnread', handleAllCountChange); - info.on('change:allCountUnvisited', handleAllCountChange); - } - } - if (info.badgeTimeout) return; - - info.badgeTimeout = setTimeout(function() { - var val; - if (settings.get('badgeMode') == 'unread') { - val = info.get('allCountUnread') > 99 ? '+' : info.get('allCountUnread'); - } else { - val = info.get('allCountUnvisited') > 99 ? '+' : info.get('allCountUnvisited'); - } - - val = val <= 0 ? '' : String(val); - chrome.browserAction.setBadgeText({ text: val }); - chrome.browserAction.setBadgeBackgroundColor({ color: '#777' }); - info.badgeTimeout = null; - }); - }; - - - - - /** - * This model stores info about count of read/unread/unvisited/total of all feeds and in trash - * @class Info - * @constructor - * @extends Backbone.Model - */ - var Info = BB.Model.extend({ - defaults: { - id: 'info-id', - allCountUnread: 0, - allCountTotal: 0, - allCountUnvisited: 0, - trashCountUnread: 0, - trashCountTotal: 0 - }, - badgeTimeout: null, - autoSetData: function() { - this.set({ - allCountUnread: items.where({ trashed: false, deleted: false, unread: true }).length, - allCountTotal: items.where({ trashed: false, deleted: false }).length, - allCountUnvisited: items.where({ visited: false, trashed: false }).length, - trashCountUnread: items.where({ trashed: true, deleted: false, unread: true }).length, - trashCountTotal: items.where({ trashed: true, deleted: false }).length - }); - - sources.forEach(function(source) { - source.set({ - count: items.where({ trashed: false, sourceID: source.id, unread: true }).length, - countAll: items.where({ trashed: false, sourceID: source.id }).length - }); - }); - - folders.forEach(function(folder) { - var count = 0; - var countAll = 0; - sources.where({ folderID: folder.id }).forEach(function(source) { - count += source.get('count'); - countAll += source.get('countAll'); - }); - folder.set({ count: count, countAll: countAll }); - }); - }, - setEvents: function() { - settings.on('change:badgeMode', handleAllCountChange); - if (settings.get('badgeMode') == 'unread') { - info.on('change:allCountUnread', handleAllCountChange); - } else if (settings.get('badgeMode') == 'unvisited') { - info.on('change:allCountUnvisited', handleAllCountChange); - } - handleAllCountChange(); - - - sources.on('destroy', function(source) { - var trashUnread = 0; - var trashAll = 0; - var allUnvisited = 0; - items.where({ sourceID: source.get('id') }).forEach(function(item) { - if (!item.get('deleted')) { - if (!item.get('visited')) allUnvisited++; - if (item.get('trashed')) trashAll++; - if (item.get('trashed') && item.get('unread')) trashUnread++; - } - item.destroy(); - }); - - /**** - * probably not neccesary because I save all the removed items to batch and then - * in next frame I remove them at once and all handleScroll anyway - ****/ - //items.trigger('render-screen'); - - - - info.set({ - allCountUnread: info.get('allCountUnread') - source.get('count'), - allCountTotal: info.get('allCountTotal') - source.get('countAll'), - allCountUnvisited: info.get('allCountUnvisited') - allUnvisited, - trashCountUnread: info.get('trashCountUnread') - trashUnread, - trashCountTotal: info.get('trashCountTotal') - trashAll - }); - - if (source.get('folderID')) { - - var folder = folders.findWhere({ id: source.get('folderID') }); - if (folder) { - folder.set({ - count: folder.get('count') - source.get('count'), - countAll: folder.get('countAll') - source.get('countAll') - }); - } - } - - if (source.get('hasNew')) { - animation.handleIconChange(); - } - - try { - chrome.alarms.clear('source-' + source.get('id')); - } catch (e) { - console.log('Alarm error: ' + e); - } - }); - - items.on('change:unread', function(model) { - var source = model.getSource(); - if (!model.previous('trashed') && source != sourceJoker) { - if (model.get('unread') == true) { - source.set({ - 'count': source.get('count') + 1 - }); - } else { - source.set({ - 'count': source.get('count') - 1 - }); - - if (source.get('count') == 0 && source.get('hasNew') == true) { - source.save('hasNew', false); - } - } - } else if (!model.get('deleted') && source != sourceJoker) { - info.set({ - trashCountUnread: info.get('trashCountUnread') + (model.get('unread') ? 1 : -1) - }); - } - }); - - items.on('change:trashed', function(model) { - var source = model.getSource(); - if (source != sourceJoker && model.get('unread') == true) { - if (model.get('trashed') == true) { - source.set({ - 'count': source.get('count') - 1, - 'countAll': source.get('countAll') - 1 - }); - - if (source.get('count') == 0 && source.get('hasNew') == true) { - source.save('hasNew', false); - } - - } else { - source.set({ - 'count': source.get('count') + 1, - 'countAll': source.get('countAll') + 1 - }); - } - - if (!model.get('deleted')) { - info.set({ - 'trashCountTotal': info.get('trashCountTotal') + (model.get('trashed') ? 1 : -1), - 'trashCountUnread': info.get('trashCountUnread') + (model.get('trashed') ? 1 : -1) - }); - } - } else if (source != sourceJoker) { - source.set({ - 'countAll': source.get('countAll') + (model.get('trashed') ? - 1 : 1) - }); - - - if (!model.get('deleted')) { - info.set({ - 'trashCountTotal': info.get('trashCountTotal') + (model.get('trashed') ? 1 : -1) - }); - } - } - }); - - - items.on('change:deleted', function(model) { - if (model.previous('trashed') == true) { - info.set({ - 'trashCountTotal': info.get('trashCountTotal') - 1, - 'trashCountUnread': !model.previous('unread') ? info.get('trashCountUnread') : info.get('trashCountUnread') - 1 - }); - } - }); - - items.on('change:visited', function(model) { - info.set({ - 'allCountUnvisited': info.get('allCountUnvisited') + (model.get('visited') ? -1 : 1) - }); - }); - - sources.on('change:count', function(source) { - // SPECIALS - info.set({ - 'allCountUnread': info.get('allCountUnread') + source.get('count') - source.previous('count') - }); - - // FOLDER - if (!(source.get('folderID'))) return; - - var folder = folders.findWhere({ id: source.get('folderID') }); - if (!folder) return; - - folder.set({ count: folder.get('count') + source.get('count') - source.previous('count') }); - }); - - sources.on('change:countAll', function(source) { - // SPECIALS - info.set({ - 'allCountTotal': info.get('allCountTotal') + source.get('countAll') - source.previous('countAll') - }); - - // FOLDER - if (!(source.get('folderID'))) return; - - var folder = folders.findWhere({ id: source.get('folderID') }); - if (!folder) return; - - folder.set({ countAll: folder.get('countAll') + source.get('countAll') - source.previous('countAll') }); - }); - - sources.on('change:folderID', function(source) { - var folder; - if (source.get('folderID')) { - - folder = folders.findWhere({ id: source.get('folderID') }); - if (!folder) return; - - folder.set({ - count: folder.get('count') + source.get('count'), - countAll: folder.get('countAll') + source.get('countAll') - }); - } - - if (source.previous('folderID')) { - folder = folders.findWhere({ id: source.previous('folderID') }); - if (!folder) return; - - folder.set({ - count: Math.max(folder.get('count') - source.get('count'), 0), - countAll: Math.max(folder.get('countAll') - source.get('countAll'), 0) - }); - } - }); - } - }); - - return Info; -}); \ No newline at end of file diff --git a/scripts/bgprocess/models/Item.js b/scripts/bgprocess/models/Item.js deleted file mode 100644 index ebfafae4..00000000 --- a/scripts/bgprocess/models/Item.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @module BgProcess - * @submodule models/Item - */ -define(['backbone'], function (BB) { - - /** - * Module for each article - * @class Item - * @constructor - * @extends Backbone.Model - */ - var Item = BB.Model.extend({ - defaults: { - title: '', - author: '', - url: 'opera:blank', - date: 0, - content: 'No content loaded.', - sourceID: -1, - unread: true, - visited: false, - deleted: false, - trashed: false, - pinned: false, - dateCreated: 0 - }, - markAsDeleted: function() { - this.save({ - trashed: true, - deleted: true, - visited: true, - unread: false, - 'pinned': false, - 'content': '', - 'author': '', - 'title': '' - }); - }, - _source: null, - getSource: function() { - if (!this._source) { - this._source = sources.findWhere({ id: this.get('sourceID') }) || sourceJoker; - } - return this._source; - }, - query: function(o) { - if (!o) return true; - for (var i in o) { - if (o.hasOwnProperty(i)) { - if (this.get(i) != o[i]) return false; - } - } - return true; - } - }); - - return Item; - -}); \ No newline at end of file diff --git a/scripts/bgprocess/models/Loader.js b/scripts/bgprocess/models/Loader.js deleted file mode 100644 index 8ee804d9..00000000 --- a/scripts/bgprocess/models/Loader.js +++ /dev/null @@ -1,274 +0,0 @@ -/** - * @module BgProcess - * @submodule models/Loader - */ -define(['backbone', 'modules/RSSParser', 'modules/Animation'], function (BB, RSSParser, animation) { - - function autoremoveItems(source) { - /* - var sourcesWithAutoRemove = sources.filter(function(source) { - return source.get('autoremove') > 0; - }); - sourcesWithAutoRemove.forEach(function(source) { - */ - - if (!parseFloat(source.get('autoremove'))) return; - - items.where({ sourceID: source.get('id'), deleted: false, pinned: false }).forEach(function(item) { - var date = item.get('dateCreated') || item.get('date'); - var removalInMs = source.get('autoremove') * 24 * 60 * 60 * 1000; - if (date + removalInMs < Date.now() ) { - item.markAsDeleted(); - } - }); - } - - function download(sourcesToDownload) { - if (!sourcesToDownload) return; - if (!Array.isArray(sourcesToDownload)) { - sourcesToDownload = [sourcesToDownload]; - } - - sourcesToDownload.forEach(downloadOne); - } - - function downloadOne(model) { - if (loader.sourceLoading == model || loader.sourcesToLoad.indexOf(model) >= 0) { - return false; - } - - if (model instanceof Folder) { - download( sources.where({ folderID: model.id }) ); - return true; - } else if (model instanceof Source) { - loader.addSources(model); - if (loader.get('loading') == false) downloadURL(); - return true; - } - - return false; - } - - function downloadAll(force) { - - if (loader.get('loading') == true) return; - - var sourcesArr = sources.toArray(); - - if (!force) { - sourcesArr = sourcesArr.filter(function(source) { - if (source.get('updateEvery') == 0) return false; - /**** - why !source.get('lastUpdate') ? .. I think I wanted !source.get('lastUpdate') => true not the other way around - ****/ - if (!source.get('lastUpdate')) return true; - if (/*!source.get('lastUpdate') ||*/ source.get('lastUpdate') > Date.now() - source.get('updateEvery') * 60 * 1000) { - return false; - } - return true; - }); - } - - if (sourcesArr.length) { - loader.addSources(sourcesArr); - downloadURL(); - } - - } - - function playNotificationSound() { - - var audio; - if (!settings.get('useSound') || settings.get('useSound') == ':user') { - audio = new Audio(settings.get('defaultSound')); - } else if (settings.get('useSound') == ':none') { - audio = false; - } else { - audio = new Audio('/sounds/' + settings.get('useSound') + '.ogg'); - } - if (audio) { - audio.volume = parseFloat(settings.get('soundVolume')); - audio.play(); - } - - } - - function downloadStopped() { - if (loader.itemsDownloaded && settings.get('soundNotifications')) { - playNotificationSound(); - } - - loader.set('maxSources', 0); - loader.set('loaded', 0); - loader.set('loading', false); - loader.sourceLoading = null; - loader.currentRequest = null; - loader.itemsDownloaded = false; - animation.stop(); - } - - function downloadURL() { - if (!loader.sourcesToLoad.length) { - // IF DOWNLOADING FINISHED, DELETED ITEMS WITH DELETED SOURCE (should not really happen) - var sourceIDs = sources.pluck('id'); - var foundSome = false; - items.toArray().forEach(function(item) { - if (sourceIDs.indexOf(item.get('sourceID')) == -1) { - console.log('DELETING ITEM BECAUSE OF MISSING SOURCE'); - item.destroy(); - foundSome = true; - } - }); - - if (foundSome) { - info.autoSetData(); - } - - downloadStopped(); - - - return; - } - - animation.start(); - loader.set('loading', true); - var sourceToLoad = loader.sourceLoading = loader.sourcesToLoad.pop(); - - autoremoveItems(sourceToLoad); - - var options = { - url: sourceToLoad.get('url'), - timeout: 20000, - dataType: 'xml', - success: function(r) { - - // parsedData step needed for debugging - var parsedData = RSSParser.parse(r, sourceToLoad.get('id')); - - var hasNew = false; - var createdNo = 0; - parsedData.forEach(function(item) { - var existingItem = items.get(item.id) || items.get(item.oldId); - if (!existingItem) { - hasNew = true; - items.create(item, { sort: false }); - createdNo++; - } else if (existingItem.get('deleted') == false && existingItem.get('content') != item.content) { - existingItem.save({ - content: item.content - }); - } - }); - - items.sort({ silent: true }); - if (hasNew) { - items.trigger('render-screen'); - loader.itemsDownloaded = true; - } - - // remove old deleted content - var fetchedIDs = _.pluck(parsedData, 'id'); - var fetchedOldIDs = _.pluck(parsedData, 'oldId'); - items.where({ - sourceID: sourceToLoad.get('id'), - deleted: true - }).forEach(function(item) { - if (fetchedIDs.indexOf(item.id) == -1 && fetchedOldIDs.indexOf(item.id) == -1) { - item.destroy(); - } - }); - - // tip to optimize: var count = items.where.call(countAll, {unread: true }).length - var countAll = items.where({ sourceID: sourceToLoad.get('id'), trashed: false }).length; - var count = items.where({ sourceID: sourceToLoad.get('id'), unread: true, trashed: false }).length; - - sourceToLoad.save({ - 'count': count, - 'countAll': countAll, - 'lastUpdate': Date.now(), - 'hasNew': hasNew || sourceToLoad.get('hasNew') - }); - - info.set({ - allCountUnvisited: info.get('allCountUnvisited') + createdNo - }); - - sourceToLoad.trigger('update', { ok: true }); - - }, - error: function() { - console.log('Failed load RSS: ' + sourceToLoad.get('url')); - - sourceToLoad.trigger('update', { ok: false }); - }, - complete: function() { - loader.set('loaded', loader.get('loaded') + 1); - - // reset alarm to make sure next call isn't too soon + to make sure alarm acutaly exists (it doesn't after import) - sourceToLoad.trigger('reset-alarm', sourceToLoad); - sourceToLoad.set('isLoading', false); - - downloadURL(); - }, - beforeSend: function(xhr) { - xhr.setRequestHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'); - xhr.setRequestHeader('Pragma', 'no-cache'); - - // Removed because "http://www.f5haber.com/rss/teknoloji_haber.xml" doesn't like it - // xhr.setRequestHeader('If-Modified-Since', 'Tue, 1 Jan 1991 00:00:00 GMT'); - xhr.setRequestHeader('X-Time-Stamp', Date.now()); - } - }; - - if (sourceToLoad.get('username') || sourceToLoad.get('password')) { - options.username = sourceToLoad.get('username') || ''; - options.password = sourceToLoad.getPass() || ''; - } - - if (settings.get('showSpinner')) { - sourceToLoad.set('isLoading', true); - } - loader.currentRequest = $.ajax(options); - } - - - /** - * Updates feeds and keeps info about progress - * @class Loader - * @constructor - * @extends Backbone.Model - */ - var Loader = Backbone.Model.extend({ - defaults: { - maxSources: 0, - loaded: 0, - loading: false - }, - currentRequest: null, - itemsDownloaded: false, - sourcesToLoad: [], - sourceLoading: null, - addSources: function(s) { - if (s instanceof Source) { - this.sourcesToLoad.push(s); - this.set('maxSources', this.get('maxSources') + 1); - } else if (Array.isArray(s)) { - this.sourcesToLoad = this.sourcesToLoad.concat(s); - this.set('maxSources', this.get('maxSources') + s.length); - } - }, - abortDownloading: function() { - loader.currentRequest.abort(); - this.sourcesToLoad = []; - downloadStopped(); - }, - download: download, - downloadURL: downloadURL, - downloadOne: downloadOne, - downloadAll: downloadAll, - playNotificationSound: playNotificationSound - }); - - return Loader; - -}); \ No newline at end of file diff --git a/scripts/bgprocess/models/Log.js b/scripts/bgprocess/models/Log.js deleted file mode 100644 index 81984fb2..00000000 --- a/scripts/bgprocess/models/Log.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @module BgProcess - * @submodule models/Log - */ -define(['backbone'], function (BB) { - - /** - * Error log model - * @class Log - * @constructor - * @extends Backbone.Model - */ - var Log = BB.Model.extend({ - defaults: { - message: '' - } - }); - - return Log; - -}); \ No newline at end of file diff --git a/scripts/bgprocess/models/Settings.js b/scripts/bgprocess/models/Settings.js deleted file mode 100644 index fde41784..00000000 --- a/scripts/bgprocess/models/Settings.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @module BgProcess - * @submodule models/Settings - */ -define(['backbone', 'preps/indexeddb'], function (BB) { - - /** - * Test navigator.language and if it matches some avalable language - */ - function getLangFromNavigator() { - var ln = String(navigator.language).split('-')[0]; - var available = ['en', 'cs', 'sk', 'de', 'tr', 'pl', 'ru', 'hu', 'nl', 'fr', 'pt', 'hr']; - var index = available.indexOf(ln); - if (index >= 0) { - return available[index]; - } - return 'en'; - } - - /** - * User settings - * @class Settings - * @constructor - * @extends Backbone.Model - */ - var Settings = BB.Model.extend({ - defaults: { - id: 'settings-id', - lang: getLangFromNavigator(), - dateType: 'normal', // normal = DD.MM.YYYY, ISO = YYYY-MM-DD, US = MM/DD/YYYY - layout: 'horizontal', // or vertical - lines: 'auto', // one-line, two-lines - posA: '250,*', - posB: '350,*', - posC: '50%,*', - sortOrder: 'desc', - sortOrder2: 'asc', - icon: 'orange', - readOnVisit: false, - askOnOpening: true, - panelToggled: true, - enablePanelToggle: false, - fullDate: false, - hoursFormat: '24h', - articleFontSize: '100', - uiFontSize: '100', - disableDateGroups: false, - thickFrameBorders: false, - badgeMode: 'disabled', - circularNavigation: true, - sortBy: 'date', - sortBy2: 'title', - askRmPinned: 'trashed', - titleIsLink: true, - soundNotifications: false, - defaultSound: '', - useSound: ':user', - soundVolume: 1, // min: 0, max: 1 - showSpinner: true - }, - - /** - * @property localStorage - * @type Backbone.LocalStorage - * @default *settings-backbone* - */ - localStorage: new Backbone.LocalStorage('settings-backbone') - }); - - return Settings; -}); \ No newline at end of file diff --git a/scripts/bgprocess/models/Source.js b/scripts/bgprocess/models/Source.js deleted file mode 100644 index 77549baf..00000000 --- a/scripts/bgprocess/models/Source.js +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @module BgProcess - * @submodule models/Source - */ -define(['backbone'], function (BB) { - - /** - * Feed module - * @class Source - * @constructor - * @extends Backbone.Model - */ - var Source = BB.Model.extend({ - defaults: { - title: '', - url: 'about:blank', - base: '', - updateEvery: 180, // in minutes - lastUpdate: 0, - count: 0, // unread - countAll: 0, - username: '', - password: '', - hasNew: false, - isLoading: false, - autoremove: 0 // in days - }, - - initialize: function() { - // in case user quits Opera when the source is being updated - this.set('isLoading', false); - }, - - getPass: function() { - var str = this.get('password'); - if (str.indexOf('enc:') != 0) return str; - - var dec = ''; - for (var i=4; i 4) this.i = 1; - }, - stop: function() { - clearInterval(this.interval); - this.interval = null; - this.i = 1; - this.handleIconChange(); - }, - start: function() { - if (this.interval) return; - var that = this; - this.interval = setInterval(function() { - that.update(); - }, 400); - this.update(); - }, - handleIconChange: function() { - if (this.interval) return; - if ( sources.findWhere({ hasNew: true }) ) { - chrome.browserAction.setIcon({ - path: '/images/icon19-' + settings.get('icon') + '.png' - }); - } else { - chrome.browserAction.setIcon({ - path: '/images/icon19.png' - }); - } - } - }; - - return Animation; - -}); \ No newline at end of file diff --git a/scripts/bgprocess/modules/RSSParser.js b/scripts/bgprocess/modules/RSSParser.js deleted file mode 100644 index b40a68d4..00000000 --- a/scripts/bgprocess/modules/RSSParser.js +++ /dev/null @@ -1,241 +0,0 @@ -/** - * @module BgProcess - * @submodule modules/RSSParser - */ -define(['md5'], function (CryptoJS) { - - /** - * RSS Parser - * @class RSSParser - * @constructor - * @extends Object - */ - function parseRSS(xml, sourceID) { - var items = []; - - if (!xml || !(xml instanceof XMLDocument)) { - return items; - } - - - var nodes = xml.querySelectorAll('item'); - if (!nodes.length) { - nodes = xml.querySelectorAll('entry'); - } - - var title = getFeedTitle(xml); - var source = sources.findWhere({ - id: sourceID - }); - if (title && (source.get('title') == source.get('url') || !source.get('title'))) { - source.save('title', title); - } - - /** - * TTL check - */ - var ttl = xml.querySelector('channel > ttl, feed > ttl, rss > ttl'); - if (ttl && source.get('lastUpdate') == 0) { - ttl = parseInt(ttl.textContent, 10); - var vals = [300, 600, 1440, 10080]; - if (ttl > 10080) { - source.save({ updateEvery: 10080 }); - } else if (ttl > 180) { - for (var i=0; i title, feed > title, rss > title'); - if (!title || !(title.textContent).trim()) { - title = xml.querySelector('channel > description, feed > description, rss > description'); - } - - if (!title || !(title.textContent).trim()) { - title = xml.querySelector('channel > description, feed > description, rss > description'); - } - - if (!title || !(title.textContent).trim()) { - title = xml.querySelector('channel > link, feed > link, rss > link'); - } - - return title && title.textContent ? title.textContent.trim() || 'rss' : 'rss'; - } - - function replaceUTCAbbr(str) { - str = String(str); - var rep = { - 'CET': '+0100', 'CEST': '+0200', 'EST': '', 'WET': '+0000', 'WEZ': '+0000', 'WEST': '+0100', - 'EEST': '+0300', 'BST': '+0100', 'EET': '+0200', 'IST': '+0100', 'KUYT': '+0400', 'MSD': '+0400', - 'MSK': '+0400', 'SAMT': '+0400' - }; - var reg = new RegExp('(' + Object.keys(rep).join('|') + ')', 'gi'); - return str.replace(reg, function(all, abbr) { - return rep[abbr]; - }); - } - - function rssGetDate(node) { - var pubDate = node.querySelector('pubDate, published'); - if (pubDate) { - return (new Date( replaceUTCAbbr(pubDate.textContent) )).getTime() || 0; - } - - pubDate = node.querySelector('date'); - if (pubDate) { - return (new Date( replaceUTCAbbr(pubDate.textContent) )).getTime() || 0; - } - - pubDate = node.querySelector('lastBuildDate, updated, update'); - - if (pubDate) { - return (new Date( replaceUTCAbbr(pubDate.textContent) )).getTime() || 0; - } - return 0; - } - - function rssGetAuthor(node, title) { - var creator = node.querySelector('creator, author > name'); - if (creator) { - creator = creator.textContent.trim(); - } - - if (!creator) { - creator = node.querySelector('author'); - if (creator) { - creator = creator.textContent.trim(); - } - } - - if (!creator && title && title.length > 0) { - creator = title; - } - - if (creator) { - if (/^\S+@\S+\.\S+\s+\(.+\)$/.test(creator)) { - creator = creator.replace(/^\S+@\S+\.\S+\s+\((.+)\)$/, '$1'); - } - creator = creator.replace(/\s*\(\)\s*$/, ''); - return creator; - } - - return 'no author'; - } - - function rssGetTitle(node) { - return node.querySelector('title') ? node.querySelector('title').textContent : '<no title>'; - } - - function rssGetContent(node) { - var desc = node.querySelector('encoded'); - if (desc) return desc.textContent; - - desc = node.querySelector('description'); - if (desc) return desc.textContent; - - // content over summary because of "http://neregate.com/blog/feed/atom/" - desc = node.querySelector('content'); - if (desc) return desc.textContent; - - desc = node.querySelector('summary'); - if (desc) return desc.textContent; - - return ' '; - } - - - - return { - parse: function() { - return parseRSS.apply(null, arguments); - } - }; -}); \ No newline at end of file diff --git a/scripts/bgprocess/preps/indexeddb.js b/scripts/bgprocess/preps/indexeddb.js deleted file mode 100644 index 69734654..00000000 --- a/scripts/bgprocess/preps/indexeddb.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Prepare IndexedDB stores - * @module BgProcess - * @submodule preps/indexeddb - */ -define(['backbone', 'backboneDB'], function(BB) { - - /** - * IndexedDB preps. - */ - - BB.LocalStorage.prepare = function(db) { - if (!db.objectStoreNames.contains('settings-backbone')) - db.createObjectStore('settings-backbone', { keyPath: 'id' }); - - if (!db.objectStoreNames.contains('items-backbone')) - db.createObjectStore('items-backbone', { keyPath: 'id' }); - - if (!db.objectStoreNames.contains('sources-backbone')) - db.createObjectStore('sources-backbone', { keyPath: 'id' }); - - if (!db.objectStoreNames.contains('folders-backbone')) - db.createObjectStore('folders-backbone', { keyPath: 'id' }); - - if (!db.objectStoreNames.contains('toolbars-backbone')) - db.createObjectStore('toolbars-backbone', { keyPath: 'region' }); - - }; - - /** - * 1 -> 3: Main objects stores and testing - * 3 -> 4: Added toolbars-backbone store - */ - BB.LocalStorage.version = 4; - - return true; -}); \ No newline at end of file diff --git a/scripts/bgprocess/staticdb/defaultToolbarItems.js b/scripts/bgprocess/staticdb/defaultToolbarItems.js deleted file mode 100644 index 8f493b0c..00000000 --- a/scripts/bgprocess/staticdb/defaultToolbarItems.js +++ /dev/null @@ -1,19 +0,0 @@ -define([], function() { - return [ - { - version: 1, - region: 'feeds', - actions: ['feeds:addSource', 'feeds:addFolder', 'feeds:updateAll'] - }, - { - version: 1, - region: 'articles', - actions: ['articles:mark', 'articles:update', 'articles:undelete', 'articles:delete', '!dynamicSpace', 'articles:search'] - }, - { - version: 2, - region: 'content', - actions: ['content:mark', 'content:print', 'articles:download', 'content:delete', '!dynamicSpace', 'global:report', 'content:showConfig'] - } - ]; -}); \ No newline at end of file diff --git a/scripts/domReady.js b/scripts/domReady.js deleted file mode 100644 index 2b541220..00000000 --- a/scripts/domReady.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @license RequireJS domReady 2.0.1 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. - * Available via the MIT or new BSD license. - * see: http://github.com/requirejs/domReady for details - */ -/*jslint */ -/*global require: false, define: false, requirejs: false, - window: false, clearInterval: false, document: false, - self: false, setInterval: false */ - - -define(function () { - 'use strict'; - - var isTop, testDiv, scrollIntervalId, - isBrowser = typeof window !== "undefined" && window.document, - isPageLoaded = !isBrowser, - doc = isBrowser ? document : null, - readyCalls = []; - - function runCallbacks(callbacks) { - var i; - for (i = 0; i < callbacks.length; i += 1) { - callbacks[i](doc); - } - } - - function callReady() { - var callbacks = readyCalls; - - if (isPageLoaded) { - //Call the DOM ready callbacks - if (callbacks.length) { - readyCalls = []; - runCallbacks(callbacks); - } - } - } - - /** - * Sets the page as loaded. - */ - function pageLoaded() { - if (!isPageLoaded) { - isPageLoaded = true; - if (scrollIntervalId) { - clearInterval(scrollIntervalId); - } - - callReady(); - } - } - - if (isBrowser) { - if (document.addEventListener) { - //Standards. Hooray! Assumption here that if standards based, - //it knows about DOMContentLoaded. - document.addEventListener("DOMContentLoaded", pageLoaded, false); - window.addEventListener("load", pageLoaded, false); - } else if (window.attachEvent) { - window.attachEvent("onload", pageLoaded); - - testDiv = document.createElement('div'); - try { - isTop = window.frameElement === null; - } catch (e) {} - - //DOMContentLoaded approximation that uses a doScroll, as found by - //Diego Perini: http://javascript.nwbox.com/IEContentLoaded/, - //but modified by other contributors, including jdalton - if (testDiv.doScroll && isTop && window.external) { - scrollIntervalId = setInterval(function () { - try { - testDiv.doScroll(); - pageLoaded(); - } catch (e) {} - }, 30); - } - } - - //Check if document already complete, and if so, just trigger page load - //listeners. Latest webkit browsers also use "interactive", and - //will fire the onDOMContentLoaded before "interactive" but not after - //entering "interactive" or "complete". More details: - //http://dev.w3.org/html5/spec/the-end.html#the-end - //http://stackoverflow.com/questions/3665561/document-readystate-of-interactive-vs-ondomcontentloaded - //Hmm, this is more complicated on further use, see "firing too early" - //bug: https://github.com/requirejs/domReady/issues/1 - //so removing the || document.readyState === "interactive" test. - //There is still a window.onload binding that should get fired if - //DOMContentLoaded is missed. - if (document.readyState === "complete") { - pageLoaded(); - } - } - - /** START OF PUBLIC API **/ - - /** - * Registers a callback for DOM ready. If DOM is already ready, the - * callback is called immediately. - * @param {Function} callback - */ - function domReady(callback) { - if (isPageLoaded) { - callback(doc); - } else { - readyCalls.push(callback); - } - return domReady; - } - - domReady.version = '2.0.1'; - - /** - * Loader Plugin API method - */ - domReady.load = function (name, req, onLoad, config) { - if (config.isBuild) { - onLoad(null); - } else { - domReady(onLoad); - } - }; - - /** END OF PUBLIC API **/ - - return domReady; -}); diff --git a/scripts/libs/backbone.indexDB.js b/scripts/libs/backbone.indexDB.js deleted file mode 100644 index 0a3d91d3..00000000 --- a/scripts/libs/backbone.indexDB.js +++ /dev/null @@ -1,309 +0,0 @@ -/** - * My own Backbone indexDB Adapter based on localStorage adapter from jeromegn: - * - * Version 1.1.6 - * https://github.com/jeromegn/Backbone.localStorage - */ -(function(root, factory) { - if (typeof exports === 'object' && root.require) { - module.exports = factory(require('underscore'), require('backbone')); - } else if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['underscore', 'backbone'], function(_, Backbone) { - // Use global variables if the locals are undefined. - return factory(_ || root._, Backbone || root.Backbone); - }); - } else { - // RequireJS isn't being used. Assume underscore and backbone are loaded in + + + + diff --git a/src/legal/he.md b/src/legal/he.md new file mode 100644 index 00000000..c869c75f --- /dev/null +++ b/src/legal/he.md @@ -0,0 +1,22 @@ +https://github.com/mathiasbynens/he + +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/legal/readability.md b/src/legal/readability.md new file mode 100644 index 00000000..a78a7c1f --- /dev/null +++ b/src/legal/readability.md @@ -0,0 +1,25 @@ +https://github.com/mozilla/readability + +Library lack any newer licensing information, so I'm copying header from the file + +Please note that the original repository mentioned here is not accessible anymore + + +--- + +Copyright (c) 2010 Arc90 Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +This code is heavily based on Arc90's readability.js (1.7.1) script +available at: http://code.google.com/p/arc90labs-readability diff --git a/src/legal/require.text.md b/src/legal/require.text.md new file mode 100644 index 00000000..8cf6e8ef --- /dev/null +++ b/src/legal/require.text.md @@ -0,0 +1,49 @@ +Extension contains heavily stripped down and minified version of https://github.com/requirejs/text as of version 2.0.16 distributed under MIT license + +The full text of license as of acquisition below: + +Copyright jQuery Foundation and other contributors, https://jquery.org/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/requirejs/text + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules directory, and certain utilities used +to build or test the software in the test and dist directories, are +externally maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 00000000..11c4f199 --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,69 @@ +{ + "name": "Smart RSS", + "developer": { + "name": "zakius" + }, + "description": "RSS Reader", + "manifest_version": 2, + "version": "2.26.0", + "background": { + "page": "index.html" + }, + "permissions": [ + "unlimitedStorage", + "alarms", + "tabs", + "contextMenus", + "", + "*://*/*", + "https://code.fb.com/*", + "https://dev.opera.com/*", + "webRequest", + "webRequestBlocking", + "notifications" + ], + "content_security_policy": "default-src * 'self' data: 'unsafe-inline'; script-src 'self'; style-src * 'self' data: 'unsafe-inline'; img-src * 'self' data:; object-src 'self'", + "browser_action": { + "default_title": "Smart RSS", + "default_icon": { + "19": "images/reload_anim_1.png" + } + }, + "content_scripts": [ + { + "matches": [ + "" + ], + "run_at": "document_end", + "js": [ + "rssDetector/scan.js" + ] + } + ], + "options_ui": { + "page": "options.html", + "open_in_tab": true + }, + "icons": { + "19": "images/icon19-arrow-orange.png", + "48": "images/48-inverted-round.png", + "64": "images/64-inverted-round.png", + "96": "images/96-inverted-round.png", + "128": "images/128-inverted-round.png" + }, + "commands": { + "_execute_browser_action": { + "suggested_key": { + "windows": "Ctrl+Shift+R", + "mac": "Command+Shift+R", + "chromeos": "Ctrl+Shift+R", + "linux": "Ctrl+Shift+R" + } + } + }, + "browser_specific_settings": { + "gecko": { + "id": "smart-rss@mozilla.firefox" + } + } +} \ No newline at end of file diff --git a/src/options.html b/src/options.html new file mode 100644 index 00000000..a97c04a6 --- /dev/null +++ b/src/options.html @@ -0,0 +1,617 @@ + + + + + Smart RSS - Options + + + + + +
+ + +
+ Localization +
+ + + + +
+
+ +
+ Appearance +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Behavior +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ Hotkeys +
+ +
+
+ +
+ Notifications + + + +
+ + + + + + + +
+
+ + +
+ Export/Import +
+

Export

+ +
+ + + +
+
+

Import

+
Warning: Smart RSS will reload after finishing the SMART import, it's expected + behavior +
+ + + + + +
+
+ + +
+ Advanced features. Mostly for troubleshooting purposes +
+

Clear Favicons

+ +
+ +
+

Clear removed articles store

+ +
+ +
+

Clear settings

+ +
+ +
+

Clear data

+ +
+
+ + +
+

Version

+

+

+
+
+ + diff --git a/src/rss.html b/src/rss.html new file mode 100644 index 00000000..c551baf3 --- /dev/null +++ b/src/rss.html @@ -0,0 +1,25 @@ + + + + + Smart RSS + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/src/rssDetector/scan.js b/src/rssDetector/scan.js new file mode 100644 index 00000000..0363c418 --- /dev/null +++ b/src/rssDetector/scan.js @@ -0,0 +1,282 @@ +(function () { + let oldHref = document.location.href; + const feedsData = []; + let scanTimeout = null; + let visibilityTimeout = null; + + function init() { + scan(); + document.addEventListener( + "visibilitychange", + updateAvailableSourcesList, + false + ); + document.addEventListener( + "pagehide", + updateAvailableSourcesList, + false + ); + + if (oldHref.includes("youtube")) { + let bodyList = document.querySelector("body"); + const observer = new MutationObserver(function () { + if (oldHref === document.location.href) { + return; + } + oldHref = document.location.href; + if (scanTimeout) { + clearTimeout(scanTimeout); + } + scanTimeout = setTimeout(scan, 1500); + }); + + const observerConfig = { + childList: true, + subtree: true, + }; + observer.observe(bodyList, observerConfig); + } + } + + function docReady(fn) { + // see if DOM is already available + if ( + document.readyState === "complete" || + document.readyState === "interactive" + ) { + // call on next available tick + setTimeout(fn, 1); + } else { + document.addEventListener("DOMContentLoaded", fn); + } + } + + function updateAvailableSourcesList() { + if (document.hidden) { + browser.runtime.sendMessage({ action: "visibility-lost" }); + return; + } + if (visibilityTimeout) { + clearTimeout(visibilityTimeout); + } + + visibilityTimeout = setTimeout(() => { + browser.runtime.sendMessage({ + action: "list-feeds", + value: feedsData, + }); + }, 500); + } + + function scan() { + feedsData.length = 0; + const address = document.location.href; + if (typeof document.getRootNode !== "undefined") { + let rootNode = document.getRootNode(); + if (rootNode) { + let rootDocumentElement = rootNode.documentElement; + // for chrome + + let d = document.getElementById("webkit-xml-viewer-source-xml"); + + if (d && d.firstChild) { + rootDocumentElement = d.firstChild; + } + + const rootName = rootDocumentElement.nodeName.toLowerCase(); + + let isRSS1 = false; + + if (rootName === "rdf" || rootName === "rdf:rdf") { + if (rootDocumentElement.attributes["xmlns"]) { + isRSS1 = + rootDocumentElement.attributes[ + "xmlns" + ].nodeValue.search("rss") > 0; + } + } + if ( + rootName === "rss" || + rootName === "channel" || // rss2 + rootName === "feed" || // atom + isRSS1 + ) { + feedsData.push({ url: address, title: "This feed" }); + } + } + } + + if (address.match(/^https:\/\/github.com\/.+\/.+/)) { + const base = address.replace( + /(^https:\/\/github.com\/.+\/.+)(\/.+)/, + "$1" + ); + feedsData.push({ + url: base + "/releases.atom", + title: + base.match(/^https:\/\/github.com\/(.+\/.+)/)[1] + + " - Releases", + }); + } + + function findFeedsForYoutubeAddress(address) { + const youtubeFeeds = []; + const userMatch = /c\/(.+)/.exec(address); + let deeperScan = true; + let feedUrl = ""; + if (userMatch) { + feedUrl = + "https://www.youtube.com/feeds/videos.xml?user=" + + userMatch[1]; + deeperScan = false; + youtubeFeeds.push({ url: feedUrl, title: "User feed" }); + } + const channelMatch = /channel\/(.+)/.exec(address); + if (channelMatch) { + feedUrl = + "https://www.youtube.com/feeds/videos.xml?channel_id=" + + channelMatch[1]; + deeperScan = false; + youtubeFeeds.push({ url: feedUrl, title: "Channel feed" }); + } + const playlistMatch = /list=([a-zA-Z\d\-_]+)/.exec(address); + if (playlistMatch) { + feedUrl = + "https://www.youtube.com/feeds/videos.xml?playlist_id=" + + playlistMatch[1]; + youtubeFeeds.push({ + url: feedUrl, + title: "Current playlist feed", + }); + } + return [youtubeFeeds, deeperScan]; + } + + if (address.includes("youtube")) { + const [youtubeFeeds, deeperScan] = + findFeedsForYoutubeAddress(address); + feedsData.push(...youtubeFeeds); + if (deeperScan && address.includes("watch")) { + const channelLink = document.querySelector( + "#upload-info .ytd-channel-name>a" + ); + if (channelLink) { + const href = channelLink.getAttribute("href"); + if (href) { + const linkFeeds = findFeedsForYoutubeAddress(href)[0]; + feedsData.push(...linkFeeds); + } + } + } + + return updateAvailableSourcesList(feedsData); + } + + if (address.includes("bitchute.com")) { + const channelLink = document.querySelector(".owner>a"); + if (channelLink) { + const channelName = channelLink.textContent; + const href = + "https://www.bitchute.com/feeds/rss/channel/" + channelName; + feedsData.push({ url: href, title: "Channel feed" }); + } + + return updateAvailableSourcesList(feedsData); + } + + if (address.includes("odysee.com")) { + const currentUrl = window.location.href; + const channelNameMatch = /@(.+?):/.exec(currentUrl); + if (channelNameMatch) { + const channelName = channelNameMatch[1]; + + const href = + "https://lbryfeed.melroy.org/channel/" + channelName; + feedsData.push({ url: href, title: "Channel feed" }); + } + + return updateAvailableSourcesList(feedsData); + } + + if (address.includes("vimeo.com")) { + const currentUrl = window.location.href; + const channelNameMatch = /vimeo\.com\/(.+)/.exec(currentUrl); + if (channelNameMatch) { + const potentialChannelName = channelNameMatch[1]; + const match2 = /([a-zA-Z]+?)/.exec(potentialChannelName); + let channelName = ""; + if (match2) { + channelName = potentialChannelName; + } else { + const channelLink = + document.querySelector("a.js-user-link"); + if (channelLink) { + channelName = channelLink.href.replace("/", ""); + } + } + if (!channelName) { + return; + } + const href = + "https://vimeo.com/" + channelName + "/videos/rss/"; + feedsData.push({ url: href, title: "Channel feed" }); + } + + return updateAvailableSourcesList(); + } + + if (address.includes("steemit.com")) { + const currentUrl = window.location.href; + const channelNameMatch = /steemit\.com\/(.+)/.exec(currentUrl); + if (channelNameMatch) { + const channelName = channelNameMatch[1]; + const href = "https://www.hiverss.com/" + channelName + "/feed"; + feedsData.push({ url: href, title: "Channel feed" }); + } + return updateAvailableSourcesList(); + } + + if (address.includes("hive.blog")) { + const currentUrl = window.location.href; + const channelNameMatch = /hive\.blog\/(.+)/.exec(currentUrl); + if (channelNameMatch) { + const channelName = channelNameMatch[1]; + const href = "https://www.hiverss.com/" + channelName + "/feed"; + feedsData.push({ url: href, title: "Channel feed" }); + } + return updateAvailableSourcesList(); + } + + const selector = + 'link[type="application/rss+xml"], link[type="application/atom+xml"]'; + + feedsData.push( + ...[...document.querySelectorAll(selector)].map((feed) => { + return { url: feed.href, title: feed.title || feed.href }; + }) + ); + + // no feeds found yet, try to bruteforce something + if (feedsData.length === 0) { + const generator = document.querySelector('meta[name="generator"]'); + + if ( + generator && + generator.getAttribute("content").includes("WordPress") + ) { + const url = document.URL; + + const feedUrl = + url.charAt(url.length - 1) === "/" + ? url + "feed" + : url + "/feed"; + + feedsData.push({ url: feedUrl, title: feedUrl }); + } + } + + updateAvailableSourcesList(feedsData); + } + + docReady(init); +})(); diff --git a/src/rss_content.html b/src/rss_content.html new file mode 100644 index 00000000..210ea2b6 --- /dev/null +++ b/src/rss_content.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + +
+ + + + diff --git a/src/scripts/app/app.js b/src/scripts/app/app.js new file mode 100644 index 00000000..06e9d896 --- /dev/null +++ b/src/scripts/app/app.js @@ -0,0 +1,248 @@ +/** + * @module App + */ +define([ + "controllers/comm", + "layouts/Layout", + "collections/Actions", + "layouts/FeedsLayout", + "layouts/ArticlesLayout", + "layouts/ContentLayout", + "staticdb/shortcuts", +], function ( + comm, + Layout, + Actions, + FeedsLayout, + ArticlesLayout, + ContentLayout, + shortcuts +) { + document.documentElement.style.fontSize = + bg.settings.get("uiFontSize") + "%"; + + document.addEventListener("contextmenu", function (event) { + if ( + !event.target.matches("#content header, #content header *, input") + ) { + event.preventDefault(); + } + }); + + browser.runtime.onMessage.addListener(onMessage); + + function changeUserStyle() { + const userStyle = bg.settings.get("userStyle"); + document.querySelector("[data-custom-style]").textContent = userStyle; + const frame = document.querySelector('[name="sandbox"]'); + if (!frame) { + return; + } + const customStyleTag = frame.contentDocument.querySelector( + "[data-custom-style]" + ); + if (!customStyleTag) { + return; + } + customStyleTag.textContent = userStyle; + } + + function changeInvertColors() { + const shouldInvertColors = bg.getBoolean("invertColors"); + const body = document.querySelector("body"); + if (shouldInvertColors) { + body.classList.add("dark-theme"); + } else { + body.classList.remove("dark-theme"); + } + const frame = document.querySelector('[name="sandbox"]'); + if (!frame) { + return; + } + const frameBody = frame.contentDocument.querySelector("body"); + if (!frameBody) { + return; + } + if (shouldInvertColors) { + frameBody.classList.add("dark-theme"); + } else { + frameBody.classList.remove("dark-theme"); + } + } + + function onMessage(message) { + if (message.action === "changeUserStyle") { + changeUserStyle(); + } + if (message.action === "changeInvertColors") { + changeInvertColors(); + } + } + + function applyStylesToSandbox() { + const baseStylePath = browser.runtime.getURL("styles/main.css"); + + const frame = document.querySelector('[name="sandbox"]'); + if (!frame) { + return; + } + + const baseStyleTag = + frame.contentDocument.querySelector("[data-base-style]"); + if (baseStyleTag) { + baseStyleTag.setAttribute("href", baseStylePath); + } + + const darkStylePath = browser.runtime.getURL("styles/dark.css"); + const darkStyleTag = + frame.contentDocument.querySelector("[data-dark-style]"); + if (darkStyleTag) { + darkStyleTag.setAttribute("href", darkStylePath); + } + } + + const app = (window.app = new (Layout.extend({ + el: "body", + fixURL: function (url) { + return url.search(/[a-z]+:\/\//) === -1 ? "https://" + url : url; + }, + events: { + mousedown: "handleMouseDown", + }, + initialize: function () { + this.actions = new Actions(); + + window.addEventListener("blur", (event) => { + this.hideContextMenus(); + if (event.target instanceof window.Window) { + comm.trigger("stop-blur"); + } + }); + + bg.settings.on("change:layout", this.handleLayoutChange, this); + bg.sources.on("clear-events", this.handleClearEvents, this); + }, + handleClearEvents: function (id) { + if (!window || id === tabID) { + bg.settings.off("change:layout", this.handleLayoutChange, this); + bg.sources.off("clear-events", this.handleClearEvents, this); + } + }, + handleLayoutChange: function () { + if (bg.settings.get("layout") === "vertical") { + this.layoutToVertical(); + this.articles.enableResizing( + bg.settings.get("layout"), + bg.settings.get("posC") + ); + } else { + this.layoutToHorizontal(); + this.articles.enableResizing( + bg.settings.get("layout"), + bg.settings.get("posB") + ); + } + }, + layoutToVertical: function () { + document.querySelector("#second-pane").classList.add("vertical"); + }, + layoutToHorizontal: function () { + document.querySelector("#second-pane").classList.remove("vertical"); + }, + + handleMouseDown: function (event) { + if (!event.target.matches(".context-menu, .context-menu *")) { + this.hideContextMenus(); + } + }, + hideContextMenus: function () { + comm.trigger("hide-overlays", { blur: true }); + }, + start: function () { + const isLoaded = () => { + if (bg.loaded) { + this.attach("feeds", new FeedsLayout()); + this.attach("articles", new ArticlesLayout()); + this.attach("content", new ContentLayout()); + + this.feeds.enableResizing( + "horizontal", + bg.settings.get("posA") + ); + this.articles.enableResizing( + "horizontal", + bg.settings.get("posB") + ); + applyStylesToSandbox(); + changeUserStyle(); + changeInvertColors(); + this.handleLayoutChange(); + document.querySelector("body").classList.remove("loading"); + } else { + setTimeout(isLoaded, 150); + } + }; + isLoaded(); + }, + handleKeyDown: (event) => { + const activeElement = document.activeElement; + const hotkeys = bg.settings.get("hotkeys"); + + if ( + activeElement && + (activeElement.tagName === "INPUT" || + activeElement.tagName === "TEXTAREA") + ) { + return; + } + + let shortcut = ""; + if (event.ctrlKey) { + shortcut += "ctrl+"; + } + if (event.altKey) { + shortcut += "alt+"; + } + if (event.shiftKey) { + shortcut += "shift+"; + } + + if (event.keyCode > 46 && event.keyCode < 91) { + shortcut += String.fromCharCode(event.keyCode).toLowerCase(); + } else if (event.keyCode in shortcuts.keys) { + shortcut += shortcuts.keys[event.keyCode]; + } else { + return; + } + + const activeRegion = activeElement.closest(".region"); + const activeRegionName = activeRegion ? activeRegion.id : null; + + if (activeRegionName && activeRegionName in hotkeys) { + if (shortcut in hotkeys[activeRegionName]) { + app.actions.execute( + hotkeys[activeRegionName][shortcut], + event + ); + event.preventDefault(); + return false; + } + } + + if (shortcut in hotkeys.global) { + app.actions.execute(hotkeys.global[shortcut], event); + event.preventDefault(); + return false; + } + }, + }))()); + if (typeof browser !== "undefined") { + window.addEventListener("unload", () => { + bg.reloadExt(); + }); + } + + document.addEventListener("keydown", app.handleKeyDown); + + return app; +}); diff --git a/src/scripts/app/collections/Actions.js b/src/scripts/app/collections/Actions.js new file mode 100644 index 00000000..fb8f555a --- /dev/null +++ b/src/scripts/app/collections/Actions.js @@ -0,0 +1,51 @@ +/** + * @module App + * @submodule collections/Actions + */ +define(function (require) { + const BB = require('backbone'); + const Action = require('models/Action'); + const actions = require('staticdb/actions'); + + /** + * Collection of executable actions. Actions are usually executed by shortcuts, buttons or context menus. + * @class Actions + * @constructor + * @extends Backbone.Collection + */ + const Actions = BB.Collection.extend({ + model: Action, + + /** + * @method initialize + */ + initialize: function () { + Object.keys(actions).forEach((region) => { + Object.keys(actions[region]).forEach((name) => { + const c = actions[region][name]; + this.add({name: region + ':' + name, fn: c.fn, icon: c.icon, title: c.title, state: c.state, glyph: c.glyph}); + }); + }); + }, + + /** + * Executes given action + * @method execute + * @param action {string|models/Action} + */ + execute: function (action) { + if (typeof action === 'string') { + action = this.get(action); + } + if (!action) { + return false; + } + const args = [].slice.call(arguments); + args.shift(); + action.get('fn').apply(app, args); + return true; + } + }); + + return Actions; +}); diff --git a/src/scripts/app/collections/Groups.js b/src/scripts/app/collections/Groups.js new file mode 100644 index 00000000..3254ea5a --- /dev/null +++ b/src/scripts/app/collections/Groups.js @@ -0,0 +1,20 @@ +/** + * @module App + * @submodule collections/Groups + */ +define(function (require) { + const BB = require('backbone'); + const Group = require('models/Group'); + + /** + * Collection of date groups + * @class Groups + * @constructor + * @extends Backbone.Collection + */ + const Groups = BB.Collection.extend({ + model: Group + }); + + return Groups; +}); diff --git a/src/scripts/app/collections/MenuCollection.js b/src/scripts/app/collections/MenuCollection.js new file mode 100644 index 00000000..9db78306 --- /dev/null +++ b/src/scripts/app/collections/MenuCollection.js @@ -0,0 +1,19 @@ +/** + * @module App + * @submodule collections/MenuCollection + */ +define(function (require) { + const BB = require('backbone'); + const MenuItem = require('models/MenuItem'); + /** + * Each ContextMenu has its own MenuCollection instance + * @class MenuCollection + * @constructor + * @extends Backbone.Collection + */ + const MenuCollection = BB.Collection.extend({ + model: MenuItem + }); + + return MenuCollection; +}); diff --git a/src/scripts/app/collections/ToolbarItems.js b/src/scripts/app/collections/ToolbarItems.js new file mode 100644 index 00000000..7d39bd40 --- /dev/null +++ b/src/scripts/app/collections/ToolbarItems.js @@ -0,0 +1,29 @@ +/** + * @module App + * @submodule collections/ToolbarItems + */ +define(function (require) { + const BB = require('backbone'); + const ToolbarButton = require('models/ToolbarButton'); + /** + * Each ToolbarView has its own ToolbarItems instance + * @class ToolbarItems + * @constructor + * @extends Backbone.Collection + */ + const ToolbarItems = BB.Collection.extend({ + model: ToolbarButton, + + comparator: function (firstItem, secondItem) { + if (!firstItem.view || !secondItem.view) { + return 0; + } + const firstRectangle = firstItem.view.el.getBoundingClientRect(); + const secondRectangle = secondItem.view.el.getBoundingClientRect(); + + return firstRectangle.left > secondRectangle.left ? 1 : -1; + } + }); + + return ToolbarItems; +}); diff --git a/scripts/app/controllers/comm.js b/src/scripts/app/controllers/comm.js similarity index 52% rename from scripts/app/controllers/comm.js rename to src/scripts/app/controllers/comm.js index 748b5b1c..fb4e1b08 100644 --- a/scripts/app/controllers/comm.js +++ b/src/scripts/app/controllers/comm.js @@ -1,8 +1,9 @@ -/** - * Application messages. Returns instance of Backbone.Events. - * @module App - * @submodule controllers/comm - */ -define(['backbone'], function(BB) { - return Object.create(BB.Events); -}); \ No newline at end of file +/** + * Application messages. Returns instance of Backbone.Events. + * @module App + * @submodule controllers/comm + */ +define(function (require) { + const BB = require('backbone'); + return Object.create(BB.Events); +}); diff --git a/src/scripts/app/factories/ToolbarItemsFactory.js b/src/scripts/app/factories/ToolbarItemsFactory.js new file mode 100644 index 00000000..0132e534 --- /dev/null +++ b/src/scripts/app/factories/ToolbarItemsFactory.js @@ -0,0 +1,29 @@ +/** + * Factory for making instances of toolbar items + * @module App + * @submodule factories/ToolbarItemsFactory + */ +define(function (require) { + const ToolbarButtonView = require('views/ToolbarButtonView'); + const ToolbarDynamicSpaceView = require('views/ToolbarDynamicSpaceView'); + const ToolbarSearchView = require('views/ToolbarSearchView'); + + return { + /** + * Returns instance of toolbar item + * @method create + * @param name {string} + * @param itemModel {Object} + * @returns ToolbarDynamicSpaceView|ToolbarSearchView|ToolbarButtonView + */ + create: function (name, itemModel) { + if (name === 'dynamicSpace') { + return new ToolbarDynamicSpaceView({model: itemModel}); + } else if (name === 'search') { + return new ToolbarSearchView({model: itemModel}); + } else { + return new ToolbarButtonView({model: itemModel}); + } + } + }; +}); diff --git a/src/scripts/app/helpers/dateUtils.js b/src/scripts/app/helpers/dateUtils.js new file mode 100644 index 00000000..98c9f886 --- /dev/null +++ b/src/scripts/app/helpers/dateUtils.js @@ -0,0 +1,115 @@ +define(function () { + const millisecondsInDay = 86400000; + const unixUtcOffset = (new Date).getTimezoneOffset() * 60000; + const zeroPad = function (num) { + if (num < 10) { + num = '0' + num; + } + return num; + }; + const toTwelveHoursFormat = function (hours) { + return hours % 12; + }; + + // All methods accept correct values for single argument variant of Date constructor + // that is: Date object, JS timestamp (number of milliseconds since epoch) or date string + // usage of the last option is discouraged due to inconsistent handling in different browsers + return { + unixutc: function (date) { + const _date = new Date(date); + return _date.getTime() - unixUtcOffset; + }, + getWeekOfYear: function (date) { + const _date = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + const dayNum = _date.getUTCDay() || 7; + _date.setUTCDate(_date.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(_date.getUTCFullYear(),0,1)); + return Math.ceil((((_date - yearStart) / millisecondsInDay) + 1)/7); + }, + getDayOfYear: function (date) { + const _date = new Date(date); + _date.setHours(0, 0, 0); + const start = new Date(_date.getFullYear(), 0, 0); + const diff = (_date - start) + ((start.getTimezoneOffset() - _date.getTimezoneOffset()) * 60 * 1000); + const oneDay = 1000 * 60 * 60 * 24; + return Math.floor(diff / oneDay); + }, + getDaysSinceEpoch: function (date) { + const _date = new Date(date); + return Math.floor(this.unixutc(_date) / millisecondsInDay); + }, + startOfWeek: function (date, firstDayOfWeekIndex = 1) { + const dayOfWeek = date.getDay(); + const _date = new Date(date); + const diff = dayOfWeek >= firstDayOfWeekIndex ? dayOfWeek - firstDayOfWeekIndex : 6 - dayOfWeek; + + _date.setDate(date.getDate() - diff); + return new Date(this.startOfDay(_date)); + }, + startOfDay: function (date) { + return new Date(date).setHours(0, 0, 0, 0); + }, + startOfMonth: function (date) { + return new Date(this.startOfDay(date)).setDate(1); + }, + addDays: function (date, days = 1) { + const _date = new Date(date); + _date.setDate(_date.getDate() + days); + return _date; + }, + formatDate: function (date, template) { + const _date = new Date(date); + const dateVal = (all, found) => { + switch (found) { + case 'DD': + return zeroPad(_date.getDate()); + case 'D': + return _date.getDate(); + case 'MM': + return zeroPad(_date.getMonth() + 1); + case 'M': + return _date.getMonth() + 1; + case 'YYYY': + return _date.getFullYear(); + case 'YY': + return _date.getFullYear().toString().substr(2, 2); + case 'hh': + return zeroPad(_date.getHours()); + case 'h': + return _date.getHours(); + case 'HH': + return zeroPad(toTwelveHoursFormat(_date.getHours())); + case 'H': + return toTwelveHoursFormat(_date.getHours()); + case 'mm': + return zeroPad(_date.getMinutes()); + case 'm': + return _date.getMinutes(); + case 'ss': + return zeroPad(_date.getSeconds()); + case 's': + return _date.getSeconds(); + case 'u': + return _date.getMilliseconds(); + case 'U': + return _date.getTime(); + case 'T': + return _date.getTime() - _date.getTimezoneOffset() * 60000; + case 'W': + return _date.getDay(); + case 'y': + return this.getDayOfYear(_date); + case 'w': + return this.getWeekOfYear(_date); + case 'G': + return _date.getTimezoneOffset(); + case 'a': + return _date.getHours() > 12 ? 'PM' : 'AM'; + default: + return ''; + } + }; + return template.replace(/(DD|D|MM|M|YYYY|YY|hh|h|HH|H|mm|m|ss|s|u|U|W|y|w|G|a|T)/g, dateVal); + } + }; +}); diff --git a/src/scripts/app/helpers/escapeHtml.js b/src/scripts/app/helpers/escapeHtml.js new file mode 100644 index 00000000..52593683 --- /dev/null +++ b/src/scripts/app/helpers/escapeHtml.js @@ -0,0 +1,29 @@ +/** + * Escapes following characters: &, <, >, ", ' + * @module App + * @submodule helpers/escapeHtml + * @param string {String} String with html to be escaped + */ +define(function () { + const entityMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + '\'': ''', + '/': '/' + }; + + return function (str) { + str = String(str).replace(/[&<>"']/gm, function (s) { + return entityMap[s]; + }); + str = str.replace(/\s/, function (f) { + if (f === ' ') { + return ' '; + } + return ''; + }); + return str; + }; +}); diff --git a/scripts/app/helpers/stripTags.js b/src/scripts/app/helpers/stripTags.js similarity index 52% rename from scripts/app/helpers/stripTags.js rename to src/scripts/app/helpers/stripTags.js index 0e276618..6315acb0 100644 --- a/scripts/app/helpers/stripTags.js +++ b/src/scripts/app/helpers/stripTags.js @@ -1,16 +1,15 @@ -/** - * This method should remove all html tags. - * Copied form underscore.string repo - * @module App - * @submodule helpers/stripTags - * @param string {String} String with html to be removed - */ -define([], function() { - - var stripTags = function(str) { - if (str == null) return ''; - return String(str).replace(/<\/?[^>]+>/g, ''); - }; - - return stripTags; -}); \ No newline at end of file +/** + * This method should remove all html tags. + * Copied form underscore.string repo + * @module App + * @submodule helpers/stripTags + * @param string {String} String with html to be removed + */ +define(function () { + return function (str) { + if (!str) { + return ''; + } + return String(str).replace(/<\/?[^>]+>/g, ''); + }; +}); diff --git a/src/scripts/app/instances/contextMenus.js b/src/scripts/app/instances/contextMenus.js new file mode 100644 index 00000000..aff5bf87 --- /dev/null +++ b/src/scripts/app/instances/contextMenus.js @@ -0,0 +1,229 @@ +define(function (require) { + const BB = require('backbone'); + const ContextMenu = require('views/ContextMenu'); + const Locale = require('modules/Locale'); + + const sourceContextMenu = new ContextMenu([ + { + title: Locale.UPDATE, + icon: 'reload.png', + action: function () { + app.actions.execute('feeds:update'); + } + }, + { + title: Locale.MARK_ALL_AS_READ, + icon: 'read.png', + action: function () { + app.actions.execute('feeds:mark'); + } + }, + { + title: Locale.DELETE, + icon: 'delete.png', + action: function () { + app.actions.execute('feeds:delete'); + } + }, + { + title: Locale.REFETCH, /**** Localization needed****/ + icon: 'save.png', + action: function () { + app.actions.execute('feeds:refetch'); + } + }, + { + title: Locale.OPENHOME, + action: function () { + app.actions.execute('feeds:openHome'); + } + }, + { + title: Locale.PROPERTIES, + icon: 'properties.png', + action: function () { + app.actions.execute('feeds:showProperties'); + } + } + ]); + + const trashContextMenu = new ContextMenu([ + { + title: Locale.MARK_ALL_AS_READ, + icon: 'read.png', + action: function () { + bg.items.where({trashed: true, deleted: false}).forEach(function (item) { + if (item.get('unread') === true) { + item.save({ + unread: false, + visited: true + }); + } + }); + } + }, + { + title: Locale.EMPTY_TRASH, + icon: 'delete.png', + action: function () { + if (confirm(Locale.REALLY_EMPTY_TRASH)) { + bg.items.where({trashed: true, deleted: false}).forEach(function (item) { + item.markAsDeleted(); + }); + } + } + } + ]); + + const allFeedsContextMenu = new ContextMenu([ + { + title: Locale.UPDATE_ALL, + icon: 'reload.png', + action: function () { + app.actions.execute('feeds:updateAll'); + } + }, + { + title: Locale.MARK_ALL_AS_READ, + icon: 'read.png', + action: function () { + if (confirm(Locale.MARK_ALL_QUESTION)) { + bg.items.forEach(function (item) { + item.save({unread: false, visited: true}); + }); + } + } + }, + { + title: Locale.DELETE_ALL_ARTICLES, + icon: 'delete.png', + action: function () { + if (confirm(Locale.DELETE_ALL_Q)) { + bg.items.forEach(function (item) { + if (item.get('deleted') === true) { + return; + } + item.markAsDeleted(); + }); + } + } + } + ]); + + const folderContextMenu = new ContextMenu([ + { + title: Locale.UPDATE, + icon: 'reload.png', + action: function () { + app.actions.execute('feeds:update'); + } + }, + { + title: Locale.MARK_ALL_AS_READ, + icon: 'read.png', + action: function () { + app.actions.execute('feeds:mark'); + } + }, + { + title: Locale.DELETE, + icon: 'delete.png', + action: function () { + app.actions.execute('feeds:delete'); + } + }, + { + title: Locale.PROPERTIES, + icon: 'properties.png', + action: function () { + app.actions.execute('feeds:showProperties'); + } + } + ]); + + const itemsContextMenu = new ContextMenu([ + { + title: Locale.NEXT_UNREAD + ' (H)', + icon: 'forward.png', + action: function () { + app.actions.execute('articles:nextUnread'); + } + }, + { + title: Locale.PREV_UNREAD + ' (Y)', + icon: 'back.png', + action: function () { + app.actions.execute('articles:prevUnread'); + } + }, + { + title: Locale.MARK_AS_READ + ' (K)', + icon: 'read.png', + action: function () { + app.actions.execute('articles:mark'); + } + }, + { + title: Locale.MARK_AND_NEXT_UNREAD + ' (G)', + icon: 'find_next.png', + action: function () { + app.actions.execute('articles:markAndNextUnread'); + } + }, + { + title: Locale.MARK_AND_PREV_UNREAD + ' (T)', + icon: 'find_previous.png', + action: function () { + app.actions.execute('articles:markAndPrevUnread'); + } + }, + { + title: Locale.FULL_ARTICLE, + icon: 'full_article.png', + action: function (e) { + app.actions.execute('articles:fullArticle', e); + } + }, + { + title: Locale.PIN + ' (P)', + icon: 'pinsource_context.png', + action: function () { + app.actions.execute('articles:pin'); + } + }, + { + title: Locale.DELETE + ' (D)', + icon: 'delete.png', + action: function (e) { + app.actions.execute('articles:delete', e); + } + }, + { + title: Locale.UNDELETE + ' (N)', + id: 'context-undelete', + icon: 'undelete.png', + action: function () { + app.actions.execute('articles:undelete'); + } + } + ]); + + return new (BB.View.extend({ + list: {}, + initialize: function () { + this.list = { + source: sourceContextMenu, + trash: trashContextMenu, + folder: folderContextMenu, + allFeeds: allFeedsContextMenu, + items: itemsContextMenu + }; + }, + get: function (name) { + if (name in this.list) { + return this.list[name]; + } + return null; + } + })); +}); diff --git a/src/scripts/app/instances/specials.js b/src/scripts/app/instances/specials.js new file mode 100644 index 00000000..35f69d31 --- /dev/null +++ b/src/scripts/app/instances/specials.js @@ -0,0 +1,34 @@ +define(function (require) { + const Special = require('models/Special'); + const contextMenus = require('instances/contextMenus'); + const Locale = require('modules/Locale'); + return { + trash: new Special({ + title: Locale.TRASH, + icon: 'trashsource.png', + filter: {trashed: true, deleted: false}, + position: 'bottom', + name: 'trash', + onReady: function () { + this.contextMenu = contextMenus.get('trash'); + } + }), + allFeeds: new Special({ + title: Locale.ALL_FEEDS, + icon: 'icon16_v2.png', + filter: {trashed: false}, + position: 'top', + name: 'all-feeds', + onReady: function () { + this.contextMenu = contextMenus.get('allFeeds'); + } + }), + pinned: new Special({ + title: Locale.PINNED, + icon: 'pinsource.png', + filter: {trashed: false, pinned: true}, + position: 'bottom', + name: 'pinned' + }) + }; +}); diff --git a/src/scripts/app/layouts/ArticlesLayout.js b/src/scripts/app/layouts/ArticlesLayout.js new file mode 100644 index 00000000..d49e3492 --- /dev/null +++ b/src/scripts/app/layouts/ArticlesLayout.js @@ -0,0 +1,55 @@ +/** + * @module App + * @submodule layouts/ArticlesLayout + */ +define([ + 'layouts/Layout', 'views/ToolbarView', 'views/articleList', + 'mixins/resizable' + ], + function (Layout, ToolbarView, articleList, resizable) { + + const toolbar = bg.toolbars.findWhere({region: 'articles'}); + + /** + * Articles layout view + * @class ArticlesLayout + * @constructor + * @extends Layout + */ + let ArticlesLayout = Layout.extend({ + el: '#articles', + events: { + 'mousedown': 'handleMouseDown' + }, + initialize: function () { + this.el.view = this; + + this.on('attach', function () { + this.attach('toolbar', new ToolbarView({model: toolbar})); + this.attach('articleList', articleList); + }); + + this.on('resize:after', this.handleResizeAfter); + }, + + /** + * Saves the new layout size + * @triggered after resize + * @method handleResizeAfter + */ + handleResizeAfter: function () { + + if (bg.settings.get('layout') === 'horizontal') { + const width = this.el.offsetWidth; + bg.settings.save({posB: width}); + } else { + const height = this.el.offsetHeight; + bg.settings.save({posC: height}); + } + } + }); + + ArticlesLayout = ArticlesLayout.extend(resizable); + + return ArticlesLayout; + }); diff --git a/src/scripts/app/layouts/ContentLayout.js b/src/scripts/app/layouts/ContentLayout.js new file mode 100644 index 00000000..67079d0b --- /dev/null +++ b/src/scripts/app/layouts/ContentLayout.js @@ -0,0 +1,42 @@ +/** + * @module App + * @submodule layouts/ContentLayout + */ +define([ + 'layouts/Layout', 'views/ToolbarView', 'views/contentView', 'views/SandboxView' + ], + function (Layout, ToolbarView, contentView, SandboxView) { + + const toolbar = bg.toolbars.findWhere({region: 'content'}); + + /** + * Content layout view + * @class ContentLayout + * @constructor + * @extends Layout + */ + let ContentLayout = Layout.extend({ + + /** + * View element + * @property el + * @default #content + * @type HTMLElement + */ + el: '#content', + + /** + * @method initialize + */ + initialize: function () { + this.on('attach', function () { + + this.attach('toolbar', new ToolbarView({model: toolbar})); + this.attach('content', contentView); + this.attach('sandbox', new SandboxView()); + }); + } + }); + + return ContentLayout; + }); diff --git a/src/scripts/app/layouts/FeedsLayout.js b/src/scripts/app/layouts/FeedsLayout.js new file mode 100644 index 00000000..03931671 --- /dev/null +++ b/src/scripts/app/layouts/FeedsLayout.js @@ -0,0 +1,61 @@ +/** + * @module App + * @submodule layouts/FeedsLayout + */ +define([ + 'layouts/Layout', 'views/ToolbarView', 'views/feedList', + 'instances/contextMenus', 'views/Properties', 'mixins/resizable', 'views/IndicatorView' + ], + function (Layout, ToolbarView, feedList, contextMenus, Properties, resizable, IndicatorView) { + + const toolbar = bg.toolbars.findWhere({region: 'feeds'}); + + /** + * Feeds layout view + * @class FeedsLayout + * @constructor + * @extends Layout + */ + let FeedsLayout = Layout.extend({ + /** + * View element + * @property el + * @default #feeds + * @type HTMLElement + */ + el: '#feeds', + + /** + * @method initialize + */ + initialize: function () { + + this.on('attach', function () { + this.attach('toolbar', new ToolbarView({model: toolbar})); + this.attach('properties', new Properties); + this.attach('feedList', feedList); + this.attach('indicator', new IndicatorView); + }); + + this.el.view = this; + + this.on('resize:after', this.handleResize); + window.addEventListener('resize', this.handleResize.bind(this)); + + this.enableResizing('horizontal', bg.settings.get('posA')); + }, + + /** + * Saves layout size + * @method handleResize + */ + handleResize: function () { + const width = this.el.offsetWidth; + bg.settings.save({posA: width}); + } + }); + + FeedsLayout = FeedsLayout.extend(resizable); + + return FeedsLayout; + }); diff --git a/src/scripts/app/layouts/Layout.js b/src/scripts/app/layouts/Layout.js new file mode 100644 index 00000000..413c10dc --- /dev/null +++ b/src/scripts/app/layouts/Layout.js @@ -0,0 +1,74 @@ +/** + * @module App + * @submodule layouts/Layout + */ +define(function (require) { + const BB = require('backbone'); + + /** + * Layout abstract class + * @class Layout + * @constructor + * @extends Backbone.View + */ + let Layout = BB.View.extend({ + + /** + * Gives focus to layout region element + * @method setFocus + * @param name {String} Name of the region + */ + setFocus: function (name) { + if (!name || !this[name]) { + return; + } + const articles = document.querySelector('#articles'); + if (articles) { + articles.classList.remove('focused'); + } + const feeds = document.querySelector('#feeds'); + if (feeds) { + feeds.classList.remove('focused'); + } + const content = document.querySelector('#content'); + if (content) { + content.classList.remove('focused'); + } + const x = document.querySelector('#' + name); + if (x) { + x.classList.add('focused'); + } + + this[name].el.focus(); + }, + + /** + * Appends new region to layout. + * If existing name is used, the old region is replaced with the new region + * and 'close' event is triggered on the old region + * @method attach + * @param name {String} Name of the region + * @param view {Backbone.View} Backbone view to be the attached region + */ + attach: function (name, view) { + const old = this[name]; + + this[name] = view; + if (!view.el.parentNode) { + if (old && old instanceof BB.View) { + old.el.insertAdjacentElement('beforebegin', view.el); + old.el.parentElement.removeChild(old.el); + old.trigger('close'); + } else { + this.el.insertAdjacentElement('beforeend', view.el); + } + } + view.trigger('attach'); + if (!this.focus) { + this.setFocus(name); + } + } + }); + + return Layout; +}); diff --git a/src/scripts/app/mixins/resizable.js b/src/scripts/app/mixins/resizable.js new file mode 100644 index 00000000..21bf96dc --- /dev/null +++ b/src/scripts/app/mixins/resizable.js @@ -0,0 +1,125 @@ +define(function () { + + let els = []; + + const resizeWidth = 6; + + function handleMouseDown(event) { + this.resizing = true; + event.preventDefault(); + [...document.querySelectorAll('iframe')].forEach((iframe) => { + iframe.style.pointerEvents = 'none'; + }); + + this.trigger('resize:before'); + } + + function handleMouseMove(event) { + if (this.resizing) { + const toLeft = 1; + event.preventDefault(); + if (this.layout === 'vertical') { + setPosition.call(this, event.clientY); + this.el.style.flexBasis = Math.abs(event.clientY - this.el.offsetTop + toLeft) + 'px'; + } else { + setPosition.call(this, event.clientX); + this.el.style.flexBasis = Math.abs(event.clientX - this.el.offsetLeft + toLeft) + 'px'; + } + this.trigger('resize'); + } + } + + function handleMouseUp() { + if (!this.resizing) { + return; + } + [...document.querySelectorAll('iframe')].forEach((iframe) => { + iframe.style.pointerEvents = 'auto'; + }); + this.resizing = false; + els.forEach((el) => { + loadPosition.call(el); + }); + + this.trigger('resize:after'); + } + + function setPosition(pos) { + const toLeft = 1; + if (this.layout === 'vertical') { + this.resizer.style.width = this.el.innerWidth + 'px'; + this.resizer.style.left = this.el.offsetLeft + 'px'; + this.resizer.style.top = pos - Math.round(resizeWidth / 2) - toLeft + 'px'; + } else { + this.resizer.style.top = this.el.offsetTop + 'px'; + this.resizer.style.left = pos - Math.round(resizeWidth / 2) - toLeft + 'px'; + } + } + + function loadPosition(resetting) { + if (!this.resizer) { + return; + } + + if (this.layout === 'vertical') { + setPosition.call(this, this.el.offsetTop + this.el.offsetHeight); + } else { + setPosition.call(this, this.el.offsetLeft + this.el.offsetWidth); + } + + if (!resetting) { + resetPositions.call(this); + } + } + + function resetPositions() { + requestAnimationFrame(() => { + els.forEach((el) => { + if (el === this) { + return; + } + loadPosition.call(el, true); + }); + }); + } + + return { + resizing: false, + resizer: null, + layout: 'horizontal', + enableResizing: function (layout, size) { + layout = this.layout = layout || 'horizontal'; + + if (size) { + this.el.style.flexBasis = size + 'px'; + } + + els.push(this); + + if (!this.resizer) { + this.resizer = document.createElement('div'); + this.resizer.className = 'resizer'; + } + + if (layout === 'vertical') { + this.resizer.style.width = '100%'; + this.resizer.style.cursor = 'n-resize'; + this.resizer.style.height = resizeWidth + 'px'; + } else { + this.resizer.style.height = '100%'; + this.resizer.style.cursor = 'w-resize'; + this.resizer.style.width = resizeWidth + 'px'; + } + + + loadPosition.call(this); + + this.resizer.addEventListener('mousedown', handleMouseDown.bind(this)); + document.addEventListener('mousemove', handleMouseMove.bind(this)); + document.addEventListener('mouseup', handleMouseUp.bind(this)); + window.addEventListener('resize', resetPositions.bind(this)); + + document.body.appendChild(this.resizer); + } + }; +}); diff --git a/src/scripts/app/mixins/selectable.js b/src/scripts/app/mixins/selectable.js new file mode 100644 index 00000000..e0500f3d --- /dev/null +++ b/src/scripts/app/mixins/selectable.js @@ -0,0 +1,160 @@ +define(function () { + + return { + selectedItems: [], + selectPivot: null, + selectFlag: false, + selectSibling: function (e, relation) { + e = e || {}; + + const sibling = relation === 'previous' ? 'previousElementSibling' : 'nextElementSibling'; + + const selector = (e.selectUnread ? '.unread' : '.' + this.itemClass) + ':not([hidden])'; + let siblingElement; + let currentElement; + if (e.selectUnread && this.selectPivot) { + siblingElement = this.selectPivot.el[sibling]; + } else { + currentElement = this.el.querySelector('.last-selected'); + currentElement && (siblingElement = currentElement[sibling]); + } + while (siblingElement && !siblingElement.matches(selector)) { + siblingElement = siblingElement[sibling]; + } + + if (bg.settings.get('circularNavigation') && !e.ctrlKey && !e.shiftKey && !siblingElement) { + siblingElement = this.el.querySelector(selector + ':nth-of-type(1)'); + if (e.currentIsRemoved && siblingElement && this.el.querySelector('.last-selected') === siblingElement) { + siblingElement = null; + } + } + if (siblingElement && siblingElement.view) { + this.select(siblingElement.view, e, true); + } else if (e.currentIsRemoved) { + app.trigger('no-items:' + this.el.id); + } + if (siblingElement) { + siblingElement.focus(); + } + }, + selectNextSelectable: function (e) { + this.selectSibling(e, 'next'); + }, + selectPrev: function (e) { + this.selectSibling(e, 'previous'); + }, + select: function (view, e = {}, forceSelect = false) { + if ((!e.shiftKey && !e.ctrlKey) || (e.shiftKey && !this.selectPivot)) { + this.selectedItems = []; + this.selectPivot = view; + const selectedItems = this.el.querySelectorAll('.selected'); + Array.from(selectedItems).forEach((element) => { + element.classList.remove('selected'); + }); + + if (!window || !window.frames) { + bg.logs.add({message: 'Event duplication bug! Clearing events now...'}); + bg.sources.trigger('clear-events', -1); + return; + } + + setTimeout(() => { + this.trigger('pick', view, e); + }, 0); + + } else if (e.shiftKey && this.selectPivot) { + const selectedItems = this.el.querySelectorAll('.selected'); + Array.from(selectedItems).forEach((element) => { + element.classList.remove('selected'); + }); + this.selectedItems = [this.selectPivot]; + this.selectedItems[0].el.classList.add('selected'); + + if (this.selectedItems[0] !== view) { + const element = this.selectedItems[0].el; + const currentIndex = [...element.parentNode.children].indexOf(element); + const viewElement = view.el; + const viewIndex = [...viewElement.parentNode.children].indexOf(viewElement); + const siblings = []; + if (currentIndex < viewIndex) { + let sibling = element.nextElementSibling; + while (sibling) { + if (sibling.classList.contains('date-group')) { + sibling = sibling.nextElementSibling; + continue; + } + if (sibling === viewElement) { + break; + } + siblings.push(sibling); + sibling = sibling.nextElementSibling; + } + } else { + let sibling = viewElement.nextElementSibling; + while (sibling) { + if (sibling.classList.contains('date-group')) { + sibling = sibling.nextElementSibling; + continue; + } + if (sibling === element) { + break; + } + siblings.push(sibling); + sibling = sibling.nextElementSibling; + } + } + siblings.forEach((element) => { + element.classList.add('selected'); + this.selectedItems.push(element.view); + }); + + } + + if (forceSelect === true) { + setTimeout(function () { + this.trigger('pick', view, e); + }.bind(this), 0); + } + + } else if (e.ctrlKey && view.el.classList.contains('selected')) { + view.el.classList.remove('selected'); + view.el.classList.remove('last-selected'); + this.selectPivot = null; + this.selectedItems.splice(this.selectedItems.indexOf(view), 1); + return; + } else if (e.ctrlKey) { + this.selectPivot = view; + } + + const lastSelected = this.el.querySelector('.last-selected'); + if (lastSelected) { + lastSelected.classList.remove('last-selected'); + } + if (!this.selectedItems[0] || this.selectedItems[0] !== view) { + this.selectedItems.push(view); + view.el.classList.add('selected'); + } + view.el.classList.add('last-selected'); + }, + handleSelectableMouseDown: function (event) { + if (event.which === 2) { + return true; + } + event.preventDefault(); + const item = event.currentTarget.view; + if (this.selectedItems.length > 1 && item.el.classList.contains('selected') && !event.ctrlKey && !event.shiftKey) { + this.selectFlag = true; + return false; + } + this.select(item, event); + return false; + }, + handleSelectableMouseUp: function (event) { + const item = event.currentTarget.view; + if (event.which === 1 && this.selectedItems.length > 1 && this.selectFlag) { + this.select(item, event); + this.selectFlag = false; + } + } + }; +}); diff --git a/src/scripts/app/models/Action.js b/src/scripts/app/models/Action.js new file mode 100644 index 00000000..90cbf492 --- /dev/null +++ b/src/scripts/app/models/Action.js @@ -0,0 +1,58 @@ +/** + * @module App + * @submodule models/Action + */ +define(function (require) { + const BB = require('backbone'); + + /** + * Executable action. Actions are usually executed by shorcuts, buttons or context menus. + * @class Action + * @constructor + * @extends Backbone.Model + */ + let Action = BB.Model.extend({ + /** + * @property idAttribute + * @type String + * @default name + */ + idAttribute: 'name', + defaults: { + /** + * @attribute name + * @type String + * @default global:default + */ + name: 'global:default', + + /** + * Function to be called when action is executed + * @attribute fn + * @type function + */ + fn: function () { + return function () { + }; + }, + + /** + * @attribute icon + * @type String + * @default unknown.png + */ + icon: null, + + /** + * @attribute title + * @type String + * @default '' + */ + title: '', + state: null, + glyph: null, + } + }); + + return Action; +}); diff --git a/src/scripts/app/models/Group.js b/src/scripts/app/models/Group.js new file mode 100644 index 00000000..ec3b5e89 --- /dev/null +++ b/src/scripts/app/models/Group.js @@ -0,0 +1,117 @@ +/** + * @module App + * @submodule models/Group + */ +define(['backbone', 'helpers/dateUtils', 'modules/Locale'], function (BB, dateUtils, Locale) { + + /** + * Date group model + * @class Group + * @constructor + * @extends Backbone.Model + */ + let Group = BB.Model.extend({ + defaults: { + /** + * Title of the date group (Today, Yesterday, 2012, ...) + * @attribute title + * @type String + * @default '' + */ + title: '', + + /** + * End date of date group (yesterdays date is midnight between yesterday and today) in unix time + * @attribute date + * @type Integer + * @default 0 + */ + date: 0 + }, + idAttribute: 'date' + }); + + /** + * Gets date group attributes of given date + * @method getGroup + * @static + * @param date {Integer|Date} + * @return {Object} Object contaning title & date attributes + */ + Group.getGroup = (function () { + const days = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY']; + const months = ['JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER']; + let currentDate = null; + let todayMidnight = null; + let currentDaysSinceEpoch = null; + let currentWeekOfYear = null; + + + return function (date) { + const itemDate = new Date(date); + currentDate = currentDate || new Date(); + const itemDaysSinceEpoch = dateUtils.getDaysSinceEpoch(itemDate); + currentDaysSinceEpoch = currentDaysSinceEpoch || dateUtils.getDaysSinceEpoch(currentDate); + + if (!todayMidnight) { + todayMidnight = dateUtils.startOfDay(currentDate); + setTimeout(function () { + todayMidnight = null; + currentDate = null; + currentDaysSinceEpoch = null; + currentWeekOfYear = null; + }, 10000); + } + + const itemMidnight = dateUtils.startOfDay(itemDate); + + let itemDateWeekOfYear = dateUtils.getWeekOfYear(itemDate); + currentWeekOfYear = currentWeekOfYear || dateUtils.getWeekOfYear(currentDate); + + const difference = itemDaysSinceEpoch - currentDaysSinceEpoch; + + if (difference >= 0) { + return { + title: Locale.TODAY.toUpperCase(), + date: dateUtils.addDays(todayMidnight, 5000) // 5000 = make sure "today" is the first element in list + }; + } + if (difference === -1) { + return { + title: Locale.YESTERDAY.toUpperCase(), + date: todayMidnight + }; + } + if (itemDateWeekOfYear === (currentWeekOfYear) && difference >= -7) { + return { + title: Locale[days[itemDate.getDay()]].toUpperCase(), + date: dateUtils.addDays(itemMidnight, 1) + }; + } + if (itemDateWeekOfYear + 1 === currentWeekOfYear && difference >= -14) { + return { + title: Locale.LAST_WEEK.toUpperCase(), + date: dateUtils.startOfWeek(currentDate) + }; + } + if (itemDate.getMonth() === currentDate.getMonth() && itemDate.getFullYear() === currentDate.getFullYear()) { + return { + title: Locale.EARLIER_THIS_MONTH.toUpperCase(), + date: dateUtils.startOfMonth(currentDate) + }; + } + if (itemDate.getFullYear() === currentDate.getFullYear()) { + return { + title: Locale[months[itemDate.getMonth()]].toUpperCase(), + date: (new Date(itemDate.getFullYear(), itemDate.getMonth() + 1, 1, 0, 0, 0, 0)) + }; + } + return { + title: itemDate.getFullYear(), + date: (new Date(itemDate.getFullYear() + 1, 0, 1, 0, 0, 0, 0)) + }; + }; + })(); + + return Group; +}); diff --git a/src/scripts/app/models/MenuItem.js b/src/scripts/app/models/MenuItem.js new file mode 100644 index 00000000..3609bc1c --- /dev/null +++ b/src/scripts/app/models/MenuItem.js @@ -0,0 +1,35 @@ +/** + * @module App + * @submodule models/MenuItem + */ +define(function (require) { + const BB = require('backbone'); + + /** + * Context menu item + * @class MenuItem + * @constructor + * @extends Backbone.Model + */ + let MenuItem = BB.Model.extend({ + defaults: { + + /** + * @attribute title + * @type String + * @default '' + */ + 'title': '', + + /** + * Function to be called when user selects this item + * @attribute action + * @type function + * @default null + */ + 'action': null + } + }); + + return MenuItem; +}); diff --git a/src/scripts/app/models/Special.js b/src/scripts/app/models/Special.js new file mode 100644 index 00000000..f9342b79 --- /dev/null +++ b/src/scripts/app/models/Special.js @@ -0,0 +1,66 @@ +/** + * @module App + * @submodule models/Special + */ +define(function (require) { + const BB = require('backbone'); + /** + * Model for special items in feed list like all-feeds, pinned and trash + * @class Special + * @constructor + * @extends Backbone.Model + */ + let Special = BB.Model.extend({ + defaults: { + /** + * Visible title of special. Chnages with localization. + * @attribute title + * @type String + * @default All feeds + */ + title: 'All feeds', + + /** + * @attribute icon + * @type String + * @default icon16_v2.png + */ + icon: 'icon16_v2.png', + + /** + * Name of the special. It is always the same for one special. + * @attribute name + * @type String + * @default '' + */ + name: '', + + /** + * Filter used in 'where' function of items collection + * @attribute filter + * @type Object + * @default {} + * @example { unread: true, trashed: false } + */ + filter: {}, + + /** + * Should the special be above or below feed sources? + * @attribute position + * @type String + * @default top + */ + position: 'top', + + /** + * Function to be called when specials view is initialized + * @attribute onReady + * @type function + * @default null + */ + onReady: null + } + }); + + return Special; +}); diff --git a/src/scripts/app/models/ToolbarButton.js b/src/scripts/app/models/ToolbarButton.js new file mode 100644 index 00000000..246d880e --- /dev/null +++ b/src/scripts/app/models/ToolbarButton.js @@ -0,0 +1,34 @@ +/** + * @module App + * @submodule models/ToolbarButton + */ +define(function (require) { + const BB = require('backbone'); + /** + * Button model for toolbars + * @class ToolbarButton + * @constructor + * @extends Backbone.Model + */ + let ToolbarButton = BB.Model.extend({ + defaults: { + + /** + * @attribute actionName + * @type String + * @default global:default + */ + actionName: 'global:default', + + /** + * Is button aligned to left or right? + * @attribute position + * @type String + * @default left + */ + position: 'left' + } + }); + + return ToolbarButton; +}); diff --git a/src/scripts/app/modules/Locale.js b/src/scripts/app/modules/Locale.js new file mode 100644 index 00000000..7046b154 --- /dev/null +++ b/src/scripts/app/modules/Locale.js @@ -0,0 +1,34 @@ +/** + * @module App + * @submodule modules/Locale + */ +const nl = bg.settings.get('lang') || 'en'; +define(['../../nls/' + nl, '../../nls/en'], function (lang, en) { + + /** + * String localization + * @class Locale + * @constructor + * @extends Object + */ + const Locale = { + lang: lang, + en: en, + translate: function (name) { + return lang[name] ? lang[name] : (en[name] ? en[name] + '*' : name + '!'); + }, + translateHTML: function (content) { + return String(content).replace(/\{\{(\w+)\}\}/gm, (all, str) => { + return this.translate(str); + }); + } + }; + + const handler = { + get(target, name) { + return target[name] ? target[name] : target.translate(name); + } + }; + + return new Proxy(Locale, handler); +}); \ No newline at end of file diff --git a/src/scripts/app/options.js b/src/scripts/app/options.js new file mode 100644 index 00000000..2f50b23f --- /dev/null +++ b/src/scripts/app/options.js @@ -0,0 +1,857 @@ +define(["../app/staticdb/actions", "staticdb/shortcuts"], function ( + actions, + shortcuts +) { + const entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }; + + function escapeHtml(string) { + return String(string) + .replace(/[&<>"']/gm, (s) => { + return entityMap[s]; + }) + .replace(/\s/, (f) => { + return f === " " ? " " : ""; + }); + } + + function decodeHTML(str = "") { + let map = { gt: ">", lt: "<", amp: "&", quot: '"' }; + return str.replace( + /&(#(?:x[0-9a-f]+|\d+)|[a-z]+);?/gim, + function ($0, $1) { + if ($1[0] === "#") { + return String.fromCharCode( + $1[1].toLowerCase() === "x" + ? parseInt($1.substr(2), 16) + : parseInt($1.substr(1), 10) + ); + } else { + return map.hasOwnProperty($1) ? map[$1] : $0; + } + } + ); + } + + JSON.safeParse = function (str) { + try { + return JSON.parse(str); + } catch (e) { + return null; + } + }; + + const documentReady = () => { + if (typeof browser === "undefined") { + const warning = document.querySelector(".ff-warning"); + warning.parentNode.removeChild(warning); + } + document.querySelector("#version").textContent = + browser.runtime.getManifest().version; + if (typeof browser !== "undefined") { + browser.runtime.getBrowserInfo().then((info) => { + document.querySelector( + "#browser-info" + ).textContent = `${info.vendor} ${info.name} ${info.version} ${info.buildID}`; + }); + } + + [ + ...document.querySelectorAll( + "select[id], input[type=number], input[type=range], textarea" + ), + ].forEach((item) => { + const v = bg.settings.get(item.id); + if (item.querySelector('option[value="true"]')) { + item.value = v ? "true" : "false"; + } else { + item.value = v; + } + if (item.type === "number") { + item.addEventListener("input", handleChange); + } else { + item.addEventListener("change", handleChange); + } + }); + + [...document.querySelectorAll("input[type=checkbox]")].forEach( + (item) => { + item.checked = bg.getBoolean(item.id); + item.addEventListener("change", handleCheck); + } + ); + + document.querySelector("#useSound").addEventListener("change", () => { + bg.loader.playNotificationSound(); + }); + + document + .querySelector("#default-sound") + .addEventListener("change", handleDefaultSound); + document + .querySelector("#suggest-style") + .addEventListener("click", handleSuggestStyle); + document + .querySelector("#reset-style") + .addEventListener("click", handleResetStyle); + document + .querySelector("#export-settings") + .addEventListener("click", handleExportSettings); + document + .querySelector("#export-smart") + .addEventListener("click", handleExportSmart); + document + .querySelector("#export-opml") + .addEventListener("click", handleExportOPML); + document + .querySelector("#clear-settings") + .addEventListener("click", handleClearSettings); + document + .querySelector("#clear-data") + .addEventListener("click", handleClearData); + document + .querySelector("#clear-removed-storage") + .addEventListener("click", handleClearDeletedStorage); + document + .querySelector("#clear-favicons") + .addEventListener("click", handleClearFavicons); + document + .querySelector("#import-settings") + .addEventListener("change", handleImportSettings); + document + .querySelector("#import-smart") + .addEventListener("change", handleImportSmart); + document + .querySelector("#import-opml") + .addEventListener("change", handleImportOPML); + document.querySelector('[name="queries"]').value = bg.settings + .get("queries") + .join(","); + + document + .querySelector('[name="queries"]') + .addEventListener("change", handleChangeQueries); + + function handleChangeQueries(event) { + const queries = event.target.value.split(","); + bg.settings.save("queries", queries); + } + + [...document.querySelectorAll("input[type=image]")].forEach( + (element) => { + element.addEventListener("click", handleLayoutChangeClick); + } + ); + + const hotkeysElement = document.querySelector("#hotkeys"); + hotkeysElement.addEventListener("keydown", (event) => { + const target = event.target; + if (target.tagName !== "INPUT") { + return true; + } + event.preventDefault(); + let shortcut = ""; + if (event.ctrlKey) { + shortcut += "ctrl+"; + } + if (event.altKey) { + shortcut += "alt+"; + } + if (event.shiftKey) { + shortcut += "shift+"; + } + + if (event.keyCode > 46 && event.keyCode < 91) { + shortcut += String.fromCharCode(event.keyCode).toLowerCase(); + } else if (event.keyCode in shortcuts.keys) { + shortcut += shortcuts.keys[event.keyCode]; + } else { + return; + } + target.value = shortcut; + return false; + }); + + const saveHotkeys = () => { + const hotkeysSettings = {}; + [...hotkeysElement.querySelectorAll("section")].forEach( + (section) => { + const sectionSettings = {}; + const sectionName = section.id; + [...section.querySelectorAll("label")].forEach((label) => { + const hotkey = label.querySelector("input").value; + const action = label.querySelector("select").value; + if (hotkey === "") { + return; + } + if (action === "") { + return; + } + sectionSettings[hotkey] = action; + }); + hotkeysSettings[sectionName] = sectionSettings; + } + ); + bg.settings.save("hotkeys", hotkeysSettings); + }; + let actionsMap = {}; + Object.entries(actions).forEach((obj) => { + Object.entries(obj[1]).forEach((action) => { + actionsMap[obj[0] + ":" + action[0]] = + obj[0] + ":" + action[1]["title"]; + }); + }); + + const renderHotkeysBlock = () => { + hotkeysElement.textContent = ""; + const resetHotkeysButton = document.createElement("button"); + resetHotkeysButton.classList.add("resetHotkeysButton"); + resetHotkeysButton.textContent = "Reset hotkeys"; + hotkeysElement.insertAdjacentElement( + "beforeend", + resetHotkeysButton + ); + const hotkeys = bg.settings.get("hotkeys"); + let actionsMap = {}; + Object.entries(actions).forEach((obj) => { + Object.entries(obj[1]).forEach((action) => { + actionsMap[obj[0] + ":" + action[0]] = action[1]["title"]; + }); + }); + + for (const region in hotkeys) { + if (hotkeys.hasOwnProperty(region)) { + const regionElement = document.createElement("section"); + regionElement.id = region; + const regionHeader = document.createElement("h3"); + regionHeader.insertAdjacentText("afterbegin", region); + regionElement.insertAdjacentElement( + "afterbegin", + regionHeader + ); + + hotkeysElement.insertAdjacentElement( + "beforeend", + regionElement + ); + + const regionHotkeys = hotkeys[region]; + const addHotkeyButton = document.createElement("button"); + addHotkeyButton.textContent = "+"; + addHotkeyButton.classList.add("addHotkeyButton"); + regionElement.insertAdjacentElement( + "beforeend", + addHotkeyButton + ); + for (const regionHotkey in regionHotkeys) { + addHotkeyToElement( + regionElement, + regionHotkey, + regionHotkeys[regionHotkey] + ); + } + } + } + }; + + const hotkeyChangeHandler = (event) => { + const target = event.target; + if ( + target.classList.contains("actionHotkey") || + target.classList.contains("actionSelect") + ) { + saveHotkeys(); + } + }; + + hotkeysElement.addEventListener("change", hotkeyChangeHandler); + hotkeysElement.addEventListener("keyup", hotkeyChangeHandler); + hotkeysElement.addEventListener("click", (event) => { + const target = event.target; + if (target.classList.contains("addHotkeyButton")) { + addHotkeyToElement(target.parentElement); + return true; + } + if (target.classList.contains("removeHotkeyButton")) { + target.parentElement.remove(); + saveHotkeys(); + return true; + } + if (target.classList.contains("resetHotkeysButton")) { + if ( + typeof browser === "undefined" || + confirm( + "Resetting hotkeys will require extension reload, do you want to continue?" + ) + ) { + bg.settings.save("hotkeys", bg.settings.defaults.hotkeys); + if (typeof browser === "undefined") { + renderHotkeysBlock(); + return true; + } + browser.runtime.reload(); + } + } + return true; + }); + + const addHotkeyToElement = ( + element, + hotkeyString = "", + actionString = "" + ) => { + const label = document.createElement("label"); + label.classList.add("web-content-select-label"); + + const hotkey = document.createElement("input"); + hotkey.classList.add("actionHotkey"); + hotkey.value = hotkeyString; + + const actionSelect = document.createElement("select"); + actionSelect.classList.add("actionSelect"); + Object.entries(actionsMap).forEach((action) => { + const actionOption = document.createElement("option"); + actionOption.value = action[0]; + actionOption.textContent = action[1] ? action[1] : action[0]; + actionSelect.insertAdjacentElement("beforeend", actionOption); + }); + actionSelect.value = actionString; + const removeHotkeyButton = document.createElement("button"); + removeHotkeyButton.classList.add("removeHotkeyButton"); + removeHotkeyButton.textContent = "-"; + + label.insertAdjacentElement("beforeend", hotkey); + label.insertAdjacentElement("beforeend", actionSelect); + label.insertAdjacentElement("beforeend", removeHotkeyButton); + element + .querySelector(".addHotkeyButton") + .insertAdjacentElement("beforebegin", label); + }; + + renderHotkeysBlock(); + handleLayoutChange(bg.settings.get("layout")); + }; + + if (document.readyState !== "loading") { + documentReady(); + } else { + document.addEventListener("DOMContentLoaded", documentReady); + } + + function handleLayoutChangeClick(event) { + const layout = event.currentTarget.value; + handleLayoutChange(layout); + bg.settings.save("layout", layout); + } + + function handleLayoutChange(layout) { + if (layout === "vertical") { + document + .querySelector("input[value=horizontal]") + .setAttribute("src", "/images/layout_horizontal.png"); + document + .querySelector("input[value=vertical]") + .setAttribute("src", "/images/layout_vertical_selected.png"); + } else { + document + .querySelector("input[value=horizontal]") + .setAttribute("src", "/images/layout_horizontal_selected.png"); + document + .querySelector("input[value=vertical]") + .setAttribute("src", "/images/layout_vertical.png"); + } + } + + function handleResetStyle() { + if (!confirm("Do you really want to reset style to default?")) { + return; + } + document.querySelector("#userStyle").value = ""; + bg.settings.save("userStyle", ""); + browser.runtime.sendMessage({ action: "changeUserStyle" }); + } + + function handleSuggestStyle() { + if (document.querySelector("#userStyle").value !== "") { + if ( + !confirm( + "Do you really want to replace your current style with colors template?" + ) + ) { + return; + } + } + const defaultStyle = bg.settings.get("defaultStyle"); + document.querySelector("#userStyle").value = defaultStyle; + bg.settings.save("userStyle", defaultStyle); + browser.runtime.sendMessage({ action: "changeUserStyle" }); + } + + function handleChange(event) { + const target = event.target; + let v = target.value; + if (target.value === "true") { + v = true; + } + if (target.value === "false") { + v = false; + } + bg.settings.save(target.id, v); + if (target.id === "userStyle") { + browser.runtime.sendMessage({ action: "changeUserStyle" }); + } + if (target.id === "invertColors") { + browser.runtime.sendMessage({ action: "changeInvertColors" }); + } + } + + function handleCheck(event) { + const target = event.target; + bg.settings.save(target.id, target.checked); + } + + function handleDefaultSound(event) { + const file = event.currentTarget.files[0]; + if (!file || file.size === 0) { + return; + } + + if (!file.type.match(/audio.*/)) { + alert("Please select audio file!"); + return; + } + + if (file.size > 500000) { + alert("Please use file smaller than 500kB!"); + return; + } + + const reader = new FileReader(); + reader.onload = function () { + bg.settings.save("defaultSound", this.result); + }; + + reader.readAsDataURL(file); + } + + function handleExportSmart() { + const smartExportStatus = document.querySelector("#smart-exported"); + const data = { + folders: bg.folders.toJSON(), + sources: bg.sources.toJSON(), + items: bg.items.toJSON(), + }; + + smartExportStatus.setAttribute("href", "#"); + smartExportStatus.removeAttribute("download"); + smartExportStatus.textContent = "Exporting, please wait"; + + setTimeout(() => { + const expr = new Blob([JSON.stringify(data)]); + smartExportStatus.setAttribute("href", URL.createObjectURL(expr)); + smartExportStatus.setAttribute("download", "exported-rss.smart"); + smartExportStatus.textContent = "Click to download exported data"; + }, 20); + } + + function handleExportSettings() { + const settingsExportStatus = + document.querySelector("#settings-exported"); + const data = { + settings: bg.settings.toJSON(), + }; + + settingsExportStatus.setAttribute("href", "#"); + settingsExportStatus.removeAttribute("download"); + settingsExportStatus.textContent = "Exporting, please wait"; + + setTimeout(() => { + const expr = new Blob([JSON.stringify(data)]); + settingsExportStatus.setAttribute( + "href", + URL.createObjectURL(expr) + ); + settingsExportStatus.setAttribute("download", "settings.smart"); + settingsExportStatus.textContent = + "Click to download exported data"; + }, 20); + } + + function handleExportOPML() { + function addFolder(doc, title, id) { + const folder = doc.createElement("outline"); + folder.setAttribute("text", escapeHtml(title)); + folder.setAttribute("title", escapeHtml(title)); + folder.setAttribute("id", id); + return folder; + } + + function addSource(doc, title, url) { + const source = doc.createElement("outline"); + source.setAttribute("text", escapeHtml(title)); + source.setAttribute("title", escapeHtml(title)); + source.setAttribute("type", "rss"); + source.setAttribute("xmlUrl", url); + return source; + } + + function addLine(doc, to, ctn = "\n\t") { + const line = doc.createTextNode(ctn); + to.appendChild(line); + } + + const opmlExportStatus = document.querySelector("#opml-exported"); + + opmlExportStatus.setAttribute("href", "#"); + opmlExportStatus.removeAttribute("download"); + opmlExportStatus.textContent = "Exporting, please wait"; + + const start = + '\n\n\n\tNewsfeeds exported from Smart RSS\n\n'; + const end = "\n\n"; + + const parser = new DOMParser(); + const doc = parser.parseFromString(start + end, "application/xml"); + + setTimeout(() => { + const body = doc.querySelector("body"); + + bg.folders.forEach((folder) => { + addLine(doc, body); + body.appendChild( + addFolder(doc, folder.get("title"), folder.get("id")) + ); + }); + + bg.sources.forEach((source) => { + if (source.get("folderID")) { + const folder = body.querySelector( + '[id="' + source.get("folderID") + '"]' + ); + if (folder) { + addLine(doc, folder, "\n\t\t"); + folder.appendChild( + addSource( + doc, + source.get("title"), + source.get("url") + ) + ); + } else { + addLine(doc, body); + body.appendChild( + addSource( + doc, + source.get("title"), + source.get("url") + ) + ); + } + } else { + addLine(doc, body); + body.appendChild( + addSource(doc, source.get("title"), source.get("url")) + ); + } + }); + + const folders = body.querySelectorAll("[id]"); + [...folders].forEach((folder) => { + folder.removeAttribute("id"); + }); + + const expr = new Blob([new XMLSerializer().serializeToString(doc)]); + opmlExportStatus.setAttribute("href", URL.createObjectURL(expr)); + opmlExportStatus.setAttribute("download", "exported-rss.opml"); + opmlExportStatus.textContent = "Click to download exported data"; + }, 20); + } + + function handleImportSettings(event) { + const settingsImportStatus = + document.querySelector("#settings-imported"); + const file = event.target.files[0]; + if (!file || file.size === 0) { + settingsImportStatus.textContent = "Wrong file"; + return; + } + settingsImportStatus.textContent = "Loading & parsing file"; + + const reader = new FileReader(); + reader.onload = function () { + const data = JSON.safeParse(this.result); + + if (!data || !data.settings) { + settingsImportStatus.textContent = "Wrong file"; + return; + } + + settingsImportStatus.textContent = "Importing, please wait!"; + + const worker = new Worker("scripts/options/worker.js"); + worker.onmessage = function (message) { + if (message.data.action === "finished-settings") { + settingsImportStatus.textContent = + "Loading data to memory!"; + bg.fetchAll().then(function () { + if (typeof browser !== "undefined") { + browser.runtime.reload(); + } + bg.info.refreshSpecialCounters(); + settingsImportStatus.textContent = + "Import fully completed!"; + bg.loader.downloadAll(true); + }); + } else if (message.data.action === "message-settings") { + settingsImportStatus.textContent = message.data.value; + } + }; + worker.postMessage({ action: "settings", value: data }); + + worker.onerror = function (error) { + alert("Importing error: " + error.message); + }; + }; + if (typeof browser !== "undefined") { + reader.readAsText(file); + } else { + const url = browser.runtime.getURL("rss.html"); + browser.tabs.query({ url: url }, function (tabs) { + for (let i = 0; i < tabs.length; i++) { + browser.tabs.remove(tabs[i].id); + } + + // wait for clear events to happen + setTimeout(function () { + reader.readAsText(file); + }, 1000); + }); + } + } + + function handleImportSmart(event) { + const smartImportStatus = document.querySelector("#smart-imported"); + const file = event.target.files[0]; + if (!file || file.size === 0) { + smartImportStatus.textContent = "Wrong file"; + return; + } + smartImportStatus.textContent = "Loading & parsing file"; + + const reader = new FileReader(); + reader.onload = function () { + const data = JSON.safeParse(this.result); + + if (!data || !data.items || !data.sources) { + smartImportStatus.textContent = "Wrong file"; + return; + } + + smartImportStatus.textContent = "Importing, please wait!"; + + const worker = new Worker("scripts/options/worker.js"); + worker.onmessage = function (message) { + if (message.data.action === "finished") { + smartImportStatus.textContent = "Loading data to memory!"; + bg.fetchAll().then(function () { + if (typeof browser !== "undefined") { + browser.runtime.reload(); + } + bg.info.refreshSpecialCounters(); + smartImportStatus.textContent = + "Import fully completed!"; + bg.loader.downloadAll(true); + }); + } else if (message.data.action === "message") { + smartImportStatus.textContent = message.data.value; + } + }; + worker.postMessage({ action: "file-content", value: data }); + + worker.onerror = function (error) { + alert("Importing error: " + error.message); + }; + }; + if (typeof browser !== "undefined") { + reader.readAsText(file); + } else { + const url = browser.runtime.getURL("rss.html"); + browser.tabs.query({ url: url }, function (tabs) { + for (let i = 0; i < tabs.length; i++) { + browser.tabs.remove(tabs[i].id); + } + + // wait for clear events to happen + setTimeout(function () { + reader.readAsText(file); + }, 1000); + }); + } + } + + function handleImportOPML(event) { + const opmlImportStatus = document.querySelector("#opml-imported"); + const file = event.target.files[0]; + if (!file || file.size === 0) { + opmlImportStatus.textContent = "Wrong file"; + return; + } + + opmlImportStatus.textContent = "Importing, please wait!"; + + const reader = new FileReader(); + reader.onload = function () { + const parser = new DOMParser(); + const doc = parser.parseFromString(this.result, "application/xml"); + + if (!doc) { + opmlImportStatus.textContent = "Wrong file"; + return; + } + + const feeds = doc.querySelectorAll( + "body > outline[text], body > outline[title]" + ); + + [...feeds].forEach((feed) => { + if (!feed.hasAttribute("xmlUrl")) { + const subFeeds = feed.querySelectorAll("outline[xmlUrl]"); + const folderTitle = decodeHTML( + feed.getAttribute("title") || feed.getAttribute("text") + ); + + const duplicate = bg.folders.findWhere({ + title: folderTitle, + }); + + const folder = + duplicate || + bg.folders.create( + { + title: folderTitle, + }, + { wait: true } + ); + const folderId = folder.get("id"); + + [...subFeeds].forEach((subFeed) => { + if ( + bg.sources.findWhere({ + url: decodeHTML(subFeed.getAttribute("xmlUrl")), + }) + ) { + return; + } + bg.sources.create( + { + title: decodeHTML( + subFeed.getAttribute("title") || + subFeed.getAttribute("text") + ), + url: decodeHTML(subFeed.getAttribute("xmlUrl")), + updateEvery: -1, + folderID: folderId, + }, + { wait: true } + ); + }); + } else { + if ( + bg.sources.findWhere({ + url: decodeHTML(feed.getAttribute("xmlUrl")), + }) + ) { + return; + } + bg.sources.create( + { + title: decodeHTML( + feed.getAttribute("title") || + feed.getAttribute("text") + ), + url: decodeHTML(feed.getAttribute("xmlUrl")), + updateEvery: -1, + }, + { wait: true } + ); + } + }); + + opmlImportStatus.textContent = "Import completed!"; + + setTimeout(function () { + bg.loader.downloadAll(true); + }, 10); + }; + + reader.readAsText(file); + } + + function handleClearSettings() { + if (!confirm("Do you really want to remove all extension settings?")) { + return; + } + const request = indexedDB.open("backbone-indexeddb", 4); + request.addEventListener("success", function () { + const db = this.result; + const transaction = db.transaction( + ["settings-backbone"], + "readwrite" + ); + const settings = transaction.objectStore("settings-backbone"); + settings.clear(); + browser.alarms.clearAll(); + browser.runtime.reload(); + }); + } + + function handleClearData() { + if (!confirm("Do you really want to remove all extension data?")) { + return; + } + + bg.indexedDB.deleteDatabase("backbone-indexeddb"); + localStorage.clear(); + browser.alarms.clearAll(); + browser.runtime.reload(); + } + + function handleClearDeletedStorage() { + if ( + !confirm( + "Do you really want to remove deleted articles metadata? This may cause some of them to appear again" + ) + ) { + return; + } + + bg.items + .where({ + deleted: true, + }) + .forEach((item) => { + item.destroy(); + }); + alert("Done,extension will reboot now"); + browser.runtime.reload(); + } + + function handleClearFavicons() { + if (!confirm("Do you really want to remove all favicons?")) { + return; + } + + bg.sources.toArray().forEach((source) => { + source.save({ + favicon: "/images/feed.png", + faviconExpires: 0, + }); + }); + alert("Done"); + } +}); diff --git a/src/scripts/app/staticdb/actions.js b/src/scripts/app/staticdb/actions.js new file mode 100644 index 00000000..3b0f3fc2 --- /dev/null +++ b/src/scripts/app/staticdb/actions.js @@ -0,0 +1,998 @@ +define(["helpers/stripTags", "modules/Locale", "controllers/comm"], function ( + stripTags, + L, + comm +) { + return { + global: { + default: { + title: "Unknown", + fn: function () { + alert("no action"); + }, + }, + hideOverlays: { + title: L.HIDE_OVERLAYS, + fn: function () { + comm.trigger("hide-overlays"); + }, + }, + openOptions: { + title: L.OPTIONS, + icon: "options.png", + fn: function () { + browser.runtime.openOptionsPage(); + }, + }, + }, + feeds: { + toggleShowOnlyUnread: { + // icon: 'icon16.png', + glyph: "📰", + state: "showOnlyUnreadSources", + title: L.TOGGLE_SHOW_ONLY_UNREAD, + fn: function () { + const currentUnread = bg.getBoolean( + "showOnlyUnreadSources" + ); + bg.settings.save("showOnlyUnreadSources", !currentUnread); + }, + }, + updateAll: { + icon: "reload.png", + title: L.UPDATE_ALL, + fn: function () { + browser.runtime.sendMessage({ action: "load-all" }); + }, + }, + update: { + icon: "reload.png", + title: L.UPDATE, + fn: function () { + const selectedItems = + require("views/feedList").selectedItems; + if (selectedItems.length) { + const models = selectedItems.map((item) => { + return item.model; + }); + bg.loader.download(models); + } + }, + }, + stopUpdate: { + icon: "stop.png", + title: L.STOP_UPDATE, + fn: function () { + bg.loader.abortDownloading(); + }, + }, + mark: { + icon: "read.png", + title: L.MARK_ALL_AS_READ, + fn: function () { + const selectedFeeds = + require("views/feedList").getSelectedFeeds(); + if (!selectedFeeds.length) { + return; + } + + bg.items.forEach(function (item) { + if ( + item.get("unread") === true && + selectedFeeds.indexOf(item.getSource()) >= 0 + ) { + item.save({ + unread: false, + visited: true, + }); + } + }); + + selectedFeeds.forEach(function (source) { + if (source.get("hasNew")) { + source.save({ hasNew: false }); + } + }); + }, + }, + openHome: { + title: L.OPEN_HOME, + fn: function () { + const selectedFeeds = + require("views/feedList").getSelectedFeeds(); + if (!selectedFeeds.length) { + return; + } + selectedFeeds.forEach((source) => { + browser.tabs.create({ + url: source.get("base"), + active: false, + }); + }); + }, + }, + refetch: { + title: L.REFETCH, + fn: function () { + const selectedFeeds = + require("views/feedList").getSelectedFeeds(); + if (!selectedFeeds.length) { + return; + } + selectedFeeds.forEach(function (source) { + bg.items + .where({ sourceID: source.get("id") }) + .forEach(function (item) { + item.destroy(); + }); + }); + app.actions.execute("feeds:update"); + }, + }, + delete: { + icon: "delete.png", + title: L.DELETE, + fn: function () { + if (!confirm(L.REALLY_DELETE)) { + return; + } + + const feeds = require("views/feedList").getSelectedFeeds(); + const folders = + require("views/feedList").getSelectedFolders(); + + feeds.forEach(function (feed) { + feed.destroy(); + }); + + folders.forEach(function (folder) { + folder.destroy(); + }); + }, + }, + scrollIntoView: { + icon: "back.png", + title: L.SCROLL_INTO_VIEW, + fn: function () { + const folders = + require("views/feedList").getSelectedFolders(); + + if (folders.length > 0) { + const id = folders[0].get("id"); + const sourceElement = document.querySelector( + `[data-id="${id}"]` + ); + sourceElement.scrollIntoView(); + return; + } + + const feeds = require("views/feedList").getSelectedFeeds(); + if (feeds.length > 0) { + const id = feeds[0].get("id"); + const sourceElement = document.querySelector( + `[data-id="${id}"]` + ); + sourceElement.scrollIntoView(); + } + }, + }, + showProperties: { + icon: "properties.png", + title: L.PROPERTIES, + fn: function () { + const properties = app.feeds.properties; + const feedList = require("views/feedList"); + const feeds = feedList.getSelectedFeeds(); + const folders = feedList.getSelectedFolders(); + + if ( + feedList.selectedItems.length === 1 && + folders.length === 1 + ) { + properties.show(folders[0]); + } else if (!folders.length && feeds.length === 1) { + properties.show(feeds[0]); + } else if (feeds.length > 0) { + properties.show(feeds); + } + }, + }, + addSource: { + icon: "add.png", + title: L.ADD_RSS_SOURCE, + fn: function () { + let url = (prompt(L.RSS_FEED_URL) || "").trim(); + if (!url) { + return; + } + + let folderID = "0"; + const list = require("views/feedList"); + if ( + list.selectedItems.length && + list.selectedItems[0].el.classList.contains("folder") + ) { + const fid = list.selectedItems[0].model.get("id"); + // make sure source is not added to folder which is not in db + if (bg.folders.get(fid)) { + folderID = fid; + } + } + + url = app.fixURL(url); + const uid = url + .replace(/^(.*:)?(\/\/)?(www*?\.)?/, "") + .replace(/\/$/, ""); + const duplicate = bg.sources.findWhere({ uid: uid }); + + if (!duplicate) { + const newFeed = bg.sources.create( + { + title: url, + url: url, + updateEvery: -1, + folderID: folderID, + }, + { wait: true } + ); + app.trigger("focus-feed", newFeed.get("id")); + } else { + app.trigger("focus-feed", duplicate.get("id")); + } + }, + }, + addFolder: { + icon: "add_folder.png", + title: L.NEW_FOLDER, + fn: function () { + const title = (prompt(L.FOLDER_NAME + ": ") || "").trim(); + if (!title) { + return; + } + + bg.folders.create( + { + title: title, + }, + { wait: true } + ); + }, + }, + focus: { + title: L.FOCUS_FEEDS, + fn: function () { + app.setFocus("feeds"); + }, + }, + selectNext: { + title: L.SELECT_NEXT_FEED, + fn: function (event) { + require("views/feedList").selectNextSelectable(event); + }, + }, + selectPrevious: { + title: L.SELECT_PREVIOUS_FEED, + fn: function (event) { + require("views/feedList").selectPrev(event); + }, + }, + closeFolders: { + title: L.CLOSE_FOLDERS, + fn: function (event) { + const folders = Array.from( + document.querySelectorAll(".folder.opened") + ); + if (!folders.length) { + return; + } + folders.forEach((folder) => { + if (folder.view) { + folder.view.handleClickArrow(event); + } + }); + }, + }, + openFolders: { + title: L.OPEN_FOLDERS, + fn: function (event) { + const folders = Array.from( + document.querySelectorAll(".folder:not(.opened)") + ); + folders.forEach((folder) => { + if (folder.view) { + folder.view.handleClickArrow(event); + } + }); + }, + }, + toggleFolder: { + title: L.TOGGLE_FOLDER, + fn: function (event) { + event = event || {}; + const selectedItems = + require("views/feedList").selectedItems; + if ( + selectedItems.length && + selectedItems[0].el.classList.contains("folder") + ) { + selectedItems[0].handleClickArrow(event); + } + }, + }, + showArticles: { + title: L.SHOW_ARTICLES, + fn: function (event = {}) { + const target = event.target || {}; + const feedList = require("views/feedList"); + const feeds = feedList.getSelectedFeeds(); + const feedIds = feeds.map((feed) => { + return feed.id; + }); + let special = Array.from( + document.querySelectorAll(".special.selected") + )[0]; + if (special) { + special = special.view.model; + } + const folder = Array.from( + document.querySelectorAll(".folder.selected") + )[0]; + + let unreadOnly = false; + if (bg.getBoolean("defaultToUnreadOnly")) { + unreadOnly = true; + } + + if (bg.getBoolean("showOnlyUnreadSources")) { + unreadOnly = true; + } + + if ( + !!event.altKey || + target.className === "source-counter" + ) { + unreadOnly = !unreadOnly; + } + + app.trigger("select:" + feedList.el.id, { + action: "new-select", + feeds: feedIds, + filter: special + ? Object.assign({}, special.get("filter")) + : null, + name: special ? special.get("name") : null, + multiple: !!(special || folder), + unreadOnly: unreadOnly, + }); + + if (special && special.get("name") === "all-feeds") { + bg.sources.forEach((source) => { + if (source.get("hasNew")) { + source.save({ hasNew: false }); + } + }); + } else if (feedIds.length) { + bg.sources.forEach((source) => { + if ( + source.get("hasNew") && + feedIds.includes(source.id) + ) { + source.save({ hasNew: false }); + } + }); + } + }, + }, + showAndFocusArticles: { + title: L.SHOW_AND_FOCUS_ARTICLES, + fn: function (event) { + event = event || {}; + const selectedItems = + require("views/feedList").selectedItems; + if (selectedItems.length) { + app.actions.execute("feeds:showArticles", event); + app.actions.execute("articles:focus"); + } + }, + }, + }, + articles: { + mark: { + icon: "read.png", + title: L.MARK_AS_READ, + fn: function () { + require("views/articleList").changeUnreadState(); + }, + }, + toggleShowOnlyUnread: { + // icon: 'icon16.png', + glyph: "📰", + state: "defaultToUnreadOnly", + title: L.DEFAULT_TO_UNREAD_ONLY, + fn: function () { + const currentUnread = bg.getBoolean("defaultToUnreadOnly"); + bg.settings.save("defaultToUnreadOnly", !currentUnread); + }, + }, + update: { + icon: "reload.png", + title: L.UPDATE, + fn: function () { + const list = require("views/articleList"); + if (list.currentData.feeds.length) { + list.currentData.feeds.forEach((id) => { + bg.loader.download(bg.sources.get(id)); + }); + } else { + browser.runtime.sendMessage({ action: "load-all" }); + } + }, + }, + delete: { + icon: "delete.png", + title: L.DELETE, + fn: function (event) { + const activeElement = document.activeElement; + const toFocus = activeElement.closest(".region"); + const list = require("views/articleList"); + if (list.currentData.name === "trash" || event.shiftKey) { + if (!confirm("Remove selected items permanently?")) { + return; + } + list.destroyBatch( + list.selectedItems, + list.removeItemCompletely + ); + } else { + list.destroyBatch(list.selectedItems, list.removeItem); + } + toFocus.focus(); + }, + }, + undelete: { + icon: "undelete.png", + title: L.UNDELETE, + fn: function () { + const articleList = require("views/articleList"); + if ( + !articleList.selectedItems || + !articleList.selectedItems.length || + articleList.currentData.name !== "trash" + ) { + return; + } + articleList.destroyBatch( + articleList.selectedItems, + articleList.undeleteItem + ); + }, + }, + selectNext: { + title: L.SELECT_NEXT_ARTICLE, + fn: function (event) { + require("views/articleList").selectNextSelectable(event); + }, + }, + selectPrevious: { + title: L.SELECT_PREVIOUS_ARTICLE, + fn: function (event) { + require("views/articleList").selectPrev(event); + }, + }, + search: { + title: L.SEARCH_TIP, + fn: function (event) { + event = event || { + currentTarget: + document.querySelector("input[type=search]"), + }; + let query = event.currentTarget.value || ""; + const list = require("views/articleList"); + if (query === "") { + [ + ...document.querySelectorAll( + ".date-group, .articles-list-item" + ), + ].map((element) => { + element.classList.remove("hidden"); + }); + return; + } else { + [...document.querySelectorAll(".date-group")].map( + (element) => { + element.classList.add("hidden"); + } + ); + } + + let searchInContent = false; + if (query[0] && query[0] === ":") { + query = query.replace(/^:/, "", query); + searchInContent = true; + } + RegExp.escape = function (text) { + return String(text).replace( + /[-[\]/{}()*+?.\\^$|]/g, + "\\$&" + ); + }; + const expression = new RegExp(RegExp.escape(query), "i"); + const selectedSpecial = document.querySelector( + ".sources-list-item.selected.special" + ); + list.views.some(function (view) { + if (!view.model) { + return false; + } + const sourceId = view.model.get("sourceID"); + const sourceItem = document.querySelector( + '[data-id="' + sourceId + '"]' + ); + if (!sourceItem) { + return false; + } + if (!sourceItem.classList.contains("selected")) { + const folderId = + sourceItem.view.model.get("folderID"); + const folderItem = document.querySelector( + '[data-id="' + folderId + '"]' + ); + + if (!selectedSpecial && !folderItem) { + return false; + } + } + const cleanedTitle = view.model + .get("title") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, ""); + const cleanedAuthor = view.model + .get("author") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, ""); + const cleanedContent = searchInContent + ? view.model + .get("content") + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + : ""; + + if ( + !( + expression.test(cleanedTitle) || + expression.test(cleanedAuthor) || + (searchInContent && + expression.test(cleanedContent)) + ) + ) { + view.el.classList.add("hidden"); + } else { + view.el.classList.remove("hidden"); + } + }); + }, + }, + focusSearch: { + title: L.FOCUS_SEARCH, + fn: function () { + document.querySelector("input[type=search]").focus(); + }, + }, + focus: { + title: L.FOCUS_ARTICLES, + fn: function () { + app.setFocus("articles"); + }, + }, + fullArticle: { + title: L.FULL_ARTICLE, + icon: "full_article.png", + fn: function (event) { + const articleList = app.articles.articleList; + if ( + !articleList.selectedItems || + !articleList.selectedItems.length + ) { + return; + } + if ( + articleList.selectedItems.length > 10 && + bg.getBoolean("askOnOpening") + ) { + if ( + !confirm( + "Do you really want to open " + + articleList.selectedItems.length + + " articles?" + ) + ) { + return; + } + } + const openNewTab = bg.settings.get("openNewTab"); + const active = + openNewTab === "background" + ? !!event.shiftKey + : !event.shiftKey; + articleList.selectedItems.forEach(function (item) { + browser.tabs.create({ + url: stripTags(item.model.get("url")), + active: active, + }); + }); + }, + }, + oneFullArticle: { + title: L.FULL_ARTICLE_SINGLE, + fn: function (event) { + event = event || {}; + const articleList = app.articles.articleList; + let view; + if ("currentTarget" in event) { + view = event.currentTarget.view; + } else { + if ( + !articleList.selectedItems || + !articleList.selectedItems.length + ) { + return; + } + view = articleList.selectedItems[0]; + } + if (view.model) { + const openNewTab = bg.settings.get("openNewTab"); + const active = + openNewTab === "background" + ? !!event.shiftKey + : !event.shiftKey; + + browser.tabs.create({ + url: stripTags(view.model.get("url")), + active: active, + }); + } + }, + }, + markAndNextUnread: { + title: L.MARK_AND_NEXT_UNREAD, + icon: "find_next.png", + fn: function () { + require("views/articleList").changeUnreadState({ + onlyToRead: true, + }); + require("views/articleList").selectNextSelectable({ + selectUnread: true, + }); + }, + }, + markAndPrevUnread: { + title: L.MARK_AND_PREV_UNREAD, + icon: "find_previous.png", + fn: function () { + require("views/articleList").changeUnreadState({ + onlyToRead: true, + }); + require("views/articleList").selectPrev({ + selectUnread: true, + }); + }, + }, + nextUnread: { + title: L.NEXT_UNREAD, + icon: "forward.png", + fn: function () { + require("views/articleList").selectNextSelectable({ + selectUnread: true, + }); + }, + }, + prevUnread: { + title: L.PREV_UNREAD, + icon: "back.png", + fn: function () { + require("views/articleList").selectPrev({ + selectUnread: true, + }); + }, + }, + markAllAsRead: { + title: L.MARK_ALL_AS_READ, + icon: "read_all.png", + fn: function () { + const articleList = require("views/articleList"); + const feeds = articleList.currentData.feeds; + var filter = articleList.currentData.filter; + if (feeds.length) { + (filter + ? bg.items.where(articleList.currentData.filter) + : bg.items + ).forEach(function (item) { + if ( + item.get("unread") === true && + feeds.indexOf(item.get("sourceID")) >= 0 + ) { + item.save({ unread: false, visited: true }); + } + }); + } else if (articleList.currentData.name === "all-feeds") { + if (confirm(L.MARK_ALL_QUESTION)) { + bg.items.forEach(function (item) { + if (item.get("unread") === true) { + item.save({ unread: false, visited: true }); + } + }); + } + } else if (articleList.currentData.filter) { + bg.items + .where(articleList.specialFilter) + .forEach(function (item) { + item.save({ unread: false, visited: true }); + }); + } + }, + }, + selectAll: { + title: L.SELECT_ALL_ARTICLES, + fn: function () { + const articleList = require("views/articleList"); + [...articleList.el.querySelectorAll(".selected")].forEach( + (element) => { + element.classList.remove("selected"); + } + ); + + articleList.selectedItems = []; + + [ + ...articleList.el.querySelectorAll( + ".articles-list-item:not(.hidden)" + ), + ].forEach((element) => { + element.view.el.classList.add("selected"); + articleList.selectedItems.push(element.view); + }); + + const lastSelected = + articleList.el.querySelector(".last-selected"); + if (lastSelected) { + lastSelected.classList.remove("last-selected"); + } + + const lastVisible = articleList.el.querySelector( + ".articles-list-item:not(.hidden):last-child" + ); + if (lastVisible) { + lastVisible.classList.add("last-selected"); + } + }, + }, + pin: { + title: L.PIN, + icon: "pinsource_context.png", + fn: function () { + const articleList = require("views/articleList"); + if ( + !articleList.selectedItems || + !articleList.selectedItems.length + ) { + return; + } + const isPinned = + !articleList.selectedItems[0].model.get("pinned"); + articleList.selectedItems.forEach(function (item) { + item.model.save({ pinned: isPinned }); + }); + }, + }, + spaceThrough: { + title: "Space Through", + fn: function () { + const articleList = require("views/articleList"); + if ( + !articleList.selectedItems || + !articleList.selectedItems.length + ) { + return; + } + app.trigger("space-pressed"); + }, + }, + pageUp: { + title: L.PAGE_UP, + fn: function () { + var el = require("views/articleList").el; + el.scrollByPages(-1); + }, + }, + pageDown: { + title: L.PAGE_DOWN, + fn: function () { + var el = require("views/articleList").el; + el.scrollByPages(1); + }, + }, + scrollToBottom: { + title: L.SCROLL_TO_BOTTOM, + fn: function () { + var el = require("views/articleList").el; + el.scrollTop = el.scrollHeight; + }, + }, + scrollToTop: { + title: L.SCROLL_TO_TOP, + fn: function () { + var el = require("views/articleList").el; + el.scrollTop = 0; + }, + }, + }, + content: { + changeView: { + title: L.CHANGE_VIEW, + icon: "report.png", + fn: function () { + const contentView = require("views/contentView"); + if (!contentView.model) { + return; + } + const view = + contentView.view === "feed" ? "mozilla" : "feed"; + contentView.render(view); + }, + }, + mark: { + title: L.MARK_AS_READ, + icon: "read.png", + fn: function () { + var contentView = require("views/contentView"); + if (!contentView.model) { + return; + } + contentView.model.save({ + unread: !contentView.model.get("unread"), + visited: true, + }); + }, + }, + delete: { + title: L.DELETE, + icon: "delete.png", + fn: function (e) { + const contentView = require("views/contentView"); + if (!contentView.model) { + return; + } + + const askRmPinned = bg.settings.get("askRmPinned"); + + if (e.shiftKey) { + if ( + contentView.model.get("pinned") && + askRmPinned && + askRmPinned !== "none" + ) { + let conf = confirm( + L.PIN_QUESTION_A + + contentView.model.escape("title") + + L.PIN_QUESTION_B + ); + if (!conf) { + return; + } + } + contentView.model.markAsDeleted(); + } else { + if ( + contentView.model.get("pinned") && + askRmPinned === "all" + ) { + let conf = confirm( + L.PIN_QUESTION_A + + contentView.model.escape("title") + + L.PIN_QUESTION_B + ); + if (!conf) { + return; + } + } + + contentView.model.trash(); + } + }, + }, + showConfig: { + title: L.SETTINGS, + icon: "config.png", + fn: function () { + let url = browser.runtime.getURL("options.html"); + browser.tabs.query( + { + url: url, + }, + function (tabs) { + if (tabs[0]) { + if (tabs[0].active) { + browser.tabs.remove(tabs[0].id); + } else { + browser.tabs.update(tabs[0].id, { + active: true, + }); + } + } else { + browser.tabs.create( + { + url: url, + }, + function () {} + ); + } + } + ); + }, + }, + focus: { + title: L.FOCUS_CONTENT, + fn: function () { + app.setFocus("content"); + }, + }, + focusSandbox: { + title: "Focus Article", + fn: function () { + app.content.sandbox.el.focus(); + }, + }, + scrollDown: { + title: L.SCROLL_DOWN, + fn: function () { + const cw = document.querySelector("iframe").contentWindow; + cw.scrollBy(0, 40); + }, + }, + scrollUp: { + title: L.SCROLL_UP, + fn: function () { + const cw = document.querySelector("iframe").contentWindow; + cw.scrollBy(0, -40); + }, + }, + spaceThrough: { + title: "Space trough", + fn: function () { + require("views/contentView").handleSpace(); + }, + }, + pageUp: { + title: L.PAGE_UP, + fn: function () { + const cw = document.querySelector("iframe").contentWindow; + const d = cw.document; + cw.scrollBy(0, -d.documentElement.clientHeight * 0.85); + }, + }, + pageDown: { + title: L.PAGE_DOWN, + fn: function () { + const cw = document.querySelector("iframe").contentWindow; + const d = cw.document; + cw.scrollBy(0, d.documentElement.clientHeight * 0.85); + }, + }, + scrollToBottom: { + title: L.SCROLL_TO_BOTTOM, + fn: function () { + const cw = document.querySelector("iframe").contentWindow; + const d = cw.document; + cw.scrollTo(0, d.documentElement.offsetHeight); + }, + }, + scrollToTop: { + title: L.SCROLL_TO_TOP, + fn: function () { + const cw = document.querySelector("iframe").contentWindow; + cw.scrollTo(0, 0); + }, + }, + }, + }; // end actions object +}); // end define function diff --git a/src/scripts/app/staticdb/shortcuts.js b/src/scripts/app/staticdb/shortcuts.js new file mode 100644 index 00000000..51acc8eb --- /dev/null +++ b/src/scripts/app/staticdb/shortcuts.js @@ -0,0 +1,22 @@ +define({ + keys: { + 8: 'backspace', + 9: 'tab', + 13: 'enter', + //16: 'shift', + //17: 'ctrl', + 20: 'capslock', + 27: 'esc', + 32: 'space', + 33: 'pgup', + 34: 'pgdown', + 35: 'end', + 36: 'home', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 45: 'insert', + 46: 'del' + } +}); diff --git a/src/scripts/app/templates/contentView.html b/src/scripts/app/templates/contentView.html new file mode 100644 index 00000000..8ac8009b --- /dev/null +++ b/src/scripts/app/templates/contentView.html @@ -0,0 +1,6 @@ +

+
+

+

+

+
diff --git a/src/scripts/app/templates/enclosureAudio.html b/src/scripts/app/templates/enclosureAudio.html new file mode 100644 index 00000000..44f97255 --- /dev/null +++ b/src/scripts/app/templates/enclosureAudio.html @@ -0,0 +1,8 @@ +
+ + + + +
diff --git a/src/scripts/app/templates/enclosureGeneral.html b/src/scripts/app/templates/enclosureGeneral.html new file mode 100644 index 00000000..ef27e059 --- /dev/null +++ b/src/scripts/app/templates/enclosureGeneral.html @@ -0,0 +1,3 @@ +

+ +

diff --git a/src/scripts/app/templates/enclosureImage.html b/src/scripts/app/templates/enclosureImage.html new file mode 100644 index 00000000..4fb8069e --- /dev/null +++ b/src/scripts/app/templates/enclosureImage.html @@ -0,0 +1,6 @@ +
+ + + + +
diff --git a/src/scripts/app/templates/enclosureVideo.html b/src/scripts/app/templates/enclosureVideo.html new file mode 100644 index 00000000..cc32ea3d --- /dev/null +++ b/src/scripts/app/templates/enclosureVideo.html @@ -0,0 +1,8 @@ +
+ + + + +
diff --git a/src/scripts/app/templates/enclosureYoutube.html b/src/scripts/app/templates/enclosureYoutube.html new file mode 100644 index 00000000..ea8998a6 --- /dev/null +++ b/src/scripts/app/templates/enclosureYoutube.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/scripts/app/templates/enclosureYoutubeCover.html b/src/scripts/app/templates/enclosureYoutubeCover.html new file mode 100644 index 00000000..89be8f55 --- /dev/null +++ b/src/scripts/app/templates/enclosureYoutubeCover.html @@ -0,0 +1,8 @@ +
+ + + +
+ +
+
diff --git a/scripts/app/templates/indicator.html b/src/scripts/app/templates/indicatorView.html similarity index 58% rename from scripts/app/templates/indicator.html rename to src/scripts/app/templates/indicatorView.html index c217bb95..32f8a590 100644 --- a/scripts/app/templates/indicator.html +++ b/src/scripts/app/templates/indicatorView.html @@ -1,6 +1,5 @@ -
- -
-
-
-
\ No newline at end of file +
+
+
+
+
diff --git a/src/scripts/app/templates/itemView.html b/src/scripts/app/templates/itemView.html new file mode 100644 index 00000000..fbcd0e1b --- /dev/null +++ b/src/scripts/app/templates/itemView.html @@ -0,0 +1,4 @@ +
+
+
+ diff --git a/src/scripts/app/templates/propertiesDetails.html b/src/scripts/app/templates/propertiesDetails.html new file mode 100644 index 00000000..907c1857 --- /dev/null +++ b/src/scripts/app/templates/propertiesDetails.html @@ -0,0 +1,23 @@ +
+ {{MORE}} + + + + + + +
diff --git a/src/scripts/app/templates/propertiesView.html b/src/scripts/app/templates/propertiesView.html new file mode 100644 index 00000000..82f8e5f5 --- /dev/null +++ b/src/scripts/app/templates/propertiesView.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + diff --git a/src/scripts/app/templates/specialView.html b/src/scripts/app/templates/specialView.html new file mode 100644 index 00000000..bd316852 --- /dev/null +++ b/src/scripts/app/templates/specialView.html @@ -0,0 +1,3 @@ + +
+
diff --git a/src/scripts/app/templates/topView.html b/src/scripts/app/templates/topView.html new file mode 100644 index 00000000..412a3b12 --- /dev/null +++ b/src/scripts/app/templates/topView.html @@ -0,0 +1,6 @@ + + + + +
+
diff --git a/src/scripts/app/views/ContextMenu.js b/src/scripts/app/views/ContextMenu.js new file mode 100644 index 00000000..7d425de0 --- /dev/null +++ b/src/scripts/app/views/ContextMenu.js @@ -0,0 +1,110 @@ +/** + * @module App + * @submodule views/ContextMenu + */ +define([ + 'backbone', 'collections/MenuCollection', 'views/MenuItemView', 'controllers/comm' + ], + function (BB, MenuCollection, MenuItemView, comm) { + + /** + * Context menu view + * @class ContextMenu + * @constructor + * @extends Backbone.View + */ + let ContextMenu = BB.View.extend({ + + /** + * Tag name of content view element + * @property tagName + * @default 'div' + * @type String + */ + tagName: 'div', + + /** + * Class name of content view element + * @property className + * @default 'context-menu' + * @type String + */ + className: 'context-menu', + + /** + * Backbone collection of all context menu items + * @property menuCollection + * @default 'context-menu' + * @type MenuCollection + */ + menuCollection: null, + + /** + * Adds one context menu item + * @method addItem + * @param item {models/MenuItem} New menu item + */ + addItem: function (item) { + const itemView = new MenuItemView({ + model: item + }); + itemView.contextMenu = this; + this.el.insertAdjacentElement('beforeend', itemView.render().el); + }, + + /** + * Adds multiple context menu items + * @method addItems + * @param items {Array|MenuCollection} List of models to add + */ + addItems: function (items) { + items.forEach((item) => { + this.addItem(item); + }); + }, + + + /** + * Called when new instance is created + * @method initialize + * @param mc {MenuCollection} Menu collection for this context menu + */ + initialize: function (mc) { + this.el.view = this; + this.el.hidden = true; + this.menuCollection = new MenuCollection(mc); + this.addItems(this.menuCollection); + document.body.appendChild(this.render().el); + this.listenTo(comm, 'hide-overlays', this.hide); + }, + + /** + * Displays the context menu and moves it to given position + * @method show + * @param x {Number} x-coordinate + * @param y {Number} y-coordinate + */ + show: function (x, y) { + this.el.hidden = false; + if (x + this.el.offsetWidth + 4 > document.body.offsetWidth) { + x = document.body.offsetWidth - this.el.offsetWidth - 8; + } + if (y + this.el.offsetHeight + 4 > document.body.offsetHeight) { + y = document.body.offsetHeight - this.el.offsetHeight - 8; + } + this.el.style.left = x + 'px'; + this.el.style.top = y + 'px'; + }, + + /** + * Hides the context menu + * @method hide + * @triggered when 'hide-overlays' comm message is sent + */ + hide: function () { + this.el.hidden = true; + } + }); + + return ContextMenu; + }); \ No newline at end of file diff --git a/src/scripts/app/views/FolderView.js b/src/scripts/app/views/FolderView.js new file mode 100644 index 00000000..9b272971 --- /dev/null +++ b/src/scripts/app/views/FolderView.js @@ -0,0 +1,214 @@ +/** + * @module App + * @submodule views/FolderView + */ +define([ + 'backbone', 'views/TopView', 'instances/contextMenus' + ], + function (BB, TopView, contextMenus) { + + /** + * View for Folder in feed list + * @class FolderView + * @constructor + * @extends views/TopView + */ + var FolderView = TopView.extend({ + + /** + * Set CSS classnames + * @property className + * @default 'list-item folder' + * @type String + */ + className: 'sources-list-item folder', + + template: `
+ + +
<%- title %>
+
<%- count %>
`, + + /** + * Reference to view/feedList instance. It should be replaced with require('views/feedList') + * @property list + * @default null + * @type Backbone.View + */ + list: null, + events: { + 'dblclick': 'handleDoubleClick', + /*'mouseup': 'handleMouseUp', + 'click': 'handleClick',*/ + 'click .folder-arrow': 'handleClickArrow' + }, + + /** + * Opens/closes folder by calling handleClickArrow method + * @method handleDoubleClick + * @triggered on double click on the folder + * @param event {MouseEvent} + */ + handleDoubleClick: function (event) { + this.handleClickArrow(event); + }, + + /** + * Shows context menu for folder + * @method showContextMenu + * @triggered on right mouse click + * @param event {MouseEvent} + */ + showContextMenu: function (event) { + if (!this.el.classList.contains('selected')) { + this.list.select(this, event); + } + contextMenus.get('folder').currentSource = this.model; + contextMenus.get('folder').show(event.clientX, event.clientY); + }, + + /** + * Initializations (*constructor*) + * @method initialize + * @param opt {Object} I don't use it, but it is automatically passed by Backbone + * @param list {Backbone.View} Reference to feedList + */ + initialize: function (opt, list) { + this.list = list; + this.el.view = this; + + this.model.on('destroy', this.handleModelDestroy, this); + this.model.on('change', this.render, this); + this.model.on('change:title', this.handleChangeTitle, this); + bg.sources.on('clear-events', this.handleClearEvents, this); + + this.el.dataset.id = this.model.get('id'); + }, + + /** + * Places folder to its right place after renaming + * @method handleChangeTitle + * @triggered when title of folder is changed + */ + handleChangeTitle: function () { + const folderViews = [...document.querySelectorAll('.folder')]; + this.list.insertBefore(this.render(), folderViews); + + const feedsInFolder = [...document.querySelectorAll('[data-in-folder="' + this.model.get('id') + '"')]; + + + feedsInFolder.forEach((element) => { + element.parentNode.removeChild(element); + }); + feedsInFolder.forEach((element) => { + this.list.placeSource(element.view); + }); + }, + + /** + * If the tab is closed, it will remove all events bound to bgprocess + * @method handleClearEvents + * @triggered when bgprocesses triggers clear-events event + * @param id {Number} ID of closed tab + */ + handleClearEvents: function (id) { + if (!window || id === tabID) { + this.clearEvents(); + } + }, + + /** + * Removes all events bound to bgprocess + * @method clearEvents + */ + clearEvents: function () { + this.model.off('destroy', this.handleModelDestroy, this); + this.model.off('change', this.render, this); + this.model.off('change:title', this.handleChangeTitle, this); + bg.sources.off('clear-events', this.handleClearEvents, this); + }, + + /** + * If the folder model is removed from DB/Backbone then remove it from DOM as well + * @method handleModelDestroy + * @triggered When model is removed from DB/Backbone + * @param id {Number} ID of closed tab + */ + handleModelDestroy: function () { + this.list.destroySource(this); + }, + + /** + * If user clicks on folder arrow then show/hide its content + * @method handleClickArrow + * @triggered Left click on folder arrow + * @param event {MouseEvent} + */ + handleClickArrow: function (event) { + let opened = !this.model.get('opened'); + this.model.save('opened', opened); + const items = document.querySelectorAll('.source[data-in-folder="' + this.model.get('id') + '"]'); + [...items].forEach((item) => { + item.hidden = !opened; + }); + event.stopPropagation(); + }, + + /** + * Reference to requestAnimationFrame frame. It is used to prevent multiple render calls in one frame + * @property renderInterval + * @type String|Number + */ + renderInterval: 'first-time', + + + /** + * Renders folder view + * @method render + */ + render: function () { + if (this.model.get('count') > 0) { + this.el.classList.add('has-unread'); + } else { + this.el.classList.remove('has-unread'); + } + + + const data = Object.create(this.model.attributes); + if (this.model.get('opened')) { + this.el.classList.add('opened'); + } else { + this.el.classList.remove('opened'); + } + while (this.el.firstChild) { + this.el.removeChild(this.el.firstChild); + } + + const fragment = document.createRange().createContextualFragment(this.template); + fragment.querySelector('.source-title').textContent = data.title; + fragment.querySelector('.source-counter').textContent = data.count; + this.el.appendChild(fragment); + + + this.setTitle(this.model.get('count'), this.model.get('countAll')); + this.el.href = '#'; + + return this; + }, + + /** + * Data to send to middle column (list of articles) when folder is selected + * @method render + * @param event {MouseEvent} + */ + getSelectData: function (event) { + return { + action: 'new-folder-select', + value: this.model.id, + unreadOnly: !!event.altKey + }; + } + }); + + return FolderView; + }); diff --git a/src/scripts/app/views/GroupView.js b/src/scripts/app/views/GroupView.js new file mode 100644 index 00000000..c300abb7 --- /dev/null +++ b/src/scripts/app/views/GroupView.js @@ -0,0 +1,78 @@ +/** + * @module App + * @submodule views/GroupView + */ +define(['backbone'], function (BB) { + + /** + * View for Date Groups in list of articles + * @class GroupView + * @constructor + * @extends Backbone.View + */ + let GroupView = BB.View.extend({ + + /** + * Tag name of date group element + * @property tagName + * @default 'div' + * @type String + */ + tagName: 'div', + + /** + * Class name of date group element + * @property className + * @default 'date-group' + * @type String + */ + className: 'date-group', + + /** + * Initializations (*constructor*) + * @method initialize + * @param model {models/Group} Date group model + * @param groups {Backbone.View} Reference to collection of groups + */ + initialize: function (model, groups) { + this.el.view = this; + this.listenTo(groups, 'reset', this.handleReset); + this.listenTo(groups, 'remove', this.handleRemove); + }, + + /** + * Renders date group view + * @method render + */ + render: function () { + this.el.textContent = this.model.get('title'); + return this; + }, + + /** + * If date group model is removed from collection of groups remove the DOM object + * @method handleRemove + * @triggered when any date group is removed from list of groups + * @param model {models/Group} Model removed from list of groups + */ + handleRemove: function (model) { + if (model === this.model) { + this.handleReset(); + } + }, + + /** + * If the reset model (that removes all models from collection) is called, removed DOM object of this date group + * and stop listening to any events of group collection. + * @method handleRemove + * @triggered when on reset + * @param model {models/Group} Model removed from list of groups + */ + handleReset: function () { + this.stopListening(); + this.el.parentNode.removeChild(this.el); + } + }); + + return GroupView; +}); \ No newline at end of file diff --git a/src/scripts/app/views/IndicatorView.js b/src/scripts/app/views/IndicatorView.js new file mode 100644 index 00000000..8f83be1e --- /dev/null +++ b/src/scripts/app/views/IndicatorView.js @@ -0,0 +1,103 @@ +/** + * @module App + * @submodule views/IndicatorView + */ +define(function (require) { + const BB = require("backbone"); + const Locale = require("modules/Locale"); + /** + * Feeds update indicator view + * @class IndicatorView + * @constructor + * @extends Backbone.View + */ + const IndicatorView = BB.View.extend({ + /** + * Indicator element id + * @property id + * @default indicator + */ + id: "indicator", + events: { + "click #indicator-stop": "handleButtonStop", + }, + + /** + * @method initialize + */ + initialize: function () { + this.loaded = 0; + this.maxSources = 0; + const fragment = document + .createRange() + .createContextualFragment( + require("text!templates/indicatorView.html") + ); + this.el.appendChild(fragment); + let port = browser.runtime.connect({ name: "port-from-cs" }); + port.onMessage.addListener((m) => { + if (m.key === "loading") { + this.loading = m.value; + } + if (m.key === "loaded") { + this.loaded = m.value; + } + if (m.key === "maxSources") { + this.maxSources = m.value; + } + this.render(); + }); + bg.sources.on("clear-events", this.handleClearEvents, this); + + this.render(); + }, + + /** + * Clears bg events it listens to + * @method handleClearEvents + * @param id {Integer} ID of the closed tab + */ + handleClearEvents: function (id) { + if (!window || id === tabID) { + bg.sources.off("clear-events", this.handleClearEvents, this); + } + }, + + /** + * Stops updating feeds + * @method handleButtonStop + * @triggered when user clicks on stop button + */ + handleButtonStop: function () { + app.actions.execute("feeds:stopUpdate"); + }, + + /** + * Renders the indicator (gradient/text) + * @method render + * @chainable + */ + render: function () { + this.el.classList.add("indicator-visible"); + + const { loaded, maxSources } = this; + if (maxSources === 0 || !this.loading) { + this.el.classList.add("indicator-invisible"); + return; + } + const percentage = Math.round((loaded * 100) / maxSources); + this.el.querySelector("#indicator-progress").style.background = + "linear-gradient(to right, #c5c5c5 " + + percentage + + "%, #eee " + + percentage + + "%)"; + this.el.querySelector("#indicator-progress").textContent = + Locale.UPDATING_FEEDS + " (" + loaded + "/" + maxSources + ")"; + this.el.classList.remove("indicator-invisible"); + return this; + }, + }); + + return IndicatorView; +}); diff --git a/src/scripts/app/views/ItemView.js b/src/scripts/app/views/ItemView.js new file mode 100644 index 00000000..e43322b3 --- /dev/null +++ b/src/scripts/app/views/ItemView.js @@ -0,0 +1,250 @@ +/** + * @module App + * @submodule views/ItemView + */ +define([ + 'backbone', 'helpers/dateUtils', 'instances/contextMenus', 'helpers/stripTags', 'text!templates/itemView.html' +], function (BB, dateUtils, contextMenus, stripTags, itemTemplate) { + + /** + * View of one article item in article list + * @class ItemView + * @constructor + * @extends Backbone.View + */ + let ItemView = BB.View.extend({ + + /** + * Tag name of article item element + * @property tagName + * @default 'a' + * @type String + */ + tagName: 'a', + + /** + * Class name of article item element + * @property className + * @default 'item' + * @type String + */ + className: 'articles-list-item', + + /** + * Reference to view/articleList instance. It should be replaced with require('views/articleList') + * @property list + * @default null + * @type Backbone.View + */ + list: null, + + /** + * Initializations (*constructor*) + * @method initialize + * @param opt {Object} I don't use it, but it is automatically passed by Backbone + * @param list {Backbone.View} Reference to articleList + */ + initialize: function (opt, list) { + this.multiple = opt.model.multiple; + this.list = list; + // this.el.setAttribute('draggable', 'true'); + this.el.view = this; + this.setEvents(); + }, + + /** + * Set events that are binded to bgprocess + * @method setEvents + */ + setEvents: function () { + this.model.on('change', this.handleModelChange, this); + this.model.on('destroy', this.handleModelDestroy, this); + bg.sources.on('clear-events', this.handleClearEvents, this); + }, + + + /** + * If the tab is closed, it will remove all events bound to bgprocess + * @method handleClearEvents + * @triggered when bgprocesses triggers clear-events event + * @param id {Number} ID of closed tab + */ + handleClearEvents: function (id) { + if (!window || id === tabID) { + this.clearEvents(); + } + }, + + /** + * Removes all events binded to bgprocess + * @method clearEvents + */ + clearEvents: function () { + if (this.model) { + this.model.off('change', this.handleModelChange, this); + this.model.off('destroy', this.handleModelDestroy, this); + } + bg.sources.off('clear-events', this.handleClearEvents, this); + }, + + /** + * Renders article item view + * @method render + * @chainable + */ + render: function () { + const classList = this.el.classList; + classList.remove('pinned'); + classList.remove('unvisited'); + classList.remove('unread'); + classList.remove('one-line'); + + if (!this.model.get('visited')) { + classList.add('unvisited'); + } + if (this.model.get('unread')) { + classList.add('unread'); + } + if (this.model.get('pinned')) { + classList.add('pinned'); + } + if (bg.settings.get('lines') === '1') { + classList.add('one-line'); + } + + const changedAttributes = this.model.changedAttributes(); + if (changedAttributes) { + const caKeys = Object.keys(changedAttributes); + if ((('unread' in changedAttributes || 'visited' in changedAttributes) && caKeys.length === 1) || ('unread' in changedAttributes && 'visited' in changedAttributes && caKeys.length === 2)) { + return this; + } + } + + let article = this.model.toJSON(); + article.datetime = new Date(article.date).toISOString(); + article.date = this.getItemDate(article.date); + if (this.multiple) { + const source = bg.sources.find({id: this.model.get('sourceID')}); + article.sourceTitle = source.get('title'); + if (bg.getBoolean('displayFaviconInsteadOfPin')) { + article.favicon = source.get('favicon'); + } + article.author = article.sourceTitle !== article.author ? article.sourceTitle + ' - ' + article.author : article.author; + } + this.el.setAttribute('href', article.url); + if (bg.getBoolean('showFullHeadline')) { + this.el.classList.add('full-headline'); + } else { + this.el.setAttribute('title', article.title); + } + + + while (this.el.firstChild) { + this.el.removeChild(this.el.firstChild); + } + + const fragment = document.createRange().createContextualFragment(itemTemplate); + const itemPin = fragment.querySelector('.item-pin'); + const icon = itemPin.querySelector('.icon'); + if (typeof article.favicon !== 'undefined') { + icon.src = article.favicon; + } else { + itemPin.removeChild(icon); + } + fragment.querySelector('.item-author').textContent = article.author; + fragment.querySelector('.item-title').textContent = article.title; + fragment.querySelector('.item-date').textContent = article.date; + fragment.querySelector('.item-date').setAttribute('datetime', article.datetime); + + this.el.appendChild(fragment); + + return this; + }, + + /** + * Returns formatted date according to user settings and time interval + * @method getItemDate + * @param date {Number} UTC time + * @return String + */ + getItemDate: function (date) { + const dateFormats = {normal: 'DD.MM.YYYY', iso: 'YYYY-MM-DD', us: 'MM/DD/YYYY'}; + const pickedFormat = dateFormats[bg.settings.get('dateType') || 'normal'] || dateFormats['normal']; + + const timeFormat = bg.settings.get('hoursFormat') === '12h' ? 'H:mm a' : 'hh:mm'; + + if (date) { + if (bg.getBoolean('fullDate')) { + date = dateUtils.formatDate(date, pickedFormat + ' ' + timeFormat); + } else if (Math.floor(dateUtils.formatDate(date, 'T') / 86400000) >= Math.floor(dateUtils.formatDate(Date.now(), 'T') / 86400000)) { + date = dateUtils.formatDate(date, timeFormat); + } else if ((new Date(date)).getFullYear() === (new Date()).getFullYear()) { + date = dateUtils.formatDate(date, pickedFormat.replace(/\/?YYYY(?!-)/, '')); + } else { + date = dateUtils.formatDate(date, pickedFormat); + } + } + + return date; + }, + + /** + * Shows context menu on right click + * @method handleMouseUp + * @triggered on mouse up + condition for right click only + * @param event {MouseEvent} + */ + handleMouseUp: function (event) { + if (event.button === 2) { + this.showContextMenu(event); + } + }, + + /** + * Shows context menu for article item + * @method showContextMenu + * @param event {MouseEvent} + */ + showContextMenu: function (event) { + if (!this.el.classList.contains('selected')) { + this.list.select(this, event); + } + contextMenus.get('items').currentSource = this.model; + contextMenus.get('items').show(event.clientX, event.clientY); + }, + + /** + * When model is changed rerender it or remove it from DOM (depending on what is changed) + * @method handleModelChange + * @triggered when model is changed + */ + handleModelChange: function () { + if (this.model.get('deleted') || (this.list.currentData.name !== 'trash' && this.model.get('trashed'))) { + this.list.destroyItem(this); + } else { + this.render(); + } + }, + + /** + * When model is removed from DB/Backbone remove it from DOM as well + * @method handleModelDestroy + * @triggered when model is destroyed + */ + handleModelDestroy: function () { + this.list.destroyItem(this); + }, + + /** + * Changes pin state (true/false) + * @method when user clicked on pin button in article item + * @triggered when model is destroyed + */ + handleClickPin: function (event) { + event.stopPropagation(); + this.model.save({pinned: !this.model.get('pinned')}); + } + }); + + return ItemView; +}); diff --git a/src/scripts/app/views/MenuItemView.js b/src/scripts/app/views/MenuItemView.js new file mode 100644 index 00000000..520e3a8c --- /dev/null +++ b/src/scripts/app/views/MenuItemView.js @@ -0,0 +1,35 @@ +define(['backbone'], function (BB) { + return BB.View.extend({ + tagName: 'div', + className: 'context-menu-item', + contextMenu: null, + events: { + 'click': 'handleClick' + }, + initialize: function () { + if (this.model.id) { + this.el.id = this.model.id; + } + }, + render: function () { + while (this.el.firstChild) { + this.el.removeChild(this.el.firstChild); + } + + const fragment = document.createRange().createContextualFragment('' + this.model.get('title')); + this.el.appendChild(fragment); + + if (this.model.get('icon')) { + this.el.querySelector('img').setAttribute('src', '/images/' + this.model.get('icon')); + } + return this; + }, + handleClick: function (e) { + const action = this.model.get('action'); + if (action && typeof action === 'function') { + action(e, app.feeds.feedList); + } + this.contextMenu.hide(); + } + }); +}); \ No newline at end of file diff --git a/src/scripts/app/views/Properties.js b/src/scripts/app/views/Properties.js new file mode 100644 index 00000000..3a60708d --- /dev/null +++ b/src/scripts/app/views/Properties.js @@ -0,0 +1,254 @@ +define([ + 'backbone', 'modules/Locale', 'text!templates/propertiesView.html', 'text!templates/propertiesDetails.html' + ], + function (BB, Locale, propertiesTemplate, propertiesDetails) { + + return BB.View.extend({ + id: 'properties', + current: null, + events: { + 'click button': 'handleClick', + 'keydown button': 'handleKeyDown' + }, + handleClick: function (event) { + const target = event.currentTarget; + if (target.id === 'prop-cancel') { + this.hide(); + } else if (target.id === 'prop-ok') { + this.saveData(); + } + }, + saveData: function () { + if (!this.current) { + this.hide(); + return; + } + + const updateEvery = parseInt(document.querySelector('#prop-update-every').value); + const autoRemove = parseInt(document.querySelector('#prop-autoremove').value); + const folderId = document.querySelector('#prop-parent').value; + + if (this.current instanceof bg.Source) { + /* encrypt the password */ + this.current.setPass(document.querySelector('#prop-password').value); + const defaultView = document.querySelector('#defaultView').value; + + + this.current.save({ + title: document.querySelector('#prop-title').value, + url: app.fixURL(document.querySelector('#prop-url').value), + username: document.querySelector('#prop-username').value, + folderID: folderId, + updateEvery: updateEvery, + autoremove: autoRemove, + proxyThroughFeedly: document.querySelector('#prop-proxy').checked, + openEnclosure: document.querySelector('#openEnclosure').value, + defaultView: defaultView + }); + if (folderId === '0') { + this.current.unset('folderID'); + } + // this.render(); + } else { + let iterator = []; + if (this.current instanceof bg.Folder) { + iterator = bg.sources.where({folderID: this.current.id}); + this.current.save({ + title: document.querySelector('#prop-title').value + }); + + } else if (Array.isArray(this.current)) { + iterator = this.current; + } + if (updateEvery >= -1) { + iterator.forEach(function (source) { + source.save({updateEvery: updateEvery}); + }); + } + if (autoRemove >= -1) { + iterator.forEach(function (source) { + source.save({autoremove: autoRemove}); + }); + } + + if (folderId >= 0) { + iterator.forEach(function (source) { + if (folderId === '0') { + source.unset('folderID'); + } else { + source.save({folderID: folderId}); + } + }); + } + } + this.hide(); + + }, + handleKeyDown: function (e) { + if (e.keyCode === 13) { + this.handleClick(e); + } + }, + initialize: function () { + this.el.hidden = true; + }, + renderSource: function () { + /* decrypt password */ + const properties = this.current.toJSON(); + properties.password = this.current.getPass(); + + const fragment = document.createRange().createContextualFragment(Locale.translateHTML(propertiesTemplate)); + + fragment.querySelector('#property-title-label input').value = properties.title; + fragment.querySelector('#property-title-address input').value = properties.url; + const details = document.createRange().createContextualFragment(Locale.translateHTML(propertiesDetails)); + details.querySelector('#prop-username').value = properties.username; + details.querySelector('#prop-password').value = properties.password; + details.querySelector('#prop-proxy').value = properties.proxyThroughFeedly; + + fragment.insertBefore(details, fragment.querySelector('button')); + + this.el.appendChild(fragment); + + let folders = bg.folders; + let parentSelect = document.querySelector('#prop-parent'); + folders.forEach((folder) => { + const option = document.createElement('option'); + option.textContent = folder.get('title'); + option.setAttribute('value', folder.get('id')); + if (folder.get('id') === this.current.get('folderID')) { + option.setAttribute('selected', ''); + } + parentSelect.insertAdjacentElement('beforeend', option); + }); + + document.querySelector('#prop-update-every').value = this.current.get('updateEvery'); + document.querySelector('#prop-autoremove').value = this.current.get('autoremove'); + document.querySelector('#openEnclosure').value = this.current.get('openEnclosure'); + document.querySelector('#defaultView').value = this.current.get('defaultView'); + document.querySelector('#prop-proxy').checked = !!this.current.get('proxyThroughFeedly'); + }, + renderGroup: function () { + const isFolder = this.current instanceof bg.Folder; + const listOfSources = isFolder ? bg.sources.where({folderID: this.current.id}) : this.current; + + const params = { + updateEveryDiffers: false, + autoremoveDiffers: false, + folderIdDiffers: false, + firstUpdate: listOfSources[0].get('updateEvery'), + firstAutoremove: listOfSources[0].get('autoremove'), + firstFolderId: listOfSources[0].get('folderID') + }; + + + const properties = isFolder ? Object.assign(params, this.current.attributes) : params; + + /** + * Test if all selected feeds has the same properties or if they are mixed + */ + if (!isFolder) { + params.updateEveryDiffers = listOfSources.some(function (c) { + if (params.firstUpdate !== c.get('updateEvery')) { + return true; + } + }); + + params.autoremoveDiffers = listOfSources.some(function (c) { + if (params.firstAutoremove !== c.get('autoremove')) { + return true; + } + }); + + params.folderIdDiffers = listOfSources.some(function (c) { + if (params.firstFolderId !== c.get('folderID')) { + return true; + } + }); + } + + /** + * Create HTML + */ + const fragment = document.createRange().createContextualFragment(Locale.translateHTML(propertiesTemplate)); + + const labelTitle = fragment.querySelector('#property-title-label'); + if (properties.title) { + labelTitle.querySelector('input').value = properties.title; + } else { + fragment.removeChild(labelTitle); + } + const labelUrl = fragment.querySelector('#property-title-address'); + if (properties.url) { + labelUrl.querySelector('input').value = properties.url; + } else { + fragment.removeChild(labelUrl); + } + + + const folders = bg.folders; + const parentSelect = fragment.querySelector('#prop-parent'); + folders.forEach((folder) => { + const option = document.createElement('option'); + option.textContent = folder.get('title'); + option.setAttribute('value', folder.get('id')); + parentSelect.insertAdjacentElement('beforeend', option); + }); + + this.el.appendChild(fragment); + + + const elementUpdateEvery = document.querySelector('#prop-update-every'); + if (properties.updateEveryDiffers) { + const option = document.createRange().createContextualFragment(``); + elementUpdateEvery.prepend(option); + elementUpdateEvery.value = -2; + } else { + elementUpdateEvery.value = params.firstUpdate; + } + + const elementParent = document.querySelector('#prop-parent'); + if (properties.folderIdDiffers) { + const option = document.createRange().createContextualFragment(``); + elementParent.prepend(option); + elementParent.value = -2; + } else { + elementParent.value = params.firstFolderId; + } + + const elementAutoremove = document.querySelector('#prop-autoremove'); + if (properties.autoremoveDiffers) { + const option = document.createRange().createContextualFragment(``); + elementAutoremove.prepend(option); + elementAutoremove.value = -2; + } else { + elementAutoremove.value = params.firstAutoremove; + } + }, + + render: function () { + if (!this.current) { + return; + } + while (this.el.firstChild) { + this.el.removeChild(this.el.firstChild); + } + + if (this.current instanceof bg.Source) { + this.renderSource(); + } else { + this.renderGroup(); + } + return this; + }, + show: function (source) { + this.current = source; + this.render(); + + this.el.hidden = false; + }, + hide: function () { + this.el.hidden = true; + } + }); + }); diff --git a/src/scripts/app/views/SandboxView.js b/src/scripts/app/views/SandboxView.js new file mode 100644 index 00000000..6238b5be --- /dev/null +++ b/src/scripts/app/views/SandboxView.js @@ -0,0 +1,42 @@ +define(["backbone", "modules/Locale"], function (BB, Locale) { + return BB.View.extend({ + tagName: "iframe", + loaded: false, + events: { + load: "handleLoad", + }, + initialize: function () { + this.el.setAttribute("src", "rss_content.html"); + this.el.setAttribute("name", "sandbox"); + this.el.setAttribute("frameborder", 0); + this.el.setAttribute("tabindex", -1); + }, + render: function () { + return this; + }, + handleLoad: function () { + this.loaded = true; + this.el.contentDocument.querySelector( + "#smart-rss-url" + ).textContent = Locale.translate("FULL_ARTICLE"); + this.el.contentDocument.addEventListener( + "keydown", + app.handleKeyDown + ); + + const baseStylePath = browser.runtime.getURL("styles/main.css"); + this.el.contentDocument + .querySelector("[data-base-style]") + .setAttribute("href", baseStylePath); + + const darkStylePath = browser.runtime.getURL("styles/dark.css"); + this.el.contentDocument + .querySelector("[data-dark-style]") + .setAttribute("href", darkStylePath); + this.el.contentDocument.querySelector( + "[data-custom-style]" + ).innerHTML = bg.settings.get("userStyle"); + this.trigger("load"); + }, + }); +}); diff --git a/src/scripts/app/views/SourceView.js b/src/scripts/app/views/SourceView.js new file mode 100644 index 00000000..0fc1cb5a --- /dev/null +++ b/src/scripts/app/views/SourceView.js @@ -0,0 +1,89 @@ +define([ + 'backbone', 'views/TopView', 'instances/contextMenus' + ], + function (BB, TopView, contextMenus) { + return TopView.extend({ + className: 'sources-list-item source', + list: null, + initialize: function (opt, list) { + this.list = list; + this.model.on('change', this.render, this); + this.model.on('destroy', this.handleModelDestroy, this); + this.model.on('change:title', this.handleChangeTitle, this); + bg.sources.on('clear-events', this.handleClearEvents, this); + this.el.dataset.id = this.model.get('id'); + this.el.view = this; + }, + handleClearEvents: function (id) { + if (!window || id === tabID) { + this.clearEvents(); + } + }, + clearEvents: function () { + this.model.off('change', this.render, this); + this.model.off('destroy', this.handleModelDestroy, this); + this.model.off('change:title', this.handleChangeTitle, this); + bg.sources.off('clear-events', this.handleClearEvents, this); + }, + showContextMenu: function (e) { + if (!this.el.classList.contains('selected')) { + app.feeds.feedList.select(this, e); + } + contextMenus.get('source').currentSource = this.model; + contextMenus.get('source').show(e.clientX, e.clientY); + }, + handleChangeTitle: function () { + this.list.placeSource(this); + }, + handleModelDestroy: function () { + this.list.destroySource(this); + }, + render: function () { + this.el.classList.remove('has-unread'); + this.el.classList.remove('loading'); + this.el.classList.remove('broken'); + + if (this.model.get('count') > 0) { + this.el.classList.add('has-unread'); + } + + if ((this.model.get('errorCount') > 0) && !this.model.get('isLoading')) { + this.el.classList.add('broken'); + } + + if (this.model.get('isLoading')) { + this.el.classList.add('loading'); + } + + + if (this.model.get('folderID') !== '0') { + this.el.dataset.inFolder = this.model.get('folderID'); + } else { + this.el.hidden = false; + delete this.el.dataset.inFolder; + } + + this.setTitle(this.model.get('count'), this.model.get('countAll')); + + while (this.el.firstChild) { + this.el.removeChild(this.el.firstChild); + } + const data = this.model.toJSON(); + const fragment = document.createRange().createContextualFragment(this.template); + fragment.querySelector('.source-icon.icon').src = data.favicon; + fragment.querySelector('.source-title').textContent = data.title; + fragment.querySelector('.source-counter').textContent = data.count; + this.el.appendChild(fragment); + this.el.href = data.base; + + if (bg.sourceToFocus === this.model.get('id')) { + setTimeout(function () { + app.trigger('focus-feed', bg.sourceToFocus); + bg.sourceToFocus = null; + }, 0); + } + + return this; + } + }); + }); diff --git a/src/scripts/app/views/SpecialView.js b/src/scripts/app/views/SpecialView.js new file mode 100644 index 00000000..c829f3c3 --- /dev/null +++ b/src/scripts/app/views/SpecialView.js @@ -0,0 +1,105 @@ +define(['views/TopView', 'text!templates/specialView.html'], + function (TopView, specialView) { + return TopView.extend({ + className: 'sources-list-item special', + /*events: { + 'mouseup': 'handleMouseUp', + 'click': 'handleClick' + },*/ + showContextMenu: function (e) { + if (!this.contextMenu) { + return; + } + + if (!this.el.classList.contains('selected')) { + app.feeds.feedList.select(this, e); + } + this.contextMenu.currentSource = this.model; + this.contextMenu.show(e.clientX, e.clientY); + }, + initialize: function () { + this.el.view = this; + if (this.model.get('onReady')) { + this.model.get('onReady').call(this); + } + bg.info.on('change', this.changeInfo, this); + bg.sources.on('clear-events', this.handleClearEvents, this); + }, + handleClearEvents: function (id) { + if (window === null || id === tabID) { + this.clearEvents(); + } + }, + clearEvents: function () { + bg.info.off('change', this.changeInfo, this); + bg.sources.off('clear-events', this.handleClearEvents, this); + }, + changeInfo: function () { + if (this.model.get('name') === 'all-feeds') { + const changed = bg.info.changedAttributes(); + if (changed && typeof changed === 'object' && 'allCountUnread' in changed) { + this.render(true); + } + this.setTitle(bg.info.get('allCountUnread'), bg.info.get('allCountTotal')); + return; + } + if (this.model.get('name') === 'pinned') { + const changed = bg.info.changedAttributes(); + if (changed && typeof changed === 'object' && 'pinnedCountUnread' in changed) { + this.render(true); + } + this.setTitle(bg.info.get('pinnedCountUnread'), bg.info.get('pinnedCountTotal')); + return; + } + if (this.model.get('name') === 'trash') { + const tot = bg.info.get('trashCountTotal'); + this.setTitle(bg.info.get('trashCountUnread'), tot); + + /** + * Change trash icon (0, 1-99, 100+) + */ + if (tot <= 0 && this.model.get('icon') !== 'trashsource.png') { + this.model.set('icon', 'trashsource.png'); + this.render(true); + } else if (tot > 0 && tot < 100 && this.model.get('icon') !== 'trash_full.png') { + this.model.set('icon', 'trash_full.png'); + this.render(true); + } else if (tot >= 100 && this.model.get('icon') !== 'trash_really_full.png') { + this.model.set('icon', 'trash_really_full.png'); + this.render(true); + } + } + }, + render: function (noinfo) { + this.el.classList.remove('has-unread'); + const data = this.model.toJSON(); + data.count = 0; + if (this.model.get('name') === 'all-feeds') { + data.count = bg.info.get('allCountUnread'); + } + + if (this.model.get('name') === 'pinned') { + data.count = bg.info.get('pinnedCountUnread'); + } + + if (data.count > 0) { + this.el.classList.add('has-unread'); + } + while (this.el.firstChild) { + this.el.removeChild(this.el.firstChild); + } + + const fragment = document.createRange().createContextualFragment(specialView); + fragment.querySelector('.source-icon').src = '/images/' + data.icon; + fragment.querySelector('.source-title').textContent = data.title; + fragment.querySelector('.source-counter').textContent = data.count; + this.el.appendChild(fragment); + this.el.href = '#'; + + if (!noinfo) { + this.changeInfo(); + } + return this; + } + }); + }); diff --git a/src/scripts/app/views/ToolbarButtonView.js b/src/scripts/app/views/ToolbarButtonView.js new file mode 100644 index 00000000..4fe33b99 --- /dev/null +++ b/src/scripts/app/views/ToolbarButtonView.js @@ -0,0 +1,33 @@ +define(['backbone'], function (BB) { + return BB.View.extend({ + tagName: 'div', + className: 'button', + initialize: function () { + const updateButtonState = () => { + this.el.classList.remove('active'); + if (action.get('state')) { + if (bg.getBoolean(action.get('state'))) { + console.log(action.get('state'), bg.getBoolean(action.get('state'))); + this.el.classList.add('active'); + } + } + }; + + const action = app.actions.get(this.model.get('actionName')); + if (action.get('icon')) { + this.el.style.background = 'url("/images/' + action.get('icon') + '") no-repeat center center'; + } + if (action.get('glyph')) { + this.el.textContent = action.get('glyph'); + } + + + this.el.dataset.action = this.model.get('actionName'); + this.el.title = action.get('title'); + updateButtonState(); + this.el.view = this; + + bg.settings.on('change', updateButtonState); + } + }); +}); diff --git a/src/scripts/app/views/ToolbarDynamicSpaceView.js b/src/scripts/app/views/ToolbarDynamicSpaceView.js new file mode 100644 index 00000000..748a0548 --- /dev/null +++ b/src/scripts/app/views/ToolbarDynamicSpaceView.js @@ -0,0 +1,9 @@ +define(['backbone', ], function (BB) { + return BB.View.extend({ + tagName: 'div', + className: 'dynamic-space', + initialize: function () { + this.el.view = this; + } + }); +}); diff --git a/src/scripts/app/views/ToolbarSearchView.js b/src/scripts/app/views/ToolbarSearchView.js new file mode 100644 index 00000000..8792ea36 --- /dev/null +++ b/src/scripts/app/views/ToolbarSearchView.js @@ -0,0 +1,18 @@ +define(['backbone', 'modules/Locale'], function (BB, Locale) { + return BB.View.extend({ + tagName: 'input', + className: 'input-search', + initialize: function () { + this.el.setAttribute('placeholder', Locale.SEARCH); + this.el.setAttribute('type', 'search'); + this.el.setAttribute('tabindex', -1); + + const action = app.actions.get(this.model.get('actionName')); + + this.el.dataset.action = this.model.get('actionName'); + this.el.title = action.get('title'); + + this.el.view = this; + } + }); +}); diff --git a/src/scripts/app/views/ToolbarView.js b/src/scripts/app/views/ToolbarView.js new file mode 100644 index 00000000..f1a051f1 --- /dev/null +++ b/src/scripts/app/views/ToolbarView.js @@ -0,0 +1,142 @@ +define([ + 'backbone', 'collections/ToolbarItems', 'factories/ToolbarItemsFactory' + ], + function (BB, ToolbarItems, ToolbarItemsFactory) { + return BB.View.extend({ + tagName: 'div', + className: 'toolbar', + items: null, + doNotRegenerate: false, + events: { + 'click .button': 'handleButtonClick', + 'input input[type=search]': 'handleButtonClick', + }, + initialize: function () { + this.el.view = this; + this.items = new ToolbarItems(); + + this.listenTo(this.items, 'add', this.addToolbarItem); + this.listenTo(this.model, 'change', this.handleChange); + bg.sources.on('clear-events', this.handleClearEvents, this); + + + this.model.get('actions').forEach(this.createToolbarItem, this); + this.hideItems('articles:undelete'); + }, + + /** + * If the tab is closed, it will remove all events bound to bgprocess + * @method handleClearEvents + * @triggered when bgprocesses triggers clear-events event + * @param id {Number} ID of closed tab + */ + handleClearEvents: function (id) { + if (!window || id === tabID) { + this.stopListening(); + bg.sources.off('clear-events', this.handleClearEvents, this); + } + }, + + /** + * Regenerates DOM of items according to new changes + * @triggered when some change to Toolbar model happens + * @method handleChange + */ + handleChange: function () { + if (!this.doNotRegenerate) { + while (this.el.firstChild) { + this.el.removeChild(this.el.firstChild); + } + this.items.reset(); + + this.model.get('actions').forEach(this.createToolbarItem, this); + + if (app.articles.articleList.currentData.name === 'trash') { + this.hideItems('articles:update'); + } else { + this.hideItems('articles:undelete'); + } + } + }, + /** + * List of hidden toolbar items. The reason for storing hidden items is so that we can show all hidden items when customizing ui. + * @param hiddenItems + * @type Array + */ + hiddenItems: [], + + /** + * Hides items from toolbar (e.g. update action while in trash) + * @method hideItems + * @chainable + */ + hideItems: function (action) { + const list = [...this.el.querySelectorAll('[data-action="' + action + '"]')]; + + list.forEach((item) => { + item.hidden = true; + }); + + this.hiddenItems = [...new Set(this.hiddenItems.concat(list))]; + + return this; + }, + + /** + * Shows again hidden items from toolbar (e.g. update action while going away from trash) + * @method showItems + * @chainable + */ + showItems: function (action) { + this.hiddenItems = this.hiddenItems.filter((item) => { + if (item.dataset.action === action) { + item.hidden = false; + return false; + } + return true; + }); + + return this; + }, + + + /** + * Called for every Toolbar action when ToolbarView is initialized + * @method createToolbarItem + * @param action {String} Name of the action + */ + createToolbarItem: function (action) { + if (action === '!dynamicSpace') { + this.items.add({type: 'dynamicSpace'}); + return null; + } + this.items.add({actionName: action, type: 'button'}); + }, + handleButtonClick: function (e) { + const button = e.currentTarget.view.model; + app.actions.execute(button.get('actionName'), e); + }, + render: function () { + return this; + }, + + /** + * Called when new model was added to _items_ collection + * @method addToolbarItem + * @param toolbarItem {ToolbarButton} Model added to the collection + */ + addToolbarItem: function (toolbarItem) { + let view; + if (toolbarItem.get('actionName') === 'articles:search') { + view = ToolbarItemsFactory.create('search', toolbarItem); + } else if (toolbarItem.get('type') !== 'dynamicSpace') { + view = ToolbarItemsFactory.create('button', toolbarItem); + } else { + view = ToolbarItemsFactory.create('dynamicSpace', toolbarItem); + } + + this.el.insertAdjacentElement('beforeend', view.render().el); + toolbarItem.view = view; + } + }); + }); diff --git a/src/scripts/app/views/TopView.js b/src/scripts/app/views/TopView.js new file mode 100644 index 00000000..bcbe7a73 --- /dev/null +++ b/src/scripts/app/views/TopView.js @@ -0,0 +1,27 @@ +define([ + 'backbone', 'modules/Locale', 'text!templates/topView.html', +], function (BB, Locale, topViewTemplate) { + return BB.View.extend({ + tagName: 'a', + template: topViewTemplate, + className: 'sources-list-item', + handleMouseUp: function (e) { + if (e.button === 2) { + this.showContextMenu(e); + } + }, + getSelectData: function (e) { + return { + action: 'new-select', + value: this.model.id || Object.assign({}, this.model.get('filter')), + name: this.model.get('name'), + unreadOnly: !!e.altKey + }; + }, + setTitle: function (unread, total) { + this.el.setAttribute('title', + this.model.get('title') + ' (' + unread + ' ' + Locale.UNREAD + ', ' + total + ' ' + Locale.TOTAL + ')' + ); + } + }); +}); diff --git a/src/scripts/app/views/articleList.js b/src/scripts/app/views/articleList.js new file mode 100644 index 00000000..f3ebfdb1 --- /dev/null +++ b/src/scripts/app/views/articleList.js @@ -0,0 +1,669 @@ +/** + * @module App + * @submodule views/articleList + */ +define([ + 'backbone', 'collections/Groups', 'models/Group', 'views/GroupView', + 'views/ItemView', 'mixins/selectable', 'modules/Locale' + ], + function (BB, Groups, Group, GroupView, ItemView, selectable, Locale) { + + const groups = new Groups(); + + /** + * List of articles + * @class ArticleListView + * @constructor + * @extends Backbone.View + */ + let ArticleListView = BB.View.extend({ + + /** + * Tag name of article list element + * @property tagName + * @default 'div' + * @type String + */ + tagName: 'div', + + /** + * ID of article list + * @property id + * @default 'article-list' + * @type String + */ + id: 'article-list', + + /** + * Class of article views + * @property itemClass + * @default 'item' + * @type string + */ + itemClass: 'articles-list-item', + + /** + * Unordered list of all article views + * @property views + * @default [] + * @type Array + */ + views: [], + + /** + * Data received from feedList about current selection (feed ids, name of special, filter, unreadOnly) + * @property currentData + * @default { feeds: [], name: 'all-feeds', filter: { trashed: false}, unreadOnly: false } + * @type Object + */ + currentData: { + feeds: [], + name: 'all-feeds', + filter: {trashed: false}, + unreadOnly: false + }, + + /** + * Flag to prevent focusing more items in one tick + * @property noFocus + * @default false + * @type Boolean + */ + noFocus: false, + + currentRenderId: 0, + + events: { + // 'dragstart .articles-list-item': 'handleDragStart', + 'mousedown .articles-list-item': 'handleMouseDown', + 'click .articles-list-item': 'handleClick', + 'mouseup .articles-list-item': 'handleMouseUp', + 'dblclick .articles-list-item': 'handleItemDblClick', + 'mousedown .item-pin,.item-pinned': 'handleClickPin' + }, + + /** + * Opens articles url in new tab + * @method handleItemDblClick + * @triggered on double click on article + */ + handleItemDblClick: function () { + app.actions.execute('articles:oneFullArticle'); + }, + + handleMouseDown(event) { + if (event.button === 1) { + const linkElement = event.target.closest('a'); + if (typeof browser !== 'undefined') { + this.prefetcher.href = linkElement.href; + } + } + }, + + /** + * Selects article + * @method handleClick + * @triggered on click on article + * @param event {MouseEvent} + */ + handleClick: function (event) { + this.handleSelectableMouseDown(event); + }, + + /** + * Changes pin state + * @method handleClickPin + * @triggered on click on pin button + * @param event {MouseEvent} + */ + handleClickPin: function (event) { + event.currentTarget.parentNode.view.handleClickPin(event); + }, + + /** + * Calls necessary select methods + * @method handleMouseUp + * @triggered on mouse up on article + * @param event {MouseEvent} + */ + handleMouseUp: function (event) { + event.currentTarget.view.handleMouseUp(event); + this.handleSelectableMouseUp(event); + }, + + /** + * Called when new instance is created + * @method initialize + */ + initialize: function () { + if (typeof browser !== 'undefined') { + this.prefetcher = document.createElement('link'); + this.prefetcher.rel = 'preload'; + this.prefetcher.setAttribute('as', 'fetch'); + this.prefetcher.setAttribute('crossorigin', 'crossorigin'); + document.head.appendChild(this.prefetcher); + } + + + bg.items.on('reset', this.addItems, this); + bg.items.on('add', this.addItem, this); + bg.items.on('sort', this.handleSort, this); + bg.items.on('search', this.handleSearch, this); + bg.sources.on('destroy', this.handleSourcesDestroy, this); + bg.sources.on('clear-events', this.handleClearEvents, this); + bg.settings.on('change', this.onSettingsChange, this); + + groups.on('add', this.addGroup, this); + + this.on('attach', this.handleAttached, this); + this.on('pick', this.handlePick, this); + }, + + onSettingsChange: function(){ + this.unreadOnly = bg.getBoolean('defaultToUnreadOnly'); + this.handleNewSelected(this.currentData); + }, + + /** + * Sends msg to show selected article + * @method handlePick + * @triggered when one article is selected + * @param view {views/ItemView} + */ + handlePick: function (view) { + if (!view.model.collection) { + // This shouldn't usually happen + // It might happen when source is deleted and created in the same tick + return; + } + app.trigger('select:' + this.el.id, {action: 'new-select', value: view.model.id}); + + if (view.model.get('unread') && bg.getBoolean('readOnVisit')) { + view.model.save({ + visited: true, + unread: false + }); + } else if (!view.model.get('visited')) { + view.model.save('visited', true); + } + }, + + /** + * Sets comm event listeners + * @method handleAttached + * @triggered when article list is attached to DOM + */ + handleAttached: function () { + app.on('select:feed-list', function (data) { + this.el.scrollTop = 0; + this.unreadOnly = bg.getBoolean('defaultToUnreadOnly'); + + if (data.action === 'new-select') { + this.handleNewSelected(data); + } + }, this); + + app.on('give-me-next', function () { + if (this.selectedItems[0] && this.selectedItems[0].model.get('unread') === true) { + this.selectedItems[0].model.save({unread: false}); + } + this.selectNextSelectable({selectUnread: true}); + app.actions.execute('content:focus'); + }, this); + + if (bg.sourceToFocus) { + setTimeout(function () { + app.trigger('focus-feed', bg.sourceToFocus); + bg.sourceToFocus = null; + }, 0); + return; + } + if (bg.getBoolean('selectAllFeeds') && bg.getBoolean('showAllFeeds')) { + this.loadAllFeeds(); + } + }, + + /** + * Loads all untrashed feeds + * @method loadAllFeeds + * @chainable + */ + loadAllFeeds: function () { + setTimeout(() => { + const unread = bg.items.where({trashed: false, unread: true}); + + if (unread.length) { + this.addItems(unread); + } else { + this.addItems(bg.items.where({trashed: false})); + } + const event = new MouseEvent('mousedown', { + view: window, + bubbles: true, + cancelable: true + }); + + const cb = document.querySelector('.special'); + cb.dispatchEvent(event); + + + }, 0); + + return this; + }, + + /** + * Renders unrendered articles in view by calling handleScroll + * @method handleSearch + * @triggered when new items arr added or when source is destroyed + */ + handleSearch: function () { + if (document.querySelector('input[type="search"]').value.trim() !== '') { + app.actions.execute('articles:search'); + } + }, + + + /** + * Unbinds all listeners to bg process + * @method handleClearEvents + * @triggered when tab is closed/refreshed + * @param id {Number} id of the closed tab + */ + handleClearEvents: function (id) { + if (window === null || id === tabID) { + bg.items.off('reset', this.addItems, this); + bg.items.off('add', this.addItem, this); + bg.items.off('sort', this.handleSort, this); + bg.items.off('search', this.handleSearch, this); + + bg.sources.off('destroy', this.handleSourcesDestroy, this); + bg.sources.off('clear-events', this.handleClearEvents, this); + } + }, + + + /** + * Clears searchbox and sorts the list + * @method handleSort + * @triggered when sort setting is changed + */ + handleSort: function () { + document.querySelector('input[type="search"]').value = ''; + this.handleNewSelected(this.currentData); + }, + + /** + * Selects new item when the last selected is deleted + * @method selectAfterDelete + * @param view {views/ItemView} + */ + selectAfterDelete: function (view) { + const children = Array.from(this.el.children); + const length = children.length; + if (children[length - 1].view === view) { + this.selectPrev({currentIsRemoved: true}); + } else { + this.selectNextSelectable({currentIsRemoved: true}); + } + }, + + /** + * Tests whether newly fetched item should be added to current list. + * (If the item's feed is selected) + * @method inCurrentData + * @return Boolean + * @param item {Item} bg.Item + */ + inCurrentData: function (item) { + const feeds = this.currentData.feeds; + if (!feeds.length) { + if (!this.currentData.filter) { + return true; + } else if (item.query(this.currentData.filter)) { + return true; + } + } else if (feeds.indexOf(item.get('sourceID')) >= 0) { + return true; + } + + return false; + }, + + /** + * Adds new article item to the list + * @method addItem + * @param item {Item} bg.Item + */ + addItem: function (item) { + //Don't add newly fetched items to middle column, when they shouldn't be + if (!this.inCurrentData(item)) { + return false; + } + + let after = null; + [ + ...document + .querySelectorAll('#article-list .articles-list-item, #article-list .date-group') + ] + .some((itemEl) => { + if (bg.items.comparator(itemEl.view.model, item) === 1) { + after = itemEl; + return true; + } + }); + + const view = new ItemView({model: item}, this); + + if (!after) { + view.render(); + this.views.push(view); + this.el.insertAdjacentElement('beforeend', view.el); + if (this.selectedItems.length === 0 && bg.getBoolean('selectFirstArticle')) { + this.select(view); + } + } else { + // is this block even executed? + after.insertAdjacentElement('afterend', view.render().el); + const indexElement = after.view instanceof ItemView ? after : after.nextElementSibling; + const index = indexElement ? this.views.indexOf(indexElement.view) : -1; + this.views.splice(index, 0, view); + } + + if (!bg.getBoolean('disableDateGroups') && bg.settings.get('sortBy') === 'date') { + const group = Group.getGroup(item.get('date')); + if (!groups.findWhere({title: group.title})) { + groups.add(new Group(group), {before: view.el}); + } + } + }, + + /** + * Adds new date group to the list + * @method addGroup + * @param model {models/Group} group create by groups.create + * @param col {collections/Groups} + * @param opt {Object} options { before: insertBeforeItem } + */ + addGroup: function (model, col, opt) { + const before = opt.before; + const view = new GroupView({model: model}, groups); + before.insertAdjacentElement('beforebegin', view.render().el); + }, + + + /** + * Removes everything from lists and adds new collection of articles + * @method setItemHeight + * @param items {Backbone.Collection} bg.Items + * @param multiple + */ + addItems: function (items, multiple = false) { + groups.reset(); + /** + * Select removal + */ + this.selectedItems = []; + while (this.el.firstChild) { + this.el.removeChild(this.el.firstChild); + } + + this.selectPivot = null; + + const length = items.length; + if (length === 0) { + return; + } + const that = this; + + const renderBlock = (renderId, startingPoint = 0) => { + if (that.currentRenderId !== renderId) { + return; + } + + let internalCounter = 0; + while (internalCounter !== 100 && startingPoint + internalCounter !== length) { + const item = items[startingPoint + internalCounter]; + if (!item) { + break; + } + item.multiple = multiple; + that.addItem(item, true); + internalCounter++; + } + if (startingPoint + internalCounter === length) { + if (document.querySelector('input[type="search"]').value !== '') { + app.actions.execute('articles:search'); + } + return; + } + window.requestIdleCallback(renderBlock.bind(this, renderId, startingPoint + internalCounter)); + }; + this.currentRenderId = Date.now(); + window.requestIdleCallback(renderBlock.bind(this, this.currentRenderId, 0)); + }, + + /** + * Called every time when new feed is selected and before it is rendered + * @method clearOnSelect + */ + clearOnSelect: function () { + // if prev selected was trash, hide undelete buttons + if (this.currentData.name === 'trash') { + app.articles.toolbar.showItems('articles:update'); + app.articles.toolbar.hideItems('articles:undelete'); + document.querySelector('#context-undelete').hidden = true; + // document.querySelector('#context-undelete').classList.add('hidden'); + } + + this.currentData = { + feeds: [], + name: 'all-feeds', + filter: {trashed: false}, + unreadOnly: false + }; + + }, + + /** + * Called every time when new feed is selected. Gets the right data from store. + * @method handleNewSelected + * @param data {Object} data object received from feed list + */ + handleNewSelected: function (data) { + this.clearOnSelect(); + this.currentData = data; + + const searchIn = data.filter ? bg.items.where(data.filter) : bg.items.where({trashed: false}); + + // if newly selected is trash + if (this.currentData.name === 'trash') { + app.articles.toolbar.hideItems('articles:update').showItems('articles:undelete'); + document.querySelector('#context-undelete').hidden = false; + } + const items = searchIn.filter((item) => { + if (!item.get('unread') && this.unreadOnly) { + return false; + } + return data.name || data.feeds.includes(item.get('sourceID')); + }, this); + this.addItems(items, data.multiple); + }, + + + /** + * If current feed is removed, select all feeds + * @triggered when any source is destroyed + * @method handleSourcesDestroy + * @param source {Source} Destroyed source + */ + handleSourcesDestroy: function (source) { + const data = this.currentData; + const index = data.feeds.indexOf(source.id); + + if (index >= 0) { + data.feeds.splice(index, 1); + } + + if (!data.feeds.length && !data.filter) { + this.clearOnSelect(); + + if (document.querySelector('.articles-list-item')) { + this.once('items-destroyed', () => { + this.loadAllFeeds(); + }, this); + } else { + this.loadAllFeeds(); + } + } + + }, + + /** + * Moves item from trash back to its original source + * @method undeleteItem + * @param view {views/ItemView} Undeleted article view + */ + undeleteItem: function (view) { + view.model.save({ + 'trashed': false + }); + this.destroyItem(view); + }, + + /** + * Moves item to trash + * @method removeItem + * @param view {views/ItemView} Removed article view + */ + removeItem: function (view) { + const askRmPinned = bg.settings.get('askRmPinned'); + if (view.model.get('pinned') && askRmPinned === 'all') { + const confirmation = confirm(Locale.PIN_QUESTION_A + view.model.escape('title') + Locale.PIN_QUESTION_B); + if (!confirmation) { + return; + } + } + view.model.trash(); + this.destroyItem(view); + this.trigger('items-destroyed'); + }, + + /** + * Removes item from both source and trash leaving only info it has been already fetched and deleted + * @method removeItemCompletely + * @param view {views/ItemView} Removed article view + */ + removeItemCompletely: function (view) { + const askRmPinned = bg.settings.get('askRmPinned'); + if (view.model.get('pinned') && askRmPinned && askRmPinned !== 'none') { + const confirmation = confirm(Locale.PIN_QUESTION_A + view.model.escape('title') + Locale.PIN_QUESTION_B); + if (!confirmation) { + return; + } + } + view.model.markAsDeleted(); + }, + + /** + * Calls undeleteItem/removeItem/removeItemCompletely in a batch for several items + * @method destroyBatch + * @param arr {Array} List of views + * @param fn {Function} Function to be called on each view + */ + destroyBatch: function (arr, fn) { + for (let i = 0, j = arr.length; i < j; i++) { + fn.call(this, arr[i]); + } + }, + + /** + * List of views to be closed when nextFrame animation frame is called + * @property nextFrame + * @default null + * @type Object + */ + nextFrameStore: [], + + /** + * RequestAnimationFrame return value for next destroy item call. + * @property nextFrame + * @default null + * @type Object + */ + nextFrame: null, + + /** + * Removes article view (clearing events and all) + * @method destroyItem + * @param view {views/ItemView} Destroyed article view + */ + destroyItem: function (view) { + this.nextFrameStore.push(view); + if (!this.nextFrame) { + this.nextFrame = requestAnimationFrame(() => { + const lastView = this.nextFrameStore[this.nextFrameStore.length - 1]; + this.selectAfterDelete(lastView); + for (let i = 0, j = this.nextFrameStore.length - 1; i < j; i++) { + this.destroyItemFrame(this.nextFrameStore[i]); + } + + this.destroyItemFrame(lastView); + + this.nextFrame = null; + this.nextFrameStore = []; + + + }); + } + }, + + /** + * Called asynchronously from destroyItem. It does the real removing job. + * @method destroyItemFrame + * @param view {views/ItemView} Destroyed article view + */ + destroyItemFrame: function (view) { + // START: REMOVE DATE GROUP + const prev = view.el.previousElementSibling; + const next = view.el.nextElementSibling; + if (prev && prev.classList.contains('date-group')) { + if (!next || next.classList.contains('date-group')) { + groups.remove(prev.view.model); + } + } + // END: REMOVE DATE GROUP + + view.clearEvents(); + view.remove(); + + const selectedItemIndex = this.selectedItems.indexOf(view); + if (selectedItemIndex >= 0) { + this.selectedItems.splice(selectedItemIndex, 1); + } + const viewIndex = this.views.indexOf(view); + if (viewIndex >= 0) { + this.views.splice(viewIndex, 1); + } + }, + + /** + * Toggles unread state of selected items (with onlyToRead option) + * @method changeUnreadState + * @param options {Object} Options { onlyToRead: bool } + */ + changeUnreadState: function (options) { + options = options || {}; + const unread = this.selectedItems.length && !options.onlyToRead ? !this.selectedItems[0].model.get('unread') : false; + this.selectedItems.forEach(function (item) { + if (!options.onlyToRead || item.model.get('unread') === true) { + item.model.save({unread: unread, visited: true}); + } + }, this); + } + }); + + ArticleListView = ArticleListView.extend(selectable); + + return new ArticleListView(); + }); diff --git a/src/scripts/app/views/contentView.js b/src/scripts/app/views/contentView.js new file mode 100644 index 00000000..e6fea8d1 --- /dev/null +++ b/src/scripts/app/views/contentView.js @@ -0,0 +1,599 @@ +/** + * @module App + * @submodule views/contentView + */ +define(function (require) { + const BB = require("backbone"); + const dateUtils = require("helpers/dateUtils"); + + /** + * Full view of one article (right column) + * @class ContentView + * @constructor + * @extends Backbone.View + */ + let ContentView = BB.View.extend({ + /** + * Tag name of content view element + * @property tagName + * @default 'header' + * @type String + */ + tagName: "header", + events: { + mousedown: "handleMouseDown", + "click .pin-button": "handlePinClick", + keydown: "handleKeyDown", + }, + + view: "", + + /** + * Changes pin state + * @method handlePinClick + * @triggered on click on pin button + * @param event {MouseEvent} + */ + handlePinClick: function (event) { + const target = event.target; + if (target.classList.contains("pinned")) { + target.classList.remove("pinned"); + } else { + target.classList.add("pinned"); + } + this.model.save({ + pinned: target.classList.contains("pinned"), + }); + }, + + /** + * Called when new instance is created + * @method initialize + */ + initialize: function () { + this.on("attach", this.handleAttached); + + bg.items.on("change:pinned", this.handleItemsPin, this); + bg.sources.on("clear-events", this.handleClearEvents, this); + }, + + /** + * Sets comm event listeners + * @method handleAttached + * @triggered when content view is attached to DOM + */ + handleAttached: function () { + app.on( + "select:article-list", + function (data) { + this.handleNewSelected( + bg.items.findWhere({ id: data.value }) + ); + }, + this + ); + + app.on( + "space-pressed", + function () { + this.handleSpace(); + }, + this + ); + + app.on( + "no-items:article-list", + function () { + if (this.renderTimeout) { + clearTimeout(this.renderTimeout); + } + this.model = null; + this.hide(); + }, + this + ); + }, + + /** + * Next page in article or next unread article + * @method handleSpace + * @triggered when space is pressed in middle column + */ + handleSpace: function () { + const cw = document.querySelector("#content"); + if (cw.offsetHeight + cw.scrollTop >= cw.scrollHeight) { + app.trigger("give-me-next"); + } else { + cw.scrollBy(0, cw.offsetHeight * 0.85); + } + }, + + /** + * Unbinds all listeners to bg process + * @method handleClearEvents + * @triggered when tab is closed/refreshed + * @param id {Number} id of the closed tab + */ + handleClearEvents: function (id) { + if (!window || id === tabID) { + bg.items.off("change:pinned", this.handleItemsPin, this); + bg.sources.off("clear-events", this.handleClearEvents, this); + } + }, + + /** + * Sets the pin button state + * @method handleItemsPin + * @triggered when the pin state of the article is changed + * @param model {Item} article that had its pin state changed + */ + handleItemsPin: function (model) { + if (model === this.model) { + const pinButton = this.el.querySelector(".pin-button"); + if (this.model.get("pinned")) { + pinButton.classList.add("pinned"); + } else { + pinButton.classList.remove("pinned"); + } + } + }, + + /** + * Gets formatted date (according to settings) from given unix time + * @method getFormattedDate + * @param unixtime {Number} + */ + getFormattedDate: function (unixtime) { + const dateFormats = { + normal: "DD.MM.YYYY", + iso: "YYYY-MM-DD", + us: "MM/DD/YYYY", + }; + const pickedFormat = + dateFormats[bg.settings.get("dateType") || "normal"] || + dateFormats["normal"]; + + const timeFormat = + bg.settings.get("hoursFormat") === "12h" + ? "H:mm:ss a" + : "hh:mm:ss"; + + return dateUtils.formatDate( + unixtime, + pickedFormat + " " + timeFormat + ); + }, + + /** + * Rendering of article is delayed with timeout for 50ms to speed up quick select changes in article list. + * This property contains descriptor for that timeout. + * @property renderTimeout + * @default null + * @type Number + */ + renderTimeout: null, + + /** + * Renders articles content asynchronously + * @method render + * @chainable + */ + render: function (overrideView = "") { + clearTimeout(this.renderTimeout); + + this.renderTimeout = setTimeout(async () => { + if (!this.model) { + return; + } + const modelUrl = this.model.get("url"); + + this.show(); + const source = this.model.getSource(); + const openEnclosure = bg.getElementBoolean( + source, + "openEnclosure" + ); + const defaultView = bg.getElementSetting(source, "defaultView"); + + const data = Object.create(this.model.attributes); + data.date = this.getFormattedDate(this.model.get("date")); + data.titleIsLink = bg.getBoolean("titleIsLink"); + data.open = openEnclosure; + + let content = ""; + + if (overrideView !== "") { + this.view = overrideView; + } else { + this.view = defaultView; + } + + if (this.view === "feed") { + content = this.model.get("content"); + } else { + // const parsedContent = this.model.get('parsedContent'); + // if (this.view in parsedContent) { + // content = parsedContent[this.view]; + // } else { + if (this.view === "mozilla") { + const response = await fetch(this.model.get("url"), { + method: "GET", + redirect: "follow", // manual, *follow, error + referrerPolicy: "no-referrer", + }); + const websiteContent = await response.text(); + + const parser = new DOMParser(); + const websiteDocument = parser.parseFromString( + websiteContent, + "text/html" + ); + const Readability = require("../../libs/readability"); + if (this.model.get("url") !== modelUrl) { + return; + } + content = new Readability(websiteDocument).parse() + .content; + } + // } + // if (bg.settings.get('cacheParsedArticles') === 'true' && !(this.view in parsedContent)) { + // parsedContent[this.view] = content; + // this.model.set('parsedContent', parsedContent); + // } + } + const toRemove = browser.runtime.getURL(""); + const re = new RegExp(toRemove, "g"); + content = content.replace(re, "/"); + + while (this.el.firstChild) { + this.el.removeChild(this.el.firstChild); + } + + const fragment = document + .createRange() + .createContextualFragment( + require("text!templates/contentView.html") + ); + fragment.querySelector(".author").textContent = data.author; + fragment.querySelector(".date").textContent = data.date; + if (data.pinned) { + fragment + .querySelector(".pin-button") + .classList.add("pinned"); + } + + function createEnclosure(enclosureData) { + let newEnclosure; + + switch (enclosureData.medium) { + case "image": { + newEnclosure = document + .createRange() + .createContextualFragment( + require("text!templates/enclosureImage.html") + ); + const img = newEnclosure.querySelector("img"); + img.src = enclosureData.url; + img.alt = enclosureData.name; + break; + } + case "video": { + newEnclosure = document + .createRange() + .createContextualFragment( + require("text!templates/enclosureVideo.html") + ); + const video = newEnclosure.querySelector("video"); + video.querySelector("source").src = + enclosureData.url; + video.querySelector("source").type = + enclosureData.type; + break; + } + case "audio": { + newEnclosure = document + .createRange() + .createContextualFragment( + require("text!templates/enclosureAudio.html") + ); + const audio = newEnclosure.querySelector("audio"); + audio.querySelector("source").src = + enclosureData.url; + break; + } + case "youtube": { + newEnclosure = document + .createRange() + .createContextualFragment( + require("text!templates/enclosureYoutubeCover.html") + ); + const videoId = /^.*\/(.*)\?(.*)$/.exec( + enclosureData.url + )[1]; + + const posterUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; + const videoUrl = `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1`; + const cover = + newEnclosure.querySelector(".youtube-cover"); + cover.style.backgroundImage = `url("${posterUrl}")`; + + cover.addEventListener("click", () => { + const iframeEnclosure = document + .createRange() + .createContextualFragment( + require("text!templates/enclosureYoutube.html") + ); + const iframe = + iframeEnclosure.querySelector("iframe"); + iframe.src = videoUrl; + cover.replaceWith(iframeEnclosure); + iframeEnclosure.focus(); + }); + + break; + } + default: { + newEnclosure = document + .createRange() + .createContextualFragment( + require("text!templates/enclosureGeneral.html") + ); + } + } + + newEnclosure.querySelector("a").href = enclosureData.url; + newEnclosure.querySelector("a").textContent = + enclosureData.name; + + return newEnclosure; + } + + if (data.enclosure) { + const enclosures = Array.isArray(data.enclosure) + ? data.enclosure + : [data.enclosure]; + enclosures.forEach((enclosureData) => { + const enclosure = createEnclosure(enclosureData); + fragment + .querySelector("#below-h1") + .appendChild(enclosure); + }); + if (data.open && enclosures.length === 1) { + fragment + .querySelector(".enclosure") + .setAttribute("open", "open"); + } + } + this.el.appendChild(fragment); + const h1 = this.el.querySelector("h1"); + if (data.titleIsLink) { + const link = document.createElement("a"); + link.target = "_blank"; + link.tabindex = "-1"; + link.href = data.url ? data.url : "#"; + link.textContent = data.title; + h1.appendChild(link); + } else { + h1.textContent = data.title; + } + + // first load might be too soon + const sandbox = app.content.sandbox; + const frame = sandbox.el; + + frame.setAttribute("scrolling", "no"); + + const resizeFrame = () => { + const scrollHeight = + frame.contentDocument.body.scrollHeight; + frame.style.minHeight = "10px"; + frame.style.minHeight = "70%"; + frame.style.minHeight = `${scrollHeight}px`; + + frame.style.height = "10px"; + frame.style.height = "70%"; + frame.style.height = `${scrollHeight}px`; + }; + + const loadContent = () => { + const body = frame.contentDocument.querySelector("body"); + const articleUrl = this.model.get("url"); + const articleDomain = new URL(articleUrl).origin; + + let base = frame.contentDocument.querySelector("base"); + base.href = articleDomain; + const shouldInvertColors = bg.getBoolean("invertColors"); + if (shouldInvertColors) { + body.classList.add("dark-theme"); + } else { + body.classList.remove("dark-theme"); + } + + frame.contentWindow.scrollTo(0, 0); + document.querySelector("#content").scrollTo(0, 0); + frame.contentDocument.documentElement.style.fontSize = + bg.settings.get("articleFontSize") + "%"; + + const contentElement = + frame.contentDocument.querySelector( + "#smart-rss-content" + ); + + while (contentElement.firstChild) { + contentElement.removeChild(contentElement.firstChild); + } + + let fragment; + switch (data.enclosure.medium) { + case "youtube": + fragment = document + .createRange() + .createContextualFragment( + content.replace(/\r/g, "
") + ); + break; + default: + fragment = document + .createRange() + .createContextualFragment(content); + } + contentElement.appendChild(fragment); + + frame.contentDocument.querySelector("#smart-rss-url").href = + articleUrl; + frame.contentDocument.querySelector( + "#full-article-url" + ).textContent = articleUrl; + + const clickHandler = (event) => { + if (event.target.matches("a")) { + event.stopPropagation(); + const href = event.target.getAttribute("href"); + if (!href || href[0] !== "#") { + return true; + } + event.preventDefault(); + const name = href.substring(1); + const nameElement = + frame.contentDocument.querySelector( + '[name="' + name + ']"' + ); + const idElement = + frame.contentDocument.getElementById(name); + let element = null; + if (nameElement) { + element = nameElement; + } else if (idElement) { + element = idElement; + } + if (element) { + const getOffset = function (el) { + const box = el.getBoundingClientRect(); + + return { + top: + box.top + + frame.contentWindow.pageYOffset - + frame.contentDocument + .documentElement.clientTop, + left: + box.left + + frame.contentWindow.pageXOffset - + frame.contentDocument + .documentElement.clientLeft, + }; + }; + + const offset = getOffset(element); + frame.contentWindow.scrollTo( + offset.left, + offset.top + ); + } + + return false; + } + }; + + frame.contentDocument.removeEventListener( + "click", + clickHandler + ); + frame.contentDocument.addEventListener( + "click", + clickHandler + ); + + frame.contentDocument.removeEventListener( + "load", + resizeFrame + ); + frame.contentDocument.addEventListener("load", resizeFrame); + + if (typeof ResizeObserver !== "undefined") { + const resizeObserver = new ResizeObserver(resizeFrame); + resizeObserver.observe(frame.contentDocument.body); + } + + [ + ...frame.contentDocument.querySelectorAll( + "img, picture, iframe, video, audio" + ), + ].forEach((element) => { + if ( + element.src.startsWith( + "https://www.youtube.com/watch?" + ) + ) { + element.src = element.src.replace( + "https://www.youtube.com/watch?v=", + "https://www.youtube-nocookie.com/embed/" + ); + element.removeAttribute("allowfullscreen"); + element.removeAttribute("height"); + element.removeAttribute("width"); + element.setAttribute( + "allowfullscreen", + "allowfullscreen" + ); + } + element.onload = resizeFrame; + }); + resizeFrame(); + }; + + if (sandbox.loaded) { + loadContent(); + } else { + sandbox.on("load", loadContent); + } + }, 50); + + return this; + }, + + /** + * Replaces old article model with newly selected one + * @method handleNewSelected + * @param model {Item} The new article model + */ + handleNewSelected: function (model) { + if (model === this.model) { + return; + } + this.model = model; + if (!this.model) { + // should not happen but happens + this.hide(); + } else { + this.render(); + } + }, + + /** + * Hides contents (header, iframe) + * @method hide + */ + hide: function () { + [...document.querySelectorAll("header,iframe")].forEach( + (element) => { + element.hidden = true; + } + ); + }, + + /** + * Show contents (header, iframe) + * @method hide + */ + show: function () { + [...document.querySelectorAll("header,iframe")].forEach( + (element) => { + element.hidden = false; + } + ); + }, + }); + + return new ContentView(); +}); diff --git a/src/scripts/app/views/feedList.js b/src/scripts/app/views/feedList.js new file mode 100644 index 00000000..f364e0c0 --- /dev/null +++ b/src/scripts/app/views/feedList.js @@ -0,0 +1,447 @@ +/** + * @module App + * @submodule views/feedList + */ +define([ + 'backbone', 'views/SourceView', 'views/FolderView', 'views/SpecialView', 'models/Special', + 'instances/contextMenus', 'mixins/selectable', 'instances/specials' + ], + function (BB, SourceView, FolderView, SpecialView, Special, contextMenus, selectable, specials) { + + /** + * List of feeds (in left column) + * @class FeedListView + * @constructor + * @extends Backbone.View + */ + let FeedListView = BB.View.extend({ + + selectedItems: [], + /** + * Tag name of the list + * @property tagName + * @default 'div' + * @type String + */ + tagName: 'div', + + /** + * Class of feed list views + * @property itemClass + * @default 'list-item' + * @type String + */ + itemClass: 'sources-list-item', + + /** + * ID of feed list + * @property id + * @default 'feed-list' + * @type String + */ + id: 'feed-list', + + events: { + 'click .sources-list-item': 'handleMouseDown', + 'mousedown .sources-list-item': 'handleMouseDown', + 'mouseup .sources-list-item': 'handleMouseUp' + }, + + /** + * Called when new instance is created + * @method initialize + */ + initialize: function () { + + this.el.view = this; + + this.on('attach', this.handleAttach); + + bg.sources.on('reset', this.addSources, this); + bg.sources.on('add', this.addSource, this); + bg.sources.on('change:folderID', this.handleChangeFolder, this); + bg.folders.on('add', this.addFolder, this); + bg.sources.on('clear-events', this.handleClearEvents, this); + bg.settings.on('change:showOnlyUnreadSources', this.insertFeeds, this); + + this.on('pick', this.handlePick); + + }, + + /** + * Sets comm event listeners and inserts feeds + * @method handleAttached + * @triggered when feed list is attached to DOM + */ + handleAttach: function () { + app.on('select-all-feeds', () => { + const allFeeds = document.querySelector('.special.all-feeds'); + if (!allFeeds) { + return; + } + this.select(allFeeds.view); + }); + + app.on('select-folder', (id) => { + const folder = document.querySelector('.folder[data-id="' + id + '"]'); + if (!folder) { + return; + } + this.select(folder.view); + }); + + app.on('focus-feed', (id) => { + const feed = document.querySelector('.sources-list-item[data-id="' + id + '"]'); + if (!feed) { + return; + } + this.select(feed.view); + feed.view.el.focus(); + app.actions.execute('feeds:showAndFocusArticles'); + }); + + this.insertFeeds(); + }, + + /** + * Adds folders specials and sources + * @method insertFeeds + * @@chainable + */ + insertFeeds: function () { + while (this.el.firstChild) { + this.el.removeChild(this.el.lastChild); + } + this.addFolders(bg.folders); + if (bg.getBoolean('showPinned')) { + this.addSpecial(specials.pinned); + } + if (bg.getBoolean('showAllFeeds')) { + this.addSpecial(specials.allFeeds); + } + + this.addSources(bg.sources); + + + this.addSpecial(specials.trash); + + + return this; + }, + + /** + * If one list-item was selected by left mouse button, show its articles. + * @triggered by selectable mixin. + * @method handlePick + * @param view {TopView} Picked source, folder or special + * @param event {Event} Mouse or key event + */ + handlePick: function (view, event) { + if (event.type && event.type === 'mousedown' && event.which === 1) { + app.actions.execute('feeds:showAndFocusArticles', event); + } + }, + + /** + * Selectable mixin bindings. The selectable mixing will trigger "pick" event when items are selected. + * @method handleClick + * @triggered on mouse down + * @param event {Event} Mouse event + */ + handleMouseDown: function (event) { + this.handleSelectableMouseDown(event); + }, + + /** + * Selectable mixin bindings, item bindings + * @method handleMouseUp + * @triggered on mouse up + * @param event {Event} Mouse event + */ + handleMouseUp: function (event) { + event.currentTarget.view.handleMouseUp(event); + this.handleSelectableMouseUp(event); + }, + /** + * Place feed to the right place + * @method handleDragStart + * @triggered when folderID of feed is changed + * @param source {Source} Source tha has its folderID changed + */ + handleChangeFolder: function (source) { + source = document.querySelector('.source[data-id="' + source.get('id') + '"]'); + if (!source) { + return; + } + + this.placeSource(source.view); + }, + + /** + * Unbinds all listeners to bg process + * @method handleClearEvents + * @triggered when tab is closed/refershed + * @param id {Number} id of the closed tab + */ + handleClearEvents: function (id) { + if (!window || id === tabID) { + bg.sources.off('reset', this.addSources, this); + bg.sources.off('add', this.addSource, this); + bg.sources.off('change:folderID', this.handleChangeFolder, this); + bg.folders.off('add', this.addFolder, this); + bg.sources.off('clear-events', this.handleClearEvents, this); + } + }, + + /** + * Adds one special (all feeds, pinned, trash) + * @method addSpecial + * @param special {models/Special} Special model to add + */ + addSpecial: function (special) { + const view = new SpecialView({model: special}); + if (view.model.get('position') === 'top') { + const element = view.render().el; + element.classList.add('topSpecial'); + element.classList.add(view.model.get('name')); + this.el.insertAdjacentElement('afterbegin', element); + } else { + this.el.insertAdjacentElement('beforeend', view.render().el); + } + + }, + + /** + * Adds one folder + * @method addFolder + * @param folder {models/Folder} Folder model to add + */ + addFolder: function (folder) { + if (folder.get('count') === 0 && bg.getBoolean('showOnlyUnreadSources')) { + return; + } + const view = new FolderView({model: folder}, this); + const folderViews = [...document.querySelectorAll('.folder')]; + if (folderViews.length) { + this.insertBefore(view, folderViews); + } else { + const special = document.querySelector('.topSpecial:last-of-type'); + if (special) { + special.insertAdjacentElement('afterend', view.render().el); + } else { + this.el.insertAdjacentElement('beforeend', view.render().el); + } + } + }, + + /** + * Adds more folders ta once + * @method addFolders + * @param folders {Array} Array of folder models to add + */ + addFolders: function (folders) { + const existingFolders = [...document.querySelectorAll('.folder')]; + if (existingFolders.length > 0) { + existingFolders.forEach((folder) => { + if (!folder.view || !(folder instanceof FolderView)) { + return; + } + this.destroySource(folder.view); + }); + } + + folders.forEach((folder) => { + this.addFolder(folder); + }); + }, + + /** + * Adds one source + * @method addSource + * @param source {models/Source} Source model to add + * @param noManualSort {Boolean} When false, the rigt place is computed + */ + addSource: function (source, noManualSort) { + this.placeSource(new SourceView({model: source}, this), noManualSort === true); + }, + + /** + * Places source to its right place + * @method placeSource + * @param view {views/TopView} Feed/Folder/Special to add + * @param noManualSort {Boolean} When false, the right place is computed + */ + placeSource: function (view, noManualSort) { + let sourceViews; + const source = view.model; + + if (source.get('count') === 0 && bg.getBoolean('showOnlyUnreadSources')) { + return; + } + + if (source.get('folderID')) { + const folder = document.querySelector('.folder[data-id="' + source.get('folderID') + '"]'); + if (folder) { + sourceViews = [...document.querySelectorAll('.source[data-in-folder="' + source.get('folderID') + '"]')]; + if (sourceViews.length && noManualSort) { + sourceViews[sourceViews.length - 1].insertAdjacentElement('afterend', view.render().el); + } else if (sourceViews.length) { + this.insertBefore(view, sourceViews); + } else { + folder.insertAdjacentElement('afterend', view.render().el); + } + + if (!folder.view.model.get('opened')) { + view.el.hidden = true; + } + + return; + } + } + + sourceViews = [...document.querySelectorAll('.source:not([data-in-folder])')]; + + if (sourceViews.length && noManualSort) { + sourceViews[sourceViews.length - 1].insertAdjacentElement('afterend', view.render().el); + return; + + } + if (sourceViews.length) { + this.insertBefore(view, sourceViews); + return; + } + const fls = [...document.querySelectorAll('[data-in-folder],.folder')]; + if (fls.length) { + fls[fls.length - 1].insertAdjacentElement('afterend', view.render().el); + return; + } + const first = document.querySelector('.topSpecial:last-of-type'); + if (first) { + // .special-first = all feeds, with more "top" specials this will have to be changed + first.insertAdjacentElement('afterend', view.render().el); + return; + } + this.el.insertAdjacentElement('beforeend', view.render().el); + + }, + + /** + * Insert element after another element + * @method insertBefore + * @param what {HTMLElement} Element to add + * @param where {Array} Element to add after + */ + insertBefore: function (what, where) { + let before = null; + where.some(function (el) { + if (el.view.model !== what.model && bg.sources.comparator(el.view.model, what.model) === 1) { + return before = el; + } + }); + if (before) { + before.insertAdjacentElement('beforebegin', what.render().el); + return; + } + if (what instanceof FolderView) { + const folderSources = [...document.querySelectorAll('[data-in-folder="' + where[where.length - 1].view.model.get('id') + '"]')]; + if (folderSources.length) { + where[where.length - 1] = folderSources[folderSources.length - 1]; + } + } + where[where.length - 1].insertAdjacentElement('afterend', what.render().el); + + }, + + /** + * Add more sources at once + * @method addSources + * @param sources {Array} Array of source models to add + */ + addSources: function (sources) { + [...document.querySelectorAll('.source')].forEach((source) => { + if (!source.view || !(source instanceof SourceView)) { + return; + } + this.destroySource(source.view); + }); + sources.forEach((source) => { + this.addSource(source, true); + }); + }, + + /** + * Destroy feed + * @method removeSource + * @param view {views/SourceView} View containing the model to be destroyed + */ + removeSource: function (view) { + view.model.destroy(); + }, + + + /** + * Closes item view + * @method destroySource + * @param view {views/TopView} View to be closed + */ + destroySource: function (view) { + view.clearEvents(); + view.undelegateEvents(); + view.off(); + view.remove(); + const indexOf = this.selectedItems.indexOf(view); + if (indexOf >= 0) { + this.selectedItems.splice(indexOf, 1); + } + }, + + /** + * Get array of selected feeds (including feeds in selected folders) + * @method getSelectedFeeds + * @param arr {Array} List of selected items + */ + getSelectedFeeds: function (arr = []) { + const selectedItems = arr.length > 0 ? arr : this.selectedItems.map((item) => { + return item.model; + }); + const selectedFeeds = []; + selectedItems.forEach((item) => { + if (item instanceof bg.Source) { + selectedFeeds.push(item); + return; + } + if (item instanceof bg.Folder) { + const folderFeeds = bg.sources.toArray().filter((source) => { + return source.get('folderID') === item.id; + }); + if (folderFeeds.length > 0) { + selectedFeeds.push(...this.getSelectedFeeds(folderFeeds)); + } + } + }); + return selectedFeeds; + }, + + /** + * Get array of selected folders + * @method getSelectedFolders + * @param selectedItems {Array} List of selected items + */ + getSelectedFolders: function (selectedItems) { + const currentlySelectedItems = selectedItems || this.selectedItems.map((item) => { + return item.model; + }); + const selectedFolders = []; + currentlySelectedItems.forEach((folder) => { + if (folder instanceof bg.Folder) { + selectedFolders.push(folder); + } + }); + return selectedFolders; + } + }); + + FeedListView = FeedListView.extend(selectable); + + return new FeedListView(); + }); diff --git a/src/scripts/appEntrypoint.js b/src/scripts/appEntrypoint.js new file mode 100644 index 00000000..83e58998 --- /dev/null +++ b/src/scripts/appEntrypoint.js @@ -0,0 +1,36 @@ +require.config({ + baseUrl: "scripts/app", + waitSeconds: 0, + + paths: { + jquery: "../libs/jquery.min", + underscore: "../libs/underscore.min", + backbone: "../libs/backbone.min", + text: "../libs/require.text", + }, + + shim: { + jquery: { + exports: "$", + }, + backbone: { + deps: ["underscore"], + exports: "Backbone", + }, + underscore: { + exports: "_", + }, + }, +}); + +browser.runtime.getBackgroundPage(function (bg) { + /** + * Setup work, that has to be done before any dependencies get executed + */ + window.bg = bg; + bg.appStarted.then(() => { + requirejs(["app"], function (app) { + app.start(); + }); + }); +}); diff --git a/src/scripts/bgprocess.js b/src/scripts/bgprocess.js new file mode 100644 index 00000000..65a72820 --- /dev/null +++ b/src/scripts/bgprocess.js @@ -0,0 +1,35 @@ +require.config({ + + baseUrl: 'scripts/bgprocess', + waitSeconds: 0, + + paths: { + jquery: '../libs/jquery.min', + underscore: '../libs/underscore.min', + backbone: '../libs/backbone.min', + backboneDB: '../libs/backbone.indexDB', + he: '../libs/he', + favicon: '../libs/favicon' + }, + + shim: { + jquery: { + exports: '$' + }, + backbone: { + deps: ['underscore'], + exports: 'Backbone' + }, + backboneDB: { + deps: ['backbone'] + }, + underscore: { + exports: '_' + } + } +}); + +requirejs(['bg'], function () { + // bg started +}); + diff --git a/src/scripts/bgprocess/bg.js b/src/scripts/bgprocess/bg.js new file mode 100644 index 00000000..5ebcab43 --- /dev/null +++ b/src/scripts/bgprocess/bg.js @@ -0,0 +1,434 @@ +/** + * @module BgProcess + */ + +define(function (require) { + const Animation = require("modules/Animation"); + const Settings = require("models/Settings"); + const Info = require("models/Info"); + const Source = require("models/Source"); + const Sources = require("collections/Sources"); + const Item = require("models/Item"); + const Items = require("collections/Items"); + const Folders = require("collections/Folders"); + const Loader = require("models/Loader"); + const Folder = require("models/Folder"); + const Toolbars = require("collections/Toolbars"); + + /** + * Messages + */ + function addSource(address) { + address = address.replace(/^feed:/i, "https:"); + + const duplicate = sources.findWhere({ url: address }); + + if (duplicate) { + duplicate.trigger("change"); + openRSS(false, duplicate.get("id")); + return; + } + const source = sources.create( + { + title: address, + url: address, + }, + { wait: true } + ); + openRSS(false, source.get("id")); + } + + function createLinksMenu() { + if (!getBoolean("displaySubscribeToLink")) { + return; + } + browser.contextMenus.create({ + title: "Subscribe to this feed", + contexts: ["link"], + checked: false, + onclick: (info) => { + addSource(info.linkUrl); + }, + }); + } + + function onMessage(message) { + if (!message.hasOwnProperty("action")) { + return; + } + + if (message.action === "load-all") { + loader.downloadAll(true); + return; + } + + if (message.action === "new-rss" && message.value) { + addSource(message.value); + return; + } + if (message.action === "list-feeds") { + browser.contextMenus.removeAll(); + createLinksMenu(); + if (!settings.get("detectFeeds")) { + return; + } + setTimeout(() => { + let feeds = message.value; + let subscribedFound = 0; + let unsubscribedFound = 0; + if (settings.get("hideSubscribedFeeds") === "hide") { + feeds = feeds.filter((feed) => { + const isFound = !sources.where({ url: feed.url }) + .length; + if (isFound) { + subscribedFound++; + } else { + unsubscribedFound++; + } + return isFound; + }); + } else { + feeds = feeds.map((feed) => { + const isFound = sources.where({ url: feed.url }).length; + if (isFound) { + subscribedFound++; + feed.title = "[*] " + feed.title; + } else { + unsubscribedFound++; + } + return feed; + }); + } + + if (feeds.length === 0) { + Animation.handleIconChange(); + return; + } + + const whenToChangeIcon = settings.get("showNewArticlesIcon"); + let shouldChangeIcon = true; + if ( + whenToChangeIcon === "not-subscribed-found" && + unsubscribedFound === 0 + ) { + shouldChangeIcon = false; + } + if ( + whenToChangeIcon === "no-subscribed-found" && + subscribedFound > 0 + ) { + shouldChangeIcon = false; + } + if (whenToChangeIcon === "never") { + shouldChangeIcon = false; + } + if (shouldChangeIcon) { + browser.browserAction.setIcon({ + path: + "/images/icon19-" + + settings.get("sourcesFoundIcon") + + ".png", + }); + } + browser.contextMenus.create( + { + id: "SmartRss", + contexts: ["browser_action"], + title: "Subscribe", + }, + function () { + feeds.forEach(function (feed) { + browser.contextMenus.create({ + id: feed.url, + title: feed.title, + contexts: ["browser_action"], + parentId: "SmartRss", + onclick: function () { + addSource(feed.url); + }, + }); + }); + } + ); + if (settings.get("badgeMode") === "sources") { + browser.browserAction.setBadgeText({ + text: feeds.length.toString(), + }); + } + }, 250); + } + if (message.action === "visibility-lost") { + Animation.handleIconChange(); + browser.contextMenus.removeAll(); + createLinksMenu(); + if (settings.get("badgeMode") === "sources") { + browser.browserAction.setBadgeText({ text: "" }); + } + } + if (message.action === "get-setting") { + return new Promise((resolve) => { + resolve(settings.get(message.key)); + }); + } + if (message.action === "save-setting") { + return new Promise((resolve) => { + settings.save(message.key, message.value); + resolve(settings.get(message.key)); + }); + } + + if (message.action === "get-settings") { + return new Promise((resolve) => { + resolve(settings.attributes); + }); + } + } + + browser.runtime.onMessage.addListener(onMessage); + + function openRSS(closeIfActive, focusSource) { + const url = browser.runtime.getURL("rss.html"); + browser.tabs.query({ url: url }, (tabs) => { + if (tabs[0]) { + if (tabs[0].active && closeIfActive) { + browser.tabs.remove(tabs[0].id); + return; + } + browser.tabs.update(tabs[0].id, { + active: true, + }); + if (focusSource) { + window.sourceToFocus = focusSource; + } + return; + } + window.sourceToFocus = focusSource; + if (settings.get("openInNewTab")) { + browser.tabs.create( + { + url: url, + }, + () => {} + ); + } else { + browser.tabs.update({ url: url }); + } + }); + } + + function openInNewTab() { + browser.tabs.create( + { + url: browser.runtime.getURL("rss.html"), + }, + () => {} + ); + } + + browser.browserAction.onClicked.addListener(function (tab, onClickData) { + if (typeof onClickData !== "undefined") { + if (onClickData.button === 1) { + openInNewTab(); + return; + } + } + openRSS(true); + }); + + window.openRSS = openRSS; + + /** + * Update animations + */ + Animation.start(); + + /** + * Items + */ + window.Source = Source; + window.Item = Item; + window.Folder = Folder; + + /** + * DB models + */ + window.settings = new Settings(); + window.info = new Info(); + window.sources = new Sources(); + window.items = new Items(); + window.folders = new Folders(); + window.loaded = false; + + /** + * This is used for when new feed is subscribed and smart rss tab is opened to focus the newly added feed + */ + window.sourceToFocus = null; + + window.toolbars = new Toolbars(); + + window.loader = new Loader(); + + window.valueToBoolean = function (value) { + if ( + value === 1 || + value === "1" || + value === "on" || + value === "yes" || + value === "true" || + value === true + ) { + return true; + } + if ( + value === 0 || + value === "0" || + value === "off" || + value === "no" || + value === "false" || + value === false + ) { + return false; + } + return value; + }; + + window.getBoolean = function (name) { + return valueToBoolean(settings.get(name)); + }; + + window.getElementBoolean = function (element, setting) { + const elementValue = element.get(setting); + if (elementValue === "global") { + return getBoolean(setting); + } + return valueToBoolean(elementValue); + }; + + window.getElementSetting = function (element, setting) { + const elementSetting = element.get(setting); + return elementSetting === "global" || elementSetting === "USE_GLOBAL" + ? settings.get(setting) + : elementSetting; + }; + + function fetchOne(tasks) { + return new Promise((resolve) => { + if (tasks.length === 0) { + resolve(true); + return; + } + const oneTask = tasks.shift(); + oneTask + .then(() => resolve(fetchOne(tasks))) + .catch(() => resolve(fetchOne(tasks))); + }); + } + + function fetchAll() { + const tasks = []; + tasks.push(settings.fetch({ silent: true })); + tasks.push(folders.fetch({ silent: true })); + tasks.push(sources.fetch({ silent: true })); + tasks.push(toolbars.fetch({ silent: true })); + tasks.push(items.fetch({ silent: true })); + + return fetchOne(tasks); + } + + window.fetchAll = fetchAll; + window.fetchOne = fetchOne; + window.reloadExt = function () { + browser.runtime.reload(); + }; + + window.appStarted = new Promise((resolve) => { + /** + * Init + */ + + fetchAll().then(function () { + window.items.sort(); + /** + * Load counters for specials + */ + info.refreshSpecialCounters(); + + /** + * Set events + */ + + sources.on("add", function (source) { + loader.download(source); + }); + + sources.on("change:url", function (source) { + loader.download(source); + }); + + sources.on("change:title", function (source) { + if (!source.get("title")) { + loader.download(source); + } + sources.sort(); + }); + + sources.on("change:hasNew", Animation.handleIconChange); + settings.on("change:icon", Animation.handleIconChange); + + info.setEvents(sources); + + /** + * Init + */ + + const version = settings.get("version") || 0; + if (version < 1) { + items.forEach((item) => { + item.save("id", item.get("id") + item.get("sourceID")); + }); + settings.save("version", 1); + } + + browser.alarms.create("scheduler", { + periodInMinutes: 1, + }); + + browser.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === "scheduler") { + if (!settings.get("disableAutoUpdate")) { + loader.downloadAll(); + } + const trashCleaningDelay = settings.get("autoremovetrash"); + if (trashCleaningDelay === 0) { + return; + } + const now = Date.now(); + const trashCleaningDelayInMs = + trashCleaningDelay * 1000 * 60 * 60 * 24; + items + .where({ trashed: true, deleted: false }) + .forEach((item) => { + if ( + now - item.get("trashedOn") > + trashCleaningDelayInMs + ) { + item.markAsDeleted(); + } + }); + } + }); + + /** + * onclick:button -> open RSS + */ + createLinksMenu(); + + /** + * Set icon + */ + Animation.stop(); + window.loaded = true; + resolve(true); + }); + }); +}); diff --git a/src/scripts/bgprocess/collections/Folders.js b/src/scripts/bgprocess/collections/Folders.js new file mode 100644 index 00000000..d398b59c --- /dev/null +++ b/src/scripts/bgprocess/collections/Folders.js @@ -0,0 +1,22 @@ +/** + * @module BgProcess + * @submodule collections/Folders + */ +define(['backbone', 'models/Folder', 'preps/indexeddb'], function (BB, Folder) { + + /** + * Collection of feed folders + * @class Folders + * @constructor + * @extends Backbone.Collection + */ + return BB.Collection.extend({ + model: Folder, + indexedDB: new Backbone.IndexedDB('folders-backbone'), + comparator: function (a, b) { + const t1 = (a.get('title') || '').trim().toLowerCase(); + const t2 = (b.get('title') || '').trim().toLowerCase(); + return t1 < t2 ? -1 : 1; + } + }); +}); \ No newline at end of file diff --git a/src/scripts/bgprocess/collections/Items.js b/src/scripts/bgprocess/collections/Items.js new file mode 100644 index 00000000..c7e483cc --- /dev/null +++ b/src/scripts/bgprocess/collections/Items.js @@ -0,0 +1,66 @@ +/** + * @module BgProcess + * @submodule collections/Items + */ +define(['backbone', 'models/Item', 'preps/indexeddb'], function (BB, Item) { + + function getS(val) { + return String(val).toLowerCase(); + } + + /** + * Collection of feed modules + * @class Items + * @constructor + * @extends Backbone.Collection + */ + let Items = BB.Collection.extend({ + model: Item, + batch: false, + indexedDB: new Backbone.IndexedDB('items-backbone'), + spaceship: function spaceship(val1, val2) { + if ((val1 === null || val2 === null) || (typeof val1 !== typeof val2)) { + return null; + } + if (typeof val1 === 'string') { + return (val1).localeCompare(val2); + } else { + if (val1 > val2) { + return 1; + } else if (val1 < val2) { + return -1; + } + return 0; + } + }, + comparator: function (a, b, sorting) { + const sortBy = sorting ? settings.get('sortBy2') : settings.get('sortBy'); + const sortOrder = sorting ? settings.get('sortOrder2') : settings.get('sortOrder'); + + const aVal = getS(a.get(sortBy)); + const bVal = getS(b.get(sortBy)); + + const val = this.spaceship(aVal, bVal); + + if (val === 0) { + return sorting ? 0 : this.comparator(a, b, true); + } + + if (sortOrder === 'desc') { + return -val; + } + return val; + + }, + initialize: function () { + this.listenTo(settings, 'change:sortOrder', this.sort); + this.listenTo(settings, 'change:sortOrder2', this.sort); + this.listenTo(settings, 'change:sortBy', this.sort); + this.listenTo(settings, 'change:sortBy2', this.sort); + } + }); + + + return Items; + +}); diff --git a/src/scripts/bgprocess/collections/Sources.js b/src/scripts/bgprocess/collections/Sources.js new file mode 100644 index 00000000..168e61f7 --- /dev/null +++ b/src/scripts/bgprocess/collections/Sources.js @@ -0,0 +1,22 @@ +/** + * @module BgProcess + * @submodule collections/Sources + */ +define(['backbone', 'models/Source', 'preps/indexeddb'], function (BB, Source) { + + /** + * Collection of feed modules + * @class Sources + * @constructor + * @extends Backbone.Collection + */ + return BB.Collection.extend({ + model: Source, + indexedDB: new Backbone.IndexedDB('sources-backbone'), + comparator: function (a, b) { + const t1 = (a.get('title') || '').trim().toLowerCase(); + const t2 = (b.get('title') || '').trim().toLowerCase(); + return t1.localeCompare(t2); + } + }); +}); diff --git a/src/scripts/bgprocess/collections/Toolbars.js b/src/scripts/bgprocess/collections/Toolbars.js new file mode 100644 index 00000000..2365d3f2 --- /dev/null +++ b/src/scripts/bgprocess/collections/Toolbars.js @@ -0,0 +1,63 @@ +/** + * @module BgProcess + * @submodule collections/Toolbars + */ +define([ + "backbone", + "models/Toolbar", + "staticdb/defaultToolbarItems", + "preps/indexeddb", +], function (BB, Toolbar, defaultToolbarItems) { + function getDataByRegion(data, region) { + if (!Array.isArray(data)) { + return null; + } + + for (let i = 0; i < data.length; i++) { + if (typeof data[i] !== "object") { + continue; + } + if (data[i].region === region) { + return data[i]; + } + } + + return null; + } + + /** + * Collection of feed modules + * @class Toolbars + * @constructor + * @extends Backbone.Collection + */ + let Toolbars = BB.Collection.extend({ + model: Toolbar, + indexedDB: new Backbone.IndexedDB("toolbars-backbone"), + parse: function (data) { + if (!data.length) { + return defaultToolbarItems; + } + + const parsedData = defaultToolbarItems; + if (!Array.isArray(parsedData)) { + return []; + } + + for (let i = 0; i < parsedData.length; i++) { + const fromdb = getDataByRegion(data, parsedData[i].region); + if (!fromdb || typeof fromdb !== "object") { + continue; + } + + if (fromdb.version && fromdb.version >= parsedData[i].version) { + parsedData[i] = fromdb; + } + } + + return parsedData; + }, + }); + + return Toolbars; +}); diff --git a/src/scripts/bgprocess/models/FeedLoader.js b/src/scripts/bgprocess/models/FeedLoader.js new file mode 100644 index 00000000..0be7d366 --- /dev/null +++ b/src/scripts/bgprocess/models/FeedLoader.js @@ -0,0 +1,479 @@ +/** + * @module BgProcess + * @submodule models/FeedLoader + */ +define(["modules/RSSParser", "favicon"], function (RSSParser, Favicon) { + return class FeedLoader { + constructor(loader) { + this.loader = loader; + this.request = new XMLHttpRequest(); + this.request.timeout = 1000 * 15; // TODO: make configurable + this.request.onload = this.onLoad.bind(this); + this.request.onerror = this.onError.bind(this); + this.request.ontimeout = this.onTimeout.bind(this); + this.request.onabort = this.onAbort.bind(this); + } + + onAbort() { + this.model.save({ isLoading: false }); + } + + parseProxyResponse() { + const response = JSON.parse(this.request.responseText); + return response.items.map((item) => { + const canonical = item.canonical + ? item.canonical[0] + : item.alternate[0]; + return { + id: item.originId, + title: item.title, + url: canonical.href, + date: item.updated + ? item.updated + : item.published + ? item.published + : Date.now(), + author: item.author ? item.author : "", + content: item.content + ? item.content.content + : item.summary.content, + sourceID: this.model.get("id"), + dateCreated: Date.now(), + }; + }); + } + + parseResponse() { + const response = this.request.responseText; + const parser = new RSSParser(response, this.model); + return parser.parse(); + } + + onLoad() { + let parsedData = []; + const queries = settings.get("queries"); + let modelUrl = this.model.get("url"); + const proxy = this.model.get("proxyThroughFeedly"); + const modelId = this.model.get("id"); + let lastArticle = this.model.get("lastArticle"); + + try { + parsedData = proxy + ? this.parseProxyResponse() + : this.parseResponse(); + } catch (e) { + console.log(`Couldn't parse`, modelUrl, e); + return this.onFeedProcessed({ success: false }); + } + + let foundNewArticles = false; + let createdNo = 0; + const currentItems = items.where({ + sourceID: modelId, + }); + const earliestDate = Math.min( + 0, + ...currentItems.map((item) => { + return item.get("date"); + }) + ); + + RegExp.escape = function (text) { + return String(text).replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); + }; + const insert = []; + + parsedData.forEach((item) => { + const existingItem = + items.get(item.id) || items.get(item.oldId); + + if (existingItem) { + if (existingItem.get("deleted")) { + return; + } + const areDifferent = function (newItem, existingItem) { + const existingContent = existingItem.get("content"); + const newContent = newItem.content; + if (existingContent !== newContent) { + const existingContentFragment = document + .createRange() + .createContextualFragment(existingContent); + if (!existingContentFragment) { + return true; + } + const newContentFragment = document + .createRange() + .createContextualFragment(newContent); + if (!newContentFragment) { + return true; + } + let existingContentText = ""; + [...existingContentFragment.children].forEach( + (child) => { + existingContentText += child.innerText; + } + ); + + let newContentText = ""; + [...newContentFragment.children].forEach( + (child) => { + newContentText += child.innerText; + } + ); + + if (!existingContentText) { + return true; + } + if ( + existingContentText.trim() !== + newContentText.trim() + ) { + return true; + } + } + return ( + existingItem.get("title").trim() !== + newItem.title.trim() + ); + }; + + if (areDifferent(item, existingItem)) { + insert.push({ + id: item.id, + content: item.content, + title: item.title, + date: item.date, + author: item.author, + enclosure: item.enclosure, + unread: true, + visited: false, + parsedContent: {}, + }); + // existingItem.save({ + // content: item.content, + // title: item.title, + // date: item.date, + // author: item.author, + // enclosure: item.enclosure, + // unread: true, + // visited: false, + // parsedContent: {} + // }); + } + return; + } + if (earliestDate > item.date) { + console.log( + "discarding entry with date older than the earliest know article in the feed", + modelUrl + ); + return; + } + foundNewArticles = true; + item.pinned = queries.some((query) => { + query = query.trim(); + if (!query) { + return false; + } + let searchInContent = false; + if (query[0] && query[0] === ":") { + query = query.replace(/^:/, "", query); + searchInContent = true; + } + if (!query) { + return false; + } + const expression = new RegExp(RegExp.escape(query), "i"); + + const cleanedTitle = item.title + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, ""); + const cleanedAuthor = item.author + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, ""); + const cleanedContent = searchInContent + ? item.content + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + : ""; + return ( + expression.test(cleanedTitle) || + expression.test(cleanedAuthor) || + (searchInContent && expression.test(cleanedContent)) + ); + }); + + insert.push(item); + lastArticle = Math.max(lastArticle, item.date); + createdNo++; + }); + items.add(insert, { sort: false, merge: true }); + + // Explicitly save each new item to ensure persistence + if (insert.length > 0) { + insert.forEach((item) => { + const model = items.get(item.id); + if (model) { + model.save(); + } + }); + } + + items.sort({ + silent: true, + }); + if (foundNewArticles) { + items.trigger("search"); + loader.itemsDownloaded = true; + // remove old deleted content + const fetchedIDs = parsedData.map((item) => { + return item.id; + }); + if (fetchedIDs.length > 0) { + items + .where({ + sourceID: modelId, + deleted: true, + }) + .forEach((item) => { + if (item.emptyDate) { + return; + } + if (fetchedIDs.includes(item.id)) { + return; + } + item.destroy(); + }); + } + } + + const articlesCount = items.where({ + sourceID: modelId, + trashed: false, + }).length; + + const unreadArticlesCount = items.where({ + sourceID: this.model.get("id"), + unread: true, + trashed: false, + }).length; + + if (this.request.responseURL !== modelUrl) { + modelUrl = this.request.responseURL; + } + + const modelUpdate = { + count: unreadArticlesCount, + countAll: articlesCount, + lastUpdate: Date.now(), + hasNew: foundNewArticles || this.model.get("hasNew"), + lastStatus: 200, + lastArticle: lastArticle, + url: modelUrl, + }; + + info.set({ + allCountUnvisited: info.get("allCountUnvisited") + createdNo, + }); + + function isFaviconExpired(model) { + return ( + model.get("faviconExpires") < + Math.round(new Date().getTime() / 1000) + ); + } + + if (isFaviconExpired(this.model)) { + return Favicon.getFavicon(this.model) + .then( + (response) => { + modelUpdate.favicon = response.favicon; + modelUpdate.faviconExpires = + response.faviconExpires; + }, + (err) => { + modelUpdate.faviconExpires = + Math.round(new Date().getTime() / 1000) + + 60 * 60 * 24 * 7; + console.warn( + `Couldn't load favicon for:`, + modelUrl, + err + ); + } + ) + .finally(() => { + return this.onFeedProcessed({ data: modelUpdate }); + }); + } + + return this.onFeedProcessed({ data: modelUpdate }); + } + + onTimeout() { + return this.onFeedProcessed({ success: false }); + } + + onError() { + this.model.save({ + lastStatus: this.request.status, + }); + return this.onFeedProcessed({ + success: false, + isOnline: this.request.status > 0, + }); + } + + getAutoRemoveTime(model) { + return parseInt(model.get("autoremove")) === -1 + ? parseInt(settings.get("autoremove")) + : parseInt(model.get("autoremove")); + } + + getAutoRemoveSetting(model) { + return getElementSetting(model, "autoremovesetting"); + } + + removeOldItems() { + const autoRemove = this.getAutoRemoveTime(this.model); + if (!autoRemove) { + return; + } + const itemsFilter = { + sourceID: this.model.get("id"), + deleted: false, + pinned: false, + }; + + const autoRemoveSetting = this.getAutoRemoveSetting(this.model); + if (autoRemoveSetting === "KEEP_UNVISITED") { + itemsFilter["visited"] = true; + } + + if (autoRemoveSetting === "KEEP_UNREAD") { + itemsFilter["unread"] = false; + itemsFilter["visited"] = true; + } + + const now = Date.now(); + items.where(itemsFilter).forEach((item) => { + const date = item.get("dateCreated") || item.get("date"); + const removalDelayInMs = + this.getAutoRemoveTime(this.model) * 24 * 60 * 60 * 1000; + if (now - date > removalDelayInMs) { + item.markAsDeleted(); + } + }); + } + + onFeedProcessed(result = {}) { + const success = `success` in result ? result.success : true; + const isOnline = + `isOnline` in result + ? result.isOnline && + (typeof navigator.onLine !== "undefined" + ? navigator.onLine + : true) + : true; + if (success && isOnline) { + this.removeOldItems(this.model); + } + const data = `data` in result ? result.data : {}; + Object.assign(data, { + isLoading: false, + lastChecked: Date.now(), + errorCount: success + ? 0 + : isOnline + ? this.model.get("errorCount") + 1 + : this.model.get("errorCount"), + folderID: + this.model.get("folderID") === "" + ? "0" + : this.model.get("folderID"), + }); + + this.model.save(data); + this.model.trigger("update", { ok: success || !isOnline }); + this.loader.sourceLoaded(this.model); + this.downloadNext(); + } + + downloadNext() { + this.model = this.loader.sourcesToLoad.shift(); + if (!this.model) { + return this.loader.workerFinished(this); + } + if (loader.sourcesLoading.includes(this.model)) { + // may happen if source is still loading after last attempt + return this.downloadNext(); + } + + let sourceUrl = this.model.get("url"); + const origin = new URL(sourceUrl).origin; + navigator.locks + .request(origin, () => { + if ( + Date.now() < + (this.loader.timestamps[origin] || 0) + 1000 * 1 && + origin.includes("openrss.org") + ) { + return false; + } + this.loader.timestamps[origin] = Date.now(); + return true; + }) + .then((canContinue) => { + if (!canContinue) { + this.loader.sourcesToLoad.push(this.model); + return this.downloadNext(); + } + + this.loader.sourcesLoading.push(this.model); + if (settings.get("showSpinner")) { + this.model.set("isLoading", true); + } + const shouldUseFeedlyCache = + this.model.get("proxyThroughFeedly"); + if (shouldUseFeedlyCache) { + const itemsArray = items.where({ + sourceID: this.model.get("sourceID"), + }); + let date = 0; + itemsArray.forEach((item) => { + if (item.date > date) { + date = item.date; + } + }); + sourceUrl = + "https://cloud.feedly.com/v3/streams/contents?streamId=feed%2F" + + encodeURIComponent(sourceUrl) + + "&count=" + + 1000 + + ("&newerThan=" + date); + } + this.request.open("GET", sourceUrl); + if (sourceUrl.startsWith("https://openrss.org/")) { + this.request.setRequestHeader( + "User-Agent", + navigator.userAgent + " + SmartRSS" + ); + } + if ( + !shouldUseFeedlyCache && + (this.model.get("username") || + this.model.get("password")) + ) { + const username = this.model.get("username") || ""; + const password = this.model.getPass() || ""; + this.request.withCredentials = true; + this.request.setRequestHeader( + "Authorization", + "Basic " + btoa(`${username}:${password}`) + ); + } + this.request.send(); + }); + } + }; +}); diff --git a/src/scripts/bgprocess/models/Folder.js b/src/scripts/bgprocess/models/Folder.js new file mode 100644 index 00000000..c222a51b --- /dev/null +++ b/src/scripts/bgprocess/models/Folder.js @@ -0,0 +1,21 @@ +/** + * @module BgProcess + * @submodule models/Folder + */ +define(['backbone'], function (BB) { + + /** + * Model for feed folders + * @class Folder + * @constructor + * @extends Backbone.Model + */ + return BB.Model.extend({ + defaults: { + title: '', + opened: false, + count: 0, // unread + countAll: 0 + } + }); +}); \ No newline at end of file diff --git a/src/scripts/bgprocess/models/Info.js b/src/scripts/bgprocess/models/Info.js new file mode 100644 index 00000000..c84599bf --- /dev/null +++ b/src/scripts/bgprocess/models/Info.js @@ -0,0 +1,416 @@ +/** + * @module BgProcess + * @submodule models/Info + */ +define(["backbone", "modules/Animation"], function (BB, animation) { + let handleAllCountChange = function (model) { + if (settings.get("badgeMode") === "disabled") { + if (model === settings) { + browser.browserAction.setBadgeText({ text: "" }); + } + return; + } + + if (model === settings) { + if (settings.get("badgeMode") === "unread") { + info.off("change:allCountUnvisited", handleAllCountChange); + info.on("change:allCountUnread", handleAllCountChange); + } else { + info.off("change:allCountUnread", handleAllCountChange); + info.on("change:allCountUnvisited", handleAllCountChange); + } + } + if (info.badgeTimeout) { + return; + } + + info.badgeTimeout = setTimeout(function () { + let val; + if (settings.get("badgeMode") === "unread") { + val = + info.get("allCountUnread") > 99 + ? "+" + : info.get("allCountUnread"); + } else { + val = + info.get("allCountUnvisited") > 99 + ? "+" + : info.get("allCountUnvisited"); + } + + val = val <= 0 ? "" : String(val); + browser.browserAction.setBadgeText({ text: val }); + browser.browserAction.setBadgeBackgroundColor({ color: "#777" }); + info.badgeTimeout = null; + }); + }; + + /** + * This model stores info about count of read/unread/unvisited/total of all feeds and in trash + * @class Info + * @constructor + * @extends Backbone.Model + */ + let Info = BB.Model.extend({ + defaults: { + id: "info-id", + allCountUnread: 0, + allCountTotal: 0, + allCountUnvisited: 0, + trashCountUnread: 0, + trashCountTotal: 0, + pinnedCountUnread: 0, + pinnedCountTotal: 0, + }, + badgeTimeout: null, + refreshSpecialCounters: function () { + this.set({ + allCountUnread: items.where({ + trashed: false, + deleted: false, + unread: true, + }).length, + allCountTotal: items.where({ trashed: false, deleted: false }) + .length, + allCountUnvisited: items.where({ + visited: false, + trashed: false, + }).length, + trashCountUnread: items.where({ + trashed: true, + deleted: false, + unread: true, + }).length, + trashCountTotal: items.where({ trashed: true, deleted: false }) + .length, + pinnedCountUnread: items.where({ + trashed: false, + deleted: false, + unread: true, + pinned: true, + }).length, + pinnedCountTotal: items.where({ + trashed: false, + deleted: false, + pinned: true, + }).length, + }); + + sources.forEach(function (source) { + source.set({ + count: items.where({ + trashed: false, + sourceID: source.id, + unread: true, + }).length, + countAll: items.where({ + trashed: false, + sourceID: source.id, + }).length, + }); + }); + + folders.forEach(function (folder) { + let count = 0; + let countAll = 0; + sources + .where({ folderID: folder.id }) + .forEach(function (source) { + count += source.get("count"); + countAll += source.get("countAll"); + }); + folder.set({ count: count, countAll: countAll }); + }); + }, + setEvents: function () { + settings.on("change:badgeMode", handleAllCountChange); + if (settings.get("badgeMode") === "unread") { + info.on("change:allCountUnread", handleAllCountChange); + } else if (settings.get("badgeMode") === "unvisited") { + info.on("change:allCountUnvisited", handleAllCountChange); + } + handleAllCountChange(); + + sources.on("destroy", function (source) { + let trashUnread = 0; + let trashAll = 0; + let allUnvisited = 0; + let pinnedAll = 0; + let pinnedUnread = 0; + items + .where({ sourceID: source.get("id") }) + .forEach(function (item) { + if (!item.get("deleted")) { + if (!item.get("visited")) { + allUnvisited++; + } + if (item.get("trashed")) { + trashAll++; + } + if (item.get("trashed") && item.get("unread")) { + trashUnread++; + } + if (item.get("pinned") && !item.get("trashed")) { + pinnedAll++; + } + if ( + item.get("pinned") && + !item.get("trashed") && + item.get("unread") + ) { + pinnedUnread++; + } + } + item.destroy(); + }); + + info.set({ + allCountUnread: + info.get("allCountUnread") - source.get("count"), + allCountTotal: + info.get("allCountTotal") - source.get("countAll"), + allCountUnvisited: + info.get("allCountUnvisited") - allUnvisited, + trashCountUnread: + info.get("trashCountUnread") - trashUnread, + trashCountTotal: info.get("trashCountTotal") - trashAll, + pinnedCountUnread: + info.get("pinnedCountUnread") - pinnedUnread, + pinnedCountTotal: info.get("pinnedCountAll") - pinnedAll, + }); + + if (source.get("folderID")) { + const folder = folders.findWhere({ + id: source.get("folderID"), + }); + if (folder) { + folder.set({ + count: folder.get("count") - source.get("count"), + countAll: + folder.get("countAll") - source.get("countAll"), + }); + } + } + + if (source.get("hasNew")) { + animation.handleIconChange(); + } + }); + + items.on("change:unread", function (model) { + const source = model.getSource(); + if (!model.previous("trashed")) { + if (model.get("unread") === true) { + source.set({ + count: source.get("count") + 1, + }); + } else { + source.set({ + count: source.get("count") - 1, + }); + + if ( + source.get("count") === 0 && + source.get("hasNew") === true + ) { + source.save("hasNew", false); + } + } + } else if (!model.get("deleted")) { + info.set({ + trashCountUnread: + info.get("trashCountUnread") + + (model.get("unread") ? 1 : -1), + }); + } + }); + + items.on("change:trashed", function (model) { + const source = model.getSource(); + if (model.get("unread") === true) { + if (model.get("trashed") === true) { + source.set({ + count: source.get("count") - 1, + countAll: source.get("countAll") - 1, + }); + + if ( + source.get("count") === 0 && + source.get("hasNew") === true + ) { + source.save("hasNew", false); + } + } else { + source.set({ + count: source.get("count") + 1, + countAll: source.get("countAll") + 1, + }); + } + + if (!model.get("deleted")) { + info.set({ + trashCountTotal: + info.get("trashCountTotal") + + (model.get("trashed") ? 1 : -1), + trashCountUnread: + info.get("trashCountUnread") + + (model.get("trashed") ? 1 : -1), + }); + } + } else { + source.set({ + countAll: + source.get("countAll") + + (model.get("trashed") ? -1 : 1), + }); + + if (!model.get("deleted")) { + info.set({ + trashCountTotal: + info.get("trashCountTotal") + + (model.get("trashed") ? 1 : -1), + }); + } + } + + if (!model.get("deleted")) { + info.set({ + pinnedCountTotal: + info.get("pinnedCountTotal") + + (model.get("trashed") ? -1 : 1) * + (model.get("pinned") ? 1 : 0), + pinnedCountUnread: + info.get("pinnedCountUnread") + + (model.get("trashed") ? -1 : 1) * + (model.get("pinned") ? 1 : 0), + }); + } + }); + + items.on("change:deleted", function (model) { + if (model.previous("trashed") === true) { + info.set({ + trashCountTotal: info.get("trashCountTotal") - 1, + trashCountUnread: !model.previous("unread") + ? info.get("trashCountUnread") + : info.get("trashCountUnread") - 1, + }); + } + }); + + items.on("change:pinned", function (model) { + const change = model.previous("pinned") ? -1 : 1; + info.set({ + pinnedCountTotal: info.get("pinnedCountTotal") + change, + pinnedCountUnread: !model.previous("unread") + ? info.get("pinnedCountUnread") + : info.get("pinnedCountUnread") + change, + }); + }); + + items.on("change:visited", function (model) { + info.set({ + allCountUnvisited: + info.get("allCountUnvisited") + + (model.get("visited") ? -1 : 1), + }); + }); + + sources.on("change:count", function (source) { + // SPECIALS + info.set({ + allCountUnread: + info.get("allCountUnread") + + source.get("count") - + source.previous("count"), + }); + + // FOLDER + if (!source.get("folderID")) { + return; + } + + const folder = folders.findWhere({ + id: source.get("folderID"), + }); + if (!folder) { + return; + } + + folder.set({ + count: + folder.get("count") + + source.get("count") - + source.previous("count"), + }); + }); + + sources.on("change:countAll", function (source) { + // SPECIALS + info.set({ + allCountTotal: + info.get("allCountTotal") + + source.get("countAll") - + source.previous("countAll"), + }); + + // FOLDER + if (!source.get("folderID")) { + return; + } + + const folder = folders.findWhere({ + id: source.get("folderID"), + }); + if (!folder) { + return; + } + + folder.set({ + countAll: + folder.get("countAll") + + source.get("countAll") - + source.previous("countAll"), + }); + }); + + sources.on("change:folderID", function (source) { + let folder; + if (source.get("folderID")) { + folder = folders.findWhere({ id: source.get("folderID") }); + if (!folder) { + return; + } + + folder.set({ + count: folder.get("count") + source.get("count"), + countAll: + folder.get("countAll") + source.get("countAll"), + }); + } + + if (source.previous("folderID")) { + folder = folders.findWhere({ + id: source.previous("folderID"), + }); + if (!folder) { + return; + } + + folder.set({ + count: Math.max( + folder.get("count") - source.get("count"), + 0 + ), + countAll: Math.max( + folder.get("countAll") - source.get("countAll"), + 0 + ), + }); + } + }); + }, + }); + + return Info; +}); diff --git a/src/scripts/bgprocess/models/Item.js b/src/scripts/bgprocess/models/Item.js new file mode 100644 index 00000000..335dbbc2 --- /dev/null +++ b/src/scripts/bgprocess/models/Item.js @@ -0,0 +1,78 @@ +/** + * @module BgProcess + * @submodule models/Item + */ +define(['backbone'], function (BB) { + + /** + * Module for each article + * @class Item + * @constructor + * @extends Backbone.Model + */ + let Item = BB.Model.extend({ + defaults: { + title: '', + author: '', + url: '', + date: 0, + content: 'No content loaded.', + sourceID: -1, + unread: true, + visited: false, + deleted: false, + trashed: false, + pinned: false, + dateCreated: 0, + enclosure: [], + emptyDate: false, + trashedOn: 0, + parsedContent: {} + }, + markAsDeleted: function () { + this.save({ + trashed: true, + deleted: true, + visited: true, + unread: false, + enclosure: '', + pinned: false, + content: '', + author: '', + title: '', + trashedOn: 0, + parsedContent: {} + }); + }, + trash: function () { + this.save({ + trashed: true, + visited: true, + trashedOn: Date.now() + }); + }, + _source: null, + getSource: function () { + if (!this._source) { + this._source = sources.findWhere({id: this.get('sourceID')}); + } + return this._source; + }, + query: function (o) { + if (!o) { + return true; + } + for (let i in o) { + if (o.hasOwnProperty(i)) { + if (this.get(i) !== o[i]) { + return false; + } + } + } + return true; + } + }); + + return Item; + +}); diff --git a/src/scripts/bgprocess/models/Loader.js b/src/scripts/bgprocess/models/Loader.js new file mode 100644 index 00000000..ba5bab50 --- /dev/null +++ b/src/scripts/bgprocess/models/Loader.js @@ -0,0 +1,270 @@ +/** + * @module BgProcess + * @submodule models/Loader + */ +define([ + "backbone", + "modules/RSSParser", + "modules/Animation", + "favicon", + "models/FeedLoader", +], function (BB, RSSParser, animation, Favicon, FeedLoader) { + /** + * Updates feeds and keeps info about progress + * @class Loader + * @constructor + * @extends Backbone.Model + */ + return class Loader { + connected(p) { + this.port = p; + p.onDisconnect.addListener(() => { + this.port = null; + }); + } + + get loading() { + return this._loading; + } + + get loaded() { + return this._loaded; + } + + get maxSources() { + return this._maxSources; + } + + set loading(value) { + this._loading = value; + if (this.port !== null) { + this.port.postMessage({ + key: "loading", + value: value, + }); + } + } + + set maxSources(value) { + this._maxSources = value; + if (this.port !== null) { + this.port.postMessage({ + key: "maxSources", + value: value, + }); + } + } + + set loaded(value) { + this._loaded = value; + if (this.port !== null) { + this.port.postMessage({ + key: "loaded", + value: value, + }); + } + } + + constructor() { + browser.runtime.onConnect.addListener(this.connected.bind(this)); + this.port = null; + this._maxSources = 0; + this._loaded = 0; + this._loading = false; + this.sourcesToLoad = []; + this.sourcesLoading = []; + this.itemsDownloaded = false; + this.sourcesLoading = []; + this.loaders = []; + this.timestamps = {}; + } + + addSources(source) { + if (source instanceof Folder) { + this.addSources( + sources.where({ + folderID: source.id, + }) + ); + return; + } + if (Array.isArray(source)) { + source.forEach((s) => { + this.addSources(s); + }); + return; + } + if (source instanceof Source) { + // don't add source to list if it is there already, it's going to get loaded soon enough + if (this.sourcesToLoad.includes(source)) { + return; + } + this.sourcesToLoad.push(source); + this.maxSources = this.maxSources + 1; + } + } + + abortDownloading() { + this.sourcesToLoad = []; + this.loaders.forEach((loader) => { + loader.request.abort(); + loader.request = null; + delete loader.request; + loader = null; + }); + this.loaders = []; + this.sourcesLoading = []; + this.workersFinished(); + } + + startDownloading() { + const workersRunning = this.loaders.length; + this.loading = true; + animation.start(); + const maxWorkers = Math.min( + settings.get("concurrentDownloads"), + this.sourcesToLoad.length + ); + const workers = Math.max(0, maxWorkers - workersRunning); + for (let i = 0; i < workers; i++) { + const feedLoader = new FeedLoader(this); + this.loaders.push(feedLoader); + feedLoader.downloadNext(); + } + } + + download(sourcesToDownload) { + if (!sourcesToDownload) { + return; + } + if (!Array.isArray(sourcesToDownload)) { + sourcesToDownload = [sourcesToDownload]; + } + this.addSources(sourcesToDownload); + this.startDownloading(); + } + + downloadAll(force) { + let sourcesArr = sources.toArray(); + if (!force) { + let globalUpdateFrequency = settings.get("updateFrequency"); + sourcesArr = sourcesArr.filter(function (source) { + let sourceUpdateFrequency = source.get("updateEvery"); + if (sourceUpdateFrequency === 0) { + return false; + } + let updateFrequency = + sourceUpdateFrequency > 0 + ? sourceUpdateFrequency + : globalUpdateFrequency; + if (updateFrequency === 0) { + return false; + } + const lastChecked = source.get("lastChecked"); + if (!lastChecked) { + return true; + } + const multiplier = 1 + source.get("errorCount"); + const finalFrequency = + Math.min( + updateFrequency * 60 * 1000 * multiplier, + 7 * 24 * 60 * 60 * 1000 + ) - + 60 * 1000; + // reduce by a minute to not delay loading by extra minute if new load starts early + return lastChecked <= Date.now() - finalFrequency; + }); + } + // no sources to load yet + if (sourcesArr.length === 0) { + return; + } + // add sources to list + this.addSources(sourcesArr); + this.startDownloading(); + } + + handleNotifications() { + if (settings.get("soundNotifications")) { + this.playNotificationSound(); + } + if (settings.get("systemNotifications")) { + this.displaySystemNotification(); + } + } + + displaySystemNotification() { + browser.notifications.create({ + type: "basic", + title: "Smart RSS", + message: "New articles found", + }); + } + + playNotificationSound() { + let audio; + if ( + !settings.get("useSound") || + settings.get("useSound") === ":user" + ) { + audio = new Audio(settings.get("defaultSound")); + } else if (settings.get("useSound") === ":none") { + audio = false; + } else { + audio = new Audio( + "/sounds/" + settings.get("useSound") + ".ogg" + ); + } + if (audio) { + audio.volume = parseFloat(settings.get("soundVolume")); + audio.play(); + } + } + + workerFinished(worker) { + const loaderIndex = this.loaders.indexOf(worker); + if (loaderIndex > -1) { + this.loaders[loaderIndex] = null; + this.loaders.splice(loaderIndex, 1); + } + if (this.loaders.length > 0) { + return; + } + this.workersFinished(); + } + + workersFinished() { + // IF DOWNLOADING FINISHED, DELETE ITEMS WITH DELETED SOURCE (should not really happen) + const sourceIDs = sources.pluck("id"); + let foundSome = false; + items.toArray().forEach((item) => { + if (sourceIDs.indexOf(item.get("sourceID")) === -1) { + item.destroy(); + foundSome = true; + } + }); + if (foundSome) { + info.refreshSpecialCounters(); + } + if (this.itemsDownloaded) { + this.handleNotifications(); + } + this.maxSources = 0; + this.loaded = 0; + this.loading = false; + this.itemsDownloaded = false; + this.sourcesToLoad = []; + this.loaders = []; + this.sourcesLoading = []; + animation.stop(); + } + + sourceLoaded(model) { + this.loaded++; + const modelIndex = this.sourcesLoading.indexOf(model); + if (modelIndex > -1) { + this.sourcesLoading.splice(modelIndex, 1); + } + } + }; +}); diff --git a/src/scripts/bgprocess/models/Settings.js b/src/scripts/bgprocess/models/Settings.js new file mode 100644 index 00000000..4aa2b96f --- /dev/null +++ b/src/scripts/bgprocess/models/Settings.js @@ -0,0 +1,215 @@ +/** + * @module BgProcess + * @submodule models/Settings + */ +define(['backbone', 'preps/indexeddb'], function (BB) { + /** + * Test `navigator.language` and if it matches some available language + */ + function getLangFromNavigator() { + const ln = String(navigator.language) + .split('-')[0]; + const available = ['en', 'cs', 'sk', 'de', 'tr', 'pl', 'ru', 'hu', 'nl', 'fr', 'pt', 'hr']; + const index = available.indexOf(ln); + if (index >= 0) { + return available[index]; + } + return 'en'; + } + + /** + * User settings + * @class Settings + * @constructor + * @extends Backbone.Model + */ + let Settings = BB.Model.extend({ + defaults: { + id: 'settings-id', + lang: getLangFromNavigator(), + dateType: 'normal', // normal = DD.MM.YYYY, ISO = YYYY-MM-DD, US = MM/DD/YYYY + layout: 'horizontal', // or vertical + lines: '2', // one-line, two-lines + posA: '250,*', + posB: '350,*', + posC: '50%,*', + sortOrder: 'desc', + sortOrder2: 'asc', + icon: 'orange', + sourcesFoundIcon: 'arrow-orange', + readOnVisit: false, + askOnOpening: true, + fullDate: false, + hoursFormat: '24h', + articleFontSize: '100', + uiFontSize: '100', + disableDateGroups: false, + badgeMode: 'disabled', + circularNavigation: true, + sortBy: 'date', + sortBy2: 'title', + askRmPinned: 'trashed', + titleIsLink: true, + soundNotifications: false, + defaultSound: '', + useSound: ':user', + soundVolume: 1, // min: 0, max: 1 + showSpinner: true, + concurrentDownloads: 5, + updateFrequency: 15, // in minutes + disableAutoUpdate: false, + openInNewTab: true, + showFullHeadline: false, + selectFirstArticle: true, + selectAllFeeds: true, + openEnclosure: false, + autoremove: false, + autoremovesetting: 'KEEP_UNREAD', + autoremovetrash: 0, + openNewTab: 'background', + userStyle: '', + defaultStyle: ':root {\n' + + '\n' + + ' --blue-40: #45a1ff;\n' + + ' --blue-50: #0a84ff;\n' + + ' --blue-50-a30: rgba(10, 132, 255, 0.3);\n' + + ' --blue-60: #0060df;\n' + + ' --blue-70: #003eaa;\n' + + ' --blue-80: #002275;\n' + + ' --blue-90: #000f40;\n' + + '\n' + + ' --grey-10: #f9f9fa;\n' + + ' --grey-20: #ededf0;\n' + + ' --grey-30: #d7d7db;\n' + + ' --grey-40: #b1b1b3;\n' + + ' --grey-50: #737373;\n' + + ' --grey-60: #4a4a4f;\n' + + ' --grey-70: #38383d;\n' + + ' --grey-80: #2a2a2e;\n' + + ' --grey-90: #0c0c0d;\n' + + '\n' + + ' --white: #fff;\n' + + '}', + hotkeys: { + feeds: { + 'up': 'feeds:selectPrevious', + 'down': 'feeds:selectNext', + 'u': 'feeds:selectPrevious', + 'j': 'feeds:selectNext', + + 'ctrl+left': 'feeds:closeFolders', + 'ctrl+right': 'feeds:openFolders', + 'left': 'feeds:toggleFolder', + 'right': 'feeds:showArticles', + 'enter': 'feeds:showAndFocusArticles', + + 'shift+j': 'feeds:selectNext', + 'shift+down': 'feeds:selectNext', + 'shift+u': 'feeds:selectPrevious', + 'shift+up': 'feeds:selectPrevious' + }, + articles: { + 'd': 'articles:delete', + 'del': 'articles:delete', + 'shift+d': 'articles:delete', + 'shift+del': 'articles:delete', + 'ctrl+f': 'articles:focusSearch', + 'shift+enter': 'articles:fullArticle', + 'enter': 'articles:fullArticle', + 'k': 'articles:mark', + 'j': 'articles:selectNext', + 'down': 'articles:selectNext', + 'u': 'articles:selectPrevious', + 'up': 'articles:selectPrevious', + + 'shift+j': 'articles:selectNext', + 'shift+down': 'articles:selectNext', + 'shift+u': 'articles:selectPrevious', + 'shift+up': 'articles:selectPrevious', + + 'g': 'articles:markAndNextUnread', + 't': 'articles:markAndPrevUnread', + 'h': 'articles:nextUnread', + 'y': 'articles:prevUnread', + 'z': 'articles:prevUnread', + + 'ctrl+shift+a': 'articles:markAllAsRead', + 'ctrl+a': 'articles:selectAll', + 'p': 'articles:pin', + 'n': 'articles:undelete', + 'space': 'articles:spaceThrough', + 'r': 'articles:update', + + 'pgup': 'articles:pageUp', + 'pgdown': 'articles:pageDown', + 'end': 'articles:scrollToBottom', + 'home': 'articles:scrollToTop' + }, + content: { + 'up': 'content:scrollUp', + 'down': 'content:scrollDown', + 'space': 'content:spaceThrough', + 'pgup': 'content:pageUp', + 'pgdown': 'content:pageDown', + 'end': 'content:scrollToBottom', + 'home': 'content:scrollToTop', + 'del': 'content:delete', + 'd': 'content:delete', + 'k': 'content:mark', + + 'g': 'articles:markAndNextUnread', + 't': 'articles:markAndPrevUnread', + 'h': 'articles:nextUnread', + 'y': 'articles:prevUnread', + 'z': 'articles:prevUnread', + 'j': 'articles:selectNext', + 'u': 'articles:selectPrevious' + }, + sandbox: { + 'del': 'content:delete', + 'd': 'content:delete', + 'k': 'content:mark', + 'space': 'content:spaceThrough', + + 'g': 'articles:markAndNextUnread', + 't': 'articles:markAndPrevUnread', + 'h': 'articles:nextUnread', + 'y': 'articles:prevUnread', + 'z': 'articles:prevUnread', + 'j': 'articles:selectNext', + 'u': 'articles:selectPrevious' + }, + global: { + 'shift+1': 'feeds:focus', + 'shift+2': 'articles:focus', + 'shift+3': 'content:focus', + 'shift+4': 'content:focusSandbox', + 'esc': 'global:hideOverlays' + } + }, + version: 1, + showNewArticlesIcon: 'always', + hideSubscribedFeeds: 'mark', + detectFeeds: true, + showAllFeeds: true, + showPinned: true, + showOnlyUnreadSources: false, + displayFaviconInsteadOfPin: false, + faviconSource: 'internal', + queries: [], + invertColors: false, + defaultView: 'feed', + cacheParsedArticles: false, + defaultToUnreadOnly: false, + displaySubscribeToLink: true, + }, + /** + * @property localStorage + * @type Backbone.IndexedDB + * @default *settings-backbone* + */ + indexedDB: new Backbone.IndexedDB('settings-backbone') + }); + + return Settings; +}); diff --git a/src/scripts/bgprocess/models/Source.js b/src/scripts/bgprocess/models/Source.js new file mode 100644 index 00000000..90fc05b1 --- /dev/null +++ b/src/scripts/bgprocess/models/Source.js @@ -0,0 +1,73 @@ +/** + * @module BgProcess + * @submodule models/Source + */ +define(['backbone'], function (BB) { + + /** + * Feed module + * @class Source + * @constructor + * @extends Backbone.Model + */ + let Source = BB.Model.extend({ + defaults: { + title: '', + url: '', + base: '', + updateEvery: -1, // in minutes, -1 to use global default + lastChecked: 0, + lastUpdate: 0, + count: 0, // unread + countAll: 0, + username: '', + password: '', + hasNew: false, + isLoading: false, + autoremove: -1, // in days + autoremovesetting: 'USE_GLOBAL', + proxyThroughFeedly: false, + favicon: '/images/feed.png', + faviconExpires: 0, + errorCount: 0, + lastArticle: 0, + uid: '', + openEnclosure: 'global', + folderID: '0', + defaultView: 'global', + lastStatus: 200, + }, + + initialize: function () { + this.set('isLoading', false); + }, + + getPass: function () { + const str = this.get('password'); + if (str.indexOf('enc:') !== 0) { + return str; + } + + let dec = ''; + for (let i = 4; i < str.length; i++) { + dec += String.fromCharCode(str.charCodeAt(i) - 13); + } + return dec; + }, + setPass: function (str) { + if (!str) { + this.save('password', ''); + return; + } + + let enc = 'enc:'; + for (let i = 0; i < str.length; i++) { + enc += String.fromCharCode(str.charCodeAt(i) + 13); + } + this.set('password', enc); + } + + }); + + return Source; +}); diff --git a/scripts/bgprocess/models/Toolbar.js b/src/scripts/bgprocess/models/Toolbar.js similarity index 90% rename from scripts/bgprocess/models/Toolbar.js rename to src/scripts/bgprocess/models/Toolbar.js index d04d5aab..61ff8b0a 100644 --- a/scripts/bgprocess/models/Toolbar.js +++ b/src/scripts/bgprocess/models/Toolbar.js @@ -1,55 +1,55 @@ -/** - * @module BgProcess - * @submodule models/Toolbar - */ -define(['backbone'], function (BB) { - - /** - * Region toolbar for buttons - * @class Toolbar - * @constructor - * @extends Backbone.Model - */ - var Toolbar = BB.Model.extend({ - defaults: { - /** - * @attribute region - * @type String - * @default feeds - */ - region: 'feeds', - - /** - * @attribute position - * @type String - * @default top - */ - position: 'top', - - /** - * List of actions. Each action = one button/search on toolbar - * @attribute actions - * @type Array - * @default [] - */ - actions: [], - - /** - * Version of toolbar (if any default changes are made -> version++) - * @attribute actions - * @type Array - * @default [] - */ - version: 1 - }, - - /** - * @method initialize - */ - initialize: function() { - // ... - } - }); - - return Toolbar; +/** + * @module BgProcess + * @submodule models/Toolbar + */ +define(['backbone'], function (BB) { + + /** + * Region toolbar for buttons + * @class Toolbar + * @constructor + * @extends Backbone.Model + */ + let Toolbar = BB.Model.extend({ + defaults: { + /** + * @attribute region + * @type String + * @default feeds + */ + region: 'feeds', + + /** + * @attribute position + * @type String + * @default top + */ + position: 'top', + + /** + * List of actions. Each action = one button/search on toolbar + * @attribute actions + * @type Array + * @default [] + */ + actions: [], + + /** + * Version of toolbar (if any default changes are made -> version++) + * @attribute actions + * @type Array + * @default [] + */ + version: 1 + }, + + /** + * @method initialize + */ + initialize: function() { + // ... + } + }); + + return Toolbar; }); \ No newline at end of file diff --git a/src/scripts/bgprocess/models/jsconfig.json b/src/scripts/bgprocess/models/jsconfig.json new file mode 100644 index 00000000..7556b0fc --- /dev/null +++ b/src/scripts/bgprocess/models/jsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "target": "es2018" + } +} diff --git a/src/scripts/bgprocess/modules/Animation.js b/src/scripts/bgprocess/modules/Animation.js new file mode 100644 index 00000000..0b617a44 --- /dev/null +++ b/src/scripts/bgprocess/modules/Animation.js @@ -0,0 +1,56 @@ +/** + * @module BgProcess + * @submodule modules/Animation + */ +define(function () { + /** + * Handles animation of browser action button icon + * @class Animation + * @constructor + * @extends Object + */ + const Animation = { + i: 2, + interval: null, + update: function () { + browser.browserAction.setIcon({ + path: "/images/reload_anim_" + this.i + ".png", + }); + this.i++; + if (this.i > 4) { + this.i = 1; + } + }, + stop: function () { + clearInterval(this.interval); + this.interval = null; + this.i = 1; + this.handleIconChange(); + }, + start: function () { + if (this.interval) { + return; + } + this.interval = setInterval(() => { + this.update(); + }, 400); + this.update(); + }, + handleIconChange: function () { + if (this.interval) { + return; + } + const icon = settings.get("icon"); + if (sources.findWhere({ hasNew: true }) && icon !== "disabled") { + browser.browserAction.setIcon({ + path: "/images/icon19-" + icon + ".png", + }); + } else { + browser.browserAction.setIcon({ + path: "/images/icon19.png", + }); + } + }, + }; + return Animation; +}); diff --git a/src/scripts/bgprocess/modules/RSSParser.js b/src/scripts/bgprocess/modules/RSSParser.js new file mode 100644 index 00000000..bacf6235 --- /dev/null +++ b/src/scripts/bgprocess/modules/RSSParser.js @@ -0,0 +1,361 @@ +/** + * @module BgProcess + * @submodule modules/RSSParser + */ +define(['he'], function (he) { + class RSSParser { + + getLink() { + const urlMatcher = /.+:\/\//; + let base = urlMatcher.exec(this.source.get('base')) ? this.source.get('base') : this.source.get('url'); // some feeds only give relative URLs but no base + + const node = this.currentNode; + let linkNode = node.querySelector('link[rel="alternate"]'); + if (!linkNode) { + linkNode = node.querySelector('link[type="text/html"]'); + } + + // prefer non atom links over atom links because of http://logbuch-netzpolitik.de/ + if (!linkNode || linkNode.prefix === 'atom') { + linkNode = node.querySelector('link'); + } + + if (!linkNode) { + const guid = node.querySelector('guid'); + let tmp; + if (guid && (tmp = guid.textContent.match(urlMatcher)) && tmp.length) { + linkNode = guid; + } + } + if (!linkNode) { + return false; + } + + let address = (linkNode.textContent || linkNode.getAttribute('href')).trim(); + + const match = urlMatcher.exec(address); + if (!match) { + try { + // it might be a relative URL, so try to convert it into one based on the base + address = new URL(address, base).toString(); + } catch (e) { + // not a valid URL + return false; + } + } + + return address.replace(/^(javascript:\.)/, ''); + } + + getSourceTitle() { + let title = this.document.querySelector('channel > title, feed > title, rss > title'); + if (!title || !(title.textContent).trim()) { + title = this.document.querySelector('channel > description, feed > description, rss > description'); + } + + if (!title || !(title.textContent).trim()) { + title = this.document.querySelector('channel > description, feed > description, rss > description'); + } + + if (!title || !(title.textContent).trim()) { + title = this.document.querySelector('channel > link, feed > link, rss > link'); + } + title = title && title.textContent ? title.textContent.trim() || 'rss' : 'rss'; + title = title.trim(); + return title.length ? title : ''; + } + + replaceUTCAbbr(str) { + str = String(str); + const rep = { + 'CET': '+0100', 'CEST': '+0200', 'EST': '', 'WET': '+0000', 'WEZ': '+0000', 'WEST': '+0100', + 'EEST': '+0300', 'BST': '+0100', 'EET': '+0200', 'IST': '+0100', 'KUYT': '+0400', 'MSD': '+0400', + 'MSK': '+0400', 'SAMT': '+0400' + }; + const reg = new RegExp('(' + Object.keys(rep).join('|') + ')', 'gi'); + return str.replace(reg, (all, abbr) => { + return rep[abbr]; + }); + } + + getDate() { + const node = this.currentNode; + let publicationDate = node.querySelector('pubDate, published'); + if (publicationDate) { + return (new Date(this.replaceUTCAbbr(publicationDate.textContent))).getTime() || 0; + } + + publicationDate = node.querySelector('date'); + if (publicationDate) { + return (new Date(this.replaceUTCAbbr(publicationDate.textContent))).getTime() || 0; + } + + publicationDate = node.querySelector('lastBuildDate, updated, update'); + + if (publicationDate) { + return (new Date(this.replaceUTCAbbr(publicationDate.textContent))).getTime() || 0; + } + return 0; + } + + getAuthor() { + const node = this.currentNode; + const feedTitle = this.source.get('title'); + let creator = node.querySelector('creator, author > name'); + if (creator) { + creator = creator.textContent.trim(); + } + + if (!creator) { + creator = node.querySelector('author'); + if (creator) { + creator = creator.textContent.trim(); + } + } + + if (!creator && feedTitle && feedTitle.length > 0) { + creator = feedTitle; + } + + if (creator) { + if (/^\S+@\S+\.\S+\s+\(.+\)$/.test(creator)) { + creator = creator.replace(/^\S+@\S+\.\S+\s+\((.+)\)$/, '$1'); + } + creator = creator.replace(/\s*\(\)\s*$/, ''); + return he.decode(creator.trim()); + } + + return 'no author'; + } + + getArticleTitle() { + const node = this.currentNode.querySelector('title'); + const title = node ? this.currentNode.querySelector('title').textContent.trim() : ''; + return he.decode(title); + } + + getArticleContent() { + const node = this.currentNode; + const encoded = node.querySelector('encoded'); + if (encoded) { + return he.decode(encoded.textContent); + } + + const description = node.querySelector('description'); + if (description) { + return he.decode(description.textContent); + } + + const content = node.querySelector('content'); + if (content) { + if (content.getAttribute('type') !== 'xhtml') { + return he.decode(content.textContent); + } + const childNodes = content.childNodes; + let stitchedText = ''; + const xmlSerializer = new XMLSerializer(); + [...childNodes].forEach((node) => { + if (node.nodeType !== Node.TEXT_NODE) { + stitchedText += xmlSerializer.serializeToString(node); + } + }); + + const text = stitchedText.replace(/xhtml:/g, ''); + return he.decode(text); + } + + const summary = node.querySelector('summary'); + if (summary) { + return summary.textContent; + } + + return ' '; + } + + getGuid() { + const node = this.currentNode; + let guid = node.querySelector('guid'); + if (!guid) { + guid = node.querySelector('id'); + } + return (guid ? guid.textContent : this.getLink() || '').trim() + this.source.get('id'); + } + + getOldGuid() { + const node = this.currentNode; + let guid = node.querySelector('guid'); + return (guid ? guid.textContent : this.getLink() || '').trim() + this.source.get('id'); + } + + getEnclosure(enclosureNode, title) { + let enclosure = {}; + enclosure.url = enclosureNode.hasAttribute('url') ? enclosureNode.getAttribute('url').replace(/^(javascript:\.)/, '') : ''; + enclosure.name = he.decode(enclosureNode.hasAttribute('url') ? enclosure.url.substring(enclosure.url.lastIndexOf('/') + 1) : title); + enclosure.type = enclosureNode.hasAttribute('type') ? enclosureNode.getAttribute('type') : ''; + enclosure.medium = enclosureNode.hasAttribute('medium') ? enclosureNode.getAttribute('medium') : this.getMediumFromType(enclosure.type, enclosure.name); + enclosure.medium = enclosure.url.includes('youtube.com') ? 'youtube' : enclosure.medium; + enclosure.length = enclosureNode.hasAttribute('length') ? enclosureNode.getAttribute('length') : ''; + return enclosure; + } + + getEnclosures() { + const node = this.currentNode; + const knownUrls = []; + const mediaTitleNode = [...node.getElementsByTagNameNS('http://search.yahoo.com/mrss/', 'title')][0]; + const title = mediaTitleNode ? mediaTitleNode.textContent : ''; + + const enclosures = []; + const enclosureNode = node.querySelector('enclosure'); + if (enclosureNode) { + const foundEnclosure = this.getEnclosure(enclosureNode, title); + enclosures.push(foundEnclosure); + knownUrls.push(foundEnclosure.url); + } + + const enclosureNodes = [...node.getElementsByTagNameNS('http://search.yahoo.com/mrss/', 'content')]; + enclosureNodes.forEach((enclosureNode) => { + const foundEnclosure = this.getEnclosure(enclosureNode, title); + if (knownUrls.includes(foundEnclosure.url)) { + return; + } + knownUrls.push(foundEnclosure.url); + enclosures.push(foundEnclosure); + }); + + return enclosures; + } + + getMediumFromType(type, name) { + const extension = name.split('.')[1] ? name.split('.')[1] : ''; + const splitType = type.split('/'); + if (splitType.length > 0) { + if (['audio', 'image', 'video'].includes(splitType[0])) { + return splitType[0]; + } + if (splitType[0] === 'text') { + return 'document'; + } + } + if (type.includes('application/octet-stream')) { + return 'executable'; + } + if (type.includes('application/x-msdownload')) { + return 'executable'; + } + const imgExtensions = [ + 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp' + ]; + if (imgExtensions.includes(extension)) { + return 'image'; + } + + + return ''; + } + + getBaseUrl() { + const rootElement = this.document.querySelector('rss, rdf, feed, channel'); + if (!rootElement) { + return; + } + let baseStr = rootElement.getAttribute('xml:base'); + if (!baseStr) { + baseStr = rootElement.getAttribute('xmlns:base'); + } + if (!baseStr) { + baseStr = rootElement.getAttribute('base'); + } + if (!baseStr) { + const node = rootElement.querySelector(':scope > link[rel="alternate"]'); + if (node) { + baseStr = node.textContent; + } + } + if (!baseStr) { + const node = rootElement.querySelector(':scope > link[rel="alternate"]'); + if (node) { + baseStr = node.getAttribute('href'); + } + } + if (!baseStr) { + const node = rootElement.querySelector(':scope > link:not([rel="self"])'); + if (node) { + baseStr = node.getAttribute('href'); + } + } + if (!baseStr) { + baseStr = new URL(this.source.get('url')).origin; + } + + const urlMatcher = /.+:\/\//; + return urlMatcher.exec(baseStr) ? baseStr : this.source.get('url'); + } + + + parse() { + const items = []; + const sourceData = {}; + + const nodes = [...this.document.querySelectorAll('item, entry')]; + + const title = he.decode(this.getSourceTitle()); + if (title && (this.source.get('title') === this.source.get('url') || !this.source.get('title'))) { + sourceData.title = title; + } + const baseUrl = this.getBaseUrl(); + if (baseUrl) { + sourceData.base = baseUrl; + } + + sourceData.uid = this.source.get('url').replace(/^(.*:)?(\/\/)?(ww+\.)?/, '').replace(/\/$/, ''); + this.source.save(sourceData); + + + [...nodes].forEach((node) => { + this.currentNode = node; + const newItem = { + id: this.getGuid(), + oldId: this.getOldGuid(), + title: this.getArticleTitle(), + url: this.getLink(), + date: this.getDate(), + author: this.getAuthor(), + content: this.getArticleContent(), + sourceID: this.source.get('id'), + enclosure: this.getEnclosures(), + dateCreated: Date.now(), + emptyDate: false + }; + if (newItem.date === 0) { + newItem.date = Date.now(); + newItem.emptyDate = true; + } + items.push(newItem); + }); + this.document = null; + this.source = null; + return items; + } + + constructor(response, source) { + const document = new DOMParser().parseFromString(response.trim(), 'text/xml'); + const error = document.querySelector('parsererror'); + if (error) { + throw error.textContent; + } + if (!document) { + throw 'No document specified'; + } + if (!(document instanceof XMLDocument)) { + throw 'Invalid document'; + } + if (!source) { + throw 'No source specified'; + } + this.document = document; + this.source = source; + } + } + + return RSSParser; +}); diff --git a/src/scripts/bgprocess/preps/indexeddb.js b/src/scripts/bgprocess/preps/indexeddb.js new file mode 100644 index 00000000..6461b59d --- /dev/null +++ b/src/scripts/bgprocess/preps/indexeddb.js @@ -0,0 +1,41 @@ +/** + * Prepare IndexedDB stores + * @module BgProcess + * @submodule preps/indexeddb + */ +define(['backbone', 'backboneDB'], function (BB) { + + /** + * IndexedDB preps. + */ + + BB.IndexedDB.prepare = function (db) { + if (!db.objectStoreNames.contains('settings-backbone')) { + db.createObjectStore('settings-backbone', {keyPath: 'id'}); + } + + if (!db.objectStoreNames.contains('items-backbone')) { + db.createObjectStore('items-backbone', {keyPath: 'id'}); + } + + if (!db.objectStoreNames.contains('sources-backbone')) { + db.createObjectStore('sources-backbone', {keyPath: 'id'}); + } + + if (!db.objectStoreNames.contains('folders-backbone')) { + db.createObjectStore('folders-backbone', {keyPath: 'id'}); + } + + if (!db.objectStoreNames.contains('toolbars-backbone')) { + db.createObjectStore('toolbars-backbone', {keyPath: 'region'}); + } + }; + + /** + * 1 -> 3: Main objects stores and testing + * 3 -> 4: Added toolbars-backbone store + */ + BB.IndexedDB.version = 4; + + return true; +}); \ No newline at end of file diff --git a/src/scripts/bgprocess/staticdb/defaultToolbarItems.js b/src/scripts/bgprocess/staticdb/defaultToolbarItems.js new file mode 100644 index 00000000..4f754b13 --- /dev/null +++ b/src/scripts/bgprocess/staticdb/defaultToolbarItems.js @@ -0,0 +1,19 @@ +define([], function () { + return [ + { + version: 1, + region: 'feeds', + actions: ['feeds:addSource', 'feeds:addFolder', 'feeds:updateAll', 'feeds:delete', 'feeds:scrollIntoView', '!dynamicSpace', 'feeds:toggleShowOnlyUnread'] + }, + { + version: 1, + region: 'articles', + actions: ['articles:markAllAsRead', 'articles:update', 'articles:undelete', 'articles:delete', '!dynamicSpace', 'articles:toggleShowOnlyUnread', 'articles:search'] + }, + { + version: 2, + region: 'content', + actions: ['content:mark', 'content:delete', '!dynamicSpace', 'content:changeView', 'content:showConfig'] + } + ]; +}); diff --git a/src/scripts/libs/backbone.indexDB.js b/src/scripts/libs/backbone.indexDB.js new file mode 100644 index 00000000..358cb164 --- /dev/null +++ b/src/scripts/libs/backbone.indexDB.js @@ -0,0 +1,324 @@ +/** + * Backbone indexDB Adapter + * Based on indexedDB adapter from jeromegn, but rewritten to use native Promises + * instead of jQuery Deferred objects. + * + * Original: https://github.com/jeromegn/Backbone.localStorage + */ +(function (factory) { + define(['backbone'], function (Backbone) { + return factory(Backbone); + }); +}(function (Backbone) { + // A simple module to replace `Backbone.sync` with *IndexedDB*-based + // persistence. Models are given GUIDS, and saved into a JSON object. + + // Generate four random hex digits. + function S4() { + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); + } + + // Generate a pseudo-GUID by concatenating random hexadecimal. + function guid() { + return (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4()); + } + + // Our Store is represented by a single JS object in *IndexedDB*. Create it + // with a meaningful name, like the name you'd give a table. + // window.Store is deprecated, use Backbone.IndexedDB instead + Backbone.IndexedDB = function (name) { + if (!window.indexedDB) { + throw 'Backbone.indexedDB: Environment does not support IndexedDB.'; + } + this.name = name; + this.db = null; + const request = window.indexedDB.open('backbone-indexeddb', Backbone.IndexedDB.version); + this.dbRequest = request; + const that = this; + + request.addEventListener('error', function (e) { + // user probably disallowed idb + throw 'Error code: ' + this.errorCode; + // what are the possible codes??? + }); + + request.addEventListener('success', function (e) { + that.db = this.result; + }); + + request.addEventListener('upgradeneeded', function (e) { + const db = this.result; + Backbone.IndexedDB.prepare(db); + }); + }; + + Backbone.IndexedDB.prototype = Object.assign(Backbone.IndexedDB.prototype, { + + // Add a model, giving it a (hopefully)-unique GUID, if it doesn't already + // have an id of it's own. + create: function (model) { + if (!model.id) { + model.id = guid(); + model.set(model.idAttribute, model.id); + } + + return new Promise((resolve, reject) => { + try { + const request = this.indexedDB().add(model.toJSON()); + request.onsuccess = () => resolve(model); + request.onerror = (error) => reject(error); + } catch (error) { + reject(error); + } + }); + }, + + // Update a model by replacing its copy in `this.data`. + update: function (model) { + return new Promise((resolve, reject) => { + try { + const request = this.indexedDB().put(model.toJSON()); + request.onsuccess = () => resolve(model); + request.onerror = (error) => reject(error); + } catch (error) { + reject(error); + } + }); + }, + + // Retrieve a model from `this.data` by id. + find: function (model) { + return new Promise((resolve, reject) => { + const request = this.indexedDB().get(model.id); + request.onsuccess = function () { + resolve(this.result); + }; + request.onerror = function () { + reject('IndexDB Error: Can\'t read from or write to database'); + }; + }); + }, + + // Return the array of all models currently in storage. + findAll: function () { + return new Promise((resolve, reject) => { + const items = []; + const request = this.indexedDB('readonly').openCursor(); + + request.onsuccess = function (event) { + const cursor = this.result; + if (cursor) { + items.push(cursor.value); + cursor.continue(); + } else { + resolve(items); + } + }; + + request.onerror = function () { + reject('IndexDB Error: Can\'t read from or write to database'); + }; + }); + }, + + // Delete a model from `this.data`, returning it. + destroy: function (model) { + if (model.isNew()) { + return Promise.resolve(false); + } + + return new Promise((resolve, reject) => { + try { + const request = this.indexedDB().delete(model.id); + request.onsuccess = () => resolve(model); + request.onerror = (error) => reject(error); + } catch (error) { + reject(error); + } + }); + }, + + indexedDB: (function () { + let tx; + + window.addEventListener('message', function (e) { + if (e.data.action === 'clear-tx') { + tx = null; + } + }); + + return function (type) { + if (tx && !type) { + try { + const tmpStore = tx.objectStore(this.name); + // necessary to trigger error with <1ms async calls + tmpStore.get(-1); + return tmpStore; + } catch (e) { + } + } + + const names = [...this.db.objectStoreNames].map((item) => { + return item; + }); + const tmpTx = this.db.transaction(names, type || 'readwrite'); + + const tmpStore = tmpTx.objectStore(this.name); + if (!type) { + tx = tmpTx; + // setImmidiate polyfill , doesn't work very wll tho. + window.postMessage({action: 'clear-tx'}, '*'); + } + + return tmpStore; + }; + })(), + + _clear: function () { + return new Promise((resolve, reject) => { + const req = this.indexedDB().clear(); + req.onsuccess = () => resolve(); + req.onerror = (error) => reject(error); + }); + }, + + // Size of indexedDB. + _storageSize: function () { + return new Promise((resolve, reject) => { + const req = this.indexedDB().count(); + req.onsuccess = function() { + resolve(this.result); + }; + req.onerror = function(error) { + reject(error); + }; + }); + } + + }); + + // localSync delegate to the model or collection's + // *indexedDB* property, which should be an instance of `Store`. + // window.Store.sync and Backbone.localSync is deprecated, use Backbone.IndexedDB.sync instead + Backbone.IndexedDB.sync = function (method, model, options) { + const store = model.indexedDB || model.collection.indexedDB; + options = options || {}; + + const that = this; + + // Use only native Promise + return new Promise((resolve, reject) => { + if (!store.db) { + store.dbRequest.addEventListener('success', function (e) { + store.db = this.result; + // Call sync again and pass the promise handlers + Backbone.IndexedDB.sync.call(that, method, model, options) + .then(resolve) + .catch(reject); + }); + } else { + try { + switch (method) { + case 'read': + if (model.id !== undefined) { + store.find(model) + .then(data => { + if (options.success) options.success(data); + if (options.complete) options.complete(data); + resolve(data); + }) + .catch(error => { + if (options.error) options.error(error); + if (options.complete) options.complete(null); + reject(error); + }); + } else { + store.findAll() + .then(data => { + if (options.success) options.success(data); + if (options.complete) options.complete(data); + resolve(data); + }) + .catch(error => { + if (options.error) options.error(error); + if (options.complete) options.complete(null); + reject(error); + }); + } + break; + case 'create': + store.create(model) + .then(data => { + const json = model.toJSON(); + if (options.success) options.success(json); + if (options.complete) options.complete(json); + resolve(json); + }) + .catch(error => { + if (options.error) options.error(error); + if (options.complete) options.complete(null); + reject(error); + }); + break; + case 'update': + store.update(model) + .then(data => { + const json = model.toJSON(); + if (options.success) options.success(json); + if (options.complete) options.complete(json); + resolve(json); + }) + .catch(error => { + if (options.error) options.error(error); + if (options.complete) options.complete(null); + reject(error); + }); + break; + case 'delete': + store.destroy(model) + .then(data => { + const json = model.toJSON(); + if (options.success) options.success(json); + if (options.complete) options.complete(json); + resolve(json); + }) + .catch(error => { + if (options.error) options.error(error); + if (options.complete) options.complete(null); + reject(error); + }); + break; + } + } catch (error) { + let errorMessage; + if (error.code === 22) { // && store._storageSize() === 0, what is code 22? + errorMessage = 'Private browsing is unsupported'; + } else { + errorMessage = error.message; + } + if (options.error) options.error(errorMessage); + if (options.complete) options.complete(null); + reject(errorMessage); + } + } + }); + }; + + Backbone.ajaxSync = Backbone.sync; + + Backbone.getSyncMethod = function (model) { + if (model.indexedDB || (model.collection && model.collection.indexedDB)) { + return Backbone.IndexedDB.sync; + } + + return Backbone.ajaxSync; + }; + + // Override 'Backbone.sync' to default to localSync, + // the original 'Backbone.sync' is still available in 'Backbone.ajaxSync' + Backbone.sync = function (method, model, options) { + return Backbone.getSyncMethod(model).apply(this, [method, model, options]); + }; + + return Backbone.IndexedDB; +})); diff --git a/src/scripts/libs/backbone.min.js b/src/scripts/libs/backbone.min.js new file mode 100644 index 00000000..98d2ddcd --- /dev/null +++ b/src/scripts/libs/backbone.min.js @@ -0,0 +1 @@ +!function(t){var e="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global;if("function"==typeof define&&define.amd)define(["underscore","jquery","exports"],function(i,n,s){e.Backbone=t(e,s,i,n)});else if("undefined"!=typeof exports){var i,n=require("underscore");try{i=require("jquery")}catch(t){}t(e,exports,n,i)}else e.Backbone=t(e,{},e._,e.jQuery||e.Zepto||e.ender||e.$)}(function(t,e,i,n){var s=t.Backbone,r=Array.prototype.slice;e.VERSION="1.4.0",e.$=n,e.noConflict=function(){return t.Backbone=s,this},e.emulateHTTP=!1,e.emulateJSON=!1;var o,a=e.Events={},h=/\s+/,u=function(t,e,n,s,r){var o,a=0;if(n&&"object"==typeof n){void 0!==s&&"context"in r&&void 0===r.context&&(r.context=s);for(o=i.keys(n);athis.length&&(s=this.length),s<0&&(s+=this.length+1);var r,o,a=[],h=[],u=[],l=[],c={},d=e.add,f=e.merge,p=e.remove,g=!1,v=this.comparator&&null==s&&!1!==e.sort,m=i.isString(this.comparator)?this.comparator:null;for(o=0;o7),this._useHashChange=this._wantsHashChange&&this._hasHashChange,this._wantsPushState=!!this.options.pushState,this._hasPushState=!(!this.history||!this.history.pushState),this._usePushState=this._wantsPushState&&this._hasPushState,this.fragment=this.getFragment(),this.root=("/"+this.root+"/").replace(F,"/"),this._wantsHashChange&&this._wantsPushState){if(!this._hasPushState&&!this.atRoot()){var e=this.root.slice(0,-1)||"/";return this.location.replace(e+"#"+this.getPath()),!0}this._hasPushState&&this.atRoot()&&this.navigate(this.getHash(),{replace:!0})}if(!this._hasHashChange&&this._wantsHashChange&&!this._usePushState){this.iframe=document.createElement("iframe"),this.iframe.src="javascript:0",this.iframe.style.display="none",this.iframe.tabIndex=-1;var n=document.body,s=n.insertBefore(this.iframe,n.firstChild).contentWindow;s.document.open(),s.document.close(),s.location.hash="#"+this.fragment}var r=window.addEventListener||function(t,e){return attachEvent("on"+t,e)};if(this._usePushState?r("popstate",this.checkUrl,!1):this._useHashChange&&!this.iframe?r("hashchange",this.checkUrl,!1):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,this.interval)),!this.options.silent)return this.loadUrl()},stop:function(){var t=window.removeEventListener||function(t,e){return detachEvent("on"+t,e)};this._usePushState?t("popstate",this.checkUrl,!1):this._useHashChange&&!this.iframe&&t("hashchange",this.checkUrl,!1),this.iframe&&(document.body.removeChild(this.iframe),this.iframe=null),this._checkUrlInterval&&clearInterval(this._checkUrlInterval),z.started=!1},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe&&(e=this.getHash(this.iframe.contentWindow)),e===this.fragment)return!1;this.iframe&&this.navigate(e),this.loadUrl()},loadUrl:function(t){return!!this.matchRoot()&&(t=this.fragment=this.getFragment(t),i.some(this.handlers,function(e){if(e.route.test(t))return e.callback(t),!0}))},navigate:function(t,e){if(!z.started)return!1;e&&!0!==e||(e={trigger:!!e}),t=this.getFragment(t||"");var i=this.root;""!==t&&"?"!==t.charAt(0)||(i=i.slice(0,-1)||"/");var n=i+t;t=t.replace(B,"");var s=this.decodeFragment(t);if(this.fragment!==s){if(this.fragment=s,this._usePushState)this.history[e.replace?"replaceState":"pushState"]({},document.title,n);else{if(!this._wantsHashChange)return this.location.assign(n);if(this._updateHash(this.location,t,e.replace),this.iframe&&t!==this.getHash(this.iframe.contentWindow)){var r=this.iframe.contentWindow;e.replace||(r.document.open(),r.document.close()),this._updateHash(r.location,t,e.replace)}}return e.trigger?this.loadUrl(t):void 0}},_updateHash:function(t,e,i){if(i){var n=t.href.replace(/(javascript:|#).*$/,"");t.replace(n+"#"+e)}else t.hash="#"+e}}),e.history=new z;m.extend=_.extend=M.extend=A.extend=z.extend=function(t,e){var n,s=this;return n=t&&i.has(t,"constructor")?t.constructor:function(){return s.apply(this,arguments)},i.extend(n,s,e),n.prototype=i.create(s.prototype,t),n.prototype.constructor=n,n.__super__=s.prototype,n};var J=function(){throw new Error('A "url" property or function must be specified')},L=function(t,e){var i=e.error;e.error=function(n){i&&i.call(e.context,t,n,e),t.trigger("error",t,n,e)}};return e}); diff --git a/src/scripts/libs/favicon.js b/src/scripts/libs/favicon.js new file mode 100644 index 00000000..fe2b4d1b --- /dev/null +++ b/src/scripts/libs/favicon.js @@ -0,0 +1,167 @@ +/** + * @module BgProcess + * @submodule modules/toDataURI + */ +define(function () { + async function getFavicon(source) { + return new Promise((resolve, reject) => { + async function getFaviconAddress(source) { + const baseUrl = new URL(source.get('base')); + return new Promise((resolve, reject) => { + if (settings.get('faviconSource') === 'duckduckgo') { + resolve('https://icons.duckduckgo.com/ip3/' + baseUrl.host + '.ico'); + return; + } + + if (settings.get('faviconsSource') === 'google') { + resolve('https://www.google.com/s2/favicons?domain=' + baseUrl.host); + return; + } + + let xhr = new XMLHttpRequest(); + xhr.ontimeout = () => { + reject('timeout'); + }; + xhr.onloadend = () => { + if (xhr.readyState !== XMLHttpRequest.DONE) { + reject('network error'); + return; + } + if (xhr.status !== 200) { + reject('Encountered non-200 response trying to parse ' + baseUrl.origin); + return; + } + const baseDocumentContents = xhr.responseText.replace(//gm, ''); + const baseDocument = new DOMParser().parseFromString(baseDocumentContents, 'text/html'); + const linkElements = [...baseDocument.querySelectorAll('link[rel*="icon"][href]')]; + + const links = new Set(); + links.add(baseUrl.origin + '/favicon.ico'); + + linkElements.forEach((linkElement) => { + const faviconAddress = linkElement.getAttribute('href'); + if (!faviconAddress) { + return; + } + if (faviconAddress.includes('svg')) { + return; + } + if (faviconAddress.startsWith('http')) { + return links.add(faviconAddress); + } + if (faviconAddress.startsWith('//')) { + return links.add(baseUrl.protocol + faviconAddress); + } + if (faviconAddress.startsWith('data')) { + return links.add(faviconAddress); + } + if (faviconAddress.startsWith('/')) { + return links.add(baseUrl.origin + faviconAddress); + } + + links.add(baseUrl.origin + '/' + faviconAddress); + }); + + + resolve([...links]); + }; + + xhr.open('GET', baseUrl.origin); + xhr.timeout = 1000 * 30; + xhr.send(); + }); + } + + getFaviconAddress(source) + .then((faviconAddresses) => { + const promises = faviconAddresses.map((favicon) => { + return toDataURI(favicon); + }); + Promise.any(promises) + .then((dataURI) => { + resolve(dataURI); + }).catch((errors) => { + reject(errors); + }); + }) + .catch((error) => { + reject(error); + }); + }); + } + + + // /** + // * Image specific data URI converter + // * @class toDataURI + // * @constructor + // * @extends Object + // */ + function toDataURI(favicon) { + return new Promise((resolve, reject) => { + if (favicon.startsWith('data')) { + resolve(favicon); + } + const xhr = new window.XMLHttpRequest(); + xhr.responseType = 'arraybuffer'; + xhr.onerror = () => { + reject('[modules/toDataURI] error on: ' + favicon); + }; + xhr.ontimeout = () => { + reject('timeout'); + }; + xhr.onloadend = function () { + if (xhr.readyState !== XMLHttpRequest.DONE) { + reject('[modules/toDataURI] network error on: ' + favicon); + return; + } + if (xhr.status !== 200) { + reject('[modules/toDataURI] non-200 on: ' + favicon); + return; + } + + const imageDataUri = getImageData(xhr); + if (!imageDataUri) { + reject('[modules/toDataURI] Not an image on: ' + favicon); + } + + const expiresHeader = xhr.getResponseHeader('expires'); + + let expires = 0; + if (expiresHeader) { + expires = Math.round((new Date(expiresHeader)).getTime() / 1000); + } else { + const cacheControlHeader = xhr.getResponseHeader('cache-control'); + let maxAge = 60 * 60 * 24 * 7; + if (cacheControlHeader && cacheControlHeader.includes('max-age=')) { + const newMaxAge = parseInt(/max-age=([0-9]+).*/gi.exec(cacheControlHeader)[1]); + maxAge = Math.max(newMaxAge, maxAge); + } + expires = Math.round((new Date()).getTime() / 1000) + maxAge; + } + + resolve({favicon: imageDataUri, faviconExpires: expires}); + }; + xhr.open('GET', favicon); + xhr.timeout = 1000 * 30; + xhr.send(); + }); + } + + function getImageData(xhr) { + const type = xhr.getResponseHeader('content-type'); + if (!~type.indexOf('image') || xhr.response.byteLength < 10) { + return; + } + + const array = new Uint8Array(xhr.response); + const raw = String.fromCharCode.apply(null, array); + + return 'data:' + type + ';base64,' + btoa(raw); + } + + return { + image: toDataURI, + getFavicon: getFavicon + }; +}); diff --git a/src/scripts/libs/he.js b/src/scripts/libs/he.js new file mode 100644 index 00000000..c54a824d --- /dev/null +++ b/src/scripts/libs/he.js @@ -0,0 +1 @@ +!function(){const r=/&#(?:[xX][^a-fA-F0-9]|[^0-9xX])/,e=/&(CounterClockwiseContourIntegral|DoubleLongLeftRightArrow|ClockwiseContourIntegral|NotNestedGreaterGreater|NotSquareSupersetEqual|DiacriticalDoubleAcute|NotRightTriangleEqual|NotSucceedsSlantEqual|NotPrecedesSlantEqual|CloseCurlyDoubleQuote|NegativeVeryThinSpace|DoubleContourIntegral|FilledVerySmallSquare|CapitalDifferentialD|OpenCurlyDoubleQuote|EmptyVerySmallSquare|NestedGreaterGreater|DoubleLongRightArrow|NotLeftTriangleEqual|NotGreaterSlantEqual|ReverseUpEquilibrium|DoubleLeftRightArrow|NotSquareSubsetEqual|NotDoubleVerticalBar|RightArrowLeftArrow|NotGreaterFullEqual|NotRightTriangleBar|SquareSupersetEqual|DownLeftRightVector|DoubleLongLeftArrow|leftrightsquigarrow|LeftArrowRightArrow|NegativeMediumSpace|blacktriangleright|RightDownVectorBar|PrecedesSlantEqual|RightDoubleBracket|SucceedsSlantEqual|NotLeftTriangleBar|RightTriangleEqual|SquareIntersection|RightDownTeeVector|ReverseEquilibrium|NegativeThickSpace|longleftrightarrow|Longleftrightarrow|LongLeftRightArrow|DownRightTeeVector|DownRightVectorBar|GreaterSlantEqual|SquareSubsetEqual|LeftDownVectorBar|LeftDoubleBracket|VerticalSeparator|rightleftharpoons|NotGreaterGreater|NotSquareSuperset|blacktriangleleft|blacktriangledown|NegativeThinSpace|LeftDownTeeVector|NotLessSlantEqual|leftrightharpoons|DoubleUpDownArrow|DoubleVerticalBar|LeftTriangleEqual|FilledSmallSquare|twoheadrightarrow|NotNestedLessLess|DownLeftTeeVector|DownLeftVectorBar|RightAngleBracket|NotTildeFullEqual|NotReverseElement|RightUpDownVector|DiacriticalTilde|NotSucceedsTilde|circlearrowright|NotPrecedesEqual|rightharpoondown|DoubleRightArrow|NotSucceedsEqual|NonBreakingSpace|NotRightTriangle|LessEqualGreater|RightUpTeeVector|LeftAngleBracket|GreaterFullEqual|DownArrowUpArrow|RightUpVectorBar|twoheadleftarrow|GreaterEqualLess|downharpoonright|RightTriangleBar|ntrianglerighteq|NotSupersetEqual|LeftUpDownVector|DiacriticalAcute|rightrightarrows|vartriangleright|UpArrowDownArrow|DiacriticalGrave|UnderParenthesis|EmptySmallSquare|LeftUpVectorBar|leftrightarrows|DownRightVector|downharpoonleft|trianglerighteq|ShortRightArrow|OverParenthesis|DoubleLeftArrow|DoubleDownArrow|NotSquareSubset|bigtriangledown|ntrianglelefteq|UpperRightArrow|curvearrowright|vartriangleleft|NotLeftTriangle|nleftrightarrow|LowerRightArrow|NotHumpDownHump|NotGreaterTilde|rightthreetimes|LeftUpTeeVector|NotGreaterEqual|straightepsilon|LeftTriangleBar|rightsquigarrow|ContourIntegral|rightleftarrows|CloseCurlyQuote|RightDownVector|LeftRightVector|nLeftrightarrow|leftharpoondown|circlearrowleft|SquareSuperset|OpenCurlyQuote|hookrightarrow|HorizontalLine|DiacriticalDot|NotLessGreater|ntriangleright|DoubleRightTee|InvisibleComma|InvisibleTimes|LowerLeftArrow|DownLeftVector|NotSubsetEqual|curvearrowleft|trianglelefteq|NotVerticalBar|TildeFullEqual|downdownarrows|NotGreaterLess|RightTeeVector|ZeroWidthSpace|looparrowright|LongRightArrow|doublebarwedge|ShortLeftArrow|ShortDownArrow|RightVectorBar|GreaterGreater|ReverseElement|rightharpoonup|LessSlantEqual|leftthreetimes|upharpoonright|rightarrowtail|LeftDownVector|Longrightarrow|NestedLessLess|UpperLeftArrow|nshortparallel|leftleftarrows|leftrightarrow|Leftrightarrow|LeftRightArrow|longrightarrow|upharpoonleft|RightArrowBar|ApplyFunction|LeftTeeVector|leftarrowtail|NotEqualTilde|varsubsetneqq|varsupsetneqq|RightTeeArrow|SucceedsEqual|SucceedsTilde|LeftVectorBar|SupersetEqual|hookleftarrow|DifferentialD|VerticalTilde|VeryThinSpace|blacktriangle|bigtriangleup|LessFullEqual|divideontimes|leftharpoonup|UpEquilibrium|ntriangleleft|RightTriangle|measuredangle|shortparallel|longleftarrow|Longleftarrow|LongLeftArrow|DoubleLeftTee|Poincareplane|PrecedesEqual|triangleright|DoubleUpArrow|RightUpVector|fallingdotseq|looparrowleft|PrecedesTilde|NotTildeEqual|NotTildeTilde|smallsetminus|Proportional|triangleleft|triangledown|UnderBracket|NotHumpEqual|exponentiale|ExponentialE|NotLessTilde|HilbertSpace|RightCeiling|blacklozenge|varsupsetneq|HumpDownHump|GreaterEqual|VerticalLine|LeftTeeArrow|NotLessEqual|DownTeeArrow|LeftTriangle|varsubsetneq|Intersection|NotCongruent|DownArrowBar|LeftUpVector|LeftArrowBar|risingdotseq|GreaterTilde|RoundImplies|SquareSubset|ShortUpArrow|NotSuperset|quaternions|precnapprox|backepsilon|preccurlyeq|OverBracket|blacksquare|MediumSpace|VerticalBar|circledcirc|circleddash|CircleMinus|CircleTimes|LessGreater|curlyeqprec|curlyeqsucc|diamondsuit|UpDownArrow|Updownarrow|RuleDelayed|Rrightarrow|updownarrow|RightVector|nRightarrow|nrightarrow|eqslantless|LeftCeiling|Equilibrium|SmallCircle|expectation|NotSucceeds|thickapprox|GreaterLess|SquareUnion|NotPrecedes|NotLessLess|straightphi|succnapprox|succcurlyeq|SubsetEqual|sqsupseteq|Proportion|Laplacetrf|ImaginaryI|supsetneqq|NotGreater|gtreqqless|NotElement|ThickSpace|TildeEqual|TildeTilde|Fouriertrf|rmoustache|EqualTilde|eqslantgtr|UnderBrace|LeftVector|UpArrowBar|nLeftarrow|nsubseteqq|subsetneqq|nsupseteqq|nleftarrow|succapprox|lessapprox|UpTeeArrow|upuparrows|curlywedge|lesseqqgtr|varepsilon|varnothing|RightFloor|complement|CirclePlus|sqsubseteq|Lleftarrow|circledast|RightArrow|Rightarrow|rightarrow|lmoustache|Bernoullis|precapprox|mapstoleft|mapstodown|longmapsto|dotsquare|downarrow|DoubleDot|nsubseteq|supsetneq|leftarrow|nsupseteq|subsetneq|ThinSpace|ngeqslant|subseteqq|HumpEqual|NotSubset|triangleq|NotCupCap|lesseqgtr|heartsuit|TripleDot|Leftarrow|Coproduct|Congruent|varpropto|complexes|gvertneqq|LeftArrow|LessTilde|supseteqq|MinusPlus|CircleDot|nleqslant|NotExists|gtreqless|nparallel|UnionPlus|LeftFloor|checkmark|CenterDot|centerdot|Mellintrf|gtrapprox|bigotimes|OverBrace|spadesuit|therefore|pitchfork|rationals|PlusMinus|Backslash|Therefore|DownBreve|backsimeq|backprime|DownArrow|nshortmid|Downarrow|lvertneqq|eqvparsl|imagline|imagpart|infintie|integers|Integral|intercal|LessLess|Uarrocir|intlarhk|sqsupset|angmsdaf|sqsubset|llcorner|vartheta|cupbrcap|lnapprox|Superset|SuchThat|succnsim|succneqq|angmsdag|biguplus|curlyvee|trpezium|Succeeds|NotTilde|bigwedge|angmsdah|angrtvbd|triminus|cwconint|fpartint|lrcorner|smeparsl|subseteq|urcorner|lurdshar|laemptyv|DDotrahd|approxeq|ldrushar|awconint|mapstoup|backcong|shortmid|triangle|geqslant|gesdotol|timesbar|circledR|circledS|setminus|multimap|naturals|scpolint|ncongdot|RightTee|boxminus|gnapprox|boxtimes|andslope|thicksim|angmsdaa|varsigma|cirfnint|rtriltri|angmsdab|rppolint|angmsdac|barwedge|drbkarow|clubsuit|thetasym|bsolhsub|capbrcup|dzigrarr|doteqdot|DotEqual|dotminus|UnderBar|NotEqual|realpart|otimesas|ulcorner|hksearow|hkswarow|parallel|PartialD|elinters|emptyset|plusacir|bbrktbrk|angmsdad|pointint|bigoplus|angmsdae|Precedes|bigsqcup|varkappa|notindot|supseteq|precneqq|precnsim|profalar|profline|profsurf|leqslant|lesdotor|raemptyv|subplus|notnivb|notnivc|subrarr|zigrarr|vzigzag|submult|subedot|Element|between|cirscir|larrbfs|larrsim|lotimes|lbrksld|lbrkslu|lozenge|ldrdhar|dbkarow|bigcirc|epsilon|simrarr|simplus|ltquest|Epsilon|luruhar|gtquest|maltese|npolint|eqcolon|npreceq|bigodot|ddagger|gtrless|bnequiv|harrcir|ddotseq|equivDD|backsim|demptyv|nsqsube|nsqsupe|Upsilon|nsubset|upsilon|minusdu|nsucceq|swarrow|nsupset|coloneq|searrow|boxplus|napprox|natural|asympeq|alefsym|congdot|nearrow|bigstar|diamond|supplus|tritime|LeftTee|nvinfin|triplus|NewLine|nvltrie|nvrtrie|nwarrow|nexists|Diamond|ruluhar|Implies|supmult|angzarr|suplarr|suphsub|questeq|because|digamma|Because|olcross|bemptyv|omicron|Omicron|rotimes|NoBreak|intprod|angrtvb|orderof|uwangle|suphsol|lesdoto|orslope|DownTee|realine|cudarrl|rdldhar|OverBar|supedot|lessdot|supdsub|topfork|succsim|rbrkslu|rbrksld|pertenk|cudarrr|isindot|planckh|lessgtr|pluscir|gesdoto|plussim|plustwo|lesssim|cularrp|rarrsim|Cayleys|notinva|notinvb|notinvc|UpArrow|Uparrow|uparrow|NotLess|dwangle|precsim|Product|curarrm|Cconint|dotplus|rarrbfs|ccupssm|Cedilla|cemptyv|notniva|quatint|frac35|frac38|frac45|frac56|frac58|frac78|tridot|xoplus|gacute|gammad|Gammad|lfisht|lfloor|bigcup|sqsupe|gbreve|Gbreve|lharul|sqsube|sqcups|Gcedil|apacir|llhard|lmidot|Lmidot|lmoust|andand|sqcaps|approx|Abreve|spades|circeq|tprime|divide|topcir|Assign|topbot|gesdot|divonx|xuplus|timesd|gesles|atilde|solbar|SOFTcy|loplus|timesb|lowast|lowbar|dlcorn|dlcrop|softcy|dollar|lparlt|thksim|lrhard|Atilde|lsaquo|smashp|bigvee|thinsp|wreath|bkarow|lsquor|lstrok|Lstrok|lthree|ltimes|ltlarr|DotDot|simdot|ltrPar|weierp|xsqcup|angmsd|sigmav|sigmaf|zeetrf|Zcaron|zcaron|mapsto|vsupne|thetav|cirmid|marker|mcomma|Zacute|vsubnE|there4|gtlPar|vsubne|bottom|gtrarr|SHCHcy|shchcy|midast|midcir|middot|minusb|minusd|gtrdot|bowtie|sfrown|mnplus|models|colone|seswar|Colone|mstpos|searhk|gtrsim|nacute|Nacute|boxbox|telrec|hairsp|Tcedil|nbumpe|scnsim|ncaron|Ncaron|ncedil|Ncedil|hamilt|Scedil|nearhk|hardcy|HARDcy|tcedil|Tcaron|commat|nequiv|nesear|tcaron|target|hearts|nexist|varrho|scedil|Scaron|scaron|hellip|Sacute|sacute|hercon|swnwar|compfn|rtimes|rthree|rsquor|rsaquo|zacute|wedgeq|homtht|barvee|barwed|Barwed|rpargt|horbar|conint|swarhk|roplus|nltrie|hslash|hstrok|Hstrok|rmoust|Conint|bprime|hybull|hyphen|iacute|Iacute|supsup|supsub|supsim|varphi|coprod|brvbar|agrave|Supset|supset|igrave|Igrave|notinE|Agrave|iiiint|iinfin|copysr|wedbar|Verbar|vangrt|becaus|incare|verbar|inodot|bullet|drcorn|intcal|drcrop|cularr|vellip|Utilde|bumpeq|cupcap|dstrok|Dstrok|CupCap|cupcup|cupdot|eacute|Eacute|supdot|iquest|easter|ecaron|Ecaron|ecolon|isinsv|utilde|itilde|Itilde|curarr|succeq|Bumpeq|cacute|ulcrop|nparsl|Cacute|nprcue|egrave|Egrave|nrarrc|nrarrw|subsup|subsub|nrtrie|jsercy|nsccue|Jsercy|kappav|kcedil|Kcedil|subsim|ulcorn|nsimeq|egsdot|veebar|kgreen|capand|elsdot|Subset|subset|curren|aacute|lacute|Lacute|emptyv|ntilde|Ntilde|lagran|lambda|Lambda|capcap|Ugrave|langle|subdot|emsp13|numero|emsp14|nvdash|nvDash|nVdash|nVDash|ugrave|ufisht|nvHarr|larrfs|nvlArr|larrhk|larrlp|larrpl|nvrArr|Udblac|nwarhk|larrtl|nwnear|oacute|Oacute|latail|lAtail|sstarf|lbrace|odblac|Odblac|lbrack|udblac|odsold|eparsl|lcaron|Lcaron|ograve|Ograve|lcedil|Lcedil|Aacute|ssmile|ssetmn|squarf|ldquor|capcup|ominus|cylcty|rharul|eqcirc|dagger|rfloor|rfisht|Dagger|daleth|equals|origof|capdot|equest|dcaron|Dcaron|rdquor|oslash|Oslash|otilde|Otilde|otimes|Otimes|urcrop|Ubreve|ubreve|Yacute|Uacute|uacute|Rcedil|rcedil|urcorn|parsim|Rcaron|Vdashl|rcaron|Tstrok|percnt|period|permil|Exists|yacute|rbrack|rbrace|phmmat|ccaron|Ccaron|planck|ccedil|plankv|tstrok|female|plusdo|plusdu|ffilig|plusmn|ffllig|Ccedil|rAtail|dfisht|bernou|ratail|Rarrtl|rarrtl|angsph|rarrpl|rarrlp|rarrhk|xwedge|xotime|forall|ForAll|Vvdash|vsupnE|preceq|bigcap|frac12|frac13|frac14|primes|rarrfs|prnsim|frac15|Square|frac16|square|lesdot|frac18|frac23|propto|prurel|rarrap|rangle|puncsp|frac25|Racute|qprime|racute|lesges|frac34|abreve|AElig|eqsim|utdot|setmn|urtri|Equal|Uring|seArr|uring|searr|dashv|Dashv|mumap|nabla|iogon|Iogon|sdote|sdotb|scsim|napid|napos|equiv|natur|Acirc|dblac|erarr|nbump|iprod|erDot|ucirc|awint|esdot|angrt|ncong|isinE|scnap|Scirc|scirc|ndash|isins|Ubrcy|nearr|neArr|isinv|nedot|ubrcy|acute|Ycirc|iukcy|Iukcy|xutri|nesim|caret|jcirc|Jcirc|caron|twixt|ddarr|sccue|exist|jmath|sbquo|ngeqq|angst|ccaps|lceil|ngsim|UpTee|delta|Delta|rtrif|nharr|nhArr|nhpar|rtrie|jukcy|Jukcy|kappa|rsquo|Kappa|nlarr|nlArr|TSHcy|rrarr|aogon|Aogon|fflig|xrarr|tshcy|ccirc|nleqq|filig|upsih|nless|dharl|nlsim|fjlig|ropar|nltri|dharr|robrk|roarr|fllig|fltns|roang|rnmid|subnE|subne|lAarr|trisb|Ccirc|acirc|ccups|blank|VDash|forkv|Vdash|langd|cedil|blk12|blk14|laquo|strns|diams|notin|vDash|larrb|blk34|block|disin|uplus|vdash|vBarv|aelig|starf|Wedge|check|xrArr|lates|lbarr|lBarr|notni|lbbrk|bcong|frasl|lbrke|frown|vrtri|vprop|vnsup|gamma|Gamma|wedge|xodot|bdquo|srarr|doteq|ldquo|boxdl|boxdL|gcirc|Gcirc|boxDl|boxDL|boxdr|boxdR|boxDr|TRADE|trade|rlhar|boxDR|vnsub|npart|vltri|rlarr|boxhd|boxhD|nprec|gescc|nrarr|nrArr|boxHd|boxHD|boxhu|boxhU|nrtri|boxHu|clubs|boxHU|times|colon|Colon|gimel|xlArr|Tilde|nsime|tilde|nsmid|nspar|THORN|thorn|xlarr|nsube|nsubE|thkap|xhArr|comma|nsucc|boxul|boxuL|nsupe|nsupE|gneqq|gnsim|boxUl|boxUL|grave|boxur|boxuR|boxUr|boxUR|lescc|angle|bepsi|boxvh|varpi|boxvH|numsp|Theta|gsime|gsiml|theta|boxVh|boxVH|boxvl|gtcir|gtdot|boxvL|boxVl|boxVL|crarr|cross|Cross|nvsim|boxvr|nwarr|nwArr|sqsup|dtdot|Uogon|lhard|lharu|dtrif|ocirc|Ocirc|lhblk|duarr|odash|sqsub|Hacek|sqcup|llarr|duhar|oelig|OElig|ofcir|boxvR|uogon|lltri|boxVr|csube|uuarr|ohbar|csupe|ctdot|olarr|olcir|harrw|oline|sqcap|omacr|Omacr|omega|Omega|boxVR|aleph|lneqq|lnsim|loang|loarr|rharu|lobrk|hcirc|operp|oplus|rhard|Hcirc|orarr|Union|order|ecirc|Ecirc|cuepr|szlig|cuesc|breve|reals|eDDot|Breve|hoarr|lopar|utrif|rdquo|Umacr|umacr|efDot|swArr|ultri|alpha|rceil|ovbar|swarr|Wcirc|wcirc|smtes|smile|bsemi|lrarr|aring|parsl|lrhar|bsime|uhblk|lrtri|cupor|Aring|uharr|uharl|slarr|rbrke|bsolb|lsime|rbbrk|RBarr|lsimg|phone|rBarr|rbarr|icirc|lsquo|Icirc|emacr|Emacr|ratio|simne|plusb|simlE|simgE|simeq|pluse|ltcir|ltdot|empty|xharr|xdtri|iexcl|Alpha|ltrie|rarrw|pound|ltrif|xcirc|bumpe|prcue|bumpE|asymp|amacr|cuvee|Sigma|sigma|iiint|udhar|iiota|ijlig|IJlig|supnE|imacr|Imacr|prime|Prime|image|prnap|eogon|Eogon|rarrc|mdash|mDDot|cuwed|imath|supne|imped|Amacr|udarr|prsim|micro|rarrb|cwint|raquo|infin|eplus|range|rangd|Ucirc|radic|minus|amalg|veeeq|rAarr|epsiv|ycirc|quest|sharp|quot|zwnj|Qscr|race|qscr|Qopf|qopf|qint|rang|Rang|Zscr|zscr|Zopf|zopf|rarr|rArr|Rarr|Pscr|pscr|prop|prod|prnE|prec|ZHcy|zhcy|prap|Zeta|zeta|Popf|popf|Zdot|plus|zdot|Yuml|yuml|phiv|YUcy|yucy|Yscr|yscr|perp|Yopf|yopf|part|para|YIcy|Ouml|rcub|yicy|YAcy|rdca|ouml|osol|Oscr|rdsh|yacy|real|oscr|xvee|andd|rect|andv|Xscr|oror|ordm|ordf|xscr|ange|aopf|Aopf|rHar|Xopf|opar|Oopf|xopf|xnis|rhov|oopf|omid|xmap|oint|apid|apos|ogon|ascr|Ascr|odot|odiv|xcup|xcap|ocir|oast|nvlt|nvle|nvgt|nvge|nvap|Wscr|wscr|auml|ntlg|ntgl|nsup|nsub|nsim|Nscr|nscr|nsce|Wopf|ring|npre|wopf|npar|Auml|Barv|bbrk|Nopf|nopf|nmid|nLtv|beta|ropf|Ropf|Beta|beth|nles|rpar|nleq|bnot|bNot|nldr|NJcy|rscr|Rscr|Vscr|vscr|rsqb|njcy|bopf|nisd|Bopf|rtri|Vopf|nGtv|ngtr|vopf|boxh|boxH|boxv|nges|ngeq|boxV|bscr|scap|Bscr|bsim|Vert|vert|bsol|bull|bump|caps|cdot|ncup|scnE|ncap|nbsp|napE|Cdot|cent|sdot|Vbar|nang|vBar|chcy|Mscr|mscr|sect|semi|CHcy|Mopf|mopf|sext|circ|cire|mldr|mlcp|cirE|comp|shcy|SHcy|vArr|varr|cong|copf|Copf|copy|COPY|malt|male|macr|lvnE|cscr|ltri|sime|ltcc|simg|Cscr|siml|csub|Uuml|lsqb|lsim|uuml|csup|Lscr|lscr|utri|smid|lpar|cups|smte|lozf|darr|Lopf|Uscr|solb|lopf|sopf|Sopf|lneq|uscr|spar|dArr|lnap|Darr|dash|Sqrt|LJcy|ljcy|lHar|dHar|Upsi|upsi|diam|lesg|djcy|DJcy|leqq|dopf|Dopf|dscr|Dscr|dscy|ldsh|ldca|squf|DScy|sscr|Sscr|dsol|lcub|late|star|Star|Uopf|Larr|lArr|larr|uopf|dtri|dzcy|sube|subE|Lang|lang|Kscr|kscr|Kopf|kopf|KJcy|kjcy|KHcy|khcy|DZcy|ecir|edot|eDot|Jscr|jscr|succ|Jopf|jopf|Edot|uHar|emsp|ensp|Iuml|iuml|eopf|isin|Iscr|iscr|Eopf|epar|sung|epsi|escr|sup1|sup2|sup3|Iota|iota|supe|supE|Iopf|iopf|IOcy|iocy|Escr|esim|Esim|imof|Uarr|QUOT|uArr|uarr|euml|IEcy|iecy|Idot|Euml|euro|excl|Hscr|hscr|Hopf|hopf|TScy|tscy|Tscr|hbar|tscr|flat|tbrk|fnof|hArr|harr|half|fopf|Fopf|tdot|gvnE|fork|trie|gtcc|fscr|Fscr|gdot|gsim|Gscr|gscr|Gopf|gopf|gneq|Gdot|tosa|gnap|Topf|topf|geqq|toea|GJcy|gjcy|tint|gesl|mid|Sfr|ggg|top|ges|gla|glE|glj|geq|gne|gEl|gel|gnE|Gcy|gcy|gap|Tfr|tfr|Tcy|tcy|Hat|Tau|Ffr|tau|Tab|hfr|Hfr|ffr|Fcy|fcy|icy|Icy|iff|ETH|eth|ifr|Ifr|Eta|eta|int|Int|Sup|sup|ucy|Ucy|Sum|sum|jcy|ENG|ufr|Ufr|eng|Jcy|jfr|els|ell|egs|Efr|efr|Jfr|uml|kcy|Kcy|Ecy|ecy|kfr|Kfr|lap|Sub|sub|lat|lcy|Lcy|leg|Dot|dot|lEg|leq|les|squ|div|die|lfr|Lfr|lgE|Dfr|dfr|Del|deg|Dcy|dcy|lne|lnE|sol|loz|smt|Cup|lrm|cup|lsh|Lsh|sim|shy|map|Map|mcy|Mcy|mfr|Mfr|mho|gfr|Gfr|sfr|cir|Chi|chi|nap|Cfr|vcy|Vcy|cfr|Scy|scy|ncy|Ncy|vee|Vee|Cap|cap|nfr|scE|sce|Nfr|nge|ngE|nGg|vfr|Vfr|ngt|bot|nGt|nis|niv|Rsh|rsh|nle|nlE|bne|Bfr|bfr|nLl|nlt|nLt|Bcy|bcy|not|Not|rlm|wfr|Wfr|npr|nsc|num|ocy|ast|Ocy|ofr|xfr|Xfr|Ofr|ogt|ohm|apE|olt|Rho|ape|rho|Rfr|rfr|ord|REG|ang|reg|orv|And|and|AMP|Rcy|amp|Afr|ycy|Ycy|yen|yfr|Yfr|rcy|par|pcy|Pcy|pfr|Pfr|phi|Phi|afr|Acy|acy|zcy|Zcy|piv|acE|acd|zfr|Zfr|pre|prE|psi|Psi|qfr|Qfr|zwj|Or|ge|Gg|gt|gg|el|oS|lt|Lt|LT|Re|lg|gl|eg|ne|Im|it|le|DD|wp|wr|nu|Nu|dd|lE|Sc|sc|pi|Pi|ee|af|ll|Ll|rx|gE|xi|pm|Xi|ic|pr|Pr|in|ni|mp|mu|ac|Mu|or|ap|Gt|GT|ii);|&(Aacute|Agrave|Atilde|Ccedil|Eacute|Egrave|Iacute|Igrave|Ntilde|Oacute|Ograve|Oslash|Otilde|Uacute|Ugrave|Yacute|aacute|agrave|atilde|brvbar|ccedil|curren|divide|eacute|egrave|frac12|frac14|frac34|iacute|igrave|iquest|middot|ntilde|oacute|ograve|oslash|otilde|plusmn|uacute|ugrave|yacute|AElig|Acirc|Aring|Ecirc|Icirc|Ocirc|THORN|Ucirc|acirc|acute|aelig|aring|cedil|ecirc|icirc|iexcl|laquo|micro|ocirc|pound|raquo|szlig|thorn|times|ucirc|Auml|COPY|Euml|Iuml|Ouml|QUOT|Uuml|auml|cent|copy|euml|iuml|macr|nbsp|ordf|ordm|ouml|para|quot|sect|sup1|sup2|sup3|uuml|yuml|AMP|ETH|REG|amp|deg|eth|not|reg|shy|uml|yen|GT|LT|gt|lt)(?!;)([=a-zA-Z0-9]?)|&#([0-9]+)(;?)|&#[xX]([a-fA-F0-9]+)(;?)|&([0-9a-zA-Z]+)/g,a={aacute:"á",Aacute:"Á",abreve:"ă",Abreve:"Ă",ac:"∾",acd:"∿",acE:"∾̳",acirc:"â",Acirc:"Â",acute:"´",acy:"а",Acy:"А",aelig:"æ",AElig:"Æ",af:"⁡",afr:"𝔞",Afr:"𝔄",agrave:"à",Agrave:"À",alefsym:"ℵ",aleph:"ℵ",alpha:"α",Alpha:"Α",amacr:"ā",Amacr:"Ā",amalg:"⨿",amp:"&",AMP:"&",and:"∧",And:"⩓",andand:"⩕",andd:"⩜",andslope:"⩘",andv:"⩚",ang:"∠",ange:"⦤",angle:"∠",angmsd:"∡",angmsdaa:"⦨",angmsdab:"⦩",angmsdac:"⦪",angmsdad:"⦫",angmsdae:"⦬",angmsdaf:"⦭",angmsdag:"⦮",angmsdah:"⦯",angrt:"∟",angrtvb:"⊾",angrtvbd:"⦝",angsph:"∢",angst:"Å",angzarr:"⍼",aogon:"ą",Aogon:"Ą",aopf:"𝕒",Aopf:"𝔸",ap:"≈",apacir:"⩯",ape:"≊",apE:"⩰",apid:"≋",apos:"'",ApplyFunction:"⁡",approx:"≈",approxeq:"≊",aring:"å",Aring:"Å",ascr:"𝒶",Ascr:"𝒜",Assign:"≔",ast:"*",asymp:"≈",asympeq:"≍",atilde:"ã",Atilde:"Ã",auml:"ä",Auml:"Ä",awconint:"∳",awint:"⨑",backcong:"≌",backepsilon:"϶",backprime:"‵",backsim:"∽",backsimeq:"⋍",Backslash:"∖",Barv:"⫧",barvee:"⊽",barwed:"⌅",Barwed:"⌆",barwedge:"⌅",bbrk:"⎵",bbrktbrk:"⎶",bcong:"≌",bcy:"б",Bcy:"Б",bdquo:"„",becaus:"∵",because:"∵",Because:"∵",bemptyv:"⦰",bepsi:"϶",bernou:"ℬ",Bernoullis:"ℬ",beta:"β",Beta:"Β",beth:"ℶ",between:"≬",bfr:"𝔟",Bfr:"𝔅",bigcap:"⋂",bigcirc:"◯",bigcup:"⋃",bigodot:"⨀",bigoplus:"⨁",bigotimes:"⨂",bigsqcup:"⨆",bigstar:"★",bigtriangledown:"▽",bigtriangleup:"△",biguplus:"⨄",bigvee:"⋁",bigwedge:"⋀",bkarow:"⤍",blacklozenge:"⧫",blacksquare:"▪",blacktriangle:"▴",blacktriangledown:"▾",blacktriangleleft:"◂",blacktriangleright:"▸",blank:"␣",blk12:"▒",blk14:"░",blk34:"▓",block:"█",bne:"=⃥",bnequiv:"≡⃥",bnot:"⌐",bNot:"⫭",bopf:"𝕓",Bopf:"𝔹",bot:"⊥",bottom:"⊥",bowtie:"⋈",boxbox:"⧉",boxdl:"┐",boxdL:"╕",boxDl:"╖",boxDL:"╗",boxdr:"┌",boxdR:"╒",boxDr:"╓",boxDR:"╔",boxh:"─",boxH:"═",boxhd:"┬",boxhD:"╥",boxHd:"╤",boxHD:"╦",boxhu:"┴",boxhU:"╨",boxHu:"╧",boxHU:"╩",boxminus:"⊟",boxplus:"⊞",boxtimes:"⊠",boxul:"┘",boxuL:"╛",boxUl:"╜",boxUL:"╝",boxur:"└",boxuR:"╘",boxUr:"╙",boxUR:"╚",boxv:"│",boxV:"║",boxvh:"┼",boxvH:"╪",boxVh:"╫",boxVH:"╬",boxvl:"┤",boxvL:"╡",boxVl:"╢",boxVL:"╣",boxvr:"├",boxvR:"╞",boxVr:"╟",boxVR:"╠",bprime:"‵",breve:"˘",Breve:"˘",brvbar:"¦",bscr:"𝒷",Bscr:"ℬ",bsemi:"⁏",bsim:"∽",bsime:"⋍",bsol:"\\",bsolb:"⧅",bsolhsub:"⟈",bull:"•",bullet:"•",bump:"≎",bumpe:"≏",bumpE:"⪮",bumpeq:"≏",Bumpeq:"≎",cacute:"ć",Cacute:"Ć",cap:"∩",Cap:"⋒",capand:"⩄",capbrcup:"⩉",capcap:"⩋",capcup:"⩇",capdot:"⩀",CapitalDifferentialD:"ⅅ",caps:"∩︀",caret:"⁁",caron:"ˇ",Cayleys:"ℭ",ccaps:"⩍",ccaron:"č",Ccaron:"Č",ccedil:"ç",Ccedil:"Ç",ccirc:"ĉ",Ccirc:"Ĉ",Cconint:"∰",ccups:"⩌",ccupssm:"⩐",cdot:"ċ",Cdot:"Ċ",cedil:"¸",Cedilla:"¸",cemptyv:"⦲",cent:"¢",centerdot:"·",CenterDot:"·",cfr:"𝔠",Cfr:"ℭ",chcy:"ч",CHcy:"Ч",check:"✓",checkmark:"✓",chi:"χ",Chi:"Χ",cir:"○",circ:"ˆ",circeq:"≗",circlearrowleft:"↺",circlearrowright:"↻",circledast:"⊛",circledcirc:"⊚",circleddash:"⊝",CircleDot:"⊙",circledR:"®",circledS:"Ⓢ",CircleMinus:"⊖",CirclePlus:"⊕",CircleTimes:"⊗",cire:"≗",cirE:"⧃",cirfnint:"⨐",cirmid:"⫯",cirscir:"⧂",ClockwiseContourIntegral:"∲",CloseCurlyDoubleQuote:"”",CloseCurlyQuote:"’",clubs:"♣",clubsuit:"♣",colon:":",Colon:"∷",colone:"≔",Colone:"⩴",coloneq:"≔",comma:",",commat:"@",comp:"∁",compfn:"∘",complement:"∁",complexes:"ℂ",cong:"≅",congdot:"⩭",Congruent:"≡",conint:"∮",Conint:"∯",ContourIntegral:"∮",copf:"𝕔",Copf:"ℂ",coprod:"∐",Coproduct:"∐",copy:"©",COPY:"©",copysr:"℗",CounterClockwiseContourIntegral:"∳",crarr:"↵",cross:"✗",Cross:"⨯",cscr:"𝒸",Cscr:"𝒞",csub:"⫏",csube:"⫑",csup:"⫐",csupe:"⫒",ctdot:"⋯",cudarrl:"⤸",cudarrr:"⤵",cuepr:"⋞",cuesc:"⋟",cularr:"↶",cularrp:"⤽",cup:"∪",Cup:"⋓",cupbrcap:"⩈",cupcap:"⩆",CupCap:"≍",cupcup:"⩊",cupdot:"⊍",cupor:"⩅",cups:"∪︀",curarr:"↷",curarrm:"⤼",curlyeqprec:"⋞",curlyeqsucc:"⋟",curlyvee:"⋎",curlywedge:"⋏",curren:"¤",curvearrowleft:"↶",curvearrowright:"↷",cuvee:"⋎",cuwed:"⋏",cwconint:"∲",cwint:"∱",cylcty:"⌭",dagger:"†",Dagger:"‡",daleth:"ℸ",darr:"↓",dArr:"⇓",Darr:"↡",dash:"‐",dashv:"⊣",Dashv:"⫤",dbkarow:"⤏",dblac:"˝",dcaron:"ď",Dcaron:"Ď",dcy:"д",Dcy:"Д",dd:"ⅆ",DD:"ⅅ",ddagger:"‡",ddarr:"⇊",DDotrahd:"⤑",ddotseq:"⩷",deg:"°",Del:"∇",delta:"δ",Delta:"Δ",demptyv:"⦱",dfisht:"⥿",dfr:"𝔡",Dfr:"𝔇",dHar:"⥥",dharl:"⇃",dharr:"⇂",DiacriticalAcute:"´",DiacriticalDot:"˙",DiacriticalDoubleAcute:"˝",DiacriticalGrave:"`",DiacriticalTilde:"˜",diam:"⋄",diamond:"⋄",Diamond:"⋄",diamondsuit:"♦",diams:"♦",die:"¨",DifferentialD:"ⅆ",digamma:"ϝ",disin:"⋲",div:"÷",divide:"÷",divideontimes:"⋇",divonx:"⋇",djcy:"ђ",DJcy:"Ђ",dlcorn:"⌞",dlcrop:"⌍",dollar:"$",dopf:"𝕕",Dopf:"𝔻",dot:"˙",Dot:"¨",DotDot:"⃜",doteq:"≐",doteqdot:"≑",DotEqual:"≐",dotminus:"∸",dotplus:"∔",dotsquare:"⊡",doublebarwedge:"⌆",DoubleContourIntegral:"∯",DoubleDot:"¨",DoubleDownArrow:"⇓",DoubleLeftArrow:"⇐",DoubleLeftRightArrow:"⇔",DoubleLeftTee:"⫤",DoubleLongLeftArrow:"⟸",DoubleLongLeftRightArrow:"⟺",DoubleLongRightArrow:"⟹",DoubleRightArrow:"⇒",DoubleRightTee:"⊨",DoubleUpArrow:"⇑",DoubleUpDownArrow:"⇕",DoubleVerticalBar:"∥",downarrow:"↓",Downarrow:"⇓",DownArrow:"↓",DownArrowBar:"⤓",DownArrowUpArrow:"⇵",DownBreve:"̑",downdownarrows:"⇊",downharpoonleft:"⇃",downharpoonright:"⇂",DownLeftRightVector:"⥐",DownLeftTeeVector:"⥞",DownLeftVector:"↽",DownLeftVectorBar:"⥖",DownRightTeeVector:"⥟",DownRightVector:"⇁",DownRightVectorBar:"⥗",DownTee:"⊤",DownTeeArrow:"↧",drbkarow:"⤐",drcorn:"⌟",drcrop:"⌌",dscr:"𝒹",Dscr:"𝒟",dscy:"ѕ",DScy:"Ѕ",dsol:"⧶",dstrok:"đ",Dstrok:"Đ",dtdot:"⋱",dtri:"▿",dtrif:"▾",duarr:"⇵",duhar:"⥯",dwangle:"⦦",dzcy:"џ",DZcy:"Џ",dzigrarr:"⟿",eacute:"é",Eacute:"É",easter:"⩮",ecaron:"ě",Ecaron:"Ě",ecir:"≖",ecirc:"ê",Ecirc:"Ê",ecolon:"≕",ecy:"э",Ecy:"Э",eDDot:"⩷",edot:"ė",eDot:"≑",Edot:"Ė",ee:"ⅇ",efDot:"≒",efr:"𝔢",Efr:"𝔈",eg:"⪚",egrave:"è",Egrave:"È",egs:"⪖",egsdot:"⪘",el:"⪙",Element:"∈",elinters:"⏧",ell:"ℓ",els:"⪕",elsdot:"⪗",emacr:"ē",Emacr:"Ē",empty:"∅",emptyset:"∅",EmptySmallSquare:"◻",emptyv:"∅",EmptyVerySmallSquare:"▫",emsp:" ",emsp13:" ",emsp14:" ",eng:"ŋ",ENG:"Ŋ",ensp:" ",eogon:"ę",Eogon:"Ę",eopf:"𝕖",Eopf:"𝔼",epar:"⋕",eparsl:"⧣",eplus:"⩱",epsi:"ε",epsilon:"ε",Epsilon:"Ε",epsiv:"ϵ",eqcirc:"≖",eqcolon:"≕",eqsim:"≂",eqslantgtr:"⪖",eqslantless:"⪕",Equal:"⩵",equals:"=",EqualTilde:"≂",equest:"≟",Equilibrium:"⇌",equiv:"≡",equivDD:"⩸",eqvparsl:"⧥",erarr:"⥱",erDot:"≓",escr:"ℯ",Escr:"ℰ",esdot:"≐",esim:"≂",Esim:"⩳",eta:"η",Eta:"Η",eth:"ð",ETH:"Ð",euml:"ë",Euml:"Ë",euro:"€",excl:"!",exist:"∃",Exists:"∃",expectation:"ℰ",exponentiale:"ⅇ",ExponentialE:"ⅇ",fallingdotseq:"≒",fcy:"ф",Fcy:"Ф",female:"♀",ffilig:"ffi",fflig:"ff",ffllig:"ffl",ffr:"𝔣",Ffr:"𝔉",filig:"fi",FilledSmallSquare:"◼",FilledVerySmallSquare:"▪",fjlig:"fj",flat:"♭",fllig:"fl",fltns:"▱",fnof:"ƒ",fopf:"𝕗",Fopf:"𝔽",forall:"∀",ForAll:"∀",fork:"⋔",forkv:"⫙",Fouriertrf:"ℱ",fpartint:"⨍",frac12:"½",frac13:"⅓",frac14:"¼",frac15:"⅕",frac16:"⅙",frac18:"⅛",frac23:"⅔",frac25:"⅖",frac34:"¾",frac35:"⅗",frac38:"⅜",frac45:"⅘",frac56:"⅚",frac58:"⅝",frac78:"⅞",frasl:"⁄",frown:"⌢",fscr:"𝒻",Fscr:"ℱ",gacute:"ǵ",gamma:"γ",Gamma:"Γ",gammad:"ϝ",Gammad:"Ϝ",gap:"⪆",gbreve:"ğ",Gbreve:"Ğ",Gcedil:"Ģ",gcirc:"ĝ",Gcirc:"Ĝ",gcy:"г",Gcy:"Г",gdot:"ġ",Gdot:"Ġ",ge:"≥",gE:"≧",gel:"⋛",gEl:"⪌",geq:"≥",geqq:"≧",geqslant:"⩾",ges:"⩾",gescc:"⪩",gesdot:"⪀",gesdoto:"⪂",gesdotol:"⪄",gesl:"⋛︀",gesles:"⪔",gfr:"𝔤",Gfr:"𝔊",gg:"≫",Gg:"⋙",ggg:"⋙",gimel:"ℷ",gjcy:"ѓ",GJcy:"Ѓ",gl:"≷",gla:"⪥",glE:"⪒",glj:"⪤",gnap:"⪊",gnapprox:"⪊",gne:"⪈",gnE:"≩",gneq:"⪈",gneqq:"≩",gnsim:"⋧",gopf:"𝕘",Gopf:"𝔾",grave:"`",GreaterEqual:"≥",GreaterEqualLess:"⋛",GreaterFullEqual:"≧",GreaterGreater:"⪢",GreaterLess:"≷",GreaterSlantEqual:"⩾",GreaterTilde:"≳",gscr:"ℊ",Gscr:"𝒢",gsim:"≳",gsime:"⪎",gsiml:"⪐",gt:">",Gt:"≫",GT:">",gtcc:"⪧",gtcir:"⩺",gtdot:"⋗",gtlPar:"⦕",gtquest:"⩼",gtrapprox:"⪆",gtrarr:"⥸",gtrdot:"⋗",gtreqless:"⋛",gtreqqless:"⪌",gtrless:"≷",gtrsim:"≳",gvertneqq:"≩︀",gvnE:"≩︀",Hacek:"ˇ",hairsp:" ",half:"½",hamilt:"ℋ",hardcy:"ъ",HARDcy:"Ъ",harr:"↔",hArr:"⇔",harrcir:"⥈",harrw:"↭",Hat:"^",hbar:"ℏ",hcirc:"ĥ",Hcirc:"Ĥ",hearts:"♥",heartsuit:"♥",hellip:"…",hercon:"⊹",hfr:"𝔥",Hfr:"ℌ",HilbertSpace:"ℋ",hksearow:"⤥",hkswarow:"⤦",hoarr:"⇿",homtht:"∻",hookleftarrow:"↩",hookrightarrow:"↪",hopf:"𝕙",Hopf:"ℍ",horbar:"―",HorizontalLine:"─",hscr:"𝒽",Hscr:"ℋ",hslash:"ℏ",hstrok:"ħ",Hstrok:"Ħ",HumpDownHump:"≎",HumpEqual:"≏",hybull:"⁃",hyphen:"‐",iacute:"í",Iacute:"Í",ic:"⁣",icirc:"î",Icirc:"Î",icy:"и",Icy:"И",Idot:"İ",iecy:"е",IEcy:"Е",iexcl:"¡",iff:"⇔",ifr:"𝔦",Ifr:"ℑ",igrave:"ì",Igrave:"Ì",ii:"ⅈ",iiiint:"⨌",iiint:"∭",iinfin:"⧜",iiota:"℩",ijlig:"ij",IJlig:"IJ",Im:"ℑ",imacr:"ī",Imacr:"Ī",image:"ℑ",ImaginaryI:"ⅈ",imagline:"ℐ",imagpart:"ℑ",imath:"ı",imof:"⊷",imped:"Ƶ",Implies:"⇒",in:"∈",incare:"℅",infin:"∞",infintie:"⧝",inodot:"ı",int:"∫",Int:"∬",intcal:"⊺",integers:"ℤ",Integral:"∫",intercal:"⊺",Intersection:"⋂",intlarhk:"⨗",intprod:"⨼",InvisibleComma:"⁣",InvisibleTimes:"⁢",iocy:"ё",IOcy:"Ё",iogon:"į",Iogon:"Į",iopf:"𝕚",Iopf:"𝕀",iota:"ι",Iota:"Ι",iprod:"⨼",iquest:"¿",iscr:"𝒾",Iscr:"ℐ",isin:"∈",isindot:"⋵",isinE:"⋹",isins:"⋴",isinsv:"⋳",isinv:"∈",it:"⁢",itilde:"ĩ",Itilde:"Ĩ",iukcy:"і",Iukcy:"І",iuml:"ï",Iuml:"Ï",jcirc:"ĵ",Jcirc:"Ĵ",jcy:"й",Jcy:"Й",jfr:"𝔧",Jfr:"𝔍",jmath:"ȷ",jopf:"𝕛",Jopf:"𝕁",jscr:"𝒿",Jscr:"𝒥",jsercy:"ј",Jsercy:"Ј",jukcy:"є",Jukcy:"Є",kappa:"κ",Kappa:"Κ",kappav:"ϰ",kcedil:"ķ",Kcedil:"Ķ",kcy:"к",Kcy:"К",kfr:"𝔨",Kfr:"𝔎",kgreen:"ĸ",khcy:"х",KHcy:"Х",kjcy:"ќ",KJcy:"Ќ",kopf:"𝕜",Kopf:"𝕂",kscr:"𝓀",Kscr:"𝒦",lAarr:"⇚",lacute:"ĺ",Lacute:"Ĺ",laemptyv:"⦴",lagran:"ℒ",lambda:"λ",Lambda:"Λ",lang:"⟨",Lang:"⟪",langd:"⦑",langle:"⟨",lap:"⪅",Laplacetrf:"ℒ",laquo:"«",larr:"←",lArr:"⇐",Larr:"↞",larrb:"⇤",larrbfs:"⤟",larrfs:"⤝",larrhk:"↩",larrlp:"↫",larrpl:"⤹",larrsim:"⥳",larrtl:"↢",lat:"⪫",latail:"⤙",lAtail:"⤛",late:"⪭",lates:"⪭︀",lbarr:"⤌",lBarr:"⤎",lbbrk:"❲",lbrace:"{",lbrack:"[",lbrke:"⦋",lbrksld:"⦏",lbrkslu:"⦍",lcaron:"ľ",Lcaron:"Ľ",lcedil:"ļ",Lcedil:"Ļ",lceil:"⌈",lcub:"{",lcy:"л",Lcy:"Л",ldca:"⤶",ldquo:"“",ldquor:"„",ldrdhar:"⥧",ldrushar:"⥋",ldsh:"↲",le:"≤",lE:"≦",LeftAngleBracket:"⟨",leftarrow:"←",Leftarrow:"⇐",LeftArrow:"←",LeftArrowBar:"⇤",LeftArrowRightArrow:"⇆",leftarrowtail:"↢",LeftCeiling:"⌈",LeftDoubleBracket:"⟦",LeftDownTeeVector:"⥡",LeftDownVector:"⇃",LeftDownVectorBar:"⥙",LeftFloor:"⌊",leftharpoondown:"↽",leftharpoonup:"↼",leftleftarrows:"⇇",leftrightarrow:"↔",Leftrightarrow:"⇔",LeftRightArrow:"↔",leftrightarrows:"⇆",leftrightharpoons:"⇋",leftrightsquigarrow:"↭",LeftRightVector:"⥎",LeftTee:"⊣",LeftTeeArrow:"↤",LeftTeeVector:"⥚",leftthreetimes:"⋋",LeftTriangle:"⊲",LeftTriangleBar:"⧏",LeftTriangleEqual:"⊴",LeftUpDownVector:"⥑",LeftUpTeeVector:"⥠",LeftUpVector:"↿",LeftUpVectorBar:"⥘",LeftVector:"↼",LeftVectorBar:"⥒",leg:"⋚",lEg:"⪋",leq:"≤",leqq:"≦",leqslant:"⩽",les:"⩽",lescc:"⪨",lesdot:"⩿",lesdoto:"⪁",lesdotor:"⪃",lesg:"⋚︀",lesges:"⪓",lessapprox:"⪅",lessdot:"⋖",lesseqgtr:"⋚",lesseqqgtr:"⪋",LessEqualGreater:"⋚",LessFullEqual:"≦",LessGreater:"≶",lessgtr:"≶",LessLess:"⪡",lesssim:"≲",LessSlantEqual:"⩽",LessTilde:"≲",lfisht:"⥼",lfloor:"⌊",lfr:"𝔩",Lfr:"𝔏",lg:"≶",lgE:"⪑",lHar:"⥢",lhard:"↽",lharu:"↼",lharul:"⥪",lhblk:"▄",ljcy:"љ",LJcy:"Љ",ll:"≪",Ll:"⋘",llarr:"⇇",llcorner:"⌞",Lleftarrow:"⇚",llhard:"⥫",lltri:"◺",lmidot:"ŀ",Lmidot:"Ŀ",lmoust:"⎰",lmoustache:"⎰",lnap:"⪉",lnapprox:"⪉",lne:"⪇",lnE:"≨",lneq:"⪇",lneqq:"≨",lnsim:"⋦",loang:"⟬",loarr:"⇽",lobrk:"⟦",longleftarrow:"⟵",Longleftarrow:"⟸",LongLeftArrow:"⟵",longleftrightarrow:"⟷",Longleftrightarrow:"⟺",LongLeftRightArrow:"⟷",longmapsto:"⟼",longrightarrow:"⟶",Longrightarrow:"⟹",LongRightArrow:"⟶",looparrowleft:"↫",looparrowright:"↬",lopar:"⦅",lopf:"𝕝",Lopf:"𝕃",loplus:"⨭",lotimes:"⨴",lowast:"∗",lowbar:"_",LowerLeftArrow:"↙",LowerRightArrow:"↘",loz:"◊",lozenge:"◊",lozf:"⧫",lpar:"(",lparlt:"⦓",lrarr:"⇆",lrcorner:"⌟",lrhar:"⇋",lrhard:"⥭",lrm:"‎",lrtri:"⊿",lsaquo:"‹",lscr:"𝓁",Lscr:"ℒ",lsh:"↰",Lsh:"↰",lsim:"≲",lsime:"⪍",lsimg:"⪏",lsqb:"[",lsquo:"‘",lsquor:"‚",lstrok:"ł",Lstrok:"Ł",lt:"<",Lt:"≪",LT:"<",ltcc:"⪦",ltcir:"⩹",ltdot:"⋖",lthree:"⋋",ltimes:"⋉",ltlarr:"⥶",ltquest:"⩻",ltri:"◃",ltrie:"⊴",ltrif:"◂",ltrPar:"⦖",lurdshar:"⥊",luruhar:"⥦",lvertneqq:"≨︀",lvnE:"≨︀",macr:"¯",male:"♂",malt:"✠",maltese:"✠",map:"↦",Map:"⤅",mapsto:"↦",mapstodown:"↧",mapstoleft:"↤",mapstoup:"↥",marker:"▮",mcomma:"⨩",mcy:"м",Mcy:"М",mdash:"—",mDDot:"∺",measuredangle:"∡",MediumSpace:" ",Mellintrf:"ℳ",mfr:"𝔪",Mfr:"𝔐",mho:"℧",micro:"µ",mid:"∣",midast:"*",midcir:"⫰",middot:"·",minus:"−",minusb:"⊟",minusd:"∸",minusdu:"⨪",MinusPlus:"∓",mlcp:"⫛",mldr:"…",mnplus:"∓",models:"⊧",mopf:"𝕞",Mopf:"𝕄",mp:"∓",mscr:"𝓂",Mscr:"ℳ",mstpos:"∾",mu:"μ",Mu:"Μ",multimap:"⊸",mumap:"⊸",nabla:"∇",nacute:"ń",Nacute:"Ń",nang:"∠⃒",nap:"≉",napE:"⩰̸",napid:"≋̸",napos:"ʼn",napprox:"≉",natur:"♮",natural:"♮",naturals:"ℕ",nbsp:" ",nbump:"≎̸",nbumpe:"≏̸",ncap:"⩃",ncaron:"ň",Ncaron:"Ň",ncedil:"ņ",Ncedil:"Ņ",ncong:"≇",ncongdot:"⩭̸",ncup:"⩂",ncy:"н",Ncy:"Н",ndash:"–",ne:"≠",nearhk:"⤤",nearr:"↗",neArr:"⇗",nearrow:"↗",nedot:"≐̸",NegativeMediumSpace:"​",NegativeThickSpace:"​",NegativeThinSpace:"​",NegativeVeryThinSpace:"​",nequiv:"≢",nesear:"⤨",nesim:"≂̸",NestedGreaterGreater:"≫",NestedLessLess:"≪",NewLine:"\n",nexist:"∄",nexists:"∄",nfr:"𝔫",Nfr:"𝔑",nge:"≱",ngE:"≧̸",ngeq:"≱",ngeqq:"≧̸",ngeqslant:"⩾̸",nges:"⩾̸",nGg:"⋙̸",ngsim:"≵",ngt:"≯",nGt:"≫⃒",ngtr:"≯",nGtv:"≫̸",nharr:"↮",nhArr:"⇎",nhpar:"⫲",ni:"∋",nis:"⋼",nisd:"⋺",niv:"∋",njcy:"њ",NJcy:"Њ",nlarr:"↚",nlArr:"⇍",nldr:"‥",nle:"≰",nlE:"≦̸",nleftarrow:"↚",nLeftarrow:"⇍",nleftrightarrow:"↮",nLeftrightarrow:"⇎",nleq:"≰",nleqq:"≦̸",nleqslant:"⩽̸",nles:"⩽̸",nless:"≮",nLl:"⋘̸",nlsim:"≴",nlt:"≮",nLt:"≪⃒",nltri:"⋪",nltrie:"⋬",nLtv:"≪̸",nmid:"∤",NoBreak:"⁠",NonBreakingSpace:" ",nopf:"𝕟",Nopf:"ℕ",not:"¬",Not:"⫬",NotCongruent:"≢",NotCupCap:"≭",NotDoubleVerticalBar:"∦",NotElement:"∉",NotEqual:"≠",NotEqualTilde:"≂̸",NotExists:"∄",NotGreater:"≯",NotGreaterEqual:"≱",NotGreaterFullEqual:"≧̸",NotGreaterGreater:"≫̸",NotGreaterLess:"≹",NotGreaterSlantEqual:"⩾̸",NotGreaterTilde:"≵",NotHumpDownHump:"≎̸",NotHumpEqual:"≏̸",notin:"∉",notindot:"⋵̸",notinE:"⋹̸",notinva:"∉",notinvb:"⋷",notinvc:"⋶",NotLeftTriangle:"⋪",NotLeftTriangleBar:"⧏̸",NotLeftTriangleEqual:"⋬",NotLess:"≮",NotLessEqual:"≰",NotLessGreater:"≸",NotLessLess:"≪̸",NotLessSlantEqual:"⩽̸",NotLessTilde:"≴",NotNestedGreaterGreater:"⪢̸",NotNestedLessLess:"⪡̸",notni:"∌",notniva:"∌",notnivb:"⋾",notnivc:"⋽",NotPrecedes:"⊀",NotPrecedesEqual:"⪯̸",NotPrecedesSlantEqual:"⋠",NotReverseElement:"∌",NotRightTriangle:"⋫",NotRightTriangleBar:"⧐̸",NotRightTriangleEqual:"⋭",NotSquareSubset:"⊏̸",NotSquareSubsetEqual:"⋢",NotSquareSuperset:"⊐̸",NotSquareSupersetEqual:"⋣",NotSubset:"⊂⃒",NotSubsetEqual:"⊈",NotSucceeds:"⊁",NotSucceedsEqual:"⪰̸",NotSucceedsSlantEqual:"⋡",NotSucceedsTilde:"≿̸",NotSuperset:"⊃⃒",NotSupersetEqual:"⊉",NotTilde:"≁",NotTildeEqual:"≄",NotTildeFullEqual:"≇",NotTildeTilde:"≉",NotVerticalBar:"∤",npar:"∦",nparallel:"∦",nparsl:"⫽⃥",npart:"∂̸",npolint:"⨔",npr:"⊀",nprcue:"⋠",npre:"⪯̸",nprec:"⊀",npreceq:"⪯̸",nrarr:"↛",nrArr:"⇏",nrarrc:"⤳̸",nrarrw:"↝̸",nrightarrow:"↛",nRightarrow:"⇏",nrtri:"⋫",nrtrie:"⋭",nsc:"⊁",nsccue:"⋡",nsce:"⪰̸",nscr:"𝓃",Nscr:"𝒩",nshortmid:"∤",nshortparallel:"∦",nsim:"≁",nsime:"≄",nsimeq:"≄",nsmid:"∤",nspar:"∦",nsqsube:"⋢",nsqsupe:"⋣",nsub:"⊄",nsube:"⊈",nsubE:"⫅̸",nsubset:"⊂⃒",nsubseteq:"⊈",nsubseteqq:"⫅̸",nsucc:"⊁",nsucceq:"⪰̸",nsup:"⊅",nsupe:"⊉",nsupE:"⫆̸",nsupset:"⊃⃒",nsupseteq:"⊉",nsupseteqq:"⫆̸",ntgl:"≹",ntilde:"ñ",Ntilde:"Ñ",ntlg:"≸",ntriangleleft:"⋪",ntrianglelefteq:"⋬",ntriangleright:"⋫",ntrianglerighteq:"⋭",nu:"ν",Nu:"Ν",num:"#",numero:"№",numsp:" ",nvap:"≍⃒",nvdash:"⊬",nvDash:"⊭",nVdash:"⊮",nVDash:"⊯",nvge:"≥⃒",nvgt:">⃒",nvHarr:"⤄",nvinfin:"⧞",nvlArr:"⤂",nvle:"≤⃒",nvlt:"<⃒",nvltrie:"⊴⃒",nvrArr:"⤃",nvrtrie:"⊵⃒",nvsim:"∼⃒",nwarhk:"⤣",nwarr:"↖",nwArr:"⇖",nwarrow:"↖",nwnear:"⤧",oacute:"ó",Oacute:"Ó",oast:"⊛",ocir:"⊚",ocirc:"ô",Ocirc:"Ô",ocy:"о",Ocy:"О",odash:"⊝",odblac:"ő",Odblac:"Ő",odiv:"⨸",odot:"⊙",odsold:"⦼",oelig:"œ",OElig:"Œ",ofcir:"⦿",ofr:"𝔬",Ofr:"𝔒",ogon:"˛",ograve:"ò",Ograve:"Ò",ogt:"⧁",ohbar:"⦵",ohm:"Ω",oint:"∮",olarr:"↺",olcir:"⦾",olcross:"⦻",oline:"‾",olt:"⧀",omacr:"ō",Omacr:"Ō",omega:"ω",Omega:"Ω",omicron:"ο",Omicron:"Ο",omid:"⦶",ominus:"⊖",oopf:"𝕠",Oopf:"𝕆",opar:"⦷",OpenCurlyDoubleQuote:"“",OpenCurlyQuote:"‘",operp:"⦹",oplus:"⊕",or:"∨",Or:"⩔",orarr:"↻",ord:"⩝",order:"ℴ",orderof:"ℴ",ordf:"ª",ordm:"º",origof:"⊶",oror:"⩖",orslope:"⩗",orv:"⩛",oS:"Ⓢ",oscr:"ℴ",Oscr:"𝒪",oslash:"ø",Oslash:"Ø",osol:"⊘",otilde:"õ",Otilde:"Õ",otimes:"⊗",Otimes:"⨷",otimesas:"⨶",ouml:"ö",Ouml:"Ö",ovbar:"⌽",OverBar:"‾",OverBrace:"⏞",OverBracket:"⎴",OverParenthesis:"⏜",par:"∥",para:"¶",parallel:"∥",parsim:"⫳",parsl:"⫽",part:"∂",PartialD:"∂",pcy:"п",Pcy:"П",percnt:"%",period:".",permil:"‰",perp:"⊥",pertenk:"‱",pfr:"𝔭",Pfr:"𝔓",phi:"φ",Phi:"Φ",phiv:"ϕ",phmmat:"ℳ",phone:"☎",pi:"π",Pi:"Π",pitchfork:"⋔",piv:"ϖ",planck:"ℏ",planckh:"ℎ",plankv:"ℏ",plus:"+",plusacir:"⨣",plusb:"⊞",pluscir:"⨢",plusdo:"∔",plusdu:"⨥",pluse:"⩲",PlusMinus:"±",plusmn:"±",plussim:"⨦",plustwo:"⨧",pm:"±",Poincareplane:"ℌ",pointint:"⨕",popf:"𝕡",Popf:"ℙ",pound:"£",pr:"≺",Pr:"⪻",prap:"⪷",prcue:"≼",pre:"⪯",prE:"⪳",prec:"≺",precapprox:"⪷",preccurlyeq:"≼",Precedes:"≺",PrecedesEqual:"⪯",PrecedesSlantEqual:"≼",PrecedesTilde:"≾",preceq:"⪯",precnapprox:"⪹",precneqq:"⪵",precnsim:"⋨",precsim:"≾",prime:"′",Prime:"″",primes:"ℙ",prnap:"⪹",prnE:"⪵",prnsim:"⋨",prod:"∏",Product:"∏",profalar:"⌮",profline:"⌒",profsurf:"⌓",prop:"∝",Proportion:"∷",Proportional:"∝",propto:"∝",prsim:"≾",prurel:"⊰",pscr:"𝓅",Pscr:"𝒫",psi:"ψ",Psi:"Ψ",puncsp:" ",qfr:"𝔮",Qfr:"𝔔",qint:"⨌",qopf:"𝕢",Qopf:"ℚ",qprime:"⁗",qscr:"𝓆",Qscr:"𝒬",quaternions:"ℍ",quatint:"⨖",quest:"?",questeq:"≟",quot:'"',QUOT:'"',rAarr:"⇛",race:"∽̱",racute:"ŕ",Racute:"Ŕ",radic:"√",raemptyv:"⦳",rang:"⟩",Rang:"⟫",rangd:"⦒",range:"⦥",rangle:"⟩",raquo:"»",rarr:"→",rArr:"⇒",Rarr:"↠",rarrap:"⥵",rarrb:"⇥",rarrbfs:"⤠",rarrc:"⤳",rarrfs:"⤞",rarrhk:"↪",rarrlp:"↬",rarrpl:"⥅",rarrsim:"⥴",rarrtl:"↣",Rarrtl:"⤖",rarrw:"↝",ratail:"⤚",rAtail:"⤜",ratio:"∶",rationals:"ℚ",rbarr:"⤍",rBarr:"⤏",RBarr:"⤐",rbbrk:"❳",rbrace:"}",rbrack:"]",rbrke:"⦌",rbrksld:"⦎",rbrkslu:"⦐",rcaron:"ř",Rcaron:"Ř",rcedil:"ŗ",Rcedil:"Ŗ",rceil:"⌉",rcub:"}",rcy:"р",Rcy:"Р",rdca:"⤷",rdldhar:"⥩",rdquo:"”",rdquor:"”",rdsh:"↳",Re:"ℜ",real:"ℜ",realine:"ℛ",realpart:"ℜ",reals:"ℝ",rect:"▭",reg:"®",REG:"®",ReverseElement:"∋",ReverseEquilibrium:"⇋",ReverseUpEquilibrium:"⥯",rfisht:"⥽",rfloor:"⌋",rfr:"𝔯",Rfr:"ℜ",rHar:"⥤",rhard:"⇁",rharu:"⇀",rharul:"⥬",rho:"ρ",Rho:"Ρ",rhov:"ϱ",RightAngleBracket:"⟩",rightarrow:"→",Rightarrow:"⇒",RightArrow:"→",RightArrowBar:"⇥",RightArrowLeftArrow:"⇄",rightarrowtail:"↣",RightCeiling:"⌉",RightDoubleBracket:"⟧",RightDownTeeVector:"⥝",RightDownVector:"⇂",RightDownVectorBar:"⥕",RightFloor:"⌋",rightharpoondown:"⇁",rightharpoonup:"⇀",rightleftarrows:"⇄",rightleftharpoons:"⇌",rightrightarrows:"⇉",rightsquigarrow:"↝",RightTee:"⊢",RightTeeArrow:"↦",RightTeeVector:"⥛",rightthreetimes:"⋌",RightTriangle:"⊳",RightTriangleBar:"⧐",RightTriangleEqual:"⊵",RightUpDownVector:"⥏",RightUpTeeVector:"⥜",RightUpVector:"↾",RightUpVectorBar:"⥔",RightVector:"⇀",RightVectorBar:"⥓",ring:"˚",risingdotseq:"≓",rlarr:"⇄",rlhar:"⇌",rlm:"‏",rmoust:"⎱",rmoustache:"⎱",rnmid:"⫮",roang:"⟭",roarr:"⇾",robrk:"⟧",ropar:"⦆",ropf:"𝕣",Ropf:"ℝ",roplus:"⨮",rotimes:"⨵",RoundImplies:"⥰",rpar:")",rpargt:"⦔",rppolint:"⨒",rrarr:"⇉",Rrightarrow:"⇛",rsaquo:"›",rscr:"𝓇",Rscr:"ℛ",rsh:"↱",Rsh:"↱",rsqb:"]",rsquo:"’",rsquor:"’",rthree:"⋌",rtimes:"⋊",rtri:"▹",rtrie:"⊵",rtrif:"▸",rtriltri:"⧎",RuleDelayed:"⧴",ruluhar:"⥨",rx:"℞",sacute:"ś",Sacute:"Ś",sbquo:"‚",sc:"≻",Sc:"⪼",scap:"⪸",scaron:"š",Scaron:"Š",sccue:"≽",sce:"⪰",scE:"⪴",scedil:"ş",Scedil:"Ş",scirc:"ŝ",Scirc:"Ŝ",scnap:"⪺",scnE:"⪶",scnsim:"⋩",scpolint:"⨓",scsim:"≿",scy:"с",Scy:"С",sdot:"⋅",sdotb:"⊡",sdote:"⩦",searhk:"⤥",searr:"↘",seArr:"⇘",searrow:"↘",sect:"§",semi:";",seswar:"⤩",setminus:"∖",setmn:"∖",sext:"✶",sfr:"𝔰",Sfr:"𝔖",sfrown:"⌢",sharp:"♯",shchcy:"щ",SHCHcy:"Щ",shcy:"ш",SHcy:"Ш",ShortDownArrow:"↓",ShortLeftArrow:"←",shortmid:"∣",shortparallel:"∥",ShortRightArrow:"→",ShortUpArrow:"↑",shy:"­",sigma:"σ",Sigma:"Σ",sigmaf:"ς",sigmav:"ς",sim:"∼",simdot:"⩪",sime:"≃",simeq:"≃",simg:"⪞",simgE:"⪠",siml:"⪝",simlE:"⪟",simne:"≆",simplus:"⨤",simrarr:"⥲",slarr:"←",SmallCircle:"∘",smallsetminus:"∖",smashp:"⨳",smeparsl:"⧤",smid:"∣",smile:"⌣",smt:"⪪",smte:"⪬",smtes:"⪬︀",softcy:"ь",SOFTcy:"Ь",sol:"/",solb:"⧄",solbar:"⌿",sopf:"𝕤",Sopf:"𝕊",spades:"♠",spadesuit:"♠",spar:"∥",sqcap:"⊓",sqcaps:"⊓︀",sqcup:"⊔",sqcups:"⊔︀",Sqrt:"√",sqsub:"⊏",sqsube:"⊑",sqsubset:"⊏",sqsubseteq:"⊑",sqsup:"⊐",sqsupe:"⊒",sqsupset:"⊐",sqsupseteq:"⊒",squ:"□",square:"□",Square:"□",SquareIntersection:"⊓",SquareSubset:"⊏",SquareSubsetEqual:"⊑",SquareSuperset:"⊐",SquareSupersetEqual:"⊒",SquareUnion:"⊔",squarf:"▪",squf:"▪",srarr:"→",sscr:"𝓈",Sscr:"𝒮",ssetmn:"∖",ssmile:"⌣",sstarf:"⋆",star:"☆",Star:"⋆",starf:"★",straightepsilon:"ϵ",straightphi:"ϕ",strns:"¯",sub:"⊂",Sub:"⋐",subdot:"⪽",sube:"⊆",subE:"⫅",subedot:"⫃",submult:"⫁",subne:"⊊",subnE:"⫋",subplus:"⪿",subrarr:"⥹",subset:"⊂",Subset:"⋐",subseteq:"⊆",subseteqq:"⫅",SubsetEqual:"⊆",subsetneq:"⊊",subsetneqq:"⫋",subsim:"⫇",subsub:"⫕",subsup:"⫓",succ:"≻",succapprox:"⪸",succcurlyeq:"≽",Succeeds:"≻",SucceedsEqual:"⪰",SucceedsSlantEqual:"≽",SucceedsTilde:"≿",succeq:"⪰",succnapprox:"⪺",succneqq:"⪶",succnsim:"⋩",succsim:"≿",SuchThat:"∋",sum:"∑",Sum:"∑",sung:"♪",sup:"⊃",Sup:"⋑",sup1:"¹",sup2:"²",sup3:"³",supdot:"⪾",supdsub:"⫘",supe:"⊇",supE:"⫆",supedot:"⫄",Superset:"⊃",SupersetEqual:"⊇",suphsol:"⟉",suphsub:"⫗",suplarr:"⥻",supmult:"⫂",supne:"⊋",supnE:"⫌",supplus:"⫀",supset:"⊃",Supset:"⋑",supseteq:"⊇",supseteqq:"⫆",supsetneq:"⊋",supsetneqq:"⫌",supsim:"⫈",supsub:"⫔",supsup:"⫖",swarhk:"⤦",swarr:"↙",swArr:"⇙",swarrow:"↙",swnwar:"⤪",szlig:"ß",Tab:"\t",target:"⌖",tau:"τ",Tau:"Τ",tbrk:"⎴",tcaron:"ť",Tcaron:"Ť",tcedil:"ţ",Tcedil:"Ţ",tcy:"т",Tcy:"Т",tdot:"⃛",telrec:"⌕",tfr:"𝔱",Tfr:"𝔗",there4:"∴",therefore:"∴",Therefore:"∴",theta:"θ",Theta:"Θ",thetasym:"ϑ",thetav:"ϑ",thickapprox:"≈",thicksim:"∼",ThickSpace:"  ",thinsp:" ",ThinSpace:" ",thkap:"≈",thksim:"∼",thorn:"þ",THORN:"Þ",tilde:"˜",Tilde:"∼",TildeEqual:"≃",TildeFullEqual:"≅",TildeTilde:"≈",times:"×",timesb:"⊠",timesbar:"⨱",timesd:"⨰",tint:"∭",toea:"⤨",top:"⊤",topbot:"⌶",topcir:"⫱",topf:"𝕥",Topf:"𝕋",topfork:"⫚",tosa:"⤩",tprime:"‴",trade:"™",TRADE:"™",triangle:"▵",triangledown:"▿",triangleleft:"◃",trianglelefteq:"⊴",triangleq:"≜",triangleright:"▹",trianglerighteq:"⊵",tridot:"◬",trie:"≜",triminus:"⨺",TripleDot:"⃛",triplus:"⨹",trisb:"⧍",tritime:"⨻",trpezium:"⏢",tscr:"𝓉",Tscr:"𝒯",tscy:"ц",TScy:"Ц",tshcy:"ћ",TSHcy:"Ћ",tstrok:"ŧ",Tstrok:"Ŧ",twixt:"≬",twoheadleftarrow:"↞",twoheadrightarrow:"↠",uacute:"ú",Uacute:"Ú",uarr:"↑",uArr:"⇑",Uarr:"↟",Uarrocir:"⥉",ubrcy:"ў",Ubrcy:"Ў",ubreve:"ŭ",Ubreve:"Ŭ",ucirc:"û",Ucirc:"Û",ucy:"у",Ucy:"У",udarr:"⇅",udblac:"ű",Udblac:"Ű",udhar:"⥮",ufisht:"⥾",ufr:"𝔲",Ufr:"𝔘",ugrave:"ù",Ugrave:"Ù",uHar:"⥣",uharl:"↿",uharr:"↾",uhblk:"▀",ulcorn:"⌜",ulcorner:"⌜",ulcrop:"⌏",ultri:"◸",umacr:"ū",Umacr:"Ū",uml:"¨",UnderBar:"_",UnderBrace:"⏟",UnderBracket:"⎵",UnderParenthesis:"⏝",Union:"⋃",UnionPlus:"⊎",uogon:"ų",Uogon:"Ų",uopf:"𝕦",Uopf:"𝕌",uparrow:"↑",Uparrow:"⇑",UpArrow:"↑",UpArrowBar:"⤒",UpArrowDownArrow:"⇅",updownarrow:"↕",Updownarrow:"⇕",UpDownArrow:"↕",UpEquilibrium:"⥮",upharpoonleft:"↿",upharpoonright:"↾",uplus:"⊎",UpperLeftArrow:"↖",UpperRightArrow:"↗",upsi:"υ",Upsi:"ϒ",upsih:"ϒ",upsilon:"υ",Upsilon:"Υ",UpTee:"⊥",UpTeeArrow:"↥",upuparrows:"⇈",urcorn:"⌝",urcorner:"⌝",urcrop:"⌎",uring:"ů",Uring:"Ů",urtri:"◹",uscr:"𝓊",Uscr:"𝒰",utdot:"⋰",utilde:"ũ",Utilde:"Ũ",utri:"▵",utrif:"▴",uuarr:"⇈",uuml:"ü",Uuml:"Ü",uwangle:"⦧",vangrt:"⦜",varepsilon:"ϵ",varkappa:"ϰ",varnothing:"∅",varphi:"ϕ",varpi:"ϖ",varpropto:"∝",varr:"↕",vArr:"⇕",varrho:"ϱ",varsigma:"ς",varsubsetneq:"⊊︀",varsubsetneqq:"⫋︀",varsupsetneq:"⊋︀",varsupsetneqq:"⫌︀",vartheta:"ϑ",vartriangleleft:"⊲",vartriangleright:"⊳",vBar:"⫨",Vbar:"⫫",vBarv:"⫩",vcy:"в",Vcy:"В",vdash:"⊢",vDash:"⊨",Vdash:"⊩",VDash:"⊫",Vdashl:"⫦",vee:"∨",Vee:"⋁",veebar:"⊻",veeeq:"≚",vellip:"⋮",verbar:"|",Verbar:"‖",vert:"|",Vert:"‖",VerticalBar:"∣",VerticalLine:"|",VerticalSeparator:"❘",VerticalTilde:"≀",VeryThinSpace:" ",vfr:"𝔳",Vfr:"𝔙",vltri:"⊲",vnsub:"⊂⃒",vnsup:"⊃⃒",vopf:"𝕧",Vopf:"𝕍",vprop:"∝",vrtri:"⊳",vscr:"𝓋",Vscr:"𝒱",vsubne:"⊊︀",vsubnE:"⫋︀",vsupne:"⊋︀",vsupnE:"⫌︀",Vvdash:"⊪",vzigzag:"⦚",wcirc:"ŵ",Wcirc:"Ŵ",wedbar:"⩟",wedge:"∧",Wedge:"⋀",wedgeq:"≙",weierp:"℘",wfr:"𝔴",Wfr:"𝔚",wopf:"𝕨",Wopf:"𝕎",wp:"℘",wr:"≀",wreath:"≀",wscr:"𝓌",Wscr:"𝒲",xcap:"⋂",xcirc:"◯",xcup:"⋃",xdtri:"▽",xfr:"𝔵",Xfr:"𝔛",xharr:"⟷",xhArr:"⟺",xi:"ξ",Xi:"Ξ",xlarr:"⟵",xlArr:"⟸",xmap:"⟼",xnis:"⋻",xodot:"⨀",xopf:"𝕩",Xopf:"𝕏",xoplus:"⨁",xotime:"⨂",xrarr:"⟶",xrArr:"⟹",xscr:"𝓍",Xscr:"𝒳",xsqcup:"⨆",xuplus:"⨄",xutri:"△",xvee:"⋁",xwedge:"⋀",yacute:"ý",Yacute:"Ý",yacy:"я",YAcy:"Я",ycirc:"ŷ",Ycirc:"Ŷ",ycy:"ы",Ycy:"Ы",yen:"¥",yfr:"𝔶",Yfr:"𝔜",yicy:"ї",YIcy:"Ї",yopf:"𝕪",Yopf:"𝕐",yscr:"𝓎",Yscr:"𝒴",yucy:"ю",YUcy:"Ю",yuml:"ÿ",Yuml:"Ÿ",zacute:"ź",Zacute:"Ź",zcaron:"ž",Zcaron:"Ž",zcy:"з",Zcy:"З",zdot:"ż",Zdot:"Ż",zeetrf:"ℨ",ZeroWidthSpace:"​",zeta:"ζ",Zeta:"Ζ",zfr:"𝔷",Zfr:"ℨ",zhcy:"ж",ZHcy:"Ж",zigrarr:"⇝",zopf:"𝕫",Zopf:"ℤ",zscr:"𝓏",Zscr:"𝒵",zwj:"‍",zwnj:"‌"},t={aacute:"á",Aacute:"Á",acirc:"â",Acirc:"Â",acute:"´",aelig:"æ",AElig:"Æ",agrave:"à",Agrave:"À",amp:"&",AMP:"&",aring:"å",Aring:"Å",atilde:"ã",Atilde:"Ã",auml:"ä",Auml:"Ä",brvbar:"¦",ccedil:"ç",Ccedil:"Ç",cedil:"¸",cent:"¢",copy:"©",COPY:"©",curren:"¤",deg:"°",divide:"÷",eacute:"é",Eacute:"É",ecirc:"ê",Ecirc:"Ê",egrave:"è",Egrave:"È",eth:"ð",ETH:"Ð",euml:"ë",Euml:"Ë",frac12:"½",frac14:"¼",frac34:"¾",gt:">",GT:">",iacute:"í",Iacute:"Í",icirc:"î",Icirc:"Î",iexcl:"¡",igrave:"ì",Igrave:"Ì",iquest:"¿",iuml:"ï",Iuml:"Ï",laquo:"«",lt:"<",LT:"<",macr:"¯",micro:"µ",middot:"·",nbsp:" ",not:"¬",ntilde:"ñ",Ntilde:"Ñ",oacute:"ó",Oacute:"Ó",ocirc:"ô",Ocirc:"Ô",ograve:"ò",Ograve:"Ò",ordf:"ª",ordm:"º",oslash:"ø",Oslash:"Ø",otilde:"õ",Otilde:"Õ",ouml:"ö",Ouml:"Ö",para:"¶",plusmn:"±",pound:"£",quot:'"',QUOT:'"',raquo:"»",reg:"®",REG:"®",sect:"§",shy:"­",sup1:"¹",sup2:"²",sup3:"³",szlig:"ß",thorn:"þ",THORN:"Þ",times:"×",uacute:"ú",Uacute:"Ú",ucirc:"û",Ucirc:"Û",ugrave:"ù",Ugrave:"Ù",uml:"¨",uuml:"ü",Uuml:"Ü",yacute:"ý",Yacute:"Ý",yen:"¥",yuml:"ÿ"},o={0:"�",128:"€",130:"‚",131:"ƒ",132:"„",133:"…",134:"†",135:"‡",136:"ˆ",137:"‰",138:"Š",139:"‹",140:"Œ",142:"Ž",145:"‘",146:"’",147:"“",148:"”",149:"•",150:"–",151:"—",152:"˜",153:"™",154:"š",155:"›",156:"œ",158:"ž",159:"Ÿ"},s=[1,2,3,4,5,6,7,8,11,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,64976,64977,64978,64979,64980,64981,64982,64983,64984,64985,64986,64987,64988,64989,64990,64991,64992,64993,64994,64995,64996,64997,64998,64999,65e3,65001,65002,65003,65004,65005,65006,65007,65534,65535,131070,131071,196606,196607,262142,262143,327678,327679,393214,393215,458750,458751,524286,524287,589822,589823,655358,655359,720894,720895,786430,786431,851966,851967,917502,917503,983038,983039,1048574,1048575,1114110,1114111],l=String.fromCharCode,i={}.hasOwnProperty,c=function(r,e){return i.call(r,e)},n=function(r,e){let a="";return r>=55296&&r<=57343||r>1114111?(e&&u("character reference outside the permissible Unicode range"),"�"):c(o,r)?(e&&u("disallowed character reference"),o[r]):(e&&function(r,e){let a=-1;const t=r.length;for(;++a65535&&(a+=l((r-=65536)>>>10&1023|55296),r=56320|1023&r),a+=l(r))},u=function(r){throw Error("Parse error: "+r)},p=function(o,s){const l=(s=function(r,e){if(!r)return e;let a={};for(let t in e)a[t]=c(r,t)?r[t]:e[t];return a}(s,p.options)).strict;return l&&r.test(o)&&u("malformed character reference"),o.replace(e,function(r,e,o,i,c,p,g,d,m){let f,h,b,q,w,v;return e?a[w=e]:o?(w=o,(v=i)&&s.isAttributeValue?(l&&"="===v&&u("`&` did not start a character reference"),r):(l&&u("named character reference was not terminated by a semicolon"),t[w]+(v||""))):c?(b=c,h=p,l&&!h&&u("character reference was not terminated by a semicolon"),f=parseInt(b,10),n(f,l)):g?(q=g,h=d,l&&!h&&u("character reference was not terminated by a semicolon"),f=parseInt(q,16),n(f,l)):(l&&u("named character reference was not terminated by a semicolon"),r)})};p.options={isAttributeValue:!1,strict:!1},define(function(){return{decode:p}})}(); diff --git a/src/scripts/libs/jquery.min.js b/src/scripts/libs/jquery.min.js new file mode 100644 index 00000000..4d9b3a25 --- /dev/null +++ b/src/scripts/libs/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w("