Compare commits

..

No commits in common. "develop" and "0.4.2.1-beta" have entirely different histories.

33 changed files with 4394 additions and 18655 deletions

View file

@ -1,15 +1,14 @@
name: auto-merge
on:
pull_request_target:
pull_request:
jobs:
auto-merge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- uses: actions/checkout@v2
- uses: ahmadnassri/action-dependabot-auto-merge@v2.3.1
- uses: actions/checkout@v2.3.3
- uses: ahmadnassri/action-dependabot-auto-merge@v2.1.2
with:
target: minor
github-token: ${{ secrets.PR_AUTO_MERGE }}

View file

@ -1,6 +1,6 @@
language: node_js
node_js:
- "16"
- "14"
dist: trusty
cache:
npm: true
@ -9,4 +9,4 @@ cache:
install:
- npm install -d
script:
- npm run dist:prod
- npm run dist:prod

View file

@ -2,6 +2,7 @@
[![Travis (.org)](https://img.shields.io/travis/serraniel/aniwatchplus?style=flat-square)](https://travis-ci.org/github/Serraniel/AniwatchPlus)
[![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/serraniel/aniwatchplus?style=flat-square)](https://snyk.io/test/github/Serraniel/AniwatchPlus?targetFile=package.json)
[![David](https://img.shields.io/david/serraniel/AniwatchPlus?style=flat-square)](https://david-dm.org/serraniel/aniwatchplus)
[![Scrutinizer code quality (GitHub/Bitbucket)](https://img.shields.io/scrutinizer/quality/g/serraniel/aniwatchplus?style=flat-square)](https://scrutinizer-ci.com/g/Serraniel/AniwatchPlus/)
[![GitHub issues](https://img.shields.io/github/issues/serraniel/aniwatchplus?style=flat-square)](https://github.com/Serraniel/AniwatchPlus/issues)
[![GitHub](https://img.shields.io/github/license/serraniel/aniwatchplus?style=flat-square)](https://github.com/Serraniel/AniwatchPlus/blob/develop/LICENSE)
@ -11,7 +12,6 @@
## Features
* Adds quick search
* Time conversion into localtime
* Cleaner list presentation
* Improved presentation of available audio and subtitles
* Better display of anime requests
@ -21,7 +21,7 @@
## Download
Click the badge for your browser to get to the download page:
[![Chrome Web Store](https://img.shields.io/chrome-web-store/v/hgniihpjiioldkafogebpkbaiflmpimb?label=Google%20Chrome&logo=Google%20Chrome&style=flat-square)](https://chrome.google.com/webstore/detail/aniwatch-plus/hgniihpjiioldkafogebpkbaiflmpimb)
[![Chrome Web Store](https://img.shields.io/chrome-web-store/v/hgniihpjiioldkafogebpkbaiflmpimb?label=Google%20Chrome&logo=Google%20Chrome&style=flat-square)](https://chrome.google.com/webstore/detail/aniwatch-plus/hgniihpjiioldkafogebpkbaiflmpimb?hl=de)
[![Mozilla Add-on](https://img.shields.io/amo/v/aniwatch-plus?label=Mozilla%20Firefox&logo=Firefox&style=flat-square)](https://addons.mozilla.org/de/firefox/addon/aniwatch-plus/?utm_source=addons.mozilla.org&utm_medium=referral&utm_content=search)
[![GitHub release tag for opera (latest by date including pre-releases)](https://img.shields.io/github/v/release/serraniel/aniwatchplus?include_prereleases&label=Opera&logo=Opera&logoColor=red&style=flat-square)](https://addons.opera.com/de/extensions/details/aniwatch-plus/)
[![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/serraniel/aniwatchplus?include_prereleases&label=Download%20manually&logo=Github&style=flat-square)](https://github.com/Serraniel/AniwatchPlus/releases)

View file

@ -4,7 +4,6 @@ const gulpLoadPlugins = require('gulp-load-plugins')
const terser = require('terser');
const del = require('del');
const browserify = require('browserify');
const tsify = require('tsify')
const babelify = require('babelify');
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
@ -15,7 +14,7 @@ const { debug } = require('console');
const $ = gulpLoadPlugins()
const sass = require('gulp-sass')(require('sass'));
$.sass.compiler = require('sass');
/* ============================================================================
Base consts
@ -82,11 +81,11 @@ gulp.task('styles', () => {
// sourcemap initialization
.pipe($.if(isDev, $.sourcemaps.init()))
// sass compilation
.pipe(sass.sync({
.pipe($.sass.sync({
outputStyle: 'expanded',
precision: 7,
includePaths: ['.'],
}).on('error', sass.logError))
}).on('error', $.sass.logError))
// autoprefix
.pipe($.autoprefixer())
// out stream size
@ -125,14 +124,13 @@ gulp.task('scripts', () => {
const streams = [];
modules.forEach(module => {
inputs.push(`${src.scripts}/${module}.ts`);
inputs.push(`${src.scripts}/${module}.js`);
streams.push(source(`${module}.js`));
});
const b = browserify(inputs, { debug: isDev });
let outstream = b
.plugin(tsify)
.transform('babelify')
.plugin(factor, { outputs: streams })
.bundle()
@ -208,7 +206,7 @@ Watchers
gulp.task('watch', (done) => {
gulp.watch(`${src.styles}/**/*.scss`, gulp.series('clean:build', 'styles', 'dist:copy', 'dist:zip'))
gulp.watch(`${src.scripts}/**/*.ts`, gulp.series('clean:build', 'scripts', 'dist:copy', 'dist:zip'))
gulp.watch(`${src.scripts}/**/*.js`, gulp.series('clean:build', 'scripts', 'dist:copy', 'dist:zip'))
gulp.watch(`${src.images}/**/*`, gulp.series('clean:build', 'images', 'dist:copy', 'dist:zip'))

21602
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "aniwatch-plus",
"version": "1.1.0",
"version": "0.4.2-beta.1",
"description": "Aniwatch Plus is a browser extension for https://aniwatch.me/",
"main": "index.js",
"scripts": {
@ -33,50 +33,43 @@
},
"homepage": "https://github.com/Serraniel/AniwatchPlus#readme",
"dependencies": {
"color": "^3.2.1",
"spacetime": "https://github.com/Serraniel/spacetime#bugfix/#255-typescript-constructor-options-missing",
"uuid": "^8.3.2"
},
"devDependencies": {
"@babel/compat-data": "^7.22.5",
"@babel/core": "^7.22.9",
"@babel/helper-module-imports": "^7.22.5",
"@babel/plugin-proposal-class-properties": "^7.17.12",
"@babel/plugin-proposal-private-methods": "^7.17.12",
"@babel/preset-env": "^7.22.9",
"@babel/register": "^7.22.5",
"@types/chrome": "0.0.241",
"@babel/compat-data": "^7.12.7",
"@babel/core": "^7.12.10",
"@babel/helper-module-imports": "^7.12.5",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-private-methods": "^7.12.1",
"@babel/preset-env": "^7.12.10",
"@babel/register": "^7.12.10",
"babelify": "^10.0.0",
"browserify": "^17.0.0",
"cross-env": "^7.0.3",
"cssnano": "^5.1.15",
"del": "^6.1.1",
"cssnano": "^4.1.10",
"del": "^6.0.0",
"factor-bundle": "^2.5.0",
"gulp": "^4.0.2",
"gulp-autoprefixer": "^8.0.0",
"gulp-autoprefixer": "^7.0.1",
"gulp-babel": "^8.0.0",
"gulp-if": "^3.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-load-plugins": "^2.0.8",
"gulp-load-plugins": "^2.0.6",
"gulp-merge": "^0.1.1",
"gulp-plumber": "^1.2.1",
"gulp-postcss": "^9.0.1",
"gulp-postcss": "^9.0.0",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.1.4",
"gulp-sass": "^5.1.0",
"gulp-size": "^4.0.1",
"gulp-replace": "^1.0.0",
"gulp-sass": "^4.1.0",
"gulp-size": "^3.0.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-terser": "^2.1.0",
"gulp-typescript": "^6.0.0-alpha.1",
"gulp-zip": "^5.1.0",
"gulp-terser": "^2.0.0",
"gulp-zip": "^5.0.2",
"merge-stream": "^2.0.0",
"postcss": "^8.4.27",
"sass": "^1.64.1",
"terser": "^5.19.2",
"tsify": "^5.0.4",
"typescript": "^4.9.5",
"postcss": "^8.2.1",
"sass": "^1.30.0",
"terser": "^5.5.1",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"web-ext-types": "^3.2.1"
"vinyl-source-stream": "^2.0.0"
}
}

View file

@ -16,9 +16,6 @@
<input type="checkbox" id="websiteDisplayQuickSearch" data-default-value="true" />
<label for="websiteDisplayQuickSearch">Enable Quick Search</label><br />
<input type="checkbox" id="websiteAutoTimeConversion" data-default-value="true" />
<label for="websiteAutoTimeConversion">Convert to local time</label><br />
<input type="checkbox" id="websiteShowNotificationsCountInTab" data-default-value="true" />
<label for="websiteShowNotificationsCountInTab">Show notification counter in browser tab</label><br />
@ -28,9 +25,6 @@
<input type="checkbox" id="websiteOptimizeListAppearance" data-default-value="true" />
<label for="websiteOptimizeListAppearance">Optimize appearance of lists</label><br />
<input type="checkbox" id="websiteOptimizeFontColors" data-default-value="true" />
<label for="websiteOptimizeFontColors">Optimize font colors</label><br />
<h3>Anime</h3>
<input type="checkbox" id="animeLanguageDisplay" data-default-value="true" />
<label for="animeLanguageDisplay">Optimize presentation of available subs and dubs</label><br />
@ -44,20 +38,9 @@
<input type="checkbox" id="playerAutoplayAfterScreenshot" data-default-value="true" />
<label for="playerAutoplayAfterScreenshot">Autoplay after screenshots</label><br />
<input type="checkbox" id="playerAutopauseAfterFocusLost" data-default-value="true" />
<label for="playerAutopauseAfterFocusLost">Autopause after tab unfocused</label><br />
<input type="checkbox" id="playerAutoplayAfterFocusGain" data-default-value="true" />
<label for="playerAutoplayAfterFocusGain">Autoplay after tab focused</label><br />
<h3>Watch2gether</h3>
<input type="checkbox" id="w2gDisplayCharacterCounter" data-default-value="true" />
<label for="w2gDisplayCharacterCounter">Display character count in chat</label><br />
<input type="checkbox" id="w2gAutoscrollToUnseen" data-default-value="true" />
<label for="w2gAutoscrollToUnseen">Autoscroll to the first unseen episode</label><br />
<input type="checkbox" id="w2gAutotoggleHide" data-default-value="true" />
<label for="w2gAutotoggleHide">Autotoggle the Hide-Button for w2g</label><br />
<br /><br />
<button id="btnSave">Save</button>

View file

@ -5,12 +5,10 @@ import { initHelpers } from './utils/helpers';
// enhancements
import { init as anilyr } from './enhancements/anilyr';
import { init as animeRequests } from './enhancements/animeRequests';
import { init as fontColor } from './enhancements/fontColor';
import { init as languageDisplay } from './enhancements/languageDisplay';
import { init as notifications } from './enhancements/notifications';
import { init as quickSearch } from './enhancements/quickSearch';
import { init as timeConversion } from './enhancements/timeConversion';
import { init as watch2gether } from './enhancements/watch2gether';
import { init as watch2getherChat } from './enhancements/watch2getherChat';
// css
import { init as cssEnhancements } from './enhancements/cssEnhancements';
@ -23,12 +21,10 @@ initHelpers();
// enhancements
anilyr();
animeRequests();
fontColor();
languageDisplay();
notifications();
quickSearch();
timeConversion();
watch2gether();
watch2getherChat();
// css
cssEnhancements();

View file

@ -0,0 +1,84 @@
const { assigned } = require("../utils/helpers")
class StorageProviderChromium {
setData(key, value) {
let obj = {};
obj[key] = value;
this.getStorage().set(obj);
}
getData(key, defaultValue, callback) {
this.getStorage().get(key, items => {
if (assigned(items) && assigned(items[key])) {
callback(items[key]);
}
else {
callback(defaultValue);
}
})
}
getStorage() {
if (assigned(chrome.storage.sync)) {
return chrome.storage.sync;
}
return chrome.storage.local;
}
}
class StorageProviderFirefox {
setData(key, value) {
let obj = {};
obj[key] = value;
this.getStorage().set(obj);
}
getData(key, defaultValue, callback) {
let promise = this.getStorage().get(key);
promise.then(items => {
if (assigned(items) && assigned(items[key])) {
callback(items[key]);
}
else {
callback(defaultValue);
}
});
}
getStorage() {
if (assigned(browser.storage.sync)) {
return browser.storage.sync;
}
return browser.storage.local;
}
}
let __storageProvieder;
function createStorageProvider() {
// chrome based browser
if (assigned(chrome?.app)) {
__storageProvieder = new StorageProviderChromium();
}
// firefox
else {
__storageProvieder = new StorageProviderFirefox();
}
}
export function getGlobalStorageProvider() {
if (!assigned(__storageProvieder)) {
createStorageProvider();
}
return __storageProvieder;
}

View file

@ -1,117 +0,0 @@
import { assigned } from '../utils/helpers';
enum BrowserApi {
Unknown,
Chromium,
Firefox,
}
export type ConfigurationStorageBooleanCallback = (value: boolean) => void;
export interface ICustomBrowserStorageProvider {
setDataAsBoolean(key: string, value: boolean): void;
getDataAsBoolean(key: string, defaultValue: boolean, callback: ConfigurationStorageBooleanCallback): void;
}
class StorageProviderChromium implements ICustomBrowserStorageProvider {
setDataAsBoolean(key: string, value: boolean): void {
let obj = {};
obj[key] = value;
this.getStorage().set(obj);
}
getDataAsBoolean(key: string, defaultValue: boolean, callback: ConfigurationStorageBooleanCallback): void {
this.getStorage().get(key, items => {
if (assigned(items) && assigned(items[key])) {
callback(items[key]);
}
else {
callback(defaultValue);
}
})
}
private getStorage(): chrome.storage.StorageArea {
if (assigned(chrome.storage.sync)) {
return chrome.storage.sync;
}
return chrome.storage.local;
}
}
class StorageProviderFirefox implements ICustomBrowserStorageProvider {
setDataAsBoolean(key: string, value: boolean): void {
let obj = {};
obj[key] = value;
this.getStorage().set(obj);
}
getDataAsBoolean(key: string, defaultValue: boolean, callback: ConfigurationStorageBooleanCallback): void {
let promise = this.getStorage().get(key);
promise.then(items => {
if (assigned(items) && assigned(items[key])) {
callback(items[key] as boolean);
}
else {
callback(defaultValue);
}
});
}
private getStorage(): browser.storage.StorageArea {
if (assigned(browser.storage.sync)) {
return browser.storage.sync;
}
return browser.storage.local;
}
}
let __storageProvieder: ICustomBrowserStorageProvider;
function getBrowserApi(): BrowserApi {
if (typeof chrome !== 'undefined') {
if (typeof browser !== 'undefined') {
return BrowserApi.Firefox;
}
return BrowserApi.Chromium;
}
else if (typeof browser !== 'undefined') {
return BrowserApi.Firefox;
}
return BrowserApi.Unknown;
}
function createStorageProvider() {
let api = getBrowserApi();
// chromium
if (api === BrowserApi.Chromium) {
__storageProvieder = new StorageProviderChromium();
}
// firefox
else if (api === BrowserApi.Firefox) {
__storageProvieder = new StorageProviderFirefox();
}
else {
throw "Unknown browser API. Cannot create storage provider.";
}
}
export function getGlobalStorageProvider(): ICustomBrowserStorageProvider {
if (!assigned(__storageProvieder)) {
createStorageProvider();
}
return __storageProvieder;
}

View file

@ -1,39 +1,31 @@
import { ConfigurationStorageBooleanCallback, getGlobalStorageProvider } from "../browserApi/storageProvider";
import { getGlobalStorageProvider } from "../browserApi/storageProvider";
import { assigned } from "../utils/helpers";
// website
export const SETTINGS_websiteDisplayQuickSearch = 'websiteDisplayQuickSearch';
export const SETTINGS_websiteAutoTimeConversion = 'websiteAutoTimeConversion';
export const SETTINGS_websiteShowNotificationsCountInTab = 'websiteShowNotificationsCountInTab';
export const SETTINGS_websiteHideUnusedTabs = 'websiteHideUnusedTabs';
export const SETTINGS_websiteOptimizeListAppearance = 'websiteOptimizeListAppearance';
export const SETTINGS_websiteOptimizeFontColors = 'websiteOptimizeFontColors';
// anime
export const SETTINGS_animeLanguageDisplay = 'animeLanguageDisplay';
// requests
export const SETTINGS_requestBeautifyPage = 'requestBeautifyPage';
// player
export const SETTINGS_playerAutoplayAfterScreenshot = 'playerAutoplayAfterScreenshot';
export const SETTINGS_playerAutopauseAfterFocusLost = 'playerAutopauseAfterFocusLost';
export const SETTINGS_playerAutoplayAfterFocusGain = 'playerAutoplayAfterFocusGain';
// w2g
export const SETTINGS_w2gDisplayCharacterCounter = 'w2gDisplayCharacterCounter';
export const SETTINGS_w2gAutotoggleHide = 'w2gAutotoggleHide';
export const SETTINGS_w2gAutoscrollToUnseen = 'w2gAutoscrollToUnseen';
class Configuration {
settingsCache: Map<string, boolean>;
constructor() {
this.settingsCache = new Map();
}
getProperty(key: string, callback: ConfigurationStorageBooleanCallback): void {
getProperty(key, callback) {
if (this.settingsCache.has(key)) {
callback(this.settingsCache.get(key));
}
else {
getGlobalStorageProvider().getDataAsBoolean(key, true, value => {
// OOOPS // currently all settings are default true. This isn´t a problem but there should be much better soloutions after migration to typescript....
getGlobalStorageProvider().getData(key, true, value => {
this.settingsCache.set(key, value);
callback(value);
});
@ -41,7 +33,7 @@ class Configuration {
}
}
let __globalConfig: Configuration;
let __globalConfig;
export function getGlobalConfiguration() {
if (!assigned(__globalConfig)) {
@ -49,4 +41,4 @@ export function getGlobalConfiguration() {
}
return __globalConfig;
}
}

View file

@ -0,0 +1,53 @@
import { getGlobalConfiguration, SETTINGS_playerAutoplayAfterScreenshot } from '../configuration/configuration';
import * as core from '../utils/aniwatchCore';
import * as helper from '../utils/helpers';
const SCREENSHOT_TOOLTIP_ID = 'anilyr-screenshots-tooltip';
const PLAYER_ID = 'player';
export function init() {
getGlobalConfiguration().getProperty(SETTINGS_playerAutoplayAfterScreenshot, value => {
if (value) {
core.registerScript(node => {
if (helper.isHtmlElement(node) && node.id === SCREENSHOT_TOOLTIP_ID) {
observeScreenshotTooltip(node);
}
}, "^/anime/[0-9]*/[0-9]*$");
}
});
}
function observeScreenshotTooltip(tooltip) {
let observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
// Switched to invisible
if (!mutation.oldValue.includes('display: none') && mutation.target.style.display == 'none') {
let player = findPlayer();
if (helper.assigned(player)) {
resumePlayer(player);
}
}
});
});
observer.observe(tooltip, {
attributes: true,
attributeOldValue: true,
attributeFilter: ['style'],
});
}
function findPlayer() {
const PLAYER_TAG_NAME = 'VIDEO'; // tagName gives UpperCase
let playerCandidate = document.getElementById(PLAYER_ID);
if (playerCandidate.tagName === PLAYER_TAG_NAME) {
return playerCandidate;
}
return undefined;
}
function resumePlayer(player) {
player.play();
}

View file

@ -1,88 +0,0 @@
import { getGlobalConfiguration,
SETTINGS_playerAutoplayAfterScreenshot,
SETTINGS_playerAutopauseAfterFocusLost,
SETTINGS_playerAutoplayAfterFocusGain } from '../configuration/configuration';
import * as core from '../utils/aniwatchCore';
import * as helper from '../utils/helpers';
const SCREENSHOT_TOOLTIP_ID = 'anilyr-screenshots-tooltip';
const PLAYER_ID = 'player';
let resumePlayerOnVisible: boolean;
export function init(): void {
getGlobalConfiguration().getProperty(SETTINGS_playerAutoplayAfterScreenshot, value => {
if (value) {
core.registerScript((node: Node) => {
let element = node as HTMLElement;
if (helper.assigned(element) && element.id === SCREENSHOT_TOOLTIP_ID) {
observeScreenshotTooltip(element);
}
}, "^/anime/[0-9]*/[0-9]*$");
}
});
getGlobalConfiguration().getProperty(SETTINGS_playerAutopauseAfterFocusLost, value => {
if (value) {
core.registerScript((node: Node) => {
window.addEventListener('visibilitychange', observeTabFocus, false);
}, "^/anime/[0-9]*/[0-9]*$");
}
});
getGlobalConfiguration().getProperty(SETTINGS_playerAutoplayAfterFocusGain, value => {
resumePlayerOnVisible = value;
});
}
function observeScreenshotTooltip(tooltip: HTMLElement): void {
let observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
// Switched to invisible
if (!mutation.oldValue.includes('display: none') && helper.isHtmlElement(mutation.target) && (mutation.target as HTMLElement).style.display == 'none') {
let playerElement = findPlayerElement(PLAYER_ID);
if (helper.assigned(playerElement)) {
resumePlayer(playerElement);
}
}
});
});
observer.observe(tooltip, {
attributes: true,
attributeOldValue: true,
attributeFilter: ['style'],
});
}
function observeTabFocus(): void {
let docState = document.visibilityState;
let playerElement = findPlayerElement(PLAYER_ID);
if (docState === 'hidden') {
if (helper.assigned(playerElement)) {
pausePlayer(playerElement);
}
}
else if (docState === 'visible' && resumePlayerOnVisible) {
if (helper.assigned(playerElement)) {
resumePlayer(playerElement);
}
}
}
export function findPlayerElement(id: string): HTMLVideoElement {
let playerCandidate = document.getElementById(id);
if (playerCandidate instanceof HTMLVideoElement) {
return playerCandidate;
}
return undefined;
}
function resumePlayer(player: HTMLVideoElement) {
player.play();
}
function pausePlayer(player: HTMLVideoElement) {
player.pause()
}

View file

@ -1,13 +1,14 @@
import { getGlobalConfiguration, SETTINGS_requestBeautifyPage } from '../configuration/configuration';
import * as core from '../utils/aniwatchCore';
import * as color from '../utils/colors';
import * as helper from '../utils/helpers';
export function init(): void {
export function init() {
getGlobalConfiguration().getProperty(SETTINGS_requestBeautifyPage, value => {
if (value) {
core.registerScript((node: Node) => {
core.registerScript(node => {
// run the scripts
if (node instanceof HTMLElement) {
if (helper.isHtmlElement(node)) {
changeFollowedStarColor(node);
changeBorderColorOwnRequests(node);
removeUnknownUsers(node);
@ -17,20 +18,20 @@ export function init(): void {
});
}
function changeFollowedStarColor(node: HTMLElement): void {
const STAR_ICON = 'star';
function changeFollowedStarColor(node) {
const starIcon = 'star';
// find stars
let followedItems = Array.from(node.querySelectorAll('i')).filter(i => i.innerText.trim() === STAR_ICON);
let followedItems = Array.from(node.querySelectorAll('i')).filter(i => i.innerText.trim() === starIcon);
// change color
followedItems.forEach(item => item.style.color = color.aniBlue);
}
function changeBorderColorOwnRequests(node: HTMLElement): void {
const TARGET_TAG_NAME = 'MD-LIST-ITEM'; // tagName is upper case
function changeBorderColorOwnRequests(node) {
const targetTagName = 'MD-LIST-ITEM'; // tagName is upper case
let updateFunc = (item: HTMLElement): void => {
let updateFunc = item => {
let profileLink = item.querySelectorAll('a[href*="/profile/"]:not([href="/profile/false"])');
// highlight left border for own request
@ -40,7 +41,7 @@ function changeBorderColorOwnRequests(node: HTMLElement): void {
}
// are we target tag?
if (node.tagName === TARGET_TAG_NAME) {
if (node.tagName === targetTagName) {
updateFunc(node);
} else {
// find items -> all
@ -48,17 +49,15 @@ function changeBorderColorOwnRequests(node: HTMLElement): void {
// update borders
requestItems.forEach(item => {
if (item instanceof HTMLElement) {
updateFunc(item);
}
updateFunc(item);
});
}
}
function removeUnknownUsers(node: HTMLElement): void {
const TARGET_TAG_NAME = 'MD-LIST-ITEM'; // tagName is upper case
function removeUnknownUsers(node) {
const targetTagName = 'MD-LIST-ITEM'; // tagName is upper case
let updateFunc = (item: Element) => {
let updateFunc = item => {
// find user profile link -> own request
let profileLink = item.querySelectorAll('a[href*="/profile/"]:not([href="/profile/false"])');
@ -67,7 +66,7 @@ function removeUnknownUsers(node: HTMLElement): void {
let lowerDiv = upperDiv.parentElement.nextElementSibling;
// remember Data
let anime = lowerDiv.textContent;
let anime = lowerDiv.innerText;
let profileData = upperDiv.innerHTML;
// add user note if own request
@ -93,7 +92,7 @@ function removeUnknownUsers(node: HTMLElement): void {
upperDiv.appendChild(bElement);
}
if (node.tagName === TARGET_TAG_NAME) {
if (node.tagName === targetTagName) {
updateFunc(node);
} else {
// find items -> all

View file

@ -0,0 +1,61 @@
import { getGlobalConfiguration, SETTINGS_websiteHideUnusedTabs, SETTINGS_websiteOptimizeListAppearance } from '../configuration/configuration';
import * as core from '../utils/aniwatchCore';
import * as helper from '../utils/helpers';
export function init() {
getGlobalConfiguration().getProperty(SETTINGS_websiteHideUnusedTabs, value => {
// if disabled, add class to avoid our css optimizations
if (!value) {
let disableFunc = node => {
if (helper.isHtmlElement(node)) {
let disableNode = node => {
node.classList.add('awp-hide-unused-disabled')
}
if (node.tagName === 'MD-TAB-ITEM') {
disableNode(node);
}
else {
node.querySelectorAll('md-tab-item').forEach(node => disableNode(node));
}
}
};
core.registerScript(node => {
disableFunc(node);
}, ".*");
core.runAfterLoad(() => {
disableFunc(document.body);
}, ".*");
}
});
getGlobalConfiguration().getProperty(SETTINGS_websiteOptimizeListAppearance, value => {
// if disabled, add class to avoid our css optimizations
if (!value) {
let disableFunc = node => {
if (helper.isHtmlElement(node)) {
let disableNode = node => {
node.classList.add('awp-list-disabled')
}
if (node.tagName === 'MD-LIST-ITEM') {
disableNode(node);
}
else {
node.querySelectorAll('md-list-item').forEach(node => disableNode(node));
}
}
}
core.registerScript(node => {
disableFunc(node);
}, ".*");
core.runAfterLoad(() => {
disableFunc(document.body);
}, ".*");
}
});
}

View file

@ -1,60 +0,0 @@
import { getGlobalConfiguration, SETTINGS_websiteHideUnusedTabs, SETTINGS_websiteOptimizeListAppearance } from '../configuration/configuration';
import * as core from '../utils/aniwatchCore';
export function init(): void {
getGlobalConfiguration().getProperty(SETTINGS_websiteHideUnusedTabs, value => {
// if disabled, add class to avoid our css optimizations
if (!value) {
let disableFunc = (node: Element) => {
let disableNode = (node: Element) => {
node.classList.add('awp-hide-unused-disabled')
}
if (node.tagName === 'MD-TAB-ITEM') {
disableNode(node);
}
else {
node.querySelectorAll('md-tab-item').forEach(node => disableNode(node));
}
};
core.registerScript((node: Node) => {
if (node instanceof Element) {
disableFunc(node);
}
}, ".*");
core.runAfterLoad(() => {
disableFunc(document.body);
}, ".*");
}
});
getGlobalConfiguration().getProperty(SETTINGS_websiteOptimizeListAppearance, value => {
// if disabled, add class to avoid our css optimizations
if (!value) {
let disableFunc = (node: Element) => {
let disableNode = (node: Element) => {
node.classList.add('awp-list-disabled')
}
if (node.tagName === 'MD-LIST-ITEM') {
disableNode(node);
}
else {
node.querySelectorAll('md-list-item').forEach(node => disableNode(node));
}
}
core.registerScript(node => {
if (node instanceof Element) {
disableFunc(node);
}
}, ".*");
core.runAfterLoad(() => {
disableFunc(document.body);
}, ".*");
}
});
}

View file

@ -1,84 +0,0 @@
import Color from 'color';
import { getGlobalConfiguration, SETTINGS_websiteOptimizeFontColors } from '../configuration/configuration';
import * as core from '../utils/aniwatchCore';
import * as helper from '../utils/helpers';
const BADGE_CLASS = 'label';
const DARKCOLOR_CLASS = 'awp-fontColor-dark';
const __observedBadges = [];
export function init(): void {
getGlobalConfiguration().getProperty(SETTINGS_websiteOptimizeFontColors, value => {
if (value) {
core.runAfterLoad(() => {
checkRunColorOptimization(document.documentElement);
}, ".*");
core.runAfterLocationChange(() => {
checkRunColorOptimization(document.documentElement);
}, ".*");
core.registerScript((node: Node) => {
if (node instanceof Element) {
checkRunColorOptimization(node);
}
}, ".*");
}
});
}
function checkRunColorOptimization(node: Element): void {
// run the scripts
if (node.classList.contains(BADGE_CLASS)) {
tryRegisterObserverForBadge(node);
optimizeFontColorsBadges(node);
}
else {
node.querySelectorAll(`.${BADGE_CLASS}`).forEach(element => {
tryRegisterObserverForBadge(element);
optimizeFontColorsBadges(element);
});
}
}
function tryRegisterObserverForBadge(badge: Node): void {
// some badges change there color via late loading so we also have to observe the classlist
// example: Navigating from a list to an anime -> "Currently Airing" late loads the color badge
// this only happens when navigating from a list, direct loading works
if (__observedBadges.indexOf(badge) >= 0) {
return;
}
let obsever = new MutationObserver(mutations => {
mutations.forEach(mutation => {
// prevent recursive calls when our class is added / removed
if (mutation.target instanceof Element) {
if ((mutation.oldValue?.indexOf(DARKCOLOR_CLASS) >= 0 ?? false) !== (mutation.target?.classList?.contains(DARKCOLOR_CLASS) ?? false)) {
return;
}
else {
optimizeFontColorsBadges(mutation.target);
}
}
});
});
obsever.observe(badge, {
attributes: true,
attributeFilter: ['class'],
attributeOldValue: true,
});
__observedBadges.push(badge);
}
function optimizeFontColorsBadges(badge: Element): void {
let colorStr = window.getComputedStyle(badge, null).getPropertyValue('background-color');
// some elements do not have a computed background color
if (colorStr.length > 0) {
let color = new Color(colorStr)
badge.classList.toggle(DARKCOLOR_CLASS, color.isLight());
}
}

View file

@ -2,14 +2,12 @@ import { getGlobalConfiguration, SETTINGS_animeLanguageDisplay } from '../config
import * as core from '../utils/aniwatchCore';
import * as helper from '../utils/helpers';
const MANIPULATED_ATTR_NAME = 'awpManipulated';
export function init(): void {
export function init() {
getGlobalConfiguration().getProperty(SETTINGS_animeLanguageDisplay, value => {
if (value) {
core.registerScript((node: Node) => {
core.registerScript(node => {
// run the scripts
if (node instanceof Element) {
if (helper.isHtmlElement(node)) {
updateLanguageDisplay(node)
}
}, "^/anime/[0-9]*$");
@ -17,35 +15,34 @@ export function init(): void {
});
}
function updateLanguageDisplay(node: Element): void {
const LIST_NODE_NAME = 'MD-LIST-ITEM';
const BOX_NODE_NAME = 'DIV';
const BOX_CLASS_NAME = 'card-margin';
function updateLanguageDisplay(node) {
const listNodeName = 'MD-LIST-ITEM';
const boxNodeName = 'DIV';
const boxClassName = 'card-margin';
if (node.nodeName === LIST_NODE_NAME) {
if (node.nodeName === listNodeName) {
updateLanguageDisplayListMode(node);
}
else if (node.nodeName === BOX_NODE_NAME && node.classList.contains(BOX_CLASS_NAME)) {
else if (node.nodeName === boxNodeName && node.classList.contains(boxClassName)) {
updateLanguageDisplayBoxMode(node);
}
}
function updateLanguageDisplayListMode(node: Element): void {
function updateLanguageDisplayListMode(node) {
// last column with flags
let col = node.querySelector('h3.layout-align-end-center');
if (!helper.assigned(col) || col.hasAttribute(MANIPULATED_ATTR_NAME)) {
if (!helper.assigned(col) || col.awpManipulated) {
return;
}
doUpdateLanguageDisplay(col, false);
}
function updateLanguageDisplayBoxMode(node: Element): void {
function updateLanguageDisplayBoxMode(node) {
// last column with flags
let col = node.querySelector('div.layout-align-end-start');
if (!helper.assigned(col) || col.hasAttribute(MANIPULATED_ATTR_NAME)) {
if (!helper.assigned(col) || col.awpManipulated) {
return;
}
@ -53,27 +50,27 @@ function updateLanguageDisplayBoxMode(node: Element): void {
}
function doUpdateLanguageDisplay(parent: Element, isBoxedModed: boolean): void {
const LIST_LANG_PREFIX = 'ep.lang.';
const BOX_LANG_PREFIX = 'episodeObject.lang.';
function doUpdateLanguageDisplay(parent, isBoxedModed) {
const listLangPrefix = 'ep.lang.';
const boxLangPrefix = 'episodeObject.lang.';
// aniwatch uses different prefixes in list und box mode :/
let realLangPrefix = isBoxedModed ? BOX_LANG_PREFIX : LIST_LANG_PREFIX;
let realLangPrefix = isBoxedModed ? boxLangPrefix : listLangPrefix;
const DUB_SUFFIX = 'dub';
const SUB_SUFFIX = 'sub';
const dubSuffix = 'dub';
const subSuffix = 'sub';
const DUB_ICON = 'volume_up';
const SUB_ICON = 'closed_caption';
const ZERO_WIDTH_SPACE_CHARACTER = ''; // &#8203;
const dubIcon = 'volume_up';
const subIcon = 'closed_caption';
const zeroWidthSpace = ''; // &#8203;
let subs: Array<string> = [];
let dubs: Array<string> = [];
let subs = [];
let dubs = [];
// find subs
let subCols = parent.querySelectorAll('[ng-hide*="sub"]');
subCols.forEach((element: Element) => {
subCols.forEach(element => {
let langAttr = element.attributes['ng-hide'].value;
let lang = langAttr.substring(langAttr.indexOf(realLangPrefix) + realLangPrefix.length, langAttr.indexOf(SUB_SUFFIX));
let lang = langAttr.substring(langAttr.indexOf(realLangPrefix) + realLangPrefix.length, langAttr.indexOf(subSuffix));
if (element.attributes['aria-hidden'].value == 'false') {
subs.push(lang);
}
@ -81,9 +78,9 @@ function doUpdateLanguageDisplay(parent: Element, isBoxedModed: boolean): void {
// find dubs
let dubCols = parent.querySelectorAll('[ng-hide*="dub"]');
dubCols.forEach((element: Element) => {
dubCols.forEach(element => {
let langAttr = element.attributes['ng-hide'].value;
let lang = langAttr.substring(langAttr.indexOf(realLangPrefix) + realLangPrefix.length, langAttr.indexOf(DUB_SUFFIX));
let lang = langAttr.substring(langAttr.indexOf(realLangPrefix) + realLangPrefix.length, langAttr.indexOf(dubSuffix));
if (element.attributes['aria-hidden'].value == 'false') {
dubs.push(lang);
}
@ -109,12 +106,12 @@ function doUpdateLanguageDisplay(parent: Element, isBoxedModed: boolean): void {
let dubIconDiv = document.createElement('i');
if (iconsRequired) {
dubIconDiv.classList.add('material-icons', 'mr-3');
dubIconDiv.innerText = DUB_ICON;
dubIconDiv.innerText = dubIcon;
}
// add dummy with 24px for correct presentation
else {
dubIconDiv.style.height = '24px';
dubIconDiv.innerText = ZERO_WIDTH_SPACE_CHARACTER;
dubIconDiv.innerText = zeroWidthSpace;
}
dubDiv.appendChild(dubIconDiv);
@ -135,16 +132,16 @@ function doUpdateLanguageDisplay(parent: Element, isBoxedModed: boolean): void {
let subIconDiv = document.createElement('i');
if (iconsRequired) {
subIconDiv.classList.add('material-icons', 'mr-3');
subIconDiv.innerText = SUB_ICON;
subIconDiv.innerText = subIcon;
}
// add dummy with 24px for correct presentation
else {
subIconDiv.style.height = '24px';
subIconDiv.innerText = ZERO_WIDTH_SPACE_CHARACTER;
subIconDiv.innerText = zeroWidthSpace;
}
subDiv.appendChild(subIconDiv);
subs.forEach((lang: string) => {
subs.forEach(lang => {
let langIcon = document.createElement('i');
langIcon.classList.add('flag', `flag-${lang}`, 'mg-all-1');
subDiv.appendChild(langIcon);
@ -157,7 +154,7 @@ function doUpdateLanguageDisplay(parent: Element, isBoxedModed: boolean): void {
}
if (dubs.length > 0) {
dubs.forEach((lang: string) => {
dubs.forEach(lang => {
let colDiv = document.createElement('div');
colDiv.setAttribute('layout', 'column');
colDiv.classList.add('layout-column');
@ -170,12 +167,12 @@ function doUpdateLanguageDisplay(parent: Element, isBoxedModed: boolean): void {
let dubIconDiv = document.createElement('i');
if (iconsRequired) {
dubIconDiv.classList.add('material-icons', 'mr-3');
dubIconDiv.innerText = DUB_ICON;
dubIconDiv.innerText = dubIcon;
}
// add dummy with 24px for correct presentation
else {
dubIconDiv.style.height = '24px';
dubIconDiv.innerText = ZERO_WIDTH_SPACE_CHARACTER;
dubIconDiv.innerText = zeroWidthSpace;
}
dubDiv.appendChild(dubIconDiv);
@ -196,12 +193,12 @@ function doUpdateLanguageDisplay(parent: Element, isBoxedModed: boolean): void {
let subIconDiv = document.createElement('i');
if (iconsRequired) {
subIconDiv.classList.add('material-icons', 'mr-3');
subIconDiv.innerText = SUB_ICON;
subIconDiv.innerText = subIcon;
}
// add dummy with 24px for correct presentation
else {
subIconDiv.style.height = '24px';
subIconDiv.innerText = ZERO_WIDTH_SPACE_CHARACTER;
subIconDiv.innerText = zeroWidthSpace;
}
subDiv.appendChild(subIconDiv);
@ -219,17 +216,13 @@ function doUpdateLanguageDisplay(parent: Element, isBoxedModed: boolean): void {
});
parent.querySelectorAll('.layout-column:not(:last-child)').forEach(div => {
if (div instanceof HTMLElement) {
div.style.borderRight = '1px solid rgba(155,155,155, 0.2)';
}
div.style.borderRight = '1px solid rgba(155,155,155, 0.2)';
})
parent.querySelectorAll('.layout-column').forEach(div => {
if (div instanceof HTMLElement) {
div.style.paddingLeft = '2px';
div.style.paddingRight = '2px';
}
div.style.paddingLeft = '2px';
div.style.paddingRight = '2px';
})
parent.setAttribute('awpManipulated', String(true));
parent.awpManipulated = true;
}

View file

@ -2,7 +2,7 @@ import { getGlobalConfiguration, SETTINGS_websiteShowNotificationsCountInTab } f
import * as core from '../utils/aniwatchCore';
import * as helper from '../utils/helpers';
export function init(): void {
export function init() {
getGlobalConfiguration().getProperty(SETTINGS_websiteShowNotificationsCountInTab, value => {
if (value) {
core.runAfterLoad(() => {
@ -16,17 +16,17 @@ export function init(): void {
});
}
function getNotificationCount(): number {
function getNotificationCount() {
if (core.isLoggedIn()) {
let menuUserText = document.getElementById('materialize-menu-dropdown').innerText.split('\n')[4];
let notificationCount = parseInt(menuUserText.match(/\d+/)?.[0]) ?? 0;
let notificationCount = menuUserText.match(/\d+/)?.[0] ?? 0;
return notificationCount;
} else {
return 0;
}
}
function updateNotificationsInTitle(): void {
function updateNotificationsInTitle() {
let count = getNotificationCount();
if (helper.assigned(count) && count > 0) {

View file

@ -5,7 +5,7 @@ import * as helper from '../utils/helpers';
const quickSearchID = 'ea-quickSearch';
const quickSearchLink = 'ea-quickSearchLink';
export function init(): void {
export function init() {
getGlobalConfiguration().getProperty(SETTINGS_websiteDisplayQuickSearch, value => {
if (value) {
core.runAfterLoad(() => {
@ -15,7 +15,7 @@ export function init(): void {
});
}
function initSearch(): void {
function initSearch() {
let entry = document.createElement('li');
entry.setAttribute('ng-repeat', 'item in navbar');
entry.setAttribute('ng-class', '{\'anime-indicator\': item[\'@attributes\'].title==\'Anime\'}');
@ -45,13 +45,13 @@ function initSearch(): void {
document.addEventListener('keypress', event => handleSearchForShiftF(event));
// additionally, the last dropdown ul has a "right: 0px" style, which has to be fixed with auto, otherwhise it will pop up in the wrong position
(Array.from(menu.querySelectorAll('ul.dropdown')).slice(-1)[0] as HTMLElement).style.right = 'auto';
Array.from(menu.querySelectorAll('ul.dropdown')).slice(-1)[0].style.right = 'auto';
}
function handleQuickSearch(event: KeyboardEvent): void {
function handleQuickSearch(event) {
if (event.key === 'Enter') {
let quickSearchElement = document.getElementById(quickSearchID) as HTMLInputElement;
let linkElement = document.getElementById(quickSearchLink) as HTMLAnchorElement;
let quickSearchElement = document.getElementById(quickSearchID);
let linkElement = document.getElementById(quickSearchLink);
let url = new URL(window.location.origin)
url.pathname = '/search';
@ -67,10 +67,10 @@ function handleQuickSearch(event: KeyboardEvent): void {
}
}
function handleSearchForShiftF(event: KeyboardEvent): void {
function handleSearchForShiftF(event) {
if (helper.isShiftPressed) {
// check if some kind of input is focused already; we then prevent our hotkey
if (document.activeElement instanceof HTMLInputElement || document.activeElement instanceof HTMLTextAreaElement || ((document.activeElement as HTMLElement)?.isContentEditable ?? false)) {
if (document.activeElement instanceof HTMLInputElement || document.activeElement instanceof HTMLTextAreaElement || document.activeElement.isContentEditable) {
return;
}

View file

@ -1,228 +0,0 @@
import spacetime from 'spacetime';
import { getGlobalConfiguration, SETTINGS_websiteAutoTimeConversion } from '../configuration/configuration';
import * as core from '../utils/aniwatchCore';
import * as helper from '../utils/helpers';
const __alteredNodes = [];
const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
export function init(): void {
getGlobalConfiguration().getProperty(SETTINGS_websiteAutoTimeConversion, value => {
if (value) {
// The regexp pattern matches anything except the airing page.
// This is because we would have to restructure the complete site to update time data.
// Additionally, there is a big hint that all data would be UTC+1
core.runAfterLoad(() => {
updateTimestamps(document.documentElement);
}, "^/(?!airing).*$");
core.runAfterLocationChange(() => {
updateTimestamps(document.documentElement);
}, "^/(?!airing).*$");
core.registerScript((node: Node) => {
updateTimestamps(node);
}, "^/(?!airing).*$");
}
});
}
function getSpaceDateTimeFormat(use24Format: boolean): string {
return `${getSpaceDateFormat()} ${getSpaceTimeFormat(use24Format)}`;
}
function getSpaceTimeFormat(use24Format: boolean): string {
if (use24Format) {
return '{time-24}';
}
return '{time}';
}
function getSpaceDateFormat(): string {
return '{date}. {month-short} {year}';
}
function tryUpdateDateTime(node: Node): boolean {
const REG_DATETIME = /(\d{2}(\/|\.)){2}\d{4} *\d?\d:\d{2}( (AM|PM))?/g;
const REG_TIME = /\d?\d:\d{2}/;
const REG_AMPM = /\s(am|pm)/i;
let hits = Array.from(node.textContent.matchAll(REG_DATETIME), match => match[0]);
if (hits.length === 0) {
return false;
}
hits.forEach(hit => {
let use24Format = false;
let processedStr = hit
// string must be converted into 12h format
if (processedStr.search(REG_AMPM) < 0) {
let timeStr = processedStr.match(REG_TIME)[0];
let hm = timeStr.split(':');
let hour = parseInt(hm[0]);
if (hour >= 12) {
timeStr = timeStr.replace(`${hour}:`, `${hour - 12}:`);
timeStr += 'pm';
}
else {
timeStr += 'am';
}
processedStr = processedStr.replace(REG_TIME, timeStr);
use24Format = true;
}
// if time has a space before am/pm, this has to be removed for spacetime
processedStr = processedStr.replace(REG_AMPM, '$1');
let datetime = spacetime(processedStr, 'UTC+1', { dmy: true });
datetime = datetime.goto(spacetime().tz);
let replaceText = String(datetime.format(getSpaceDateTimeFormat(use24Format)));
node.textContent = node.textContent.replace(hit, replaceText);
});
return true;
}
function tryUpdateDate(node: Node): boolean {
const REG_DATE = /(\d{2}(\/|\.)){2}\d{4}/g;
let hits = Array.from(node.textContent.matchAll(REG_DATE), match => match[0]);
if (hits.length === 0) {
return false;
}
hits.forEach(hit => {
let datetime = spacetime(hit, 'UTC+1', { dmy: true });
datetime = datetime.goto(spacetime().tz);
let replaceText = String(datetime.format(getSpaceDateFormat()));
node.textContent = node.textContent.replace(hit, replaceText);
});
return true;
}
function tryUpdateTime(node: Node): boolean {
const REG_TIME = /\d?\d:\d{2}( (AM|PM))?/g;
const REG_AMPM = /\s(am|pm)/i;
let hits = Array.from(node.textContent.matchAll(REG_TIME), match => match[0]);
if (hits.length === 0) {
return false;
}
hits.forEach(hit => {
let use24Format = false;
let processedStr = hit
// string must be converted into 12h format
if (processedStr.search(REG_AMPM) < 0) {
let timeStr = processedStr.match(REG_TIME)[0];
let hm = timeStr.split(':');
let hour = parseInt(hm[0]);
if (hour >= 12) {
timeStr = timeStr.replace(`${hour}:`, `${hour - 12}:`);
timeStr += 'pm';
}
else {
timeStr += 'am';
}
processedStr = processedStr.replace(REG_TIME, timeStr);
use24Format = true;
}
// if time has a space before am/pm, this has to be removed for spacetime
processedStr = processedStr.replace(REG_AMPM, '$1');
let datetime = spacetime();
datetime = datetime.goto('UTC+1');
datetime = datetime.time(processedStr);
datetime = datetime.goto(spacetime().tz);
let replaceText = String(datetime.format(getSpaceTimeFormat(use24Format)));
node.textContent = node.textContent.replace(hit, replaceText);
// SPECIAL CASE: Anime has the day written in broadcast bade. This may be different in another timezone
let tzMeta = spacetime().timezone();
let originalH = datetime.hour() - tzMeta.current.offset + 1;
let dOffset = 0;
// we moved to next day
if (originalH < 0) {
dOffset = 1;
}
// we moved to previous day
else if (originalH > 24) {
dOffset = -1;
}
// if day changed
if (dOffset != 0) {
let dayNode = (node.parentNode as Element)?.previousElementSibling;
if (helper.assigned(dayNode)) {
for (let i = 0; i < DAYS.length; i++) {
if (dayNode.textContent.indexOf(DAYS[i]) >= 0) {
dayNode.textContent = dayNode.textContent.replace(DAYS[i], DAYS[(i + DAYS.length + dOffset) % DAYS.length]); // add DAYS.length to avoid negative numbers in the modulo operation
break;
}
}
}
}
});
return true;
}
function tryUpdateTimeZone(node: Node): boolean {
const HINT_UTC = 'UTC+1';
if (node.textContent === HINT_UTC) {
let tzMeta = spacetime().timezone();
node.textContent = `${tzMeta.name} (UTC${tzMeta.current.offset >= 0 ? '+' : ''}${tzMeta.current.offset})`;
return true;
}
return false;
}
function updateTimestamps(node) {
let nodes = helper.findTextNodes(node);
nodes.forEach(node => {
// avoid double updates
if (__alteredNodes.indexOf(node) >= 0) {
return;
}
if (tryUpdateDateTime(node)) {
__alteredNodes.push(node);
return;
}
if (tryUpdateDate(node)) {
__alteredNodes.push(node);
return;
}
if (tryUpdateTime(node)) {
__alteredNodes.push(node);
return;
}
if (tryUpdateTimeZone(node)) {
__alteredNodes.push(node);
return;
}
});
}

View file

@ -1,153 +0,0 @@
import * as core from '../utils/aniwatchCore';
import { v4 as uuidv4 } from 'uuid';
import { getGlobalConfiguration, SETTINGS_w2gDisplayCharacterCounter, SETTINGS_w2gAutotoggleHide, SETTINGS_w2gAutoscrollToUnseen } from '../configuration/configuration';
import { assigned } from '../utils/helpers';
import { findPlayerElement } from "../enhancements/anilyr";
const PLAYER_ID = 'wPlayer';
let hidden: boolean;
export function init(): void {
getGlobalConfiguration().getProperty(SETTINGS_w2gDisplayCharacterCounter, value => {
if (value) {
core.runAfterLoad(() => {
manipulateChatInput();
}, "^/watch2gether/.*$");
core.runAfterLocationChange(() => {
manipulateChatInput();
}, "^/watch2gether/.*$");
}
});
getGlobalConfiguration().getProperty(SETTINGS_w2gAutotoggleHide, value => {
if (value) {
core.runAfterLoad(() => {
addAutohideListener();
}, "^/watch2gether/.*$");
core.runAfterLocationChange(() => {
addAutohideListener();
}, "^/watch2gether/.*$");
}
});
getGlobalConfiguration().getProperty(SETTINGS_w2gAutoscrollToUnseen, value => {
if (value) {
core.runAfterLoad(() => {
let element = findSearchResults();
if (assigned(element)) {
scrollSearchResults(element);
}
}, "^/watch2gether/.*$");
core.runAfterLocationChange(() => {
let element = findSearchResults();
if (assigned(element)) {
scrollSearchResults(element);
}
}, "^/watch2gether/.*$");
}
});
}
function manipulateChatInput(): void {
let textarea = document.querySelector('.chat-input textarea') as HTMLTextAreaElement;
// avoid duplicate registration
if (assigned(textarea.dataset.charCounterId)) {
return;
}
addCharCounter(textarea);
}
function addCharCounter(textarea: HTMLTextAreaElement): void {
let chatDiv = textarea.parentElement.parentElement; // div with chat input and controls
let controlRow = chatDiv.children[1]; // row with controls
let btn = controlRow.querySelector('button'); // find send button
let charCounterSpan = document.createElement('span'); // create span for counter
charCounterSpan.classList.add('awp-w2g-chatCounter');
// id and "connection"
let counterId = `awp-${uuidv4()}`
charCounterSpan.id = counterId;
textarea.dataset.charCounterId = counterId;
btn.parentElement.insertBefore(charCounterSpan, btn); // and insert in front of the button
updateCharCounter(textarea, charCounterSpan);
textarea.addEventListener('keyup', () => {
updateCharCounter(textarea, charCounterSpan)
});
}
function updateCharCounter(textarea: HTMLTextAreaElement, charCounterSpan: HTMLSpanElement): void {
const SHAKE_CLASS = 'awp-w2g-chatCounter-max';
let current = textarea.value.length;
let max = textarea.maxLength;
charCounterSpan.innerText = `${current} / ${max}`;
// animation if at max
if (current >= max && !charCounterSpan.classList.contains(SHAKE_CLASS)) {
charCounterSpan.classList.add(SHAKE_CLASS);
// remove css class after animation finished, so it can be restarted again
setTimeout(() => {
charCounterSpan.classList.remove(SHAKE_CLASS);
}, 200);
}
}
function addAutohideListener(): void {
let playerElement = findPlayerElement(PLAYER_ID);
let hideButton: HTMLButtonElement = document.getElementsByClassName('no-margin md-button md-ink-ripple layout-align-center-center layout-row')[0] as HTMLButtonElement;
if (assigned(playerElement) && assigned(hideButton)) {
if (hideButton.textContent.includes('HIDE')) {
hidden = false;
} else if (hideButton.textContent.includes('SHOW')) {
hidden = true;
}
playerElement.addEventListener('play', fn => {
if (!hidden) {
hideButton.click();
hidden = !hidden;
}
})
playerElement.addEventListener('pause', fn => {
if (hidden) {
hideButton.click();
hidden = !hidden;
}
})
}
}
function scrollSearchResults(searchRes: Element): void {
let observer = new MutationObserver(mutations => {
let scrollTarget = searchRes.querySelector('md-list-item:not(.animelist-completed):not(.animelist-completed-add)') as HTMLElement;
if (assigned(scrollTarget)) {
// The node isn´t in its correct position directly when it´s added so we wait a small bit of time before we start scrolling.
// Also works for long loading lists which need more time to load than we wait (for example One Piece).
window.setTimeout(() => {
// scroll container to episode first
searchRes.scrollTop = scrollTarget.offsetTop;
// then scroll page to episode if neccessarry
scrollTarget.scrollIntoView({ behavior: "smooth", block: "nearest" });
}, 500);
}
});
observer.observe(searchRes, {
childList: true,
});
}
function findSearchResults(): Element {
return document.querySelector('.search-results .ep-view');
}

View file

@ -0,0 +1,65 @@
import * as core from '../utils/aniwatchCore';
import { v4 as uuidv4 } from 'uuid';
import { getGlobalConfiguration, SETTINGS_w2gDisplayCharacterCounter } from '../configuration/configuration';
import { assigned } from '../utils/helpers';
export function init() {
getGlobalConfiguration().getProperty(SETTINGS_w2gDisplayCharacterCounter, value => {
if (value) {
core.runAfterLocationChange(() => {
manipulateChatInput();
}, "^/watch2gether/.*$");
}
});
}
function manipulateChatInput() {
let textarea = document.querySelector('.chat-input textarea');
// avoid duplicate registration
if (assigned(textarea.dataset.charCounterId)) {
return;
}
addCharCounter(textarea);
}
function addCharCounter(textarea) {
let chatDiv = textarea.parentElement.parentElement; // div with chat input and controls
let controlRow = chatDiv.children[1]; // row with controls
let btn = controlRow.querySelector('button'); // find send button
let charCounterSpan = document.createElement('span'); // create span for counter
charCounterSpan.classList.add('awp-w2g-chatCounter');
// id and "connection"
let counterId = `awp-${uuidv4()}`
charCounterSpan.id = counterId;
textarea.dataset.charCounterId = counterId;
btn.parentElement.insertBefore(charCounterSpan, btn); // and insert in front of the button
updateCharCounter(textarea, charCounterSpan);
textarea.addEventListener('keyup', () => {
updateCharCounter(textarea, charCounterSpan)
});
}
function updateCharCounter(textarea, charCounterSpan) {
const SHAKE_CLASS = 'awp-w2g-chatCounter-max';
let current = textarea.value.length;
let max = textarea.maxLength;
charCounterSpan.innerText = `${current} / ${max}`;
// animation if at max
if (current >= max && !charCounterSpan.classList.contains(SHAKE_CLASS)) {
charCounterSpan.classList.add(SHAKE_CLASS);
// remove css class after animation finished, so it can be restarted again
setTimeout(() => {
charCounterSpan.classList.remove(SHAKE_CLASS);
}, 200);
}
}

View file

@ -5,28 +5,25 @@ const OPTION_SELECTOR = 'input[type="checkbox"';
function storeOptions() {
document.querySelectorAll(OPTION_SELECTOR).forEach(optionElement => {
let optionInputElement = optionElement as HTMLInputElement;
getGlobalStorageProvider().setDataAsBoolean(optionInputElement.id, optionInputElement.checked);
getGlobalStorageProvider().setData(optionElement.id, optionElement.checked);
});
}
function restoreOptions() {
document.querySelectorAll(OPTION_SELECTOR).forEach(optionElement => {
let optionInputElement = optionElement as HTMLInputElement;
let defaultValue = optionInputElement.dataset.defaultValue === 'true' ? true : false;
document.querySelectorAll(OPTION_SELECTOR).forEach(optionElement => {
let defaultValue = optionElement.dataset.defaultValue === 'true' ? true : false;
getGlobalStorageProvider().getDataAsBoolean(optionInputElement.id, defaultValue, value => {
optionInputElement.checked = value;
getGlobalStorageProvider().getData(optionElement.id, defaultValue, value => {
optionElement.checked = value;
});
});
}
function resetOptions() {
document.querySelectorAll(OPTION_SELECTOR).forEach(optionElement => {
let optionInputElement = optionElement as HTMLInputElement;
let defaultValue = optionInputElement.dataset.defaultValue === 'true' ? true : false;
let defaultValue = optionElement.dataset.defaultValue === 'true' ? true : false;
optionInputElement.checked = defaultValue;
optionElement.checked = defaultValue;
});
}

View file

@ -1,24 +1,11 @@
import * as helper from './helpers';
type ScriptCallback = () => void;
type NodeScriptCallback = (node: Node) => void;
type ScriptObj = {
function: ScriptCallback,
pattern: string
}
type NodeScriptObj = {
function: NodeScriptCallback,
pattern: string
}
/* SCRIPT LOGICS */
let __scripts: Array<NodeScriptObj> = [];
let __afterLoadScripts: Array<ScriptObj> = [];
let __afterLocationChangeScripts: Array<ScriptObj> = [];
let __scripts = [];
let __afterLoadScripts = [];
let __afterLocationChangeScripts = [];
export function initCore(): void {
export function initCore() {
let observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
for (let i = 0; i < mutation.addedNodes.length; i++) {
@ -59,11 +46,11 @@ export function initCore(): void {
helper.onReady(() => awaitPageLoaded());
}
export function registerScript(func: NodeScriptCallback, pattern: string = '.*'): void {
__scripts.push({ function: func, pattern: pattern });
export function registerScript(func, pattern = '.*') {
__scripts.push({ "function": func, "pattern": pattern });
}
export function runScripts(node: Node): void {
export function runScripts(node) {
__scripts.forEach(script => {
if (window.location.pathname.match(script.pattern)) {
script.function(node);
@ -71,20 +58,20 @@ export function runScripts(node: Node): void {
});
}
function findPreloader(): HTMLElement {
function findPreloader() {
return document.getElementById('preloader');
}
export function runAfterLoad(func: ScriptCallback, pattern: string = '.*'): void {
export function runAfterLoad(func, pattern = '.*') {
let preloader = findPreloader();
if (typeof preloader !== undefined && preloader.style.display !== "none") {
__afterLoadScripts.push({ function: func, pattern: pattern });
__afterLoadScripts.push({ "function": func, "pattern": pattern });
} else {
func();
}
}
function awaitPageLoaded(): void {
function awaitPageLoaded() {
let preLoader = findPreloader();
let runScripts = () => {
@ -100,9 +87,9 @@ function awaitPageLoaded(): void {
return;
}
let loop = window.setInterval(() => {
let loop = setInterval(() => {
if (preLoader.style.display === "none" && document.readyState === 'complete') {
window.clearInterval(loop);
clearInterval(loop);
runScripts();
}
@ -110,12 +97,12 @@ function awaitPageLoaded(): void {
}
/* PATHNAME LOGIC */
export function runAfterLocationChange(func: ScriptCallback, pattern: string = '.*'): void {
__afterLocationChangeScripts.push({ function: func, pattern: pattern });
export function runAfterLocationChange(func, pattern = '.*') {
__afterLocationChangeScripts.push({ "function": func, "pattern": pattern });
}
/* LOGIN LOGIC */
export function isLoggedIn(): boolean {
export function isLoggedIn() {
let menu = document.getElementById('materialize-menu-dropdown');
let result = true;

View file

@ -0,0 +1,39 @@
export var isShiftPressed = false;
export var isCtrlPressed = false;
export function isHtmlElement(object) {
return object instanceof HTMLElement;
}
export function initHelpers() {
document.addEventListener('keydown', event => handleKeyDown(event));
document.addEventListener('keyup', event => handleKeyUp(event));
}
export function onReady(fn) {
if (document.readyState != 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
export function assigned(obj) {
return !(typeof obj === 'undefined' || obj === null);
}
function handleKeyDown(event) {
handleKeyToggle(event, true);
}
function handleKeyUp(event) {
handleKeyToggle(event, false);
}
function handleKeyToggle(event, isPressed) {
if (event.key === 'Shift') {
isShiftPressed = isPressed;
} else if (event.key === 'Control') {
isCtrlPressed = isPressed;
}
}

View file

@ -1,54 +0,0 @@
export var isShiftPressed: boolean = false;
export var isCtrlPressed: boolean = false;
export function isHtmlElement(object: any) {
return object instanceof HTMLElement;
}
export function initHelpers(): void {
document.addEventListener('keydown', event => handleKeyDown(event));
document.addEventListener('keyup', event => handleKeyUp(event));
}
export function onReady(fn: () => void) {
if (document.readyState != 'loading') {
fn();
} else {
document.addEventListener('DOMContentLoaded', fn);
}
}
export function assigned(obj: any): boolean {
return !(typeof obj === 'undefined' || obj === null);
}
function handleKeyDown(event: KeyboardEvent) {
handleKeyToggle(event, true);
}
function handleKeyUp(event: KeyboardEvent) {
handleKeyToggle(event, false);
}
function handleKeyToggle(event: KeyboardEvent, isPressed: boolean) {
if (event.key === 'Shift') {
isShiftPressed = isPressed;
} else if (event.key === 'Control') {
isCtrlPressed = isPressed;
}
}
export function findTextNodes(baseNode: Node): Array<Node> {
if (!assigned(baseNode)) {
baseNode = document.documentElement;
}
let walker = document.createTreeWalker(baseNode, NodeFilter.SHOW_TEXT, null);
let node;
let results = [];
while (node = walker.nextNode()) {
results.push(node);
}
return results;
}

View file

@ -1,9 +1,9 @@
{
"name": "Aniwatch Plus",
"short_name": "AW+",
"version": "1.1.0.0",
"version_name": "1.1",
"description": "Aniwatch Plus is an unofficial extension which provides several UI improvements for https://aniwatch.me.",
"version": "0.4.2.1",
"version_name": "0.4.2 Beta",
"description": "Aniwatch Plus is an unofficial extension which provides several UI improvments for https://aniwatch.me.",
"permissions": [
"storage",
"*://aniwatch.me/*"

View file

@ -2,7 +2,6 @@
@import "./vars/colors";
// enhancements
@import "./enhancements/fontColor";
@import "./enhancements/lists";
@import "./enhancements/tabs";
@import "./enhancements/watch2gether";

View file

@ -1,7 +0,0 @@
.awp{
&-fontColor{
&-dark{
color: $aniwatchDark !important;
}
}
}

View file

@ -1,5 +1,2 @@
$aniwatchBlue: #348fff;
$aniwatchDark: #282a39;
$gray: rgba(155, 155, 155, 0.2);

View file

@ -1,9 +0,0 @@
{
"compilerOptions": {
"esModuleInterop": true,
// You have to explicitly set @types to get DefinitelyTyped type definitions
"typeRoots": ["node_modules/@types", "node_modules/web-ext-types"],
// Default would be DOM,ES6,DOM.Iterable,ScriptHost (see https://www.typescriptlang.org/docs/handbook/compiler-options.html). However the ES 2020 is required for string.prototype.matchAll (see https://stackoverflow.com/a/57298833).
"lib": ["ES2020", "DOM", "DOM.Iterable", "ScriptHost"]
}
}