Compare commits

..

No commits in common. "c3eaddb31455eed5a14085d24ac5413d439ff11b" and "095a3daadd8539e1da15fd4731a7d86d56fe5e84" have entirely different histories.

81 changed files with 14449 additions and 24179 deletions

33319
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,17 +4,17 @@
"description": "Aspect ratio fixer for youtube and other sites, with automatic aspect ratio detection. Supports ultrawide and other ratios.",
"author": "Tamius Han <tamius.han@gmail.com>",
"scripts": {
"build": "npm run pre-build; cross-env NODE_ENV=production BROWSER=firefox CHANNEL=stable webpack",
"build": "npm run pre-build; cross-env NODE_ENV=production BROWSER=firefox CHANNEL=stable webpack --hide-modules",
"build-all": "bash ./scripts/build-all.sh",
"build-chrome": "cross-env NODE_ENV=production BROWSER=chrome CHANNEL=stable webpack",
"build-chrome:dev": "cross-env NODE_ENV=development BROWSER=chrome webpack",
"build-edge": "cross-env NODE_ENV=production BROWSER=edge CHANNEL=stable webpack",
"build-nightly": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=nightly webpack",
"build-nightly-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=nightly webpack",
"build-testing": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=testing webpack",
"build-testing-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=testing webpack",
"build-chrome": "cross-env NODE_ENV=production BROWSER=chrome CHANNEL=stable webpack --hide-modules",
"build-chrome:dev": "cross-env NODE_ENV=development BROWSER=chrome webpack --hide-modules",
"build-edge": "cross-env NODE_ENV=production BROWSER=edge CHANNEL=stable webpack --hide-modules",
"build-nightly": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=nightly webpack --hide-modules",
"build-nightly-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=nightly webpack --hide-modules",
"build-testing": "cross-env NODE_ENV=development BROWSER=firefox CHANNEL=testing webpack --hide-modules",
"build-testing-chrome": "cross-env NODE_ENV=development BROWSER=chrome CHANNEL=testing webpack --hide-modules",
"build-zip": "node scripts/build-zip.js",
"build:dev": "webpack",
"build:dev": "webpack --hide-modules",
"dev": "cross-env NODE_ENV=development CHANNEL=dev concurrently \"cross-env BROWSER=firefox npm run build:dev -- --watch\" \"cross-env BROWSER=chrome npm run build:dev -- --watch\" \"cross-env BROWSER=edge npm run build:dev -- --watch\"",
"dev:windows": "cross-env NODE_ENV=development CHANNEL=dev concurrently \"cross-env BROWSER=firefox npm run build:dev -- -w --watch-poll\" \"cross-env BROWSER=chrome npm run build:dev -- -w --watch-poll\" \"cross-env BROWSER=edge npm run build:dev -- -w --watch-poll\"",
"dev-ff": "rm -rf ./dist-ff && cross-env NODE_ENV=development CHANNEL=dev BROWSER=firefox npm run build:dev -- --watch",
@ -25,63 +25,55 @@
"start:windows": "npm run dev:windows"
},
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@mdi/font": "^7.4.47",
"@mdi/js": "^7.4.47",
"@types/resize-observer-browser": "^0.1.11",
"concurrently": "^9.2.1",
"fs-extra": "^11.3.2",
"gl-matrix": "^3.4.4",
"json-cyclic": "1.0.2",
"@babel/plugin-proposal-class-properties": "^7.16.0",
"@mdi/font": "^6.5.95",
"@mdi/js": "^6.4.95",
"@types/resize-observer-browser": "^0.1.6",
"concurrently": "^5.3.0",
"fs-extra": "^7.0.1",
"gl-matrix": "^3.4.3",
"json-cyclic": "0.0.3",
"lodash": "^4.17.21",
"mdi-vue": "^3.0.13",
"typescript": "^5.9.3",
"vanilla-jsoneditor": "^3.10.0",
"vue": "^3.5.25",
"mdi-vue": "^3.0.11",
"typescript": "^4.4.4",
"vanilla-jsoneditor": "^3.3.0",
"vue": "^3.2.21",
"vue-style-loader": "^4.1.3",
"vuex": "^4.1.0",
"vuex": "^4.0.2",
"vuex-webextensions": "^1.3.3",
"webextension-polyfill": "^0.12.0"
},
"devDependencies": {
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@tailwindcss/postcss": "^4.1.17",
"@types/chrome": "0.1.31",
"@types/core-js": "^2.5.8",
"@types/es6-promise": "^3.3.2",
"@types/firefox": "0.0.34",
"@types/lodash": "^4.17.21",
"@types/node": "^24.10.1",
"@vue/cli": "^5.0.9",
"@vue/cli-plugin-typescript": "^5.0.9",
"@vue/compiler-sfc": "^3.5.25",
"archiver": "^7.0.1",
"babel-loader": "^10.0.0",
"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.0",
"@types/chrome": "0.0.240",
"@types/core-js": "^2.5.5",
"@types/es6-promise": "^3.3.0",
"@types/firefox": "0.0.31",
"@types/lodash": "^4.14.176",
"@types/node": "^14.17.32",
"@vue/cli": "^4.5.15",
"@vue/cli-plugin-typescript": "^4.5.15",
"@vue/compiler-sfc": "^3.2.21",
"archiver": "^3.0.0",
"babel-loader": "^8.2.3",
"babel-preset-es2020": "^1.0.2",
"copy-webpack-plugin": "^13.0.1",
"cross-env": "^10.1.0",
"css-loader": "^7.1.2",
"ejs": "^3.1.10",
"file-loader": "^6.2.0",
"mini-css-extract-plugin": "^2.9.4",
"postcss": "^8.5.6",
"postcss-import": "^16.1.1",
"postcss-nested": "^7.0.2",
"postcss-preset-env": "^10.4.0",
"postcss-strip-inline-comments": "^0.1.5",
"raw-loader": "^4.0.2",
"copy-webpack-plugin": "^4.5.3",
"cross-env": "^5.2.0",
"css-loader": "^0.28.11",
"ejs": "^2.7.4",
"file-loader": "^1.1.11",
"mini-css-extract-plugin": "^0.4.4",
"node-sass": "^4.14.1",
"resolve-url-loader": "^5.0.0",
"tailwindcss": "^4.1.17",
"ts-loader": "^9.5.4",
"tsconfig-paths": "^4.2.0",
"sass-loader": "^7.1.0",
"ts-loader": "^8.3.0",
"vue-cli-plugin-vue-next": "~0.1.4",
"vue-loader": "^17.4.2",
"vue-tsc": "^3.1.5",
"web-ext-types": "^3.2.1",
"webpack": "^5.103.0",
"webpack-cli": "^6.0.1",
"webpack-shell-plugin": "^0.5.0",
"webpack-shell-plugin-next": "^2.3.3"
"vue-loader": "^16.8.2",
"web-ext-types": "^2.3.0",
"webpack": "^4.46.0",
"webpack-chrome-extension-reloader": "^0.8.3",
"webpack-cli": "^3.3.12",
"webpack-shell-plugin": "^0.5.0"
}
}

View File

@ -1,7 +0,0 @@
module.exports = {
plugins: [
require('@tailwindcss/postcss'),
require('postcss-nested'),
require('autoprefixer'),
],
};

View File

