├── .github
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── request.md
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── cypress.json
├── deploy
├── chrome.js
├── firefox.js
├── sentry.sh
└── zipFolder.js
├── docs
└── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── promo
├── Головний екран.png
├── Коментарі.png
├── Переклади.png
└── Серії.png
├── public
├── pause.png
├── play-128.png
├── play.png
└── shikimori-logo.svg
├── src
├── UI
│ ├── App.vue
│ ├── components
│ │ ├── app-footer.vue
│ │ ├── clear-btn.vue
│ │ ├── comments-feed
│ │ │ ├── comment-form.vue
│ │ │ ├── comment.vue
│ │ │ ├── index.vue
│ │ │ └── load-more.vue
│ │ ├── controls
│ │ │ ├── download.vue
│ │ │ ├── index.vue
│ │ │ ├── next.vue
│ │ │ ├── open-on-shikimori.vue
│ │ │ ├── preference.vue
│ │ │ └── previous.vue
│ │ ├── episode-list.vue
│ │ ├── main-menu.vue
│ │ ├── messages.vue
│ │ ├── options-controllers
│ │ │ ├── login.vue
│ │ │ └── theme.vue
│ │ ├── player.vue
│ │ ├── translation-list.vue
│ │ └── video-controls-origin.vue
│ ├── index.html
│ ├── main.ts
│ ├── mixins
│ │ └── boilerplate.ts
│ ├── plugins
│ │ └── vuetify.ts
│ ├── router.ts
│ ├── store
│ │ ├── index.ts
│ │ ├── player
│ │ │ └── index.ts
│ │ ├── profile
│ │ │ └── index.ts
│ │ ├── shikimori
│ │ │ └── index.ts
│ │ ├── types.ts
│ │ └── worker.ts
│ └── views
│ │ ├── FooterView.vue
│ │ ├── History.vue
│ │ ├── Options.vue
│ │ └── Player.vue
├── _locales
│ └── ru
│ │ └── messages.json
├── background
│ ├── background.ts
│ ├── browserAction.ts
│ ├── index.html
│ ├── install-update-events.ts
│ ├── loadBroadcast.ts
│ ├── loadRuntimeMessages.ts
│ ├── request-provider.ts
│ └── setBadgeMessageCount.ts
├── content-scripts
│ ├── anime365-player-events.ts
│ ├── anime365-player-styles.css
│ ├── inject-content-scripts.ts
│ └── shikimori-watch-button.ts
├── helpers
│ ├── API
│ │ ├── Anime365Provider.ts
│ │ ├── BackgroundRequestProvider.ts
│ │ ├── MyAnimeListProvider.ts
│ │ ├── RequestProvider.ts
│ │ └── ShikimoriProvider.ts
│ ├── abusiveWords.ts
│ ├── chrome-storage.ts
│ ├── clear-string.ts
│ ├── errors
│ │ ├── APIError.class.ts
│ │ ├── AppError.class.ts
│ │ ├── NetworkError.class.ts
│ │ └── PermissionError.class.ts
│ ├── filter-episodes.ts
│ ├── find-episode.ts
│ ├── get-review-url.ts
│ ├── get-translation-priority.ts
│ ├── injectScript.ts
│ ├── oauth-provider.ts
│ ├── pluralize.ts
│ ├── runtime-messages.ts
│ └── version-compare.ts
└── manifest.js
├── tests
├── e2e
│ ├── plugins
│ │ └── index.js
│ ├── specs
│ │ └── test.js
│ └── support
│ │ ├── commands.js
│ │ └── index.js
└── unit
│ ├── clearString.spec.js
│ ├── example.spec.ts
│ └── priorityTranslation
│ ├── getPriorityTranslation.spec.js
│ ├── sampleEpisodes.js
│ └── sampleHistory.js
├── tsconfig.json
├── tslint.json
├── types
├── AppErrorSchema.d.ts
├── RuntimeMessage.d.ts
├── UI.ts
├── Worker.d.ts
├── anime365.d.ts
├── myanimelist.d.ts
├── shikimori.d.ts
├── shims-tsx.d.ts
└── shims-vue.d.ts
└── vue.config.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | patreon: Kozack
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Сообщение об ошибке
3 | about: Создать отчет, об ошибке или обнаруженной проблеме
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Опишите ошибку**
11 | Четкое и краткое описание ошибки.
12 |
13 | **Как воспроизвести**
14 | Шаги для воспроизведения поведения:
15 | 1. Откройте аниме «...», серия № «...» в переводе «...»
16 | 1. Перейдите к «...»
17 | 2. Нажмите «....»
18 | 3. Прокрутите вниз до «....»
19 | 4. Смотри ошибку
20 |
21 | **Ожидаемое поведение**
22 | Четкое и краткое описание того, что вы ожидали.
23 |
24 | **Скриншоты**
25 | Если применимо, добавьте скриншоты, чтобы объяснить вашу проблему.
26 |
27 | **Мета (пожалуйста, заполните следующую информацию):**
28 | - Браузер [например, хром, сафари]
29 | - Версия браузера [например, 22]
30 | - Версия расширения [например, 1.1.0]
31 | - Из какого магазина расширение было установлено [Chrome WebStore, Opera addons, Firefox addons]
32 |
33 | **Дополнительный контекст**
34 | Добавьте любой другой контекст о проблеме здесь.
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Предложения
3 | about: Предложить идею для этого проекта
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Ваше предложение связано с проблемой? Пожалуйста, опишите.**
11 | Четкое и краткое описание проблемы. Например: «Я всегда расстраиваюсь, когда [...]»
12 |
13 | **Опишите решение, которое вы хотели бы**
14 | Четкое и краткое описание того, что вы хотите, чтобы произошло.
15 |
16 | **Дополнительный контекст**
17 | Добавьте любой другой контекст или скриншоты о предложении здесь.
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 | /tests/e2e/videos/
6 | /tests/e2e/screenshots/
7 |
8 | # local env files
9 | .env.local
10 | .env.*.local
11 |
12 | # Log files
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | # Editor directories and files
18 | .idea
19 | .vscode
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | *.zip
27 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # Конфиг сделан по примеру
2 | # https://github.com/vuetifyjs/vuetify/blob/aed4309f6e845b64d97a7b43f4275a36526bab04/.travis.yml
3 |
4 | sudo: false
5 |
6 | language: node_js
7 |
8 | node_js:
9 | - "12"
10 |
11 | stages:
12 | # - test
13 | - name: deploy
14 | if: (tag IS present) AND (type = push) AND (repo = cawa-93/play-shikimori-online)
15 |
16 | jobs:
17 | include:
18 | # - stage: test
19 | # name: "Test"
20 | # script:
21 | # - npm run lint
22 | # # TODO: написать тесты
23 | # - NODE_ENV=production npm run build
24 | # # - npm run test:unit
25 | # # - npm run test:e2e
26 |
27 | - stage: deploy
28 | name: "Deploy"
29 | script:
30 | # - NODE_ENV=production BROWSER=chrome npm run build
31 | - NODE_ENV=production BROWSER=firefox npm run build
32 | deploy:
33 | # - provider: script
34 | # script: node deploy/chrome.js
35 | # skip_cleanup: true
36 | # on:
37 | # repo: cawa-93/play-shikimori-online
38 | # tags: true
39 |
40 | - provider: script
41 | script: node deploy/firefox.js
42 | skip_cleanup: true
43 | on:
44 | repo: cawa-93/play-shikimori-online
45 | tags: true
46 |
47 |
48 | # - provider: releases
49 | # file:
50 | # - chrome.zip
51 | # - firefox.zip
52 | # api_key: $GITHUB_OAUTH_TOKEN
53 | # draft: true
54 | # skip_cleanup: true
55 | # on:
56 | # repo: cawa-93/play-shikimori-online
57 | # tags: true
58 |
59 |
60 | # - provider: script
61 | # script: bash deploy/sentry.sh
62 | # skip_cleanup: true
63 | # on:
64 | # repo: cawa-93/play-shikimori-online
65 | # tags: true
66 |
67 |
68 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # История изменения
2 | Все заметные изменения в этом проекте будут документированы в этом файле
3 |
4 | ## [1.3.1] — 13.11.2019
5 | ### Изменения
6 | - Множество исправлений и улучшений в зависимых библиотеках.
7 | - [Vuetify обновлен до версии 2.1.10](https://github.com/vuetifyjs/vuetify/releases/tag/v2.1.10)
8 | - [Vuex обновлен до версии 3.1.2](https://github.com/vuejs/vuex/releases/tag/v3.1.2)
9 | - [TypeScript обновлен до версиии 3.7.2](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html)
10 |
11 | ## [1.3.0] — 30.10.2019
12 | ### Изменения
13 | - Часть компонентов загружаются не сразу, а только при попадании в область видимости. Это увеличивает скорость отображениея и уменьшиет количество потребляемого трафика на ранних этапах отображения, что позволяет загружаться видео быстрее.
14 | - Кнопки под плеером больше не "прыгают" в стороны. Теперь они всегда на одном и том же месте.
15 | - Улучшена доступность.
16 | - Удалена система фонового отслеживания ошибок.
17 |
18 | ## [1.2.0] — 08.10.2019
19 | ### Изменения
20 | - Повышена производительность интерфейса.
21 | - Исправлена проблема пролагивания при загрузке страницы
22 | - Добавлен индикатор Full-HD видео.
23 |
24 | Так же, я планировал ввести новый способ загрузки интерфейса, чтобы сделать его более плавным и "дружелюбным", но при тестировании обнаружил некоторые пробелмы так что это откладывается на следующий релиз.
25 |
26 | ## [1.1.6] — 19.09.2019
27 | ### Изменения
28 | - Создана отдельная страница с настройками расширения. Теперь можно кликнуть правой кнопкой по иконке расширения и перейти к параметрам. Туда вынесены опции для оправления темой и включения/выключения синхронизации с Шикимори.
29 | - Для тех у кого есть подписка на anime365 теперь есть возможность скачать видео в два клика прямо в интерфейсе расширения.
30 | - Переработано главное меню:
31 | - Все элементы теперь располагаются не в выпадающем меню а в один ряд под плеером.
32 | - "Следующая серия" и "Открыть на Шикимори" стали больше.
33 | - Добавлена ссылка для перехода на отдельную страницу с настройками.
34 | - Добавлена ссылка для скачивания видео.
35 | - Удалена кнопка "Сообщить о проблеме с видео".
36 | - Удалена кнопка для быстрого перехода в историю просмотров.
37 | - Удалена кнопка для переключения темы.
38 | - Удалена кнопка для включения/выключения синхронизации с Шикимори.
39 | - Исправлено множество проблем с плеером anime365.
40 | - Наверняка многие сталкивались с ситуациями, когда видео не запускалось самостоятельно, или вместо видеоряда автоматически запускалась реклама, или и видео и реклама запускались одновременно перекрикивая друг друга. Однако, после обсуждения этих проблем с разработчиками anime365 был добавлен специальный API, с помощью которого моё расширение может максимально надежно интегрироваться в видеоплейер не ломая его.
41 | - Увеличена производительность и скорость отображения.
42 | - Малозаметные исправления и обновление компонентов.
43 |
44 | ## [1.1.5] — 13.09.2019
45 | В этой версии я продолжил работу над внутренним хранилищем. В этой версии изменения коснулись процесса загрузки списка серий и их названий.
46 |
47 | Я реализовал временный кэш для списка серий. Теперь список серий для онгоингов кэшируется на сутки. А для уже вышедших аниме на более долгий срок.
48 |
49 | Кроме того, в кэше хранится не более 5 последних аниме. Таким образом объем потребляемой памяти остаётся низким.
50 |
51 | Благодаря этому нововведению время между началом загрузки страницы и инициализацией видео-плеера уменьшилось на 25-30%.
52 |
53 | ## [1.1.3] — 11.09.2019
54 | Кто-то знает, а кто-то нет, но расширение умеет предугадывать в каком переводе вы бы предпочли смотреть аниме. Как это работает. Расширение запоминает историю ваших просмотров и то какой перевод (озвучка/субтитры) и от какой релиз-группы вы выбираете. На основе этой модели строиться ваш персонализированный "Профиль предпочтений".И чем больше аниме вы смотрите, тем умнее и точнее работает прогноз.
55 |
56 | В предыдущей версии [1.1.2] была добавлена синхронизация этих данных между вашими устройствами. А в нынешней для этой функции нашлось ещё одно применение:
57 | - Теперь, на странице аниме на Шикимори, под кнопкой "Начать просмотр" будет отображаться максимально подходящий для вас перевод. Таким образом, даже не открывая плеер, вы сможете узнать добавлено ли видео от вашей любимой релиз-группы.
58 |
59 | ### Другие изменения
60 | - Хранилище данных Google, оказалось недостаточно быстрым. В качестве временного решение я реализовал частичную предзагрузку данных в оперативную память. Как следствие:
61 | - Увеличена скорость поиска подходящего перевода.
62 | - Увеличена скорость загрузки истории просмотров.
63 | - Увеличена скорость загрузки информации о вашем профиле.
64 | - По просьбе [@Ivan](https://t.me/playshikionline_chat/48) я написал небольшой [FAQ](https://github.com/cawa-93/play-shikimori-online/wiki/FAQ) и добавил ссылку на него в интерфейс расширения.
65 | - Исправлена ошибка отображения забаненного пользователя.
66 | - Исправлена ошибка синхронизации списков если пользователь не авторизовался.
67 | - Некоторые изменения, чтобы упростить рендеринг браузеру.
68 |
69 |
70 | ## [1.1.2] — 09.09.2019
71 |
72 | ### Изменения
73 | - *Синхронизация ваших избранных переводов*. Теперь расширение будет запоминать какие переводы вы предпочитаете и синхронизировать эту информацию между всеми вашими устройствами.
74 | - Добавлен механизм ротации (удаление устаревших данных чтобы освободить место для свежих) для внутреннего кэша и для списка избранных переводов.
75 | - Объем занимаемой памяти уменшился.
76 | - Изменено хранилище для внутреннего кэша.
77 |
78 |
79 |
80 | ## [1.1.1] — 06.09.2019
81 |
82 | ### Изменения
83 | - Добавлены сопровождающие подсказки для формы комментариев.
84 | - Если в тексте комментария присутствуют матерные слова — вы получите предупреждение о том, что за этот комментарий вас могут забанить.
85 | - Добавлены подсказки к кнопкам.
86 | - Несколько незначительных исправлений.
87 |
88 |
89 |
90 | ## [1.1.0] — 30.08.2019
91 | Расширение полностью переписано практически с нуля. Для таких радикальных изменений есть несколько причин:
92 | 1. С новой архитектурой мне будет проще внедрить те функции которые я планирую.
93 | 2. Мне захотелось. 😅
94 |
95 | ### Визуальные изменения
96 | - Область видео-плеера увеличена. Это сделает просмотр более комфортным для тех, кто по какой-либо причине смотрит аниме не в полноэкранном режиме.
97 | - Высота списка серий и переводов также увеличена. Теперь он вмещает в себя до 12 строк. 12 серий теперь видно без необходимости скролить.
98 | - Доработан внешний вид комментариев. Теперь они менее "плоские".
99 | - Добавлено визуальное оформление для цитат, сообщений про баны, картинок и видео.
100 | - Добавлена возможность ответить на комментарий прямо в интерфейсе расширения.
101 | - Переработана форма нового комментария. Теперь она более минималистична.
102 |
103 | ### Внутренние изменения
104 | - Расширение переписано на другой язык программирования — TypeScript.
105 | - Используеться новый компилятор. Благодаря этому, несмотря на то, что кодовая база выросла, итоговый вес программы снизился на 10.5%
106 | - Переработано ядро расширения — модуль выполняющий сетевые запросы.
107 | - Сетевые запросы в интерфейсе стали быстрее.
108 | - Добавлено "Network First" кэширование, что делает программу намного устойчивее к "падению" сервера.
109 | - Более дружелюбные сообщения об исключениях содержат в себе описание и причину проблемы.
110 | - Исправлена масса малозаметных ошибок.
111 |
112 |
113 |
114 |
115 |
116 |
117 | ## [1.0.9] - 20.08.2019
118 | Эта версия посвящена изменениям чтобы соответствовать изменениям в [политике Шикимори](https://shikimori.one/forum/news/290529).
119 | ### Изменения
120 | - Изменено название. Теперь расширение называеться "Play Шики Online". Возможно в будущем название смениться ещё раз.
121 | - Новые каналы распространения новостей:
122 |
123 |
124 |
125 | ### Список публичных каналов и их предназначение.
126 | #### [Telegram]
127 | - История изменений
128 | - **Все** новости проекта
129 | - Опросы пользователей
130 |
131 | #### [VK]
132 | - История изменений
133 | - **Важные** новости проекта
134 | - Обсуждение новинок в комментариях
135 |
136 |
137 | #### [Telegram chat]
138 | - Обсуждение новинок в общем чате
139 | - Помощь в решении проблем
140 | - Ответы на вопросы
141 | - История изменений
142 | - **Все** новости проекта
143 | - Опросы пользователей
144 |
145 |
146 | #### [GitHub]
147 | - История изменений
148 | - **Приоритетная** помощь в решении проблем
149 |
150 |
151 | [1.3.1]: https://github.com/cawa-93/play-shikimori-online/compare/v1.3.0...v1.3.1
152 | [1.3.0]: https://github.com/cawa-93/play-shikimori-online/compare/v1.2.0...v1.3.0
153 | [1.2.0]: https://github.com/cawa-93/play-shikimori-online/compare/v1.1.6...v1.2.0
154 | [1.1.6]: https://github.com/cawa-93/play-shikimori-online/compare/v1.1.5...v1.1.6
155 | [1.1.5]: https://github.com/cawa-93/play-shikimori-online/compare/v1.1.4...v1.1.5
156 | [1.1.3]: https://github.com/cawa-93/play-shikimori-online/compare/v1.1.2...v1.1.3
157 | [1.1.2]: https://github.com/cawa-93/play-shikimori-online/compare/v1.1.1...v1.1.2
158 | [1.1.1]: https://github.com/cawa-93/play-shikimori-online/compare/v1.1.0...v1.1.1
159 | [1.1.0]: https://github.com/cawa-93/play-shikimori-online/compare/v1.0.9...v1.1.0
160 | [1.0.9]: https://github.com/cawa-93/play-shikimori-online/compare/v1.0.8...v1.0.9
161 |
162 | [GitHub]: https://github.com/cawa-93/play-shikimori-online
163 | [Telegram]: https://t.me/playshikionline
164 | [VK]: https://vk.com/playshikionline
165 | [Telegram chat]: https://vk.com/playshikionline_chat
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Play Шикимори Online
2 |
3 | [](https://vshymanskyy.github.io/StandWithUkraine)
4 |
5 | Это расширение больше не получает обновления! Вместо него я сделал полноценное приложение для
6 | Windows. **[Скачать можно тут](https://github.com/cawa-93/anime-library/releases/latest)**.
7 |
8 |
9 |
10 | [](https://addons.mozilla.org/ru/firefox/addon/play-shikimori/reviews/)
11 | [](https://addons.mozilla.org/ru/firefox/addon/play-shikimori)
12 |
13 | ### Развитие проекта спонсируется на [Patreon](https://www.patreon.com/Kozack)
14 |
15 |  | 
16 | --- | ---
17 |  | 
18 |
19 | Это браузерное расширение, которое позволяет вам смотреть аниме онлайн и синхронизировать его с вашим списком на Шикимори.
20 |
21 |
22 | ## [Установка](https://github.com/cawa-93/play-shikimori-online/wiki/%D0%98%D0%BD%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%86%D0%B8%D1%8F-%D0%BF%D0%BE-%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B5)
23 |
24 | ## Возможности
25 |
26 | В разработке этого расширения основной упор делается непосредственно на онлайн просмотре и всего что с ним связано. Моя цель — сделать его настолько удобным, насколько это возможно.
27 |
28 | * Вам не нужно регистрироваться чтобы смотреть аниме онлайн.
29 | * Новые серии добавляются в тот же миг, как они появляются на хостинге-видео.
30 | * Плеер умеет самостоятельно переключаться на следующую серию, когда текущая подходит к концу.
31 | * Плеер запоминает время, на котором вы остановили просмотр серии, и возобновляет воспроизведение с этого же места.
32 | * Вы можете начать просмотр серии на одном устройстве, а продолжить на другом. Время на котором вы остановились синхронизируется между всеми вашими устройствами
33 | * Ведётся учет в каком переводе вы смотрите. Благодаря этому, когда вы открываете новый сериал, доступен более интелектуальный выбор переводов на основе всех ваших предпочтений.
34 |
35 | ## [Ответы на возникающие вопросы](https://github.com/cawa-93/play-shikimori-online/wiki/FAQ)
36 |
37 |
38 | ## [Дорожная карта](https://github.com/cawa-93/play-shikimori-online/projects/1)
39 |
40 | ## Contributing
41 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
42 |
43 | ### Project setup
44 | ```
45 | npm install
46 | ```
47 |
48 | ### Environments
49 | ```bash
50 | # Setup environment
51 | NODE_ENV=production
52 | BROWSER=chrome # or firefox
53 |
54 | # Shikimori oauth
55 | SHIKIMORI_REDIRECT_URI=
56 | SHIKIMORI_CLIENT_ID=
57 | SHIKIMORI_CLIENT_SECRET=
58 | ```
59 |
60 | ### Compiles for development
61 | ```
62 | npm run build:dev
63 | ```
64 |
65 | ### Compiles and minifies for production
66 | ```
67 | npm run build
68 | ```
69 |
70 | ### Run your tests
71 | ```
72 | npm run test
73 | ```
74 |
75 | ### Lints and fixes files
76 | ```
77 | npm run lint
78 | ```
79 |
80 | ### Run your end-to-end tests
81 | ```
82 | npm run test:e2e
83 | ```
84 |
85 | ### Run your unit tests
86 | ```
87 | npm run test:unit
88 | ```
89 |
90 | ### Customize configuration
91 | See [Configuration Reference](https://cli.vuejs.org/config/).
92 |
93 |
94 | ## License
95 | [GPL-3.0](https://github.com/cawa-93/play-shikimori-online/blob/master/LICENSE)
96 |
--------------------------------------------------------------------------------
/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "pluginsFile": "tests/e2e/plugins/index.js"
3 | }
4 |
--------------------------------------------------------------------------------
/deploy/chrome.js:
--------------------------------------------------------------------------------
1 | const zipFolder = require('./zipFolder.js')
2 | const fs = require('fs')
3 |
4 | const browser = 'chrome'
5 |
6 | let folder = `dist/${browser}`
7 | let zipName = `${browser}.zip`
8 |
9 |
10 | zipFolder(folder, zipName)
11 | .then(() => {
12 | console.log(`Successfully Zipped ${folder} and saved as ${zipName}`)
13 | // uploadZip() // on successful zipping, call upload
14 | })
15 | .catch(err => {
16 | console.log('Can not create zip:', err)
17 | process.exit(1)
18 | })
19 |
20 |
21 | function uploadZip() {
22 | // credentials and IDs from gitlab-ci.yml file (your appropriate config file)
23 | let REFRESH_TOKEN = process.env.WEBSTORE_REFRESH_TOKEN
24 | let EXTENSION_ID = process.env.WEBSTORE_EXTENSION_ID
25 | let CLIENT_SECRET = process.env.WEBSTORE_CLIENT_SECRET
26 | let CLIENT_ID = process.env.WEBSTORE_CLIENT_ID
27 |
28 | const webStore = require('chrome-webstore-upload')({
29 | extensionId: EXTENSION_ID,
30 | clientId: CLIENT_ID,
31 | clientSecret: CLIENT_SECRET,
32 | refreshToken: REFRESH_TOKEN,
33 | })
34 |
35 |
36 | // creating file stream to upload
37 | const extensionSource = fs.createReadStream(`./${zipName}`)
38 |
39 | // upload the zip to webstore
40 | webStore.uploadExisting(extensionSource).then(res => {
41 | console.log('Successfully uploaded the ZIP')
42 |
43 | // publish the uploaded zip
44 | webStore.publish().then(res => {
45 | console.log('Successfully published the newer version')
46 | }).catch((error) => {
47 | console.log(`Error while publishing uploaded extension: ${error}`)
48 | process.exit(1)
49 | })
50 |
51 | }).catch((error) => {
52 | console.log(`Error while uploading ZIP: ${error}`)
53 | process.exit(1)
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/deploy/firefox.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | var deploy = require('firefox-extension-deploy')
3 | const zipFolder = require('./zipFolder.js')
4 |
5 |
6 | const browser = 'firefox'
7 |
8 | let folder = `dist/${browser}`
9 | let zipName = `${browser}.zip`
10 |
11 |
12 | // credentials and IDs from gitlab-ci.yml file (your appropriate config file)
13 | let ISSUER = process.env.FIREFOX_ISSUER
14 | let SECRET = process.env.FIREFOX_SECRET
15 | let EXTENSION_ID = process.env.FIREFOX_EXTENSION_ID
16 |
17 |
18 | zipFolder(folder, zipName)
19 | .then(() => {
20 | console.log(`Successfully Zipped ${folder} and saved as ${zipName}`)
21 | uploadZip() // on successful zipping, call upload
22 | })
23 | .catch(err => {
24 | console.log('Can not create zip:', err)
25 | process.exit(1)
26 | })
27 |
28 |
29 | function uploadZip() {
30 | const manifest = JSON.parse(fs.readFileSync(folder + '/manifest.json'))
31 |
32 | deploy({
33 | // obtained by following the instructions here:
34 | // https://addons-server.readthedocs.io/en/latest/topics/api/auth.html
35 | // or from this page:
36 | // https://addons.mozilla.org/en-US/developers/addon/api/key/
37 | issuer: ISSUER,
38 | secret: SECRET,
39 |
40 | // the ID of your extension
41 | id: EXTENSION_ID,
42 |
43 | version: manifest.version,
44 |
45 | // a ReadStream containing a .zip (WebExtensions) or .xpi (Add-on SDK)
46 | src: fs.createReadStream(zipName),
47 | }).then(function () {
48 | console.log('Successfully uploaded the ZIP')
49 | }, function (error) {
50 | console.log(`Error while uploading ZIP: ${error}`)
51 | process.exit(1)
52 | })
53 |
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/deploy/sentry.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | #TRAVIS_TAG="v1.1.0"
4 | #TRAVIS_REPO_SLUG="cawa-93/play-shikimori-online"
5 | #TRAVIS_COMMIT="89a0454966c78e2c82c3f78e6259e73c48eb0dbc"
6 |
7 | VERSION="Play Шики Online@${TRAVIS_TAG:1}"
8 | echo version is "$VERSION"
9 |
10 | # Create a release
11 | npx sentry-cli releases new -p extension "$VERSION"
12 | npx sentry-cli releases -p extension files "$VERSION" upload-sourcemaps ./dist/chrome/
13 | npx sentry-cli releases -p extension set-commits --commit $TRAVIS_REPO_SLUG@$TRAVIS_COMMIT "$VERSION"
14 | npx sentry-cli releases -p extension deploys "$VERSION" new -e production
15 |
--------------------------------------------------------------------------------
/deploy/zipFolder.js:
--------------------------------------------------------------------------------
1 | // require modules
2 | var fs = require('fs')
3 | var archiver = require('archiver')
4 |
5 |
6 | module.exports = function zipFolder(folder, zipName, options = {}) {
7 | return new Promise((resolve, reject) => {
8 | const output = fs.createWriteStream(zipName)
9 | const archive = archiver('zip')
10 |
11 | output.on('close', function () {
12 | resolve()
13 | })
14 |
15 | // good practice to catch this error explicitly
16 | archive.on('error', function (err) {
17 | reject(err)
18 | })
19 |
20 | // pipe archive data to the file
21 | archive.pipe(output)
22 |
23 | options = Object.assign({}, {
24 | ignore: '**/*.map',
25 | cwd: folder,
26 | dot: false,
27 | stat: false,
28 | }, options)
29 |
30 | // append files from a sub-directory, putting its contents at the root of archive
31 | archive.glob(`**`, options)
32 |
33 | // finalize the archive (ie we are done appending files but streams have to finish yet)
34 | // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand
35 | archive.finalize()
36 | })
37 | }
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 | hello world
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": "Расширение позволяет смотреть аниме онлайн и синхронизировать его с вашим списком на Шикимори (shikimori.org и shikimori.one)",
3 | "name": "play-shiki-online",
4 | "version": "1.4.1",
5 | "private": true,
6 | "browserslist": [
7 | "Chrome >= 73",
8 | "ChromeAndroid >= 73",
9 | "Firefox >= 63",
10 | "FirefoxAndroid >= 63"
11 | ],
12 | "scripts": {
13 | "serve": "vue-cli-service serve",
14 | "build": "vue-cli-service build",
15 | "build:dev": "vue-cli-service build --mode development",
16 | "build:watch": "npm run build:dev -- --watch",
17 | "lint": "vue-cli-service lint",
18 | "lint:fix": "vue-cli-service lint --fix"
19 | },
20 | "dependencies": {
21 | "@mdi/font": "^4.5.95",
22 | "async-retry": "latest",
23 | "kv-storage-polyfill": "^2.0.0",
24 | "lodash.throttle": "^4.1.1",
25 | "roboto-fontface": "*",
26 | "vue": "^2.6.10",
27 | "vue-class-component": "^7.0.2",
28 | "vue-property-decorator": "^8.3.0",
29 | "vue-router": "^3.1.3",
30 | "vuetify": "^2.1.10",
31 | "vuex": "^3.1.2"
32 | },
33 | "devDependencies": {
34 | "@types/async-retry": "^1.4.1",
35 | "@types/chai": "^4.2.5",
36 | "@types/chrome": "0.0.91",
37 | "@types/lodash.throttle": "^4.1.6",
38 | "@types/mocha": "^5.2.4",
39 | "@types/video.js": "^7.3.2",
40 | "@vue/cli-plugin-typescript": "^4.0.5",
41 | "@vue/cli-service": "^4.0.5",
42 | "@vue/devtools": "^5.3.2",
43 | "@vue/test-utils": "1.0.0-beta.29",
44 | "archiver": "^3.1.1",
45 | "copy-webpack-plugin": "^5.0.5",
46 | "firefox-extension-deploy": "^1.1.2",
47 | "lint-staged": "^9.4.2",
48 | "sass": "^1.23.3",
49 | "sass-loader": "^8.0.0",
50 | "typescript": "^3.7.2",
51 | "vue-cli-plugin-vuetify": "^1.1.2",
52 | "vue-template-compiler": "^2.6.10",
53 | "vuetify-loader": "^1.3.1",
54 | "vuex-module-decorators": "^0.10.1",
55 | "webpack-extension-manifest-plugin": "^0.5.0",
56 | "worker-loader": "^2.0.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/promo/Головний екран.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cawa-93/play-shikimori-online/1f6b00f2819504dfe4cbdd7c7f72570235789f40/promo/Головний екран.png
--------------------------------------------------------------------------------
/promo/Коментарі.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cawa-93/play-shikimori-online/1f6b00f2819504dfe4cbdd7c7f72570235789f40/promo/Коментарі.png
--------------------------------------------------------------------------------
/promo/Переклади.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cawa-93/play-shikimori-online/1f6b00f2819504dfe4cbdd7c7f72570235789f40/promo/Переклади.png
--------------------------------------------------------------------------------
/promo/Серії.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cawa-93/play-shikimori-online/1f6b00f2819504dfe4cbdd7c7f72570235789f40/promo/Серії.png
--------------------------------------------------------------------------------
/public/pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cawa-93/play-shikimori-online/1f6b00f2819504dfe4cbdd7c7f72570235789f40/public/pause.png
--------------------------------------------------------------------------------
/public/play-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cawa-93/play-shikimori-online/1f6b00f2819504dfe4cbdd7c7f72570235789f40/public/play-128.png
--------------------------------------------------------------------------------
/public/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cawa-93/play-shikimori-online/1f6b00f2819504dfe4cbdd7c7f72570235789f40/public/play.png
--------------------------------------------------------------------------------
/src/UI/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
16 |
17 |
39 |
--------------------------------------------------------------------------------
/src/UI/components/app-footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 | mdi-patreon
12 | Угостить автора печенькой
13 |
14 |
15 |
16 |
24 | {{ link.icon }}
25 |
26 |
27 |
28 | {{link.label}}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | mdi-copyright
38 | {{domain}}
44 |
45 |
46 |
47 |
48 |
49 |
87 |
--------------------------------------------------------------------------------
/src/UI/components/clear-btn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mdi-delete-forever
6 |
7 |
8 | Сбросить все данные
9 |
10 |
11 |
12 |
13 |
58 |
--------------------------------------------------------------------------------
/src/UI/components/comments-feed/comment-form.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
34 |
35 |
36 |
37 | mdi-sync
38 | Чтобы оставить отзыв необходимо включить синхронизацию
39 |
40 |
41 |
42 |
43 |
44 |
104 |
105 |
108 |
--------------------------------------------------------------------------------
/src/UI/components/comments-feed/comment.vue:
--------------------------------------------------------------------------------
1 |
2 |
40 |
41 |
42 |
52 |
53 |
240 |
--------------------------------------------------------------------------------
/src/UI/components/comments-feed/load-more.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | $emit('loadMore')"
9 | @click.shift.exact="() => $emit('loadAll')"
10 | aria-label="Загрузить больше комментариев"
11 | icon
12 | v-bind="attrs"
13 | v-on="mergeHandlers(left, right)"
14 | >
15 | mdi-chevron-down
16 |
17 |
18 | + Shift — Загрузить все
19 |
20 |
21 | Загрузить больше
22 |
23 |
24 |
25 |
59 |
--------------------------------------------------------------------------------
/src/UI/components/controls/download.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 | mdi-download
13 | Скачать
14 |
15 |
16 |
17 |
18 |
25 | {{ item.height }}p
26 |
27 |
28 |
29 |
30 |
31 |
72 |
--------------------------------------------------------------------------------
/src/UI/components/controls/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
32 |
33 |
34 |
63 |
--------------------------------------------------------------------------------
/src/UI/components/controls/next.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 | {{label}}
11 | mdi-skip-next
12 |
13 |
14 |
15 |
71 |
--------------------------------------------------------------------------------
/src/UI/components/controls/open-on-shikimori.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 | Открыть на Шикимори
12 |
13 |
14 |
15 |
34 |
35 |
40 |
--------------------------------------------------------------------------------
/src/UI/components/controls/preference.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 | mdi-{{user ? 'settings' : 'sync-off'}}
9 | Настройки
10 |
11 |
12 |
13 |
32 |
--------------------------------------------------------------------------------
/src/UI/components/controls/previous.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 | mdi-skip-previous
11 | Предыдущая серия
12 |
13 |
14 |
15 |
36 |
--------------------------------------------------------------------------------
/src/UI/components/episode-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{item.episodeFull}}
25 |
26 |
27 |
28 |
29 |
30 | {{item.episodeFull}}
31 | — просмотрено
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | mdi-plus-box
41 |
42 |
43 |
44 | Добавить серию
45 |
46 |
47 |
48 | mdi-open-in-new
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
117 |
--------------------------------------------------------------------------------
/src/UI/components/main-menu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mdi-sync-alert
6 | {{$vuetify.breakpoint.xsOnly ? 'Меню' : user ? 'Открыть меню' : 'Синхронизация отключена'}}
9 | mdi-menu-down
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | mdi-account-circle
19 |
20 |
21 |
22 | {{user.nickname}}
23 | Синхронизация включена
24 |
25 |
26 |
27 |
28 |
29 |
35 | mdi-brightness-6
36 |
37 |
38 | Включить {{$vuetify.theme.dark ? 'светлую' : 'темную'}} тему
39 |
40 |
41 |
42 |
43 |
44 |
45 |
51 | mdi-history
52 |
53 |
54 | История просмотров
55 |
56 |
57 |
58 |
59 |
60 |
61 |
67 | mdi-sync-off
68 |
69 |
70 | Выключить синхронизацию
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | mdi-sync
81 |
82 |
83 |
84 | Включить синхронизацию
85 |
86 |
87 |
88 |
89 |
90 |
97 | mdi-brightness-6
98 |
99 |
100 | Включить {{$vuetify.theme.dark ? 'светлую' : 'темную'}} тему
101 |
102 |
103 |
104 |
109 | mdi-history
110 |
111 |
112 |
113 |
114 |
115 |
116 |
121 |
122 | mdi-open-in-new
123 |
124 |
125 | {{ shikiLink.label }}
126 |
127 |
128 |
133 |
134 | mdi-alert-octagon
135 |
136 |
137 | {{ reportAboutError.label }}
138 |
139 |
140 |
141 |
142 |
143 |
144 |
206 |
207 |
--------------------------------------------------------------------------------
/src/UI/components/messages.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
24 | mdi-close-circle
25 | Закрыть
26 |
27 |
28 |
29 |
30 |
97 |
98 |
109 |
--------------------------------------------------------------------------------
/src/UI/components/options-controllers/login.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 | Синхронизация прогресса
8 |
9 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 | Синхронизировать с Шикимори
24 | Все просмотренные серии будут автоматически добавлены в ваш список на shikimori.one
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
77 |
78 |
81 |
--------------------------------------------------------------------------------
/src/UI/components/options-controllers/theme.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 | Внешний вид
8 |
9 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 | Включить темную тему
24 | Все елементы управления на всех страницах будут иметь приглушенные тона
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
55 |
56 |
59 |
--------------------------------------------------------------------------------
/src/UI/components/player.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
113 |
114 |
124 |
--------------------------------------------------------------------------------
/src/UI/components/translation-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 | {{item.label}}
21 |
22 |
23 |
24 | mdi-explicit
25 | mdi-quality-high
26 |
27 |
28 |
29 | {{item.authorsSummary}}
30 |
31 |
32 |
33 | {{item.height}}p
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | mdi-plus-box
42 |
43 |
44 |
45 | Добавить перевод
46 |
47 |
48 |
49 | mdi-open-in-new
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
198 |
199 |
200 |
206 |
--------------------------------------------------------------------------------
/src/UI/components/video-controls-origin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 | mdi-skip-previous
13 | Предыдущая {{ $vuetify.breakpoint.xsOnly ? '' : 'серия'}}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
31 | Следующая {{ $vuetify.breakpoint.xsOnly ? '' : 'серия'}}
32 | mdi-skip-next
33 |
34 |
35 |
40 | {{nextSeason.name}}
41 | mdi-skip-next
42 |
43 |
44 |
45 |
46 |
47 |
48 |
122 |
123 |
139 |
--------------------------------------------------------------------------------
/src/UI/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Загрузка…
14 |
15 |
16 |
57 |
58 |
59 |
Это расширение больше не получает обновления! Вместо него я сделал новое приложение с открытым исходным кодом
64 |
Скачать
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/src/UI/main.ts:
--------------------------------------------------------------------------------
1 | import '@mdi/font/css/materialdesignicons.css';
2 | // @ts-ignore
3 | // import devtools from '@vue/devtools';
4 | import 'roboto-fontface/css/roboto/roboto-fontface.css';
5 | import Vue from 'vue';
6 | import App from './App.vue';
7 | import vuetify from './plugins/vuetify';
8 | import router from './router';
9 | import store from './store';
10 |
11 | // if (process.env.NODE_ENV === 'development') {
12 | // devtools.connect(/* host, port */);
13 | // }
14 |
15 | Vue.config.productionTip = process.env.NODE_ENV === 'production';
16 | Vue.config.performance = process.env.NODE_ENV === 'development';
17 |
18 | new Vue({
19 | router,
20 | store,
21 | vuetify,
22 | render: (h) => h(App),
23 | }).$mount('#app');
24 |
--------------------------------------------------------------------------------
/src/UI/mixins/boilerplate.ts:
--------------------------------------------------------------------------------
1 | import {Component, Vue} from 'vue-property-decorator';
2 |
3 | @Component
4 | export default class Boilerplate extends Vue {
5 | public readyToShow: boolean = false;
6 | }
7 |
--------------------------------------------------------------------------------
/src/UI/plugins/vuetify.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuetify from 'vuetify/lib';
3 | import ru from 'vuetify/src/locale/ru';
4 |
5 | Vue.use(Vuetify);
6 | const savedTheme = localStorage.getItem('theme') || 'dark';
7 | export default new Vuetify({
8 | theme: {
9 | dark: savedTheme === 'dark',
10 | },
11 | lang: {
12 | locales: {ru},
13 | current: 'ru',
14 | },
15 | icons: {
16 | iconfont: 'mdi',
17 | },
18 | });
19 |
20 | document.querySelector('html')!.style.background = savedTheme === 'dark' ? '#303030' : '#fafafa';
21 |
--------------------------------------------------------------------------------
/src/UI/router.ts:
--------------------------------------------------------------------------------
1 | import FooterTemplate from '@/UI/views/FooterView.vue';
2 | import History from '@/UI/views/History.vue';
3 | import Options from '@/UI/views/Options.vue';
4 | import Player from '@/UI/views/Player.vue';
5 | import Vue from 'vue';
6 | import Router from 'vue-router';
7 |
8 | Vue.use(Router);
9 |
10 | export default new Router({
11 | routes: [
12 | {
13 | path: '/', component: FooterTemplate,
14 | children: [
15 | {name: 'player', path: 'player/anime/:anime/:episode?', component: Player},
16 | {name: 'history', path: 'history', component: History},
17 |
18 | {path: '', redirect: '/history'},
19 | ],
20 | },
21 | {name: 'options', path: '/options', component: Options},
22 | {path: '/player/anime', redirect: '/history'},
23 | {path: '/player', redirect: '/history'},
24 | ],
25 | });
26 |
--------------------------------------------------------------------------------
/src/UI/store/index.ts:
--------------------------------------------------------------------------------
1 | import {sync} from '@/helpers/chrome-storage';
2 |
3 | import Vue from 'vue';
4 | import Vuex from 'vuex';
5 | import Worker from 'worker-loader!./worker.ts';
6 | import {SelectedTranslation, WatchingHistoryItem} from '../../../types/UI';
7 | import {RootState} from './types';
8 |
9 | Vue.use(Vuex);
10 |
11 | export default new Vuex.Store({
12 | plugins: [
13 | async (store) => {
14 | chrome.storage.onChanged.addListener((changes) => {
15 | if (changes.userAuth) {
16 | store.commit('profile/loadCredentialsFromServer', changes.userAuth.newValue);
17 | store.dispatch('profile/loadUser');
18 | }
19 |
20 | if (changes.watching_history) {
21 | store.commit('player/setWatchingHistory', changes.watching_history.newValue || []);
22 | }
23 |
24 | if (changes.selectedTranslations) {
25 | store.commit('player/setSelectedTranslations', changes.selectedTranslations.newValue || []);
26 | }
27 | });
28 |
29 |
30 | store.subscribe((mutation, state) => {
31 | // вызывается после каждой мутации
32 | // мутация передаётся в формате `{ type, payload }`.
33 |
34 | if (mutation.type === 'profile/saveCredentials' || mutation.type === 'profile/logout') {
35 | sync.set({userAuth: state.profile.credentials});
36 | }
37 | });
38 |
39 |
40 | const {
41 | userAuth,
42 | watching_history,
43 | selectedTranslations,
44 | } = await sync.get<{
45 | userAuth: shikimori.Oauth | null
46 | watching_history: WatchingHistoryItem[],
47 | selectedTranslations: SelectedTranslation[],
48 | }>({
49 | userAuth: null,
50 | watching_history: [],
51 | selectedTranslations: [],
52 | });
53 |
54 | if (userAuth) {
55 | store.commit('profile/loadCredentialsFromServer', userAuth);
56 | store.dispatch('profile/loadUser');
57 | }
58 |
59 | if (watching_history) {
60 | store.commit('player/setWatchingHistory', watching_history);
61 | }
62 |
63 | if (selectedTranslations) {
64 | store.commit('player/setSelectedTranslations', selectedTranslations);
65 | }
66 | },
67 | ],
68 | });
69 |
70 | const worker = new Worker();
71 |
72 | export {
73 | worker,
74 | };
75 |
--------------------------------------------------------------------------------
/src/UI/store/profile/index.ts:
--------------------------------------------------------------------------------
1 | import {ShikimoriProvider} from '@/helpers/API/ShikimoriProvider';
2 | import {updateAuth} from '@/helpers/oauth-provider';
3 | import store from '@/UI/store';
4 | import {Action, getModule, Module, Mutation, VuexModule} from 'vuex-module-decorators';
5 |
6 | @Module({
7 | dynamic: true,
8 | namespaced: true,
9 | name: 'profile',
10 | store,
11 | })
12 | export class Profile extends VuexModule {
13 | public user: shikimori.User | null = null;
14 | public credentials: shikimori.Oauth | null = null;
15 |
16 |
17 | @Mutation
18 | public setUser(user: shikimori.User) {
19 | this.user = user;
20 | }
21 |
22 | @Mutation
23 | public saveCredentials(credentials: shikimori.Oauth) {
24 | this.credentials = credentials;
25 | }
26 |
27 |
28 | @Mutation
29 | public loadCredentialsFromServer(credentials: shikimori.Oauth) {
30 | this.credentials = credentials;
31 |
32 | if (!this.credentials || !this.credentials.access_token) {
33 | this.user = null;
34 | }
35 | }
36 |
37 | @Mutation
38 | public logout() {
39 | this.user = null;
40 | if (!this.credentials || !this.credentials.access_token) {
41 | return;
42 | }
43 | this.credentials.access_token = undefined;
44 | }
45 |
46 |
47 | @Action
48 | public async loadUser() {
49 | const auth = await this.getValidCredentials();
50 | if (!auth) {
51 | return;
52 | }
53 |
54 | try {
55 | /** @type {shikimori.User} */
56 | const user = await ShikimoriProvider.fetch(`/api/users/whoami`, {
57 | headers: {
58 | Authorization: `${auth.token_type} ${auth.access_token}`,
59 | },
60 | errorMessage: 'Невозможно загрузить ваш профиль',
61 | });
62 |
63 | if (user) {
64 | this.setUser(user);
65 | }
66 | } catch (e) {
67 | console.error(e);
68 | e.alert().track();
69 | }
70 | }
71 |
72 | @Action
73 | public async getValidCredentials(force: boolean = false) {
74 |
75 | let auth = this.credentials;
76 | const isLoggedIn = (auth && auth.access_token);
77 | const isFresh = (
78 | isLoggedIn
79 | && auth
80 | && auth.created_at
81 | && auth.expires_in
82 | && (
83 | 1000 * (auth.created_at + auth.expires_in) > Date.now()
84 | )
85 | );
86 |
87 | if (!isLoggedIn && !force) {
88 | return null;
89 | }
90 |
91 |
92 | if (isFresh) {
93 | return auth;
94 | }
95 |
96 | try {
97 | auth = await updateAuth();
98 | this.saveCredentials(auth);
99 | return auth;
100 | } catch (e) {
101 | console.error(e);
102 | e.alert().track();
103 |
104 | // Если сервер ответил ошибкой 401 — принудительно розлогинить пользователя
105 | if (e.response && e.response.status && e.response.status === 401) {
106 | this.logout();
107 | }
108 |
109 | return null;
110 | }
111 |
112 | }
113 | }
114 |
115 | export default getModule(Profile);
116 |
--------------------------------------------------------------------------------
/src/UI/store/types.ts:
--------------------------------------------------------------------------------
1 | import {Player} from '@/UI/store/player';
2 | import {Profile} from '@/UI/store/profile';
3 | import {Shikimori} from '@/UI/store/shikimori';
4 |
5 | export interface RootState {
6 | profile: Profile;
7 | shikimori: Shikimori;
8 | player: Player;
9 | }
10 |
--------------------------------------------------------------------------------
/src/UI/store/worker.ts:
--------------------------------------------------------------------------------
1 | import {getMostPriorityTranslation, getPriorityTranslationForEpisode} from '@/helpers/get-translation-priority';
2 | import {SelectedTranslation} from '../../../types/UI';
3 | // @ts-ignore
4 |
5 |
6 | const ctx: Worker = self as any;
7 |
8 | ctx.onmessage = async (
9 | {
10 | data: {episode, selectedTranslations},
11 | }: {
12 | data: { episode: anime365.Episode; selectedTranslations: SelectedTranslation[] };
13 | },
14 | ) => {
15 | if (!episode || !episode.translations || !episode.translations.length) {
16 | return ctx.postMessage({translation: undefined});
17 | }
18 |
19 | const history = new Map();
20 |
21 | selectedTranslations.forEach((translation) => {
22 | history.set(translation.id, translation);
23 | });
24 |
25 | const previousSelectedTranslation = history.get(episode.seriesId);
26 |
27 | // Если предыдущий перевод принадлежит текущей серии — его и возвращаем
28 | if (previousSelectedTranslation && previousSelectedTranslation.eId === episode.id) {
29 | const previousSelectedTranslationInEpisode = episode.translations.find(
30 | (t) => t.id === previousSelectedTranslation.id);
31 | if (previousSelectedTranslationInEpisode) {
32 | return ctx.postMessage({translation: previousSelectedTranslationInEpisode});
33 | }
34 | }
35 |
36 | const primaryTranslations = getPriorityTranslationForEpisode(history, episode);
37 | const primaryActiveTranslations = filterActiveTranslations(primaryTranslations);
38 |
39 | if (primaryActiveTranslations.length) {
40 | return ctx.postMessage({translation: getMostPriorityTranslation(primaryActiveTranslations)});
41 | }
42 |
43 | if (primaryTranslations.length) {
44 | return ctx.postMessage({translation: getMostPriorityTranslation(primaryTranslations)});
45 | }
46 |
47 | return ctx.postMessage({translation: getMostPriorityTranslation(episode.translations)});
48 |
49 |
50 | };
51 |
52 |
53 | function filterActiveTranslations(translations: anime365.Translation[]) {
54 | return translations.filter((t) => t.isActive);
55 | }
56 |
--------------------------------------------------------------------------------
/src/UI/views/FooterView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
30 |
--------------------------------------------------------------------------------
/src/UI/views/History.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 | {{anime.name}}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Здесь будет отображаться ваша история просмотров
26 |
27 | Откройте любое аниме на
28 | Шикимори
29 | или
30 | MyAnimeList
31 | и нажмите «Начать просмотр»
32 |
33 |
34 |
35 |
36 |
37 |
89 |
90 |
91 |
108 |
--------------------------------------------------------------------------------
/src/UI/views/Options.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
25 |
26 |
29 |
--------------------------------------------------------------------------------
/src/UI/views/Player.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
178 |
179 |
199 |
--------------------------------------------------------------------------------
/src/_locales/ru/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extName": {
3 | "message": "Play Шики Online"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/background/background.ts:
--------------------------------------------------------------------------------
1 | import './browserAction';
2 | import './install-update-events';
3 | import './loadBroadcast';
4 | import './request-provider';
5 | import './setBadgeMessageCount';
6 |
--------------------------------------------------------------------------------
/src/background/browserAction.ts:
--------------------------------------------------------------------------------
1 | chrome.browserAction.onClicked.addListener(() => { // Fired when User Clicks ICON
2 | const url = chrome.runtime.getURL('player.html#/history');
3 | chrome.tabs.create({url, active: true});
4 | });
5 |
--------------------------------------------------------------------------------
/src/background/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Play Шики Online Фоновый процесс
9 |
10 |
11 |
12 | We're sorry but play-shiki-class doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/background/install-update-events.ts:
--------------------------------------------------------------------------------
1 | import {local, sync} from '@/helpers/chrome-storage';
2 | import {push} from '@/helpers/runtime-messages';
3 | import {versionCompare} from '@/helpers/version-compare';
4 | // @ts-ignore
5 | import storage from 'kv-storage-polyfill';
6 | import {SelectedTranslation} from '../../types/UI';
7 |
8 | /**
9 | * Отслеживание установок и обновлений
10 | */
11 | chrome.runtime.onInstalled.addListener(async ({reason, previousVersion}: chrome.runtime.InstalledDetails) => {
12 | // reason = ENUM "install", "update", "chrome_update", or "shared_module_update"
13 |
14 | if (reason === 'install') {
15 | // Сохраняем время установки расширения
16 | await sync.set({
17 | installAt: Date.now(),
18 | });
19 |
20 | // Загружать сообщения из рассылки начиная с времени установки
21 | await local.set({
22 | runtimeMessagesLastCheck: Date.now(),
23 | });
24 | }
25 |
26 | // Создаем сообщение об обновлении
27 | if (reason === 'update') {
28 | const manifest = chrome.runtime.getManifest();
29 | await push({
30 | id: 'runtime-message-update',
31 | color: 'success',
32 | html: `${manifest.name} обновлен до версии ${manifest.version} Подробнее об изменениях → `,
33 | });
34 |
35 |
36 | /**
37 | * Если предыдущая версия 1.1.1 или ниже — перенести историю выбранных переводов
38 | * из kv-storage в chrome.storage.sync
39 | */
40 | if (previousVersion && versionCompare(previousVersion, '1.1.1') <= 0) {
41 | const lastSelectedTranslations: Map
42 | = await storage.get('lastSelectedTranslations');
43 |
44 | if (lastSelectedTranslations && lastSelectedTranslations.size > 0) {
45 |
46 | const newHistoryOfTranslations: SelectedTranslation[] = [];
47 | lastSelectedTranslations.forEach((translation) => {
48 | newHistoryOfTranslations.push({
49 | tId: translation.id,
50 | id: translation.seriesId,
51 | eId: translation.episodeId,
52 | author: translation.authorsSummary,
53 | type: translation.type,
54 | priority: translation.priority,
55 | });
56 | });
57 |
58 | /**
59 | * Пытаемся сохранить массив значений в хранилище
60 | * Если размер массива превышает квоту — удалить самую старую запись и повторить попытку
61 | */
62 | while (newHistoryOfTranslations.length > 0) {
63 | try {
64 | await sync.set({selectedTranslations: newHistoryOfTranslations});
65 | break;
66 | } catch (error) {
67 | if (error.message.indexOf('QUOTA_BYTES') !== -1) {
68 | newHistoryOfTranslations.shift();
69 | } else {
70 | throw error;
71 | }
72 | }
73 | }
74 |
75 | await storage.delete('lastSelectedTranslations');
76 | }
77 |
78 |
79 | }
80 |
81 |
82 | // Удаление устаревшего общего кэша
83 | setTimeout(() => {
84 | local.remove([
85 | 'anime-365-cache-v1',
86 | 'cachedSeries',
87 | 'myanimelist-cache-v1',
88 | 'requests-cache-v1',
89 | 'shikimori-cache-v1',
90 | ]);
91 | }, 0);
92 | }
93 |
94 | });
95 |
--------------------------------------------------------------------------------
/src/background/loadBroadcast.ts:
--------------------------------------------------------------------------------
1 | import {local} from '@/helpers/chrome-storage';
2 | import {loadRuntimeMessages} from './loadRuntimeMessages';
3 |
4 | export async function loadBroadcast() {
5 | let {runtimeMessagesLastCheck} = await local.get('runtimeMessagesLastCheck');
6 |
7 | // Сохраняем время запуска для ограничения следующей итерации
8 | await local.set({
9 | runtimeMessagesLastCheck: Date.now(),
10 | });
11 |
12 | if (!runtimeMessagesLastCheck || isNaN(runtimeMessagesLastCheck)) {
13 | runtimeMessagesLastCheck = Date.now();
14 | return [];
15 | }
16 |
17 | return loadRuntimeMessages(runtimeMessagesLastCheck, 'broadcast');
18 | }
19 |
20 | // Нет возможности загружать сообщения так как клуб был удален
21 | // setInterval(loadBroadcast, /* каждые 15 минут */1000 * 60 * 15);
22 |
--------------------------------------------------------------------------------
/src/background/loadRuntimeMessages.ts:
--------------------------------------------------------------------------------
1 | import {ShikimoriProvider} from '@/helpers/API/ShikimoriProvider';
2 | import {push as message} from '@/helpers/runtime-messages';
3 |
4 | export async function loadRuntimeMessages(minTimestamp: number, broadcastType = 'broadcast', maxLoadedMessages = 10) {
5 | const commentWithMessages = [];
6 | let page = 1;
7 | let lastCommentTimestamp = Date.now();
8 | try {
9 | while (minTimestamp <= lastCommentTimestamp && commentWithMessages.length < maxLoadedMessages) {
10 | const comments =
11 | await ShikimoriProvider
12 | .fetch(
13 | // tslint:disable-next-line:max-line-length
14 | `/api/comments/?desc=1&commentable_id=285393&commentable_type=Topic&limit=100&page=${page++}`,
15 | {errorMessage: 'Невозможно загрузить уведомления'},
16 | );
17 |
18 | if (!comments || !comments.length) {
19 | break;
20 | }
21 |
22 | lastCommentTimestamp = new Date(comments[comments.length - 1].created_at).getTime();
23 |
24 | commentWithMessages.push(
25 | ...comments
26 | .filter((comment: any) =>
27 | comment.user.id === 143570
28 | && new RegExp(`\\[div=runtime-message-${broadcastType} hidden\\]`, 'mi').test(comment.body)
29 | && new Date(comment.created_at).getTime() >= minTimestamp,
30 | ),
31 | );
32 |
33 | }
34 | } catch (e) {
35 | console.error(e);
36 | e.track();
37 | }
38 |
39 | if (commentWithMessages.length) {
40 | for (const comment of commentWithMessages) {
41 | if (!comment || !comment.body) {
42 | continue;
43 | }
44 | try {
45 | const runtimeMessage = JSON.parse(
46 | // @ts-ignore
47 | comment.body
48 | .replace(/\n+/gim, ' ')
49 | .match(
50 | new RegExp(
51 | `\\[div=runtime-message-${broadcastType} hidden\\](.+?)\\[\\/div\\]`,
52 | 'im',
53 | ),
54 | )[1],
55 | )
56 | ;
57 |
58 | runtimeMessage.id = comment.id;
59 |
60 | if (!runtimeMessage.link) {
61 | runtimeMessage.link = `https://shikimori.one/comments/${comment.id}`;
62 | }
63 |
64 | if (!runtimeMessage.html) {
65 | const rows = [];
66 |
67 | if (runtimeMessage.text) {
68 | rows.push(runtimeMessage.text);
69 | }
70 | if (runtimeMessage.linkText) {
71 | rows.push(
72 | `
73 |
74 | ${runtimeMessage.linkText}
75 |
76 | `,
77 | );
78 | }
79 |
80 | runtimeMessage.html = rows.join(' ');
81 | }
82 |
83 | message(runtimeMessage);
84 | } catch (e) {
85 | console.error(`Can't show broadcast message`, {error: e, comment});
86 | e.track();
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/background/request-provider.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Исполнение сетевых запросов
3 | */
4 | import {RequestProvider} from '@/helpers/API/RequestProvider';
5 |
6 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
7 | if (request.contentScriptQuery === 'fetchUrl') {
8 | RequestProvider.fetch(request.url, request.options)
9 | .then((response) => sendResponse({response}))
10 | .catch((error) => sendResponse({error: error.toJSON ? error.toJSON() : error}));
11 |
12 | return true; // Will respond asynchronously.
13 | }
14 | });
15 |
16 |
17 | chrome.webRequest.onBeforeSendHeaders.addListener(
18 | (details) => {
19 | const requestHeaders = details.requestHeaders;
20 | if (!requestHeaders || details.initiator !== `chrome-extension://${chrome.runtime.id}`) {
21 | return {requestHeaders};
22 | }
23 |
24 | for (const header of requestHeaders) {
25 | if (header.name === 'User-Agent') {
26 | const manifest = chrome.runtime.getManifest();
27 | header.value = `${manifest.name}; Browser extension; ${manifest.homepage_url}`;
28 | break;
29 | }
30 | }
31 | return {requestHeaders};
32 | },
33 | {
34 | urls: [
35 | 'https://shikimori.one/api/*',
36 | 'https://shikimori.one/oauth/*',
37 | 'https://smotret-anime-365.ru/api/*',
38 | 'https://smotret-anime.online/api/*',
39 | ],
40 | },
41 | ['requestHeaders', 'blocking'],
42 | );
43 |
--------------------------------------------------------------------------------
/src/background/setBadgeMessageCount.ts:
--------------------------------------------------------------------------------
1 | import {local} from '@/helpers/chrome-storage';
2 |
3 | async function setBadgeMessageCount() {
4 | const {runtimeMessages} = await local.get('runtimeMessages');
5 | if (!runtimeMessages || !Array.isArray(runtimeMessages)) {
6 | return chrome.browserAction.setBadgeText({text: ``});
7 | }
8 |
9 | const count = runtimeMessages.filter((m) => m.important).length;
10 | const text = count ? `${count}` : '';
11 | return chrome.browserAction.setBadgeText({text});
12 | }
13 |
14 |
15 | setBadgeMessageCount();
16 |
17 | chrome.storage.onChanged.addListener(({runtimeMessages}) => {
18 | if (runtimeMessages) {
19 | return setBadgeMessageCount();
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/src/content-scripts/anime365-player-events.ts:
--------------------------------------------------------------------------------
1 | import '@/content-scripts/anime365-player-styles.css';
2 | // @ts-ignore
3 | import storage from 'kv-storage-polyfill';
4 | import throttle from 'lodash.throttle';
5 | import videojs from 'video.js';
6 |
7 | declare global {
8 | interface Window {
9 | playerGlobal: (videojs.Player & { isPublicReady: boolean }) | undefined;
10 | }
11 | }
12 |
13 | declare const site: {
14 | isPremiumUser?: boolean,
15 | } | undefined;
16 |
17 |
18 | (() => {
19 | const params = new URLSearchParams(location.hash.slice(1));
20 |
21 | const SERIES_ID = params.get('play-shikimori[seriesId]');
22 | const EPISODE_ID = params.get('play-shikimori[episodeId]');
23 | const NEXT_EPISODE = params.get('play-shikimori[nextEpisode]') === '1';
24 | const IS_AUTO_PLAY = params.get('play-shikimori[isAutoPlay]') === '1';
25 | const IS_FULL_SCREEN = params.get('play-shikimori[fullScreen]') === '1';
26 |
27 | const savedTimePromise: Promise<{ episodeId: string, time: number } | undefined>
28 | = storage.get(`play-${SERIES_ID}-time`);
29 |
30 | async function main(player: videojs.Player) {
31 |
32 | // Перематываем видео
33 | restoreCurrentTime(player)
34 | .then(() => {
35 | // Если нет рекламы — запускаем видео
36 | if (IS_AUTO_PLAY && site && site.isPremiumUser) {
37 | player.play();
38 | }
39 | });
40 |
41 | // Инициализируем полноэкранный режим
42 | if (IS_FULL_SCREEN) {
43 | player.requestFullscreen();
44 | }
45 |
46 | // Проксируем события родителю
47 | proxyEventToParent(player, ['public-play', 'public-pause', 'public-ended']);
48 |
49 | // Начинаем сохранять прогресс просмотра
50 | if (SERIES_ID && EPISODE_ID) {
51 | saveCurrentTime(player);
52 | }
53 |
54 |
55 | // Создаём кнопку следующей серии
56 | if (NEXT_EPISODE) {
57 | createNextEpisodeButton(player);
58 | }
59 |
60 |
61 | }
62 |
63 |
64 | /**
65 | * Создаёт кнопку для переключения серии,
66 | * и подписывается на сотытия
67 | * click — для отправки события родителю
68 | * timeupdate — для изменения прозрачности
69 | */
70 | function createNextEpisodeButton(player: videojs.Player) {
71 | // Обязательно нужно добавлять кнопку в контейнер #main-video
72 | // Иначе она будет невидимой в полноэкранном режиме
73 | const mainVideo = document.querySelector('#main-video');
74 | if (!mainVideo) {
75 | return null;
76 | }
77 |
78 | /**
79 | * Создание кнопки
80 | */
81 | const nextEpisodeButton = document.createElement('button');
82 | nextEpisodeButton.innerText = 'Следующая серия';
83 | nextEpisodeButton.classList.add('next-episode');
84 | mainVideo.appendChild(nextEpisodeButton);
85 |
86 |
87 | /**
88 | * По клику отправляем событие родителю для переключения сериии
89 | */
90 | nextEpisodeButton.addEventListener('click', () => {
91 | window.parent.postMessage('mark-as-watched', '*');
92 | });
93 |
94 |
95 | /**
96 | * Отслеживаем прогресс просмотра и показываем/скрываем кнопку
97 | */
98 | const onTimeUpdateThrottled = throttle(() => {
99 | const currentTime = player.currentTime();
100 | const duration = player.duration();
101 |
102 | const endingTime = duration > 600 ? 120 : duration * 0.1;
103 | if (player.isFullscreen() && duration - currentTime <= endingTime) {
104 | if (!nextEpisodeButton.classList.contains('show')) {
105 | nextEpisodeButton.classList.add('show');
106 | }
107 | } else {
108 | if (nextEpisodeButton.classList.contains('show')) {
109 | nextEpisodeButton.classList.remove('show');
110 | }
111 | }
112 | }, 1000);
113 |
114 |
115 | player.on('timeupdate', onTimeUpdateThrottled);
116 |
117 | return nextEpisodeButton;
118 | }
119 |
120 |
121 | /**
122 | * Перематывает видео до последнего сохраненного момента
123 | * @param player
124 | */
125 | async function restoreCurrentTime(player: videojs.Player) {
126 | const savedTime = await savedTimePromise;
127 | if (!savedTime) {
128 | return;
129 | }
130 |
131 | if (savedTime.episodeId === EPISODE_ID) {
132 | player.currentTime(Math.max(0, savedTime.time));
133 | }
134 | }
135 |
136 |
137 | /**
138 | * Подписывается на переданный список событий и проксирует их родителю
139 | * @param player
140 | * @param events Список событий
141 | */
142 | function proxyEventToParent(player: videojs.Player, events: string[]) {
143 | player.on(events, (event) => {
144 | const message = event.type;
145 | window.parent.postMessage(message, '*');
146 | });
147 | }
148 |
149 |
150 | /**
151 | * Подписывается на событие timeupdate и сохраняет в памяти currentTime
152 | * @param player
153 | */
154 | function saveCurrentTime(player: videojs.Player) {
155 | const saveTimeThrottled = throttle(() => {
156 | return storage.set(`play-${SERIES_ID}-time`, {
157 | episodeId: EPISODE_ID,
158 | time: player.currentTime(),
159 | });
160 | }, 10000);
161 |
162 | player.on('timeupdate', saveTimeThrottled);
163 | }
164 |
165 | function sendUploadRequest() {
166 | const addUploadRequestForm = document.body.querySelector(
167 | 'form[action*="/translations/embedAddUploadRequest"]');
168 |
169 | if (addUploadRequestForm) {
170 | addUploadRequestForm.submit();
171 | return true;
172 | }
173 |
174 | return false;
175 | }
176 |
177 | /**
178 | * Проверяем наличие playerGlobal и запускаем главную функцию
179 | */
180 | function runOnReady(attempt: number = 0) {
181 | if (window.playerGlobal) {
182 | if (window.playerGlobal.isPublicReady) {
183 | main(window.playerGlobal)
184 | .catch((e) => {
185 | console.error(e);
186 | });
187 | } else {
188 | window.playerGlobal.one('public-ready', () =>
189 | main(window.playerGlobal!)
190 | .catch((e) => {
191 | console.error(e);
192 | }),
193 | );
194 | }
195 | } else if (document.readyState === 'complete') {
196 | if (attempt < 10 && !sendUploadRequest()) {
197 | setTimeout(() => runOnReady(attempt + 1), 100);
198 | }
199 | } else {
200 | window.addEventListener('load', () => runOnReady(), {once: true});
201 | }
202 | }
203 |
204 | runOnReady();
205 | })();
206 |
--------------------------------------------------------------------------------
/src/content-scripts/anime365-player-styles.css:
--------------------------------------------------------------------------------
1 | .next-episode {
2 | display: block;
3 | border: none;
4 | border-radius: 2px;
5 | padding: 10px 15px;
6 | font-size: 16px;
7 | color: rgba(255, 255, 255, 0.73) !important;
8 | text-decoration: none !important;
9 | box-sizing: border-box;
10 | position: fixed;
11 | z-index: 3;
12 | bottom: 70px;
13 | background: rgba(0, 0, 0, 0.71);
14 | cursor: pointer;
15 | right: 40px;
16 | opacity: 0;
17 | pointer-events: none;
18 | transition: opacity .5s;
19 | will-change: opacity;
20 | }
21 |
22 | .next-episode.show {
23 | opacity: 0.3;
24 | pointer-events: auto;
25 | }
26 |
27 |
28 | .next-episode.show:hover, .next-episode.show:focus {
29 | opacity: 1;
30 | }
31 |
--------------------------------------------------------------------------------
/src/content-scripts/inject-content-scripts.ts:
--------------------------------------------------------------------------------
1 | import {injectScript} from '@/helpers/injectScript';
2 |
3 |
4 | const config = new URLSearchParams(location.hash);
5 | if (config.get('#extension-id') === chrome.runtime.id) {
6 | injectScript(
7 | chrome.runtime.getURL('anime-365-player.js'),
8 | true,
9 | document.body || document.head || document.children[0],
10 | );
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/src/helpers/API/Anime365Provider.ts:
--------------------------------------------------------------------------------
1 | import {RequestProvider} from '@/helpers/API/RequestProvider';
2 | // @ts-ignore
3 | import storage from 'kv-storage-polyfill';
4 |
5 | declare interface CachedSeries {
6 | resp: anime365.api.SeriesCollection;
7 | maxAge: number;
8 | }
9 |
10 | const DAY = 1000 * 60 * 60 * 24;
11 |
12 | export class Anime365Provider extends RequestProvider {
13 | public static baseURL = 'https://smotret-anime.online/api';
14 | public static cachedSeries: Map = new Map();
15 |
16 | public static cachedSeriesReady: Promise = storage.get('cachedSeries')
17 | .then((storeData?: Map) => {
18 | if (storeData) {
19 | Anime365Provider.cachedSeries = storeData;
20 | }
21 | });
22 |
23 |
24 | public static async request(
25 | url: string,
26 | options: RequestInit & { errorMessage: string },
27 | ): Promise {
28 | const isSeriesRequest = /\/api\/series\//.test(url);
29 | if (isSeriesRequest) {
30 | await this.cachedSeriesReady;
31 | }
32 |
33 | if (isSeriesRequest && this.cachedSeries.has(url)) {
34 | const cache = this.cachedSeries.get(url);
35 | if (cache && cache.maxAge && cache.maxAge > Date.now()) {
36 | // @ts-ignore
37 | return cache.resp as T;
38 | }
39 | }
40 |
41 | try {
42 | const resp = await super.request(url, options);
43 |
44 | /**
45 | * Если текущий запрос — это запрос к списку серий
46 | * Запустить процесс кэширования
47 | */
48 | if (isSeriesRequest) {
49 | // @ts-ignore
50 | const SeriesCollection = resp as anime365.api.SeriesCollection;
51 |
52 | /**
53 | * Если список серий не пуст
54 | */
55 | if (SeriesCollection
56 | && SeriesCollection.data[0]
57 | && SeriesCollection.data[0].episodes
58 | && SeriesCollection.data[0].episodes.length > 0
59 | ) {
60 | // По умолчанию не кэшируем ответ
61 | let maxAge = 0;
62 |
63 | // Определяем дату последней добавленной серии
64 | const newestEpisodeDateTime = Math.max(
65 | ...SeriesCollection.data[0].episodes.map((e) => new Date(e.firstUploadedDateTime).getTime()),
66 | );
67 |
68 | // Если последняя серия была добавлена более двух недель назад
69 | // увеличить срок кэширования до 7 дней
70 | // if (newestEpisodeDateTime && newestEpisodeDateTime < (Date.now() - DAY * 14)) {
71 | // maxAge = Date.now() + DAY * 7;
72 | // }
73 |
74 | // Если последняя серия была добавлена более 30 дней назад
75 | // увеличить срок кэширования до 30 дней
76 | if (newestEpisodeDateTime && newestEpisodeDateTime < (Date.now() - DAY * 30)) {
77 | maxAge = Date.now() + DAY * 30;
78 | }
79 |
80 | this.cachedSeries.set(url, {resp: SeriesCollection, maxAge});
81 | if (this.cachedSeries.size > 5) {
82 | this.cachedSeries = new Map([...this.cachedSeries.entries()].splice(-5));
83 | }
84 | storage.set('cachedSeries', this.cachedSeries);
85 | }
86 |
87 | }
88 |
89 | return resp;
90 | } catch (e) {
91 | throw e;
92 | }
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/src/helpers/API/BackgroundRequestProvider.ts:
--------------------------------------------------------------------------------
1 | import {RequestProvider} from '@/helpers/API/RequestProvider';
2 | import {APIError} from '@/helpers/errors/APIError.class';
3 | import {AppError} from '@/helpers/errors/AppError.class';
4 | import {NetworkError} from '@/helpers/errors/NetworkError.class';
5 | import {PermissionError} from '@/helpers/errors/PermissionError.class';
6 |
7 | export class BackgroundRequestProvider extends RequestProvider {
8 | public static request(
9 | url: string,
10 | options: RequestInit & { errorMessage: string },
11 | ): Promise {
12 | return new Promise((resolve, reject) => {
13 | chrome.runtime.sendMessage(
14 | {
15 | contentScriptQuery: 'fetchUrl',
16 | url,
17 | options,
18 | },
19 | ({response, error}) => {
20 | if (error) {
21 | if (error.reason) {
22 | // Причина ошибки уже записана в поле message.
23 | // Её нужно удалить так как далее она будет добавлена заномо
24 | error.message = error.message.replace(`: ${error.reason}`, '');
25 | }
26 | switch (error.name) {
27 | case 'AppError':
28 | return reject(new AppError(error));
29 | case 'APIError':
30 | return reject(new APIError(error));
31 | case 'NetworkError':
32 | return reject(new NetworkError(error));
33 | case 'PermissionError':
34 | return reject(new PermissionError(error));
35 | default:
36 | return reject(new AppError(error));
37 | }
38 | } else {
39 | resolve(response);
40 | }
41 | },
42 | );
43 | });
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/helpers/API/MyAnimeListProvider.ts:
--------------------------------------------------------------------------------
1 | import {RequestProvider} from '@/helpers/API/RequestProvider';
2 |
3 | export class MyAnimeListProvider extends RequestProvider {
4 | public static baseURL = 'https://api.jikan.moe/v3';
5 | }
6 |
--------------------------------------------------------------------------------
/src/helpers/API/RequestProvider.ts:
--------------------------------------------------------------------------------
1 | import {APIError} from '@/helpers/errors/APIError.class';
2 | import {PermissionError} from '@/helpers/errors/PermissionError.class';
3 | // @ts-ignore
4 | import {origins, permissions} from '@/manifest.js';
5 | import retry from 'async-retry';
6 |
7 | export class RequestProvider {
8 | public static baseURL = '';
9 |
10 |
11 | public static async fetch(
12 | url: string,
13 | options: RequestInit & { errorMessage: string },
14 | ): Promise {
15 | url = this.baseURL + url;
16 | let granted = await this.isPermissionsGranted(url);
17 | if (!granted) {
18 | try {
19 | granted = await this.requestPermissions({permissions, origins});
20 | } catch (e) {
21 | console.warn('Невозможно запросить у пользователя разрешение', {error: e});
22 | }
23 | }
24 |
25 | if (!granted) {
26 | throw new PermissionError({
27 | message: options.errorMessage,
28 | }, new URL(url).host);
29 | }
30 |
31 | return this.request(url, options);
32 | }
33 |
34 | public static async request(
35 | url: string,
36 | options: RequestInit & { errorMessage: string },
37 | ): Promise {
38 |
39 | if (options.credentials === undefined) {
40 | options.credentials = 'omit';
41 | }
42 |
43 | options.headers = Object.assign({
44 | 'Accept': 'application/json',
45 | 'Content-Type': 'application/json',
46 | }, options.headers || {});
47 |
48 |
49 | return retry(async (bail: (e: Error) => void) => {
50 | return fetch(url, options)
51 | .then(async (resp) => {
52 | const error = await this.checkResponse(resp, bail, options.errorMessage);
53 | if (error) {
54 | error.request = {url, options};
55 |
56 | const apiError = new APIError(error);
57 |
58 | if (resp.status !== 429 && resp.status >= 400 && resp.status < 500) {
59 | return bail(apiError);
60 | } else {
61 | throw apiError;
62 | }
63 | } else {
64 | return await resp.json();
65 | }
66 | });
67 | }, {
68 | retries: 3,
69 | });
70 | }
71 |
72 | public static isPermissionsGranted(url: string) {
73 | return new Promise((resolve) => {
74 | if (!chrome || !chrome.permissions || !chrome.permissions.contains) {
75 | return resolve(true);
76 | }
77 | const info = new URL(url);
78 | chrome.permissions.contains(
79 | {
80 | origins: [`${info.protocol}//${info.hostname}/*`],
81 | },
82 | resolve,
83 | );
84 | });
85 | }
86 |
87 | // tslint:disable-next-line:no-shadowed-variable
88 | public static requestPermissions(permissions: chrome.permissions.Permissions) {
89 | return new Promise((resolve, reject) => {
90 | chrome.permissions.request(permissions, (granted) => {
91 | if (chrome.runtime.lastError) {
92 | reject(new Error(chrome.runtime.lastError.message));
93 | }
94 | resolve(granted);
95 | });
96 | });
97 | }
98 |
99 | protected static async checkResponse(
100 | resp: Response,
101 | bail: (e: Error) => void,
102 | errorMessage: string = 'Невозможно выполнить запрос',
103 | ): Promise {
104 | if (resp.ok) {
105 | return null;
106 | }
107 |
108 | let body = null;
109 |
110 | try {
111 | body = await resp.json();
112 | } catch (e) {
113 | try {
114 | body = await resp.text();
115 | } catch {
116 | }
117 | }
118 |
119 | let reason = `Сервер ответил с ошибкой ${resp.status}`;
120 |
121 | if (body && body.message) {
122 | reason = `${reason}; ${body.message}`;
123 | }
124 |
125 | return {
126 | name: 'APIError',
127 | message: errorMessage,
128 | reason,
129 | response: {
130 | status: resp.status,
131 | body,
132 | },
133 | };
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/helpers/API/ShikimoriProvider.ts:
--------------------------------------------------------------------------------
1 | import {RequestProvider} from '@/helpers/API/RequestProvider';
2 |
3 | export class ShikimoriProvider extends RequestProvider {
4 | public static baseURL = 'https://shikimori.one';
5 |
6 | protected static async checkResponse(
7 | resp: Response,
8 | bail: (e: Error) => void,
9 | errorMessage: string = 'Невозможно выполнить запрос к Шикимори',
10 | ) {
11 | const error = await super.checkResponse(resp, bail, errorMessage);
12 |
13 | if (
14 | error
15 | && error.response
16 | && error.response.body
17 | && Array.isArray(error.response.body)
18 | && error.response.body.every((i) => typeof i === 'string')
19 | ) {
20 | error.reason = `${error.reason}; ${error.response.body.join('; ')}`;
21 | }
22 |
23 | return error;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/helpers/chrome-storage.ts:
--------------------------------------------------------------------------------
1 | import LocalStorageArea = chrome.storage.LocalStorageArea;
2 | import SyncStorageArea = chrome.storage.SyncStorageArea;
3 |
4 | class ChromeStorageArea {
5 | private namespace: SyncStorageArea | LocalStorageArea;
6 |
7 | /**
8 | * @param {'sync' | 'local'} namespace
9 | */
10 | constructor(namespace: 'sync' | 'local') {
11 | this.namespace = chrome.storage[namespace];
12 | }
13 |
14 |
15 | /**
16 | *
17 | * @param {string | Object | string[]} keys
18 | */
19 | public get(keys: string | string[] | object | null): Promise {
20 | return new Promise((resolve, reject) => {
21 | this.namespace.get(keys, (items) => {
22 | const err = chrome.runtime.lastError;
23 | if (err) {
24 | reject(err);
25 | } else {
26 | // @ts-ignore
27 | resolve(items);
28 | }
29 | });
30 | });
31 | }
32 |
33 |
34 | /**
35 | *
36 | * @param {Object} items
37 | */
38 | public set(items: object) {
39 | return new Promise((resolve, reject) => {
40 | this.namespace.set(items, () => {
41 | const err = chrome.runtime.lastError;
42 | if (err) {
43 | reject(err);
44 | } else {
45 | resolve();
46 | }
47 | });
48 | });
49 | }
50 |
51 |
52 | /**
53 | *
54 | * @param {string | string[] | undefined} keys
55 | */
56 | public getBytesInUse(keys: string | string[] | null) {
57 | return new Promise((resolve, reject) => {
58 | this.namespace.getBytesInUse(keys, (items) => {
59 | const err = chrome.runtime.lastError;
60 | if (err) {
61 | reject(err);
62 | } else {
63 | resolve(items);
64 | }
65 | });
66 | });
67 | }
68 |
69 |
70 | /**
71 | *
72 | * @param {string | string[] | undefined} keys
73 | */
74 | public remove(keys: string | string[]) {
75 | return new Promise((resolve, reject) => {
76 | this.namespace.remove(keys, () => {
77 | const err = chrome.runtime.lastError;
78 | if (err) {
79 | reject(err);
80 | } else {
81 | resolve();
82 | }
83 | });
84 | });
85 | }
86 |
87 |
88 | public clear() {
89 | return new Promise((resolve, reject) => {
90 | this.namespace.clear(() => {
91 | const err = chrome.runtime.lastError;
92 | if (err) {
93 | reject(err);
94 | } else {
95 | resolve();
96 | }
97 | });
98 | });
99 | }
100 |
101 |
102 | /**
103 | * Выполняет unshift в массив данных в Chrome Storage.
104 | * Если новый массив превышает квоту — удаляет елементы массива, начиная от самых старых,
105 | * до тех пор пока результирующий массив данных не поместится в квоту
106 | *
107 | * @param {string} key Ключ переменной в хранилище
108 | * @param {{id: any, [key: string]: any}} value Данные для сохранения в массыв в хранилище
109 | * @returns {Promise} Массив сохраненных данных
110 | */
111 | public async unshift(key: string, value: { id: any, [k: string]: any }) {
112 | let {[key]: array} = await this.get<{ [key: string]: any[] }>({[key]: []});
113 | array = (array || []).filter((item) => item && item.id !== value.id);
114 | array.unshift(value);
115 |
116 | while (array.length) {
117 | try {
118 | await this.set({[key]: array});
119 | break;
120 | } catch (error) {
121 | if (error.message.indexOf('QUOTA_BYTES') !== -1) {
122 | array.pop();
123 | } else {
124 | return Promise.reject(error);
125 | }
126 | }
127 | }
128 |
129 | return array;
130 | }
131 | }
132 |
133 |
134 | export const sync = new ChromeStorageArea('sync');
135 | export const local = new ChromeStorageArea('local');
136 |
--------------------------------------------------------------------------------
/src/helpers/clear-string.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Функция очищает строку
3 | * Удаляет всё кроме букв
4 | * @param {string} name
5 | * @returns {string}
6 | *
7 | * @example
8 | *
9 | * clearString('AniDub (JAM & Nika & Persona99)') // -> anidubjamnikapersona
10 | */
11 | export function clearString(name: string): string {
12 | // const regex = /[^\p{L}]/giu; // не работает в firefox 67.0.4
13 | // tslint:disable-next-line:max-line-length
14 | const regex = /(?:[\0-@\[-`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u036F\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482-\u0489\u0530\u0557\u0558\u055A-\u055F\u0589-\u05CF\u05EB-\u05EE\u05F3-\u061F\u064B-\u066D\u0670\u06D4\u06D6-\u06E4\u06E7-\u06ED\u06F0-\u06F9\u06FD\u06FE\u0700-\u070F\u0711\u0730-\u074C\u07A6-\u07B0\u07B2-\u07C9\u07EB-\u07F3\u07F6-\u07F9\u07FB-\u07FF\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u083F\u0859-\u085F\u086B-\u089F\u08B5\u08BE-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962-\u0970\u0981-\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA-\u09BC\u09BE-\u09CD\u09CF-\u09DB\u09DE\u09E2-\u09EF\u09F2-\u09FB\u09FD-\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34-\u0A37\u0A3A-\u0A58\u0A5D\u0A5F-\u0A71\u0A75-\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA-\u0ABC\u0ABE-\u0ACF\u0AD1-\u0ADF\u0AE2-\u0AF8\u0AFA-\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A-\u0B3C\u0B3E-\u0B5B\u0B5E\u0B62-\u0B70\u0B72-\u0B82\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BCF\u0BD1-\u0C04\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C3E-\u0C57\u0C5B-\u0C5F\u0C62-\u0C7F\u0C81-\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA-\u0CBC\u0CBE-\u0CDD\u0CDF\u0CE2-\u0CF0\u0CF3-\u0D04\u0D0D\u0D11\u0D3B\u0D3C\u0D3E-\u0D4D\u0D4F-\u0D53\u0D57-\u0D5E\u0D62-\u0D79\u0D80-\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0E00\u0E31-\u0E3F\u0E47-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EB1\u0EB4-\u0EBC\u0EBE\u0EBF\u0EC5\u0EC7-\u0EDB\u0EE0-\u0EFF\u0F01-\u0F3F\u0F48\u0F6D-\u0F87\u0F8D-\u0FFF\u102B-\u103E\u1040-\u104F\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16F0\u16F9-\u16FF\u170D\u1712-\u171F\u1732-\u173F\u1752-\u175F\u176D\u1771-\u177F\u17B4-\u17D6\u17D8-\u17DB\u17DD-\u181F\u1879-\u187F\u1885-\u18A9\u18AB-\u18AF\u18F6-\u18FF\u191F-\u194F\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19FF\u1A17-\u1A1F\u1A55-\u1AA6\u1AA8-\u1B04\u1B34-\u1B44\u1B4C-\u1B82\u1BA1-\u1BAD\u1BB0-\u1BB9\u1BE6-\u1BFF\u1C24-\u1C4C\u1C50-\u1C59\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CE8\u1CED\u1CF4\u1CF7-\u1CF9\u1CFB-\u1CFF\u1DC0-\u1DFF\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u2182\u2185-\u2BFF\u2C2F\u2C5F\u2CE5-\u2CEA\u2CEF-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7F\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF-\u2E2E\u2E30-\u3004\u3007-\u3030\u3036-\u303A\u303D-\u3040\u3097-\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31BB-\u31EF\u3200-\u33FF\u4DB6-\u4DFF\u9FF0-\u9FFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA620-\uA629\uA62C-\uA63F\uA66F-\uA67E\uA69E\uA69F\uA6E6-\uA716\uA720\uA721\uA789\uA78A\uA7C0\uA7C1\uA7C7-\uA7F6\uA802\uA806\uA80B\uA823-\uA83F\uA874-\uA881\uA8B4-\uA8F1\uA8F8-\uA8FA\uA8FC\uA8FF-\uA909\uA926-\uA92F\uA947-\uA95F\uA97D-\uA983\uA9B3-\uA9CE\uA9D0-\uA9DF\uA9E5\uA9F0-\uA9FF\uAA29-\uAA3F\uAA43\uAA4C-\uAA5F\uAA77-\uAA79\uAA7B-\uAA7D\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAC3-\uAADA\uAADE\uAADF\uAAEB-\uAAF1\uAAF5-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB68-\uAB6F\uABE3-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB1E\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFE6F\uFE75\uFEFD-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEFF\uDF20-\uDF2C\uDF41\uDF4A-\uDF4F\uDF76-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0-\uDFFF]|\uD801[\uDC9E-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE01-\uDE0F\uDE14\uDE18\uDE36-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE5-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD24-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF46-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC00-\uDC02\uDC38-\uDC82\uDCB0-\uDCCF\uDCE9-\uDD43\uDD45-\uDD4F\uDD73-\uDD75\uDD77-\uDD82\uDDB3-\uDDC0\uDDC5-\uDDD9\uDDDB\uDDDD-\uDDFF\uDE12\uDE2C-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEDF-\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A-\uDF3C\uDF3E-\uDF4F\uDF51-\uDF5C\uDF62-\uDFFF]|\uD805[\uDC35-\uDC46\uDC4B-\uDC5E\uDC60-\uDC7F\uDCB0-\uDCC3\uDCC6\uDCC8-\uDD7F\uDDAF-\uDDD7\uDDDC-\uDDFF\uDE30-\uDE43\uDE45-\uDE7F\uDEAB-\uDEB7\uDEB9-\uDEFF\uDF1B-\uDFFF]|\uD806[\uDC2C-\uDC9F\uDCE0-\uDCFE\uDD00-\uDD9F\uDDA8\uDDA9\uDDD1-\uDDE0\uDDE2\uDDE4-\uDDFF\uDE01-\uDE0A\uDE33-\uDE39\uDE3B-\uDE4F\uDE51-\uDE5B\uDE8A-\uDE9C\uDE9E-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC2F-\uDC3F\uDC41-\uDC71\uDC90-\uDCFF\uDD07\uDD0A\uDD31-\uDD45\uDD47-\uDD5F\uDD66\uDD69\uDD8A-\uDD97\uDD99-\uDEDF\uDEF3-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC00-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD823-\uD82B\uD82D\uD82E\uD830-\uD834\uD836\uD837\uD839\uD83C-\uD83F\uD87B-\uD87D\uD87F-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F-\uDECF\uDEEE-\uDF3F\uDF44-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4F\uDF51-\uDF92\uDFA0-\uDFDF\uDFE2\uDFE4-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD822[\uDEF3-\uDFFF]|\uD82C[\uDD1F-\uDD4F\uDD53-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC-\uDFFF]|\uD838[\uDC00-\uDCFF\uDD2D-\uDD36\uDD3E-\uDD4D\uDD4F-\uDEBF\uDEEC-\uDFFF]|\uD83A[\uDCC5-\uDCFF\uDD44-\uDD4A\uDD4C-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04-\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD869[\uDED7-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/gi;
15 | return name.replace(regex, '')
16 | .trim()
17 | .toLowerCase();
18 | }
19 |
--------------------------------------------------------------------------------
/src/helpers/errors/APIError.class.ts:
--------------------------------------------------------------------------------
1 | import {AppError} from '@/helpers/errors/AppError.class';
2 |
3 | export class APIError extends AppError {
4 | constructor(error: string | AppErrorSchema = {}) {
5 |
6 | if (typeof error === 'string') {
7 | error = {message: error};
8 | }
9 |
10 | error = Object.assign({
11 | name: 'APIError',
12 | }, error);
13 |
14 | super(error);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/helpers/errors/AppError.class.ts:
--------------------------------------------------------------------------------
1 | import {errorMessage} from '@/helpers/runtime-messages';
2 |
3 |
4 | /**
5 | * Показывает всплывающий попап с ошибкой для пользователя
6 | */
7 | Error.prototype.alert = DOMException.prototype.alert = function() {
8 | errorMessage(this.message);
9 | return this;
10 | };
11 |
12 |
13 | /**
14 | * Отправить ошибку в систему трекинга
15 | */
16 | Error.prototype.track = DOMException.prototype.track = function() {
17 | return this;
18 | };
19 |
20 | Error.prototype.toJSON = DOMException.prototype.toJSON = function() {
21 | const error = {
22 | name: this.name,
23 | message: this.message,
24 | stack: this.stack || '',
25 | };
26 |
27 | for (const key in this) {
28 | // @ts-ignore
29 | if (typeof this[key] !== 'function') {
30 | // @ts-ignore
31 | error[key] = this[key];
32 | }
33 | }
34 |
35 | return error;
36 | };
37 |
38 |
39 | export class AppError extends Error {
40 | public reason?: string;
41 | public response?: {
42 | [key: string]: any,
43 | };
44 | public request?: {
45 | [key: string]: any,
46 | };
47 |
48 | constructor(error: string | AppErrorSchema = {}) {
49 | if (typeof error === 'string') {
50 | error = {message: error};
51 | }
52 |
53 |
54 | if (error.reason) {
55 | error.message = `${error.message}: ${error.reason}`;
56 | }
57 |
58 |
59 | super(error.message);
60 |
61 | Object.assign(this, {
62 | name: 'AppError',
63 | message: 'Невозможно выполнить операцию',
64 | },
65 | error,
66 | );
67 |
68 | // Підтримує правильне трасування стеку в точці, де була викинута помилка (працює лише на V8)
69 | // @ts-ignore
70 | if (Error.captureStackTrace) {
71 | // @ts-ignore
72 | Error.captureStackTrace(this, this.__proto__.constructor);
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/helpers/errors/NetworkError.class.ts:
--------------------------------------------------------------------------------
1 | import {AppError} from '@/helpers/errors/AppError.class';
2 |
3 | export class NetworkError extends AppError {
4 | constructor(error: string | AppErrorSchema = {}) {
5 |
6 | if (typeof error === 'string') {
7 | error = {message: error};
8 | }
9 |
10 | error = Object.assign({
11 | name: 'NetworkError',
12 | reason: 'нет интернет соединения',
13 | }, error);
14 |
15 | super(error);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/helpers/errors/PermissionError.class.ts:
--------------------------------------------------------------------------------
1 | import {AppError} from '@/helpers/errors/AppError.class';
2 |
3 | export class PermissionError extends AppError {
4 |
5 | constructor(error: string | AppErrorSchema = {}, origin?: string) {
6 |
7 | if (typeof error === 'string') {
8 | error = {message: error};
9 | }
10 |
11 | error = Object.assign({
12 | name: 'PermissionError',
13 | reason: `вы не дали разрешение на доступ к ресурсу ${origin || ''}`,
14 | }, error);
15 |
16 | super(error);
17 |
18 | /**
19 | * Если ошибка произошла из-за отсутствия прав на доступ к ресурсу
20 | * Нет необходимости её отслеживать
21 | */
22 | this.track = function() {
23 | return this;
24 | };
25 | }
26 | }
27 |
28 | // @ts-ignore
29 | window.PermissionError = PermissionError;
30 |
--------------------------------------------------------------------------------
/src/helpers/filter-episodes.ts:
--------------------------------------------------------------------------------
1 | export function filterEpisodes(
2 | {episodes, type, numberOfEpisodes}
3 | : { episodes?: anime365.Episode[], type?: string, numberOfEpisodes?: number }
4 | = {},
5 | ) {
6 | if (!episodes || !episodes.length) {
7 | return [];
8 | }
9 |
10 | let filteredEpisodes = episodes.filter((e) => e.isActive && (
11 | // @ts-ignore
12 | !numberOfEpisodes || parseFloat(e.episodeInt) <= numberOfEpisodes
13 | ));
14 |
15 | if (!filteredEpisodes.length) {
16 | return [];
17 | }
18 |
19 | const episodeType = filteredEpisodes[0].episodeType;
20 | if (!filteredEpisodes.every((e) => e.episodeType === episodeType)) {
21 | filteredEpisodes = filteredEpisodes.filter((e) => e.episodeType === type);
22 | }
23 |
24 | return filteredEpisodes;
25 | }
26 |
--------------------------------------------------------------------------------
/src/helpers/find-episode.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Оптимальный поиск серии на основе её порядкового номера
3 | * @param {anime365.Episode[]} episodes
4 | * @param {number} episodeInt
5 | */
6 | export function findEpisode(episodes: anime365.Episode[], episodeInt: number) {
7 | const targetEpisode = episodes[episodeInt];
8 | // @ts-ignore
9 | if (targetEpisode && parseFloat(targetEpisode.episodeInt) === episodeInt) {
10 | return targetEpisode;
11 | }
12 |
13 | let index = 1;
14 | while (episodes[episodeInt + index] || episodes[episodeInt - index]) {
15 | // @ts-ignore
16 | if (episodes[episodeInt - index] && parseFloat(episodes[episodeInt - index].episodeInt) === episodeInt) {
17 | return episodes[episodeInt - index];
18 | }
19 |
20 | // @ts-ignore
21 | if (episodes[episodeInt + index] && parseFloat(episodes[episodeInt + index].episodeInt) === episodeInt) {
22 | return episodes[episodeInt + index];
23 | }
24 | index++;
25 | }
26 |
27 | return undefined;
28 | }
29 |
--------------------------------------------------------------------------------
/src/helpers/get-review-url.ts:
--------------------------------------------------------------------------------
1 | export function getReviewUrl() {
2 | 'https://addons.mozilla.org/uk/firefox/addon/play-shikimori/reviews/';
3 |
4 | // Другие способы не допускаются, так как расширение не размещено в магазинах
5 | // switch (chrome.runtime.id) {
6 | // case 'ionmfilakmebhkkioabnjhghleommgjb':
7 | // return 'https://addons.opera.com/ru/extensions/details/play-shikimori-beta/';
8 | //
9 | // case '{dd3b05c4-06cb-4775-b47a-a30f3dfe8532}':
10 | // return 'https://addons.mozilla.org/uk/firefox/addon/play-shikimori/reviews/';
11 | //
12 | // default:
13 | // tslint:disable-next-line:max-line-length
14 | // return 'https://chrome.google.com/webstore/detail/play-%D1%88%D0%B8%D0%BA%D0%B8%D0%BC%D0%BE%D1%80%D0%B8-online/eopmgkejoplocjnpljjhgbeadjoomcbd/reviews?hl=ru';
15 | //
16 | // }
17 | }
18 |
--------------------------------------------------------------------------------
/src/helpers/get-translation-priority.ts:
--------------------------------------------------------------------------------
1 | import {SelectedTranslation} from '../../types/UI';
2 | import {clearString} from './clear-string';
3 |
4 |
5 | export function clearAuthorSummary(authorsSummary: string) {
6 | return clearString(authorsSummary.replace(/\(.*\)/g, ''));
7 | }
8 |
9 |
10 | /**
11 | *
12 | * @param {Map} translations
13 | */
14 | export function getAuthorsPriority(translations: Map) {
15 | const authors = new Map();
16 |
17 | translations.forEach((transaction) => {
18 | if (!transaction || !transaction.author) {
19 | return;
20 | }
21 |
22 | const authorsSummary = clearAuthorSummary(transaction.author);
23 |
24 | if (!authorsSummary) {
25 | return;
26 | }
27 |
28 | if (!authors.has(authorsSummary)) {
29 | authors.set(authorsSummary, 0);
30 | }
31 |
32 | authors.set(authorsSummary, authors.get(authorsSummary) + 1);
33 | });
34 |
35 | authors.forEach((count, author) => {
36 | authors.set(author, count / translations.size);
37 | });
38 |
39 | return authors;
40 | }
41 |
42 |
43 | /**
44 | *
45 | * @param {Map} translations
46 | */
47 | export function getTypesPriority(translations: Map) {
48 | const types = new Map();
49 |
50 | translations.forEach((transaction) => {
51 | if (!transaction || !transaction.type) {
52 | return;
53 | }
54 |
55 | if (!types.has(transaction.type)) {
56 | types.set(transaction.type, 0);
57 | }
58 |
59 | types.set(transaction.type, types.get(transaction.type) + 1);
60 | });
61 |
62 | types.forEach((count, t) => {
63 | types.set(t, count / translations.size);
64 | });
65 |
66 | return types;
67 | }
68 |
69 |
70 | /**
71 | *
72 | * @param {anime365.Translation[]} translations
73 | * @param {string} authorsSummaryRaw
74 | * @param {string} type
75 | */
76 | export function filterTranslationsByAuthor(
77 | translations: anime365.Translation[],
78 | authorsSummaryRaw: string,
79 | type?: string,
80 | ) {
81 | const authorsSummary = clearAuthorSummary(authorsSummaryRaw);
82 | if (!authorsSummary) {
83 | return [];
84 | }
85 |
86 | return translations.filter((translation) => {
87 | const summary = clearAuthorSummary(translation.authorsSummary);
88 |
89 | return summary &&
90 | (
91 | authorsSummary.indexOf(summary) >= 0 || summary.indexOf(authorsSummary) >= 0
92 | )
93 | && (
94 | !type || type === translation.type
95 | );
96 |
97 | });
98 | }
99 |
100 |
101 | /**
102 | *
103 | * @param {anime365.Translation[]} translations
104 | */
105 | export function getMostPriorityTranslation(translations: anime365.Translation[]) {
106 | if (!translations || !translations.length) {
107 | return null;
108 | }
109 | let maxPriority = 0;
110 | let maxPriorityTranslation = translations[0];
111 |
112 | for (const t of translations) {
113 | if (t.priority > maxPriority) {
114 | maxPriority = t.priority;
115 | maxPriorityTranslation = t;
116 | }
117 | }
118 |
119 | return maxPriorityTranslation;
120 | }
121 |
122 |
123 | /**
124 | *
125 | * @param {Map} history
126 | * @param {anime365.Episode} episode
127 | */
128 | export function getPriorityTranslationForEpisode(
129 | history: Map,
130 | episode: anime365.Episode,
131 | ) {
132 |
133 | if (!episode || !episode.translations || !episode.translations.length) {
134 | return [];
135 | }
136 |
137 | // Выбираем перевод используемый для предыдущих серий
138 | const previousUserTranslation = history.get(episode.seriesId);
139 |
140 | const previousUserTranslationAuthor = previousUserTranslation ? previousUserTranslation.author : undefined;
141 | const previousUserTranslationType = previousUserTranslation ? previousUserTranslation.type : undefined;
142 | if (previousUserTranslationAuthor && previousUserTranslationType) {
143 | // Поиск перевода от того же автора
144 | const priorityTranslations = filterTranslationsByAuthor(
145 | episode.translations,
146 | previousUserTranslationAuthor,
147 | previousUserTranslationType,
148 | );
149 |
150 | // Если для текущей серии найден перевод того же автора что сохранен в истории — возвращаем
151 | if (priorityTranslations.length) {
152 | return priorityTranslations;
153 | }
154 | }
155 |
156 | // Карта авторов и их индекс популярности
157 | const authorPriorityMap = [...getAuthorsPriority(history)]
158 | // Не учитываем авторов которые используються реже чем в 10% случаев
159 | .filter(([, rating]) => rating >= 0.1)
160 | // Сортируем всех авторов в порядке популярности
161 | .sort(([, rating1], [, rating2]) => rating2 - rating1);
162 |
163 |
164 | // Перебираем всех авторов в порядке популярности
165 | for (const [author] of authorPriorityMap) {
166 | const filtered = filterTranslationsByAuthor(episode.translations, author, previousUserTranslationType);
167 |
168 | // Если перевод от одного из популярных авторов найден — вернуть его
169 | if (filtered && filtered.length) {
170 | return filtered;
171 | }
172 | }
173 |
174 | // Поиск перевода от того же типа что и сохраненный
175 | if (previousUserTranslationType) {
176 | const priorityTranslations = episode.translations.filter((t) => t.type === previousUserTranslationType);
177 |
178 | // Если для текущей серии найден перевод того же типа что сохранен в истории — возвращаем
179 | if (priorityTranslations.length) {
180 | return priorityTranslations;
181 | }
182 | }
183 |
184 | // Карта типов переводов и их индекс популярности
185 | const typePriorityMap = [...getTypesPriority(history)]
186 | // Не учитываем типы которые используються реже чем в 10% случаев
187 | .filter(([, rating]) => rating >= 0.1)
188 | // Сортируем все типы в порядке популярности
189 | .sort(([, rating1], [, rating2]) => rating2 - rating1);
190 |
191 | // Перебираем все типы в порядке популярности
192 | for (const [type] of typePriorityMap) {
193 | const filtered = episode.translations.filter((t) => t.type === type);
194 |
195 | // Если перевод одного из популярных типов найден — вернуть его
196 | if (filtered && filtered.length) {
197 | return filtered;
198 | }
199 | }
200 |
201 | return episode.translations;
202 | }
203 |
--------------------------------------------------------------------------------
/src/helpers/injectScript.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Вставляет скрипт на страницу
3 | * @param {string} src URL script
4 | * @param {boolean} async attribute
5 | * @param {HTMLElement} parent container
6 | */
7 | export function injectScript(src: string, async = true, parent = document.head) {
8 | return new Promise((resolve, reject) => {
9 | const script = document.createElement('script');
10 | script.defer = async;
11 | script.async = async;
12 | script.src = src;
13 |
14 | const res = () => {
15 | resolve();
16 | clear();
17 | };
18 |
19 | const rej = () => {
20 | reject('Error loading script');
21 | clear();
22 | };
23 |
24 | const clear = () => {
25 | script.removeEventListener('load', res);
26 | script.removeEventListener('error', rej);
27 | script.removeEventListener('abort', rej);
28 | };
29 |
30 | script.addEventListener('load', res, {once: true});
31 | script.addEventListener('error', rej, {once: true});
32 | script.addEventListener('abort', rej, {once: true});
33 | parent.appendChild(script);
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/src/helpers/oauth-provider.ts:
--------------------------------------------------------------------------------
1 | import {ShikimoriProvider} from '@/helpers/API/ShikimoriProvider';
2 | import {sync} from './chrome-storage';
3 |
4 |
5 | export async function getAuth(): Promise {
6 | const {userAuth} = await sync.get<{ userAuth: shikimori.Oauth }>('userAuth');
7 | return userAuth;
8 | }
9 |
10 |
11 | export async function updateAuth() {
12 | const oldAuth = await getAuth();
13 |
14 | if (!oldAuth || !oldAuth.refresh_token) {
15 | const code = await getNewCode();
16 | const newAuth = await ShikimoriProvider.fetch('/oauth/token', {
17 | errorMessage: 'Невозможно получить access_token',
18 | method: 'POST',
19 | body: JSON.stringify({
20 | code,
21 | grant_type: 'authorization_code',
22 | client_id: process.env.VUE_APP_SHIKIMORI_CLIENT_ID,
23 | client_secret: process.env.VUE_APP_SHIKIMORI_CLIENT_SECRET,
24 | redirect_uri: process.env.VUE_APP_SHIKIMORI_REDIRECT_URI,
25 | }),
26 | });
27 |
28 | if (newAuth.access_token && newAuth.refresh_token) {
29 | await sync.set({userAuth: newAuth});
30 | return newAuth;
31 | } else {
32 | return Promise.reject(newAuth);
33 | }
34 | } else {
35 | const newAuth = await ShikimoriProvider.fetch('/oauth/token', {
36 | errorMessage: 'Невозможно обновить access_token',
37 | method: 'POST',
38 | body: JSON.stringify({
39 | grant_type: 'refresh_token',
40 | client_id: process.env.VUE_APP_SHIKIMORI_CLIENT_ID,
41 | client_secret: process.env.VUE_APP_SHIKIMORI_CLIENT_SECRET,
42 | refresh_token: oldAuth.refresh_token,
43 | }),
44 | });
45 |
46 | if (newAuth.access_token && newAuth.refresh_token) {
47 | await sync.set({userAuth: newAuth});
48 | return newAuth;
49 | } else {
50 | return Promise.reject(newAuth);
51 | }
52 | }
53 | }
54 |
55 |
56 | export function getNewCode() {
57 | return new Promise((resolve, reject) => {
58 | const url = new URL('https://shikimori.one/oauth/authorize');
59 | url.searchParams.set('client_id', process.env.VUE_APP_SHIKIMORI_CLIENT_ID);
60 | url.searchParams.set('redirect_uri', process.env.VUE_APP_SHIKIMORI_REDIRECT_URI);
61 | url.searchParams.set('response_type', 'code');
62 | chrome.tabs.query({active: true}, ([selectedTab]) =>
63 | chrome.tabs.create({active: true, url: url.toString()}, (createdTab: chrome.tabs.Tab) => {
64 |
65 | const onRemoved = (tabId: number) => {
66 | if (tabId === createdTab.id) {
67 | reject({error: 'tab-removed'});
68 | clear();
69 | }
70 | };
71 |
72 | const onUpdated = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => {
73 | if (tabId !== createdTab.id || !changeInfo.url) {
74 | return;
75 | }
76 |
77 | const tabUrl = new URL(changeInfo.url);
78 | if (tabUrl.hostname !== 'shikimori.one'
79 | || tabUrl.pathname !== '/tests/oauth'
80 | || tabUrl.searchParams.get('app') !== 'play-shikimori-online') {
81 | return;
82 | }
83 |
84 | const error = tabUrl.searchParams.get('error');
85 | const errorDescription = tabUrl.searchParams.get('error_description');
86 |
87 | if (error || errorDescription) {
88 | reject({error, error_description: errorDescription});
89 | } else {
90 | const code = tabUrl.searchParams.get('code');
91 | resolve(code);
92 | }
93 |
94 | clear();
95 | if (selectedTab && selectedTab.id) {
96 | chrome.tabs.update(selectedTab.id, {active: true}, () => {
97 | if (createdTab && createdTab.id) {
98 | chrome.tabs.remove(createdTab.id);
99 | }
100 | });
101 | }
102 |
103 | };
104 |
105 | const clear = () => {
106 | chrome.tabs.onRemoved.removeListener(onRemoved);
107 | chrome.tabs.onUpdated.removeListener(onUpdated);
108 | };
109 |
110 | chrome.tabs.onRemoved.addListener(onRemoved);
111 | chrome.tabs.onUpdated.addListener(onUpdated);
112 | }));
113 |
114 | });
115 | }
116 |
117 |
--------------------------------------------------------------------------------
/src/helpers/pluralize.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Выбирает нужную форму существительного в зависимости от количества
3 | * @param {number} n
4 | * @param {[string, string, string]} titles
5 | * @returns {string}
6 | *
7 | * @see https://gist.github.com/realmyst/1262561#gistcomment-2299442
8 | * @see http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html?id=l10n/pluralforms
9 | *
10 | */
11 | export function pluralize(n: number, titles: [string, string, string]) {
12 | return titles[(
13 | n % 10 === 1 && n % 100 !== 11
14 | ) ? 0 : n % 10 >= 2 && n % 10 <= 4 && (
15 | n % 100 < 10 || n % 100 >= 20
16 | ) ? 1 : 2];
17 | }
18 |
--------------------------------------------------------------------------------
/src/helpers/runtime-messages.ts:
--------------------------------------------------------------------------------
1 | import {local} from './chrome-storage';
2 |
3 |
4 | export async function push(message: RuntimeMessage) {
5 | if (!message || !message.html) {
6 | return;
7 | }
8 |
9 | const defaults = {
10 | important: false,
11 | color: 'info',
12 | };
13 |
14 | message = Object.assign({}, defaults, message);
15 | let {runtimeMessages} = await local.get<{ runtimeMessages: RuntimeMessage[] }>({runtimeMessages: []});
16 |
17 | // Если у сообщения указан уникальный ID,
18 | // необходимо удалить из очереди сообщений все сообщения с аналогичным ID.
19 | // Это необходимо, чтобы избегать повторяющихся однотипных сообщений
20 | if (message.id) {
21 | runtimeMessages = runtimeMessages.filter((m) => m.id && m.id !== message.id);
22 | }
23 |
24 | runtimeMessages.push(message);
25 | await local.set({runtimeMessages});
26 | }
27 |
28 |
29 | export async function shift() {
30 | const {runtimeMessages} = await local.get<{ runtimeMessages: RuntimeMessage[] }>({runtimeMessages: []});
31 | const message = runtimeMessages.shift();
32 | await local.set({runtimeMessages});
33 | return message;
34 | }
35 |
36 |
37 | export function errorMessage(str: string) {
38 | return push({
39 | color: 'error',
40 | html: str,
41 | mode: 'single',
42 | timeout: 15000,
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/src/helpers/version-compare.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Compares two software version numbers (e.g. "1.7.1" or "1.2b").
3 | *
4 | * This function was born in http://stackoverflow.com/a/6832721.
5 | *
6 | * @see https://github.com/Rombecchi/version-compare
7 | *
8 | * @param {string} v1 The first version to be compared.
9 | * @param {string} v2 The second version to be compared.
10 | * @param {object} [options] Optional flags that affect comparison behavior:
11 | * lexicographical: (true/[false]) compares each part of the version strings lexicographically instead of naturally;
12 | * this allows suffixes such as "b" or "dev" but will cause "1.10" to be considered smaller than "1.2".
13 | * zeroExtend: ([true]/false) changes the result if one version string has less parts than the other. In
14 | * this case the shorter string will be padded with "zero" parts instead of being considered smaller.
15 | *
16 | * @returns {number|NaN}
17 | * - 0 if the versions are equal
18 | * - a negative integer iff v1 < v2
19 | * - a positive integer iff v1 > v2
20 | * - NaN if either version string is in the wrong format
21 | */
22 |
23 | export function versionCompare(
24 | v1: string,
25 | v2: string,
26 | options?: { lexicographical: boolean, zeroExtend: boolean },
27 | ): number {
28 | const lexicographical = (options && options.lexicographical) || false;
29 | const zeroExtend = (options && options.zeroExtend) || true;
30 | let v1parts: string[] | number[] = (v1 || '0').split('.');
31 | let v2parts: string[] | number[] = (v2 || '0').split('.');
32 |
33 | const isValidPart = (x: string) => (lexicographical ? /^\d+[A-Za-zαß]*$/ : /^\d+[A-Za-zαß]?$/).test(x);
34 |
35 | if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
36 | return NaN;
37 | }
38 |
39 | if (zeroExtend) {
40 | while (v1parts.length < v2parts.length) {
41 | v1parts.push('0');
42 | }
43 | while (v2parts.length < v1parts.length) {
44 | v2parts.push('0');
45 | }
46 | }
47 |
48 | if (!lexicographical) {
49 | v1parts = v1parts.map((x) => {
50 | const match = /[A-Za-zαß]/.exec(x);
51 | return Number(match ? x.replace(match[0], '.' + x.charCodeAt(match.index)) : x);
52 | });
53 |
54 | v2parts = v2parts.map((x) => {
55 | const match = (/[A-Za-zαß]/).exec(x);
56 | return Number(match ? x.replace(match[0], '.' + x.charCodeAt(match.index)) : x);
57 | });
58 | }
59 |
60 | for (let i = 0; i < v1parts.length; ++i) {
61 | if (v2parts.length === i) {
62 | return 1;
63 | }
64 |
65 | if (v1parts[i] > v2parts[i]) {
66 | return 1;
67 | } else if (v1parts[i] !== v2parts[i]) {
68 | return -1;
69 | }
70 | }
71 |
72 | if (v1parts.length !== v2parts.length) {
73 | return -1;
74 | }
75 |
76 | return 0;
77 | }
78 |
--------------------------------------------------------------------------------
/src/manifest.js:
--------------------------------------------------------------------------------
1 | const permissions = [
2 | 'webRequest',
3 | 'webRequestBlocking',
4 | 'storage',
5 | 'tabs',
6 | ]
7 |
8 | const origins = [
9 | 'https://shikimori.one/*',
10 | 'https://smotret-anime-365.ru/*',
11 | 'https://smotret-anime.online/*',
12 | 'https://api.jikan.moe/*',
13 | ]
14 |
15 | const manifest = {
16 | manifest_version: 2,
17 |
18 | name: '__MSG_extName__',
19 |
20 | default_locale: 'ru',
21 |
22 | homepage_url: 'https://t.me/playshikionline',
23 |
24 | icons: {
25 | '192': 'play.png',
26 | '128': 'play-128.png',
27 | },
28 |
29 | minimum_chrome_version: '73',
30 |
31 | incognito: 'split',
32 |
33 | browser_action: {
34 | default_title: 'Открыть историю просмотров',
35 | },
36 |
37 | background: {
38 | page: 'background.html',
39 | persistent: true,
40 | },
41 |
42 | 'options_ui': {
43 | 'page': 'player.html#/options',
44 | 'open_in_tab': false,
45 | },
46 |
47 | web_accessible_resources: [
48 | '*',
49 | 'anime365-player-events.js',
50 | ],
51 |
52 | permissions: [
53 | ...permissions,
54 | ...origins,
55 | ],
56 |
57 | content_scripts: [
58 | {
59 | matches: [
60 | 'https://shikimori.org/*',
61 | 'https://shikimori.one/*',
62 | ],
63 | js: [
64 | 'shikimori-watch-button.js',
65 | ],
66 | run_at: 'document_idle',
67 | },
68 | {
69 | matches: [
70 | 'https://smotret-anime.online/translations/embed/*',
71 | 'https://smotret-anime-365.ru/translations/embed/*',
72 | 'https://hentai365.ru/translations/embed/*',
73 | ],
74 | js: [
75 | 'anime-365-inject.js',
76 | ],
77 | css: [
78 | 'css/anime-365-player.css',
79 | ],
80 | run_at: 'document_start',
81 | all_frames: true,
82 | },
83 | // {
84 | // matches: [
85 | // 'https://myanimelist.net/anime/*',
86 | // ],
87 | // js: [
88 | // 'watch-button-myanime-list.js',
89 | // ],
90 | // run_at: 'document_end',
91 | // },
92 | ],
93 | }
94 |
95 | if (process.env.BROWSER === 'firefox') {
96 |
97 | manifest.browser_specific_settings = {
98 | gecko: {
99 | id: process.env.FIREFOX_EXTENSION_ID,
100 | strict_min_version: '67.0',
101 | },
102 | }
103 |
104 | manifest.incognito = 'spanning'
105 |
106 | }
107 |
108 | module.exports = {
109 | default: manifest,
110 | permissions,
111 | origins,
112 | }
113 |
--------------------------------------------------------------------------------
/tests/e2e/plugins/index.js:
--------------------------------------------------------------------------------
1 | // https://docs.cypress.io/guides/guides/plugins-guide.html
2 |
3 | // if you need a custom webpack configuration you can uncomment the following import
4 | // and then use the `file:preprocessor` event
5 | // as explained in the cypress docs
6 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples
7 |
8 | /* eslint-disable import/no-extraneous-dependencies, global-require, arrow-body-style */
9 | // const webpack = require('@cypress/webpack-preprocessor')
10 |
11 | module.exports = (on, config) => {
12 | // on('file:preprocessor', webpack({
13 | // webpackOptions: require('@vue/cli-service/webpack.config'),
14 | // watchOptions: {}
15 | // }))
16 |
17 | return Object.assign({}, config, {
18 | fixturesFolder: 'tests/e2e/fixtures',
19 | integrationFolder: 'tests/e2e/specs',
20 | screenshotsFolder: 'tests/e2e/screenshots',
21 | videosFolder: 'tests/e2e/videos',
22 | supportFile: 'tests/e2e/support/index.js'
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/tests/e2e/specs/test.js:
--------------------------------------------------------------------------------
1 | // https://docs.cypress.io/api/introduction/api.html
2 |
3 | describe('My First Test', () => {
4 | it('Visits the app root url', () => {
5 | cy.visit('/')
6 | cy.contains('h1', 'Welcome to Your Vue.js + TypeScript App')
7 | })
8 | })
9 |
--------------------------------------------------------------------------------
/tests/e2e/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add("login", (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This is will overwrite an existing command --
25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/tests/e2e/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
--------------------------------------------------------------------------------
/tests/unit/clearString.spec.js:
--------------------------------------------------------------------------------
1 | import {clearString} from '../../src/helpers/clear-string.js'
2 |
3 |
4 | describe('Clear the string function', () => {
5 |
6 | it('Empty string', () => {
7 | expect(clearString('')).toEqual('')
8 | })
9 |
10 |
11 | it('AniDUB', () => {
12 | expect(clearString('AniDUB')).toEqual('anidub')
13 | })
14 |
15 | it('AniDUB (Lelik_time & Lonely Dragon)', () => {
16 | expect(clearString('AniDUB (Lelik_time & Lonely Dragon)')).toEqual('anidubleliktimelonelydragon')
17 | })
18 | })
--------------------------------------------------------------------------------
/tests/unit/example.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { shallowMount } from '@vue/test-utils';
3 | import HelloWorld from '@/components/HelloWorld.vue';
4 |
5 | describe('HelloWorld.vue', () => {
6 | it('renders props.msg when passed', () => {
7 | const msg = 'new message';
8 | const wrapper = shallowMount(HelloWorld, {
9 | propsData: { msg },
10 | });
11 | expect(wrapper.text()).to.include(msg);
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/tests/unit/priorityTranslation/getPriorityTranslation.spec.js:
--------------------------------------------------------------------------------
1 | import {
2 | filterTranslationsByAuthor,
3 | getAuthorsPriority,
4 | getPriorityTranslationForEpisode,
5 | getTypesPriority,
6 | } from '../../../src/helpers/get-translation-priority.js'
7 |
8 | import episodes from './sampleEpisodes.js'
9 | import lastSelectedTranslations from './sampleHistory.js'
10 |
11 |
12 | describe('Calculation of authors priority translation', () => {
13 |
14 | it('should return correct authors priority map', () => {
15 | const authors = getAuthorsPriority(lastSelectedTranslations)
16 | expect(authors).toEqual(new Map([
17 | ['anidub', 0.6],
18 | ['animevost', 0.2],
19 | ['newcomers', 0.2],
20 | ]))
21 | })
22 |
23 | })
24 |
25 | describe('Calculation of types priority translation', () => {
26 | it('should return correct types priority map', () => {
27 | const types = getTypesPriority(lastSelectedTranslations)
28 | expect(types).toEqual(new Map([
29 | ['voiceRu', 1],
30 | ]))
31 | })
32 | })
33 |
34 | describe('Filter translations by author summary', () => {
35 | it('should return correct translations for AniDUB', () => {
36 | const filtered = filterTranslationsByAuthor(episodes.Alchemist[0].translations, 'AniDUB')
37 | expect(filtered.map(t => t.id)).toEqual([398664, 1794388])
38 | })
39 |
40 | it('should return correct translations for Razmes', () => {
41 | const filtered = filterTranslationsByAuthor(episodes.Gintama[0].translations, 'Razmes')
42 | expect(filtered.map(t => t.id)).toEqual([228101, 255141])
43 | })
44 |
45 |
46 | it('should return empry array if author summary is empty', () => {
47 | expect(filterTranslationsByAuthor(episodes.Alchemist[0].translations, '').map(t => t.id)).toEqual([])
48 | })
49 | it('should return empry array if author not exist', () => {
50 | expect(filterTranslationsByAuthor(episodes.Alchemist[0].translations, 'Not existing author').map(t => t.id)).toEqual([])
51 | })
52 |
53 | })
54 |
55 | describe('Calculation priority translation for episode', () => {
56 |
57 | it('should return a translation of the same author as the one saved in history', () => {
58 | const translations = getPriorityTranslationForEpisode(lastSelectedTranslations, episodes.Alchemist[1])
59 | expect(translations.map(t => t.id)).toEqual([398668, 677084])
60 | })
61 |
62 | it('should return the translation of the most popular author', () => {
63 | const translations = getPriorityTranslationForEpisode(lastSelectedTranslations, episodes.Gintama[0])
64 | expect(translations.map(t => t.id)).toEqual([2240730])
65 | })
66 |
67 |
68 | it('should return the translation of the most popular type', () => {
69 | const translations = getPriorityTranslationForEpisode(lastSelectedTranslations, episodes.OnePiece[0])
70 | const expected = episodes.OnePiece[0].translations.filter(t => t.type === 'voiceRu')
71 | expect(translations).toEqual(expected)
72 | })
73 |
74 |
75 |
76 | it('should return all translations', () => {
77 |
78 | // Создаём копию епизода но удаляем все переводы типа voiceRu
79 | const episodeCopy = JSON.parse(JSON.stringify(episodes.OnePiece[1]))
80 | episodeCopy.translations = episodeCopy.translations.filter(t => t.type !== 'voiceRu')
81 |
82 | const translations = getPriorityTranslationForEpisode(lastSelectedTranslations, episodeCopy)
83 | expect(translations).toEqual(episodeCopy.translations)
84 | })
85 | })
--------------------------------------------------------------------------------
/tests/unit/priorityTranslation/sampleHistory.js:
--------------------------------------------------------------------------------
1 | export default new Map([
2 | [
3 | 19302,
4 | {
5 | 'id': 2330245,
6 | 'addedDateTime': '2019-03-16 23:59:45',
7 | 'activeDateTime': '2019-07-11 10:26:43',
8 | 'authorsList': ['AniDUB (Lelik_time', 'Lonely Dragon)'],
9 | 'fansubsTranslationId': 0,
10 | 'isActive': 1,
11 | 'priority': 1746,
12 | 'qualityType': 'tv',
13 | 'type': 'voiceRu',
14 | 'typeKind': 'voice',
15 | 'typeLang': 'ru',
16 | 'updatedDateTime': '2019-07-11 10:26:43',
17 | 'title': '22 серия Невероятное приключение ДжоДжо: Золотой ветер / JoJo no Kimyou na Bouken Part 5: Ougon no Kaze / JJBA 2018 озвучка от AniDUB (Lelik_time & Lonely Dragon)',
18 | 'seriesId': 19302,
19 | 'episodeId': 191559,
20 | 'countViews': 399,
21 | 'url': 'https://smotret-anime-365.ru/catalog/jojo-no-kimyou-na-bouken-part-5-ougon-no-kaze-19302/22-seriya-191559/ozvuchka-2330245',
22 | 'embedUrl': 'https://smotret-anime-365.ru/translations/embed/2330245',
23 | 'authorsSummary': 'AniDUB (Lelik_time & Lonely Dragon)',
24 | 'duration': '1421.42',
25 | 'width': 1920,
26 | 'height': 1080,
27 | },
28 | ],
29 |
30 | [
31 | 16677,
32 | {
33 | 'id': 1991314,
34 | 'addedDateTime': '2018-10-21 09:40:21',
35 | 'activeDateTime': '2019-07-13 15:15:58',
36 | 'authorsList': ['NewComers'],
37 | 'fansubsTranslationId': 0,
38 | 'isActive': 1,
39 | 'priority': 1786,
40 | 'qualityType': 'tv',
41 | 'type': 'voiceRu',
42 | 'typeKind': 'voice',
43 | 'typeLang': 'ru',
44 | 'updatedDateTime': '2019-07-13 15:15:58',
45 | 'title': '12 серия Вторжение гигантов 3 сезон / Shingeki no Kyojin Season 3 / SnK3 озвучка от NewComers',
46 | 'seriesId': 16677,
47 | 'episodeId': 182155,
48 | 'countViews': 348,
49 | 'url': 'https://smotret-anime-365.ru/catalog/shingeki-no-kyojin-season-3-16677/12-seriya-182155/ozvuchka-1991314',
50 | 'embedUrl': 'https://smotret-anime-365.ru/translations/embed/1991314',
51 | 'authorsSummary': 'NewComers',
52 | 'duration': '1425.26',
53 | 'width': 1920,
54 | 'height': 1080,
55 | },
56 | ],
57 | [
58 | 1622,
59 | {
60 | 'id': 2240731,
61 | 'addedDateTime': '2019-01-30 00:58:17',
62 | 'activeDateTime': '2019-06-30 16:24:36',
63 | 'authorsList': ['AnimeVost'],
64 | 'fansubsTranslationId': 0,
65 | 'isActive': 1,
66 | 'priority': 1664,
67 | 'qualityType': 'tv',
68 | 'type': 'voiceRu',
69 | 'typeKind': 'voice',
70 | 'typeLang': 'ru',
71 | 'updatedDateTime': '2019-06-30 16:24:36',
72 | 'title': '2 серия Гинтама / gintama tv озвучка от AnimeVost',
73 | 'seriesId': 1622,
74 | 'episodeId': 97305,
75 | 'countViews': 26,
76 | 'url': 'https://smotret-anime-365.ru/catalog/gintama-tv-8868/2-seriya-95989/ozvuchka-2240731',
77 | 'embedUrl': 'https://smotret-anime-365.ru/translations/embed/2240731',
78 | 'authorsSummary': 'AnimeVost',
79 | 'duration': '1450.1',
80 | 'width': 1280,
81 | 'height': 720,
82 | },
83 | ],
84 | [
85 | 19608,
86 | {
87 | 'id': 2877961,
88 | 'addedDateTime': '2019-07-04 18:37:03',
89 | 'activeDateTime': '2019-07-27 15:03:50',
90 | 'authorsList': ['AniDUB (JAM', 'Trina_D)'],
91 | 'fansubsTranslationId': 0,
92 | 'isActive': 1,
93 | 'priority': 1796,
94 | 'qualityType': 'tv',
95 | 'type': 'voiceRu',
96 | 'typeKind': 'voice',
97 | 'typeLang': 'ru',
98 | 'updatedDateTime': '2019-07-27 15:03:50',
99 | 'title': '10 серия Вторжение гигантов 3. Часть 2 сезон / Shingeki no Kyojin Season 3 Part 2 / SnK3.5 озвучка от AniDUB (JAM & Trina_D)',
100 | 'seriesId': 19608,
101 | 'episodeId': 225454,
102 | 'countViews': 1262,
103 | 'url': 'https://smotret-anime-365.ru/catalog/shingeki-no-kyojin-season-3-part-2-19608/10-seriya-225454/ozvuchka-2877961',
104 | 'embedUrl': 'https://smotret-anime-365.ru/translations/embed/2877961',
105 | 'authorsSummary': 'AniDUB (JAM & Trina_D)',
106 | 'duration': '1420.13',
107 | 'width': 1920,
108 | 'height': 1080,
109 | },
110 | ],
111 | [
112 | 2400,
113 | {
114 | 'id': 398664,
115 | 'addedDateTime': '2015-05-03 14:04:59',
116 | 'activeDateTime': '2019-02-16 01:07:25',
117 | 'authorsList': ['AniDUB (Ancord', 'n_o_i_r)'],
118 | 'fansubsTranslationId': 0,
119 | 'isActive': 1,
120 | 'priority': 1846,
121 | 'qualityType': 'bd',
122 | 'type': 'voiceRu',
123 | 'typeKind': 'voice',
124 | 'typeLang': 'ru',
125 | 'updatedDateTime': '2019-02-16 01:07:25',
126 | 'title': '1 серия Стальной алхимик: Братство / Hagane no Renkinjutsushi (2009) / FMA (2009) озвучка от AniDUB (Ancord & n_o_i_r) (BD)',
127 | 'seriesId': 2400,
128 | 'episodeId': 56738,
129 | 'countViews': 15446,
130 | 'url': 'https://smotret-anime-365.ru/catalog/hagane-no-renkinjutsushi-2009-2400/1-seriya-56738/ozvuchka-398664',
131 | 'embedUrl': 'https://smotret-anime-365.ru/translations/embed/398664',
132 | 'authorsSummary': 'AniDUB (Ancord & n_o_i_r)',
133 | 'duration': '1469.64',
134 | 'width': 1920,
135 | 'height': 1080,
136 | },
137 | ],
138 | ])
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "experimentalDecorators": true,
10 | "emitDecoratorMetadata": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "sourceMap": true,
14 | "baseUrl": ".",
15 | "types": [
16 | "webpack-env",
17 | "mocha",
18 | "chai",
19 | "vuetify",
20 | "chrome"
21 | ],
22 | "paths": {
23 | "@/*": [
24 | "src/*"
25 | ]
26 | },
27 | "lib": [
28 | "esnext",
29 | "dom",
30 | "dom.iterable",
31 | "scripthost"
32 | ]
33 | },
34 | "include": [
35 | "src/**/*.ts",
36 | "src/**/*.tsx",
37 | "src/**/*.vue",
38 | "tests/**/*.ts",
39 | "tests/**/*.tsx",
40 | "types/**/*.d.ts"
41 | ],
42 | "exclude": [
43 | "node_modules"
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "warning",
3 | "extends": [
4 | "tslint:recommended"
5 | ],
6 | "linterOptions": {
7 | "exclude": [
8 | "node_modules/**"
9 | ]
10 | },
11 | "rules": {
12 | "indent": [
13 | true,
14 | "spaces",
15 | 2
16 | ],
17 | "interface-name": false,
18 | "no-consecutive-blank-lines": false,
19 | "object-literal-sort-keys": false,
20 | "no-console": [
21 | true,
22 | "log",
23 | "table"
24 | ],
25 | "ordered-imports": false,
26 | "quotemark": [
27 | true,
28 | "single"
29 | ],
30 | "no-empty": [
31 | true,
32 | "allow-empty-catch"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/types/AppErrorSchema.d.ts:
--------------------------------------------------------------------------------
1 | interface AppErrorSchema {
2 | name?: string;
3 | message?: string;
4 | reason?: string;
5 | response?: {
6 | status: number;
7 | body: {
8 | [key: string]: any,
9 | };
10 | };
11 | request?: {
12 | url: string;
13 | options: {
14 | [key: string]: any,
15 | }
16 | };
17 | }
18 |
19 | interface Error {
20 | alert: () => Error;
21 | track: () => Error;
22 | toJSON: () => AppErrorSchema;
23 | }
24 |
25 | interface DOMException {
26 | alert: () => Error;
27 | track: () => Error;
28 | toJSON: () => AppErrorSchema;
29 | stack?: string;
30 | }
31 |
--------------------------------------------------------------------------------
/types/RuntimeMessage.d.ts:
--------------------------------------------------------------------------------
1 | declare interface RuntimeMessage {
2 | id?: any;
3 | x?: 'left' | 'right';
4 | y?: 'top' | 'bottom';
5 | color?: string;
6 | mode?: 'multi-line' | 'vertical' | 'single';
7 | timeout?: number;
8 | html: string;
9 | }
10 |
--------------------------------------------------------------------------------
/types/UI.ts:
--------------------------------------------------------------------------------
1 | export interface WatchingHistoryItem {
2 | id: shikimori.Anime['id'];
3 | episodes: shikimori.UserRate['episodes'];
4 | name: shikimori.Anime['russian'] | shikimori.Anime['name'];
5 | image: shikimori.Anime['image']['original'];
6 | }
7 |
8 |
9 | export interface SelectedTranslation {
10 | id: anime365.Translation['seriesId'];
11 | tId: anime365.Translation['id'];
12 | eId: anime365.Translation['episodeId'];
13 | type: anime365.Translation['type'];
14 | priority: anime365.Translation['priority'];
15 | author: anime365.Translation['authorsSummary'];
16 | }
17 |
--------------------------------------------------------------------------------
/types/Worker.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'worker-loader!*' {
2 | class WebpackWorker extends Worker {
3 | constructor();
4 | }
5 |
6 | export = WebpackWorker;
7 | }
8 |
--------------------------------------------------------------------------------
/types/anime365.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace anime365 {
2 | interface Link {
3 | title: string;
4 | url: string;
5 | }
6 |
7 | interface Titles {
8 | ru?: string;
9 | en?: string;
10 | romaji?: string;
11 | ja?: string;
12 | short?: string;
13 | }
14 |
15 | interface Description {
16 | source: string;
17 | value: string;
18 | updatedDateTime: string;
19 | }
20 |
21 | interface Episode {
22 | id: number;
23 | episodeFull: string;
24 | episodeInt: number;
25 | episodeTitle: string;
26 | episodeType: string;
27 | firstUploadedDateTime: string;
28 | isActive: boolean;
29 | isFirstUploaded: boolean;
30 | seriesId: number;
31 | countViews: number;
32 | translations?: Translation[];
33 | preselectedTranslation?: Translation;
34 | myAnimelist?: number;
35 | next?: Episode;
36 | previous?: Episode;
37 | }
38 |
39 | interface Translation {
40 | id: number;
41 | addedDateTime: string;
42 | activeDateTime: string;
43 | authorsList: string[];
44 | fansubsTranslationId: number;
45 | isActive: number;
46 | priority: number;
47 | qualityType: string;
48 | type: string;
49 | typeKind: string;
50 | typeLang: string;
51 | updatedDateTime: string;
52 | title: string;
53 | seriesId: number;
54 | episodeId: number;
55 | countViews: number;
56 | url: string;
57 | embedUrl: string;
58 | authorsSummary: string;
59 | duration: string;
60 | width: number;
61 | height: number;
62 | }
63 |
64 |
65 | interface Genre {
66 | id: number;
67 | title: string;
68 | url: string;
69 | }
70 |
71 | interface Series {
72 | id: number;
73 | aniDbId: number;
74 | animeNewsNetworkId: number;
75 | fansubsId: number;
76 | imdbId: number;
77 | worldArtId: number;
78 | isActive: boolean;
79 | isAiring: boolean;
80 | isHentai: boolean;
81 | links: Link[];
82 | myAnimeListId: number;
83 | myAnimeListScore: string;
84 | worldArtScore: string;
85 | worldArtTopPlace: number;
86 | numberOfEpisodes: number;
87 | season: string;
88 | year: number;
89 | type: string;
90 | typeTitle: string;
91 | countViews: number;
92 | titles: Titles;
93 | posterUrl: string;
94 | posterUrlSmall: string;
95 | titleLines: string[];
96 | allTitles: string[];
97 | title: string;
98 | url: string;
99 | descriptions: Description[];
100 | episodes: Episode[];
101 | genres: Genre[];
102 | }
103 |
104 | interface DownloadItem {
105 | height: number;
106 | url: string;
107 | }
108 |
109 |
110 | interface StreamItem {
111 | height: number;
112 | urls: string[];
113 | }
114 |
115 | namespace api {
116 | interface SeriesCollection {
117 | data: Series[];
118 | }
119 |
120 | interface SeriesSelf {
121 | data: Series;
122 | }
123 |
124 | interface EpisodeSelf {
125 | data: Episode;
126 | }
127 |
128 | interface TranslationEmbed {
129 | data?: {
130 | embedUrl: string;
131 | download: DownloadItem[];
132 | stream: StreamItem[];
133 | };
134 | error?: {
135 | code: number,
136 | message: string;
137 | };
138 | }
139 | }
140 | }
141 |
142 |
143 | // namespace vuex {
144 | // interface Context {
145 | // state: State;
146 | // commit: Funtion;
147 | // dispatch: Function;
148 | // getters: Object;
149 | // }
150 | //
151 | // interface State {
152 | // player: Player;
153 | // shikimori: Shikimori;
154 | // }
155 | //
156 | // interface Player {
157 | // episodes?: anime365.Episode[];
158 | // currentEpisode?: anime365.Episode;
159 | // currentTranslation?: anime365.Translation;
160 | // }
161 | //
162 | // interface Shikimori {
163 | // anime?: shikimori.Anime;
164 | // franchise?: shikimori.FranchiseNode;
165 | // user?: shikimori.User;
166 | // domain: string;
167 | // }
168 | // }
169 |
--------------------------------------------------------------------------------
/types/myanimelist.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace myanimelist {
2 |
3 | interface VideoUrl {
4 | }
5 |
6 | interface Episode {
7 | episode_id: number;
8 | title: string;
9 | title_japanese: string;
10 | title_romanji: string;
11 | aired: Date;
12 | filler: boolean;
13 | recap: boolean;
14 | video_url: VideoUrl;
15 | forum_url: string;
16 | }
17 |
18 | namespace api {
19 | interface EpisodeCollection {
20 | request_hash: string;
21 | request_cached: boolean;
22 | request_cache_expiry: number;
23 | episodes_last_page: number;
24 | episodes: Episode[];
25 | }
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/types/shikimori.d.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable-next-line:no-namespace
2 | declare namespace shikimori {
3 |
4 | interface Image {
5 | original: string;
6 | preview: string;
7 | x96: string;
8 | x48: string;
9 | }
10 |
11 | interface RatesScoresStat {
12 | name: number;
13 | value: number;
14 | }
15 |
16 | interface RatesStatusesStat {
17 | name: string;
18 | value: number;
19 | }
20 |
21 | interface Genre {
22 | id: number;
23 | name: string;
24 | russian: string;
25 | kind: string;
26 | }
27 |
28 | interface Studio {
29 | id: number;
30 | name: string;
31 | filtered_name: string;
32 | real: boolean;
33 | image?: any;
34 | }
35 |
36 | interface Video {
37 | id: number;
38 | url: string;
39 | image_url: string;
40 | player_url: string;
41 | name: string;
42 | kind: string;
43 | hosting: string;
44 | }
45 |
46 | interface Screenshot {
47 | original: string;
48 | preview: string;
49 | }
50 |
51 | interface UserRate {
52 | id?: number;
53 | score?: number;
54 | status?: string;
55 | text?: any;
56 | episodes?: number;
57 | chapters?: any;
58 | volumes?: any;
59 | text_html?: any;
60 | rewatches?: number;
61 | }
62 |
63 | interface Anime {
64 | id: number;
65 | name: string;
66 | russian: string;
67 | image: Image;
68 | url: string;
69 | kind: string;
70 | status: string;
71 | episodes: number;
72 | episodes_aired: number;
73 | aired_on: string;
74 | released_on: string;
75 | rating: string;
76 | english: string[];
77 | japanese: string[];
78 | synonyms: string[];
79 | license_name_ru?: any;
80 | duration: number;
81 | score: string;
82 | description: string;
83 | description_html: string;
84 | description_source?: any;
85 | franchise: string;
86 | favoured: boolean;
87 | anons: boolean;
88 | ongoing: boolean;
89 | thread_id: number;
90 | topic_id: number;
91 | myanimelist_id: number;
92 | rates_scores_stats: RatesScoresStat[];
93 | rates_statuses_stats: RatesStatusesStat[];
94 | updated_at: Date;
95 | next_episode_at: Date;
96 | genres: Genre[];
97 | studios: Studio[];
98 | videos: Video[];
99 | screenshots: Screenshot[];
100 | user_rate?: UserRate;
101 | }
102 |
103 | interface Avatar {
104 | x160?: string;
105 | x148?: string;
106 | x80?: string;
107 | x64?: string;
108 | x48?: string;
109 | x32?: string;
110 | x16?: string;
111 | }
112 |
113 | interface User {
114 | id: number;
115 | nickname: string;
116 | avatar?: string;
117 | image?: Avatar;
118 | last_online_at: Date;
119 | name?: string;
120 | sex?: string;
121 | website?: string;
122 | birth_on?: any;
123 | locale: string;
124 | }
125 |
126 | interface FranchiseLink {
127 | id: number;
128 | source_id: number;
129 | target_id: number;
130 | source: number;
131 | target: number;
132 | weight: number;
133 | relation: string;
134 | }
135 |
136 | interface FranchiseNode {
137 | id: number;
138 | date: number;
139 | name: string;
140 | image_url: string;
141 | url: string;
142 | year: number;
143 | kind: string;
144 | weight: number;
145 | series?: number;
146 | episodeInt?: number;
147 | }
148 |
149 | interface Oauth {
150 | access_token?: string;
151 | refresh_token: string;
152 | token_type: 'Bearer';
153 | created_at: number;
154 | expires_in: number;
155 | }
156 |
157 | interface Comment {
158 | id: number;
159 | user_id: number;
160 | commentable_id: number;
161 | commentable_type: string;
162 | body: string;
163 | html_body: string;
164 | created_at: string;
165 | created_at_relative?: string;
166 | updated_at: Date;
167 | is_offtopic: boolean;
168 | is_summary: boolean;
169 | can_be_edited: boolean;
170 | user: User;
171 | }
172 |
173 | interface Franchise {
174 | links: FranchiseLink[];
175 | nodes: FranchiseNode[];
176 | }
177 |
178 |
179 | export interface Forum {
180 | id: number;
181 | position: number;
182 | name: string;
183 | permalink: string;
184 | url: string;
185 | }
186 |
187 | export interface Linked {
188 | id: number;
189 | name: string;
190 | russian?: any;
191 | image: Image;
192 | url: string;
193 | kind: string;
194 | status: string;
195 | episodes: number;
196 | episodes_aired: number;
197 | aired_on?: any;
198 | released_on?: any;
199 | }
200 |
201 | export interface Topic {
202 | id: number;
203 | topic_title: string;
204 | body?: string;
205 | html_body?: string;
206 | html_footer?: string;
207 | created_at: Date;
208 | comments_count: number;
209 | forum: Forum;
210 | user: User;
211 | type: string;
212 | linked_id: number;
213 | linked_type: string;
214 | linked: Linked;
215 | viewed: boolean;
216 | last_comment_viewed?: any;
217 | event?: string;
218 | episode?: number;
219 | }
220 |
221 | export interface EpisodeNotification {
222 | id: number;
223 | anime_id: number;
224 | episode: number;
225 | is_raw: boolean;
226 | is_subtitles: boolean;
227 | is_fandub: boolean;
228 | is_anime365: boolean;
229 | topic_id: number;
230 | }
231 |
232 | }
233 |
--------------------------------------------------------------------------------
/types/shims-tsx.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, {VNode} from 'vue';
2 |
3 | declare global {
4 | namespace JSX {
5 | // tslint:disable no-empty-interface
6 | interface Element extends VNode {
7 | }
8 |
9 | // tslint:disable no-empty-interface
10 | interface ElementClass extends Vue {
11 | }
12 |
13 | interface IntrinsicElements {
14 | [elem: string]: any;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/types/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue';
3 | export default Vue;
4 | }
5 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const WebpackExtensionManifestPlugin = require('webpack-extension-manifest-plugin')
2 | const CopyPlugin = require('copy-webpack-plugin')
3 |
4 | const baseManifest = require('./src/manifest.js').default
5 | const pkg = require('./package.json')
6 | process.env.BROWSER = process.env.BROWSER || 'firefox'
7 |
8 |
9 | module.exports = {
10 |
11 | chainWebpack: config => {
12 | config.module
13 | .rule('vue')
14 | .use('vue-loader')
15 | .loader('vue-loader')
16 | .tap(options => {
17 | options.hotReload = false
18 | options.fix = true
19 | return options
20 | })
21 |
22 | // config.module
23 | // .rule('worker')
24 | // .test(/worker\.js$/)
25 | // .use('worker-loader')
26 | // .loader('worker-loader')
27 | // .end()
28 |
29 | config.optimization.delete('splitChunks')
30 | },
31 |
32 |
33 | filenameHashing: false,
34 |
35 | pages: {
36 | player: {
37 | entry: './src/UI/main.ts',
38 | template: './src/UI/index.html',
39 | },
40 |
41 | background: {
42 | entry: './src/background/background.ts',
43 | template: './src/background/index.html',
44 | },
45 | },
46 |
47 | lintOnSave: 'error',
48 |
49 | outputDir: `./dist/${process.env.BROWSER}`,
50 |
51 | configureWebpack: {
52 | devtool: process.env.NODE_ENV === 'production' ? 'source-map' : 'inline-source-map',
53 |
54 | performance: {
55 | maxEntrypointSize: 2048000,
56 | maxAssetSize: 2048000,
57 | },
58 |
59 | entry: {
60 | 'shikimori-watch-button': './src/content-scripts/shikimori-watch-button.ts',
61 | // 'watch-button-myanime-list': './src/content-scripts/myanimelist.ts',
62 | 'anime-365-inject': './src/content-scripts/inject-content-scripts.ts',
63 | 'anime-365-player': './src/content-scripts/anime365-player-events.ts',
64 | },
65 |
66 | output: {
67 | filename: '[name].js',
68 | chunkFilename: '[name].js',
69 | },
70 |
71 | plugins: [
72 | new WebpackExtensionManifestPlugin({
73 | config: {
74 | base: baseManifest,
75 | extend: {
76 | description: pkg.description,
77 | version: pkg.version,
78 | author: pkg.author,
79 | },
80 | },
81 | }),
82 | new CopyPlugin([
83 | {from: './src/_locales', to: '_locales'},
84 | ]),
85 | ],
86 | },
87 |
88 |
89 | css: {
90 | extract: true,
91 | },
92 | }
93 |
--------------------------------------------------------------------------------