diff --git a/.compilerc b/.compilerc index 6c1a6c295..fb9429932 100644 --- a/.compilerc +++ b/.compilerc @@ -3,7 +3,7 @@ "development": { "application/javascript": { "presets": [ - ["env", { "targets": { "electron": "1.4" } }], + ["env", { "targets": { "electron-renderer": "1.4" } }], "react" ], "plugins": ["transform-async-to-generator"], @@ -13,7 +13,7 @@ "production": { "application/javascript": { "presets": [ - ["env", { "targets": { "electron": "1.4" } }], + ["env", { "targets": { "electron-renderer": "1.4" } }], "react" ], "plugins": ["transform-async-to-generator"], diff --git a/.gitignore b/.gitignore index 2ec91a4da..5c067f640 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ subscriptions\.db .vscode/ .eslintrc* *.db +*.code-workspace electron-packager/win32-x64/FreeTube-win32-x64/ diff --git a/.jsbeautifyrc b/.jsbeautifyrc new file mode 100644 index 000000000..9de93e7b0 --- /dev/null +++ b/.jsbeautifyrc @@ -0,0 +1,49 @@ +{ + "html": { + "allowed_file_extensions": ["htm", "html", "xhtml", "shtml", "xml", "svg", "dust"], + "brace_style": "collapse", + "end_with_newline": true, + "indent_char": " ", + "indent_handlebars": true, + "indent_inner_html": false, + "indent_scripts": "keep", + "indent_size": 4, + "max_preserve_newlines": 10, + "preserve_newlines": true, + "unformatted": ["a", "span", "img", "code", "pre", "sub", "sup", "em", "strong", "b", "i", "u", "strike", "big", "small", "pre", "h1", "h2", "h3", "h4", "h5", "h6"], // List of tags that should not be reformatted + "wrap_line_length": 0 + }, + "css": { + "allowed_file_extensions": ["css", "scss", "sass", "less"], + "end_with_newline": true, + "indent_char": " ", + "indent_size": 4, + "newline_between_rules": true, + "selector_separator": " ", + "selector_separator_newline": true, + "preserve_newlines": true, + "max_preserve_newlines": 10 + }, + "js": { + "allowed_file_extensions": ["js", "json", "jshintrc", "jsbeautifyrc"], + "brace_style": "collapse", + "break_chained_methods": false, + "e4x": false, + "end_with_newline": false, + "indent_char": " ", + "indent_level": 0, + "indent_size": 4, + "indent_with_tabs": false, + "jslint_happy": true, + "keep_array_indentation": false, + "keep_function_indentation": false, + "max_preserve_newlines": 0, + "preserve_newlines": true, + "space_after_anon_function": true, + "space_before_conditional": true, + "space_in_empty_paren": false, + "space_in_paren": false, + "unescape_strings": false, + "wrap_line_length": 0 + } +} \ No newline at end of file diff --git a/locales/es.json b/locales/es.json new file mode 100644 index 000000000..11bff2911 --- /dev/null +++ b/locales/es.json @@ -0,0 +1,95 @@ +{ + "File": "Archivo", + "Quit": "Salir", + "Edit": "Editar", + "Undo": "Deshacer", + "Redo": "Rehacer", + "Cut": "Cortar", + "Copy": "Copiar", + "Paste": "Pegar", + "Delete": "Eliminar", + "Select all": "Seleccionar todo", + "View": "Ver", + "Reload": "Recargar", + "Force Reload": "Forzar Recarga", + "Toggle Developer Tools": "Herramientas para desarrolladores", + "Actual size": "Tamaño real", + "Zoom in": "Aumentar zoom", + "Zoom out": "Reducir zoom", + "Toggle fullscreen": "Cambiar a pantalla completa", + "Window": "Ventana", + "Minimize": "Minimizar", + "Close": "Cerrar", + "FreeTube": "FreeTube", + "Subscriptions": "Suscripciones", + "Featured": "Destacados", + "Most Popular": "Más Popular", + "Saved": "Guardados", + "Playlists": "Listas de Reproducción", + "History": "Historial", + "Settings": "Ajustes", + "About": "Acerca de", + "Search / Go to URL": "Buscar / Ir a la URL", + "Search Results": "Resultados de búsqueda", + "Subscriber": "Suscriptor", + "Subscriber": "Suscriptores", + "Video": "Vídeo", + "Videos": "Vídeos", + "View Full Playlist": "Ver Lista de Reproducción completa", + "Live Now": "En vivo ahora", + "Fetch more results": "Obtener más resultados", + "Fetching results. Please wait": "Obteniendo más resultados. Por favor espere", + "Latest Subscriptions": "Últimas Suscripciones", + "Save Video": "Guardar Vídeo", + "Remove Saved Video": "Eliminar Vídeo Guardado", + "Open in YouTube": "Abrir en YouTube", + "Copy YouTube Link": "Copiar Enlace a YouTube", + "Open in HookTube": "Abrir en HookTube", + "Copy HookTube Link": "Copiar Enlace a HookTube", + "URL has been copied to the clipboard": "La URL ha sido copiada al portapapeles", + "Found valid URL for 480p, but returned a 404. Video type might be available in the future.": "URL para 480p encontrada, pero devolvió un 404. El tipo de vídeo podría estar disponible en el futuro.", + "Save": "Guardar", + "Mini Player": "Mini Reproductor", + "View": "Reproducción", + "Views": "Reproducciones", + "Subscribe": "Suscribirse", + "Unsubscribe": "Desuscribirse", + "Published on": "Publicado el", + "Jan": "ene", + "Feb": "feb", + "Mar": "mar", + "Apr": "abr", + "May": "may", + "Jun": "jun", + "Jul": "jul", + "Aug": "ago", + "Sep": "sep", + "Oct": "oct", + "Nov": "nov", + "Dec": "dic", + "Show Comments": "Ver Comentarios", + "Max of 100": "Máximo de 100", + "Recommendations": "Recomendaciones", + "Latest Subscriptions": "Últimas Suscripciones", + "Getting Subscriptions. Please wait...": "Obteniendo Suscripciones. Por favor espere...", + "Your Subscription list is currently empty. Start adding subscriptions to see them here.": "Tu lista de Suscripción está actualmente vacía. Empieza añadiendo suscripciones para verlas ahí.", + "Saved Videos": "Videos Guardados", + "Watch History": "Ver Historial", + "API Key": "API Key", + "Set API Key: Leave blank to use default": "Establecer API Key: Dejar en blanco para usar la de por defecto", + "Use Dark Theme": "Usar Tema Oscuro", + "Import Subscriptions": "Importar Suscripción", + "Export Subscriptions": "Exportar Suscripción", + "Clear History": "Limpiar Historial", + "Are you sure you want to delete your history?": "¿Estás seguro de que quieres eliminar tu historial?", + "Clear Saved Videos": "Limpiar Vídeos Guardados", + "Are you sure you want to remove all saved videos?": "¿Estás seguro de que quieres eliminar todos los vídeos guardados?", + "Clear Subscriptions": "Limpiar Suscripciones", + "Are you sure you want to remove all subscriptions?": "¿Estás seguro de que quieres eliminar todas las suscripciones?", + "Save Settings": "Guardar Ajustes", + "Yes": "Sí", + "No": "No", + "Beta": "Beta", + "This software is FOSS and released under the GNU Public License v3+.": "Este software es FOSS y liberado bajo la licencia GNU Public License v3+.", + "Found a bug? Want to suggest a feature? Want to help out? Check out our GitHub page. Pull requests are welcome.": "¿Encontraste un bug? ¿Quieres sugerir una nueva característica? ¿Quieres ayudar? Visita nuestra página de Github. Las contribuciones son bienvenidas" +} diff --git a/package-lock.json b/package-lock.json index da071de35..3007079f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "FreeTube", - "version": "0.3.0", + "version": "0.3.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -78,9 +78,7 @@ "abab": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", - "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", - "dev": true, - "optional": true + "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=" }, "abbrev": { "version": "1.1.1", @@ -163,6 +161,11 @@ "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", "dev": true }, + "ansi-font": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/ansi-font/-/ansi-font-0.0.2.tgz", + "integrity": "sha1-iQMBvVhBRi/TnAt3Ca/R9SUUMzE=" + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -355,6 +358,11 @@ "sprintf-js": "~1.0.2" } }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" + }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -418,6 +426,11 @@ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1347,6 +1360,11 @@ "concat-map": "0.0.1" } }, + "browser-process-hrtime": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz", + "integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44=" + }, "browserslist": { "version": "1.7.7", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.7.7.tgz", @@ -1712,6 +1730,63 @@ "htmlparser2": "~3.8.1", "jsdom": "^7.0.2", "lodash": "^4.1.0" + }, + "dependencies": { + "cssstyle": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", + "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", + "dev": true, + "optional": true, + "requires": { + "cssom": "0.3.x" + } + }, + "jsdom": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz", + "integrity": "sha1-QLQCdwwr2iNGkJa+6Rq2deOx/G4=", + "dev": true, + "optional": true, + "requires": { + "abab": "^1.0.0", + "acorn": "^2.4.0", + "acorn-globals": "^1.0.4", + "cssom": ">= 0.3.0 < 0.4.0", + "cssstyle": ">= 0.2.29 < 0.3.0", + "escodegen": "^1.6.1", + "nwmatcher": ">= 1.3.7 < 2.0.0", + "parse5": "^1.5.1", + "request": "^2.55.0", + "sax": "^1.1.4", + "symbol-tree": ">= 3.1.0 < 4.0.0", + "tough-cookie": "^2.2.0", + "webidl-conversions": "^2.0.0", + "whatwg-url-compat": "~0.6.5", + "xml-name-validator": ">= 2.0.1 < 3.0.0" + } + }, + "parse5": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", + "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=", + "dev": true, + "optional": true + }, + "webidl-conversions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-2.0.1.tgz", + "integrity": "sha1-O/glj30xjHRDw28uFpQCoaZwNQY=", + "dev": true, + "optional": true + }, + "xml-name-validator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", + "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", + "dev": true, + "optional": true + } } }, "chromium-pickle-js": { @@ -1901,6 +1976,15 @@ "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==", "dev": true }, + "commonjs": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/commonjs/-/commonjs-0.0.1.tgz", + "integrity": "sha1-ZcUx3P9lZcp8ld38lmIricwClNU=", + "requires": { + "system": ">=0.0.1", + "test": ">=0.0.5" + } + }, "compare-version": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", @@ -2193,17 +2277,14 @@ } }, "cssom": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.2.tgz", - "integrity": "sha1-uANhcMefB6kP8vFuIihAJ6JDhIs=", - "dev": true + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.4.tgz", + "integrity": "sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog==" }, "cssstyle": { - "version": "0.2.37", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.37.tgz", - "integrity": "sha1-VBCXI0yyUTyDzu06zdwn/yeYfVQ=", - "dev": true, - "optional": true, + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.3.1.tgz", + "integrity": "sha512-tNvaxM5blOnxanyxI6panOsnfiyLRj3HV4qjqqS45WPNS1usdYWRUQjqTEEELK73lpeP/1KoIGYUwrBn/VcECA==", "requires": { "cssom": "0.3.x" } @@ -2230,6 +2311,16 @@ "assert-plus": "^1.0.0" } }, + "data-urls": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.0.0.tgz", + "integrity": "sha512-ai40PPQR0Fn1lD2PPie79CibnlMN2AYiDhwFX/rZHVsxbs5kNJSjegqXIprhouGXlRdEnfybva7kqRGnB6mypA==", + "requires": { + "abab": "^1.0.4", + "whatwg-mimetype": "^2.0.0", + "whatwg-url": "^6.4.0" + } + }, "dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", @@ -2277,8 +2368,7 @@ "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, "defaults": { "version": "1.0.3", @@ -2417,6 +2507,14 @@ "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", "dev": true }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "requires": { + "webidl-conversions": "^4.0.2" + } + }, "domhandler": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", @@ -4225,11 +4323,9 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", - "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", - "dev": true, - "optional": true, + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", + "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", "requires": { "esprima": "^3.1.3", "estraverse": "^4.2.0", @@ -4241,15 +4337,12 @@ "esprima": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true, - "optional": true + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "optional": true } } @@ -4262,15 +4355,12 @@ "estraverse": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true, - "optional": true + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" }, "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "events": { "version": "1.1.1", @@ -4382,8 +4472,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "fd-slicer": { "version": "1.0.1", @@ -5054,6 +5143,14 @@ "integrity": "sha1-ZouTd26q5V696POtRkswekljYl4=", "dev": true }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, "html-entities": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", @@ -5642,27 +5739,56 @@ "optional": true }, "jsdom": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz", - "integrity": "sha1-QLQCdwwr2iNGkJa+6Rq2deOx/G4=", - "dev": true, - "optional": true, + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.11.0.tgz", + "integrity": "sha512-ou1VyfjwsSuWkudGxb03FotDajxAto6USAlmMZjE2lc0jCznt7sBWkhfRBRaWwbnmDqdMSTKTLT5d9sBFkkM7A==", "requires": { - "abab": "^1.0.0", - "acorn": "^2.4.0", - "acorn-globals": "^1.0.4", - "cssom": ">= 0.3.0 < 0.4.0", - "cssstyle": ">= 0.2.29 < 0.3.0", - "escodegen": "^1.6.1", - "nwmatcher": ">= 1.3.7 < 2.0.0", - "parse5": "^1.5.1", - "request": "^2.55.0", - "sax": "^1.1.4", - "symbol-tree": ">= 3.1.0 < 4.0.0", - "tough-cookie": "^2.2.0", - "webidl-conversions": "^2.0.0", - "whatwg-url-compat": "~0.6.5", - "xml-name-validator": ">= 2.0.1 < 3.0.0" + "abab": "^1.0.4", + "acorn": "^5.3.0", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": ">= 0.3.1 < 0.4.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.0", + "escodegen": "^1.9.0", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.2.0", + "nwsapi": "^2.0.0", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.83.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.3", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^4.0.0", + "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", + "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==" + }, + "acorn-globals": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz", + "integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==", + "requires": { + "acorn": "^5.0.0" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + } } }, "jsesc": { @@ -5817,6 +5943,11 @@ "invert-kv": "^1.0.0" } }, + "left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" + }, "less": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", @@ -6005,7 +6136,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, "requires": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" @@ -6060,8 +6190,7 @@ "lodash": { "version": "4.17.10", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", - "dev": true + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" }, "lodash._reinterpolate": { "version": "3.0.0", @@ -6135,6 +6264,11 @@ "integrity": "sha1-YAYMxr1iW01FZ+wn3EXNG+nuwBI=", "dev": true }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, "lodash.template": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", @@ -6619,6 +6753,11 @@ "dev": true, "optional": true }, + "nwsapi": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.7.tgz", + "integrity": "sha512-VZXniaaaORAXGCNsvUNefsKRQYk8zCzQZ57jalgrpHcU70OrAzKAiN/3plYtH/VPRmZeYyUzQiYfKzcMXC1g5Q==" + }, "oauth-sign": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", @@ -6706,7 +6845,6 @@ "version": "0.8.2", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, "requires": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.4", @@ -6885,11 +7023,9 @@ "dev": true }, "parse5": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", - "integrity": "sha1-m387DeMr543CQBsXVzzK8Pb1nZQ=", - "dev": true, - "optional": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==" }, "parser-toolkit": { "version": "0.0.5", @@ -6986,6 +7122,11 @@ } } }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" + }, "postcss": { "version": "5.2.18", "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", @@ -7301,8 +7442,7 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "prepend-http": { "version": "1.0.4", @@ -7683,6 +7823,24 @@ "uuid": "^3.1.0" } }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "requires": { + "lodash": "^4.13.1" + } + }, + "request-promise-native": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", + "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "requires": { + "request-promise-core": "1.1.1", + "stealthy-require": "^1.1.0", + "tough-cookie": ">=2.3.3" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8147,6 +8305,11 @@ "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", "dev": true }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, "stream-json": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-0.6.1.tgz", @@ -8368,9 +8531,7 @@ "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", - "dev": true, - "optional": true + "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=" }, "synchronous-promise": { "version": "1.0.18", @@ -8378,6 +8539,11 @@ "integrity": "sha512-UqMAK6BBBXu8qaDI5omlyV9iDpM9nQfgthaBOK0nlfXnMgiuOBv+meWG73CGeCCFRhOOOa2e4rvqYzfynzy5zg==", "dev": true }, + "system": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/system/-/system-2.0.1.tgz", + "integrity": "sha512-BwSUSa8LMHZouGadZ34ck3TsrH5s3oMmTKPK+xHdbBnTCZOZMJ38fHGKLAHkBl0PXru1Z4BsymQU4qqvTxWzdQ==" + }, "tabtab": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tabtab/-/tabtab-2.2.2.tgz", @@ -8596,6 +8762,14 @@ "lazy-val": "^1.0.3" } }, + "test": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/test/-/test-0.6.0.tgz", + "integrity": "sha1-WYasRF7Bd1QyJRLRBLoyyKY+k44=", + "requires": { + "ansi-font": "0.0.2" + } + }, "thenify": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", @@ -8759,11 +8933,19 @@ } }, "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true, - "optional": true + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } }, "transformers": { "version": "2.1.0", @@ -8865,7 +9047,6 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, "requires": { "prelude-ls": "~1.1.2" } @@ -9102,6 +9283,14 @@ "integrity": "sha512-x3LV3wdmmERhVCYy3quqA57NJW7F3i6faas++pJQWtknWT+n7k30F4TVdHvCLn48peTJFRvCpxs3UuFPqgeELg==", "dev": true }, + "w3c-hr-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", + "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "requires": { + "browser-process-hrtime": "^0.1.2" + } + }, "wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -9112,11 +9301,39 @@ } }, "webidl-conversions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-2.0.1.tgz", - "integrity": "sha1-O/glj30xjHRDw28uFpQCoaZwNQY=", - "dev": true, - "optional": true + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "whatwg-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz", + "integrity": "sha512-jLBwwKUhi8WtBfsMQlL4bUUcT8sMkAtQinscJAe/M4KHCkHuUJAF6vuB0tueNIw4c8ziO6AkRmgY+jL3a0iiPw==", + "requires": { + "iconv-lite": "0.4.19" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + } + } + }, + "whatwg-mimetype": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz", + "integrity": "sha512-FKxhYLytBQiUKjkYteN71fAUA3g6KpNXoho1isLiLSB3N1G4F35Q5vUxWfKFhBwi5IWF27VE6WxhrnnC+m0Mew==" + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } }, "whatwg-url-compat": { "version": "0.6.5", @@ -9126,6 +9343,15 @@ "optional": true, "requires": { "tr46": "~0.0.1" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", + "dev": true, + "optional": true + } } }, "whet.extend": { @@ -9190,8 +9416,7 @@ "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" }, "wrap-ansi": { "version": "2.1.0", @@ -9214,12 +9439,19 @@ "dev": true, "optional": true }, + "ws": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", + "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0" + } + }, "xml-name-validator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz", - "integrity": "sha1-TYuPHszTQZqjYgYb7O9RXh5VljU=", - "dev": true, - "optional": true + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, "xml2js": { "version": "0.4.17", diff --git a/package.json b/package.json index 25dd0c99a..e19efb21f 100644 --- a/package.json +++ b/package.json @@ -1,95 +1,102 @@ { - "name": "FreeTube", - "productName": "FreeTube", - "version": "0.3.1", - "description": "An Open Source YouTube app for privacy.", - "main": "src/js/init.js", - "scripts": { - "start": "electron-forge start", - "package": "electron-forge package", - "make": "electron-forge make", - "publish": "electron-forge publish", - "make:mac": "electron-forge make --platform=darwin", - "make:linux": "electron-forge make --platform=linux", - "make:linux:zip": "electron-forge make --platform=linux --targets=zip", - "make:deb": "electron-forge make --platform=linux --targets=deb", - "make:rpm": "electron-forge make --platform=linux --targets=rpm", - "make:snap": "electron-forge package && electron-installer-snap --src=out/FreeTube-linux-x64", - "make:flatpak": "electron-installer-flatpak --src out/FreeTube-linux-x64/ --dest out/make --arch x64", - "make:appimage": "electron-forge make --platform=linux --targets=electron-forge-maker-appimage", - "make:win": "electron-forge make --platform=win32", - "make:win:zip": "electron-forge make --platform=win32 --targets=zip" - }, - "keywords": [], - "author": { - "name": "PrestonN", - "email": "FreeTubeApp@protonmail.com", - "url": "https://github.com/FreeTubeApp/FreeTube" - }, - "license": "GPL-3.0-or-later", - "config": { - "forge": { - "make_targets": { - "win32": [ - "squirrel", - "zip" - ], - "darwin": [ - "zip" - ], - "linux": [ - "deb", - "rpm", - "electron-forge-maker-appimage", - "zip" - ] - }, - "protocols": [ - { - "name": "freetube", - "role": "Viewer", - "schemes": [ - "freetube" - ] - } - ], - "electronPackagerConfig": { - "packageManager": "yarn", - "icon": "./src/icons/iconColor.icns" - }, - "electronWinstallerConfig": { - "name": "freetube", - "iconUrl": "https://raw.githubusercontent.com/FreeTubeApp/FreeTubeApp.github.io/master/images/iconColor.ico", - "setupIcon": "./src/icons/iconColor.ico" - }, - "electronInstallerDebian": { - "icon": "src/icons/iconColor.png" - }, - "repository": { - "type": "git", + "name": "FreeTube", + "productName": "FreeTube", + "version": "0.3.2", + "description": "An Open Source YouTube app for privacy.", + "main": "src/js/init.js", + "scripts": { + "start": "electron-forge start", + "package": "electron-forge package", + "make": "electron-forge make", + "publish": "electron-forge publish", + "make:all": "npm run make:mac && npm run make:linux:x86 && npm run make:linux:arm && npm run make:win", + "make:mac": "electron-forge make --platform=darwin", + "make:win": "electron-forge make --platform=win32", + "make:win:zip": "electron-forge make --platform=win32 --targets=zip", + "make:linux:x86": "electron-forge make --platform=linux --arch x64", + "make:linux:x86:zip": "electron-forge make --platform=linux --targets=zip --arch x64", + "make:linux:x86:deb": "electron-forge make --platform=linux --targets=deb --arch x64", + "make:linux:x86:rpm": "electron-forge make --platform=linux --targets=rpm --arch x64", + "make:linux:x86:snap": "electron-forge package && electron-installer-snap --src=out/FreeTube-linux-x64 --arch x64", + "make:linux:x86:flatpak": "electron-installer-flatpak --src out/FreeTube-linux-x64/ --dest out/make --arch x64", + "make:linux:x86:appimage": "electron-forge make --platform=linux --targets=electron-forge-maker-appimage --arch x64", + "make:linux:arm": "electron-forge make --platform=linux --arch arm64", + "make:linux:arm:zip": "electron-forge make --platform=linux --targets=zip --arch arm64", + "make:linux:arm:deb": "electron-forge make --platform=linux --targets=deb --arch arm64", + "make:linux:arm:rpm": "electron-forge make --platform=linux --targets=rpm --arch arm64", + "make:linux:arm:appimage": "electron-forge make --platform=linux --targets=electron-forge-maker-appimage --arch arm64" + }, + "keywords": [], + "author": { + "name": "PrestonN", + "email": "FreeTubeApp@protonmail.com", "url": "https://github.com/FreeTubeApp/FreeTube" - } + }, + "license": "GPL-3.0-or-later", + "config": { + "forge": { + "make_targets": { + "win32": [ + "squirrel" + ], + "darwin": [ + "zip" + ], + "linux": [ + "deb", + "rpm", + "electron-forge-maker-appimage", + "zip" + ] + }, + "protocols": [ + { + "name": "freetube", + "role": "Viewer", + "schemes": [ + "freetube" + ] + } + ], + "electronPackagerConfig": { + "packageManager": "yarn", + "icon": "./src/icons/iconColor.icns" + }, + "electronWinstallerConfig": { + "name": "freetube", + "iconUrl": "https://raw.githubusercontent.com/FreeTubeApp/FreeTubeApp.github.io/master/images/iconColor.ico", + "setupIcon": "./src/icons/iconColor.ico" + }, + "electronInstallerDebian": { + "icon": "src/icons/iconColor.png" + }, + "repository": { + "type": "git", + "url": "https://github.com/FreeTubeApp/FreeTube" + } + } + }, + "devDependencies": { + "electron-forge": "^5.2.2", + "electron-forge-maker-appimage": "^20.14.4", + "electron-installer-flatpak": "^0.8.0", + "electron-installer-snap": "^2.0.1", + "electron-prebuilt-compile": "2.0.2", + "electron-winstaller": "^2.6.4" + }, + "dependencies": { + "autolinker": "^1.6.2", + "commonjs": "0.0.1", + "dateformat": "^3.0.3", + "electron-compile": "6.4.2", + "electron-squirrel-startup": "^1.0.0", + "github-version-checker": "^2.0.1", + "jquery": "^3.3.1", + "jsdom": "^11.11.0", + "mustache": "^2.3.0", + "nedb": "^1.8.0", + "opml-to-json": "0.0.3", + "tor-request": "^2.1.2", + "ytdl-core": "^0.20.4" } - }, - "devDependencies": { - "electron-forge": "^5.2.2", - "electron-forge-maker-appimage": "^20.14.4", - "electron-installer-flatpak": "^0.8.0", - "electron-installer-snap": "^2.0.1", - "electron-prebuilt-compile": "2.0.2", - "electron-winstaller": "^2.6.4" - }, - "dependencies": { - "autolinker": "^1.6.2", - "dateformat": "^3.0.3", - "electron-compile": "6.4.2", - "electron-squirrel-startup": "^1.0.0", - "github-version-checker": "^2.0.1", - "jquery": "^3.3.1", - "mustache": "^2.3.0", - "nedb": "^1.8.0", - "opml-to-json": "0.0.3", - "tor-request": "^2.1.2", - "ytdl-core": "^0.20.4" - } } diff --git a/src/index.html b/src/index.html index 2b6e3187a..caeb83d81 100644 --- a/src/index.html +++ b/src/index.html @@ -11,22 +11,11 @@ - - - - - - - - - - - Freetube Player -
+
@@ -47,23 +36,22 @@
- -   - +   +
    -
  •   Subscriptions
  • -
  •   Most Popular
  • -
  •   Saved
  • -
  •   History
  • +
  •   Subscriptions
  • +
  •   Most Popular
  • +
  •   Favorites
  • +
  •   History

    -
  •   Settings
  • -
  •   About
  • +
  •   Settings
  • +
  •   About

    @@ -71,7 +59,41 @@
+
+

+ Your Subscription list is currently empty. Start adding subscriptions + to see them here. +

+ +

+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + diff --git a/src/js/channels.js b/src/js/channels.js index b4e88ec05..866a6d765 100644 --- a/src/js/channels.js +++ b/src/js/channels.js @@ -1,75 +1,58 @@ /* -This file is part of FreeTube. + This file is part of FreeTube. -FreeTube is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + FreeTube is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. -FreeTube is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. -You should have received a copy of the GNU General Public License -along with FreeTube. If not, see . + You should have received a copy of the GNU General Public License + along with FreeTube. If not, see . */ /* -* File for all functions related specifically for channels. -*/ - -/*function getChannelThumbnail(channelId, callback) { - let url = ''; - - youtubeAPI('channels', { - 'id': channelId, - 'part': 'snippet', - }, function (data){ - callback(data.items[0].snippet.thumbnails.high.url); - }); -}*/ + * File for all functions related specifically for channels. + */ /** -* Display a channel page, showing latest uploads. -* -* @param {string} channelId - The channel ID to display. -* -* @return {Void} -*/ + * Display a channel page, showing latest uploads. + * + * @param {string} channelId - The channel ID to display. + * + * @return {Void} + */ function goToChannel(channelId) { - event.stopPropagation(); - clearMainContainer(); - startLoadingAnimation(); - let subButtonText; + headerView.title = 'Latest Uploads'; + hideViews(); + loadingView.seen = true; + // Setting subButtonText here as Mustache templates are logic-less. isSubscribed(channelId).then((subscribed) => { - subButtonText = (subscribed ? "UNSUBSCRIBE" : "SUBSCRIBE"); + channelView.subButtonText = (subscribed ? "UNSUBSCRIBE" : "SUBSCRIBE"); }); // Grab general channel information youtubeAPI('channels', { part: 'snippet,brandingSettings,statistics', id: channelId, - }, function (data){ + }, (data) => { const channelData = data.items[0]; - const channelViewTemplate = require('./templates/channelView.html'); - mustache.parse(channelViewTemplate); - const rendered = mustache.render(channelViewTemplate, { - channelId: channelId, - channelName: channelData.brandingSettings.channel.title, - channelBanner: channelData.brandingSettings.image.bannerImageUrl, - channelImage: channelData.snippet.thumbnails.high.url, - subCount: channelData.statistics.subscriberCount.toLocaleString(), //toLocaleString adds commas as thousands separators - channelDescription: autolinker.link(channelData.brandingSettings.channel.description), //autolinker makes URLs clickable - subButtonText: subButtonText, - }); - $('#main').html(rendered); - stopLoadingAnimation(); + channelView.id = channelId; + channelView.name = channelData.brandingSettings.channel.title; + channelView.banner = channelData.brandingSettings.image.bannerImageUrl; + channelView.icon = channelData.snippet.thumbnails.high.url; + channelView.subCount = channelData.statistics.subscriberCount.toLocaleString(); //toLocaleString adds commas as thousands separators + channelView.description = autolinker.link(channelData.brandingSettings.channel.description); //autolinker makes URLs clickable + // Grab the channel's latest uploads. API forces a max of 50. youtubeAPI('search', { @@ -79,12 +62,40 @@ function goToChannel(channelId) { maxResults: 50, order: 'date', }, function (data) { - // Display recent uploads to #main let grabDuration = getDuration(data.items); grabDuration.then((videoList) => { + channelVideosView.videoList = []; + + if (subscriptionView.seen === false && aboutView.seen === false && headerView.seen === false && searchView.seen === false && settingsView.seen === false && popularView.seen === false && savedView.seen === false && historyView.seen === false) { + channelVideosView.seen = true; + channelView.seen = true; + } + else{ + return; + } + + loadingView.seen = false; videoList.items.forEach((video) => { - displayVideo(video); + displayVideo(video, 'channel'); + }); + + // Grab the channel's latest uploads. API forces a max of 50. + youtubeAPI('search', { + part: 'snippet', + channelId: channelId, + type: 'video', + maxResults: 50, + order: 'date', + }, function (data) { + // Display recent uploads to #main + let grabDuration = getDuration(data.items); + + grabDuration.then((videoList) => { + videoList.items.forEach((video) => { + displayVideo(video); + }); + }); }); }); }); diff --git a/src/js/db.js b/src/js/db.js new file mode 100644 index 000000000..2a8fb1018 --- /dev/null +++ b/src/js/db.js @@ -0,0 +1,40 @@ +/* +This file is part of FreeTube. + +FreeTube is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +FreeTube is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with FreeTube. If not, see . +*/ + +const electron = require('electron'); +const Datastore = require('nedb'); // database logic +const localDataStorage = electron.remote.app.getPath('userData'); // Grabs the userdata directory based on the user's OS + +const subDb = new Datastore({ + filename: localDataStorage + '/subscriptions.db', + autoload: true +}); + +const historyDb = new Datastore({ + filename: localDataStorage + '/videohistory.db', + autoload: true +}); + +const savedVidsDb = new Datastore({ + filename: localDataStorage + '/savedvideos.db', + autoload: true +}); + +const settingsDb = new Datastore({ + filename: localDataStorage + '/settings.db', + autoload: true +}); diff --git a/src/js/events.js b/src/js/events.js index ae5fd4e69..c49b04875 100644 --- a/src/js/events.js +++ b/src/js/events.js @@ -1,22 +1,20 @@ - /* -This file is part of FreeTube. +/* + This file is part of FreeTube. -FreeTube is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + FreeTube is free software: you can redistribute it and/or modify + sit under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. -FreeTube is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. -You should have received a copy of the GNU General Public License -along with FreeTube. If not, see . + You should have received a copy of the GNU General Public License + along with FreeTube. If not, see . */ - - /* * File for events within application. Work needs to be done throughout the application * to use this style more. Please use this style going forward if possible. @@ -26,185 +24,179 @@ along with FreeTube. If not, see . * Event when user clicks comment box, * and wants to show/display comments for the user. */ -let showComments = function(event) { - let comments = $('#comments'); +let showComments = function (event) { + let comments = $('#comments'); - if (comments.css('display') === 'none') { - comments.attr('loaded', 'true'); + if (comments.css('display') === 'none') { + comments.attr('loaded', 'true'); - youtubeAPI('commentThreads', { - 'videoId': $('#comments').attr('data-video-id'), - 'part': 'snippet,replies', - 'maxResults': 100, - }, function (data){ - let comments = []; - let items = data.items; + youtubeAPI('commentThreads', { + 'videoId': $('#comments').attr('data-video-id'), + 'part': 'snippet,replies', + 'maxResults': 100, + }, function (data) { + let comments = []; + let items = data.items; - items.forEach((object) => { - let snippet = object.snippet.topLevelComment.snippet; + items.forEach((object) => { + let snippet = object.snippet.topLevelComment.snippet; - snippet.publishedAt = dateFormat(new Date(snippet.publishedAt), "mmm dS, yyyy"); + snippet.publishedAt = dateFormat(new Date(snippet.publishedAt), "mmm dS, yyyy"); - comments.push(snippet); - }) - const commentsTemplate = require('./templates/comments.html'); - const html = mustache.render(commentsTemplate, { - comments: comments, - }); - $('#comments').html(html); - }); + comments.push(snippet); + }) + const commentsTemplate = require('./templates/comments.html'); + const html = mustache.render(commentsTemplate, { + comments: comments, + }); + $('#comments').html(html); + }); - comments.show(); - } else { - comments.hide(); - } + comments.show(); + } else { + comments.hide(); + } }; /** * Play / Pause the video player upon click. */ -let playPauseVideo = function(event) { - let el = event.currentTarget; - el.paused ? el.play() : el.pause(); +let playPauseVideo = function (event) { + let el = event.currentTarget; + el.paused ? el.play() : el.pause(); }; -$('.videoPlayer').keypress((event) => { - console.log(event.which); -}); -let videoShortcutHandler = function(event) { - console.log(event.which); - let videoPlayer = $('.videoPlayer').get(0); - if (typeof(videoPlayer) !== 'undefined' && !$('#jumpToInput').is(':focus') && !$('#search').is(':focus')){ - switch (event.which) { - case 32: - // Space Bar - event.preventDefault(); - videoPlayer.paused ? videoPlayer.play() : videoPlayer.pause(); - break; - case 74: - // J Key - event.preventDefault(); - changeDurationBySeconds(-10); - break; - case 75: - // K Key - event.preventDefault(); - videoPlayer.paused ? videoPlayer.play() : videoPlayer.pause(); - break; - case 76: - // L Key - event.preventDefault(); - changeDurationBySeconds(10); - break; - case 70: - // F Key - event.preventDefault(); - videoPlayer.webkitRequestFullscreen(); - break; - case 77: - // M Key - event.preventDefault(); - let volume = videoPlayer.volume; - console.log(volume); - if (volume > 0){ - changeVolume(-1); +let videoShortcutHandler = function (event) { + + let videoPlayer = $('.videoPlayer').get(0); + if (typeof (videoPlayer) !== 'undefined' && !$('#jumpToInput').is(':focus') && !$('#search').is(':focus')) { + switch (event.which) { + case 32: + // Space Bar + event.preventDefault(); + videoPlayer.paused ? videoPlayer.play() : videoPlayer.pause(); + break; + case 74: + // J Key + event.preventDefault(); + changeDurationBySeconds(-10); + break; + case 75: + // K Key + event.preventDefault(); + videoPlayer.paused ? videoPlayer.play() : videoPlayer.pause(); + break; + case 76: + // L Key + event.preventDefault(); + changeDurationBySeconds(10); + break; + case 70: + // F Key + event.preventDefault(); + videoPlayer.webkitRequestFullscreen(); + break; + case 77: + // M Key + event.preventDefault(); + let volume = videoPlayer.volume; + + if (volume > 0) { + changeVolume(-1); + } else { + changeVolume(1); + } + break; + case 67: + // C Key + let subtitleMode = $('.videoPlayer').get(0).textTracks[0].mode; + if (subtitleMode === 'hidden') { + $('.videoPlayer').get(0).textTracks[0].mode = 'showing' + } else { + $('.videoPlayer').get(0).textTracks[0].mode = 'hidden' + } + break; + case 38: + // Up Arrow Key + event.preventDefault(); + changeVolume(0.05); + break; + case 40: + // Down Arrow Key + event.preventDefault(); + changeVolume(-0.05); + break; + case 37: + // Left Arrow Key + event.preventDefault(); + changeDurationBySeconds(-5); + break; + case 39: + // Right Arrow Key + event.preventDefault(); + changeDurationBySeconds(5); + break; + case 49: + // 1 Key + event.preventDefault(); + changeDurationByPercentage(0.1); + break; + case 50: + // 2 Key + event.preventDefault(); + changeDurationByPercentage(0.2); + break; + case 51: + // 3 Key + event.preventDefault(); + changeDurationByPercentage(0.3); + break; + case 52: + // 4 Key + event.preventDefault(); + changeDurationByPercentage(0.4); + break; + case 53: + // 5 Key + event.preventDefault(); + changeDurationByPercentage(0.5); + break; + case 54: + // 6 Key + event.preventDefault(); + changeDurationByPercentage(0.6); + break; + case 55: + // 7 Key + event.preventDefault(); + changeDurationByPercentage(0.7); + break; + case 56: + // 8 Key + event.preventDefault(); + changeDurationByPercentage(0.8); + break; + case 57: + // 9 Key + event.preventDefault(); + changeDurationByPercentage(0.9); + break; + case 48: + // 0 Key + event.preventDefault(); + changeDurationByPercentage(0); + break; } - else{ - changeVolume(1); - } - break; - case 67: - // C Key - let subtitleMode = $('.videoPlayer').get(0).textTracks[0].mode; - if (subtitleMode === 'hidden'){ - $('.videoPlayer').get(0).textTracks[0].mode = 'showing' - } - else{ - $('.videoPlayer').get(0).textTracks[0].mode = 'hidden' - } - break; - case 38: - // Up Arrow Key - event.preventDefault(); - changeVolume(0.05); - break; - case 40: - // Down Arrow Key - event.preventDefault(); - changeVolume(-0.05); - break; - case 37: - // Left Arrow Key - event.preventDefault(); - changeDurationBySeconds(-5); - break; - case 39: - // Right Arrow Key - event.preventDefault(); - changeDurationBySeconds(5); - break; - case 49: - // 1 Key - event.preventDefault(); - changeDurationByPercentage(0.1); - break; - case 50: - // 2 Key - event.preventDefault(); - changeDurationByPercentage(0.2); - break; - case 51: - // 3 Key - event.preventDefault(); - changeDurationByPercentage(0.3); - break; - case 52: - // 4 Key - event.preventDefault(); - changeDurationByPercentage(0.4); - break; - case 53: - // 5 Key - event.preventDefault(); - changeDurationByPercentage(0.5); - break; - case 54: - // 6 Key - event.preventDefault(); - changeDurationByPercentage(0.6); - break; - case 55: - // 7 Key - event.preventDefault(); - changeDurationByPercentage(0.7); - break; - case 56: - // 8 Key - event.preventDefault(); - changeDurationByPercentage(0.8); - break; - case 57: - // 9 Key - event.preventDefault(); - changeDurationByPercentage(0.9); - break; - case 48: - // 0 Key - event.preventDefault(); - changeDurationByPercentage(0); - break; } - } }; -let fullscreenVideo = function(event){ - if (document.webkitFullscreenElement !== null){ - document.webkitExitFullscreen(); - } - else{ - $('.videoPlayer').get(0).webkitRequestFullscreen(); - } +let fullscreenVideo = function (event) { + if (document.webkitFullscreenElement !== null) { + document.webkitExitFullscreen(); + } else { + $('.videoPlayer').get(0).webkitRequestFullscreen(); + } } /** @@ -212,6 +204,7 @@ let fullscreenVideo = function(event){ * Bind click events * -------------------------- */ + $(document).on('click', '#showComments', showComments); $(document).on('click', '.videoPlayer', playPauseVideo); @@ -221,3 +214,17 @@ $(document).on('dblclick', '.videoPlayer', fullscreenVideo); $(document).on('keydown', videoShortcutHandler); $(document).on('click', '#confirmNo', hideConfirmFunction); + +// Open links externally by default +$(document).on('click', 'a[href^="http"]', (event) => { + let el = event.currentTarget; + event.preventDefault(); + shell.openExternal(el.href); +}); + +// Open links externally on middle click. +$(document).on('auxclick', 'a[href^="http"]', (event) => { + let el = event.currentTarget; + event.preventDefault(); + shell.openExternal(el.href); +}); diff --git a/src/js/general.js b/src/js/general.js new file mode 100644 index 000000000..d8fa6cebb --- /dev/null +++ b/src/js/general.js @@ -0,0 +1,39 @@ +/* + This file is part of FreeTube. + + FreeTube is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FreeTube. If not, see . +*/ + +let ft = {}; + +/** + * + * Use this function instead of console.log. + * This function logs the date, time and presents the information in a readable format + * + * @param {*} data + * + * @returns {Void} + */ +ft.log = function (...data) { + let currentTime = new Date(); + let time = currentTime.getDate() + "/" + + (currentTime.getMonth() + 1) + "/" + + currentTime.getFullYear() + "@" + + currentTime.getHours() + ":" + + currentTime.getMinutes() + ":" + + currentTime.getSeconds(); + + console.log('[' + time + '] ' + '[FREETUBE]', data); +} \ No newline at end of file diff --git a/src/js/history.js b/src/js/history.js index 0b8636afd..1df7585e9 100644 --- a/src/js/history.js +++ b/src/js/history.js @@ -1,16 +1,13 @@ /* This file is part of FreeTube. - FreeTube is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - FreeTube is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License along with FreeTube. If not, see . */ @@ -54,8 +51,8 @@ function removeFromHistory(videoId){ * @return {Void} */ function showHistory(){ - clearMainContainer(); - startLoadingAnimation(); + //clearMainContainer(); + //startLoadingAnimation(); console.log('checking history'); let videoList = ''; @@ -80,15 +77,15 @@ function showHistory(){ id: videoList, maxResults: 50, }, function (data) { - createVideoListContainer('Watch History:'); let grabDuration = getDuration(data.items); grabDuration.then((videoList) => { + historyView.videoList = []; + loadingView.seen = false; videoList.items.forEach((video) => { displayVideo(video, 'history'); }); }); - stopLoadingAnimation() }); }); } diff --git a/src/js/init.js b/src/js/init.js index 2be8daaa9..bedaca721 100644 --- a/src/js/init.js +++ b/src/js/init.js @@ -1,28 +1,34 @@ /* -This file is part of FreeTube. + This file is part of FreeTube. -FreeTube is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + FreeTube is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. -FreeTube is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. -You should have received a copy of the GNU General Public License -along with FreeTube. If not, see . + You should have received a copy of the GNU General Public License + along with FreeTube. If not, see . */ /* -* File used to initializing the application -*/ -const {app, BrowserWindow, dialog, protocol} = require('electron'); + * File used to initializing the application + */ +const { + app, + BrowserWindow, + dialog, + protocol +} = require('electron'); const path = require('path'); const url = require('url'); + let win; protocol.registerStandardSchemes(['freetube']); @@ -30,103 +36,144 @@ protocol.registerStandardSchemes(['freetube']); app.setAsDefaultProtocolClient('freetube'); const isSecondInstance = app.makeSingleInstance((commandLine, workingDirectory) => { - // Someone tried to run a second instance, we should focus our window. - if (win) { - if (win.isMinimized()) win.restore() - win.focus() + // Someone tried to run a second instance, we should focus our window. + if (win) { + if (win.isMinimized()) win.restore() + win.focus() - win.webContents.send('ping', commandLine) - } + win.webContents.send('ping', commandLine) + } }); -if(require('electron-squirrel-startup') || isSecondInstance) app.quit(); +if (require('electron-squirrel-startup') || isSecondInstance) app.quit(); /** * Initialize the Electron application * 1. create the browser window * 2. load the index.html */ -let init = function() { - const Menu = require('electron').Menu; +let init = function () { + const Menu = require('electron').Menu; - win = new BrowserWindow({width: 1200, height: 800, autoHideMenuBar: true}); + win = new BrowserWindow({ + width: 1200, + height: 800, + autoHideMenuBar: true + }); - win.loadURL(url.format({ - pathname: path.join(__dirname, '../index.html'), - protocol: 'file:', - slashes: true, - })); + win.loadURL(url.format({ + pathname: path.join(__dirname, '../index.html'), + protocol: 'file:', + slashes: true, + })); - if (process.env = 'development') { - //win.webContents.openDevTools();ff - } - - win.on('closed', () => { - win = null; - }); - - const template = [ - { - label: 'File', - submenu: [ - {role: 'quit'} - ] - }, - { - label: 'Edit', - submenu: [ - {role: 'cut'}, - {role: 'copy', accelerator: "CmdOrCtrl+C", selector: "copy:" }, - {role: 'paste', accelerator: "CmdOrCtrl+V", selector: "paste:" }, - {role: 'pasteandmatchstyle'}, - {role: 'delete'}, - {role: 'selectall'} - ] - }, - { - label: 'View', - submenu: [ - {role: 'reload'}, - {role: 'forcereload'}, - {role: 'toggledevtools'}, - {type: 'separator'}, - {role: 'resetzoom'}, - {role: 'zoomin'}, - {role: 'zoomout'}, - {type: 'separator'}, - {role: 'togglefullscreen'} - ] - }, - { - role: 'window', - submenu: [ - {role: 'minimize'}, - {role: 'close'} - ] + if (process.env = 'development') { + //win.webContents.openDevTools();ff } - ]; - const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); + win.on('closed', () => { + win = null; + }); + + const template = [{ + label: 'File', + submenu: [{ + label: 'Open New Window', + click () { init() } + }, + {role: 'quit'} + ] + }, + { + label: 'Edit', + submenu: [{ + role: 'cut' + }, + { + role: 'copy', + accelerator: "CmdOrCtrl+C", + selector: "copy:" + }, + { + role: 'paste', + accelerator: "CmdOrCtrl+V", + selector: "paste:" + }, + { + role: 'pasteandmatchstyle' + }, + { + role: 'delete' + }, + { + role: 'selectall' + } + ] + }, + { + label: 'View', + submenu: [{ + role: 'reload' + }, + { + role: 'forcereload' + }, + { + role: 'toggledevtools' + }, + { + type: 'separator' + }, + { + role: 'resetzoom' + }, + { + role: 'zoomin' + }, + { + role: 'zoomout' + }, + { + type: 'separator' + }, + { + role: 'togglefullscreen' + } + ] + }, + { + role: 'window', + submenu: [{ + role: 'minimize' + }, + { + role: 'close' + } + ] + } + ]; + + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); }; /** * Quit the application */ -let allWindowsClosed = function() { - win.webContents.session.clearStorageData([], (data) => {}); - win.webContents.session.clearCache((data) => {}); - app.quit(); +let allWindowsClosed = function () { + win.webContents.session.clearStorageData([], (data) => {}); + win.webContents.session.clearCache((data) => {}); + app.quit(); }; /** * On Mac, when dock icon is clicked, * create a new window and launch the editor */ -let active = function() { - if (win === null) { - init(); - } +let active = function () { + if (win === null) { + init(); + } }; /** diff --git a/src/js/layout.js b/src/js/layout.js index 75734d019..aa7d5c13a 100644 --- a/src/js/layout.js +++ b/src/js/layout.js @@ -1,18 +1,18 @@ /* -This file is part of FreeTube. + This file is part of FreeTube. -FreeTube is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + FreeTube is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. -FreeTube is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. -You should have received a copy of the GNU General Public License -along with FreeTube. If not, see . + You should have received a copy of the GNU General Public License + along with FreeTube. If nsot, see . */ @@ -23,103 +23,60 @@ along with FreeTube. If not, see . */ // Add general variables. Please put all require statements here. -const Datastore = require('nedb'); // database logic window.$ = window.jQuery = require('jquery'); const mustache = require('mustache'); // templating const dateFormat = require('dateformat'); // formatting dates -//const RxPlayer = require('rx-player'); // formatting dates - // Used for finding links within text and making them clickable. Used mostly for video descriptions. const autolinker = require('autolinker'); -const electron = require('electron'); const protocol = electron.remote.protocol; // Used for getting the user's subscriptions. Can probably remove this when that function // is rewritten. -//const asyncLoop = require('node-async-loop'); -//const youtubedl = require('youtube-dl'); const ytdl = require('ytdl-core'); const shell = electron.shell; // Used to open external links into the user's native browser. -const localDataStorage = electron.remote.app.getPath('userData'); // Grabs the userdata directory based on the user's OS const clipboard = electron.clipboard; const getOpml = require('opml-to-json'); // Gets the file type for imported files. const fs = require('fs'); // Used to read files. Specifically in the settings page. const tor = require('tor-request'); let currentTheme = ''; -let apiKey; let useTor = false; let dialog = electron.remote.dialog; // Used for opening file browser to export / import subscriptions. let toastTimeout; // Timeout for toast notifications. let mouseTimeout; // Timeout for hiding the mouse cursor on video playback -require.extensions['.html'] = function(module, filename) { - module.exports = fs.readFileSync(filename, 'utf8'); +require.extensions['.html'] = function (module, filename) { + module.exports = fs.readFileSync(filename, 'utf8'); }; -const subDb = new Datastore({ - filename: localDataStorage + '/subscriptions.db', - autoload: true -}); - -const historyDb = new Datastore({ - filename: localDataStorage + '/videohistory.db', - autoload: true -}); - -const savedVidsDb = new Datastore({ - filename: localDataStorage + '/savedvideos.db', - autoload: true -}); - -const settingsDb = new Datastore({ - filename: localDataStorage + '/settings.db', - autoload: true -}); - // Grabs the default settings from the settings database file. Makes defaults if // none are found. -checkDefaultSettings(); -require('electron').ipcRenderer.on('ping', function(event, message) { - console.log(message); +electron.ipcRenderer.on('ping', function(event, message) { + ft.log(message); let url = message[1].replace('freetube://', ''); parseSearchText(url); - console.log(message); + ft.log(message); }); -// Open links externally by default -$(document).on('click', 'a[href^="http"]', (event) => { - let el = event.currentTarget; - event.preventDefault(); - shell.openExternal(el.href); -}); - -// Open links externally on middle click. -$(document).on('auxclick', 'a[href^="http"]', (event) => { - let el = event.currentTarget; - event.preventDefault(); - shell.openExternal(el.href); -}); - - $(document).ready(() => { - const searchBar = document.getElementById('search'); - const jumpToInput = document.getElementById('jumpToInput'); + const searchBar = document.getElementById('search'); + const jumpToInput = document.getElementById('jumpToInput'); - // Displays the list of subscriptions in the side bar. - displaySubs(); + // Displays the list of subscriptions in the side bar. + displaySubs(); - // Allow user to use the 'enter' key to search for a video. - searchBar.onkeypress = (e) => { - if (e.keyCode === 13) { - parseSearchText(); - } - }; + // Allow user to use the 'enter' key to search for a video. + searchBar.onkeypress = (e) => { + if (e.keyCode === 13) { + parseSearchText(); + } + }; // Display subscriptions upon the app opening up. May allow user to specify. // Home page in the future. + loadingView.seen = true; loadSubscriptions(); }); @@ -129,94 +86,16 @@ $(document).ready(() => { * @return {Void} */ function toggleSideNavigation() { - const sideNav = document.getElementById('sideNav'); - const mainContainer = document.getElementById('main'); + const sideNav = document.getElementById('sideNav'); + const mainContainer = document.getElementById('main'); - if (sideNav.style.display === 'none') { - sideNav.style.display = 'inline'; - mainContainer.style.marginLeft = '250px'; - } else { - sideNav.style.display = 'none'; - mainContainer.style.marginLeft = '0px'; - } -} - -/** - * Clears out the #main container to allow other information to be shown. - * - * @return {Void} - */ -function clearMainContainer() { - const container = document.getElementById('main'); - container.innerHTML = ''; - hideConfirmFunction(); -} - -function startLoadingAnimation() { - const loading = document.getElementById('loading'); - const sideNavDisabled = document.getElementById('sideNavDisabled'); - const searchBar = document.getElementById('search'); - - loading.style.display = 'inherit'; - if(sideNavDisabled !== null){ - sideNavDisabled.style.display = 'inherit'; - } - - searchBar.disabled = true; -} - -function stopLoadingAnimation() { - const loading = document.getElementById('loading'); - const sideNavDisabled = document.getElementById('sideNavDisabled'); - const searchBar = document.getElementById('search'); - - loading.style.display = 'none'; - if(sideNavDisabled !== null){ - sideNavDisabled.style.display = 'none'; - } - - searchBar.disabled = false; -} - -/** - * Creates a div container in #main meant to be a container for video lists. - * - * @param {string} headerLabel - The header of the container. Not used for showing video recommendations. - * - * @return {Void} - */ -function createVideoListContainer(headerLabel = '') { - const videoListContainer = document.createElement("div"); - videoListContainer.id = 'videoListContainer'; - let headerSpacer; - if (headerLabel != '') { - const headerElement = document.createElement("h2"); - headerElement.innerHTML = headerLabel; - headerElement.style.marginLeft = '15px'; - headerElement.appendChild(document.createElement("hr")); - videoListContainer.appendChild(headerElement); - } - document.getElementById("main").appendChild(videoListContainer); -} - -/** - * Displays the about page to #main - * - * @return {Void} - */ -function showAbout() { - // Remove current information and display loading animation - clearMainContainer(); - startLoadingAnimation(); - - const aboutTemplate = require('./templates/about.html') - mustache.parse(aboutTemplate); - $('#main').html( - mustache.render(aboutTemplate, { - versionNumber: require('electron').remote.app.getVersion(), - }) - ); - stopLoadingAnimation(); + if (sideNav.style.display === 'none') { + sideNav.style.display = 'inline'; + mainContainer.style.marginLeft = '250px'; + } else { + sideNav.style.display = 'none'; + mainContainer.style.marginLeft = '0px'; + } } /** @@ -228,18 +107,18 @@ function showAbout() { * @return {Void} */ function showToast(message) { - let toast = document.getElementById('toast'); - let toastMessage = document.getElementById('toastMessage'); + let toast = document.getElementById('toast'); + let toastMessage = document.getElementById('toastMessage'); - // If a toast message is already being displayed, this will remove the previous timer that was set. - clearTimeout(toastTimeout); + // If a toast message is already being displayed, this will remove the previous timer that was set. + clearTimeout(toastTimeout); - toastMessage.innerHTML = message; - toast.style.visibility = 'visible'; - toast.style.opacity = 0.9; + toastMessage.innerHTML = message; + toast.style.visibility = 'visible'; + toast.style.opacity = 0.9; - // Set the timer for the toast to be removed. - toastTimeout = window.setTimeout(hideToast, 5000); + // Set the timer for the toast to be removed. + toastTimeout = window.setTimeout(hideToast, 5000); } /** @@ -248,9 +127,9 @@ function showToast(message) { * @return {Void} */ function hideToast() { - let toast = document.getElementById('toast'); - toast.style.opacity = 0; - toast.style.visibility = 'hidden'; + let toast = document.getElementById('toast'); + toast.style.opacity = 0; + toast.style.visibility = 'hidden'; } /** @@ -264,21 +143,20 @@ function hideToast() { * @return {Void} */ function confirmFunction(message, performFunction, parameters = '') { - let confirmContainer = document.getElementById('confirmFunction'); - let confirmMessage = document.getElementById('confirmMessage'); + let confirmContainer = document.getElementById('confirmFunction'); + let confirmMessage = document.getElementById('confirmMessage'); - confirmMessage.innerHTML = message; - confirmContainer.style.visibility = 'visible'; + confirmMessage.innerHTML = message; + confirmContainer.style.visibility = 'visible'; - $(document).on('click', '#confirmYes', (event) => { - if(parameters != ''){ - performFunction(parameters); - } - else{ - performFunction(); - } - hideConfirmFunction(); - }); + $(document).on('click', '#confirmYes', (event) => { + if (parameters != '') { + performFunction(parameters); + } else { + performFunction(); + } + hideConfirmFunction(); + }); } /** @@ -287,8 +165,8 @@ function confirmFunction(message, performFunction, parameters = '') { * @return {Void} */ function hideConfirmFunction() { - let confirmContainer = document.getElementById('confirmFunction'); - confirmContainer.style.visibility = 'hidden'; + let confirmContainer = document.getElementById('confirmFunction'); + confirmContainer.style.visibility = 'hidden'; } /** @@ -298,11 +176,11 @@ function hideConfirmFunction() { * @return {Void} */ function hideMouseTimeout() { - $('.videoPlayer')[0].style.cursor = 'default'; - clearTimeout(mouseTimeout); - mouseTimeout = window.setTimeout(function() { - $('.videoPlayer')[0].style.cursor = 'none'; - }, 3150); + $('.videoPlayer')[0].style.cursor = 'default'; + clearTimeout(mouseTimeout); + mouseTimeout = window.setTimeout(function () { + $('.videoPlayer')[0].style.cursor = 'none'; + }, 3150); } /** @@ -311,14 +189,14 @@ function hideMouseTimeout() { * @return {Void} */ function removeMouseTimeout() { - $('.videoPlayer')[0].style.cursor = 'default'; - clearTimeout(mouseTimeout); + $('.videoPlayer')[0].style.cursor = 'default'; + clearTimeout(mouseTimeout); } function showVideoOptions(element) { - if (element.nextElementSibling.style.display == 'none' || element.nextElementSibling.style.display == '') { - element.nextElementSibling.style.display = 'inline-block' - } else { - element.nextElementSibling.style.display = 'none' - } -} + if (element.nextElementSibling.style.display == 'none' || element.nextElementSibling.style.display == '') { + element.nextElementSibling.style.display = 'inline-block' + } else { + element.nextElementSibling.style.display = 'none' + } +} \ No newline at end of file diff --git a/src/js/models/comment.model.js b/src/js/models/comment.model.js index 854f85b31..79ffd5570 100644 --- a/src/js/models/comment.model.js +++ b/src/js/models/comment.model.js @@ -1,18 +1,18 @@ /* -This file is part of FreeTube. + This file is part of FreeTube. -FreeTube is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + FreeTube is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. -FreeTube is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. -You should have received a copy of the GNU General Public License -along with FreeTube. If not, see . + You should have received a copy of the GNU General Public License + along with FreeTube. If not, see . */ @@ -21,9 +21,9 @@ along with FreeTube. If not, see . * Video Comment Model */ export class comment { - authorDisplayName: string; - authorProfileImageUrl: string; - authorChannelId: string; - textDisplay: string; - publishedAt: string; -} + authorDisplayName; + authorProfileImageUrl; + authorChannelId; + textDisplay; + publishedAt; +} \ No newline at end of file diff --git a/src/js/models/commentThread.model.js b/src/js/models/commentThread.model.js index e54de7878..436956b3d 100644 --- a/src/js/models/commentThread.model.js +++ b/src/js/models/commentThread.model.js @@ -1,18 +1,18 @@ /* -This file is part of FreeTube. + This file is part of FreeTube. -FreeTube is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + FreeTube is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. -FreeTube is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. -You should have received a copy of the GNU General Public License -along with FreeTube. If not, see . + You should have received a copy of the GNU General Public License + along with FreeTube. If not, see . */ diff --git a/src/js/player.js b/src/js/player.js index fccc516d4..e62b7aedf 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -1,18 +1,18 @@ /* -This file is part of FreeTube. + This file is part of FreeTube. -FreeTube is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + FreeTube is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. -FreeTube is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. -You should have received a copy of the GNU General Public License -along with FreeTube. If not, see . + You should have received a copy of the GNU General Public License + along with FreeTube. If not, see . */ @@ -28,128 +28,101 @@ along with FreeTube. If not, see . * * @return {Void} */ -function playVideo(videoId, videoThumbnail = '', useWindowPlayer = false) { - if (useWindowPlayer === false){ - clearMainContainer(); - startLoadingAnimation(); - } - else{ - showToast('Getting video information. Please wait...') - } +function playVideo(videoId) { + hideViews(); - let subscribeText = ''; - let savedText = ''; - let savedIconClass = ''; - let savedIconColor = ''; - let video480p; - let video720p; - let videoSubtitles = ''; - let subtitleHtml = ''; - let subtitleLabel; - let subtitleLanguage; - let subtitleCode; - let subtitleUrl; - let defaultUrl; - let defaultQuality; - let channelId; - let videoHtml; - let videoType = 'video'; - let embedPlayer = ""; - let useEmbedPlayer = false; - let validUrl; - let videoLikes; - let videoDislikes; - let totalLikes; - let likePercentage; + playerView.playerSeen = true; + playerView.videoId = videoId; + playerView.video480p = undefined; + playerView.video720p = undefined; + playerView.embededHtml = ""; - const checkSavedVideo = videoIsSaved(videoId); + const checkSavedVideo = videoIsSaved(videoId); // Change the save button icon and text depending on if the user has saved the video or not. checkSavedVideo.then((results) => { if (results === false) { - savedText = 'SAVE'; - savedIconClass = 'far unsaved'; + playerView.savedText = 'FAVORITE'; + playerView.savedIconType = 'far unsaved'; } else { - savedText = 'SAVED'; - savedIconClass = 'fas saved'; + playerView.savedText = 'FAVORITED'; + playerView.savedIconType = 'fas saved'; } }); - youtubeAPI('videos', { - part: 'statistics', - id: videoId, - }, function(data) { - console.log(data); + youtubeAPI('videos', { + part: 'statistics', + id: videoId, + }, function (data) { // Figure out the width for the like/dislike bar. - videoLikes = data['items'][0]['statistics']['likeCount']; - videoDislikes = data['items'][0]['statistics']['dislikeCount']; - totalLikes = parseInt(videoLikes) + parseInt(videoDislikes); - likePercentage = parseInt((videoLikes / totalLikes) * 100); + playerView.videoLikes = data['items'][0]['statistics']['likeCount']; + playerView.videoDislikes = data['items'][0]['statistics']['dislikeCount']; + let totalLikes = parseInt(playerView.videoLikes) + parseInt(playerView.videoDislikes); + playerView.likePercentage = parseInt((playerView.videoLikes / totalLikes) * 100); }); - /* - * FreeTube calls youtube-dl to grab the direct video URL. - */ - youtubedlGetInfo(videoId, (info) => { - console.log(info); + /* + * FreeTube calls youtube-dl to grab the direct video URL. + */ + youtubedlGetInfo(videoId, (info) => { - console.log(videoLikes); - - channelId = info['author']['id']; - let channelThumbnail = info['author']['avatar']; + playerView.videoTitle = info['title']; + playerView.channelName = info['author']['name']; + playerView.channelId = info['author']['id']; + playerView.channelIcon = info['author']['avatar']; let videoUrls = info['formats']; // Add commas to the video view count. - const videoViews = info['view_count'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + playerView.videoViews = info['view_count'].toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); - videoThumbnail = info['player_response']['videoDetails']['thumbnail']['thumbnails'][3]['url']; + playerView.videoThumbnail = info['player_response']['videoDetails']['thumbnail']['thumbnails'][3]['url']; // Format the date to a more readable format. let dateString = new Date(info['published']); dateString.setDate(dateString.getDate() + 1); - const publishedDate = dateFormat(dateString, "mmm dS, yyyy"); + playerView.publishedDate = dateFormat(dateString, "mmm dS, yyyy"); let description = info['description']; // Adds clickable links to the description. - description = autolinker.link(description); + playerView.description = autolinker.link(description); // Search through the returned object to get the 480p and 720p video URLs (If available) Object.keys(videoUrls).forEach((key) => { switch (videoUrls[key]['itag']) { case '18': - video480p = decodeURIComponent(videoUrls[key]['url']); - console.log(video480p); + playerView.video480p = decodeURIComponent(videoUrls[key]['url']); + //console.log(video480p); break; case '22': - video720p = decodeURIComponent(videoUrls[key]['url']); - console.log(video720p); + playerView.video720p = decodeURIComponent(videoUrls[key]['url']); + //console.log(video720p); break; } }); + let useEmbedPlayer = false; + // Default to the embeded player if the URLs cannot be found. - if (typeof(video720p) === 'undefined' && typeof(video480p) === 'undefined') { + if (typeof(playerView.video720p) === 'undefined' && typeof(playerView.video480p) === 'undefined') { + //useEmbedPlayer = true; + playerView.currentQuality = 'EMBED'; + playerView.playerSeen = false; useEmbedPlayer = true; - defaultQuality = 'EMBED'; - videoHtml = embedPlayer.replace(/\"\;/g, '"'); showToast('Unable to get video file. Reverting to embeded player.'); } else if (typeof(video720p) === 'undefined' && typeof(video480p) !== 'undefined') { // Default to the 480p video if the 720p URL cannot be found. - defaultUrl = video480p; - defaultQuality = '480p'; + playerView.videoUrl = playerView.video480p; + playerView.currentQuality = '480p'; } else { // Default to the 720p video. - defaultUrl = video720p; - defaultQuality = '720p'; - // Force the embeded player if needed. - //videoHtml = embedPlayer; + playerView.videoUrl = playerView.video720p; + playerView.currentQuality = '720p'; } if (!useEmbedPlayer) { - //videoHtml = ''; - } + playerView.subtitleHtml = videoHtml; +} - const checkSubscription = isSubscribed(channelId); - // Change the subscribe button text depending on if the user has subscribed to the channel or not. + const checkSubscription = isSubscribed(playerView.channelId); checkSubscription.then((results) => { - const subscribeButton = document.getElementById('subscribeButton'); - if (results === false) { if (subscribeButton != null) { - subscribeButton.innerHTML = 'SUBSCRIBE'; + playerView.subscribedText = 'SUBSCRIBE'; } } else { if (subscribeButton != null) { - subscribeButton.innerHTML = 'UNSUBSCRIBE'; + playerView.subscribedText = 'UNSUBSCRIBE'; } } }); - const playerTemplate = require('./templates/player.html') - mustache.parse(playerTemplate); - const rendered = mustache.render(playerTemplate, { - videoQuality: defaultQuality, - subtitleHtml: videoHtml, - defaultUrl: defaultUrl, - videoTitle: info['title'], - videoViews: videoViews, - videoThumbnail: videoThumbnail, - channelName: info['author']['name'], - videoLikes: videoLikes, - videoDislikes: videoDislikes, - likePercentage: likePercentage, - videoId: videoId, - channelId: channelId, - channelIcon: channelThumbnail, - publishedDate: publishedDate, - description: description, - isSubscribed: subscribeText, - savedText: savedText, - savedIconClass: savedIconClass, - savedIconColor: savedIconColor, - video480p: video480p, - video720p: video720p, - embedPlayer: embedPlayer, - }); + showVideoRecommendations(videoId); - // Add the video to the user's history - addToHistory(videoId); + loadingView.seen = false; - if (useWindowPlayer){ - // Create a new browser window. - const BrowserWindow = electron.remote.BrowserWindow; - - let newWindow = new BrowserWindow({ - width: 1200, - height: 700 - }); - - let playerWindowHeader = require('./templates/playerWindow.html'); - - mustache.parse(playerWindowHeader); - const playerHeaderRender = mustache.render(playerWindowHeader, { - videoId: videoId, - channelId: channelId - }); - - newWindow.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(playerHeaderRender + rendered), { - baseURLForDataURL: `file://${__dirname}/src` - }); + if (subscriptionView.seen === false && aboutView.seen === false && headerView.seen === false && searchView.seen === false && settingsView.seen === false && popularView.seen === false && savedView.seen === false && historyView.seen === false && channelView.seen === false && channelVideosView.seen === false) { + playerView.seen = true; } else{ - $('#main').html(rendered); - stopLoadingAnimation(); - - showVideoRecommendations(videoId); - - // Hide subtitles by default - if (typeof(info['subtitles']) !== 'undefined' && Object.keys(info['subtitles']).length > 0) { - let textTracks = $('.videoPlayer').get(0).textTracks; - Object.keys(textTracks).forEach((track) => { - textTracks[track].mode = 'hidden'; - }); - } + return; } - // Sometimes a video URL is found, but the video will not play. I believe the issue is - // that the video has yet to render for that quality, as the video will be available at a later time. - // This will check the URLs and switch video sources if there is an error. - //checkVideoUrls(video480p, video720p); + addToHistory(videoId); - window.setTimeout(checkVideoUrls, 5000, video480p, video720p); + // Hide subtitles by default + if (typeof(info['subtitles']) !== 'undefined' && Object.keys(info['subtitles']).length > 0) { + let textTracks = $('.videoPlayer').get(0).textTracks; + Object.keys(textTracks).forEach((track) => { + textTracks[track].mode = 'hidden'; + }); + } + window.setTimeout(checkVideoUrls, 5000, playerView.video480p, playerView.video720p); }); } @@ -272,40 +191,40 @@ function playVideo(videoId, videoThumbnail = '', useWindowPlayer = false) { * * @return {Void} */ -function openMiniPlayer(videoThumbnail) { - let lastTime; - let videoHtml; + function openMiniPlayer() { + let lastTime; + let videoHtml; - // Grabs whatever the HTML is for the current video player. Done this way to grab - // the HTML5 player (with varying qualities) as well as the YouTube embeded player. - if ($('.videoPlayer').length > 0) { - $('.videoPlayer').get(0).pause(); - lastTime = $('.videoPlayer').get(0).currentTime; - videoHtml = $('.videoPlayer').get(0).outerHTML; - } else { - videoHtml = $('iframe').get(0).outerHTML; - } + // Grabs whatever the HTML is for the current video player. Done this way to grab + // the HTML5 player (with varying qualities) as well as the YouTube embeded player. + if ($('.videoPlayer').length > 0) { + $('.videoPlayer').get(0).pause(); + lastTime = $('.videoPlayer').get(0).currentTime; + videoHtml = $('.videoPlayer').get(0).outerHTML; + } else { + videoHtml = $('iframe').get(0).outerHTML; + } - // Create a new browser window. - const BrowserWindow = electron.remote.BrowserWindow; + // Create a new browser window. + const BrowserWindow = electron.remote.BrowserWindow; - let miniPlayer = new BrowserWindow({ - width: 1200, - height: 710 - }); + let miniPlayer = new BrowserWindow({ + width: 1200, + height: 710 + }); - // Use the miniPlayer.html template. - $.get('templates/miniPlayer.html', (template) => { - mustache.parse(template); - const rendered = mustache.render(template, { - videoHtml: videoHtml, - videoThumbnail: videoThumbnail, - startTime: lastTime, - }); - // Render the template to the new browser window. - miniPlayer.loadURL("data:text/html;charset=utf-8," + encodeURI(rendered)); - }); -} + // Use the miniPlayer.html template. + $.get('templates/miniPlayer.html', (template) => { + mustache.parse(template); + const rendered = mustache.render(template, { + videoHtml: videoHtml, + videoThumbnail: playerView.thumbnail, + startTime: lastTime, + }); + // Render the template to the new browser window. + miniPlayer.loadURL("data:text/html;charset=utf-8," + encodeURI(rendered)); + }); + } /** * Change the quality of the current video. @@ -316,54 +235,54 @@ function openMiniPlayer(videoThumbnail) { * * @return {Void} */ -function changeQuality(videoHtml, qualityType, isEmbed = false) { +function changeQuality(url, qualityText, isEmbed = false) { if (videoHtml == '') { showToast('Video quality type is not available. Unable to change quality.') return; } - videoHtml = videoHtml.replace(/\"\;/g, '"'); + videoHtml = videoHtml.replace(/\"\;/g, '"'); - console.log(videoHtml); - console.log(isEmbed); + ft.log('HTML Video: ', videoHtml); + ft.log('(Is the video embeded?) isEmbed: ', isEmbed); - // The YouTube API creates 2 more iFrames. This is why a boolean value is sent - // with the function. - const embedPlayer = document.getElementsByTagName('IFRAME')[0]; + // The YouTube API creates 2 more iFrames. This is why a boolean value is sent + // with the function. + const embedPlayer = document.getElementsByTagName('IFRAME')[0]; - const html5Player = document.getElementsByClassName('videoPlayer'); + const html5Player = document.getElementsByClassName('videoPlayer'); - console.log(embedPlayer); - console.log(html5Player); + ft.log('Embeded Player Element: ', embedPlayer); + ft.log('HTML5 Player Element: ', html5Player); - if (isEmbed && html5Player.length == 0) { - // The embeded player is already playing. Return. - showToast('You are already using the embeded player.') - return; - } else if (isEmbed) { - // Switch from HTML 5 player to embeded Player - html5Player[0].remove(); - const mainHtml = $('#main').html(); - $('#main').html(videoHtml + mainHtml); - $('#currentQuality').html(qualityType); - } else if (html5Player.length == 0) { - // Switch from embeded player to HTML 5 player - embedPlayer.remove(); - let videoPlayer = document.createElement('video'); - videoPlayer.className = 'videoPlayer'; - videoPlayer.src = videoHtml; - videoPlayer.controls = true; - videoPlayer.autoplay = true; - $('#main').prepend(videoPlayer); - $('#currentQuality').html(qualityType); - } else { - // Switch src on HTML 5 player - const currentPlayBackTime = $('.videoPlayer').get(0).currentTime; - html5Player[0].src = videoHtml; - html5Player[0].load(); - $('.videoPlayer').get(0).currentTime = currentPlayBackTime; - $('#currentQuality').html(qualityType); - } + if (isEmbed && html5Player.length == 0) { + // The embeded player is already playing. Return. + showToast('You are already using the embeded player.') + return; + } else if (isEmbed) { + // Switch from HTML 5 player to embeded Player + html5Player[0].remove(); + const mainHtml = $('#main').html(); + $('#main').html(videoHtml + mainHtml); + $('#currentQuality').html(qualityType); + } else if (html5Player.length == 0) { + // Switch from embeded player to HTML 5 player + embedPlayer.remove(); + let videoPlayer = document.createElement('video'); + videoPlayer.className = 'videoPlayer'; + videoPlayer.src = videoHtml; + videoPlayer.controls = true; + videoPlayer.autoplay = true; + $('#main').prepend(videoPlayer); + $('#currentQuality').html(qualityType); + } else { + // Switch src on HTML 5 player + const currentPlayBackTime = $('.videoPlayer').get(0).currentTime; + html5Player[0].src = videoHtml; + html5Player[0].load(); + $('.videoPlayer').get(0).currentTime = currentPlayBackTime; + $('#currentQuality').html(qualityType); + } } /** @@ -374,8 +293,8 @@ function changeQuality(videoHtml, qualityType, isEmbed = false) { * @return {Void} */ function changeVideoSpeed(speed) { - $('#currentSpeed').html(speed); - $('.videoPlayer').get(0).playbackRate = speed; + $('#currentSpeed').html(speed); + $('.videoPlayer').get(0).playbackRate = speed; } /** @@ -386,16 +305,16 @@ function changeVideoSpeed(speed) { * @return {Void} */ function changeVolume(amount) { - const videoPlayer = $('.videoPlayer').get(0); - let volume = videoPlayer.volume; - volume = volume + amount; - if (volume > 1) { - videoPlayer.volume = 1; - } else if (volume < 0) { - videoPlayer.volume = 0; - } else { - videoPlayer.volume = volume; - } + const videoPlayer = $('.videoPlayer').get(0); + let volume = videoPlayer.volume; + volume = volume + amount; + if (volume > 1) { + videoPlayer.volume = 1; + } else if (volume < 0) { + videoPlayer.volume = 0; + } else { + videoPlayer.volume = volume; + } } /** @@ -406,8 +325,8 @@ function changeVolume(amount) { * @return {Void} */ function changeDurationBySeconds(seconds) { - const videoPlayer = $('.videoPlayer').get(0); - videoPlayer.currentTime = videoPlayer.currentTime + seconds; + const videoPlayer = $('.videoPlayer').get(0); + videoPlayer.currentTime = videoPlayer.currentTime + seconds; } /** @@ -418,6 +337,6 @@ function changeDurationBySeconds(seconds) { * @return {Void} */ function changeDurationByPercentage(percentage) { - const videoPlayer = $('.videoPlayer').get(0); - videoPlayer.currentTime = videoPlayer.duration * percentage; + const videoPlayer = $('.videoPlayer').get(0); + videoPlayer.currentTime = videoPlayer.duration * percentage; } diff --git a/src/js/savedVideos.js b/src/js/savedVideos.js index 39037d99f..9952a2d0b 100644 --- a/src/js/savedVideos.js +++ b/src/js/savedVideos.js @@ -1,118 +1,111 @@ /* -This file is part of FreeTube. + This file is part of FreeTube. -FreeTube is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + FreeTube is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. -FreeTube is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. -You should have received a copy of the GNU General Public License -along with FreeTube. If not, see . + You should have received a copy of the GNU General Public License + along with FreeTube. If not, see . */ /* -* File used for functions related to saving videos -*/ + * File used for functions related to saving videos + */ /** -* Adds a video to the user's saved video database. -* -* @param {string} videoId - The video ID of the video that will be saved. -* -* @return {Void} -*/ -function addSavedVideo(videoId){ - let checkIfSaved = videoIsSaved(videoId); + * Adds a video to the user's saved video database. + * + * @param {string} videoId - The video ID of the video that will be saved. + * + * @return {Void} + */ +function addSavedVideo(videoId) { + let checkIfSaved = videoIsSaved(videoId); - checkIfSaved.then((saved) => { - if (saved === false){ - let data = { - videoId: videoId, - timeSaved: new Date().getTime(), - }; + checkIfSaved.then((saved) => { + if (saved === false) { + let data = { + videoId: videoId, + timeSaved: new Date().getTime(), + }; - savedVidsDb.insert(data, (err, newDoc) => { - showToast('Video has been saved!'); - }); - } - else{ - showToast('Video already exists in saved file.') - } - }); + savedVidsDb.insert(data, (err, newDoc) => { + showToast('The video has been favorited!'); + }); + } else { + showToast('The video has already been favorited!') + } + }); } /** -* Removes a video from the user's saved video database. -* -* @param {string} videoId - The video ID of the video that will be removed. -* -* @return {Void} -*/ -function removeSavedVideo(videoId, string){ - savedVidsDb.remove({ - videoId: videoId - }, {}, (err, numRemoved) => { - showToast('Video has been removed from saved list.'); - }); + * Removes a video from the user's saved video database. + * + * @param {string} videoId - The video ID of the video that will be removed. + * + * @return {Void} + */ +function removeSavedVideo(videoId, string) { + savedVidsDb.remove({ + videoId: videoId + }, {}, (err, numRemoved) => { + showToast('Video has been removed from the favorites list.'); + }); } /** -* Toggles the save video button styling and saved / remove a video based on the current status. -* -* @param {string} videoId - The video ID to toggle between. -* -* @return {Void} -*/ + * Toggles the save video button styling and saved / remove a video based on the current status. + * + * @param {string} videoId - The video ID to toggle between. + * + * @return {Void} + */ function toggleSavedVideo(videoId) { - event.stopPropagation(); + event.stopPropagation(); const checkIfSaved = videoIsSaved(videoId); - const saveIcon = document.getElementById('saveIcon'); - const savedText = document.getElementById('savedText'); checkIfSaved.then((results) => { if (results === false) { - savedText.innerHTML = 'SAVED'; - saveIcon.classList.remove('far'); - saveIcon.classList.remove('unsaved'); - saveIcon.classList.add('fas'); - saveIcon.classList.add('saved'); + playerView.savedText = 'FAVORITED'; + playerView.savedIconType = 'fas saved'; addSavedVideo(videoId); } else { - savedText.innerHTML = 'SAVE'; - saveIcon.classList.remove('fas'); - saveIcon.classList.remove('saved'); - saveIcon.classList.add('far'); - saveIcon.classList.add('unsaved'); + playerView.savedText = 'FAVORITE'; + playerView.savedIconType = 'far unsaved'; removeSavedVideo(videoId); } }); } /** -* Checks if a video was saved in the user's saved video database -* -* @param {string} videoId - The video ID to check -* -* @return {promise} - A boolean value if the video was found or not. -*/ + * Checks if a video was saved in the user's saved video database + * + * @param {string} videoId - The video ID to check + * + * @return {promise} - A boolean value if the video was found or not. + */ function videoIsSaved(videoId) { - return new Promise((resolve, reject) => { - savedVidsDb.find({videoId: videoId}, (err, docs) => { - if (jQuery.isEmptyObject(docs)) { - resolve(false); - } else { - resolve(true); - } + return new Promise((resolve, reject) => { + savedVidsDb.find({ + videoId: videoId + }, (err, docs) => { + if (jQuery.isEmptyObject(docs)) { + resolve(false); + } else { + resolve(true); + } + }); }); - }); } /** @@ -121,28 +114,27 @@ function videoIsSaved(videoId) { * @return {Void} */ function showSavedVideos(){ - clearMainContainer(); - startLoadingAnimation(); + //clearMainContainer(); + //startLoadingAnimation(); console.log('checking saved videos'); - let videoList = ''; + let videoList = ''; - // Check the database for the list of videos. - savedVidsDb.find({}).sort({ - timeSaved: -1 - }).exec((err, docs) => { - // The YouTube API requires a max of 50 videos to be shown. Don't show more than 50. - // TODO: Allow the app to show more than 50 saved videos. - if(docs.length > 49){ - for (let i = 0; i < 49; i++) { - videoList = videoList + ',' + docs[i].videoId; - } - } - else{ - docs.forEach((video) => { - videoList = videoList + ',' + video.videoId; - }); - } + // Check the database for the list of videos. + savedVidsDb.find({}).sort({ + timeSaved: -1 + }).exec((err, docs) => { + // The YouTube API requires a max of 50 videos to be shown. Don't show more than 50. + // TODO: Allow the app to show more than 50 saved videos. + if (docs.length > 49) { + for (let i = 0; i < 49; i++) { + videoList = videoList + ',' + docs[i].videoId; + } + } else { + docs.forEach((video) => { + videoList = videoList + ',' + video.videoId; + }); + } // Call the YouTube API youtubeAPI('videos', { @@ -151,14 +143,14 @@ function showSavedVideos(){ maxResults: 50, }, (data) => { // Render the videos to the screen - createVideoListContainer('Saved Videos:'); let grabDuration = getDuration(data.items); grabDuration.then((videoList) => { + savedView.videoList = []; + loadingView.seen = false; videoList.items.forEach((video) => { displayVideo(video, 'saved'); }); }); - stopLoadingAnimation(); }); }); } diff --git a/src/js/settings.js b/src/js/settings.js index 4aff3062d..a5a49baf3 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -1,16 +1,13 @@ /* This file is part of FreeTube. - FreeTube is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - FreeTube is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - You should have received a copy of the GNU General Public License along with FreeTube. If not, see . */ @@ -22,20 +19,14 @@ along with FreeTube. If not, see . */ // To any third party devs that fork the project, please be ethical and change the API keys. -const apiKeyBank = ['AIzaSyC9E579nh_qqxg6BH4xIce3k_7a9mT4uQc', 'AIzaSyCKplYT6hZIlm2O9FbWTi1G7rkpsLNTq78', 'AIzaSyAE5xzh5GcA_tEDhXmMFd1pEzrL-W7z51E', 'AIzaSyDoFzqwuO9l386eF6BmNkVapjiTJ93CBy4', 'AIzaSyBljfZFPioB0TRJAj-0LS4tlIKl2iucyY4']; +const apiKeyBank = ['AIzaSyC9E579nh_qqxg6BH4xIce3k_7a9mT4uQc', 'AIzaSyCKplYT6hZIlm2O9FbWTi1G7rkpsLNTq78', 'AIzaSyAE5xzh5GcA_tEDhXmMFd1pEzrL-W7z51E', 'AIzaSyDoFzqwuO9l386eF6BmNkVapjiTJ93CBy4', 'AIzaSyBljfZFPioB0TRJAj-0LS4tlIKl2iucyY4', 'AIzaSyAiKgR75e3XAznCcb1cj4NUJ5rR_y3uB8E', 'AIzaSyBZL2Ie1masjwbIa74bR2GONF3p518npVU', 'AIzaSyA0CkT2lS1q9HHaFYGNGM4Ycjl1kmRy22s', 'AIzaSyDPy5jq2l1Bgv3-MbpGdZd3W3ik1BMZeDc']; /** * Display the settings screen to the user. * * @return {Void} */ -function showSettings() { - clearMainContainer(); - startLoadingAnimation(); - - let isChecked = ''; - let key = ''; - +function updateSettingsView() { /* * Check the settings database for the user's current settings. This is so the * settings page has the correct toggles related when it is rendered. @@ -45,7 +36,7 @@ function showSettings() { switch (setting['_id']) { case 'apiKey': if (apiKeyBank.indexOf(setting['value']) == -1) { - key = setting['value']; + settingsView.apiKey = setting['value']; } break; case 'theme': @@ -55,28 +46,17 @@ function showSettings() { } }); - // Grab the settings.html template to prepare for rendering - const settingsTemplate = require('./templates/settings.html') - mustache.parse(settingsTemplate); - const rendered = mustache.render(settingsTemplate, { - isChecked: isChecked, - key: key, - }); - // Render template to application - $('#main').html(rendered); - stopLoadingAnimation(); - // Check / uncheck the switch depending on the user's settings. if (currentTheme === 'light') { - document.getElementById('themeSwitch').checked = false; + settingsView.useTheme = false; } else { - document.getElementById('themeSwitch').checked = true; + settingsView.useTheme = true; } if (useTor) { - document.getElementById('torSwitch').checked = true; + settingsView.useTor = true; } else { - document.getElementById('torSwitch').checked = false; + settingsView.useTor = false; } }); } @@ -89,12 +69,12 @@ function showSettings() { function checkDefaultSettings() { // Grab a random API Key. - apiKey = apiKeyBank[Math.floor(Math.random() * apiKeyBank.length)]; + settingsView.apiKey = apiKeyBank[Math.floor(Math.random() * apiKeyBank.length)]; let newSetting; let settingDefaults = { 'theme': 'light', - 'apiKey': apiKey, + 'apiKey': settingsView.apiKey, 'useTor': false }; @@ -121,7 +101,10 @@ function checkDefaultSettings() { break; case 'apiKey': if (apiKeyBank.indexOf(docs[0]['value']) == -1) { - apiKey = docs[0]['value']; + settingsView.apiKey = docs[0]['value']; + } + else{ + settingsView.apiKey = settingDefaults.apiKey; } break; case 'useTor': @@ -146,7 +129,12 @@ function updateSettings() { let key = document.getElementById('api-key').value; let theme = 'light'; - apiKey = apiKeyBank[Math.floor(Math.random() * apiKeyBank.length)]; + if (apiKeyBank.indexOf(key) == -1 && key !== '') { + settingsView.apiKey = key; + } + else{ + settingsView.apiKey = apiKeyBank[Math.floor(Math.random() * apiKeyBank.length)]; + } console.log(themeSwitch); @@ -177,20 +165,12 @@ function updateSettings() { useTor = torSwitch; }); - if (key != '') { - settingsDb.update({ - _id: 'apiKey' - }, { - value: key - }, {}); - } else { - // To any third party devs that fork the project, please be ethical and change the API key. - settingsDb.update({ - _id: 'apiKey' - }, { - value: apiKey - }, {}); - } + // To any third party devs that fork the project, please be ethical and change the API key. + settingsDb.update({ + _id: 'apiKey' + }, { + value: settingsView.apiKey + }, {}); showToast('Settings have been saved.'); } @@ -300,11 +280,6 @@ function importSubscriptions(){ let fileType = (i < 0) ? '' : fileLocation[0].substr(i); console.log(fileType); - /*if (fileType !== '.db'){ - showToast('Incorrect filetype. Import was unsuccessful.'); - return; - }*/ - fs.readFile(fileLocation[0], function(readErr, data){ if(readErr){ showToast('Unable to read file. File may be corrupt or have invalid permissions.'); @@ -424,3 +399,5 @@ function clearFile(type, showMessage = true){ } }) } + +checkDefaultSettings(); diff --git a/src/js/subscriptions.js b/src/js/subscriptions.js index ff288224e..37a431771 100644 --- a/src/js/subscriptions.js +++ b/src/js/subscriptions.js @@ -1,18 +1,18 @@ /* -This file is part of FreeTube. + This file is part of FreeTube. -FreeTube is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + FreeTube is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. -FreeTube is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. -You should have received a copy of the GNU General Public License -along with FreeTube. If not, see . + You should have received a copy of the GNU General Public License + along with FreeTube. If not, see . */ @@ -21,6 +21,9 @@ along with FreeTube. If not, see . * File for all functions related to subscriptions. */ + let subscriptionTimer; + let checkSubscriptions = true; + /** * Add a channel to the user's subscription database. * @@ -29,30 +32,30 @@ along with FreeTube. If not, see . * @return {Void} */ function addSubscription(channelId, useToast = true) { - console.log(channelId); - // Request YouTube API - youtubeAPI('channels', { - part: 'snippet', - id: channelId, - }, function(data) { - const channelInfo = data['items'][0]['snippet']; - const channelName = channelInfo['title']; - const thumbnail = channelInfo['thumbnails']['high']['url']; + ft.log('Channel ID: ', channelId); + // Request YouTube API + youtubeAPI('channels', { + part: 'snippet', + id: channelId, + }, (data) => { + const channelInfo = data['items'][0]['snippet']; + const channelName = channelInfo['title']; + const thumbnail = channelInfo['thumbnails']['high']['url']; - const channel = { - channelId: channelId, - channelName: channelName, - channelThumbnail: thumbnail, - }; + const channel = { + channelId: channelId, + channelName: channelName, + channelThumbnail: thumbnail, + }; - // Refresh the list of subscriptions on the side navigation bar. - subDb.insert(channel, (err, newDoc) => { - if (useToast) { - showToast('Added ' + channelName + ' to subscriptions.'); - displaySubs(); - } + // Refresh the list of subscriptions on the side navigation bar. + subDb.insert(channel, (err, newDoc) => { + if (useToast) { + showToast('Added ' + channelName + ' to subscriptions.'); + displaySubs(); + } + }); }); - }); } /** @@ -63,13 +66,13 @@ function addSubscription(channelId, useToast = true) { * @return {Void} */ function removeSubscription(channelId) { - subDb.remove({ - channelId: channelId - }, {}, (err, numRemoved) => { - // Refresh the list of subscriptions on the side navigation bar. - displaySubs(); - showToast('Removed channel from subscriptions.'); - }); + subDb.remove({ + channelId: channelId + }, {}, (err, numRemoved) => { + // Refresh the list of subscriptions on the side navigation bar. + displaySubs(); + showToast('Removed channel from subscriptions.'); + }); } /** @@ -77,94 +80,103 @@ function removeSubscription(channelId) { * * @return {Void} */ -function loadSubscriptions() { - clearMainContainer(); - showToast('Getting Subscriptions. Please wait...'); - const loading = document.getElementById('loading'); + function loadSubscriptions() { + if (checkSubscriptions === false && subscriptionView.videoList.length > 0){ + console.log('Will not load subscriptions. Timer still on.'); + loadingView.seen = false; + return; + } + else{ + showToast('Refreshing Subscription List. Please wait...'); + checkSubscriptions = false; + } - startLoadingAnimation() + let videoList = []; - let videoList = []; + const subscriptions = returnSubscriptions(); - const subscriptions = returnSubscriptions(); + subscriptions.then((results) => { + let channelId = ''; + let videoList = []; - // Welcome to callback hell, we hope you enjoy your stay. - subscriptions.then((results) => { - let channelId = ''; - let videoList = []; + if (results.length > 0) { + let counter = 0; - if (results.length > 0) { - let counter = 0; + for (let i = 0; i < results.length; i++) { + channelId = results[i]['channelId']; - for (let i = 0; i < results.length; i++) { - channelId = results[i]['channelId']; + youtubeAPI('search', { + part: 'snippet', + channelId: channelId, + type: 'video', + maxResults: 15, + order: 'date', + }, (data) => { + console.log(data); + videoList = videoList.concat(data.items); + counter++; + progressView.progressWidth = (counter / results.length) * 100; + if (counter === results.length) { + videoList.sort((a, b) => { + const date1 = Date.parse(a.snippet.publishedAt); + const date2 = Date.parse(b.snippet.publishedAt); - youtubeAPI('search', { - part: 'snippet', - channelId: channelId, - type: 'video', - maxResults: 15, - order: 'date', - }, (data) => { - console.log(data); - videoList = videoList.concat(data.items); - counter++; - if (counter === results.length) { - videoList.sort((a, b) => { - const date1 = Date.parse(a.snippet.publishedAt); - const date2 = Date.parse(b.snippet.publishedAt); + return date2.valueOf() - date1.valueOf(); + }); - return date2.valueOf() - date1.valueOf(); - }); + // The YouTube website limits the subscriptions to 100 before grabbing more so we only show 100 + // to keep the app running at a good speed. + if (videoList.length < 50) { + let grabDuration = getDuration(videoList.slice(0, 49)); - // Render the videos to the application. - createVideoListContainer('Latest Subscriptions:'); + grabDuration.then((list) => { + subscriptionView.videoList = []; + list.items.forEach((video) => { + displayVideo(video, 'subscriptions'); + }); + loadingView.seen = false; + progressView.seen = false; + progressView.progressWidth = 0; + }); + } else { + console.log(videoList); + let finishedList = []; + let firstBatchDuration = getDuration(videoList.slice(0, 49)); - // The YouTube website limits the subscriptions to 100 before grabbing more so we only show 100 - // to keep the app running at a good speed. - if (videoList.length < 50) { - let grabDuration = getDuration(videoList.slice(0, 49)); + firstBatchDuration.then((list1) => { + finishedList = finishedList.concat(list1.items); + let secondBatchDuration = getDuration(videoList.slice(50, 99)); - grabDuration.then((list) => { - list.items.forEach((video) => { - displayVideo(video); - }); - stopLoadingAnimation(); - }); - } else { - console.log(videoList); - let finishedList = []; - let firstBatchDuration = getDuration(videoList.slice(0, 49)); - - firstBatchDuration.then((list1) => { - finishedList = finishedList.concat(list1.items); - let secondBatchDuration = getDuration(videoList.slice(50, 99)); - - secondBatchDuration.then((list2) => { - finishedList = finishedList.concat(list2.items); - finishedList.forEach((video) => { - displayVideo(video); - }); - stopLoadingAnimation(); - }); - }); - } - } - } - ); - } + secondBatchDuration.then((list2) => { + finishedList = finishedList.concat(list2.items); + console.log(finishedList); + subscriptionView.videoList = []; + finishedList.forEach((video) => { + displayVideo(video, 'subscriptions'); + }); + loadingView.seen = false; + progressView.seen = false; + progressView.progressWidth = 0; + subscriptionTimer = window.setTimeout(() => { + checkSubscriptions = true; + }, 60000); + }); + }); + } + } + } + ); + } - } else { - // User has no subscriptions. Display message. - const container = document.getElementById('main'); - stopLoadingAnimation(); - - container.innerHTML = `

Your Subscription list is currently empty. Start adding subscriptions - to see them here.

`; - } - }); -} + } else { + // User has no subscriptions. Display message. + loadingView.seen = false; + headerView.seen = false; + noSubscriptions.seen = true; + } + }); + } /** * Get the list of subscriptions from the user's subscription database. @@ -172,11 +184,11 @@ function loadSubscriptions() { * @return {promise} The list of subscriptions. */ function returnSubscriptions() { - return new Promise((resolve, reject) => { - subDb.find({}, (err, subs) => { - resolve(subs); + return new Promise((resolve, reject) => { + subDb.find({}, (err, subs) => { + resolve(subs); + }); }); - }); } /** @@ -185,31 +197,31 @@ function returnSubscriptions() { * @return {Void} */ function displaySubs() { - const subList = document.getElementById('subscriptions'); + const subList = document.getElementById('subscriptions'); - subList.innerHTML = ''; + subList.innerHTML = ''; - // Sort alphabetically - subDb.find({}).sort({ - channelName: 1 - }).exec((err, subs) => { - subs.forEach((channel) => { - // Grab subscriptions.html to be used as a template. - const subsTemplate = require('./templates/subscriptions.html') - mustache.parse(subsTemplate); - const rendered = mustache.render(subsTemplate, { - channelIcon: channel['channelThumbnail'], - channelName: channel['channelName'], - channelId: channel['channelId'], - }); - // Render template to page. - const subscriptionsHtml = $('#subscriptions').html(); - $('#subscriptions').html(subscriptionsHtml + rendered); + // Sort alphabetically + subDb.find({}).sort({ + channelName: 1 + }).exec((err, subs) => { + subs.forEach((channel) => { + // Grab subscriptions.html to be used as a template. + const subsTemplate = require('./templates/subscriptions.html') + mustache.parse(subsTemplate); + const rendered = mustache.render(subsTemplate, { + channelIcon: channel['channelThumbnail'], + channelName: channel['channelName'], + channelId: channel['channelId'], + }); + // Render template to page. + const subscriptionsHtml = $('#subscriptions').html(); + $('#subscriptions').html(subscriptionsHtml + rendered); + }); }); - }); - // Add onclick function - $('#subscriptions .fa-times').onClick = removeSubscription; + // Add onclick function + $('#subscriptions .fa-times').onClick = removeSubscription; } /** @@ -219,25 +231,25 @@ function displaySubs() { * @return {Void} */ function toggleSubscription(channelId) { - event.stopPropagation(); + event.stopPropagation(); - const checkIfSubscribed = isSubscribed(channelId); - const subscribeButton = document.getElementById('subscribeButton'); + const checkIfSubscribed = isSubscribed(channelId); + const subscribeButton = document.getElementById('subscribeButton'); - checkIfSubscribed.then((results) => { + checkIfSubscribed.then((results) => { - if (results === false) { - if (subscribeButton != null) { - subscribeButton.innerHTML = 'UNSUBSCRIBE'; - } - addSubscription(channelId); - } else { - if (subscribeButton != null) { - subscribeButton.innerHTML = 'SUBSCRIBE'; - } - removeSubscription(channelId); - } - }); + if (results === false) { + if (subscribeButton != null) { + subscribeButton.innerHTML = 'UNSUBSCRIBE'; + } + addSubscription(channelId); + } else { + if (subscribeButton != null) { + subscribeButton.innerHTML = 'SUBSCRIBE'; + } + removeSubscription(channelId); + } + }); } /** @@ -248,15 +260,15 @@ function toggleSubscription(channelId) { * @return {promise} - A boolean value if the channel is currently subscribed or not. */ function isSubscribed(channelId) { - return new Promise((resolve, reject) => { - subDb.find({ - channelId: channelId - }, (err, docs) => { - if (jQuery.isEmptyObject(docs)) { - resolve(false); - } else { - resolve(true); - } + return new Promise((resolve, reject) => { + subDb.find({ + channelId: channelId + }, (err, docs) => { + if (jQuery.isEmptyObject(docs)) { + resolve(false); + } else { + resolve(true); + } + }); }); - }); } diff --git a/src/js/templates.js b/src/js/templates.js new file mode 100644 index 000000000..bfd774d18 --- /dev/null +++ b/src/js/templates.js @@ -0,0 +1,432 @@ +/* +This file is part of FreeTube. + +FreeTube is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +FreeTube is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with FreeTube. If not, see . +*/ + +import Vue from './js/vue.js'; + +const mainHeaderTemplate = require('./templates/mainHeader.html'); +const aboutTemplate = require('./templates/about.html'); +const settingsTemplate = require('./templates/settings.html'); +const videoListTemplate = require('./templates/videoTemplate.html'); +const nextPageTemplate = require('./templates/searchNextPage.html'); +const playerTemplate = require('./templates/player.html'); +const channelTemplate = require('./templates/channelView.html'); +const progressViewTemplate = require('./templates/progressView.html'); + +/* +* Progress view +* +* Shows progress bar on bottom of application. +* +* seen: Toggles visibility of view +* progressWidth: sets width of the progress bar +*/ +let progressView = new Vue({ + el: '#progressView', + data: { + seen: true, + progressWidth: 0 + }, + template: progressViewTemplate +}); + +let loadingView = new Vue({ + el: '#loading', + data: { + seen: false + } +}); + +let noSubscriptions = new Vue({ + el: '#noSubscriptions', + data: { + seen: false + } +}); + +let sideNavBar = new Vue({ + el: '#sideNav', + methods: { + subscriptions: (event) => { + hideViews(); + if(subscriptionView.videoList.length === 0){ + loadingView.seen = true; + } + headerView.seen = true; + headerView.title = 'Latest Subscriptions'; + subscriptionView.seen = true; + loadSubscriptions(); + }, + popular: (event) => { + hideViews(); + if (loadingView.seen !== false){ + loadingView.seen = false; + } + if(popularView.videoList.length === 0){ + loadingView.seen = true; + } + headerView.seen = true; + headerView.title = 'Most Popular'; + popularView.seen = true; + showMostPopular(); + }, + saved: (event) => { + hideViews(); + if (loadingView.seen !== false){ + loadingView.seen = false; + } + if(savedView.videoList.length === 0){ + loadingView.seen = true; + } + headerView.seen = true; + headerView.title = 'Favorited Videos'; + savedView.seen = true; + showSavedVideos(); + }, + history: (event) => { + hideViews(); + if (loadingView.seen !== false){ + loadingView.seen = false; + } + if(historyView.videoList.length === 0){ + loadingView.seen = true; + } + headerView.seen = true; + headerView.title = 'Video History'; + historyView.seen = true; + showHistory(); + }, + settings: (event) => { + hideViews(); + if (loadingView.seen !== false){ + loadingView.seen = false; + } + settingsView.seen = true; + updateSettingsView(); + }, + about: (event) => { + hideViews(); + if (loadingView.seen !== false){ + loadingView.seen = false; + } + aboutView.seen = true; + } + } +}); + +let headerView = new Vue({ + el: '#mainHeaderView', + data: { + seen: true, + title: 'Latest Subscriptions' + }, + template: mainHeaderTemplate +}); + +let subscriptionView = new Vue({ + el: '#subscriptionView', + data: { + seen: true, + isSearch: false, + videoList: [] + }, + methods: { + play: (videoId) => { + loadingView.seen = true; + playVideo(videoId); + }, + channel: (channelId) => { + goToChannel(channelId); + }, + toggleSave: (videoId) => { + toggleSavedVideo(videoId); + }, + copy: (site, videoId) => { + const url = 'https://' + site + '/watch?v=' + videoId; + clipboard.writeText(url); + showToast('URL has been copied to the clipboard'); + } + }, + template: videoListTemplate +}); + +let popularView = new Vue({ + el: '#popularView', + data: { + seen: false, + isSearch: false, + videoList: [] + }, + methods: { + play: (videoId) => { + loadingView.seen = true; + playVideo(videoId); + }, + channel: (channelId) => { + goToChannel(channelId); + }, + toggleSave: (videoId) => { + addSavedVideo(videoId); + }, + copy: (site, videoId) => { + const url = 'https://' + site + '/watch?v=' + videoId; + clipboard.writeText(url); + showToast('URL has been copied to the clipboard'); + } + }, + template: videoListTemplate +}); + +let savedView = new Vue({ + el: '#savedView', + data: { + seen: false, + isSearch: false, + videoList: [] + }, + methods: { + play: (videoId) => { + loadingView.seen = true; + playVideo(videoId); + }, + channel: (channelId) => { + goToChannel(channelId); + }, + toggleSave: (videoId) => { + addSavedVideo(videoId); + }, + copy: (site, videoId) => { + const url = 'https://' + site + '/watch?v=' + videoId; + clipboard.writeText(url); + showToast('URL has been copied to the clipboard'); + } + }, + template: videoListTemplate +}); + +let historyView = new Vue({ + el: '#historyView', + data: { + seen: false, + isSearch: false, + videoList: [] + }, + methods: { + play: (videoId) => { + loadingView.seen = true; + playVideo(videoId); + }, + channel: (channelId) => { + goToChannel(channelId); + }, + toggleSave: (videoId) => { + addSavedVideo(videoId); + }, + copy: (site, videoId) => { + const url = 'https://' + site + '/watch?v=' + videoId; + clipboard.writeText(url); + showToast('URL has been copied to the clipboard'); + } + }, + template: videoListTemplate +}); + +let aboutView = new Vue({ + el: '#aboutView', + data: { + seen: false, + versionNumber: electron.remote.app.getVersion() + }, + template: aboutTemplate +}); + +let settingsView = new Vue({ + el: '#settingsView', + data: { + seen: false, + useTheme: false, + useTor: false, + apiKey: '' + }, + template: settingsTemplate +}); + +let searchView = new Vue({ + el: '#searchView', + data: { + seen: false, + isSearch: true, + nextPageToken: '', + videoList: [] + }, + methods: { + play: (videoId) => { + loadingView.seen = true; + playVideo(videoId); + }, + channel: (channelId) => { + goToChannel(channelId); + }, + toggleSave: (videoId) => { + addSavedVideo(videoId); + }, + copy: (site, videoId) => { + const url = 'https://' + site + '/watch?v=' + videoId; + clipboard.writeText(url); + showToast('URL has been copied to the clipboard'); + }, + nextPage: (nextPageToken) => { + console.log(searchView.nextPageToken); + search(searchView.nextPageToken); + } + }, + template: videoListTemplate +}); + +let channelView = new Vue({ + el: '#channelView', + data: { + seen: false, + id: '', + name: '', + icon: '', + baner: '', + subCount: '', + subButtonText: '', + description: '' + }, + methods: { + subscription: (channelId) => { + toggleSubscription(channelId); + }, + }, + template: channelTemplate +}); + +let channelVideosView = new Vue({ + el: '#channelVideosView', + data: { + seen: false, + isSearch: false, + videoList: [] + }, + methods: { + play: (videoId) => { + loadingView.seen = true; + playVideo(videoId); + }, + channel: (channelId) => { + goToChannel(channelId); + }, + toggleSave: (videoId) => { + addSavedVideo(videoId); + }, + copy: (site, videoId) => { + const url = 'https://' + site + '/watch?v=' + videoId; + clipboard.writeText(url); + showToast('URL has been copied to the clipboard'); + }, + }, + template: videoListTemplate +}); + +let playerView = new Vue({ + el: '#playerView', + data: { + seen: false, + publishedDate: '', + videoUrl: '', + videoId: '', + channelId: '', + channelIcon: '', + channelName: '', + subscribedText: '', + savedText: '', + savedIconType: 'far', + description: '', + videoThumbnail: '', + subtitleHtml: '', + currentQuality: '', + video480p: '', + video720p: '', + embededHtml: '', + currentSpeed: 1, + videoTitle: '', + videoViews: '', + likePercentage: 0, + videoLikes: 0, + videoDislikes: 0, + playerSeen: true, + recommendedVideoList: [] + }, + methods: { + channel: (channelId) => { + goToChannel(channelId); + }, + subscription: (videoId) => { + toggleSubscription(videoId); + }, + quality: (url, qualityText) => { + console.log(url); + console.log(qualityText); + if(playerView.playerSeen === true){ + // Update time to new url + const currentPlayBackTime = $('.videoPlayer').get(0).currentTime; + console.log(currentPlayBackTime); + playerView.videoUrl = url; + playerView.currentQuality = qualityText; + setTimeout(() => {$('.videoPlayer').get(0).currentTime = currentPlayBackTime;}, 100); + } + else{ + playerView.playerSeen = true; + playerView.videoUrl = url; + playerView.currentQuality = qualityText; + } + }, + embededPlayer: () => { + playerView.playerSeen = false; + playerView.currentQuality = 'EMBED'; + }, + copy: (site, videoId) => { + const url = 'https://' + site + '/watch?v=' + videoId; + clipboard.writeText(url); + showToast('URL has been copied to the clipboard'); + }, + save: (videoId) => { + toggleSavedVideo(videoId); + }, + play: (videoId) => { + loadingView.seen = true; + playVideo(videoId); + } + }, + template: playerTemplate +}); + +function hideViews(){ + subscriptionView.seen = false; + noSubscriptions.seen = false; + aboutView.seen = false; + headerView.seen = false; + searchView.seen = false; + settingsView.seen = false; + popularView.seen = false; + savedView.seen = false; + historyView.seen = false; + playerView.seen = false; + channelView.seen = false; + channelVideosView.seen = false; +} diff --git a/src/js/updates.js b/src/js/updates.js index 7e448c9c9..4c66017ff 100644 --- a/src/js/updates.js +++ b/src/js/updates.js @@ -1,45 +1,45 @@ /* -This file is part of FreeTube. + This file is part of FreeTube. -FreeTube is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + FreeTube is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. -FreeTube is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. -You should have received a copy of the GNU General Public License -along with FreeTube. If not, see . + You should have received a copy of the GNU General Public License + along with FreeTube. If not, see . */ - +// import {freeTubeLog} from './events.js'; /* * A file for checking / managing updates */ - const updateChecker = require('github-version-checker'); +const updateChecker = require('github-version-checker'); - const options = { - token: 'USERACCESSTOKEN', // personal access token. Github will not allow commiting the access token, which is why this is blank. - repo: 'freetube', // repository name - owner: 'freetubeapp', // repository owner - currentVersion: require('electron').remote.app.getVersion(), // your app's current version - fetchTags: false // whether to fetch releases or tags - }; +const options = { + token: 'USERACCESSTOKEN', // personal access token. Github will not allow commiting the access token, which is why this is blank. + repo: 'freetube', // repository name + owner: 'freetubeapp', // repository owner + currentVersion: require('electron').remote.app.getVersion(), // your app's current version + fetchTags: false // whether to fetch releases or tags +}; - const openReleasePage = function(){ - shell.openExternal('https://github.com/FreeTubeApp/FreeTube/releases'); - } +const openReleasePage = function () { + shell.openExternal('https://github.com/FreeTubeApp/FreeTube/releases'); +} /*function checkForUpdates() { updateChecker(options, function(error, update) { // callback function if (error){ showToast('There was a problem with checking for updates'); - console.log(error); + freeTubeLog(error); } if (update) { // print some update info if an update is available confirmFunction(update.name + ' is now available! Would you like to download the update?', openReleasePage); @@ -50,9 +50,9 @@ along with FreeTube. If not, see . }); }*/ -updateChecker(options, function(error, update) { // callback function - if (error) throw error; - if (update) { // print some update info if an update is available - confirmFunction(update.name + ' is now available! Would you like to download the update?', openReleasePage); - } -}); +updateChecker(options, function (error, update) { // callback function + if (error) throw error; + if (update) { // print some update info if an update is available + confirmFunction(update.name + ' is now available! Would you like to download the update?', openReleasePage); + } +}); \ No newline at end of file diff --git a/src/js/videos.js b/src/js/videos.js index 5f018e353..9570fa50f 100644 --- a/src/js/videos.js +++ b/src/js/videos.js @@ -1,18 +1,18 @@ /* -This file is part of FreeTube. + This file is part of FreeTube. -FreeTube is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. + FreeTube is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. -FreeTube is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. + FreeTube is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. -You should have received a copy of the GNU General Public License -along with FreeTube. If not, see . + You should have received a copy of the GNU General Public License + along with FreeTube. If not, see . */ @@ -25,70 +25,72 @@ along with FreeTube. If not, see . * @return {Void} */ function search(nextPageToken = '') { - const query = document.getElementById('search').value; + const query = document.getElementById('search').value; - if (query === '') { - return; - } + if (query === '') { + return; + } if (nextPageToken === '') { - clearMainContainer(); - startLoadingAnimation(); + hideViews(); + headerView.seen = true; + headerView.title = 'Search Results'; + searchView.videoList = []; + searchView.seen = true; } else { console.log(nextPageToken); showToast('Fetching results. Please wait...'); } - youtubeAPI('search', { - q: query, - part: 'id', - pageToken: nextPageToken, - maxResults: 25, - }, function(data) { - console.log(data); + youtubeAPI('search', { + q: query, + part: 'id', + pageToken: nextPageToken, + maxResults: 25, + }, function (data) { + ft.log('Search Data: ', data); - let channels = data.items.filter((item) => { - if (item.id.kind === 'youtube#channel') { - return true; - } - }); + let channels = data.items.filter((item) => { + if (item.id.kind === 'youtube#channel') { + return true; + } + }); - let playlists = data.items.filter((item) => { - if (item.id.kind === 'youtube#playlist') { - return true; - } - }); + let playlists = data.items.filter((item) => { + if (item.id.kind === 'youtube#playlist') { + return true; + } + }); - let videos = data.items.filter((item) => { - if (item.id.kind === 'youtube#video') { - return true; - } - }); + let videos = data.items.filter((item) => { + if (item.id.kind === 'youtube#video') { + return true; + } + }); - console.log(channels); - console.log(typeof(channels)); - console.log(playlists); + ft.log('Channels: ', channels); + ft.log('Typeof object above (channels) ^^', typeof (channels)); + ft.log('Playlists', playlists); - if(playlists.length > 0){ - //displayPlaylists(playlists); - } + if (playlists.length > 0) { + //displayPlaylists(playlists); + } - if(channels.length > 0){ - displayChannels(channels); - } + if (channels.length > 0) { + displayChannels(channels); + } - let grabDuration = getDuration(videos); + let grabDuration = getDuration(videos); grabDuration.then((videoList) => { console.log(videoList); - videoList.items.forEach(displayVideo); + videoList.items.forEach((video) => { + displayVideo(video, 'search'); + }); }); - if (nextPageToken === '') { - createVideoListContainer('Search results:'); - stopLoadingAnimation(); - } - addNextPage(data.nextPageToken); + searchView.nextPageToken = data.nextPageToken; + loadingView.seen = false; }) } @@ -100,32 +102,32 @@ function search(nextPageToken = '') { * @return {promise} - The list of videos with the duration included. */ function getDuration(data) { - return new Promise((resolve, reject) => { - let videoIdList = ''; + return new Promise((resolve, reject) => { + let videoIdList = ''; - for (let i = 0; i < data.length; i++) { - if (videoIdList === '') { - if (typeof(data[i]['id']) === 'string') { - videoIdList = data[i]['id']; - } else { - videoIdList = data[i]['id']['videoId']; + for (let i = 0; i < data.length; i++) { + if (videoIdList === '') { + if (typeof (data[i]['id']) === 'string') { + videoIdList = data[i]['id']; + } else { + videoIdList = data[i]['id']['videoId']; + } + } else { + if (typeof (data[i]['id']) === 'string') { + videoIdList = videoIdList + ', ' + data[i]['id']; + } else { + videoIdList = videoIdList + ', ' + data[i]['id']['videoId']; + } + } } - } else { - if (typeof(data[i]['id']) === 'string') { - videoIdList = videoIdList + ', ' + data[i]['id']; - } else { - videoIdList = videoIdList + ', ' + data[i]['id']['videoId']; - } - } - } - youtubeAPI('videos', { - part: 'snippet, contentDetails', - id: videoIdList - }, (data) => { - resolve(data); + youtubeAPI('videos', { + part: 'snippet, contentDetails', + id: videoIdList + }, (data) => { + resolve(data); + }); }); - }); } /** @@ -137,21 +139,21 @@ function getDuration(data) { * * @return {Void} */ -function displayVideo(video, listType = '') { - const videoSnippet = video.snippet; +function displayVideo(videoData, listType = '') { + let video = {}; - const videoDuration = parseVideoDuration(video.contentDetails.duration); - //const videoDuration = '00:00'; + const videoSnippet = videoData.snippet; + + video.duration = parseVideoDuration(videoData.contentDetails.duration); // Grab the published date for the video and convert to a user readable state. const dateString = new Date(videoSnippet.publishedAt); - const publishedDate = dateFormat(dateString, "mmm dS, yyyy"); + video.publishedDate = dateFormat(dateString, "mmm dS, yyyy"); const searchMenu = $('#videoListContainer').html(); - const videoId = video.id; // Include a remove icon in the list if the application is displaying the history list or saved videos. - const deleteHtml = () => { + video.deleteHtml = () => { switch (listType) { case 'saved': return `
  • Remove Saved Video
  • `; @@ -160,113 +162,128 @@ function displayVideo(video, listType = '') { } }; + video.id = videoData.id; + video.youtubeUrl = 'https://youtube.com/watch?v=' + video.id; + video.invidiousUrl = 'https://invidio.us/watch?v=' + video.id; // Includes text if the video is live. - const liveText = (videoSnippet.liveBroadcastContent === 'live') ? 'LIVE NOW' : ''; - const videoListTemplate = require('./templates/videoList.html'); + video.liveText = (videoSnippet.liveBroadcastContent === 'live') ? 'LIVE NOW' : ''; + video.thumbnail = videoSnippet.thumbnails.medium.url; + video.title = videoSnippet.title; + video.channelName = videoSnippet.channelTitle; + video.channelId = videoSnippet.channelId; + video.description = videoSnippet.description; + video.isVideo = true; - mustache.parse(videoListTemplate); - const rendered = mustache.render(videoListTemplate, { - videoId: videoId, - videoThumbnail: videoSnippet.thumbnails.medium.url, - videoTitle: videoSnippet.title, - channelName: videoSnippet.channelTitle, - videoDescription: videoSnippet.description, - channelId: videoSnippet.channelId, - videoDuration: videoDuration, - publishedDate: publishedDate, - liveText: liveText, - deleteHtml: deleteHtml, - }); - - // Apply the render to the page - const nextButton = document.getElementById('getNextPage'); - if (nextButton === null) { - $('#videoListContainer').append(rendered); - } else { - $(rendered).insertBefore('#getNextPage'); + switch (listType) { + case 'subscriptions': + subscriptionView.videoList = subscriptionView.videoList.concat(video); + video.removeFromSave = true; + break; + case 'search': + searchView.videoList = searchView.videoList.concat(video); + video.removeFromSave = false; + break; + case 'popular': + popularView.videoList = popularView.videoList.concat(video); + video.removeFromSave = false; + break; + case 'saved': + savedView.videoList = savedView.videoList.concat(video); + video.removeFromSave = false; + break; + case 'history': + historyView.videoList = historyView.videoList.concat(video); + video.removeFromSave = false; + break; + case 'channel': + channelVideosView.videoList = channelVideosView.videoList.concat(video); + video.removeFromSave = false; + break; } } function displayChannels(channels) { - let channelIds; + let channelIds; - channels.forEach((channel) => { - if (typeof(channelIds) === 'undefined') { - channelIds = channel.id.channelId; - } else { - channelIds = channelIds + ',' + channel.id.channelId; - } - }); + channels.forEach((channel) => { + if (typeof (channelIds) === 'undefined') { + channelIds = channel.id.channelId; + } else { + channelIds = channelIds + ',' + channel.id.channelId; + } + }); - console.log(channelIds); + ft.log('Channel IDs: ', channelIds); - youtubeAPI('channels', { - part: 'snippet,statistics', - id: channelIds, - }, function(data) { - console.log(data); - let items = data['items'].reverse(); - const videoListTemplate = require('./templates/channelList.html'); + youtubeAPI('channels', { + part: 'snippet,statistics', + id: channelIds, + }, function (data) { + ft.log('Channel Data: ', data); + let items = data['items'].reverse(); - console.log(items); + ft.log('Channel Items: ', items); items.forEach((item) => { - mustache.parse(videoListTemplate); - let rendered = mustache.render(videoListTemplate, { - channelId: item.id, - channelThumbnail: item.snippet.thumbnails.medium.url, - channelName: item.snippet.title, - channelDescription: item.snippet.description, - subscriberCount: item.statistics.subscriberCount, - videoCount: item.statistics.videoCount, - }); + let channelData = {}; - $(rendered).insertBefore('#getNextPage'); + channelData.channelId = item.id; + channelData.thumbnail = item.snippet.thumbnails.medium.url; + channelData.channelName = item.snippet.title; + channelData.description = item.snippet.description; + channelData.subscriberCount = item.statistics.subscriberCount; + channelData.videoCount = item.statistics.videoCount; + channelData.isVideo = false; + + console.log(searchView.videoList); + console.log(channelData); + + searchView.videoList = searchView.videoList.concat(channelData); }); }); } function displayPlaylists(playlists) { - let playlistIds; + let playlistIds; - playlists.forEach((playlist) => { - if (typeof(playlistIds) === 'undefined') { - playlistIds = playlist.id.playlistId; - } else { - playlistIds = playlistIds + ',' + playlist.id.playlistId; - } - }); - - console.log(playlistIds); - - youtubeAPI('playlists', { - part: 'snippet,contentDetails', - id: playlistIds, - }, function(data) { - console.log(data); - let items = data['items'].reverse(); - const playlistListTemplate = require('./templates/playlistList.html'); - - console.log(items); - - items.forEach((item) => { - let dateString = new Date(item.snippet.publishedAt); - let publishedDate = dateFormat(dateString, "mmm dS, yyyy"); - - mustache.parse(playlistListTemplate); - let rendered = mustache.render(playlistListTemplate, { - channelId: item.snippet.channelId, - channelName: item.snippet.channelTitle, - playlistThumbnail: item.snippet.thumbnails.medium.url, - playlistTitle: item.snippet.title, - playlistDescription: item.snippet.description, - videoCount: item.contentDetails.itemCount, - publishedDate: publishedDate, - }); - - $(rendered).insertBefore('#getNextPage'); + playlists.forEach((playlist) => { + if (typeof (playlistIds) === 'undefined') { + playlistIds = playlist.id.playlistId; + } else { + playlistIds = playlistIds + ',' + playlist.id.playlistId; + } + }); + + ft.log('Playlist IDs: ', playlistIds); + + youtubeAPI('playlists', { + part: 'snippet,contentDetails', + id: playlistIds, + }, function (data) { + ft.log('Playlist Data: ', data); + let items = data['items'].reverse(); + const playlistListTemplate = require('./templates/playlistList.html'); + + ft.log('Playlist Items: ', items); + + items.forEach((item) => { + let dateString = new Date(item.snippet.publishedAt); + let publishedDate = dateFormat(dateString, "mmm dS, yyyy"); + + mustache.parse(playlistListTemplate); + let rendered = mustache.render(playlistListTemplate, { + channelId: item.snippet.channelId, + channelName: item.snippet.channelTitle, + playlistThumbnail: item.snippet.thumbnails.medium.url, + playlistTitle: item.snippet.title, + playlistDescription: item.snippet.description, + videoCount: item.contentDetails.itemCount, + publishedDate: publishedDate, + }); + + $(rendered).insertBefore('#getNextPage'); + }); }); - }); } /** @@ -277,22 +294,22 @@ function displayPlaylists(playlists) { * @return {Void} */ function addNextPage(nextPageToken) { - let oldFetchButton = document.getElementById('getNextPage'); + let oldFetchButton = document.getElementById('getNextPage'); - // Creates the element if it doesn't exist. - if (oldFetchButton === null) { - let fetchButton = document.createElement('div'); - fetchButton.id = 'getNextPage'; - fetchButton.innerHTML = ' Fetch more results...'; + // Creates the element if it doesn't exist. + if (oldFetchButton === null) { + let fetchButton = document.createElement('div'); + fetchButton.id = 'getNextPage'; + fetchButton.innerHTML = ' Fetch more results...'; - $('#videoListContainer').append(fetchButton); - } + $('#videoListContainer').append(fetchButton); + } - // Update the on click method of the button. - $(document).off('click', '#getNextPage'); - $(document).on('click', '#getNextPage', (event) => { - search(nextPageToken); - }); + // Update the on click method of the button. + $(document).off('click', '#getNextPage'); + $(document).on('click', '#getNextPage', (event) => { + search(nextPageToken); + }); } /** @@ -303,6 +320,8 @@ function addNextPage(nextPageToken) { * @param {string} videoId - The video ID of the video to get recommendations from. */ function showVideoRecommendations(videoId) { + playerView.recommendedVideoList = []; + youtubeAPI('search', { part: 'id', type: 'video', @@ -312,21 +331,17 @@ function showVideoRecommendations(videoId) { let grabDuration = getDuration(data.items); grabDuration.then((videoList) => { videoList.items.forEach((video) => { + let data = {} const snippet = video.snippet; - const videoDuration = parseVideoDuration(video.contentDetails.duration); - const recommTemplate = require('./templates/recommendations.html') - mustache.parse(recommTemplate); - const rendered = mustache.render(recommTemplate, { - videoId: video.id, - videoTitle: snippet.title, - channelName: snippet.channelTitle, - videoThumbnail: snippet.thumbnails.medium.url, - videoDuration: videoDuration, - publishedDate: dateFormat(snippet.publishedAt, "mmm dS, yyyy") - }); - const recommendationHtml = $('#recommendations').html(); - $('#recommendations').html(recommendationHtml + rendered); + data.duration = parseVideoDuration(video.contentDetails.duration); + data.id = video.id; + data.title = snippet.title; + data.channelName = snippet.channelTitle; + data.thumbnail = snippet.thumbnails.medium.url; + data.publishedDate = dateFormat(snippet.publishedAt, "mmm dS, yyyy"); + + playerView.recommendedVideoList = playerView.recommendedVideoList.concat(data); }); }); }); @@ -339,34 +354,52 @@ function showVideoRecommendations(videoId) { * @return {Void} */ function parseSearchText(url = '') { - let input; + let input; - if (url === ''){ - input = document.getElementById('search').value; - } - else{ - input = url; - } + if (url === '') { + input = document.getElementById('search').value; + } else { + input = url; + } - if (input === '') { - return; - } + if (input === '') { + return; + } - // The regex to get the video id from a YouTube link. Thanks StackOverflow. - let rx = /^.*(?:(?:(you|hook)tu\.?be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/; + // The regex to get the video id from a YouTube link. Thanks StackOverflow. + let rx = /^.*(?:(?:(you|hook)tu\.?be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/; - let match = input.match(rx); + let match = input.match(rx); + + ft.log('Video ID: ', match); + let urlSplit = input.split('/'); + if (match) { + ft.log('Video found'); + loadingView.seen = true; + playVideo(match[2]); + } else if (urlSplit[3] == 'channel') { + ft.log('channel found'); + loadingView.seen = true; + goToChannel(urlSplit[4]); + } else if (urlSplit[3] == 'user') { + ft.log('user found'); + // call api to get the ID and then call goToChannel(id) + youtubeAPI('channels', { + part: 'id', + forUsername: urlSplit[4] + }, (data) => { + ft.log('Channel Data: ', data.items[0].id); + let channelID = data.items[0].id; + loadingView.seen = true; + goToChannel(channelID); + }); + } else { + ft.log('Video not found'); + loadingView.seen = true; + search(); + } - console.log(match); - // Play video if a match is found. - try { - console.log('Match Found'); - playVideo(match[2]); - } catch (err) { - console.log('Video not found'); - search(); - } } /** @@ -377,42 +410,42 @@ function parseSearchText(url = '') { * @return {string} - The formated string. Ex: 12:34:56 */ function parseVideoDuration(durationString) { - let match = durationString.match(/PT(\d+H)?(\d+M)?(\d+S)?/); - let duration = ''; + let match = durationString.match(/P.*T(\d+H)?(\d+M)?(\d+S)?/); + let duration = ''; - match = match.slice(1).map(function(x) { - if (x != null) { - return x.replace(/\D/, ''); + match = match.slice(1).map(function (x) { + if (x != null) { + return x.replace(/\D/, ''); + } + }); + + let hours = (parseInt(match[0]) || 0); + let minutes = (parseInt(match[1]) || 0); + let seconds = (parseInt(match[2]) || 0); + + if (hours != 0) { + duration = hours + ':'; + } else { + duration = minutes + ':'; } - }); - let hours = (parseInt(match[0]) || 0); - let minutes = (parseInt(match[1]) || 0); - let seconds = (parseInt(match[2]) || 0); + if (hours != 0 && minutes < 10) { + duration = duration + '0' + minutes + ':'; + } else if (hours != 0 && minutes > 10) { + duration = duration + minutes + ':'; + } else if (hours != 0 && minutes == 0) { + duration = duration + '00:'; + } - if (hours != 0) { - duration = hours + ':'; - } else { - duration = minutes + ':'; - } + if (seconds == 0) { + duration = duration + '00'; + } else if (seconds < 10) { + duration = duration + '0' + seconds; + } else { + duration = duration + seconds; + } - if (hours != 0 && minutes < 10) { - duration = duration + '0' + minutes + ':'; - } else if (hours != 0 && minutes > 10) { - duration = duration + minutes + ':'; - } else if (hours != 0 && minutes == 0) { - duration = duration + '00:'; - } - - if (seconds == 0) { - duration = duration + '00'; - } else if (seconds < 10) { - duration = duration + '0' + seconds; - } else { - duration = duration + seconds; - } - - return duration; + return duration; } /** @@ -421,12 +454,10 @@ function parseVideoDuration(durationString) { * @return {Void} */ function showMostPopular() { - clearMainContainer(); - startLoadingAnimation(); - // Get the date of 2 days ago. - var d = new Date(); - d.setDate(d.getDate() - 2); + // Get the date of 2 days ago. + var d = new Date(); + d.setDate(d.getDate() - 2); // Grab all videos published 2 days ago and after and order them by view count. // These are the videos that are considered as 'most popular' and is how similar @@ -439,20 +470,23 @@ function showMostPopular() { publishedAfter: d.toISOString(), maxResults: 50, }, function(data) { - createVideoListContainer('Most Popular:'); + //createVideoListContainer('Most Popular:'); console.log(data); let grabDuration = getDuration(data.items); grabDuration.then((videoList) => { console.log(videoList); - videoList.items.forEach(displayVideo); + popularView.videoList = []; + loadingView.seen = false; + videoList.items.forEach((video) => { + displayVideo(video, 'popular'); + }); }); - stopLoadingAnimation(); }); } /** - * Create a link of the video to HookTube or YouTube and copy it to the user's clipboard. + * Create a link of the video to Invidious or YouTube and copy it to the user's clipboard. * * @param {string} website - The website to watch the video on. * @param {string} videoId - The video ID of the video to add to the URL @@ -460,10 +494,21 @@ function showMostPopular() { * @return {Void} */ function copyLink(website, videoId) { - // Create the URL and copy to the clipboard. - const url = 'https://' + website + '.com/watch?v=' + videoId; - clipboard.writeText(url); - showToast('URL has been copied to the clipboard'); + // Create the URL and copy to the clipboard. + if (website == "youtube") { + const url = 'https://' + website + '.com/watch?v=' + videoId; + clipboard.writeText(url); + showToast('URL has been copied to the clipboard'); + } + + if (website == "invidious") { + website = "invidio"; + const url = "https://" + website + ".us/watch?v=" + videoId; + clipboard.writeText(url); + showToast('URL has been copied to the clipboard'); + } + + } /** @@ -474,20 +519,20 @@ function copyLink(website, videoId) { * @return {promise} - The HTML of the embeded player */ function getChannelAndPlayer(videoId) { - console.log(videoId); - return new Promise((resolve, reject) => { - youtubeAPI('videos', { - part: 'snippet,player', - id: videoId, - }, function(data) { - let embedHtml = data.items[0].player.embedHtml; - embedHtml = embedHtml.replace('src="', 'src="https:'); - embedHtml = embedHtml.replace('width="480px"', ''); - embedHtml = embedHtml.replace('height="270px"', ''); - embedHtml = embedHtml.replace(/\"/g, '"'); - resolve([embedHtml, data.items[0].snippet.channelId]); + ft.log('Video ID: ', videoId); + return new Promise((resolve, reject) => { + youtubeAPI('videos', { + part: 'snippet,player', + id: videoId, + }, function (data) { + let embedHtml = data.items[0].player.embedHtml; + embedHtml = embedHtml.replace('src="', 'src="https:'); + embedHtml = embedHtml.replace('width="480px"', ''); + embedHtml = embedHtml.replace('height="270px"', ''); + embedHtml = embedHtml.replace(/\"/g, '"'); + resolve([embedHtml, data.items[0].snippet.channelId]); + }); }); - }); } /** @@ -499,68 +544,68 @@ function getChannelAndPlayer(videoId) { * @param {string} video720p - The URL to the 720p video. */ function checkVideoUrls(video480p, video720p) { - const currentQuality = $('#currentQuality').html(); - let buttonEmbed = document.getElementById('qualityEmbed'); + const currentQuality = $('#currentQuality').html(); + let buttonEmbed = document.getElementById('qualityEmbed'); - let valid480 = false; + let valid480 = false; - if (typeof(video480p) !== 'undefined') { - let get480pUrl = fetch(video480p); - get480pUrl.then((status) => { - switch (status.status) { - case 404: - showToast('Found valid URL for 480p, but returned a 404. Video type might be available in the future.'); - $(document).off('click', '#quality480p'); - $(document).on('click', '#quality480p', (event) => { - changeQuality(''); - }); - buttonEmbed.click(); - return; - break; - case 403: - showToast('This video is unavailable in your country.'); - $(document).off('click', '#quality480p'); - $(document).on('click', '#quality480p', (event) => { - changeQuality(''); - }); - return; - break; - default: - console.log('480p is valid'); - if (currentQuality === '720p' && typeof(video720p) === 'undefined') { - changeQuality(video480p); - } - break; - } - }); - } + if (typeof (video480p) !== 'undefined') { + let get480pUrl = fetch(video480p); + get480pUrl.then((status) => { + switch (status.status) { + case 404: + showToast('Found valid URL for 480p, but returned a 404. Video type might be available in the future.'); + $(document).off('click', '#quality480p'); + $(document).on('click', '#quality480p', (event) => { + changeQuality(''); + }); + buttonEmbed.click(); + return; + break; + case 403: + showToast('This video is unavailable in your country.'); + $(document).off('click', '#quality480p'); + $(document).on('click', '#quality480p', (event) => { + changeQuality(''); + }); + return; + break; + default: + ft.log('480p is valid'); + if (currentQuality === '720p' && typeof (video720p) === 'undefined') { + changeQuality(video480p); + } + break; + } + }); + } - if (typeof(video720p) !== 'undefined') { - let get720pUrl = fetch(video720p); - get720pUrl.then((status) => { - switch (status.status) { - case 404: - showToast('Found valid URL for 720p, but returned a 404. Video type might be available in the future.'); - $(document).off('click', '#quality720p'); - $(document).on('click', '#quality720p', (event) => { - changeQuality(''); - }); - if (typeof(valid480) !== 'undefined') { - changeQuality(video480p, '480p'); - } - break; - case 403: - showToast('This video is unavailable in your country.'); - $(document).off('click', '#quality720p'); - $(document).on('click', '#quality720p', (event) => { - changeQuality(''); - }); - return; - break; - default: - console.log('720p is valid'); - break; - } - }); - } + if (typeof (video720p) !== 'undefined') { + let get720pUrl = fetch(video720p); + get720pUrl.then((status) => { + switch (status.status) { + case 404: + showToast('Found valid URL for 720p, but returned a 404. Video type might be available in the future.'); + $(document).off('click', '#quality720p'); + $(document).on('click', '#quality720p', (event) => { + changeQuality(''); + }); + if (typeof (valid480) !== 'undefined') { + changeQuality(video480p, '480p'); + } + break; + case 403: + showToast('This video is unavailable in your country.'); + $(document).off('click', '#quality720p'); + $(document).on('click', '#quality720p', (event) => { + changeQuality(''); + }); + return; + break; + default: + ft.log('720p is valid'); + break; + } + }); + } } diff --git a/src/js/vue.js b/src/js/vue.js new file mode 100644 index 000000000..657cb37ea --- /dev/null +++ b/src/js/vue.js @@ -0,0 +1,10947 @@ +/*! + * Vue.js v2.5.16 + * (c) 2014-2018 Evan You + * Released under the MIT License. + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.Vue = factory()); +}(this, (function () { 'use strict'; + +/* */ + +var emptyObject = Object.freeze({}); + +// these helpers produces better vm code in JS engines due to their +// explicitness and function inlining +function isUndef (v) { + return v === undefined || v === null +} + +function isDef (v) { + return v !== undefined && v !== null +} + +function isTrue (v) { + return v === true +} + +function isFalse (v) { + return v === false +} + +/** + * Check if value is primitive + */ +function isPrimitive (value) { + return ( + typeof value === 'string' || + typeof value === 'number' || + // $flow-disable-line + typeof value === 'symbol' || + typeof value === 'boolean' + ) +} + +/** + * Quick object check - this is primarily used to tell + * Objects from primitive values when we know the value + * is a JSON-compliant type. + */ +function isObject (obj) { + return obj !== null && typeof obj === 'object' +} + +/** + * Get the raw type string of a value e.g. [object Object] + */ +var _toString = Object.prototype.toString; + +function toRawType (value) { + return _toString.call(value).slice(8, -1) +} + +/** + * Strict object type check. Only returns true + * for plain JavaScript objects. + */ +function isPlainObject (obj) { + return _toString.call(obj) === '[object Object]' +} + +function isRegExp (v) { + return _toString.call(v) === '[object RegExp]' +} + +/** + * Check if val is a valid array index. + */ +function isValidArrayIndex (val) { + var n = parseFloat(String(val)); + return n >= 0 && Math.floor(n) === n && isFinite(val) +} + +/** + * Convert a value to a string that is actually rendered. + */ +function toString (val) { + return val == null + ? '' + : typeof val === 'object' + ? JSON.stringify(val, null, 2) + : String(val) +} + +/** + * Convert a input value to a number for persistence. + * If the conversion fails, return original string. + */ +function toNumber (val) { + var n = parseFloat(val); + return isNaN(n) ? val : n +} + +/** + * Make a map and return a function for checking if a key + * is in that map. + */ +function makeMap ( + str, + expectsLowerCase +) { + var map = Object.create(null); + var list = str.split(','); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase + ? function (val) { return map[val.toLowerCase()]; } + : function (val) { return map[val]; } +} + +/** + * Check if a tag is a built-in tag. + */ +var isBuiltInTag = makeMap('slot,component', true); + +/** + * Check if a attribute is a reserved attribute. + */ +var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + +/** + * Remove an item from an array + */ +function remove (arr, item) { + if (arr.length) { + var index = arr.indexOf(item); + if (index > -1) { + return arr.splice(index, 1) + } + } +} + +/** + * Check whether the object has the property. + */ +var hasOwnProperty = Object.prototype.hasOwnProperty; +function hasOwn (obj, key) { + return hasOwnProperty.call(obj, key) +} + +/** + * Create a cached version of a pure function. + */ +function cached (fn) { + var cache = Object.create(null); + return (function cachedFn (str) { + var hit = cache[str]; + return hit || (cache[str] = fn(str)) + }) +} + +/** + * Camelize a hyphen-delimited string. + */ +var camelizeRE = /-(\w)/g; +var camelize = cached(function (str) { + return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) +}); + +/** + * Capitalize a string. + */ +var capitalize = cached(function (str) { + return str.charAt(0).toUpperCase() + str.slice(1) +}); + +/** + * Hyphenate a camelCase string. + */ +var hyphenateRE = /\B([A-Z])/g; +var hyphenate = cached(function (str) { + return str.replace(hyphenateRE, '-$1').toLowerCase() +}); + +/** + * Simple bind polyfill for environments that do not support it... e.g. + * PhantomJS 1.x. Technically we don't need this anymore since native bind is + * now more performant in most browsers, but removing it would be breaking for + * code that was able to run in PhantomJS 1.x, so this must be kept for + * backwards compatibility. + */ + +/* istanbul ignore next */ +function polyfillBind (fn, ctx) { + function boundFn (a) { + var l = arguments.length; + return l + ? l > 1 + ? fn.apply(ctx, arguments) + : fn.call(ctx, a) + : fn.call(ctx) + } + + boundFn._length = fn.length; + return boundFn +} + +function nativeBind (fn, ctx) { + return fn.bind(ctx) +} + +var bind = Function.prototype.bind + ? nativeBind + : polyfillBind; + +/** + * Convert an Array-like object to a real Array. + */ +function toArray (list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret +} + +/** + * Mix properties into target object. + */ +function extend (to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to +} + +/** + * Merge an Array of Objects into a single Object. + */ +function toObject (arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res +} + +/** + * Perform no operation. + * Stubbing args to make Flow happy without leaving useless transpiled code + * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/) + */ +function noop (a, b, c) {} + +/** + * Always return false. + */ +var no = function (a, b, c) { return false; }; + +/** + * Return same value + */ +var identity = function (_) { return _; }; + +/** + * Generate a static keys string from compiler modules. + */ +function genStaticKeys (modules) { + return modules.reduce(function (keys, m) { + return keys.concat(m.staticKeys || []) + }, []).join(',') +} + +/** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + */ +function looseEqual (a, b) { + if (a === b) { return true } + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return a.length === b.length && a.every(function (e, i) { + return looseEqual(e, b[i]) + }) + } else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function (key) { + return looseEqual(a[key], b[key]) + }) + } else { + /* istanbul ignore next */ + return false + } + } catch (e) { + /* istanbul ignore next */ + return false + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b) + } else { + return false + } +} + +function looseIndexOf (arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) { return i } + } + return -1 +} + +/** + * Ensure a function is called only once. + */ +function once (fn) { + var called = false; + return function () { + if (!called) { + called = true; + fn.apply(this, arguments); + } + } +} + +var SSR_ATTR = 'data-server-rendered'; + +var ASSET_TYPES = [ + 'component', + 'directive', + 'filter' +]; + +var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured' +]; + +/* */ + +var config = ({ + /** + * Option merge strategies (used in core/util/options) + */ + // $flow-disable-line + optionMergeStrategies: Object.create(null), + + /** + * Whether to suppress warnings. + */ + silent: false, + + /** + * Show production mode tip message on boot? + */ + productionTip: "development" !== 'production', + + /** + * Whether to enable devtools + */ + devtools: "development" !== 'production', + + /** + * Whether to record perf + */ + performance: false, + + /** + * Error handler for watcher errors + */ + errorHandler: null, + + /** + * Warn handler for watcher warns + */ + warnHandler: null, + + /** + * Ignore certain custom elements + */ + ignoredElements: [], + + /** + * Custom user key aliases for v-on + */ + // $flow-disable-line + keyCodes: Object.create(null), + + /** + * Check if a tag is reserved so that it cannot be registered as a + * component. This is platform-dependent and may be overwritten. + */ + isReservedTag: no, + + /** + * Check if an attribute is reserved so that it cannot be used as a component + * prop. This is platform-dependent and may be overwritten. + */ + isReservedAttr: no, + + /** + * Check if a tag is an unknown element. + * Platform-dependent. + */ + isUnknownElement: no, + + /** + * Get the namespace of an element + */ + getTagNamespace: noop, + + /** + * Parse the real tag name for the specific platform. + */ + parsePlatformTagName: identity, + + /** + * Check if an attribute must be bound using property, e.g. value + * Platform-dependent. + */ + mustUseProp: no, + + /** + * Exposed for legacy reasons + */ + _lifecycleHooks: LIFECYCLE_HOOKS +}) + +/* */ + +/** + * Check if a string starts with $ or _ + */ +function isReserved (str) { + var c = (str + '').charCodeAt(0); + return c === 0x24 || c === 0x5F +} + +/** + * Define a property. + */ +function def (obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); +} + +/** + * Parse simple path. + */ +var bailRE = /[^\w.$]/; +function parsePath (path) { + if (bailRE.test(path)) { + return + } + var segments = path.split('.'); + return function (obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) { return } + obj = obj[segments[i]]; + } + return obj + } +} + +/* */ + +// can we use __proto__? +var hasProto = '__proto__' in {}; + +// Browser environment sniffing +var inBrowser = typeof window !== 'undefined'; +var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; +var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); +var UA = inBrowser && window.navigator.userAgent.toLowerCase(); +var isIE = UA && /msie|trident/.test(UA); +var isIE9 = UA && UA.indexOf('msie 9.0') > 0; +var isEdge = UA && UA.indexOf('edge/') > 0; +var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); +var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); +var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; + +// Firefox has a "watch" function on Object.prototype... +var nativeWatch = ({}).watch; + +var supportsPassive = false; +if (inBrowser) { + try { + var opts = {}; + Object.defineProperty(opts, 'passive', ({ + get: function get () { + /* istanbul ignore next */ + supportsPassive = true; + } + })); // https://github.com/facebook/flow/issues/285 + window.addEventListener('test-passive', null, opts); + } catch (e) {} +} + +// this needs to be lazy-evaled because vue may be required before +// vue-server-renderer can set VUE_ENV +var _isServer; +var isServerRendering = function () { + if (_isServer === undefined) { + /* istanbul ignore if */ + if (!inBrowser && !inWeex && typeof global !== 'undefined') { + // detect presence of vue-server-renderer and avoid + // Webpack shimming the process + _isServer = global['process'].env.VUE_ENV === 'server'; + } else { + _isServer = false; + } + } + return _isServer +}; + +// detect devtools +var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + +/* istanbul ignore next */ +function isNative (Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) +} + +var hasSymbol = + typeof Symbol !== 'undefined' && isNative(Symbol) && + typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); + +var _Set; +/* istanbul ignore if */ // $flow-disable-line +if (typeof Set !== 'undefined' && isNative(Set)) { + // use native Set when available. + _Set = Set; +} else { + // a non-standard Set polyfill that only works with primitive keys. + _Set = (function () { + function Set () { + this.set = Object.create(null); + } + Set.prototype.has = function has (key) { + return this.set[key] === true + }; + Set.prototype.add = function add (key) { + this.set[key] = true; + }; + Set.prototype.clear = function clear () { + this.set = Object.create(null); + }; + + return Set; + }()); +} + +/* */ + +var warn = noop; +var tip = noop; +var generateComponentTrace = (noop); // work around flow check +var formatComponentName = (noop); + +{ + var hasConsole = typeof console !== 'undefined'; + var classifyRE = /(?:^|[-_])(\w)/g; + var classify = function (str) { return str + .replace(classifyRE, function (c) { return c.toUpperCase(); }) + .replace(/[-_]/g, ''); }; + + warn = function (msg, vm) { + var trace = vm ? generateComponentTrace(vm) : ''; + + if (config.warnHandler) { + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && (!config.silent)) { + console.error(("[Vue warn]: " + msg + trace)); + } + }; + + tip = function (msg, vm) { + if (hasConsole && (!config.silent)) { + console.warn("[Vue tip]: " + msg + ( + vm ? generateComponentTrace(vm) : '' + )); + } + }; + + formatComponentName = function (vm, includeFile) { + if (vm.$root === vm) { + return '' + } + var options = typeof vm === 'function' && vm.cid != null + ? vm.options + : vm._isVue + ? vm.$options || vm.constructor.options + : vm || {}; + var name = options.name || options._componentTag; + var file = options.__file; + if (!name && file) { + var match = file.match(/([^/\\]+)\.vue$/); + name = match && match[1]; + } + + return ( + (name ? ("<" + (classify(name)) + ">") : "") + + (file && includeFile !== false ? (" at " + file) : '') + ) + }; + + var repeat = function (str, n) { + var res = ''; + while (n) { + if (n % 2 === 1) { res += str; } + if (n > 1) { str += str; } + n >>= 1; + } + return res + }; + + generateComponentTrace = function (vm) { + if (vm._isVue && vm.$parent) { + var tree = []; + var currentRecursiveSequence = 0; + while (vm) { + if (tree.length > 0) { + var last = tree[tree.length - 1]; + if (last.constructor === vm.constructor) { + currentRecursiveSequence++; + vm = vm.$parent; + continue + } else if (currentRecursiveSequence > 0) { + tree[tree.length - 1] = [last, currentRecursiveSequence]; + currentRecursiveSequence = 0; + } + } + tree.push(vm); + vm = vm.$parent; + } + return '\n\nfound in\n\n' + tree + .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) + ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") + : formatComponentName(vm))); }) + .join('\n') + } else { + return ("\n\n(found in " + (formatComponentName(vm)) + ")") + } + }; +} + +/* */ + + +var uid = 0; + +/** + * A dep is an observable that can have multiple + * directives subscribing to it. + */ +var Dep = function Dep () { + this.id = uid++; + this.subs = []; +}; + +Dep.prototype.addSub = function addSub (sub) { + this.subs.push(sub); +}; + +Dep.prototype.removeSub = function removeSub (sub) { + remove(this.subs, sub); +}; + +Dep.prototype.depend = function depend () { + if (Dep.target) { + Dep.target.addDep(this); + } +}; + +Dep.prototype.notify = function notify () { + // stabilize the subscriber list first + var subs = this.subs.slice(); + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } +}; + +// the current target watcher being evaluated. +// this is globally unique because there could be only one +// watcher being evaluated at any time. +Dep.target = null; +var targetStack = []; + +function pushTarget (_target) { + if (Dep.target) { targetStack.push(Dep.target); } + Dep.target = _target; +} + +function popTarget () { + Dep.target = targetStack.pop(); +} + +/* */ + +var VNode = function VNode ( + tag, + data, + children, + text, + elm, + context, + componentOptions, + asyncFactory +) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text; + this.elm = elm; + this.ns = undefined; + this.context = context; + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = undefined; + this.parent = undefined; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; +}; + +var prototypeAccessors = { child: { configurable: true } }; + +// DEPRECATED: alias for componentInstance for backwards compat. +/* istanbul ignore next */ +prototypeAccessors.child.get = function () { + return this.componentInstance +}; + +Object.defineProperties( VNode.prototype, prototypeAccessors ); + +var createEmptyVNode = function (text) { + if ( text === void 0 ) text = ''; + + var node = new VNode(); + node.text = text; + node.isComment = true; + return node +}; + +function createTextVNode (val) { + return new VNode(undefined, undefined, undefined, String(val)) +} + +// optimized shallow clone +// used for static nodes and slot nodes because they may be reused across +// multiple renders, cloning them avoids errors when DOM manipulations rely +// on their elm reference. +function cloneVNode (vnode) { + var cloned = new VNode( + vnode.tag, + vnode.data, + vnode.children, + vnode.text, + vnode.elm, + vnode.context, + vnode.componentOptions, + vnode.asyncFactory + ); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.isCloned = true; + return cloned +} + +/* + * not type checking this file because flow doesn't play well with + * dynamically accessing methods on Array prototype + */ + +var arrayProto = Array.prototype; +var arrayMethods = Object.create(arrayProto); + +var methodsToPatch = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' +]; + +/** + * Intercept mutating methods and emit events + */ +methodsToPatch.forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + case 'unshift': + inserted = args; + break + case 'splice': + inserted = args.slice(2); + break + } + if (inserted) { ob.observeArray(inserted); } + // notify change + ob.dep.notify(); + return result + }); +}); + +/* */ + +var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + +/** + * In some cases we may want to disable observation inside a component's + * update computation. + */ +var shouldObserve = true; + +function toggleObserving (value) { + shouldObserve = value; +} + +/** + * Observer class that is attached to each observed + * object. Once attached, the observer converts the target + * object's property keys into getter/setters that + * collect dependencies and dispatch updates. + */ +var Observer = function Observer (value) { + this.value = value; + this.dep = new Dep(); + this.vmCount = 0; + def(value, '__ob__', this); + if (Array.isArray(value)) { + var augment = hasProto + ? protoAugment + : copyAugment; + augment(value, arrayMethods, arrayKeys); + this.observeArray(value); + } else { + this.walk(value); + } +}; + +/** + * Walk through each property and convert them into + * getter/setters. This method should only be called when + * value type is Object. + */ +Observer.prototype.walk = function walk (obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + defineReactive(obj, keys[i]); + } +}; + +/** + * Observe a list of Array items. + */ +Observer.prototype.observeArray = function observeArray (items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } +}; + +// helpers + +/** + * Augment an target Object or Array by intercepting + * the prototype chain using __proto__ + */ +function protoAugment (target, src, keys) { + /* eslint-disable no-proto */ + target.__proto__ = src; + /* eslint-enable no-proto */ +} + +/** + * Augment an target Object or Array by defining + * hidden properties. + */ +/* istanbul ignore next */ +function copyAugment (target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target, key, src[key]); + } +} + +/** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + */ +function observe (value, asRootData) { + if (!isObject(value) || value instanceof VNode) { + return + } + var ob; + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if ( + shouldObserve && + !isServerRendering() && + (Array.isArray(value) || isPlainObject(value)) && + Object.isExtensible(value) && + !value._isVue + ) { + ob = new Observer(value); + } + if (asRootData && ob) { + ob.vmCount++; + } + return ob +} + +/** + * Define a reactive property on an Object. + */ +function defineReactive ( + obj, + key, + val, + customSetter, + shallow +) { + var dep = new Dep(); + + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return + } + + // cater for pre-defined getter/setters + var getter = property && property.get; + if (!getter && arguments.length === 2) { + val = obj[key]; + } + var setter = property && property.set; + + var childOb = !shallow && observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter () { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + if (Array.isArray(value)) { + dependArray(value); + } + } + } + return value + }, + set: function reactiveSetter (newVal) { + var value = getter ? getter.call(obj) : val; + /* eslint-disable no-self-compare */ + if (newVal === value || (newVal !== newVal && value !== value)) { + return + } + /* eslint-enable no-self-compare */ + if ("development" !== 'production' && customSetter) { + customSetter(); + } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = !shallow && observe(newVal); + dep.notify(); + } + }); +} + +/** + * Set a property on an object. Adds the new property and + * triggers change notification if the property doesn't + * already exist. + */ +function set (target, key, val) { + if ("development" !== 'production' && + (isUndef(target) || isPrimitive(target)) + ) { + warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.length = Math.max(target.length, key); + target.splice(key, 1, val); + return val + } + if (key in target && !(key in Object.prototype)) { + target[key] = val; + return val + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + "development" !== 'production' && warn( + 'Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.' + ); + return val + } + if (!ob) { + target[key] = val; + return val + } + defineReactive(ob.value, key, val); + ob.dep.notify(); + return val +} + +/** + * Delete a property and trigger change if necessary. + */ +function del (target, key) { + if ("development" !== 'production' && + (isUndef(target) || isPrimitive(target)) + ) { + warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.splice(key, 1); + return + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + "development" !== 'production' && warn( + 'Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.' + ); + return + } + if (!hasOwn(target, key)) { + return + } + delete target[key]; + if (!ob) { + return + } + ob.dep.notify(); +} + +/** + * Collect dependencies on array elements when the array is touched, since + * we cannot intercept array element access like property getters. + */ +function dependArray (value) { + for (var e = (void 0), i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + if (Array.isArray(e)) { + dependArray(e); + } + } +} + +/* */ + +/** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + */ +var strats = config.optionMergeStrategies; + +/** + * Options with restrictions + */ +{ + strats.el = strats.propsData = function (parent, child, vm, key) { + if (!vm) { + warn( + "option \"" + key + "\" can only be used during instance " + + 'creation with the `new` keyword.' + ); + } + return defaultStrat(parent, child) + }; +} + +/** + * Helper that recursively merges two data objects together. + */ +function mergeData (to, from) { + if (!from) { return to } + var key, toVal, fromVal; + var keys = Object.keys(from); + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if (isPlainObject(toVal) && isPlainObject(fromVal)) { + mergeData(toVal, fromVal); + } + } + return to +} + +/** + * Data + */ +function mergeDataOrFn ( + parentVal, + childVal, + vm +) { + if (!vm) { + // in a Vue.extend merge, both should be functions + if (!childVal) { + return parentVal + } + if (!parentVal) { + return childVal + } + // when parentVal & childVal are both present, + // we need to return a function that returns the + // merged result of both functions... no need to + // check if parentVal is a function here because + // it has to be a function to pass previous merges. + return function mergedDataFn () { + return mergeData( + typeof childVal === 'function' ? childVal.call(this, this) : childVal, + typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal + ) + } + } else { + return function mergedInstanceDataFn () { + // instance merge + var instanceData = typeof childVal === 'function' + ? childVal.call(vm, vm) + : childVal; + var defaultData = typeof parentVal === 'function' + ? parentVal.call(vm, vm) + : parentVal; + if (instanceData) { + return mergeData(instanceData, defaultData) + } else { + return defaultData + } + } + } +} + +strats.data = function ( + parentVal, + childVal, + vm +) { + if (!vm) { + if (childVal && typeof childVal !== 'function') { + "development" !== 'production' && warn( + 'The "data" option should be a function ' + + 'that returns a per-instance value in component ' + + 'definitions.', + vm + ); + + return parentVal + } + return mergeDataOrFn(parentVal, childVal) + } + + return mergeDataOrFn(parentVal, childVal, vm) +}; + +/** + * Hooks and props are merged as arrays. + */ +function mergeHook ( + parentVal, + childVal +) { + return childVal + ? parentVal + ? parentVal.concat(childVal) + : Array.isArray(childVal) + ? childVal + : [childVal] + : parentVal +} + +LIFECYCLE_HOOKS.forEach(function (hook) { + strats[hook] = mergeHook; +}); + +/** + * Assets + * + * When a vm is present (instance creation), we need to do + * a three-way merge between constructor options, instance + * options and parent options. + */ +function mergeAssets ( + parentVal, + childVal, + vm, + key +) { + var res = Object.create(parentVal || null); + if (childVal) { + "development" !== 'production' && assertObjectType(key, childVal, vm); + return extend(res, childVal) + } else { + return res + } +} + +ASSET_TYPES.forEach(function (type) { + strats[type + 's'] = mergeAssets; +}); + +/** + * Watchers. + * + * Watchers hashes should not overwrite one + * another, so we merge them as arrays. + */ +strats.watch = function ( + parentVal, + childVal, + vm, + key +) { + // work around Firefox's Object.prototype.watch... + if (parentVal === nativeWatch) { parentVal = undefined; } + if (childVal === nativeWatch) { childVal = undefined; } + /* istanbul ignore if */ + if (!childVal) { return Object.create(parentVal || null) } + { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = {}; + extend(ret, parentVal); + for (var key$1 in childVal) { + var parent = ret[key$1]; + var child = childVal[key$1]; + if (parent && !Array.isArray(parent)) { + parent = [parent]; + } + ret[key$1] = parent + ? parent.concat(child) + : Array.isArray(child) ? child : [child]; + } + return ret +}; + +/** + * Other object hashes. + */ +strats.props = +strats.methods = +strats.inject = +strats.computed = function ( + parentVal, + childVal, + vm, + key +) { + if (childVal && "development" !== 'production') { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = Object.create(null); + extend(ret, parentVal); + if (childVal) { extend(ret, childVal); } + return ret +}; +strats.provide = mergeDataOrFn; + +/** + * Default strategy. + */ +var defaultStrat = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal +}; + +/** + * Validate component names + */ +function checkComponents (options) { + for (var key in options.components) { + validateComponentName(key); + } +} + +function validateComponentName (name) { + if (!/^[a-zA-Z][\w-]*$/.test(name)) { + warn( + 'Invalid component name: "' + name + '". Component names ' + + 'can only contain alphanumeric characters and the hyphen, ' + + 'and must start with a letter.' + ); + } + if (isBuiltInTag(name) || config.isReservedTag(name)) { + warn( + 'Do not use built-in or reserved HTML elements as component ' + + 'id: ' + name + ); + } +} + +/** + * Ensure all props option syntax are normalized into the + * Object-based format. + */ +function normalizeProps (options, vm) { + var props = options.props; + if (!props) { return } + var res = {}; + var i, val, name; + if (Array.isArray(props)) { + i = props.length; + while (i--) { + val = props[i]; + if (typeof val === 'string') { + name = camelize(val); + res[name] = { type: null }; + } else { + warn('props must be strings when using array syntax.'); + } + } + } else if (isPlainObject(props)) { + for (var key in props) { + val = props[key]; + name = camelize(key); + res[name] = isPlainObject(val) + ? val + : { type: val }; + } + } else { + warn( + "Invalid value for option \"props\": expected an Array or an Object, " + + "but got " + (toRawType(props)) + ".", + vm + ); + } + options.props = res; +} + +/** + * Normalize all injections into Object-based format + */ +function normalizeInject (options, vm) { + var inject = options.inject; + if (!inject) { return } + var normalized = options.inject = {}; + if (Array.isArray(inject)) { + for (var i = 0; i < inject.length; i++) { + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + for (var key in inject) { + var val = inject[key]; + normalized[key] = isPlainObject(val) + ? extend({ from: key }, val) + : { from: val }; + } + } else { + warn( + "Invalid value for option \"inject\": expected an Array or an Object, " + + "but got " + (toRawType(inject)) + ".", + vm + ); + } +} + +/** + * Normalize raw function directives into object format. + */ +function normalizeDirectives (options) { + var dirs = options.directives; + if (dirs) { + for (var key in dirs) { + var def = dirs[key]; + if (typeof def === 'function') { + dirs[key] = { bind: def, update: def }; + } + } + } +} + +function assertObjectType (name, value, vm) { + if (!isPlainObject(value)) { + warn( + "Invalid value for option \"" + name + "\": expected an Object, " + + "but got " + (toRawType(value)) + ".", + vm + ); + } +} + +/** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + */ +function mergeOptions ( + parent, + child, + vm +) { + { + checkComponents(child); + } + + if (typeof child === 'function') { + child = child.options; + } + + normalizeProps(child, vm); + normalizeInject(child, vm); + normalizeDirectives(child); + var extendsFrom = child.extends; + if (extendsFrom) { + parent = mergeOptions(parent, extendsFrom, vm); + } + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + var options = {}; + var key; + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField (key) { + var strat = strats[key] || defaultStrat; + options[key] = strat(parent[key], child[key], vm, key); + } + return options +} + +/** + * Resolve an asset. + * This function is used because child instances need access + * to assets defined in its ancestor chain. + */ +function resolveAsset ( + options, + type, + id, + warnMissing +) { + /* istanbul ignore if */ + if (typeof id !== 'string') { + return + } + var assets = options[type]; + // check local registration variations first + if (hasOwn(assets, id)) { return assets[id] } + var camelizedId = camelize(id); + if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } + var PascalCaseId = capitalize(camelizedId); + if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } + // fallback to prototype chain + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + if ("development" !== 'production' && warnMissing && !res) { + warn( + 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, + options + ); + } + return res +} + +/* */ + +function validateProp ( + key, + propOptions, + propsData, + vm +) { + var prop = propOptions[key]; + var absent = !hasOwn(propsData, key); + var value = propsData[key]; + // boolean casting + var booleanIndex = getTypeIndex(Boolean, prop.type); + if (booleanIndex > -1) { + if (absent && !hasOwn(prop, 'default')) { + value = false; + } else if (value === '' || value === hyphenate(key)) { + // only cast empty string / same name to boolean if + // boolean has higher priority + var stringIndex = getTypeIndex(String, prop.type); + if (stringIndex < 0 || booleanIndex < stringIndex) { + value = true; + } + } + } + // check default value + if (value === undefined) { + value = getPropDefaultValue(vm, prop, key); + // since the default value is a fresh copy, + // make sure to observe it. + var prevShouldObserve = shouldObserve; + toggleObserving(true); + observe(value); + toggleObserving(prevShouldObserve); + } + { + assertProp(prop, key, value, vm, absent); + } + return value +} + +/** + * Get the default value of a prop. + */ +function getPropDefaultValue (vm, prop, key) { + // no default, return undefined + if (!hasOwn(prop, 'default')) { + return undefined + } + var def = prop.default; + // warn against non-factory defaults for Object & Array + if ("development" !== 'production' && isObject(def)) { + warn( + 'Invalid default value for prop "' + key + '": ' + + 'Props with type Object/Array must use a factory function ' + + 'to return the default value.', + vm + ); + } + // the raw prop value was also undefined from previous render, + // return previous default value to avoid unnecessary watcher trigger + if (vm && vm.$options.propsData && + vm.$options.propsData[key] === undefined && + vm._props[key] !== undefined + ) { + return vm._props[key] + } + // call factory function for non-Function types + // a value is Function if its prototype is function even across different execution context + return typeof def === 'function' && getType(prop.type) !== 'Function' + ? def.call(vm) + : def +} + +/** + * Assert whether a prop is valid. + */ +function assertProp ( + prop, + name, + value, + vm, + absent +) { + if (prop.required && absent) { + warn( + 'Missing required prop: "' + name + '"', + vm + ); + return + } + if (value == null && !prop.required) { + return + } + var type = prop.type; + var valid = !type || type === true; + var expectedTypes = []; + if (type) { + if (!Array.isArray(type)) { + type = [type]; + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ''); + valid = assertedType.valid; + } + } + if (!valid) { + warn( + "Invalid prop: type check failed for prop \"" + name + "\"." + + " Expected " + (expectedTypes.map(capitalize).join(', ')) + + ", got " + (toRawType(value)) + ".", + vm + ); + return + } + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ); + } + } +} + +var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + +function assertType (value, type) { + var valid; + var expectedType = getType(type); + if (simpleCheckRE.test(expectedType)) { + var t = typeof value; + valid = t === expectedType.toLowerCase(); + // for primitive wrapper objects + if (!valid && t === 'object') { + valid = value instanceof type; + } + } else if (expectedType === 'Object') { + valid = isPlainObject(value); + } else if (expectedType === 'Array') { + valid = Array.isArray(value); + } else { + valid = value instanceof type; + } + return { + valid: valid, + expectedType: expectedType + } +} + +/** + * Use function string name to check built-in types, + * because a simple equality check will fail when running + * across different vms / iframes. + */ +function getType (fn) { + var match = fn && fn.toString().match(/^\s*function (\w+)/); + return match ? match[1] : '' +} + +function isSameType (a, b) { + return getType(a) === getType(b) +} + +function getTypeIndex (type, expectedTypes) { + if (!Array.isArray(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1 + } + for (var i = 0, len = expectedTypes.length; i < len; i++) { + if (isSameType(expectedTypes[i], type)) { + return i + } + } + return -1 +} + +/* */ + +function handleError (err, vm, info) { + if (vm) { + var cur = vm; + while ((cur = cur.$parent)) { + var hooks = cur.$options.errorCaptured; + if (hooks) { + for (var i = 0; i < hooks.length; i++) { + try { + var capture = hooks[i].call(cur, err, vm, info) === false; + if (capture) { return } + } catch (e) { + globalHandleError(e, cur, 'errorCaptured hook'); + } + } + } + } + } + globalHandleError(err, vm, info); +} + +function globalHandleError (err, vm, info) { + if (config.errorHandler) { + try { + return config.errorHandler.call(null, err, vm, info) + } catch (e) { + logError(e, null, 'config.errorHandler'); + } + } + logError(err, vm, info); +} + +function logError (err, vm, info) { + { + warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); + } + /* istanbul ignore else */ + if ((inBrowser || inWeex) && typeof console !== 'undefined') { + console.error(err); + } else { + throw err + } +} + +/* */ +/* globals MessageChannel */ + +var callbacks = []; +var pending = false; + +function flushCallbacks () { + pending = false; + var copies = callbacks.slice(0); + callbacks.length = 0; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } +} + +// Here we have async deferring wrappers using both microtasks and (macro) tasks. +// In < 2.4 we used microtasks everywhere, but there are some scenarios where +// microtasks have too high a priority and fire in between supposedly +// sequential events (e.g. #4521, #6690) or even between bubbling of the same +// event (#6566). However, using (macro) tasks everywhere also has subtle problems +// when state is changed right before repaint (e.g. #6813, out-in transitions). +// Here we use microtask by default, but expose a way to force (macro) task when +// needed (e.g. in event handlers attached by v-on). +var microTimerFunc; +var macroTimerFunc; +var useMacroTask = false; + +// Determine (macro) task defer implementation. +// Technically setImmediate should be the ideal choice, but it's only available +// in IE. The only polyfill that consistently queues the callback after all DOM +// events triggered in the same loop is by using MessageChannel. +/* istanbul ignore if */ +if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { + macroTimerFunc = function () { + setImmediate(flushCallbacks); + }; +} else if (typeof MessageChannel !== 'undefined' && ( + isNative(MessageChannel) || + // PhantomJS + MessageChannel.toString() === '[object MessageChannelConstructor]' +)) { + var channel = new MessageChannel(); + var port = channel.port2; + channel.port1.onmessage = flushCallbacks; + macroTimerFunc = function () { + port.postMessage(1); + }; +} else { + /* istanbul ignore next */ + macroTimerFunc = function () { + setTimeout(flushCallbacks, 0); + }; +} + +// Determine microtask defer implementation. +/* istanbul ignore next, $flow-disable-line */ +if (typeof Promise !== 'undefined' && isNative(Promise)) { + var p = Promise.resolve(); + microTimerFunc = function () { + p.then(flushCallbacks); + // in problematic UIWebViews, Promise.then doesn't completely break, but + // it can get stuck in a weird state where callbacks are pushed into the + // microtask queue but the queue isn't being flushed, until the browser + // needs to do some other work, e.g. handle a timer. Therefore we can + // "force" the microtask queue to be flushed by adding an empty timer. + if (isIOS) { setTimeout(noop); } + }; +} else { + // fallback to macro + microTimerFunc = macroTimerFunc; +} + +/** + * Wrap a function so that if any code inside triggers state change, + * the changes are queued using a (macro) task instead of a microtask. + */ +function withMacroTask (fn) { + return fn._withTask || (fn._withTask = function () { + useMacroTask = true; + var res = fn.apply(null, arguments); + useMacroTask = false; + return res + }) +} + +function nextTick (cb, ctx) { + var _resolve; + callbacks.push(function () { + if (cb) { + try { + cb.call(ctx); + } catch (e) { + handleError(e, ctx, 'nextTick'); + } + } else if (_resolve) { + _resolve(ctx); + } + }); + if (!pending) { + pending = true; + if (useMacroTask) { + macroTimerFunc(); + } else { + microTimerFunc(); + } + } + // $flow-disable-line + if (!cb && typeof Promise !== 'undefined') { + return new Promise(function (resolve) { + _resolve = resolve; + }) + } +} + +/* */ + +var mark; +var measure; + +{ + var perf = inBrowser && window.performance; + /* istanbul ignore if */ + if ( + perf && + perf.mark && + perf.measure && + perf.clearMarks && + perf.clearMeasures + ) { + mark = function (tag) { return perf.mark(tag); }; + measure = function (name, startTag, endTag) { + perf.measure(name, startTag, endTag); + perf.clearMarks(startTag); + perf.clearMarks(endTag); + perf.clearMeasures(name); + }; + } +} + +/* not type checking this file because flow doesn't play well with Proxy */ + +var initProxy; + +{ + var allowedGlobals = makeMap( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require' // for Webpack/Browserify + ); + + var warnNonPresent = function (target, key) { + warn( + "Property or method \"" + key + "\" is not defined on the instance but " + + 'referenced during render. Make sure that this property is reactive, ' + + 'either in the data option, or for class-based components, by ' + + 'initializing the property. ' + + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', + target + ); + }; + + var hasProxy = + typeof Proxy !== 'undefined' && isNative(Proxy); + + if (hasProxy) { + var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); + config.keyCodes = new Proxy(config.keyCodes, { + set: function set (target, key, value) { + if (isBuiltInModifier(key)) { + warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); + return false + } else { + target[key] = value; + return true + } + } + }); + } + + var hasHandler = { + has: function has (target, key) { + var has = key in target; + var isAllowed = allowedGlobals(key) || key.charAt(0) === '_'; + if (!has && !isAllowed) { + warnNonPresent(target, key); + } + return has || !isAllowed + } + }; + + var getHandler = { + get: function get (target, key) { + if (typeof key === 'string' && !(key in target)) { + warnNonPresent(target, key); + } + return target[key] + } + }; + + initProxy = function initProxy (vm) { + if (hasProxy) { + // determine which proxy handler to use + var options = vm.$options; + var handlers = options.render && options.render._withStripped + ? getHandler + : hasHandler; + vm._renderProxy = new Proxy(vm, handlers); + } else { + vm._renderProxy = vm; + } + }; +} + +/* */ + +var seenObjects = new _Set(); + +/** + * Recursively traverse an object to evoke all converted + * getters, so that every nested property inside the object + * is collected as a "deep" dependency. + */ +function traverse (val) { + _traverse(val, seenObjects); + seenObjects.clear(); +} + +function _traverse (val, seen) { + var i, keys; + var isA = Array.isArray(val); + if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { + return + } + if (val.__ob__) { + var depId = val.__ob__.dep.id; + if (seen.has(depId)) { + return + } + seen.add(depId); + } + if (isA) { + i = val.length; + while (i--) { _traverse(val[i], seen); } + } else { + keys = Object.keys(val); + i = keys.length; + while (i--) { _traverse(val[keys[i]], seen); } + } +} + +/* */ + +var normalizeEvent = cached(function (name) { + var passive = name.charAt(0) === '&'; + name = passive ? name.slice(1) : name; + var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first + name = once$$1 ? name.slice(1) : name; + var capture = name.charAt(0) === '!'; + name = capture ? name.slice(1) : name; + return { + name: name, + once: once$$1, + capture: capture, + passive: passive + } +}); + +function createFnInvoker (fns) { + function invoker () { + var arguments$1 = arguments; + + var fns = invoker.fns; + if (Array.isArray(fns)) { + var cloned = fns.slice(); + for (var i = 0; i < cloned.length; i++) { + cloned[i].apply(null, arguments$1); + } + } else { + // return handler return value for single handlers + return fns.apply(null, arguments) + } + } + invoker.fns = fns; + return invoker +} + +function updateListeners ( + on, + oldOn, + add, + remove$$1, + vm +) { + var name, def, cur, old, event; + for (name in on) { + def = cur = on[name]; + old = oldOn[name]; + event = normalizeEvent(name); + /* istanbul ignore if */ + if (isUndef(cur)) { + "development" !== 'production' && warn( + "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), + vm + ); + } else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on[name] = createFnInvoker(cur); + } + add(event.name, cur, event.once, event.capture, event.passive, event.params); + } else if (cur !== old) { + old.fns = cur; + on[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on[name])) { + event = normalizeEvent(name); + remove$$1(event.name, oldOn[name], event.capture); + } + } +} + +/* */ + +function mergeVNodeHook (def, hookKey, hook) { + if (def instanceof VNode) { + def = def.data.hook || (def.data.hook = {}); + } + var invoker; + var oldHook = def[hookKey]; + + function wrappedHook () { + hook.apply(this, arguments); + // important: remove merged hook to ensure it's called only once + // and prevent memory leak + remove(invoker.fns, wrappedHook); + } + + if (isUndef(oldHook)) { + // no existing hook + invoker = createFnInvoker([wrappedHook]); + } else { + /* istanbul ignore if */ + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // already a merged invoker + invoker = oldHook; + invoker.fns.push(wrappedHook); + } else { + // existing plain hook + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + + invoker.merged = true; + def[hookKey] = invoker; +} + +/* */ + +function extractPropsFromVNodeData ( + data, + Ctor, + tag +) { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return + } + var res = {}; + var attrs = data.attrs; + var props = data.props; + if (isDef(attrs) || isDef(props)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + { + var keyInLowerCase = key.toLowerCase(); + if ( + key !== keyInLowerCase && + attrs && hasOwn(attrs, keyInLowerCase) + ) { + tip( + "Prop \"" + keyInLowerCase + "\" is passed to component " + + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + + " \"" + key + "\". " + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." + ); + } + } + checkProp(res, props, key, altKey, true) || + checkProp(res, attrs, key, altKey, false); + } + } + return res +} + +function checkProp ( + res, + hash, + key, + altKey, + preserve +) { + if (isDef(hash)) { + if (hasOwn(hash, key)) { + res[key] = hash[key]; + if (!preserve) { + delete hash[key]; + } + return true + } else if (hasOwn(hash, altKey)) { + res[key] = hash[altKey]; + if (!preserve) { + delete hash[altKey]; + } + return true + } + } + return false +} + +/* */ + +// The template compiler attempts to minimize the need for normalization by +// statically analyzing the template at compile time. +// +// For plain HTML markup, normalization can be completely skipped because the +// generated render function is guaranteed to return Array. There are +// two cases where extra normalization is needed: + +// 1. When the children contains components - because a functional component +// may return an Array instead of a single root. In this case, just a simple +// normalization is needed - if any child is an Array, we flatten the whole +// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep +// because functional components already normalize their own children. +function simpleNormalizeChildren (children) { + for (var i = 0; i < children.length; i++) { + if (Array.isArray(children[i])) { + return Array.prototype.concat.apply([], children) + } + } + return children +} + +// 2. When the children contains constructs that always generated nested Arrays, +// e.g.