@ -688,9 +688,23 @@ export default {
flex-direction: row;
align-content: center;
align-items: center;
// width: 100%;
// height: 100%;
color: #fff;
// .context-item {
// font-size: .95rem;
// padding: 1rem 1.6rem;
// background-color: rgba(0, 0, 0, 0.5);
// backdrop-filter: blur(16px) saturate(120%);
// white-space: nowrap;
// }
// .spawn-container {
// border: 1px solid white;
// }
}
.extension-status-messages {
@ -702,6 +716,9 @@ export default {
font-size: 0.9rem;
// width: 112.25%;
// transform: translate(-12.5%, 12.5%) scale(0.75);
> * {
margin-top: 0.5rem;
margin-bottom: 0.5rem;

View File

@ -125,6 +125,7 @@ import BrowserDetect from '../ext/conf/BrowserDetect';
import CommsClient, {CommsOrigin} from '../ext/lib/comms/CommsClient';
import Settings from '../ext/lib/settings/Settings';
import EventBus from '../ext/lib/EventBus';
import {ChromeShittinessMitigations as CSM} from '../common/js/ChromeShittinessMitigations';
import SupportLevelIndicator from '@csui/src/components/SupportLevelIndicator.vue'
import { LogAggregator } from '@src/ext/lib/logging/LogAggregator';
import { ComponentLogger } from '@src/ext/lib/logging/ComponentLogger';
@ -338,13 +339,12 @@ export default {
</script>
<style lang="scss">
/*
// @import 'res/css/uwui-base.scss';
@import 'res/css/colors.scss';
@import 'res/css/font/overpass.css';
@import 'res/css/font/overpass-mono.css';
@import 'res/css/common.scss';
@import './src/res-common/_variables';
*/
.header {
background-color: rgb(90, 28, 13);
@ -352,6 +352,10 @@ export default {
color: #fff;
padding: 8px;
// display: flex;
// flex-direction: row;
// justify-content: space-between;
border-bottom: 1px dotted #fa6;
@ -525,6 +529,7 @@ export default {
padding: 1rem;
font-size: 1.25rem;
// height: rem;
min-height: 3rem;
border-bottom: 1px solid rgba(128, 128, 128, 0.5);
@ -557,6 +562,9 @@ export default {
opacity: 0.9;
color: #eee;
// .label {
// color: rgb(239, 192, 152);
// }
}
}
}

View File

@ -1,8 +1,9 @@
/* @import "colors.scss"; */
/*
@import "colors.scss";
@import "fonts.scss";
@import "flex.scss";
*/
// @import "~/@mdi/font/css/materialdesignicons.css";
// @import "form.scss";
* {
box-sizing: border-box;
@ -254,6 +255,10 @@ small {
button,
.button {
/*display: inline-block;*/
// padding-top: 8px;
// padding-bottom: 3px;
//padding-left: 5px;
//padding-right: 5px;
border: 1px solid rgb(39, 39, 39);
margin-top: 3px;
margin-bottom: 3px;
@ -397,8 +402,10 @@ button,
}
.experimental::after {
// content: "Experimental";
content: "";
color: #ffde12;
// background-color: #1f1f1f;
display: inline-block;
font-size: .75em;
font-variant: small-caps;
@ -409,7 +416,9 @@ button,
}
.experimental:hover::after {
content: "Ⓔ Experimental";
// content: "";
color: #ffde12;
// background-color: #1f1f1f;
display: inline-block;
font-size: .75em;
font-variant: small-caps;
@ -422,6 +431,7 @@ button,
.sticky-bottom {
width: 100%;
position: sticky;
// position: fixed;
bottom: 0px;
background-color: rgba($background-primary, 0.7);
}

View File

@ -1,14 +1,12 @@
/* @import 'flex.scss';
@import 'flex.scss';
@import 'colors.scss';
@import 'common.scss';
*/
* {
font-family: 'Overpass';
}
.uw-ultrawidify-container-root {
/*
// here's the defaults:
// all: initial;
// * {
@ -20,7 +18,6 @@
// all: declaration overrides everything.)
// we put our UI _over_ site's player:
*/
z-index: 999999;
position: absolute;
width: 100%;
@ -29,10 +26,10 @@
left: 0;
background-color: none transparent !important;
/* Ensure we're display:block */
// Ensure we're display:block
display: block;
/* we are click-through by default: */
// we are click-through by default:
pointer-events: none;
}

View File

@ -25,17 +25,112 @@
</div>
</div>
<SettingsWindowContent
:tabs="tabs"
:selectedTab="selectedTab"
<div class="tab-main flex flex-row">
<div class="tab-row flex flex-col grow-0 shrink-0">
<div
v-for="tab of tabs"
:key="tab.id"
>
<div
v-if="!tab.hidden"
class="tab"
:class="{
'active': tab.id === selectedTab,
'highlight-tab': tab.highlight,
}"
@click="selectTab(tab.id)"
>
<div class="icon-container">
<mdicon
v-if="tab.icon"
:name="tab.icon"
:size="32"
/>
</div>
<div class="label">
{{tab.label}}
</div>
</div>
</div>
</div>
<div class="content flex flex-col">
<!-- autodetection warning -->
<div class="warning-area">
<div
v-if="statusFlags.hasDrm"
class="warning-box"
>
<div class="icon-container">
<mdicon name="alert" :size="32" />
</div>
<div>
This site is blocking automatic aspect ratio detection. You will have to adjust aspect ratio manually.<br/>
<a>Learn more ...</a>
</div>
</div>
</div>
<div class="flex flex-row panel-content">
<!-- Panel section -->
<!-- <VideoSettings
v-if="selectedTab === 'videoSettings'"
:settings="settings"
:siteSettings="siteSettings"
:eventBus="eventBus"
:site="site"
></VideoSettings> -->
<PlayerDetectionPanel
v-if="selectedTab === 'playerDetection'"
:siteSettings="siteSettings"
:eventBus="eventBus"
:site="site"
>
</PlayerDetectionPanel>
<PlayerUiSettings
v-if="selectedTab === 'playerUiSettings'"
:settings="settings"
:siteSettings="siteSettings"
:eventBus="eventBus"
>
</PlayerUiSettings>
<BaseExtensionSettings
v-if="selectedTab === 'extensionSettings'"
:settings="settings"
:siteSettings="siteSettings"
:site="site"
:enableSettingsEditor="true"
></BaseExtensionSettings>
<AutodetectionSettingsPanel
v-if="selectedTab === 'autodetectionSettings'"
:settings="settings"
:siteSettings="siteSettings"
:eventBus="eventBus"
:site="site"
>
</AutodetectionSettingsPanel>
<DebugPanel
v-if="selectedTab === 'debugging'"
:settings="settings"
:eventBus="eventBus"
:logger="logger"
:in-player="inPlayer"
:site="site"
@selectTab="selectTab"
@preventClose="setPreventClose"
></SettingsWindowContent>
></DebugPanel>
<ChangelogPanel
v-if="selectedTab === 'changelog'"
:settings="settings"
></ChangelogPanel>
<AboutPanel
v-if="selectedTab === 'about'"
>
</AboutPanel>
<!-- <ResetBackupPanel
v-if="selectedTab === 'resetBackup'"
:settings="settings"
>
</ResetBackupPanel> -->
</div>
</div>
</div>
</div>
</template>
<script>
@ -70,6 +165,19 @@ export default {
statusFlags: {
hasDrm: undefined,
},
tabs: [
// {id: 'videoSettings', label: 'Video settings', icon: 'crop'},
{id: 'extensionSettings', label: 'Site and Extension options', icon: 'cogs' },
{id: 'playerUiSettings', label: 'UI and keyboard', icon: 'movie-cog-outline' },
{id: 'playerDetection', label: 'Player detection', icon: 'television-play'},
{id: 'autodetectionSettings', label: 'Autodetection options', icon: 'auto-fix'},
// {id: 'advancedOptions', label: 'Advanced options', icon: 'cogs' },
{id: 'changelog', label: 'What\'s new', icon: 'alert-decagram' },
{id: 'about', label: 'About', icon: 'information-outline'},
{id: 'debugging', label: 'Debugging', icon: 'bug-outline', hidden: true},
],
selectedTab: 'extensionSettings',
BrowserDetect: BrowserDetect,
preventClose: false,
siteSettings: null,
@ -141,12 +249,19 @@ export default {
}
</script>
<style lang="scss" scoped>
/*@import '../res/css/uwui-base.scss';
@import '../res/css/uwui-base.scss';
@import '../res/css/colors.scss';
@import '../res/css/font/overpass.css';
@import '../res/css/font/overpass-mono.css';
@import '../res/css/common.scss';
@import '../src/res-common/_variables';*/
@import '../src/res-common/_variables';
// .relative-wrapper {
// position: relative;
// width: 100%;
// height: 100%;
// }
.tab-row {
@ -301,6 +416,10 @@ export default {
&.highlight-tab {
opacity: 0.9;
color: #eee;
// .label {
// color: rgb(239, 192, 152);
// }
}
}
}

View File

@ -81,6 +81,9 @@ export default {
<style lang="scss" scoped module>
.alignment-box {
aspect-ratio: 1;
// width: 100%;
// min-width: 40px;
// max-width: 80px;
display: grid;
grid-template-columns: repeat(3, 1fr);

View File

@ -338,6 +338,11 @@ export default {
<style lang="scss" src="@csui/src/res-common/common.scss" scoped module></style>
<style lang="scss" scoped module>
@import '../res-common/variables';
// .aard-settings-group {
// max-width: 69rem;
// }
.performance-graph-container {
position: relative;
@ -356,7 +361,7 @@ export default {
width: 100%;
height: 100%;
/* graph is 100 ms wide */
// graph is 100 ms wide
.time-budget {
height: 100%;
display: inline-block;
@ -368,12 +373,15 @@ export default {
&.hz144 {
width: 6.9%;
// background-color: rgba(143, 143, 235, 0.47);
}
&.hz120 {
width: 1.39%;
// background-color: rgba(108, 108, 211, 0.441);
}
&.hz60 {
width: 8.33%;
// background-color: rgba(78, 78, 182, 0.327);
border-right: 2px solid rgb(96, 96, 227);
&::after {
content: '60fps';
@ -392,6 +400,7 @@ export default {
}
&.hz30 {
width: 16.67%;
// background-color: rgba(56, 56, 151, 0.308);
border-right: 2px solid #fb772a;
&::after {
@ -411,6 +420,7 @@ export default {
}
&.hz24 {
width: 8.33%;
// background-color: rgba(37, 37, 118, 0.269);
}
}

View File

@ -14,12 +14,7 @@
</div>
<div class="flex flex-row">
<small>
<span style="text-transform: uppercase; font-size: 0.9rem">
Enable extension: <span style="font-size: 0.9rem;" :style="getSiteEnabledColor('@global', 'enable')">{{ getSiteEnabledModes('@global', 'enable') }}</span>
{{getSiteEnabledModes('@global', 'enable') === 'disabled'}}
<span v-if="getSiteEnabledModes('@global', 'enable') === 'disabled'" class="text-[0.9em]">&nbsp;(except on whitelisted sites)</span>"
</span>&nbsp;
<br/>
<span style="text-transform: uppercase; font-size: 0.9rem">Enable extension: <span style="font-size: 0.9rem;" :style="getSiteEnabledColor('@global', 'enable')">{{ getSiteEnabledModes('@global', 'enable') }}</span></span>&nbsp;<br/>
Autodetection: <span :style="getSiteEnabledColor('@global', 'enableAard')"><small>{{ getSiteEnabledModes('@global', 'enableAard') }}</small></span>;&nbsp;
Keyboard shortcuts: <span :style="getSiteEnabledColor('@global', 'enableKeyboard')"><small>{{ getSiteEnabledModes('@global', 'enableKeyboard') }}</small></span>;
In-player UI: <span :style="getSiteEnabledColor('@global', 'enableUI')"><small>{{ getSiteEnabledModes('@global', 'enableUI') }}</small></span>;

View File

@ -417,7 +417,13 @@ export default({
}
})
</script>
<style lang="scss" src="../../res/css/flex.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped module></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped module></style>
<style lang="scss" scoped>
@import "@csui/res/css/colors.scss";
p {
font-size: 1rem;
}
@ -507,6 +513,8 @@ p {
.element-tree {
.element-row {
// display: flex;
// flex-direction: row;
margin: 0.5rem;
.status {
@ -561,6 +569,13 @@ p {
}
}
// .css-fixes {
// // display: flex;
// // flex-direction: row;
// // flex-wrap: wrap;
// // align-items: flex-start;
// // justify-content:flex-start;
// }
.css-line {
display: inline-block;
flex-grow: 0;

View File

@ -82,6 +82,7 @@ export default {
width: 100%;
height: 100%;
// background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: saturate(50%) brightness(50%) blur(1rem);
z-index: 99999;

View File

@ -174,6 +174,7 @@ export default {
<style lang="scss" src="@csui/src/res-common/common.scss" scoped></style>
<style lang="scss" scoped>
@import "../res-common/variables";
.center-text {
text-align: center;

View File

@ -79,6 +79,7 @@ export default {
width: 100%;
height: 100%;
// background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: saturate(50%) brightness(50%) blur(1rem);
z-index: 99999;

View File

@ -191,6 +191,7 @@ button, .button {
max-width: 100%;
color: #fff;
// border: 0px solid transparent;
border: 1px solid rgba(255, 171, 102, 0.42);
padding: 0.5rem 1rem 0.25rem;
@ -207,6 +208,7 @@ button, .button {
margin-top: 0.25rem;
margin-bottom: 0.75rem;
margin-left: 5rem;
// width: 100%;
box-sizing:border-box;
&.error {
@ -239,6 +241,9 @@ button, .button {
padding-right: 0.25rem;
}
// .input-slider {
// width: 480px;
// }
.warning-lite {
padding-right: 16px;
padding-bottom: 16px;

View File

@ -156,7 +156,7 @@ const ExtensionConf: SettingsInterface = {
aard: {
aardType: 'auto',
useLegacy: false,
useLegacy: true,
polling: {
runInBackgroundTabs: AardPollingOptions.Reduced,

View File

@ -107,9 +107,9 @@ class Logger {
}
static syncConfig(callback: (x) => void) {
chrome.storage.onChanged.addListener( (changes: any, area: string) => {
chrome.storage.onChanged.addListener( (changes, area) => {
if (changes.uwLogger) {
const newLoggerConf = JSON.parse(changes.uwLogger.newValue as any)
const newLoggerConf = JSON.parse(changes.uwLogger.newValue)
if (process.env.CHANNEL === 'dev') {
console.info('Logger settings reloaded. New conf:', newLoggerConf);
}
@ -172,7 +172,7 @@ class Logger {
this.temp_disable = false;
this.stopTime = this.conf.timeout ? performance.now() + (this.conf.timeout * 1000) : undefined;
chrome.storage.onChanged.addListener( (changes: any, area: any) => {
chrome.storage.onChanged.addListener( (changes, area) => {
if (process.env.CHANNEL === 'dev') {
if (!changes.uwLogger) {
// console.info('[Logger::<storage/on change> No new logger settings!');
@ -195,7 +195,7 @@ class Logger {
});
}
storageChangeListener(changes: any, area: any) {
storageChangeListener(changes, area) {
if (!changes.uwLogger) {
console.info('We dont have any logging settings, not processing frther');
return;

View File

@ -42,7 +42,7 @@ export class LogAggregator {
history: any[];
static async getConfig() {
let ret: any = await chrome.storage.local.get(STORAGE_LOG_SETTINGS_KEY);
let ret = await chrome.storage.local.get(STORAGE_LOG_SETTINGS_KEY);
if (process.env.CHANNEL === 'dev') {
try {
@ -73,7 +73,7 @@ export class LogAggregator {
}
static syncConfig(callback: (x) => void) {
chrome.storage.onChanged.addListener( (changes: any, area) => {
chrome.storage.onChanged.addListener( (changes, area) => {
if (changes.uwLogger) {
const newLoggerConf = JSON.parse(changes.uwLogger.newValue)
if (process.env.CHANNEL === 'dev') {

View File

@ -61,7 +61,7 @@ class Settings {
constructor(options: SettingsOptions) {
// Options: activeSettings, updateCallback, logger
this.logger = options.logAggregator && new ComponentLogger(options.logAggregator, 'Settings', {styles: SETTINGS_LOGGER_STYLES}) || undefined;
this.logger = options.logAggregator && new ComponentLogger(options.logAggregator, 'Settings', {styles: SETTINGS_LOGGER_STYLES}) || undefined;;
this.onSettingsChanged = options.onSettingsChanged;
this.afterSettingsSaved = options.afterSettingsSaved;
this.active = options.activeSettings ?? undefined;
@ -73,24 +73,6 @@ class Settings {
chrome.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)});
}
updateOptions(options: SettingsOptions) {
if (options.logAggregator) {
this.logger = options.logAggregator && new ComponentLogger(options.logAggregator, 'Settings', {styles: SETTINGS_LOGGER_STYLES})
}
if (options.onSettingsChanged) {
this.onSettingsChanged = options.onSettingsChanged;
}
if (options.afterSettingsSaved) {
this.afterSettingsSaved = options.afterSettingsSaved;
}
if (options.activeSettings) {
this.active = options.activeSettings;
}
}
private storageChangeListener(changes, area) {
if (!changes.uwSettings) {
return;

View File

@ -1,5 +1,7 @@
import { settings } from 'cluster'
import SettingsInterface from '@src/common/interfaces/SettingsInterface';
export interface SettingsSnapshot {
isAutomatic?: boolean;
isProtected?: boolean;
@ -90,7 +92,7 @@ export class SettingsSnapshotManager {
async listSnapshots(): Promise<SettingsSnapshot[]> {
const ret = await chrome.storage.local.get('uwSettings-snapshots');
try {
const json = JSON.parse(ret['uwSettings-snapshots'] as string) as SettingsSnapshot[];
const json = JSON.parse(ret['uwSettings-snapshots']) as SettingsSnapshot[];
return json;
} catch (e) {
return [] as SettingsSnapshot[];

View File

@ -1,33 +0,0 @@
import SettingsInterface from '../../../common/interfaces/SettingsInterface';
/**
* Upgrading from v5 to v6 introduced major changes in the settings structure. As such, upgrading from v5 to v6
* start with default settings for v6, and then manually migrate some of the more important settings from v5.
* @param currentSettings
* @param defaultSettings
* @returns
*/
export function migrate5to6(currentSettings: SettingsInterface & any, defaultSettings: SettingsInterface): SettingsInterface {
// if settings do not have a version, return default settings.
// This is okay, because currentSettings.version should be undefined only if settings are either very ancient
// or settings were never modified at all.
if (!currentSettings.version) {
return defaultSettings;
}
// do not perform migration if the version is already 6 or higher
const [majorVersion] = currentSettings.version.split('.');
if (+majorVersion >= 6) {
return currentSettings;
}
const newSettings = JSON.parse(JSON.stringify(defaultSettings));
// TODO: migrate the following:
// * when migrating from v5, default extension mode should be enabled for all sites
// unless it was changed by the user
// * keyboard shortcuts should be migrated
// * attempt to migrate custom site settings for users's custom sites
return newSettings;
}

View File

@ -1,56 +0,0 @@
export class BasicNotifications {
bodyElement: HTMLBodyElement;
basicNotificationContainer: HTMLDivElement;
isIframe = window.self !== window.top;
activeNotifications = [];
notificationCount = 0;
constructor() {
this.bodyElement = document.body as HTMLBodyElement;
this.createNotificationContainer();
}
private createNotificationContainer() {
this.basicNotificationContainer = document.createElement('div')
this.basicNotificationContainer.style.all = 'initial';
this.basicNotificationContainer.style.fontSize = '16px';
this.basicNotificationContainer.style.fontFamily = "'Overpass', sans-serif";
this.basicNotificationContainer.style.position = 'fixed';
this.basicNotificationContainer.style.left = '0';
this.basicNotificationContainer.style.top = '0';
this.basicNotificationContainer.style.width = '100vw';
this.basicNotificationContainer.style.height = '100vh';
this.basicNotificationContainer.style.zIndex = '999999';
this.basicNotificationContainer.style.pointerEvents = 'none';
this.basicNotificationContainer.style.display = 'flex';
this.basicNotificationContainer.style.flexDirection = 'column';
this.bodyElement.appendChild(this.basicNotificationContainer);
}
showNotification(notification: {title: string, body?: string, expire?: number | false}) {
if (this.isIframe) {
return;
}
const notificationDiv = document.createElement('div');
const notificationId = this.notificationCount++;
notificationDiv.innerHTML = `
<div style="
display: flex; flex-direction: row;
color: #fff;
background-color: #fff;
backdrop-filter: blur(1rem) brightness(0.33);
margin: 1rem;
">
<div style="display: flex; flex-direction: column;">
<div><b style="text: #fff">${notification.title}</b></div>
${notification.body ? `<div style="color: #ccc">${document.body}</div>` : ''}
</div>
<div>
<button id="ultrawidify-uw-notification-id-${notificationId}">Dismiss</button>
</div>
</div>
`;
}
}

View File

@ -1,4 +1,5 @@
import ExtensionMode from '@src/common/enums/ExtensionMode.enum';
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
import { EventBusConnector } from '../EventBus';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading: UI");

View File

@ -130,7 +130,7 @@ class PlayerData {
function: (data) => this.markElement(data)
}],
'update-player': [{
function: () => this._requestTick(true)
function: () => this.updatePlayer()
}],
'set-run-level': [{
function: (runLevel) => this.setRunLevel(runLevel)
@ -207,7 +207,7 @@ class PlayerData {
// do the rest
this.invalid = false;
this.updatePlayer(this.getPlayer());
this.updatePlayer();
this.isTooSmall = this.checkIfTooSmall();;
this.initEventBus();
@ -331,7 +331,7 @@ class PlayerData {
private reloadPlayerDataConfig(siteConfUpdate) {
// this.siteSettings = siteConfUpdate;
// this.updatePlayer();
this.updatePlayer();
this.periodicallyRefreshPlayerElement = false;
try {
@ -471,7 +471,7 @@ class PlayerData {
commitChanges:
{
if (changes.fullScreen) this.isFullscreen = fs;
if (changes.player) this.updatePlayer(newPlayerCandidate);
if (changes.player) this.element = newPlayerCandidate;
if (changes.theater) this.isTheaterMode = isTheaterMode;
if (changes.dimensions) this.dimensions = currentDimensions;
@ -560,18 +560,13 @@ class PlayerData {
return this.elementStack;
}
/**
* Updates current player element
* @param options
* @returns
*/
updatePlayer(newPlayer: HTMLElement, options?: {verbose?: boolean}) {
updatePlayer(options?: {verbose?: boolean, newElement?: HTMLElement}) {
const newPlayer = options?.newElement ?? this.getPlayer(options);
if (newPlayer === this.element || !newPlayer) {
return;
}
if (this.element) {
this.observer?.unobserve(this.element);
}
// clean up and re-initialize UI
this.ui?.destroy();
@ -592,6 +587,8 @@ class PlayerData {
siteSettings: this.siteSettings,
}
);
this.requestTick();
}
/**
@ -673,7 +670,7 @@ class PlayerData {
// console.log('updated site settings:', this.siteSettings.data.playerAutoConfig);
// this.videoData.settings.saveWithoutReload();
this.updatePlayer(elementStack[nextIndex].element);
this.updatePlayer({newElement: elementStack[nextIndex].element});
}
}
@ -717,6 +714,7 @@ class PlayerData {
// Don't bother thinking about this too much, as any "thinking" was quickly
// corrected by bugs caused by various edge cases.
if (
equalish(elementData.height, videoHeight, 5)
|| equalish(elementData.width, videoWidth, 5)
) {
@ -787,23 +785,17 @@ class PlayerData {
// BUT WAIT! THERE'S MORE
// Some sites (youtube) can re-parent elements, causing current player element to vanish from DOM
// Which means we need to set up an observer that will re-acquire the player when that happens.
// TODO: Ideally, observer should request a tick
if (bestCandidate) {
const observer = new MutationObserver(
(mutations) => {
const observer = new MutationObserver( (mutations) => {
mutations.forEach((mutation) => {
mutation.removedNodes.forEach((node) => {
if (node === bestCandidate.element) {
observer.disconnect();
this.updatePlayer(
this.getPlayer()
);
this.updatePlayer();
}
})
});
}
);
});
observer.observe(bestCandidate.element.parentNode, {childList: true});
}
@ -874,7 +866,7 @@ class PlayerData {
*/
private handlePlayerTreeRequest() {
// this populates this.elementStack fully
// this.updatePlayer({verbose: true});
this.updatePlayer({verbose: true});
this.eventBus.send('uw-config-broadcast', {type: 'player-tree', config: JSON.parse(JSON.stringify(this.elementStack))});
}
@ -913,8 +905,7 @@ class PlayerData {
}
forceRefreshPlayerElement() {
// this.updatePlayer();
this._requestTick(true);
this.updatePlayer();
}
}

View File

@ -343,6 +343,10 @@ class Resizer {
}
this.pendingAr = null;
if (! this.videoData.player.dimensions) {
this.videoData.player.updatePlayer();
}
// If no aspect ratio is applied AND if no stretch mode is active,
// we disable our CSS in order to prevent breaking websites by default,
// without any human interaction

View File

@ -173,7 +173,7 @@ class Scaler {
// ONCE before throwing an error
if( (! this.conf.player.dimensions) || this.conf.player.dimensions.width === 0 || this.conf.player.dimensions.height === 0 ){
this.logger.error('calculateCrop', "ERROR — no (or invalid) this.conf.player.dimensions:",this.conf.player.dimensions);
(this.conf.player as any)._requestTick(true);
this.conf.player.updatePlayer();
if( (! this.conf.player.dimensions) || this.conf.player.dimensions.width === 0 || this.conf.player.dimensions.height === 0 ){
return {error: "this.conf.player.dimensions_error"};

View File

@ -61,7 +61,7 @@
</template>
<script>
import BrowserDetect from '../../../ext/conf/BrowserDetect';
import BrowserDetect from '../../ext/conf/BrowserDetect';
import { LogAggregator } from '@src/ext/lib/logging/LogAggregator';
import { ComponentLogger } from '@src/ext/lib/logging/ComponentLogger';
@ -187,6 +187,8 @@ html, body {
.tabitem {
font-variant: normal;
// font-size: 0.69em;
// margin-left: 16px;
border-left: transparent 3px solid;
padding-left: 12px;
margin-left: -10px;
@ -215,6 +217,8 @@ html, body {
}
.popup {
// max-width: 780px;
// width: 800px;
height: 600px;
}
</style>

View File

@ -180,6 +180,8 @@ body {
.tabitem {
font-variant: normal;
// font-size: 0.69em;
// margin-left: 16px;
border-left: transparent 3px solid;
padding-left: 12px;
margin-left: -10px;
@ -208,6 +210,8 @@ body {
}
.popup {
// max-width: 780px;
// width: 800px;
height: 600px;
}
</style>

View File

@ -1,77 +0,0 @@
@import "tailwindcss";
@import "tailwindcss/utilities";
@theme {
--color-primary: #ff872c;
--color-primary-50: #ffddbe;
--color-primary-100: #ffcda0;
--color-primary-200: #ffbd83;
--color-primary-300: #ffac66;
--color-primary-400: #ff9a4a;
--color-primary-500: #ff872c;
--color-primary-600: #de7622;
--color-primary-700: #be6518;
--color-primary-800: #9f540e;
--color-primary-900: #824406;
--color-primary-950: #663400;
}
@import './ui/res/styles/forms.css';
@import './ui/res/styles/fonts.css';
@import './ui/res/styles/buttons.css';
html, body {
@apply bg-stone-950 text-stone-300;
font-size: 16px;
}
@media (max-width: 999px) {
html, body {
font-size: 12px;
}
}
@layer base {
body {
@apply font-sans text-stone-300;
}
h1, h2, h3, h4, h5, h6 {
@apply font-mono;
}
h2 {
@apply text-[1.75em];
}
h3 {
@apply text-primary-300 text-[1.25em] mt-4 border-b-1 border-b-stone-700;
}
a {
@apply text-primary-400;
&:hover {
@apply text-primary-300;
}
}
p {
@apply mt-1 mb-2;
}
p, li {
@apply leading-tight;
}
ul {
@apply bg-stone-900/50 px-4 py-2 mt-1 mb-2;
li {
@apply ml-4 list-disc marker:text-primary-400 mt-1 mb-2;
}
}
small, strike {
@apply opacity-50;
}
}

View File

@ -31,11 +31,6 @@
]
},
"options_ui": {
"page": "ui/pages/settings/index.html",
"open_in_tab": true
},
"web_accessible_resources": [
{
"resources": [
@ -47,7 +42,7 @@
"res/icons/*",
"res/img/*",
"csui/*",
"ui/*"
"install/*"
],
"matches": [
"*://*/*"

View File

@ -1,473 +0,0 @@
<template>
<div class="flex flex-col relative w-full">
<h2 class="text-[1.75em]">Automatic Aspect Ratio Detection</h2>
<p class="text-stone-500">Ultrawidify can attempt to automatically detect aspect ratio of videos playing on the page.</p>
<p class="text-stone-500">However, autodetection will not work on every website, as some websites make it impossible for autodetection to work.</p>
<div class="settings-segment">
<h3 class="text-primary-300 text-[1.25em] mt-4 border-b-1 border-b-stone-700">Early stop</h3>
<div class="field">
<div class="label">Stop autodetection after first detection:</div>
<div class="select">
<select v-model="aardSettings.autoDisable.onFirstChange" @change="settings.saveWithoutReload">
<option :value="true">Yes</option>
<option :value="false">No</option>
</select>
</div>
</div>
<div class="field" :class="{disabled: aardSettings.autoDisable.onFirstChange}">
<div class="label">Stop autodetection if aspect ratio doesn't change for some time:</div>
<div class="select">
<select v-model="aardSettings.autoDisable.ifNotChanged" @change="settings.saveWithoutReload">
<option :value="true">Yes</option>
<option :value="false">No</option>
</select>
</div>
</div>
<div v-if="aardSettings.autoDisable.ifNotChanged" class="field">
<div class="label">Stop autodetection if aspect ratio doesn't change for:</div>
<div class="range-input">
<input
type="range"
:value="Math.log(aardSettings.autoDisable.ifNotChangedTimeout / 10)"
@change="setAutoDisableTimeout($event.target.value, 10)"
min="2.3"
max="9.3"
step="0.01"
/>
<input
:value="aardSettings.autoDisable.ifNotChangedTimeout / 1000"
@change="setAutoDisableTimeout($event.target.value, 1000)"
class="input"
type="text"
>
<div class="unit">s</div>
</div>
</div>
</div>
<div class="settings-segment">
<h3 class="text-primary-300 text-[1.25em] mt-4 border-b-1 border-b-stone-700">Subtitle handling</h3>
<div class="warning-area">
Subtitle detection is experimental to high heaven.
</div>
<div class="field">
<div class="label">Autodetection mode (requires page reload)</div>
<div class="select">
<select v-model="settings.active.aard.useLegacy" @change="aardLegacyModeChanged">
<option :value="true">Legacy</option>
<option :value="false">Experimental</option>
</select>
</div>
</div>
<div class="field">
<div class="label">Subtitle handling strategy:</div>
<div class="select">
<select v-model="aardSettings.subtitles.subtitleCropMode" @change="settings.saveWithoutReload">
<!-- <option :value="AardSubtitleCropMode.DisableScan">Do not detect subtitles</option> -->
<option :value="AardSubtitleCropMode.ResetAR">Do not crop while subtitles are on screen</option>
<option :value="AardSubtitleCropMode.ResetAndDisable">Stop autodetection if subtitles are detected</option>
<option :value="AardSubtitleCropMode.CropSubtitles">Always crop subtitles</option>
</select>
</div>
</div>
<div v-if="aardSettings.subtitles.subtitleCropMode === AardSubtitleCropMode.ResetAR" class="field">
<div class="label">Wait before resuming detection:</div>
<div class="range-input">
<input
type="range"
:value="Math.log(aardSettings.subtitles.resumeAfter / 10)"
@change="setSubtitleTimeout($event.target.value, 10)"
min="2.3"
max="9.3"
step="0.01"
/>
<input
:value="aardSettings.subtitles.resumeAfter / 1000"
@change="setSubtitleTimeout($event.target.value, 1000)"
class="input"
type="text"
>
<div class="unit">s</div>
</div>
</div>
</div>
<div class="settings-segment">
<h3 class="text-primary-300 text-[1.25em] mt-4 border-b-1 border-b-stone-700">Detection tolerances</h3>
<div class="field">
<div class="label">Maximum allowed vertical video misalignment:</div>
<div class="input">
<input v-model="aardSettings.allowedMisaligned" />
</div>
<div class="hint">
Ultrawidify detects letterbox only if video is vertically centered. Some people are bad at vertically
centering the content, though. This is how off-center the video can be before autodetection will
refuse to crop it (% of total height).
</div>
</div>
</div>
<!-- AARD performance metrics -->
<div class="settings-segment">
<h3 class="text-primary-300 text-[1.25em] mt-4 border-b-1 border-b-stone-700">Performance options</h3>
<div class="field">
<div class="label">Autodetection frequency (time between samples)</div>
<div class="range-input">
<input
type="range"
:value="Math.log(aardSettings.timers.playing)"
@change="setArCheckFrequency($event.target.value)"
min="2.3"
max="9.3"
step="0.01"
/>
<input
v-model="aardSettings.timers.playing"
@change="setArCheckFrequency($event.target.value)"
class="input"
type="text"
>
<div class="unit">ms</div>
</div>
</div>
<div class="field">
<div class="label">Reduced autodetection frequency</div>
<div class="range-input">
<input
type="range"
:value="Math.log(aardSettings.timers.playingReduced)"
@change="setArCheckFrequency($event.target.value, 'playingReduced')"
min="2.3"
max="9.3"
step="0.01"
/>
<input
v-model="aardSettings.timers.playingReduced"
@change="setArCheckFrequency($event.target.value, 'playingReduced')"
class="input"
type="text"
>
<div class="unit">ms</div>
</div>
</div>
<div class="field">
<div class="label">Poll for aspect ratio changes in background tabs:</div>
<div class="select">
<select v-model="aardSettings.polling.runInBackgroundTabs" @change="settings.saveWithoutReload">
<option :value="AardPollingOptions.No">Never</option>
<option :value="AardPollingOptions.Reduced">Use reduced polling rate</option>
<option :value="AardPollingOptions.Full">Use normal polling rate</option>
</select>
</div>
</div>
<div v-if="aardSettings.polling.runInBackgroundTabs === AardPollingOptions.Full" class="hint warn">
Using normal polling rate in background tabs is NOT recommended.
</div>
<div class="field">
<div class="label">Poll for aspect ratio changes in small players:</div>
<div class="select">
<select v-model="aardSettings.polling.runOnSmallVideos" @change="settings.saveWithoutReload">
<option :value="AardPollingOptions.No">Never</option>
<option :value="AardPollingOptions.Reduced">Use reduced polling rate</option>
<option :value="AardPollingOptions.Full">Use normal polling rate</option>
</select>
</div>
</div>
<div v-if="aardSettings.polling.runOnSmallVideos === AardPollingOptions.Full" class="hint warn">
Using normal polling rate on small videos is NOT recommended.
</div>
<div class="field">
<div class="label">Autodetection canvas type:</div>
<div class="select">
<select v-model="aardSettings.aardType" @change="settings.saveWithoutReload">
<option value="auto">Automatic</option>
<option value="webgl">WebGL only</option>
<option value="legacy">Legacy / fallback</option>
</select>
</div>
</div>
</div>
<div v-if="settings.active.ui.devMode" class="settings-segment w-full">
<h3 class="text-primary-300 text-[1.25em] mt-4 border-b-1 border-b-stone-700">Debug options</h3>
<div class="flex flex-row w-full gap-2">
<div class="grow flex flex-col gap-2">
<div>
<button v-if="eventBus" @click="eventBus?.sendToTunnel('aard-enable-debug', true)">Show debug overlay</button>
</div>
<div class="field">
<div class="label">Show debug overlay on startup</div>
<div class="checkbox">
<input
type="checkbox"
v-model="settings.active.ui.dev.aardDebugOverlay.showOnStartup"
@change="settings.saveWithoutReload"
>
</div>
</div>
</div>
<div class="grow">
<JsonEditor
v-model="settingsJson"
>
</JsonEditor>
<button @click="saveDebugUiSettings">Save debug UI settings</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import BrowserDetect from '../../../ext/conf/BrowserDetect';
import JsonEditor from '@csui/src/components/JsonEditor';
import {AardPollingOptions} from '@src/ext/lib/aard/enums/aard-polling-options.enum';
import {AardSubtitleCropMode} from '@src/ext/lib/aard/enums/aard-subtitle-crop-mode.enum';
import { defineComponent } from 'vue';
export default defineComponent({
components: {
JsonEditor
},
mixins: [
],
props: [
'settings',
'frame',
'eventBus',
'site'
],
data() {
return {
exec: null,
performanceData: {},
graphRefreshInterval: undefined,
settingsJson: {},
// enums n stuff
AardPollingOptions,
AardSubtitleCropMode
}
},
computed: {
aardSettings() {
return this.settings.active.aard.useLegacy ? this.settings.active.aardLegacy : this.settings.active.aard;
}
},
created() {
this.eventBus?.subscribe(
'uw-config-broadcast',
{
source: this,
function: (config) => this.handleConfigBroadcast(config)
}
);
},
mounted() {
this.eventBus?.sendToTunnel('get-aard-timing');
// this.graphRefreshInterval = setInterval(() => this.eventBus.sendToTunnel('get-aard-timing'), 500);
this.resetSettingsEditor();
},
destroyed() {
this.eventBus?.unsubscribeAll(this);
clearInterval(this.graphRefreshInterval);
},
methods: {
async openOptionsPage() {
BrowserDetect.runtime.openOptionsPage();
},
setArCheckFrequency(event, timer) {
this.aardSettings.timers[timer ?? 'playing'] = Math.floor(Math.pow(Math.E, event));
this.settings.saveWithoutReload();
},
setAutoDisableTimeout(event, multiplier) {
this.aardSettings.autoDisable.ifNotChangedTimeout = Math.floor(Math.pow(Math.E, event)) * multiplier;
this.settings.saveWithoutReload();
},
setSubtitleTimeout(event, multiplier) {
this.aardSettings.subtitles.resumeAfter = Math.floor(Math.pow(Math.E, event)) * multiplier;
this.settings.saveWithoutReload();
},
refreshGraph() {
this.eventBus.sendToTunnel('get-aard-timing');
},
handleConfigBroadcast(data) {
if (data.type === 'aard-performance-data') {
this.performanceData = data.performanceData;
this.$nextTick( () => this.$forceUpdate() );
}
},
resetSettingsEditor() {
this.settingsJson = JSON.parse(JSON.stringify(this.settings?.active.ui.dev.aardDebugOverlay ?? {}));
},
saveDebugUiSettings() {
this.settings.active.ui.dev.aardDebugOverlay = JSON.parse(JSON.stringify(this.settingsJson));
this.settings.saveWithoutReload();
},
aardLegacyModeChanged() {
this.settings.save();
}
},
});
</script>
<style lang="postcss" scoped module>
.performance-graph-container {
position: relative;
width: 100%;
height: 8rem;
padding: 1rem;
.performance-graph {
border: 1px solid #fa6;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
/* graph is 100 ms wide */
.time-budget {
height: 100%;
display: inline-block;
position: relative;
box-sizing: border-box;
z-index: 100;
&.hz144 {
width: 6.9%;
}
&.hz120 {
width: 1.39%;
}
&.hz60 {
width: 8.33%;
border-right: 2px solid rgb(96, 96, 227);
&::after {
content: '60fps';
position: absolute;
top: 0;
right: -2px;
border-right: 2px solid rgb(96, 96, 227);
transform: translateY(-100%);
font-size: 0.6rem;
padding-right: 0.25rem;
text-transform: small-caps;
}
}
&.hz30 {
width: 16.67%;
border-right: 2px solid #fb772a;
&::after {
content: '30fps';
position: absolute;
top: 0;
right: -2px;
border-right: 2px solid #fb772a;
transform: translateY(-100%);
font-size: 0.6rem;
padding-right: 0.25rem;
text-transform: small-caps;
}
}
&.hz24 {
width: 8.33%;
}
}
.bar-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
> div {
height: 2.5rem;
}
.stats {
background-color: rgba(0, 0, 0, 0.544);
font-size: 0.75rem;
font-family: 'Overpass Mono';
z-index: 11010;
span {
font-size: 0.75rem;
z-index: 11011;
&::before {
content: '';
display: inline-block;
width: 0.75rem;
height: 0.75rem;
margin-top: 0.25rem;
margin-right: 0.5rem;
}
&.draw::before {
background-color: #fb772a;
}
&.draw-blackframe::before {
background-color: #e70c0c;
}
&.processing::before {
background-color: rgb(176, 167, 239);
}
}
}
.bar {
width: 100%;
height: 0.69rem;
background-color: #000;
overflow: hidden;
* {
display: inline-block;
height: 100%;
}
.draw {
background-color: #fb772a;
}
.draw-blackframe {
background-color: #e70c0c;
}
.processing {
background-color: rgb(176, 167, 239);
}
}
}
}
}
</style>

View File

@ -1,103 +0,0 @@
<template>
<div>
<div>
<h1>{{loggerPanel.title}}<small><br/>{{loggerPanel.subtitle}}</small></h1>
<div>Logger configuration:</div>
<JsonEditor
v-model="lastSettings"
></JsonEditor>
<div class="flex flex-row flex-end">
<div class="button" @click="getLoggerSettings()">
Revert
</div>
<div class="button button-primary" @click="saveLoggerSettings()">
Save
</div>
<div class="button button-primary" @click="startLogging()">
Save and start logging
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { LogAggregator, BLANK_LOGGER_CONFIG } from '@src/ext/lib/logging/LogAggregator';
import JsonEditor from '@csui/src/components/JsonEditor';
import { defineComponent } from 'vue';
export default defineComponent({
components: {
JsonEditor
},
data() {
return {
loggerPanelRotation: [{
title: 'DEFORESTATOR 5000',
subtitle: 'Iron Legion\'s finest logging tool'
},{
title: 'Astinus',
subtitle: 'Ultrawidify logging tool'
}, {
title: 'Tracer',
subtitle: "Maybe I'll be Tracer — I'm already printing stack traces."
}, {
title: 'Grûmsh',
subtitle: "He who watches (all the things printed to the console)."
}, {
title: 'Situation Room/The Council',
subtitle: 'We will always be watching.'
}, {
title: 'Isengard Land Management Services',
subtitle: "#log4saruman"
}, {
title: 'Saw Hero',
subtitle: 'Ultrawidify logging tool'
}],
loggerPanel: {
title: 'whoopsie daisy',
subtitle: 'you broke the header choosing script',
pasteConfMode: false,
},
lastSettings: undefined,
parsedSettings: undefined,
currentSettings: undefined
};
},
props: [
'settings',
'eventBus',
],
created() {
this.loggerPanel = {
...this.loggerPanelRotation[Math.floor(Math.random() * this.loggerPanelRotation.length)],
pasteConfMode: false,
};
this.getLoggerSettings();
},
methods: {
loadDefaultConfig() {
this.lastSettings = JSON.parse(JSON.stringify(BLANK_LOGGER_CONFIG));
},
async getLoggerSettings() {
this.lastSettings = await LogAggregator.getConfig() || BLANK_LOGGER_CONFIG;
},
async saveLoggerSettings() {
console.log('Saving logger settings', this.lastSettings);
await LogAggregator.saveConfig({...this.lastSettings});
console.log('[ok] logger settings saved');
},
async startLogging(){
this.logStringified = undefined;
await LogAggregator.saveConfig({...this.lastSettings});
window.location.reload();
},
}
});
</script>

View File

@ -1,35 +0,0 @@
<template>
<div class="flex flex-col w-full h-full gap-2">
<h2 class="text-[1.75em]">About Ultrawidify</h2>
<ExtensionInfoContent :settings="settings"></ExtensionInfoContent>
</div>
</template>
<script lang="ts">
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import { defineComponent } from 'vue';
import ExtensionInfoContent from './segments/ExtensionInfoContent.vue';
export default defineComponent({
components: {
ExtensionInfoContent,
},
props: [
'settings'
],
data() {
return {
BrowserDetect: BrowserDetect,
// reminder webextension-polyfill doesn't seem to work in vue!
addonVersion: BrowserDetect.firefox ? chrome.runtime.getManifest().version : chrome.runtime.getManifest().version,
addonSource: BrowserDetect.processEnvVersion,
mailtoLink: '',
redditLink: '',
showEasterEgg: false,
}
},
mounted() {
this.settings.active.whatsNewChecked = true;
this.settings.saveWithoutReload();
}
});
</script>

View File

@ -1,80 +0,0 @@
<template>
<div class="flex flex-col w-full h-full gap-2">
<h2 class="text-[1.75em]">What's new</h2>
<div class="flex flex-row md:flex-col gap-4">
<div>
<h3>Changelog</h3>
<ChangelogContent :settings="settings"></ChangelogContent>
</div>
<div>
<h3>Report problems</h3>
<ExtensionInfoContent :settings="settings"></ExtensionInfoContent>
</div>
</div>
</div>
</template>
<script lang="ts">
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import { defineComponent } from 'vue';
import ChangelogContent from './segments/ChangelogContent.vue';
import ExtensionInfoContent from './segments/ExtensionInfoContent.vue';
export default defineComponent({
components: {
ChangelogContent,
ExtensionInfoContent,
},
props: [
'settings'
],
data() {
return {
BrowserDetect: BrowserDetect,
// reminder webextension-polyfill doesn't seem to work in vue!
addonVersion: BrowserDetect.firefox ? chrome.runtime.getManifest().version : chrome.runtime.getManifest().version,
addonSource: BrowserDetect.processEnvVersion,
mailtoLink: '',
redditLink: '',
showEasterEgg: false,
}
},
mounted() {
this.settings.active.whatsNewChecked = true;
this.settings.saveWithoutReload();
}
});
</script>
<style lang="scss" scoped>
.flex {
display: flex;
}
.flex-row {
flex-direction: row;
}
.grow {
flex-grow: 1;
}
p, li {
font-size: 1rem;
}
small {
opacity: 0.5;
font-size: 0.8em;
}
a {
color: #fa6;
}
.text-center {
text-align: center;
}
.donate {
margin: 1rem;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
background-color: #fa6;
color: #000;
}
</style>

View File

@ -1,59 +0,0 @@
<template>
<p class="mb-4">Full changelog for older versions <a href="https://github.com/tamius-han/ultrawidify/blob/master/CHANGELOG.md" target="_blank">is available here</a>.</p>
<p>Changes in version <b class="font-semibold text-primary-400">6.4.0</b>:</p>
<ul>
<li>In-player UI can now appear in full-screen for websites that put players into top layer (allegedly).</li>
<li>Embedded sites now inherit settings of the parent frame. <small>However, this hasn't been tested for all edge cases and may contain bugs.</small></li>
<li>Added validation to custom aspect ratio entry menu. Corrected parsing of aspect ratios given in the X:Y format, even though aspect ratios should be ideally given as a single number.</li>
<li>Autodetection can be set to stop after first aspect ratio detection, or after a period of no changes.</li>
<li>
There is a new, experiental mode for autodetection. At this point, it can be manually enabled in the autodetection settings. It will become the default option in 2026.<br/>
If you enable experimental mode, please consider reporting problems <a href="https://github.com/tamius-han/ultrawidify/issues/291" target="_blank">in this thread</a> on Github.
</li>
<li>
Autodetection can now detect hardcoded subtitles. You may change how extension handles subtitles in the settings.
Use of new experimental autodetection is required for subtitle handling options to work.
</li>
<li>
Keyboard shortcut settings have been split from UI settings, and are now presented in list form on the settings page.
</li>
<li>
Re-design of settings page.
</li>
</ul>
</template>
<script>
import BrowserDetect from '@src/ext/conf/BrowserDetect';
export default({
props: [
'settings'
],
data() {
return {
BrowserDetect: BrowserDetect,
// reminder webextension-polyfill doesn't seem to work in vue!
addonVersion: BrowserDetect.firefox ? chrome.runtime.getManifest().version : chrome.runtime.getManifest().version,
addonSource: BrowserDetect.processEnvVersion,
mailtoLink: '',
redditLink: '',
showEasterEgg: false,
}
},
mounted() {
this.settings.active.whatsNewChecked = true;
this.settings.saveWithoutReload();
}
});
</script>
<style lang="scss" scoped>
.donate {
margin: 1rem;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
background-color: #fa6;
color: #000;
}
</style>

View File

@ -1,93 +0,0 @@
<template>
<div class="flex flex-col ml-12 p-4">
<div class="text-[1.75em] font-thin text-primary-400">Ultrawidify</div>
<div class="-mb-1 text-stone-500 text-[0.9em]">Version {{addonVersion}}/{{addonSource}} ({{addonChannel}})</div>
<div class="text-stone-500 text-[0.9em]">Source code can be viewed on <a target="_blank" src="https://github.com/tamius-han/ultrawidify">github</a>.</div>
</div>
<p>
Please report <strike>undocumented features</strike> bugs using one of the following options (in order of preference):
</p>
<ul>
<li> <a target="_blank" href="https://github.com/tamius-han/ultrawidify/issues"><b>Github (preferred)</b></a><br/></li>
<li>Email: <a target="_blank" :href="mailtoLink">tamius.han@gmail.com</a></li>
</ul>
<p>
When reporting bugs, please include the following info:
</p>
<div class="bg-stone-900 border-l border-l-primary-500 px-4 py-2 font-mono pre">
Ultrawidify version: <span class="text-primary-400">{{addonVersion}}</span><br/>
Install source: <span class="text-primary-400">{{addonSource ?? '[fill out]'}}</span><br/>
OS: [fill out]
</div>
<p>
In addition, please include a description of your issue, which should ideally include:
</p>
<ul>
<li>What doesn't work</li>
<li>Where it happens (which website)</li>
<li>Steps to reproduce the issue</li>
<li>What you expected to happen</li>
<li>What actually happened</li>
</ul>
<p>
Optionally, you can also include screenshots in your report (though if screenshots contain any personal info,
such as names or addresses, you should redact that).
</p>
<h3>Thank you monies</h3>
<p>
If you think I deserve money for the work I did up to this point, you can bankroll my caffeine addiction.
</p>
<p class="text-center">
<a class="donate" href="https://www.paypal.com/paypalme/tamius" target="_blank">Donate on Paypal</a>
</p>
<p>
If you're into that, you can also stoke my ego over at
<a href="https://instagram.com/shaman_urkog" target="_blank">instagram</a> or
<a href="https://bsky.app/profile/tamius-han.bsky.social" target="_blank">bluesky</a>
by liking the stuff I put up on there <small>mostly things related to mini-painting</small>.
</p>
</template>
<script lang="ts">
import BrowserDetect from '@src/ext/conf/BrowserDetect';
export default({
props: [
'settings'
],
data() {
return {
BrowserDetect: BrowserDetect,
// reminder webextension-polyfill doesn't seem to work in vue!
addonVersion: chrome.runtime.getManifest().version,
addonSource: BrowserDetect.processEnvBrowser,
addonChannel: BrowserDetect.processEnvChannel,
mailtoLink: '',
redditLink: '',
showEasterEgg: false,
}
},
mounted() {
this.addonSource = BrowserDetect.processEnvBrowser;
this.addonChannel = BrowserDetect.processEnvChannel;
this.settings.active.whatsNewChecked = true;
this.settings.saveWithoutReload();
}
});
</script>
<style lang="scss" scoped>
.donate {
margin: 1rem;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
background-color: #fa6;
color: #000;
}
</style>

View File

@ -1,310 +0,0 @@
<template>
<div class="flex flex-row w-full h-full">
<div class="flex flex-col w-full">
<!-- TAB ROW -->
<div class="flex flex-row">
<div
class="tab"
:class="{'active': tab === 'siteSettings'}"
@click="setTab('siteSettings')"
>
Current site<br/>
<small>{{ site }}</small>
</div>
<div
v-if="hosts"
class="tab"
:class="{'active': tab === 'embeddedSites'}"
@click="setTab(tab = 'embeddedSites')"
>
Embedded content ({{hosts?.length}} {{hosts?.length === 1 ? 'site' : 'sites'}})
</div>
<div
class="tab"
:class="{'active': tab === 'otherSites'}"
@click="setTab(tab = 'otherSites')"
>
Defaults & other sites
</div>
</div>
<template v-if="tab === 'siteSettings' && siteSettings">
<SiteExtensionSettings
v-if="settings"
:settings="settings"
:siteSettings="siteSettings"
:isDefaultConfiguration="false"
></SiteExtensionSettings>
</template>
<template v-if="hosts && tab === 'embeddedSites' && globalSettings">
<FrameSiteSettings
v-if="settings"
:parentHost="site"
:hosts="hosts"
:settings="settings"
></FrameSiteSettings>
</template>
<template v-if="tab === 'otherSites'">
<OtherSiteSettings
v-if="settings"
:settings="settings"
>
</OtherSiteSettings>
</template>
<!-- Reset options -->
<div class="flex flex-col" style="margin-top: 2rem">
<h2>Reset and backup</h2>
<p><small>Settings import must be done from in-player UI.</small></p>
<div class="flex flex-row w-full">
<UploadJsonFileButton
class="flex-grow"
@importedJson="handleImportedSettings"
@error="handleSettingsImportError"
>
Import settings
</UploadJsonFileButton>
<Popup
v-if="importSettingDialogConfig.visible"
title="Overwrite existing settings?"
message="Importing settings from a file will overwrite existing settings. Continue?"
confirmButtonText="Import settings"
cancelButtonText="Cancel"
@onConfirm="importSettingDialogConfig.confirm"
@onCancel="importSettingDialogConfig.reject"
>
</Popup>
<button class="flex-grow" @click="exportSettings()">Export settings</button>
</div>
<div></div>
<ConfirmButton
dialogType="danger"
@onConfirmed="resetSettings"
>
Reset settings
</ConfirmButton>
<div v-if="enableSettingsEditor" class="field">
<div class="label">Show developer options</div>
<input
type="checkbox"
v-model="settings.active.ui.devMode"
@change="settings.saveWithoutReload"
>
</div>
</div>
</div>
<div v-if="enableSettingsEditor && settings.active.ui.devMode" class="h-full grow">
<h2>Settings editor</h2>
<div class="flex flex-row w-full">
<div class="flex flex-row items-center">
<div>Enable save button:</div>
<input v-model="allowSettingsEditing" type="checkbox">
</div>
<div class="grow">
</div>
<div>
<div v-if="editorSaveFinished">Settings saved ...</div>
<button v-else
class="danger"
:class="{'disabled': !allowSettingsEditing}"
:disabled="!allowSettingsEditing"
@click="() => saveSettingsChanges()"
>
Save
</button>
</div>
<button @click="resetSettingsEditor">
Cancel
</button>
</div>
<div>
<JsonEditor
v-model="settingsJson"
>
</JsonEditor>
</div>
<h2>Settings snapshots</h2>
<div class="flex flex-col">
<div v-for="(snapshot, index) of settingsSnapshots" :key="snapshot.createdAt">
<small>{{new Date(snapshot.createdAt).toISOString()}}</small>
<div class="flex flex-row">
<div class="grow">
{{snapshot.name}}
</div>
<div v-if="settings.isAutomatic">(auto)</div>
<div v-if="settings.isAutomatic && settings.isProtected">(protected)</div>
<div v-if="settings.default">(default)</div>
</div>
<div>
<button @click="() => markDefaultSnapshot(index)"><template v-if="settings.isDefault">Revoke default</template><template v-else>Make default</template></button>
<button v-if="settings.isAutomatic" @click="() => toggleSnapshotProtection(index)">Toggle protection</button>
<button @click="() => deleteSnapshot(index)">Delete snapshot</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import SiteExtensionSettings from '@components/ExtensionSettings/Panels/SiteExtensionSettings.vue';
import FrameSiteSettings from '@components/ExtensionSettings/Panels/FrameSiteSettings.vue';
import OtherSiteSettings from '@components/ExtensionSettings/Panels/OtherSiteSettings.vue';
import Popup from '@csui/src/components/Popup';
import ConfirmButton from '@csui/src/components/ConfirmButton';
import UploadJsonFileButton from '@csui/src/components/UploadJsonFileButton';
import JsonEditor from '@csui/src/components/JsonEditor';
export default {
components: {
SiteExtensionSettings,
OtherSiteSettings,
Popup,
ConfirmButton,
UploadJsonFileButton,
JsonEditor,
FrameSiteSettings,
},
mixins: [],
props: [
'settings',
'site',
'enableSettingsEditor',
'hosts',
],
data() {
return {
tab: 'siteSettings',
importSettingDialogConfig: {visible: false},
allowSettingsEditing: false,
editorSaveFinished: false,
settingsJson: {},
settingsSnapshots: []
}
},
computed: {
globalSettings() {
return this.settings?.getSiteSettings('@global') ?? null;
},
siteSettings() {
if (this.site) {
return this.settings?.getSiteSettings({site: this.site}) ?? null;
}
return null;
},
},
mounted() {
this.resetSettingsEditor();
this.loadSettingsSnapshots();
},
methods: {
setTab(tab) {
this.tab = tab;
},
//#region settings management
/**
* Exports extension settings into a json file.
*/
exportSettings() {
const settingBlob = new Blob(
[ JSON.stringify(this.settings.active, null, 2) ],
{ type: "application/json"}
);
const url = window.URL.createObjectURL(settingBlob);
const a = document.createElement("a");
a.href = url;
a.download = "ultrawidify-settings.json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
handleImportedSettings(newSettings) {
this.importSettingDialogConfig = {
visible: true,
confirm: () => {
this.settings.active = newSettings;
this.settings.saveWithoutReload();
this.importSettingDialogConfig = {visible: false};
this.resetSettingsEditor();
},
reject: () => {
this.importSettingDialogConfig = {visible: false};
}
}
},
handleSettingsImportError(error) {
console.error('Error importing settings:', error);
},
/**
* Resets settings to default
*/
resetSettings() {
this.settings.active = JSON.parse(JSON.stringify(this.settings.default));
this.settings.saveWithoutReload();
this.resetSettingsEditor();
},
async saveSettingsChanges() {
if (this.allowSettingsEditing) {
const currentVersion = this.settings.active?.version;
this.settings.active = this.settingsJson;
if (currentVersion !== this.settingsJson.version) {
await this.settings.save({forcePreserveVersion: true});
return;
} else {
await this.settings.saveWithoutReload();
}
this.resetSettingsEditor();
this.editorSaveFinished = true;
setTimeout(() => {
this.editorSaveFinished = false;
}, 3000);
}
},
resetSettingsEditor() {
this.settingsJson = JSON.parse(JSON.stringify(this.settings?.active ?? {}));
},
//#endregion
//#region settings snapshot management
async loadSettingsSnapshots() {
this.settingsSnapshots = await this.settings.snapshotManager.listSnapshots();
},
async markDefaultSnapshot(index) {
await this.settings.snapshotManager.setDefaultSnapshot(index, !this.settingsSnapshots[index].isDefault);
await this.loadSettingsSnapshots();
this.settings.active.dev.loadFromSnapshot = this.settingsSnapshots[index].isDefault;
this.saveSettingsChanges();
},
async toggleSnapshotProtection(index) {
await this.settings.snapshotManager.setSnapshotAsProtected(index, !this.settingsSnapshots[index].isProtected);
await this.loadSettingsSnapshots();
},
async deleteSnapshot(index) {
await this.settings.snapshotManager.deleteSnapshot(index);
await this.loadSettingsSnapshots();
},
}
//#endregion
}
</script>

View File

@ -1,109 +0,0 @@
<template>
<div class="">
<template v-if="!selectedSite">
<div style="margin: 1rem 0rem" class="w-full">
<div class="flex flex-row items-baseline">
<div style="margin-right: 1rem">Search for site:</div>
<div class="input flex-grow">
<input v-model="siteFilter" />
</div>
</div>
</div>
<div v-for="host of hosts" :key="host" @click="selectedSite = host" class="flex flex-col container pointer hoverable" style="margin-top: 4px; padding: 0.5rem 1rem;">
<SiteListItem
:parentHost="parentHost"
:host="host"
:settings="settings"
></SiteListItem>
</div>
</template>
<template v-if="selectedSite">
<div class="flex flex-row container" style="align-items: center; color: #dedede; margin-top: 1rem;">
<div @click="selectedSite = null" class="pointer button-hover" style=" font-size: 2em; padding: 0.5rem; margin-right: 1em;">
</div>
<div>
Editing {{ selectedSite === '@global' ? 'default settings' : selectedSite }}
</div>
</div>
<div>
<SiteExtensionSettings
v-if="selectedSiteSettings"
:settings="settings"
:siteSettings="selectedSiteSettings"
:isDefaultConfiguration="selectedSite === '@global'"
></SiteExtensionSettings>
</div>
</template>
</div>
</template>
<script>
import SiteExtensionSettings from './SiteExtensionSettings.vue';
import SiteListItem from './SiteListItem.vue';
export default {
components: {
SiteExtensionSettings,
SiteListItem,
},
props: [
'settings',
'parentHost',
'hosts',
],
data() {
return {
selectedSite: null,
siteFilter: '',
filteredSites: []
}
},
computed: {
sites() {
if (!this.settings?.active?.sites) {
return [];
} else {
const sites = [];
for (const siteKey in this.settings.active.sites) {
if (!siteKey.startsWith('@') && (!this.siteFilter.trim() || siteKey.includes(this.siteFilter))) {
sites.push({
key: siteKey,
...this.settings.active.sites[siteKey]
})
}
};
sites.sort((a, b) => {
const cmpa = a.key.replace('www.', '');
const cmpb = b.key.replace('www.', '');
return cmpa < cmpb ? -1 : 1;
});
return sites;
}
},
selectedSiteSettings() {
return this.settings?.getSiteSettings({site: this.selectedSite}) ?? null;
}
},
methods: {
}
}
</script>
<style lang="scss" src="../../../../res/css/flex.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/panels.scss" scoped></style>
<style lang="scss" src="@csui/src/res-common/common.scss" scoped></style>
<style lang="scss" scoped>
.hoverable {
border: 1px solid #333;
&:hover {
border: 1px solid #fa6;
color: rgb(255, 231, 212);
background-color: rgba(#fa6, 0.125);
}
}
</style>

View File

@ -1,160 +0,0 @@
<template>
<div class="flex flex-row gap-8">
<div v-if="!selectedSite || !isCompact">
<div
class="flex flex-col cursor-pointer mt-1 px-4 py-2 bg-black border-1 border-stone-500"
:class="{'opacity-50 hover:opacity-100': selectedSite && selectedSite !== '@global' }"
@click="selectedSite = '@global'"
>
<div class="flex flex-row">
<div class="flex-grow pointer">
<b style="color: #fa6">Default settings</b>
</div>
<div>Edit</div>
</div>
<div class="flex flex-row">
<small>
<span style="text-transform: uppercase; font-size: 0.9rem">
Enable extension: <span style="font-size: 0.9rem;" :style="getSiteEnabledColor('@global', 'enable')">{{ getSiteEnabledModes('@global', 'enable') }}</span>
<span v-if="getSiteEnabledModes('@global', 'enable') === 'disabled'" class="text-[0.75em] lowercase text-stone-400">&nbsp;(except on whitelisted sites)</span>
</span>&nbsp;<br/>
Autodetection: <span :style="getSiteEnabledColor('@global', 'enableAard')"><small>{{ getSiteEnabledModes('@global', 'enableAard') }}</small></span>;&nbsp;
Keyboard shortcuts: <span :style="getSiteEnabledColor('@global', 'enableKeyboard')"><small>{{ getSiteEnabledModes('@global', 'enableKeyboard') }}</small></span>;
In-player UI: <span :style="getSiteEnabledColor('@global', 'enableUI')"><small>{{ getSiteEnabledModes('@global', 'enableUI') }}</small></span>;
</small>
</div>
</div>
<div style="margin-top: 1rem; margin-bottom: 1rem;">
<div class="border border-blue-500 text-blue-500 bg-slate-950 px-4 py-2" style="float: none">
<b>NOTE:</b> Sites not on this list use default extension settings.
</div>
</div>
<div class="w-full text-center" style="margin-bottom: -1.25rem">
<b>Other sites</b>
</div>
<div style="margin: 1rem 0rem" class="w-full">
<div class="flex flex-row items-baseline">
<div style="margin-right: 1rem">Search for site:</div>
<div class="input flex-grow">
<input v-model="siteFilter" />
</div>
</div>
</div>
<div class="flex flex-col gap-2">
<div v-for="site of sites" :key="site.key" :class="{'opacity-50 hover:opacity-100': selectedSite && selectedSite !== site.key }">
<SiteListItem
:host="site.key"
:settings="settings"
:isActive="selectedSite === site.key"
@edit="selectedSite = site.key"
></SiteListItem>
</div>
</div>
</div>
<div v-if="selectedSite">
<div class="flex flex-row text-[1.5em] items-center gap-2">
<div @click="selectedSite = null" class="cursor-pointer bg-black rounded-full px-2">
</div>
<div>
Editing {{ selectedSite === '@global' ? 'default settings' : selectedSite }}
</div>
</div>
<div class="border-t-1 border-t-stone-800 mt-4 pt-2">
<SiteExtensionSettings
v-if="selectedSiteSettings"
:settings="settings"
:siteSettings="selectedSiteSettings"
:isDefaultConfiguration="selectedSite === '@global'"
></SiteExtensionSettings>
</div>
</div>
</div>
</template>
<script>
import ExtensionMode from '@src/common/enums/ExtensionMode.enum';
import SiteExtensionSettings from './SiteExtensionSettings.vue';
import SiteListItem from './SiteListItem.vue';
export default {
data() {
return {
selectedSite: null,
siteFilter: '',
filteredSites: []
}
},
props: [
'settings',
'isCompact',
],
components: {
SiteExtensionSettings,
SiteListItem,
},
computed: {
sites() {
if (!this.settings?.active?.sites) {
return [];
} else {
const sites = [];
for (const siteKey in this.settings.active.sites) {
if (!siteKey.startsWith('@') && (!this.siteFilter.trim() || siteKey.includes(this.siteFilter))) {
sites.push({
key: siteKey,
...this.settings.active.sites[siteKey]
})
}
};
sites.sort((a, b) => {
const cmpa = a.key.replace('www.', '');
const cmpb = b.key.replace('www.', '');
return cmpa < cmpb ? -1 : 1;
});
return sites;
}
},
selectedSiteSettings() {
return this.settings?.getSiteSettings({site: this.selectedSite}) ?? null;
}
},
methods: {
getSiteTypeColor(siteType) {
switch (siteType) {
case 'official': return 'color: #fa6';
case 'community': return 'color: rgb(114, 114, 218)';
case 'officially-disabled': return 'color: #f00';
case 'testing': return 'color: #d81';
default: return 'color: rgb(138, 65, 126)'
};
},
getSiteEnabledColor(site, component) {
const status = this.getSiteEnabledModes(site, component);
return status === 'disabled' ? 'color: #f00' : 'color: #1f8';
},
getSiteEnabledModes(site, component) {
if (this.settings?.getSiteSettings({site: site}).data[component]?.normal === ExtensionMode.Enabled) {
return 'always';
}
if (this.settings?.getSiteSettings({site: site}).data[component]?.theater === ExtensionMode.Enabled) {
return 'T + FS';
}
if (this.settings?.getSiteSettings({site: site}).data[component]?.fullscreen === ExtensionMode.Enabled) {
return 'fullscreen';
}
return 'disabled';
}
}
}
</script>
<style lang="postcss" scoped>
.container {
@apply: bg-[#000] px-4 py-2 border border-primary-500;
}
</style>

View File

@ -1,656 +0,0 @@
<template>
<div>
<!-- Enable extension -->
<div class="field">
<div class="label">
Enable <span class="color-emphasis">extension</span>
<span class="sub-label"><br/>under the following conditions:</span>
</div>
<div class="select">
<select
:value="simpleExtensionSettings.enable"
@click="setExtensionMode('enable', $event)"
>
<option
v-if="simpleExtensionSettings.enable === 'complex'"
value="complex"
>
(Site uses advanced settings)
</option>
<template v-if="isDefaultConfiguration">
<option value="disabled">
Disabled by default
</option>
</template>
<template v-else>
<option value="default">
Use default ({{simpleDefaultSettings.enable}})
</option>
<option value="disabled">
Never
</option>
</template>
<option value="fs">
Fullscreen only
</option>
<option value="theater">
Fullscreen and theater mode
</option>
<option value="enabled">
Always
</option>
</select>
</div>
</div>
<!-- The rest of the menu is disabled when extension is disabled -->
<div :class="{disabled: simpleEffectiveSettings.enable === 'disabled' && !isDefaultConfiguration}">
<!-- Enable AARD -->
<div class="field">
<div class="label">
Enable <span class="color-emphasis">automatic aspect ratio detection</span>
<span class="sub-label"><br/>under the following conditions:</span>
</div>
<div class="select">
<select
:value="simpleExtensionSettings.enableAard"
@click="setExtensionMode('enableAard', $event)"
>
<option
v-if="simpleExtensionSettings.enable === 'complex'"
value="complex"
>
(Site uses advanced settings)
</option>
<template v-if="isDefaultConfiguration">
<option value="disabled">
Disabled by default
</option>
</template>
<template v-else>
<option value="default">
Use default ({{simpleDefaultSettings.enableAard}})
</option>
<option value="disabled">
Never
</option>
</template>
<option value="fs">
Fullscreen only
</option>
<option value="theater">
Fullscreen and theater mode
</option>
<option value="enabled">
Always
</option>
</select>
</div>
</div>
<!-- Enable keyboard -->
<div class="field">
<div class="label">
Enable <span class="color-emphasis">keyboard shortcuts</span>
<span class="sub-label"><br/>under the following conditions:</span>
</div>
<div class="select">
<select
:value="simpleExtensionSettings.enableKeyboard"
@click="setExtensionMode('enableKeyboard', $event)"
>
<option
v-if="simpleExtensionSettings.enable === 'complex'"
value="complex"
>
(Site uses advanced settings)
</option>
<template v-if="isDefaultConfiguration">
<option value="disabled">
Disabled by default
</option>
</template>
<template v-else>
<option value="default">
Use default ({{simpleDefaultSettings.enableKeyboard}})
</option>
<option value="disabled">
Never
</option>
</template>
<option value="fs">
Fullscreen only
</option>
<option value="theater">
Fullscreen and theater mode
</option>
<option value="enabled">
Always
</option>
</select>
</div>
</div>
<!-- Enable UI -->
<div class="field">
<div class="label">
Enable <span class="color-emphasis">in-player UI</span>
<span class="sub-label"><br/>under the following conditions:</span>
</div>
<div class="select">
<select
:value="simpleExtensionSettings.enableUI"
@click="setExtensionMode('enableUI', $event)"
>
<template v-if="isDefaultConfiguration">
<option value="disabled">
Disabled by default
</option>
</template>
<template v-else>
<option value="default">
Use default ({{simpleDefaultSettings.enableUI}})
</option>
<option value="disabled">
Never
</option>
</template>
<option value="fs">
Fullscreen only
</option>
<option value="theater">
Always where possible
</option>
</select>
</div>
</div>
<div class="field">
<div class="label">
Use these settings for <span class="color-emphasis">embedded content</span>?
<!-- <span class="sub-label"><br/>under the following conditions:</span> -->
</div>
<div class="select">
<select
:value="siteDefaultForEmbedded"
@click="setSiteOption('applyToEmbeddedContent', $event)"
>
<option :value="EmbeddedContentSettingsOverridePolicy.Always">
Always(-ish)
</option>
<option :value="EmbeddedContentSettingsOverridePolicy.UseAsDefault">
Use as default
</option>
<option :value="EmbeddedContentSettingsOverridePolicy.Never">
Never
</option>
</select>
</div>
</div>
<div class="field">
<div class="label">
Force these settings <span class="color-emphasis">when embedded</span>?
<!-- <span class="sub-label"><br/>under the following conditions:</span> -->
</div>
<div class="select">
<select
:value="siteDefaultOverrideEmbedded"
@click="setSiteOption('overrideWhenEmbedded', $event)"
>
<option :value="true">
Yes
</option>
<option :value="false">
No
</option>
</select>
</div>
</div>
<!-- Default crop -->
<div class="field">
<div class="label">Default crop:</div>
<div class="select">
<select
:value="siteDefaultCrop"
@change="setOption('defaults.crop', $event)"
>
<option
v-if="!isDefaultConfiguration"
:value="JSON.stringify({useDefault: true})"
>
Use default ({{getCommandValue(settings?.active.commands.crop, siteSettings.data.defaults.crop)}})
</option>
<option
v-for="(command, index) of settings?.active.commands.crop"
:key="index"
:value="JSON.stringify(command.arguments)"
>
{{command.label}}
</option>
</select>
</div>
</div>
<div class="hint">This is how extension will crop video if/when autodetection is disabled. Pick 'Reset' option to keep aspect ratio as-is by default.</div>
<!-- Default stretch -->
<div class="field">
<div class="label">Default stretch:</div>
<div class="select">
<select
v-model="siteDefaultStretch"
@change="setOption('defaults.stretch', $event)"
>
<option
v-if="!isDefaultConfiguration"
:value="JSON.stringify({useDefault: true})"
>
Use default ({{getCommandValue(settings?.active.commands.stretch, siteSettings.data.defaults.stretch)}})
</option>
<option
v-for="(command, index) of settings?.active.commands.stretch"
:key="index"
:value="JSON.stringify(command.arguments)"
>
{{command.label}}
</option>
</select>
</div>
</div>
<!-- Default alignment -->
<div class="field">
<div class="label">Default alignment:</div>
<div class="select">
<select
v-model="siteDefaultAlignment"
@change="setOption('defaults.alignment', $event)"
>
<option
v-if="!isDefaultConfiguration"
:value="JSON.stringify({useDefault: true})"
>
Use default ({{getAlignmentLabel(siteSettings.data.defaults.alignment)}})
</option>
<option
v-for="(command, index) of alignmentOptions"
:key="index"
:value="JSON.stringify(command.arguments)"
>
{{command.label}}
</option>
</select>
</div>
</div>
<!-- Crop, et. al. Persistence -->
<div class="field">
<div class="label">Persist crop, stretch, and alignment between videos</div>
<div class="select">
<select
v-model="siteDefaultCropPersistence"
@click="setOption('persistCSA', $event)"
>
<option
v-if="!isDefaultConfiguration"
:value="CropModePersistence.Default"
>
Use default ({{defaultPersistanceLabel}})
</option>
<option :value="CropModePersistence.Disabled">Disabled</option>
<option :value="CropModePersistence.UntilPageReload">Until page reload</option>
<option :value="CropModePersistence.CurrentSession">Current session</option>
<option :value="CropModePersistence.Forever">Always persist</option>
</select>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import ExtensionMode from '@src/common/enums/ExtensionMode.enum';
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import CropModePersistence from '@src/common/enums/CropModePersistence.enum';
import EmbeddedContentSettingsOverridePolicy from '@src/common/enums/EmbeddedContentSettingsOverridePolicy.enum';
export default defineComponent({
data() {
return {
CropModePersistence: CropModePersistence,
ExtensionMode,
EmbeddedContentSettingsOverridePolicy,
alignmentOptions: [
{label: 'Top left', arguments: {x: VideoAlignmentType.Left, y: VideoAlignmentType.Top}},
{label: 'Top center', arguments: {x: VideoAlignmentType.Center, y: VideoAlignmentType.Top}},
{label: 'Top right', arguments: {x: VideoAlignmentType.Right, y: VideoAlignmentType.Top}},
{label: 'Left', arguments: {x: VideoAlignmentType.Left, y: VideoAlignmentType.Center}},
{label: 'Center', arguments: {x: VideoAlignmentType.Center, y: VideoAlignmentType.Center}},
{label: 'Right', arguments: {x: VideoAlignmentType.Right, y: VideoAlignmentType.Center}},
{label: 'Bottom left', arguments: {x: VideoAlignmentType.Left, y: VideoAlignmentType.Bottom}},
{label: 'Bottom center', arguments: {x: VideoAlignmentType.Center, y: VideoAlignmentType.Bottom}},
{label: 'Bottom right', arguments: {x: VideoAlignmentType.Right, y: VideoAlignmentType.Bottom}}
]
}
},
mixins: [
],
props: [
'settings',
'siteSettings',
'isDefaultConfiguration'
],
components: {
},
computed: {
simpleExtensionSettings() {
return {
enable: this.compileSimpleSettings('enable'),
enableAard: this.compileSimpleSettings('enableAard'),
enableKeyboard: this.compileSimpleSettings('enableKeyboard'),
enableUI: this.compileSimpleSettings('enableUI')
}
},
simpleEffectiveSettings() {
return {
enable: this.compileSimpleSettings('enable', 'site-effective'),
enableAard: this.compileSimpleSettings('enableAard', 'site-effective'),
enableKeyboard: this.compileSimpleSettings('enableKeyboard', 'site-effective'),
enableUI: this.compileSimpleSettings('enableUI', 'site-effective')
}
},
simpleDefaultSettings() {
return {
enable: this.getDefaultOptionLabel('enable'),
enableAard: this.getDefaultOptionLabel('enableAard'),
enableKeyboard: this.getDefaultOptionLabel('enableKeyboard'),
enableUI: this.getDefaultOptionLabel('enableUI')
};
},
siteDefaultForEmbedded() {
return this.siteSettings.raw?.applyToEmbeddedContent;
},
siteDefaultOverrideEmbedded() {
return this.siteSettings.raw?.overrideWhenEmbedded ?? false;
},
siteDefaultCrop() {
return this.siteSettings.raw?.defaults?.crop ? JSON.stringify(this.siteSettings.raw?.defaults?.crop) : JSON.stringify({useDefault: true});
},
siteDefaultStretch() {
return this.siteSettings.raw?.defaults?.stretch ? JSON.stringify(this.siteSettings.raw?.defaults?.stretch) : JSON.stringify({useDefault: true});
},
siteDefaultAlignment() {
return this.siteSettings.raw?.defaults?.alignment ? JSON.stringify(this.siteSettings.raw?.defaults?.alignment) : JSON.stringify({useDefault: true});
},
siteDefaultCropPersistence() {
return this.siteSettings.raw?.persistCSA ?? undefined;
},
defaultPersistanceLabel() {
switch (this.siteSettings.defaultSettings.persistCSA) {
case CropModePersistence.CurrentSession:
return 'current session';
case CropModePersistence.Disabled:
return 'disabled';
case CropModePersistence.UntilPageReload:
return 'until page reload';
case CropModePersistence.Forever:
return 'Always persist';
}
return '??';
}
},
mounted() {
this.forceRefreshPage();
},
methods: {
/**
* Compiles our extension settings into more user-friendly options
*/
compileSimpleSettings(component, getFor = 'site') {
let settingsData;
switch (getFor) {
case 'site':
settingsData = this.siteSettings?.raw;
break;
case 'site-effective':
settingsData = this.siteSettings?.data;
break;
case 'default':
settingsData = this.settings.active.sites['@global'];
break;
}
try {
if (
( settingsData?.[component]?.normal === ExtensionMode.Disabled || component === 'enableUI')
&& settingsData?.[component]?.theater === ExtensionMode.Disabled
&& settingsData?.[component]?.fullscreen === ExtensionMode.Disabled
) {
return 'disabled';
}
if (
( settingsData?.[component]?.normal === ExtensionMode.Default || component === 'enableUI')
&& settingsData?.[component]?.theater === ExtensionMode.Default
&& settingsData?.[component]?.fullscreen === ExtensionMode.Default
) {
// console.log(
// component, 'is set to default because:\n',
// `\nsettingsData[${component}].normal: ${settingsData?.[component]?.normal} || component is enableUI?`, component,
// `\nsettingsData[${component}].theater: ${settingsData?.[component]?.normal}`,
// `\nsettingsData[${component}].fullscreen: ${settingsData?.[component]?.normal}`,
// `\n\n(expected values:`, ExtensionMode.Default
// )
return 'default';
}
if (
( settingsData?.[component]?.normal === ExtensionMode.Disabled || component === 'enableUI')
&& settingsData?.[component]?.theater === ExtensionMode.Disabled
&& settingsData?.[component]?.fullscreen === ExtensionMode.Enabled
) {
return 'fs';
}
if (
( settingsData?.[component]?.normal === ExtensionMode.Disabled || component === 'enableUI')
&& settingsData?.[component]?.theater === ExtensionMode.Enabled
&& settingsData?.[component]?.fullscreen === ExtensionMode.Enabled
) {
return 'theater';
}
if (
( settingsData?.[component]?.normal === ExtensionMode.Enabled || component === 'enableUI')
&& settingsData?.[component]?.theater === ExtensionMode.Enabled
&& settingsData?.[component]?.fullscreen === ExtensionMode.Enabled
) {
return 'enabled';
}
return 'complex';
} catch (e) {
return 'loading';
}
},
getDefaultOptionLabel(component) {
const componentValue = this.compileSimpleSettings(component, 'default');
if (componentValue === 'loading') {
return componentValue;
}
if (component === 'enableUI') {
switch (componentValue) {
case 'fs':
return 'fullscreen only';
case 'enabled':
case 'theater':
return 'where possible';
case 'disabled':
return 'disabled';
}
}
switch (componentValue) {
case 'fs':
return 'Fullscreen only';
case 'theater':
return 'theater & FS';
case 'enabled':
return 'always';
case 'disabled':
return 'disabled';
case 'default':
return 'complex'
}
},
getCommandValue(availableCommands, command) {
for (const cmd of availableCommands) {
if (JSON.stringify(cmd.arguments) === JSON.stringify(command)) {
return cmd.label;
}
}
return 'Unknown command';
},
getAlignmentLabel(alignment) {
// in case default settings don't have this set
if (!alignment) {
return 'Center'
}
let x, y;
if (alignment.x === VideoAlignmentType.Center) {
x = 'center';
} else if (alignment.x === VideoAlignmentType.Left) {
x = 'left';
} else if (alignment.x === VideoAlignmentType.Right) {
x = 'right';
} else {
x = '??'
}
if (alignment.y === VideoAlignmentType.Center) {
y = 'center';
} else if (alignment.y === VideoAlignmentType.Bottom) {
y = 'bottom';
} else if (alignment.y === VideoAlignmentType.Top) {
y = 'top';
} else {
y = '???'
};
if (x === y) {
return x;
}
return `${y} ${x}`;
},
getOption(option) {
},
async setOption(option, $event) {
const value = $event.target.value;
let commandArguments;
// if argument is json, parse json. Otherwise, pass the value as-is
try {
commandArguments = value !== undefined ? JSON.parse(value) : undefined;
} catch(e) {
commandArguments = value;
}
if (commandArguments.useDefault) {
commandArguments = undefined;
}
await this.siteSettings.set(option, commandArguments, {reload: false});
// we also need to force re-compute all watchers, otherwise UI will lag behind
// actual state of settings until reload
this.forceRefreshPage();
},
forceRefreshPage() {
this._computedWatchers?.simpleExtensionSettings?.run();
this._computedWatchers?.simpleDefaultSettings?.run();
this._computedWatchers?.siteDefaultCrop?.run();
this._computedWatchers?.siteDefaultStretch?.run();
this._computedWatchers?.siteDefaultAlignment?.run();
this._computedWatchers?.siteDefaultCropPersistence?.run();
this._computedWatchers?.defaultPersistanceLabel?.run();
this.$nextTick( () => this.$forceUpdate());
},
setSiteOption(optionPath, event) {
const value = event.target.value;
this.siteSettings.set(optionPath, value);
},
setExtensionMode(component, event) {
const option = event.target.value;
if (option === 'complex') {
return;
}
if (component === 'enable' && !this.isDefaultConfiguration) {
this.setExtensionMode('enableAard', event);
this.setExtensionMode('enableKeyboard', event);
// in enableUI, 'enabled' is unused and 'theater' uses its place
if (option === 'enabled') {
this.setExtensionMode('enableUI', {target: {value: 'theater'}});
} else {
this.setExtensionMode('enableUI', event);
}
}
if (option === 'default') {
return this.siteSettings.set(component, {
normal: ExtensionMode.Default,
theater: ExtensionMode.Default,
fullscreen: ExtensionMode.Default
});
}
if (option === 'disabled') {
return this.siteSettings.set(component, {
normal: ExtensionMode.Disabled,
theater: ExtensionMode.Disabled,
fullscreen: ExtensionMode.Disabled
});
}
if (option === 'enabled') {
return this.siteSettings.set(component, {
normal: ExtensionMode.Enabled,
theater: ExtensionMode.Enabled,
fullscreen: ExtensionMode.Enabled
});
}
if (option === 'theater') {
return this.siteSettings.set(component, {
normal: ExtensionMode.Disabled,
theater: ExtensionMode.Enabled,
fullscreen: ExtensionMode.Enabled
});
}
if (option === 'fs') {
return this.siteSettings.set(component, {
normal: ExtensionMode.Disabled,
theater: ExtensionMode.Disabled,
fullscreen: ExtensionMode.Enabled
});
}
}
}
});
</script>
<style scoped>
</style>

View File

@ -1,79 +0,0 @@
<template>
<div
class="p-2 border border-stone-600 hover:border-primary-400 bg-black hover:bg-stone-900"
:class="{'!border-primary-400 text-primary-300': isActive}"
>
<div class="flex flex-row cursor-default w-full">
<div class="grow">
<b>{{ host }}</b>
<span :style="getSiteTypeColor(siteSettings?.data?.type)">
(config: {{siteSettings?.data?.type ?? 'unknown'}})
</span>
</div>
<div class="cursor-pointer" @click="triggerEdit()">Edit</div>
</div>
<div v-if="this.siteSettings?.usesSettingsFor">
<div v-if="this.siteSettings.usesSettingsFor === '@global'">Uses default settings</div>
<div v-else>Uses settings for: <span class="info-color">{{this.siteSettings.usesSettingsFor}}</span></div>
</div>
<div class="flex flex-row">
<small>
Enabled: <span :style="getSiteEnabledColor(host, 'enable')"><small>{{ getSiteEnabledModes(host, 'enable') }}</small></span>;&nbsp;
Autodetect: <span :style="getSiteEnabledColor(host, 'enableAard')"><small>{{ getSiteEnabledModes(host, 'enableAard') }}</small></span>;&nbsp;
kbd: <span :style="getSiteEnabledColor(host, 'enableKeyboard')"><small>{{ getSiteEnabledModes(host, 'enableKeyboard') }}</small></span>
UI: <span :style="getSiteEnabledColor(host, 'enableUI')"><small>{{ getSiteEnabledModes(host, 'enableUI') }}</small></span>
</small>
</div>
</div>
</template>
<script>
import ExtensionMode from '@src/common/enums/ExtensionMode.enum';
export default {
data() {
return {
siteSettings: undefined,
supportType: undefined
}
},
props: [
'settings',
'parentHost',
'host',
'isActive',
],
created() {
this.siteSettings = this.settings.getSiteSettings({site: this.host, isIframe: this.parentHost && this.host !== this.parentHost, parentHostname: this.parentHost});
},
methods: {
getSiteTypeColor(siteType) {
switch (siteType) {
case 'official': return 'color: #fa6';
case 'community': return 'color: rgb(114, 114, 218)';
case 'officially-disabled': return 'color: #f00';
case 'testing': return 'color: #d81';
default: return 'color: rgb(138, 65, 126)'
};
},
getSiteEnabledColor(site, component) {
const status = this.getSiteEnabledModes(site, component);
return status === 'disabled' ? 'color: #f00' : 'color: #1f8';
},
getSiteEnabledModes(site, component) {
if (this.siteSettings?.normal === ExtensionMode.Enabled) {
return 'always';
}
if (this.siteSettings?.data[component]?.theater === ExtensionMode.Enabled) {
return 'T + FS';
}
if (this.siteSettings?.data[component]?.fullscreen === ExtensionMode.Enabled) {
return 'fullscreen';
}
return '❌';
},
triggerEdit() {
this.$emit('edit');
}
}
}
</script>

View File

@ -1,192 +0,0 @@
<template>
<div class="flex flex-row items-center">
<div
class="shortcut-button"
@click="editShortcut()"
>
<template v-if="!editing">
<span v-if="!command?.shortcut" class="italic text-[0.9em] text-stone-500">
(not assigned)
</span>
<span v-else>
{{shortcutDisplay}}
</span>
</template>
<template v-else>
{{currentKeypress ?? 'Press a key'}}
<input ref="input"
class="hidden-input"
@keyup.capture="keyup($event)"
@keydown.capture="keydown($event)"
@input.prevent=""
@blur="editing = false;"
>
</template>
</div>
</div>
</template>
<script lang="ts">
import KeyboardShortcutParser from '@src/common/js/KeyboardShortcutParser';
import { defineComponent } from 'vue';
export default defineComponent({
props: {
command: Object,
settings: Object,
},
data() {
return {
currentKeypress: undefined,
currentKey: undefined,
editing: false,
}
},
computed: {
shortcutDisplay() {
if (!this.command?.shortcut) {
return '(no shortcut)'
}
return KeyboardShortcutParser.parseShortcut(this.command.shortcut);
}
},
methods: {
editShortcut() {
this.editing = true;
this.currentKeypress = undefined;
this.currentKey = undefined;
// input doesn't exist now, but will exist on the next tick
this.$nextTick(()=> this.$refs.input.focus());
},
/**
* Updates currently pressed keypress for display
*/
keydown(event) {
// event.repeat is set to 'true' when key is being held down, but not on
// first keydown. We don't need to process subsequent repeats of a keypress
// we already processed.
if (event.repeat) {
return;
}
const shortcut = KeyboardShortcutParser.generateShortcutFromKeypress(event);
const fixedShortcut = this.handleModifierKeypress(shortcut);
if (this.currentKey === undefined) {
this.currentKey = fixedShortcut;
} else {
// here's a fun fact. Keydown doesn't do modifier keys the way we want
// notably, A-Z0-9 keys are returned without modifier state (all modifiers)
// are set to false in keydown events. That means we need to keep track of
// modifiers ourselves.
if (fixedShortcut.notModifier) {
this.currentKey.key = fixedShortcut.key;
this.currentKey.code = fixedShortcut.code;
} else {
this.currentKey = {
...fixedShortcut,
key: this.currentKey.key,
code: this.currentKey.code
}
}
}
// update display
this.currentKeypress = KeyboardShortcutParser.parseShortcut(this.currentKey);
},
/**
* Emits shortcutChanged when shortcut is considered changed
*/
keyup(event) {
const shortcut = KeyboardShortcutParser.generateShortcutFromKeypress(event);
const fixedShortcut = this.handleModifierKeypress(shortcut);
if (fixedShortcut.notModifier) {
this.command.shortcut = this.currentKey;
this.settings?.saveWithoutReload();
this.editing = false;
this.$emit('shortcutChanged', this.currentKey);
} else {
// if none of the modifiers are pressed and if no other key is being held down,
// we need to reset label back to 'pls press key'
if (!fixedShortcut.altKey && !fixedShortcut.ctrlKey && !fixedShortcut.metaKey && !fixedShortcut.shiftKey && !fixedShortcut.code) {
this.currentKeypress = undefined;
this.currentKey = undefined;
} else {
this.currentKey = shortcut;
this.currentKeypress = KeyboardShortcutParser.parseShortcut(this.currentKey);
}
}
},
/**
* Handles current keypress if event.keyCode is a modifier key.
* Returns true if current event was a modifier key, false if
* if was a regular A-Z 0-9 key.
*/
handleModifierKeypress(event) {
const modifierPressed = event.type === 'keydown';
switch (event.code) {
case 'ShiftLeft':
case 'ShiftRight':
return {
...event,
key: '…',
code: null,
shiftKey: modifierPressed
}
case 'ControlLeft':
case 'ControlRight':
return {
...event,
key: '…',
code: null,
controlKey: modifierPressed
};
case 'MetaLeft':
case 'MetaRight':
return {
...event,
key: '…',
code: null,
metaKey: modifierPressed
};
case 'AltLeft':
case 'AltRight':
return {
...event,
key: '…',
code: null,
altKey: modifierPressed
};
}
return {
...event,
notModifier: true,
}
}
}
});
</script>
<style lang="scss" scoped>
@import './KeyboardShortcutEntry.css';
.center-text {
text-align: center;
}
.dark {
opacity: 50%;
}
.hidden-input {
position: absolute;
z-index: -9999;
opacity: 0;
}
</style>

View File

@ -1,30 +0,0 @@
@import '../../../../main.css'; /** postcss processor doesn't support aliases */
.keyboard-shortcut-row {
@apply py-2 px-4
even:bg-black/25
flex flex-row items-center gap-4
border-y-1 border-y-transparent hover:border-y-stone-700 hover:bg-stone-900;
&:hover {
.hover-hide {
@apply hidden;
}
}
&:not(:hover) {
.hover-show {
@apply hidden;
}
}
}
.keyboard-shortcut-row, .hover-shortcut {
.shortcut-button {
@apply px-4 text-[0.9em] border-1 border-transparent
cursor-pointer select-none;
&:hover {
@apply bg-black border-1 border-stone-500 rounded-sm
}
}
}

View File

@ -1,97 +0,0 @@
<template>
<div class="keyboard-shortcut-row">
<div class="w-[12em] text-stone-300">
<span v-if="indent" :style="{width: indent + 'rem'}" class="inline-block"></span>{{command.label}}
</div>
<div class="w-[18em] flex flex-row items-center">
<EditShortcutButton
:settings="settings"
:command="command"
>
</EditShortcutButton>
</div>
<div v-if="state !== 'removing'" class="justify-self-end cursor-pointer hover-show flex flex-row items-center text-[0.8em]" @click="clearShortcut(command)">
<mdicon class="text-red-500" name="close" :size="12"></mdicon> <span class="pl-1">Clear</span>
</div>
<div v-if="removable && state !== 'removing'" class="justify-self-end text-red-500 cursor-pointer hover-show flex flex-row items-center text-[0.8em]" @click="removeAction(command)">
<mdicon name="delete" :size="12"></mdicon> <span class="pl-1">Remove action</span>
</div>
<div v-if="state === 'removing'" class="justify-self-end flex flex-row items-center gap-2 text-[0.8em]">
<div>Remove option?</div>
<div @click="cancelRemoval()" class="cursor-pointer flex flex-row items-center gap-1">
<mdicon name="close" :size="12"></mdicon> <span>No, do not remove</span>
</div>
<div @click="confirmRemoval(command)" class="cursor-pointer text-red-500 flex flex-row items-center gap-1">
<mdicon name="remove" :size="12"></mdicon> <span>Yes</span>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import KeyboardShortcutParserMixin from '../../../../csui/src/utils/KeyboardShortcutParserMixin';
import EditShortcutButton from './EditShortcutButton.vue';
export default defineComponent({
components: {
EditShortcutButton,
},
mixins: [
KeyboardShortcutParserMixin
],
props: [
'settings', // not used only passed on
'command',
'type',
'indent',
'removable',
],
data() {
return {
state: null,
}
},
methods: {
clearShortcut(command) {
command.shortcut = undefined;
this.settings.saveWithoutReload();
},
removeAction(command) {
if (!this.removable) {
return;
}
this.state = 'removing';
},
confirmRemoval(command) {
console.info('confirmong removal:', command)
const commandsArray = this.settings.active.commands[this.type];
let index = commandsArray.indexOf(command);
let reload = false;
// let's try fallback
if (index === -1) {
index = commandsArray.findIndex(x => JSON.stringify(x) === JSON.stringify(command))
reload = true;
}
console.log('command index:', index, this.settings.active.commands[this.type]);
if (index !== -1) {
commandsArray.splice(index, 1);
if (reload) {
this.settings.save();
} else {
this.settings.saveWithoutReload();
}
}
this.state = null;
},
cancelRemoval() {
this.state = null;
}
}
});
</script>
<style lang="postcss" scoped>
@import './KeyboardShortcutEntry.css';
</style>

View File

@ -1,109 +0,0 @@
<template>
<div class="compact-form">
<div class="field">
<div class="label"></div>
<div class="text-[0.9em] font-semibold">
Add new ratio:
</div>
</div>
<div class="field">
<div class="label">Aspect ratio:</div>
<div class="input">
<input
v-model="command.aspectRatio"
placeholder="Enter aspect ratio ..."
@blur="updateLabel()"
>
</div>
</div>
<div class="field">
<div class="label">Label:</div>
<div class="input">
<input
v-model="command.label"
placeholder="Button label"
>
</div>
</div>
<div class="field">
<div class="label">Shortcut:</div>
<div class="hover-shortcut">
<EditShortcutButton
:command="command"
></EditShortcutButton>
</div>
</div>
<div class="flex flex-row gap-1 justify-end -mt-2 text-[0.9em] text-stone-400 font-bold hover:font-bold">
<div
class="flex flex-row items-center px-2 py-1 hover:text-white cursor-pointer"
:class="{'opacity-50 pointer-events-none': !command.aspectRatio || !command.label}"
@click="add()"
>
<mdicon name="content-save" :size="16"></mdicon>
Add
</div>
<div
class="flex flex-row items-center px-2 py-1 hover:text-red-500 cursor-pointer"
@click="cancel()"
>
<mdicon name="close" :size="16"></mdicon>
Cancel
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import EditShortcutButton from './EditShortcutButton.vue';
export default defineComponent({
components: {
EditShortcutButton,
},
data() {
return {
command: {
aspectRatio: '',
label: '',
shortcut: undefined as any,
}
}
},
methods: {
updateLabel() {
this.command.label = this.command.aspectRatio;
},
add() {
console.log('emitting add', this.command);
this.$emit('add', this.command);
},
cancel() {
this.$emit('cancel');
}
}
});
</script>
<style lang="postcss" scoped>
@import './KeyboardShortcutEntry.css';
.compact-form {
@apply flex flex-col gap-1;
.field {
@apply flex flex-row items-center gap-2 mx-0 my-0 px-0 py-0;
.label {
@apply w-24 text-right;
}
.input {
@apply flex-1;
input {
@apply w-full placeholder:text-[0.8rem] placeholder:text-stone-500;
}
}
}
}
</style>

View File

@ -1,259 +0,0 @@
<template>
<div class="flex flex-col relative w-full">
<h2 class="text-[1.75em]">Keyboard shortcuts</h2>
<p class="text-stone-500 text-[0.8em]">You can edit keyboard shortcuts here. If the aspect ratio you're looking for doesn't appear on the list, you can add it.</p>
<div class="flex flex-row gap-2 w-full justify-end mt-4">
<button @click="clearShortcuts()"><mdicon name="close" :size="16"></mdicon>Clear all shortcuts</button>
<button @click="resetToDefault()"><mdicon name="restore" :size="16"></mdicon>Reset to default</button>
</div>
<div class="flex flex-col w-full">
<div class="keyboard-shortcut-row bg-stone-900 mt-4 pt-2 pb-1 border-b-1 border-b-stone-700">
<div class="w-[12em] text-center border-r-1 border-r-stone-700">Command</div>
<div class="w-[12em] text-center">Keystroke</div>
</div>
<!-- CROP -->
<div
class="keyboard-shortcut-row"
>
<div
class="w-[32em] flex flex-row gap-2 items-center"
@click="toggleGroupState('crop')"
>
<mdicon
v-if="!groupVisible.crop"
name="chevron-right"
:size="16"
/>
<mdicon
v-if="groupVisible.crop"
name="chevron-down"
:size="16"
/>
<div>Crop<br/>
<p class="text-[0.8em] text-stone-500">Safe crop options. Prefers to leave empty space instead of cutting off video edges.</p>
</div>
</div>
</div>
<div>
<!-- Here just to break even/odd -->
</div>
<template v-if="groupVisible.crop">
<KeyboardShortcutEntry
v-for="command in settings.active.commands.crop"
:key="command"
type="crop"
:indent="2"
:command="command"
:settings="settings"
:removable="command?.arguments?.type === AspectRatioType.Fixed"
></KeyboardShortcutEntry>
<div
v-if="state !== 'add-crop'"
class="ml-12 w-[16em] flex flex-row text-[0.8rem] gap-2 my-2 font-bold items-center hover:text-white cursor-pointer"
@click="startAdding('crop')"
>
<mdicon name="plus" :size="16"></mdicon><div>Add new ratio</div>
</div>
<template v-else>
<NewAspectRatioForm
type="crop"
:settings="settings"
@cancel="state = null"
@add="addAction('crop', $event)"
></NewAspectRatioForm>
</template>
</template>
<!-- ZOOM -->
<div
class="keyboard-shortcut-row"
>
<div
class="w-[32em] flex flex-row gap-2 items-center"
@click="toggleGroupState('zoom')"
>
<mdicon
v-if="!groupVisible.zoom"
name="chevron-right"
:size="16"
/>
<mdicon
v-if="groupVisible.zoom"
name="chevron-down"
:size="16"
/>
<div>Zoom<br/>
<p class="text-[0.8em] text-stone-500">Controls zoom. Will cut off parts of video in order to achieve desired zoom.</p>
</div>
</div>
</div>
<template v-if="groupVisible.zoom">
<KeyboardShortcutEntry
v-for="command in settings.active.commands.zoom"
:key="command"
type="zoom"
:indent="2"
:command="command"
:settings="settings"
:removable="command?.arguments?.type === AspectRatioType.Fixed"
></KeyboardShortcutEntry>
<div class="ml-12 w-[16em] flex flex-row text-[0.8rem] gap-2 my-2 font-bold items-center hover:text-white cursor-pointer">
<mdicon name="plus" :size="16"></mdicon><div>Add new</div>
</div>
</template>
<!-- STRETCH -->
<div
class="keyboard-shortcut-row"
>
<div
class="w-[32em] flex flex-row gap-2 items-center"
@click="toggleGroupState('stretch')"
>
<mdicon
v-if="!groupVisible.stretch"
name="chevron-right"
:size="16"
/>
<mdicon
v-if="groupVisible.stretch"
name="chevron-down"
:size="16"
/>
<div>Stretch<br/>
<p class="text-[0.8em] text-stone-500">Control stretching.</p>
</div>
</div>
</div>
<template v-if="groupVisible.stretch">
<KeyboardShortcutEntry
v-for="command in settings.active.commands.stretch"
:key="command"
type="stretch"
:indent="2"
:command="command"
:settings="settings"
:removable="command?.arguments?.type === StretchType.FixedSource"
></KeyboardShortcutEntry>
<div class="ml-12 w-[16em] flex flex-row text-[0.8rem] gap-2 my-2 font-bold items-center hover:text-white cursor-pointer">
<mdicon name="plus" :size="16"></mdicon><div>Add new</div>
</div>
</template>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import KeyboardShortcutEntry from './Components/KeyboardShortcutEntry.vue';
import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
import StretchType from '@src/common/enums/StretchType.enum';
import NewAspectRatioForm from './Components/NewAspectRatioForm.vue';
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import CropOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/CropOptionsPanel.vue'
import StretchOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/StretchOptionsPanel.vue'
import ZoomOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/ZoomOptionsPanel.vue'
export default defineComponent({
components: {
KeyboardShortcutEntry,
NewAspectRatioForm,
CropOptionsPanel,
StretchOptionsPanel,
ZoomOptionsPanel,
},
data() {
return {
AspectRatioType: AspectRatioType,
StretchType: StretchType,
groupVisible: {
crop: true,
stretch: true,
zoom: true,
},
state: null,
}
},
mixins: [
],
props: [
'settings', // required for buttons and actions, which are global
],
mounted() {
this.ghettoComputed = {
minEnabledWidth: this.optionalToFixed(this.settings.active.ui.inPlayer.minEnabledWidth * 100, 0),
minEnabledHeight: this.optionalToFixed(this.settings.active.ui.inPlayer.minEnabledHeight * 100, 0),
}
},
methods: {
toggleGroupState(groupName: string) {
this.groupVisible[groupName] = !this.groupVisible[groupName];
},
forcePositiveNumber(value) {
// Change EU format to US if needed
// | remove everything after second period if necessary
// | | | remove non-numeric characters
// | | | |
return value.replaceAll(',', '.').split('.', 2).join('.').replace(/[^0-9.]/g, '');
},
optionalToFixed(v, n) {
if ((`${v}`.split('.')[1]?.length ?? 0) > n) {
return v.toFixed(n);
}
return v;
},
addAction(segment: 'stretch' | 'crop' | 'zoom', newCommand) {
const actionMap = {
stretch: 'set-stretch',
crop: 'set-ar',
zoom: 'set-ar-zoom',
};
const typeMap = {
stretch: StretchType.FixedSource,
crop: AspectRatioType.Fixed,
zoom: AspectRatioType.Fixed,
};
const command = {
action: actionMap[segment],
label: newCommand.label,
arguments: {
type: typeMap[segment],
ratio: newCommand.ratio
},
shortcut: newCommand.shortcut,
};
this.settings.active.commands[segment].push(command);
this.settings.saveWithoutReload();
this.state = null;
},
clearShortcuts() {
for (const commandList of ['stretch', 'crop', 'zoom']) {
for (const command of this.settings.active.commands[commandList]) {
command.shortcut = undefined;
}
}
this.settings.saveWithoutReload();
},
resetToDefault() {
this.settings.active.commands = JSON.parse(JSON.stringify(this.settings.default.commands));
this.settings.save();
},
startAdding(type: string) {
this.state = `add-${type}`;
},
}
});
</script>
<style lang="postcss" scoped>
@import './Components/KeyboardShortcutEntry.css';
</style>

View File

@ -1,336 +0,0 @@
<template>
<div
class="grow shrink w-full h-full flex flex-col uw-clickable main-window relative overflow-hidden"
>
<div class="flex flex-row w-full h-full overflow-hidden">
<div class="settings-categories">
<div
v-for="tab of tabs"
:key="tab.id"
>
<div
v-if="!tab.hidden"
class="tab"
:class="{
'active': tab.id === selectedTab,
'highlight-tab': tab.highlight,
}"
@click="selectTab(tab.id)"
>
<div class="label">
{{tab.label}}
</div>
<div class="icon-container">
<mdicon
v-if="tab.icon"
:name="tab.icon"
:size="32"
/>
</div>
</div>
</div>
</div>
<div class="grow content flex flex-col overflow-auto pr-4 pb-12">
<!-- autodetection warning -->
<div class="warning-area">
<div
v-if="statusFlags.hasDrm"
class="warning-box"
>
<div class="icon-container">
<mdicon name="alert" :size="32" />
</div>
<div>
This site is blocking automatic aspect ratio detection. You will have to adjust aspect ratio manually.<br/>
<a>Learn more ...</a>
</div>
</div>
</div>
<div class="flex flex-row panel-content">
<!-- Panel section -->
<VideoSettings
v-if="selectedTab === 'video-settings'"
:settings="settings"
:siteSettings="siteSettings"
:eventBus="eventBus"
:site="site"
></VideoSettings>
<OtherSiteSettings
v-if="selectedTab === 'extensionSettings'"
:settings="settings"
:enableSettingsEditor="true"
></OtherSiteSettings>
<AutodetectionSettings
v-if="selectedTab === 'autodetectionSettings'"
:settings="settings"
:siteSettings="siteSettings"
:eventBus="eventBus"
:site="site"
>
</AutodetectionSettings>
<UISettings
v-if="selectedTab === 'uiSettings'"
:settings="settings"
:siteSettings="siteSettings"
:eventBus="eventBus"
>
</UISettings>
<KeyboardShortcutSettings
v-if="selectedTab === 'keyboardShortcuts'"
:settings="settings"
:eventBus="eventBus"
></KeyboardShortcutSettings>
<WhatsNew
v-if="selectedTab === 'changelog'"
:settings="settings"
></WhatsNew>
<About
v-if="selectedTab === 'about'"
:settings="settings"
>
</About>
<PlayerDetectionPanel
v-if="selectedTab === 'playerDetection'"
:siteSettings="siteSettings"
:eventBus="eventBus"
:site="site"
>
</PlayerDetectionPanel>
<DebugPanel
v-if="selectedTab === 'debugging'"
:settings="settings"
:eventBus="eventBus"
:site="site"
></DebugPanel>
<!-- <ResetBackupPanel
v-if="selectedTab === 'resetBackup'"
:settings="settings"
>
</ResetBackupPanel> -->
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import VideoSettings from '@components/VideoSettings/VideoSettings.vue';
import OtherSiteSettings from '@components/ExtensionSettings/Panels/OtherSiteSettings.vue';
import AutodetectionSettings from '@components/AutodetectionSettings/AutodetectionSettings.vue';
import UISettings from '@components/UISettings/UISettings.vue';
import KeyboardShortcutSettings from '@components/KeyboardShortcuts/KeyboardShortcutSettings.vue';
import WhatsNew from '@components/ExtensionInfo/WhatsNew.vue';
import About from '@components/ExtensionInfo/About.vue';
import DebugPanel from '../../csui/src/PlayerUiPanels/DebugPanel.vue'
import PlayerDetectionPanel from '../../csui/src/PlayerUiPanels/PlayerDetectionPanel.vue'
import BrowserDetect from '../../ext/conf/BrowserDetect'
import ChangelogPanel from '../../csui/src/PlayerUiPanels/ChangelogPanel.vue'
import AboutPanel from '@csui/src/PlayerUiPanels/AboutPanel.vue'
import ResetBackupPanel from '../../csui/src/PlayerUiPanels/ResetBackupPanel.vue'
import SupportLevelIndicator from '@csui/src/components/SupportLevelIndicator.vue'
const AVAILABLE_TABS = {
'video-settings': {id: 'video-settings', label: 'Video settings', icon: 'crop'},
'site-extension-settings': {id: 'extensionSettings', label: 'Site and Extension options', icon: 'cogs' },
'extensionSettings': {id: 'extensionSettings', label: 'Site and Extension options', icon: 'cogs' },
'siteSettings': {id: 'extensionSettings', label: 'Site and Extension options', icon: 'cogs' },
'autodetectionSettings': {id: 'autodetectionSettings', label: 'Autodetection options', icon: 'auto-fix'},
'uiSettings': {id: 'uiSettings', label: 'UI settings', icon: 'movie-cog-outline' },
'keyboardShortcuts': {id: 'keyboardShortcuts', label: 'Keyboard shortcuts', icon: 'keyboard-outline' },
'playerDetection': {id: 'playerDetection', label: 'Player detection', icon: 'television-play'},
'changelog': {id: 'changelog', label: 'What\'s new', icon: 'alert-decagram' },
'about': {id: 'about', label: 'About', icon: 'information-outline'},
'debugging': {id: 'debugging', label: 'Debugging', icon: 'bug-outline', hidden: true}
};
const TAB_LOADOUT = {
'settings': [
'extensionSettings',
'autodetectionSettings',
'uiSettings',
'keyboardShortcuts',
'changelog',
'about',
'debugging',
],
'popup': [
'video-settings',
'site-extension-settings',
'changelog',
'about'
],
}
export default defineComponent({
components: {
VideoSettings,
OtherSiteSettings,
AutodetectionSettings,
KeyboardShortcutSettings,
UISettings,
WhatsNew,
About,
PlayerDetectionPanel,
DebugPanel,
ChangelogPanel,
AboutPanel,
SupportLevelIndicator,
ResetBackupPanel,
},
mixins: [],
data() {
return {
statusFlags: {
hasDrm: undefined,
},
tabs: [
// // {id: 'videoSettings', label: 'Video settings', icon: 'crop'},
// {id: 'extensionSettings', label: 'Site and Extension options', icon: 'cogs' },
// {id: 'autodetectionSettings', label: 'Autodetection options', icon: 'auto-fix'},
// {id: 'uiSettings', label: 'UI settings', icon: 'movie-cog-outline' },
// {id: 'keyboardShortcuts', label: 'Keyboard shortcuts', icon: 'keyboard-outline' },
// {id: 'playerDetection', label: 'Player detection', icon: 'television-play'},
// // {id: 'advancedOptions', label: 'Advanced options', icon: 'cogs' },
// {id: 'changelog', label: 'What\'s new', icon: 'alert-decagram' },
// {id: 'about', label: 'About', icon: 'information-outline'},
// {id: 'debugging', label: 'Debugging', icon: 'bug-outline', hidden: true},
],
selectedTab: 'extensionSettings',
BrowserDetect: BrowserDetect,
preventClose: false,
siteSettings: null,
}
},
props: [
'role',
'initialPath',
'settings',
'eventBus',
'logger',
'inPlayer',
'site',
],
computed: {
// LPT: NO ARROW FUNCTIONS IN COMPUTED,
// IS SUPER HARAM
// THINGS WILL NOT WORK IF YOU USE ARROWS
siteSupportLevel() {
return (this.site && this.siteSettings) ? this.siteSettings.data.type || 'no-support' : 'waiting';
}
},
created() {
this.generateTabs();
this.settings.listenAfterChange(this.setDebugTabVisibility);
if (this.initialPath && this.initialPath.length) {
this.selectedTab = this.initialPath[0];
}
const changelogTab = this.tabs.find(x => x.id === 'changelog');
if (changelogTab) {
changelogTab.highlight = !this.settings.active?.whatsNewChecked;
}
this.siteSettings = this.settings.getSiteSettings({site: this.site});
this.eventBus?.subscribe(
'uw-show-ui',
{
source: this,
function: () => {
if (this.inPlayer) {
return; // show-ui is only intended for global overlay
}
},
}
)
this.setDebugTabVisibility();
},
destroyed() {
this.settings.removeListenerAfterChange(this.setDebugTabVisibility);
this.eventBus?.unsubscribeAll(this);
},
methods: {
/**
* Gets URL of the browser settings page (i think?)
*/
getUrl(url) {
return BrowserDetect.getURL(url);
},
generateTabs() {
const tabs = [];
for (const tab of TAB_LOADOUT[this.role]) {
if (!AVAILABLE_TABS[tab]) {
console.warn('[uw:SettingsWindowContent] tab', tab, 'is not present in available tabs:', AVAILABLE_TABS, '— tabs for role', this.role, TAB_LOADOUT[this.role]);
continue;
} else {
tabs.push(AVAILABLE_TABS[tab]);
}
}
this.tabs = tabs;
},
selectTab(tab) {
console.log("Selecting tab", tab);
this.selectedTab = tab;
window.location.hash = `#${this.role}/${this.selectedTab}`;
},
setPreventClose(bool) {
this.preventClose = bool;
this.$emit('preventClose', bool);
},
setDebugTabVisibility() {
const debugTab = this.tabs.find( x => x.id === 'debugging');
if (debugTab) {
debugTab.hidden = !this.settings.active.ui.devMode;
}
},
}
});
</script>
<style lang="postcss" scoped>
@import '../../main.css'; /** postcss processor doesn't support aliases */
.settings-categories {
@apply w-[20em] max-w-[20em] flex flex-col grow-0 shrink-0 mr-4 border-r border-r-stone-800 text-right;
.tab {
@apply flex flex-row gap-4 px-4 py-2 justify-end items-center
text-[1.125em] text-stone-300 text-right font-mono
cursor-pointer
border-r-2 border-r-transparent
hover:bg-stone-800
hover:text-primary-200
hover:border-r-stone-600;
.label {
@apply grow-0;
}
&.active {
@apply !bg-transparent !text-primary-300 bg-gradient-to-r from-transparent to-black hover:!text-primary-200 border-r-primary-400;
}
}
}
pre {
white-space: pre-wrap;
}
</style>

View File

@ -1,233 +0,0 @@
<template>
<div class="flex flex-col relative w-full">
<h2 class="text-[1.75em]">Player UI options</h2>
<!-- The rest of the tab is under 'edit ratios and shortcuts' row -->
<div class="flex flex-col" style="width: 100%">
<div class="flex flex-col compact-form">
<div v-if="!siteSettings.data.enableUI.fullscreen">
UI is disabled for this site.
</div>
<div
class="flex flex-col field-group compact-form"
:class="{disabled: !siteSettings.data.enableUI.fullscreen}"
>
<div class="field disabled">
<div class="label">
Popup activator position:
</div>
<div class="select">
<select
v-model="settings.active.ui.inPlayer.popupAlignment"
@change="saveSettings()"
>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
</div>
</div>
<div class="field">
<div class="label">
Activate in-player UI:
</div>
<div class="select">
<select
v-model="settings.active.ui.inPlayer.activation"
@change="saveSettings()"
>
<option value="player">
When mouse hovers over player
</option>
<option value="trigger-zone">
When mouse hovers over trigger zone
</option>
</select>
</div>
</div>
<div class="field" :class="{'disabled': settings.active.ui.inPlayer.activation !== 'trigger-zone'}">
<div class="label">Edit trigger zone:</div>
<button @click="startTriggerZoneEdit()">Edit</button>
</div>
<div class="field">
<div class="label">
Do not show in-player UI when video player is narrower than
</div>
<div class="input range-input">
<input
:value="settings.active.ui.inPlayer.minEnabledWidth"
class="slider"
type="range"
min="0"
max="1"
step="0.01"
@input="(event) => setPlayerRestrictions('minEnabledWidth', event.target.value)"
@change="(event) => saveSettings()"
>
<input
style="margin-right: 0.6rem;"
:value="ghettoComputed.minEnabledWidth"
@input="(event) => setPlayerRestrictions('minEnabledWidth', event.target.value, true)"
@change="(event) => saveSettings(true)"
>
<div class="unit">% of screen</div>
</div>
</div>
<div class="field">
<div class="label">
Do not show in-player UI when video player is shorter than
</div>
<div class="input range-input">
<input
:value="settings.active.ui.inPlayer.minEnabledHeight"
class="slider"
type="range"
min="0"
max="1"
step="0.01"
@input="(event) => setPlayerRestrictions('minEnabledHeight', event.target.value)"
@change="(event) => saveSettings()"
>
<input
style="margin-right: 0.6rem;"
:value="ghettoComputed.minEnabledHeight"
@input="(event) => setPlayerRestrictions('minEnabledHeight', event.target.value, true)"
@change="(event) => saveSettings(true)"
>
<div class="unit">% of screen</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import CropOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/CropOptionsPanel.vue'
import StretchOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/StretchOptionsPanel.vue'
import ZoomOptionsPanel from '@csui/src/PlayerUiPanels/PanelComponents/VideoSettings/ZoomOptionsPanel.vue'
import { defineComponent } from 'vue';
export default defineComponent({
components: {
CropOptionsPanel,
StretchOptionsPanel,
ZoomOptionsPanel,
},
data() {
return {
ghettoComputed: { }
}
},
mixins: [
],
props: [
'settings', // required for buttons and actions, which are global
'siteSettings',
'eventBus',
],
mounted() {
this.ghettoComputed = {
minEnabledWidth: this.optionalToFixed(this.settings.active.ui.inPlayer.minEnabledWidth * 100, 0),
minEnabledHeight: this.optionalToFixed(this.settings.active.ui.inPlayer.minEnabledHeight * 100, 0),
}
},
methods: {
forcePositiveNumber(value) {
// Change EU format to US if needed
// | remove everything after second period if necessary
// | | | remove non-numeric characters
// | | | |
return value.replaceAll(',', '.').split('.', 2).join('.').replace(/[^0-9.]/g, '');
},
optionalToFixed(v, n) {
if ((`${v}`.split('.')[1]?.length ?? 0) > n) {
return v.toFixed(n);
}
return v;
},
setPlayerRestrictions(key, value, isTextInput) {
if (isTextInput) {
value = (+this.forcePositiveNumber(value) / 100);
}
if (isNaN(+value)) {
value = 0.5;
}
this.settings.active.ui.inPlayer[key] = value;
if (isTextInput) {
this.ghettoComputed[key] = this.optionalToFixed(value, 0);
} else {
this.ghettoComputed[key] = this.optionalToFixed(value * 100, 0);
}
},
saveSettings(forceRefresh) {
this.settings.saveWithoutReload();
if (forceRefresh) {
this.$nextTick( () => this.$forceRefresh() );
}
},
startTriggerZoneEdit() {
this.eventBus.send('start-trigger-zone-edit');
},
async openOptionsPage() {
BrowserDetect.runtime.openOptionsPage();
},
}
});
</script>
<style lang="postcss" scoped>
.trigger-zone-editor {
background-color: rgba(0,0,0,0.25);
padding-bottom: 2rem;
.field {
margin-bottom: -1em;
}
}
.disabled {
pointer-events: none;
/* color: #666; */
filter: contrast(50%) brightness(40%) grayscale(100%);
}
.compact-form {
> .field, > .field-group {
margin-top: 0;
margin-bottom: 0;
}
}
.keyboard-settings {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 1rem;
> * {
width: calc(33% - 0.5rem);
}
}
.mt2r {
margin-top: 2rem;
margin-bottom: 0.5rem;
}
.mth3 {
margin-top: 1.5rem;
}
</style>

View File

@ -1,240 +0,0 @@
<template>
<div class="flex flex-col" style="position: relative; width: 100%;">
<!-- 'Change UI' options is a tiny bit in upper right corner. -->
<h3>Crop</h3>
<div class="w-full grid grid-cols-4 gap-2 pt-2+">
<ShortcutButton
v-for="(command, index) of settings?.active.commands.crop"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
></ShortcutButton>
</div>
<h3>Zoom</h3>
<div class="w-full grid grid-cols-4 gap-2 pt-2">
<ShortcutButton
v-for="(command, index) of settings?.active.commands.zoom"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
></ShortcutButton>
</div>
<div class="text-white font-mono text-semibold mt-4 mb-1">Free-form zoom</div>
<div class="flex flex-row">
<!-- SLIDERS BEFORE X/Y LOCK -->
<div class="grow flex flex-col gap-[0.125em]">
<div class="flex flex-row w-full items-center">
<div class="w-[4em] opacity-75 text-right leading-none">Width:</div>
<div class="w-[4.5em] pr-4 text-right leading-none">{{getZoomForDisplay('x')}}</div>
<div class="grow leading-none pt-[0.25em]">
<input id="_input_zoom_slider"
class="w-full"
type="range"
step="any"
min="-1"
max="4"
:value="zoom.x"
@input="changeZoom($event.target.value, 'x')"
/>
</div>
</div>
<div class="flex flex-row w-full items-center">
<div class="w-[4em] opacity-75 text-right leading-none">Height:</div>
<div class="w-[4.5em] pr-4 text-right leading-none">{{getZoomForDisplay('y')}}</div>
<div class="grow leading-none pt-[0.5em]">
<input id="_input_zoom_slider_2"
class="w-full"
type="range"
step="any"
min="-1"
max="4"
:value="zoom.y"
@input="changeZoom($event.target.value, 'y')"
/>
</div>
</div>
</div>
<!-- X/Y LOCK -->
<div class="h-full flex flex-col justify-center ml-2">
<div
class="h-[2.75em] mt-[0.125em] border-2 border-stone-500 border-l-transparent w-[0.5em]"
:class="{'xy-lock-bar-break': !zoomOptions.lockAr}"
></div>
</div>
<div class="h-full flex flex-col justify-center p-2">
<div
class="cursor-pointer border-1 border-stone-800 p-2 hover:bg-stone-700 hover:text-white"
@click="zoomOptions.lockAr = !zoomOptions.lockAr"
>
<mdicon v-if="zoomOptions.lockAr" name="link-variant" :size="16"></mdicon>
<mdicon v-if="!zoomOptions.lockAr" name="link-variant-off" :size="16"></mdicon>
</div>
</div>
<div class="h-full flex flex-col justify-center p-2">
<div
class="cursor-pointer border-1 border-stone-800 p-2 hover:bg-stone-700 hover:text-white"
@click="resetZoom()"
>
<mdicon name="restore" :size="16"></mdicon>
</div>
</div>
</div>
<h3>Stretch</h3>
<div class="w-full grid grid-cols-4 gap-2 pt-2">
<ShortcutButton
v-for="(command, index) of settings?.active.commands.stretch"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
></ShortcutButton>
</div>
<h3>Align video</h3>
<div
ref="alignmentSvgContainer"
class="w-full h-[12em] flex flex-row justify-center"
></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import ShortcutButton from './components/ShortcutButton.vue';
import CommsMixin from '@components/common/mixins/CommsMixin.vue';
import KeyboardShortcutParserMixin from '@components/common/mixins/KeyboardShortcutParserMixin.vue';
import alignmentIndicatorSvg from '!!raw-loader!@ui/res/img/alignment-indicators.svg?raw';
import {setupVideoAlignmentIndicatorInteraction, setVideoAlignmentIndicatorState} from '@ui/utils/video-alignment-indicator-handling.ts';
import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
export default defineComponent({
components: {
ShortcutButton
},
props: [
'settings', // required for buttons and actions, which are global
'siteSettings',
'frame',
'eventBus',
'site'
],
mixins: [
CommsMixin,
KeyboardShortcutParserMixin,
],
data() {
return {
zoom: {x: 0, y: 0}, // zoom is logarithmic, so "100%" is represented as 0 instead of 1.
zoomOptions: {
lockAr: false,
},
alignmentSvgContainer: undefined as HTMLElement | undefined,
}
},
created() {
console.log('created ....');
this.eventBus.subscribe(
'uw-config-broadcast',
{
source: this,
function: (config) => this.handleConfigBroadcast(config)
}
);
},
mounted() {
this.alignmentSvgContainer = this.$refs.alignmentSvgContainer as HTMLElement;
if (this.alignmentSvgContainer) {
this.alignmentSvgContainer.innerHTML = alignmentIndicatorSvg;
const svgElement = this.alignmentSvgContainer.querySelector('svg') as SVGSVGElement;
console.log('svg element:', svgElement);
if (svgElement) {
setupVideoAlignmentIndicatorInteraction(svgElement, (x: VideoAlignmentType, y: VideoAlignmentType) => {
console.log('clicked!');
// Update selection visually
setVideoAlignmentIndicatorState(svgElement, x, y);
this.align(x,y);
});
}
}
this.eventBus.sendToTunnel('get-ar');
},
destroyed() {
this.eventBus.unsubscribeAll(this);
},
methods: {
async openOptionsPage() {
BrowserDetect.runtime.openOptionsPage();
},
getZoomForDisplay(axis: 'x' | 'y') {
// zoom is internally handled logarithmically, because we want to have x0.5, x1, x2, x4 ... magnifications
// spaced out at regular intervals. When displaying, we need to convert that to non-logarithmic values.
return `${(Math.pow(2, this.zoom[axis]) * 100).toFixed()}%`;
},
toggleZoomAr() {
this.zoomOptions.lockAr = !this.zoomOptions.lockAr;
},
resetZoom() {
// we store zoom logarithmically on this component
this.zoom = {x: 0, y: 0};
// we do not use logarithmic zoom elsewhere
// todo: replace eventBus with postMessage to parent
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'y'});
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1});
},
changeZoom(logZoom: number, axis: 'x' | 'y') {
// we do not use logarithmic zoom elsewhere, therefore we need to convert
const linearZoom = Math.pow(2, logZoom);
if (this.zoomOptions.lockAr) {
this.zoom.x = logZoom;
this.zoom.y = logZoom;
this.eventBus?.sendToTunnel('set-zoom', {zoom: linearZoom});
} else {
this.zoom[axis] = logZoom;
this.eventBus?.sendToTunnel('set-zoom', {zoom: {[axis]: linearZoom}});
}
},
isActiveZoom(command) {
return false;
},
align(alignmentX: VideoAlignmentType, alignmentY: VideoAlignmentType) {
this.eventBus?.sendToTunnel('set-alignment', {x: alignmentX, y: alignmentY})
}
}
});
</script>
<style lang="postcss" scoped>
@import '../../../main.css';
.xy-lock-bar-break {
@apply relative;
&:before {
@apply absolute box-content h-[1em] w-[3px] border-3 border-stone-950;
content: '';
top: 50%;
left: 100%;
transform: translate(-25%, -50%) rotate(45deg);
}
}
</style>

View File

@ -1,22 +0,0 @@
<template>
<button class="flex flex-col justify-center items-center" :class="classList">
<div class="flex justify-self-center">
{{label}}
</div>
<div class="-mt-2 font-[0.8em] flex justify-self-center text-stone-500">
{{shortcut ? `(${shortcut})` : ''}}
</div>
</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
label: String,
shortcut: String,
classList: String
}
});
</script>

View File

@ -1,22 +0,0 @@
<script lang="ts">
export default {
methods: {
handleConfigBroadcast(message) {
if (message.type === 'ar') {
this.resizerConfig.crop = message.config;
}
this.$nextTick( () => this.$forceUpdate() );
},
/**
* Sends commands to main content script in parent iframe
* @param {*} command
*/
execAction(command) {
const cmd = JSON.parse(JSON.stringify(command));
this.eventBus?.sendToTunnel(cmd.action, cmd.arguments);
},
}
}
</script>

View File

@ -1,17 +0,0 @@
<script lang="ts">
import KeyboardShortcutParser from '@src/common/js/KeyboardShortcutParser';
export default {
methods: {
/**
* Parses command's keyboard shortcut into human-readable label
*/
getKeyboardShortcutLabel(command): string {
if (! command.shortcut) {
return '';
}
return KeyboardShortcutParser.parseShortcut(command.shortcut);
},
}
};
</script>

View File

@ -1,30 +0,0 @@
import EventBus from '../../../ext/lib/EventBus';
import { ComponentLogger } from '../../../ext/lib/logging/ComponentLogger';
import { LogAggregator } from '../../../ext/lib/logging/LogAggregator';
import Settings from '../../../ext/lib/settings/Settings';
const loadedTabs = [
'videoSettings',
'extensionSettings',
'changelog',
'about',
];
async function load() {
const _uw_data = {
eventBus: new EventBus(),
settings: {},
settingsInitialized: false,
logAggregator: undefined,
logger: undefined,
site: undefined,
siteSettings: undefined,
comms: undefined,
};
_uw_data.logAggregator = new LogAggregator('🔵ext-popup🔵');
_uw_data.logger = new ComponentLogger(_uw_data.logAggregator, 'Popup');
_uw_data.settings = new Settings({logAggregator: _uw_data.logAggregator});
_uw_data.
}

View File

@ -1,213 +0,0 @@
<template>
<div class="w-full h-[100dvh] overflow-hidden flex flex-row justify-center items-center py-4 px-16">
<!-- page content -->
<div class="w-full max-w-[1920px] h-[100dvh] overflow-hidden flex flex-col">
<h1 class="text-[3em] grow-0 shrink-0">Ultrawidify settings</h1>
<div v-if="!settingsInitialized" class="flex flex-row w-full justify-center items-center">
Loading settings...
</div>
<SettingsWindowContent v-else
class="grow shrink"
:role="role"
:initialPath="initialPath"
:settings="settings"
:eventBus="eventBus"
:logger="logger"
:inPlayer="false"
:site="null"
>
</SettingsWindowContent>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import BrowserDetect from '../../../ext/conf/BrowserDetect';
import { LogAggregator } from '@src/ext/lib/logging/LogAggregator';
import { ComponentLogger } from '@src/ext/lib/logging/ComponentLogger';
import Settings from '@src/ext/lib/settings/Settings';
import SettingsWindowContent from '@components/SettingsWindowContent.vue';
import EventBus from '@src/ext/lib/EventBus';
import CommsClient, { CommsOrigin } from '@src/ext/lib/comms/CommsClient';
import {ChromeShittinessMitigations as CSM} from '@src/common/js/ChromeShittinessMitigations';
export default defineComponent({
components: {
SettingsWindowContent
},
data () {
return {
settings: undefined as Settings | undefined,
logger: undefined as ComponentLogger | undefined,
logAggregator: undefined as LogAggregator | undefined,
settingsInitialized: false,
role: 'settings',
initialPath: undefined as string[] | undefined,
}
},
async created() {
const [segment, ...path] = window.location.hash.split('/');
this.initialPath = path;
switch (segment) {
case '#popup':
await this.setupPopup();
break;
case '#iframe':
await this.setupIframe();
break;
default:
await this.setupSettingsPage();
}
await this.settings.init();
this.settingsInitialized = true;
},
methods: {
updateConfig() {
this.settings.init();
this.$nextTick( () => this.$forceUpdate());
},
/**
* Initializes page when it's being loaded from the settings
*/
async setupSettingsPage() {
this.role = 'settings';
this.logAggregator = new LogAggregator('');
this.logger = new ComponentLogger(this.logAggregator, 'App.vue');
this.settings = new Settings({
logAggregator: this.logAggregator,
onSettingsChanged: () => this.updateConfig()
});
},
/**
* Initializes page when it's being loaded as extension popup
*/
async setupPopup() {
this.role = 'popup';
try {
this.logAggregator = new LogAggregator('🔵ext-popup🔵');
this.logger = new ComponentLogger(this.logAggregator, 'Popup');
this.settings = new Settings({afterSettingsSaved: () => this.updateConfig(), logAggregator: this.logAggregator});
await this.settings.init();
this.settingsInitialized = true;
// const port = chrome.runtime.connect({name: 'popup-port'});
// port.onMessage.addListener( (m,p) => this.processReceivedMessage(m,p));
// CSM.setProperty('port', port);
this.eventBus = new EventBus();
this.eventBus.subscribe(
'set-current-site',
{
source: this,
function: (config, context) => {
if (this.site) {
if (!this.site.host) {
// dunno why this fix is needed, but sometimes it is
this.site.host = config.site.host;
}
}
this.site = config.site;
// this.selectedSite = this.selectedSite || config.site.host;
this.siteSettings = this.settings.getSiteSettings({site: this.site.host});
this.eventBus.setupPopupTunnelWorkaround({
origin: CommsOrigin.Popup,
comms: {
forwardTo: 'active'
}
});
this.loadHostnames();
this.loadFrames();
}
},
);
this.eventBus.subscribe(
'open-popup-settings',
{
source: this,
function: (config) => {
this.selectTab(config.tab)
}
}
)
this.comms = new CommsClient('popup-port', this.logger, this.eventBus);
this.eventBus.setComms(this.comms);
this.eventBus.setupPopupTunnelWorkaround({
origin: CommsOrigin.Popup,
comms: {forwardTo: 'active'}
});
// ensure we'll clean player markings on popup close
window.addEventListener("unload", () => {
CSM.port.postMessage({
cmd: 'unmark-player',
forwardToAll: true,
});
// if (BrowserDetect.anyChromium) {
// chrome.extension.getBackgroundPage().sendUnmarkPlayer({
// cmd: 'unmark-player',
// forwardToAll: true,
// });
// }
});
// get info about current site from background script
while (true) {
this.requestSite();
await this.sleep(5000);
}
} catch (e) {
console.error('[Popup.vue::created()] An error happened:', e)
}
},
/**
* Initializes page when it's being loaded as in-page settings window
*/
async setupIframe() {
this.role='ui-window';
this.logAggregator = new LogAggregator('settings-page');
this.logger = new ComponentLogger(this.logAggregator, 'SettingsPage');
this.settings = new Settings({
logAggregator: this.logAggregator,
onSettingsChanged: () => this.updateConfig()
});
if (this.defaultTab) {
this.selectedTab = this.defaultTab;
}
this.siteSettings = this.settings.getSiteSettings({site: this.site});
this.tabs.find(x => x.id === 'changelog').highlight = !this.settings.active?.whatsNewChecked;
this.eventBus?.subscribe(
'uw-show-ui',
{
source: this,
function: () => {
if (this.inPlayer) {
return; // show-ui is only intended for global overlay
}
},
}
)
}
}
});
</script>

View File

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en"; style="position: relative">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="light dark">
<title>Ultrawidify - settings</title>
<!-- <link rel="stylesheet" href="csui.css"> -->
<% if (NODE_ENV === 'development') { %>
<!-- Load some resources only in development environment -->
<% } %>
</head>
<body class="uw-ultrawidify-container-root">
<div id="app"></div>
<script src="settings.js"></script>
</body>
</html>

View File

@ -1,13 +0,0 @@
import { createApp } from 'vue';
import App from './App';
import mdiVue from 'mdi-vue/v3';
import * as mdijs from '@mdi/js';
import '@src/main.css';
// NOTE — this is in-player interface for ultrawidify
// it is injected into the page in UI.init()
createApp(App)
.use(mdiVue, {icons: mdijs})
.mount('#app');

Binary file not shown.

View File

@ -1,93 +0,0 @@
Copyright 2014 The Heebo Project Authors (https://github.com/OdedEzer/heebo)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -1,93 +0,0 @@
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@ -1,81 +0,0 @@
<svg width="90" height="90" viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Glow filter for selected symbol -->
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="0" stdDeviation="4" flood-color="#ffac66"/>
</filter>
</defs>
<style>
path {
/*fill: oklch(86.9% 0.005 56.366)*/;
fill: oklch(55.3% 0.013 58.071);
transition: fill 0.2s, filter 0.2s;
}
rect.indicator-bg {
fill: transparent;
transition: fill 0.2s;
}
/* Hover */
g:hover path {
fill: #fff;
}
g:hover rect.indicator-bg {
fill: oklch(37.4% 0.01 67.558);
}
g:hover {
cursor: pointer;
}
/* Selected state: white + glow */
g.selected path {
fill: #fff;
filter: url(#glow);
}
</style>
<!-- === Top Row === -->
<g id="top-left">
<rect class="indicator-bg" x="0" y="0" width="30" height="30"/>
<path d="M7.5 7.5h15v3h-15z M7.5 7.5h3v15h-3z"/>
</g>
<g id="top-center">
<rect class="indicator-bg" x="30" y="0" width="30" height="30"/>
<path d="M37.5 7.5h15v3h-15z"/>
</g>
<g id="top-right">
<rect class="indicator-bg" x="60" y="0" width="30" height="30"/>
<path d="M67.5 7.5h15v3h-15z M79.5 7.5h3v15h-3z"/>
</g>
<!-- === Middle Row === -->
<g id="center-left">
<rect class="indicator-bg" x="0" y="30" width="30" height="30"/>
<path d="M7.5 37.5h3v15h-3z"/>
</g>
<g id="center-center">
<rect class="indicator-bg" x="30" y="30" width="30" height="30"/>
<path d="M41 41h8v8h-8z"/>
</g>
<g id="center-right">
<rect class="indicator-bg" x="60" y="30" width="30" height="30"/>
<path d="M79.5 37.5h3v15h-3z"/>
</g>
<!-- === Bottom Row === -->
<g id="bottom-left">
<rect class="indicator-bg" x="0" y="60" width="30" height="30"/>
<path d="M7.5 79.5h15v3h-15z M7.5 67.5h3v15h-3z"/>
</g>
<g id="bottom-center">
<rect class="indicator-bg" x="30" y="60" width="30" height="30"/>
<path d="M37.5 79.5h15v3h-15z"/>
</g>
<g id="bottom-right">
<rect class="indicator-bg" x="60" y="60" width="30" height="30"/>
<path d="M67.5 79.5h15v3h-15z M79.5 67.5h3v15h-3z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,18 +0,0 @@
@import "tailwindcss";
@import "tailwindcss/utilities";
@layer base {
button {
@apply
flex flex-row items-center gap-1
px-4 py-1
bg-black
text-white font-medium
border-1 border-stone-700
cursor-pointer;
&:hover {
@apply border-primary-500 text-primary-300;
}
}
}

View File

@ -1,25 +0,0 @@
@font-face {
font-family: "Heebo";
src: url("../fonts/Heebo.ttf") format("truetype-variations");
font-weight: 100 900;
font-stretch: 75% 125%;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Source Code Pro";
src: url("../fonts/SourceCodePro.ttf") format("truetype-variations");
font-weight: 200 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Source Code Pro";
src: url("../fonts/SourceCodePro-Italic.ttf") format("truetype-variations");
font-weight: 200 900;
font-style: italic;
font-display: swap;
}

View File

@ -1,135 +0,0 @@
@import "tailwindcss";
@import "tailwindcss/utilities";
.input, .range-input {
@apply py-1 px-2 bg-black/25 border-b border-white/50 w-full relative;
&:active, &:focus, &:focus-within {
@apply border-b-1 border-b-primary-400;
}
&.no-bg {
background-color: transparent;
border-color: transparent;
}
input {
width: 100%;
outline: none;
border: 1px solid transparent;
background-color: transparent;
color: #fff;
}
.unit {
position: absolute;
right: 0px;
pointer-events: none;
opacity: 0.69;
font-size: 0.8rem;
top: 0;
transform: translateY(69%);
}
}
.range-input {
@apply flex flex-row;
* {
@apply mx-2;
}
input {
max-width: 5rem;
}
input[type=range] {
max-width: none;
}
}
.field {
@apply flex flex-row w-full my-2 items-center;
&.l2 {
margin-left: 4rem;
}
.label {
flex: 0 0 25%;
min-width: 16rem;
text-align: right;
padding-right: 1rem;
.color-emphasis {
@apply text-primary-400;
}
.sub-label {
font-size: 0.9em;
opacity: 0.69;
}
}
.input, .range-input, .checkbox {
flex-grow: 1;
flex-shrink: 1;
max-width: 24rem;
min-width: 16rem;
}
.has-hint {
display: flex;
flex-direction: column;
}
.select {
flex-grow: 1;
flex-shrink: 1;
min-width: 12px;
max-width: 24rem;
select {
@apply bg-stone-950 border border-primary-800 text-white px-4 py-1;
min-width: 12px;
max-width: 100%;
outline: none;
font: inherit;
font-size: inherit;
option {
@apply bg-stone-950 text-white;
&:checked {
@apply text-primary-400;
}
&:before {
content: ">";
display: none;
background-color: #f41 !important;
font-size: 5rem;;
}
&:hover {
@apply text-black bg-primary-400;
}
}
}
}
}
.hint {
font-size: 0.8rem;
opacity: 0.7;
margin-top: 0.25rem;
margin-bottom: 0.75rem;
margin-left: 5rem;
box-sizing:border-box;
&.error {
color: #f41;
}
}

View File

@ -1,68 +0,0 @@
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
const positionMap: Record<VideoAlignmentType, string> = {
[VideoAlignmentType.Left]: 'left',
[VideoAlignmentType.Center]: 'center',
[VideoAlignmentType.Right]: 'right',
[VideoAlignmentType.Top]: 'top',
[VideoAlignmentType.Bottom]: 'bottom',
[VideoAlignmentType.Default]: ''
};
const location2enum: Record<string, VideoAlignmentType> = {
left: VideoAlignmentType.Left,
center: VideoAlignmentType.Center,
right: VideoAlignmentType.Right,
top: VideoAlignmentType.Top,
bottom: VideoAlignmentType.Bottom
};
/**
* Sets alignment indicator state of the video alignment SVG
* @param svg SVG element
* @param x horizontal alignment
* @param y vertical alignment
* @returns
*/
export function setVideoAlignmentIndicatorState(
svg: SVGSVGElement,
x: VideoAlignmentType,
y: VideoAlignmentType
) {
// reset all indicators
svg.querySelectorAll<SVGGElement>('g').forEach(g => g.classList.remove('selected'));
// select the appropriate square
if (x === VideoAlignmentType.Default || y === VideoAlignmentType.Default) {
return;
}
const gId = `${positionMap[y]}-${positionMap[x]}`;
const selected = svg.getElementById(gId);
if (selected) selected.classList.add('selected');
}
/**
* Sets up interaction for the video alignment SVG
* @param svg SVG element
* @param callback function to be called, with x and y alignment (VideoAlignmentType)
*/
export function setupVideoAlignmentIndicatorInteraction(
svg: SVGSVGElement,
callback: (x: VideoAlignmentType, y: VideoAlignmentType) => void
) {
svg.querySelectorAll<SVGGElement>('g').forEach(g => {
g.addEventListener('click', () => {
const [y, x] = g.id.split('-');
if (!x || !y)
return;
callback(
location2enum[x],
location2enum[y]
)
});
});
}

View File

@ -17,7 +17,7 @@ const server = new UWServer();
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "update") {
chrome.tabs.create({
url: chrome.runtime.getURL("ui/pages/updated/index.html")
url: chrome.runtime.getURL("install/updated/updated.html")
});
}
});

View File

@ -1,28 +0,0 @@
{
"content": [
"./src/**/*.{vue,js,ts,jsx,tsx}"
],
"theme": {
"extend": {
"colors": {
"primary": {
"50": "#ffddbe",
"100": "#ffcda0",
"200": "#ffbd83",
"300": "#ffac66",
"400": "#ff9a4a",
"500": "#ff872c",
"600": "#de7622",
"700": "#be6518",
"800": "#9f540e",
"900": "#824406",
"950": "#663400"
}
},
"fontFamily": {
"sans": ["Heebo", "ui-sans-serif", "system-ui"],
"mono": ["Source Code Pro", "ui-monospace", "monospace"]
}
}
}
}

View File

@ -1,10 +1,9 @@
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "node",
"outDir": "./ts-out",
"allowJs": true,
"target": "esnext",
"target": "es2018",
"types": [
"chrome",
"node"
@ -13,15 +12,6 @@
"node_modules/@types",
"node_modules/web-ext-types"
],
"baseUrl": "./src",
"allowSyntheticDefaultImports": true,
"skipDefaultLibCheck": true,
"esModuleInterop": true,
"paths": {
"@src/*": [ "*" ],
"@csui/*": [ "csui/*" ],
"@/*": [ "*" ]
}
},
"include": [ "./src/**/*" ],
}

View File

@ -1,8 +1,9 @@
const webpack = require('webpack');
const ejs = require('ejs');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const WebpackShellPluginNext = require('webpack-shell-plugin-next');
const WebpackShellPlugin = require('webpack-shell-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ChromeExtensionReloader = require('webpack-chrome-extension-reloader');
const { VueLoaderPlugin } = require('vue-loader');
const path = require('path');
@ -17,12 +18,9 @@ const config = {
'ext/uw': './ext/uw.js',
'uw-bg': './uw-bg.js',
'csui/csui-popup': './csui/csui-popup.js',
// 'csui/settings': './csui/settings.js',
// 'csui/csui': './csui/csui.js',
// 'ui/pages/updated/updated': './ui/pages/updated/updated.js',
'ui/pages/updated/updated': './ui/pages/updated/updated.js',
'ui/pages/settings/settings': './ui/pages/settings/settings.js',
'csui/csui': './csui/csui.js',
// 'install/first-time/first-time':'./install/first-time/first-time.js',
'install/updated/updated': './install/updated/updated.js',
},
output: {
path: __dirname + `/dist-${process.env.BROWSER == 'firefox' ? 'ff' : process.env.BROWSER}`,
@ -35,9 +33,6 @@ const config = {
alias: {
'@src': path.resolve(__dirname, 'src'),
'@csui': path.resolve(__dirname, 'src/csui'),
'@ui': path.resolve(__dirname, 'src/ui'),
'@components': path.resolve(__dirname, 'src/ui/components'),
'@': path.resolve(__dirname, 'src'),
},
// maybe we'll move vue stuff to TS some day, but today is not the day
extensions: [
@ -49,77 +44,89 @@ const config = {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.vue$/,
loaders: 'vue-loader',
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.(sc|c)ss$/,
use: [
// MiniCssExtractPlugin.loader,
'vue-style-loader',
{
loader: 'css-loader',
// modules: {
// localIdentName: "[name]-[hash]"
// }
// options: {
// modules: true,
// // localIdentName: "🔶uw_[local]"
// localIdentName: "[name]-[hash]"
// // localIdentName: "uw_[local]"
// }
},
{
loader: 'sass-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
transpileOnly: true,
sourceMap: true
}
}
],
},
// {
// test: /\.scss$/,
// use: [
// // MiniCssExtractPlugin.loader,
// 'css-loader',
// // {
// // loader: 'css-loader',
// // // options: {
// // // modules: true,
// // // localIdentName: "🔶uw_[local]"
// // // }
// // },
// 'sass-loader'
// ],
// },
{
test: /\.vue$/,
use: [
test: /\.(png|jpg|gif|svg|ico)$/,
loader: 'file-loader',
options: {
name: '[path][name].[ext]',
},
},
{
loader: 'vue-loader'
}
]
test: /\.(woff(2)?)$/,
loader: 'file-loader',
options: {
name: '[path][name].[ext]',
},
},
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader'
}
]
loader: "babel-loader"
},
{
test: /\.(sc|c|postc)ss$/,
use: [
'vue-style-loader',
{
loader: 'css-loader',
options: {
// Uncomment if you want CSS modules
// modules: {
// localIdentName: "[name]-[hash]"
// }
}
},
'postcss-loader',
]
},
{
test: /\.(png|jpg|webp|gif|svg|ico)$/,
type: 'asset/resource',
generator: {
filename: '[path][name][ext]' // Webpack 5 uses generator.filename
}
},
{
test: /\.(woff(2)?)$/,
type: 'asset/resource',
generator: {
filename: '[path][name][ext]'
}
}
]
],
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: '[name].css',
}),
new CopyWebpackPlugin({
patterns: [
{ from: 'res', to: 'res', globOptions: { ignore: ['css', 'css/**'] } },
{ from: 'ext', to: 'ext', globOptions: { ignore: ['conf/*', 'lib/**'] } },
{ from: 'csui', to: 'csui', globOptions: { ignore: ['src'] }},
{ from: 'ui/res', to: 'ui/res' },
new CopyWebpackPlugin([
{ from: 'res', to: 'res', ignore: ['css', 'css/**']},
{ from: 'ext', to: 'ext', ignore: ['conf/*', 'lib/**']},
{ from: 'csui', to: 'csui', ignore: ['src']},
{ from: 'install', to: 'install' },
// we need to get webextension-polyfill and put it in common/lib
{ from: '../node_modules/webextension-polyfill/dist/browser-polyfill.js', to: 'common/lib/browser-polyfill.js'},
@ -127,15 +134,13 @@ const config = {
// This is extension icon, as used on extension lists and/or extension's action button
// This folder does not contain any GUI icons — these are in /res/icons.
// (TODO: check if this copy is even necessary — /icons has same content as /res/icons)
{ from: 'icons', to: 'icons', globOptions: { ignore: ['icon.xcf'] } },
{ from: 'icons', to: 'icons', ignore: ['icon.xcf'] },
{ from: 'csui/csui-popup.html', to: 'csui/csui-popup.html', transform: transformHtml },
{ from: 'csui/csui-overlay-normal.html', to: 'csui/csui.html', transform: transformHtml },
{ from: 'csui/csui-overlay-dark.html', to: 'csui/csui-dark.html', transform: transformHtml },
{ from: 'csui/csui-overlay-light.html', to: 'csui/csui-light.html', transform: transformHtml },
{ from: 'ui/pages/settings/index.html', to: 'ui/pages/settings/index.html', transform: transformHtml },
// { from: 'ui/pages/installed/index.html', to: 'ui/pages/installed/index.html', transform: transformHtml },
{ from: 'ui/pages/updated/index.html', to: 'ui/pages/updated/index.html', transform: transformHtml },
{ from: 'ui/pages/settings/index.html', to: 'ui/pages/settings/index.html', transform: transformHtml },
// { from: 'install/first-time/first-time.html', to: 'install/first-time/first-time.html', transform: transformHtml},
{ from: 'install/updated/updated.html', to: 'install/updated/updated.html', transform: transformHtml },
{
from: 'manifest.json',
to: 'manifest.json',
@ -196,14 +201,9 @@ const config = {
return JSON.stringify(jsonContent, null, 2);
},
},
]
}),
new WebpackShellPluginNext({
onBuildEnd: {
scripts: ['node scripts/remove-evals.js'],
blocking: true,
parallel: false,
},
]),
new WebpackShellPlugin({
onBuildEnd: ['node scripts/remove-evals.js'],
}),
new webpack.DefinePlugin({
'process.env.BROWSER': JSON.stringify(process.env.BROWSER),
@ -239,7 +239,7 @@ if (config.mode === 'production') {
if (process.env.HMR === 'true') {
config.plugins = (config.plugins || []).concat([
// new ChromeExtensionReloader(), // no longer a thing for webpack 5
new ChromeExtensionReloader(),
]);
}