Add zoom sliders to in-player popup (non-functional)

This commit is contained in:
Tamius Han 2026-01-03 03:19:03 +01:00
parent 56321e52fc
commit e651e51c50
89 changed files with 616 additions and 32612 deletions

View File

@ -2,7 +2,9 @@ export interface MenuItemConfig {
label: string;
subitems?: MenuItemConfig[];
action?: () => void;
customHTML?: HTMLElement;
customHTML?: HTMLElement | string;
customId?: string;
customClassList?: string;
}
export enum MenuPosition {

View File

@ -1,16 +1,13 @@
import { SiteSupportLevel } from './../enums/SiteSupportLevel.enum';
import { Action } from '../../../node_modules/vuex/types/index'
import { AardPollingOptions } from '../../ext/lib/aard/enums/aard-polling-options.enum'
import { AardSubtitleCropMode } from '../../ext/lib/aard/enums/aard-subtitle-crop-mode.enum'
import AntiGradientMode from '../enums/AntiGradientMode.enum'
import AspectRatioType from '../enums/AspectRatioType.enum'
import CropModePersistence from '../enums/CropModePersistence.enum'
import EmbeddedContentSettingsOverridePolicy from '../enums/EmbeddedContentSettingsOverridePolicy.enum'
import ExtensionMode from '../enums/ExtensionMode.enum'
import { SiteSupportLevel } from '../enums/SiteSupportLevel.enum'
import StretchType from '../enums/StretchType.enum'
import VideoAlignmentType from '../enums/VideoAlignmentType.enum'
import { PlayerDetectionMode } from '../enums/PlayerDetectionMode.enum';
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 { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
import AspectRatioType from '@src/common/enums/AspectRatioType.enum'
import CropModePersistence from '@src/common/enums/CropModePersistence.enum'
import EmbeddedContentSettingsOverridePolicy from '@src/common/enums/EmbeddedContentSettingsOverridePolicy.enum'
import ExtensionMode from '@src/common/enums/ExtensionMode.enum'
import StretchType from '@src/common/enums/StretchType.enum'
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'
import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
export enum ExtensionEnvironment {
Normal = ExtensionMode.All,

View File

@ -1,62 +0,0 @@
<template>
<div class="uw-clickthrough relative w-full h-full">
<template v-for="rectangle of drawnRectangles" :key="rectangle.id ?? rectangle">
<!-- Player element overlays -->
<div class="absolute z-index-overlay"
:style="rectangle.style"
>
<!-- used for drawing player element overlay rectangles - keep this section empty -->
</div>
<!-- Notification overlay -->
<div class="absolute z-index-notification notification-area">
<template v-for="notification of displayedNotifications" :key="notification.id">
<div class="notification d-flex flex-row">
<div class="notification-icon">
<mdicon :name="notification.icon ?? 'alert'" :size="128"></mdicon>
</div>
<div class="notification-text">
<h3 class="notification-title">{{ notification.title }}</h3>
<p class="notification-verbose">{{ notification.text }}</p>
</div>
</div>
</template>
</div>
</template>
</div>
</template>
<script>
import UIProbeMixin from './src/utils/UIProbeMixin';
export default {
components: {
},
mixins: [
UIProbeMixin
],
data() {
return {
drawnRectangles: [],
displayedNotifications: [],
}
},
async created() {
/**
* Setup the "companion" onMouseMove handler to the one in the content script.
* We can handle events with the same function we use to handle events from
* the content script.
*/
document.addEventListener('mousemove', (event) => {
this.handleProbe({
coords: {
x: event.clientX,
y: event.clientY
}
}, this.origin);
});
}
}
</script>

View File

@ -1,740 +0,0 @@
<template>
<div
class="context-spawn uw-ui-area"
style="z-index: 1000;"
v-if="!triggerZoneEditorVisible"
>
<div
class="spawn-container uw-ui-trigger"
:style="triggerZoneStyles"
>
&nbsp;
</div>
</div>
<div
v-if="contextMenuActive || settingsInitialized && uwTriggerZoneVisible && !isGlobal"
class="context-spawn uw-ui-area"
style="z-index: 1001"
>
<GhettoContextMenu
alignment="right" class="uw-menu"
@mouseenter="() => {preventContextMenuHide(); newFeatureViewUpdate('uw6.ui-popup')}"
@mouseleave="allowContextMenuHide()"
>
<template v-slot:activator>
Ultrawidify
</template>
<slot>
<!--
Didn't manage to ensure that extension status pops up above other menu items in less than 3 minutes with z-index,
so wrapping 'status' and 'real menu items' in two different divs, ordering them in the opposite way, and then
ensuring correct ordering with flex-direction: column-reverse ended up being easier and faster.
-->
<div class="uw-clickable menu-width flex-reverse-order">
<div style="z-index: 1000">
<GhettoContextMenu alignment="right">
<template v-slot:activator>
Crop
</template>
<slot>
<GhettoContextMenuOption
v-for="(command, index) of settings?.active.commands.crop"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
>
</GhettoContextMenuOption>
</slot>
</GhettoContextMenu>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
Stretch
</template>
<slot>
<GhettoContextMenuOption
v-for="(command, index) of settings?.active.commands.stretch"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
>
</GhettoContextMenuOption>
</slot>
</GhettoContextMenu>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
Zoom
</template>
<slot>
<GhettoContextMenuOption
v-for="(command, index) of settings?.active.commands.zoom"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
>
</GhettoContextMenuOption>
<GhettoContextMenuItem
:disableHover="true"
>
<ZoomControl
:settings="settings"
:eventBus="eventBus"
/>
</GhettoContextMenuItem>
</slot>
</GhettoContextMenu>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
<div class="context-item">
Align
</div>
</template>
<slot>
<GhettoContextMenuItem :disableHover="true" :css="{'reduced-padding': true}">
<AlignmentOptionsControlComponent
:eventBus="eventBus"
>
</AlignmentOptionsControlComponent>
</GhettoContextMenuItem>
</slot>
</GhettoContextMenu>
<!-- shortcut for configuring UI -->
<GhettoContextMenuOption
v-if="settings.active.newFeatureTracker?.['uw6.ui-popup']?.show > 0"
@click="showUwWindow('playerUiSettings')"
>
<span style="color: #fa6;">Change when and if<br/>this popup appears</span>
<span style="font-size: 0.8rem">
<span style="font-size: 0.8rem; opacity: 0.5">This menu option will show {{settings.active.newFeatureTracker?.['uw6.ui-popup']?.show}} more<br/> times; or until clicked or dismissed.<br/>
Also accessible via:<br/> <span style="font-size: 0.85em">EXTENSION SETTINGS > UI AND KEYBOARD</span>.
</span>
<br/>
<a style="color: #fa6; cursor: pointer;" @click="() => acknowledgeNewFeature('uw6.ui-popup')">Dismiss this option</a>
</span>
</GhettoContextMenuOption>
<!-- -->
<GhettoContextMenuOption
@click="showUwWindow()"
label="Extension settings"
>
</GhettoContextMenuOption>
<GhettoContextMenuOption
@click="showUwWindow('playerDetection')"
label="Incorrect cropping?"
>
</GhettoContextMenuOption>
<GhettoContextMenuOption
@click="showUwWindow('about')"
label="Not working?"
>
</GhettoContextMenuOption>
</div>
<div style="z-index: 10000">
<GhettoContextMenuItem
class="extension-status-messages"
:disableHover="true"
>
Site compatibility:
<SupportLevelIndicator
:siteSupportLevel="siteSupportLevel"
supportLevelStyle="font-size: 0.69rem !important;"
tooltipStyle="font-size: 0.8rem;"
>
</SupportLevelIndicator>
<div v-if="statusFlags.hasDrm" class="aard-blocked">
Autodetection blocked<br/>
by <a style="color: #fff" href="https://en.wikipedia.org/wiki/Digital_rights_management" target="_blank">DRM</a>.
</div>
<div v-else-if="statusFlags.aardErrors?.cors" class="aard-blocked">
Autodetection blocked<br/>
by site/browser (CORS).
</div>
<div v-else-if="statusFlags.aardErrors?.webglError" class="aard-blocked">
Autodetection unavailable<br/>
(webgl error)
</div>
</GhettoContextMenuItem>
</div>
</div>
</slot>
</GhettoContextMenu>
</div>
<div
v-if="settingsInitialized && uwWindowVisible"
class="uw-window flex flex-col uw-clickable uw-ui-area"
:class="{'fade-out': uwWindowFadeOut}"
>
<PlayerUIWindow
:settings="settings"
:eventBus="eventBus"
:logger="logger"
:in-player="!isGlobal"
:site="site"
:defaultTab="defaultWindowTab"
@close="uwWindowVisible = false"
@preventClose="(event) => uwWindowFadeOutDisabled = event"
></PlayerUIWindow>
</div>
<div
v-if="triggerZoneEditorVisible"
class="context-spawn uw-ui-area"
style="z-index: 1000;"
>
<TriggerZoneEditor
:settings="settings"
:eventBus="eventBus"
:playerDimensions="playerDimensions"
>
</TriggerZoneEditor>
</div>
</template>
<script>
import PlayerUIWindow from '@csui/src/PlayerUIWindow.vue';
import GhettoContextMenu from '@csui/src/components/GhettoContextMenu.vue';
import GhettoContextMenuItem from '@csui/src/components/GhettoContextMenuItem.vue';
import GhettoContextMenuOption from '@csui/src/components/GhettoContextMenuOption.vue';
import AlignmentOptionsControlComponent from '@csui/src/PlayerUiPanels/AlignmentOptionsControlComponent.vue';
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import Settings from '@src/ext/lib/settings/Settings';
import EventBus from '@src/ext/lib/EventBus';
import UIProbeMixin from '@csui/src/utils/UIProbeMixin';
import KeyboardShortcutParserMixin from '@csui/src/utils/KeyboardShortcutParserMixin';
import CommsMixin from '@csui/src/utils/CommsMixin';
import SupportLevelIndicator from '@csui/src/components/SupportLevelIndicator.vue';
import TriggerZoneEditor from '@csui/src/components/TriggerZoneEditor.vue';
import ZoomControl from '@csui/src/popup/player-menu/ZoomControl.vue';
import { LogAggregator } from '@src/ext/lib/logging/LogAggregator';
import { ComponentLogger } from '@src/ext/lib/logging/ComponentLogger';
export default {
components: {
PlayerUIWindow,
GhettoContextMenu,
GhettoContextMenuItem,
GhettoContextMenuOption,
AlignmentOptionsControlComponent,
SupportLevelIndicator,
TriggerZoneEditor,
ZoomControl,
},
mixins: [
UIProbeMixin,
KeyboardShortcutParserMixin,
CommsMixin
],
data() {
return {
uwTriggerZoneVisible: false,
uwTriggerZoneTimeout: undefined,
uwTriggerRegionConf: {
left: "10%",
top: "10%",
height: "30%",
width: "30%",
maxWidth: "24rem",
maxHeight: "13.37rem",
},
uwWindowFadeOutDisabled: false,
uwWindowFadeOut: false,
uwWindowCloseTimeout: undefined,
uwWindowVisible: false,
triggerZoneEditorVisible: false,
// component properties
settings: {},
BrowserDetect: BrowserDetect,
settingsInitialized: false,
eventBus: new EventBus(),
logAggregator: null,
logger: null,
// NOTE: chromium doesn't allow us to access window.parent.location
// meaning we will have to correct this value from our uwui-probe
// messages ... which is a bummer.
site: null,
origin: '*', // will be set appropriately once the first uwui-probe event is received
lastProbeTs: null,
isGlobal: true,
disabled: false,
contextMenuActive: false,
triggerZoneActive: false,
uiVisible: true,
debugData: {
resizer: {},
player: {},
},
debugDataPrettified: '',
// in global overlay, this property is used to determine
// if closing the window should emit uw-set-ui-state
// event on eventBus
showPlayerUIAfterClose: false,
statusFlags: {
hasDrm: undefined,
aardErrors: undefined,
},
defaultWindowTab: 'videoSettings',
saveState: {},
siteSettings: undefined,
previewZoneVisible: false,
};
},
computed: {
// LPT: NO ARROW FUNCTIONS IN COMPUTED,
// IS SUPER HARAM
// THINGS WILL NOT WORK IF YOU USE ARROWS
windowWidth() {
return window.innerWidth;
},
windowHeight() {
return window.innerHeight;
},
// 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';
}
},
watch: {
showUi(visible) {
if (visible !== undefined) {
this.uiVisible = visible;
}
},
resizerDebugData(newData) {
this.debugData.resizer = newData;
this.debugDataPrettified = JSON.stringify(this.debugData, null, 2);
},
playerDebugData(newData) {
this.debugData.player = newData;
this.debugDataPrettified = JSON.stringify(this.debugData, null, 2);
}
},
async created() {
this.logAggregator = new LogAggregator('player-overlay');
this.logger = new ComponentLogger(this.logAggregator, 'PlayerOverlay.vue');
this.settings = new Settings({afterSettingsSaved: this.updateConfig, logger: this.logger});
this.settings.listenAfterChange(() => this.updateTriggerZones());
await this.settings.init();
this.settingsInitialized = true;
// set up communication with client script.
// NOTE: companion onmousemove is set up in UIProbeMixin
window.addEventListener('message', event => {
this.handleMessage(event);
});
this.eventBus.subscribeMulti(
{
'uw-config-broadcast': {
function:
(data) => {
switch (data.type) {
case 'drm-status':
this.statusFlags.hasDrm = data.hasDrm;
break;
case 'aard-error':
this.statusFlags.aardErrors = data.aardErrors;
break;
case 'player-dimensions':
this.playerDimensionsUpdate(data.data);
break;
}
}
},
'uw-set-ui-state': {
function: (data) => {
if (data.globalUiVisible !== undefined) {
if (this.isGlobal) {
if (data.globalUiVisible) {
this.showUwWindow();
} else {
this.hideUwWindow(true);
}
// this.showPlayerUIAfterClose = data.showPlayerUIAfterClose;
} else {
// non global UIs are hidden while global overlay
// is visible and vice versa
// this.disabled = data.globalUiVisible;
this.saveState = {
uwWindowVisible: this.uwWindowVisible,
uwWindowFadeOutDisabled: this.uwWindowFadeOutDisabled,
uwWindowFadeOut: this.uwWindowFadeOut
};
this.uwWindowFadeOutDisabled = false;
this.hideUwWindow(true);
}
}
}
},
'uw-restore-ui-state': {
function: (data) => {
if (this.saveState) {
if (this.saveState.uwWindowVisible) {
this.showUwWindow();
}
this.uwWindowFadeOutDisabled = this.saveState.uwWindowFadeOutDisabled;
this.uwWindowFadeOut = this.saveState.uwWindowFadeOut;
}
this.saveState = {};
}
},
'uw-restore-ui-state': {
function: (data) => {
if (this.saveState) {
if (this.saveState.uwWindowVisible) {
this.showUwWindow();
}
this.uwWindowFadeOutDisabled = this.saveState.uwWindowFadeOutDisabled;
this.uwWindowFadeOut = this.saveState.uwWindowFadeOut;
}
this.saveState = {};
}
},
'ui-trigger-zone-update': {
function: (data) => {
this.showTriggerZonePreview = data.previewZoneVisible;
// this.;
}
},
'start-trigger-zone-edit': {
function: () => {
this.triggerZoneEditorVisible = true;
this.uwWindowVisible = false;
}
},
'finish-trigger-zone-edit': {
function: () => {
this.triggerZoneEditorVisible = false;
this.showUwWindow('playerUiSettings');
}
},
},
this
);
this.sendToParentLowLevel('uwui-get-role', null);
this.sendToParentLowLevel('uwui-get-theme', null);
// console.log('player overlay created get player dims:')
this.sendToParentLowLevel('uw-bus-tunnel', {
action: 'get-player-dimensions'
});
},
destroyed() {
this.eventBus.unsubscribeAll(this)
},
methods: {
/**
* Gets URL of the browser settings page (i think?)
*/
getUrl(url) {
return BrowserDetect.getURL(url);
},
/**
* Mostly intended to process messages received via window.addEventListener('message').
* This method should include minimal logic instead, it should only route messages
* to the correct function down the line.
*/
handleMessage(event) {
switch (event.data.action) {
case 'uwui-probe':
if (!this.site) {
this.origin = event.origin;
this.site = event.origin.split('//')[1];
this.siteSettings = this.settings.getSiteSettings({site: this.site, isIframe: event.data.isIframe});
}
return this.handleProbe(event.data, event.origin); // handleProbe is defined in UIProbeMixin
case 'uw-bus-tunnel':
return this.handleBusTunnelIn(event.data.payload);
case 'uwui-set-role':
this.isGlobal = event.data.payload.role === 'global';
this.sendToParentLowLevel('uwui-interface-ready', true);
break;
}
},
/**
* Handles trigger zone
*/
handleTriggerZone(mouseInside) {
// this.triggerZoneActive = mouseInside;
},
acknowledgeNewFeature(featureKey) {
this.settings.active.newFeatureTracker[featureKey].show = 0;
this.settings.active.newFeatureTracker[featureKey].acknowledged = true;
this.settings.saveWithoutReload();
},
newFeatureViewUpdate(featureKey) {
if (!this.settings.active.newFeatureTracker[featureKey]) {
return;
}
try {
this.settings.active.newFeatureTracker[featureKey].show--;
this.settings.saveWithoutReload();
if (this.settings.active.newFeatureTracker[featureKey]?.show < 0) {
this.acknowledgeNewFeature(featureKey);
}
} catch (e) {
// do nothing
}
},
/**
* Sends message to parent _without_ using event bus.
*/
sendToParentLowLevel(action, payload, lowLevelExtras = {}) {
window.parent.postMessage(
{
action, payload, ...lowLevelExtras
},
'*'
);
},
preventContextMenuHide() {
this.contextMenuActive = true;
// refresh DRM status
this.eventBus.sendToTunnel('get-drm-status');
},
allowContextMenuHide() {
this.contextMenuActive = false;
},
setTriggerZoneActive(active, event) {
this.triggerZoneActive = active;
},
showUwWindow(tab) {
this.defaultWindowTab = tab; // can be undefined
this.uwWindowFadeOut = false;
this.uwWindowVisible = true;
this.uwTriggerZoneVisible = false;
this.allowContextMenuHide();
// refresh DRM status
this.eventBus.sendToTunnel('get-drm-status');
// if (this.isGlobal) {
// this.sendToParentLowLevel('uwui-clickable', undefined, {clickable: true});
// }
},
hideUwWindow(skipTimeout = false) {
if (this.uwWindowFadeOutDisabled) {
return;
}
const timeout = skipTimeout ? 0 : 1100;
this.uwWindowCloseTimeout = setTimeout(
() => {
this.uwWindowVisible = false;
// Global UI has some extra housekeeping to do when window gets hidden
if (this.isGlobal) {
this.sendToParentLowLevel('uwui-global-window-hidden', {});
}
},
timeout
);
this.uwWindowFadeOut = true;
},
cancelUwWindowHide() {
this.uwWindowFadeOut = false;
clearTimeout(this.uwWindowCloseTimeout);
},
handleBusTunnelIn(payload) {
this.eventBus.send(
payload.action,
payload.config,
{
...payload.context,
borderCrossings: {
...payload.context?.borderCrossings,
iframe: true
}
}
);
},
updateConfig() {
this.settings.init();
this.$nextTick( () => this.$forceUpdate());
}
}
}
</script>
<style lang="scss">
.ard-blocked {
color: rgb(219, 125, 48) !important;
background-color: rgba(0,0,0,0.85) !important;
}
</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>
.uw-hover {
position: absolute;
z-index: 999999999999999999;
}
.reduced-padding {
padding: 1rem !important;
}
.uv-hover-trigger-region {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 0.5rem dashed #fff;
color: #fff;
backdrop-filter: blur(0.5rem) brightness(0.5);
}
.uw-window {
position: fixed;
top: 10%;
left: 10%;
z-index: 999999999999999999;
width: 2500px;
height: 1200px;
max-width: 80%;
max-height: 80%;
pointer-events: all !important;
opacity: 1;
backdrop-filter: blur(16px) saturate(120%);
&.fade-out {
opacity: 0;
transition: opacity 0.5s;
transition-delay: 0.5s;
}
}
.gib-bg {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(16px) saturate(120%);
width: fit-content;
block-size: fit-content;
}
.ui-warning {
position: absolute;
top: 0;
transform: translateY(-100%);
max-width: 16rem;
width: 16rem;
overflow: hidden;
overflow-wrap: break-word;
white-space: normal;
word-break: break-word;
word-wrap: break-word;
}
.context-spawn {
position: absolute;
top: 0;
left: 0;
width: 100dvw;
height: 100dvh;
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: row;
align-content: center;
align-items: center;
color: #fff;
}
.extension-status-messages {
z-index: 1000;
text-transform: uppercase;
display: flex;
flex-direction: column;
text-align: center;
font-size: 0.9rem;
> * {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
}
.flex-reverse-order {
display: flex;
flex-direction: column-reverse;
}
.aard-blocked {
font-size: 0.8rem;
color: #fa6;
}
.trigger-zone-preview {
border: 4px solid #fa4;
}
.debug-1 {
border: 1px solid yellow;
&:hover {
background-color: rbba(255,255,0,0.5);
}
}
.debug-2 {
border: 1px solid blue;
&:hover {
background-color: rbba(0,0,255,.5);
}
}
</style>

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 - Content Script User Interface (global overlay)</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="csui-global.js"></script> -->
</body>
</html>

View File

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

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en" style="position: relative">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<title>Ultrawidify - Content Script User Interface (in-player overlay)</title>
<!-- <link rel="stylesheet" href="csui.css"> -->
</head>
<body class="uw-ultrawidify-container-root" style="background-color: transparent;">
<div id="app"></div>
<!-- <script src="csui.js"></script> -->
</body>
</html>

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en" style="position: relative">
<head>
<meta charset="UTF-8">
<meta name="color-scheme" content="light">
<title>Ultrawidify - Content Script User Interface (in-player overlay)</title>
<!-- <link rel="stylesheet" href="csui.css"> -->
</head>
<body class="uw-ultrawidify-container-root" style="background-color: transparent;">
<div id="app"></div>
<!-- <script src="csui.js"></script> -->
</body>
</html>

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en" style="position: relative">
<head>
<meta charset="UTF-8">
<title>Ultrawidify - Content Script User Interface (in-player overlay)</title>
<!-- <link rel="stylesheet" href="csui.css"> -->
</head>
<body class="uw-ultrawidify-container-root" style="background-color: transparent;">
<div id="app"></div>
<!-- <script src="csui.js"></script> -->
</body>
</html>

View File

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

View File

@ -1,91 +0,0 @@
<template>
<div class="flex flex-col">
<div>Ultrawidify</div>
<div class="flex flex-row">
<div v-if="notification.icon">
<mdicon :name="notification.icon"></mdicon>
</div>
<div class="flex flex-grow flex-col">
<div v-if="notification.title">{{notification.title}}</div>
<div v-if="notification.description">{{notification.description}}</div>
</div>
</div>
<div v-if="notification.actions" class="flex flex-row flex-end">
<button
v-for="(action, index) of notification.actions"
:key="index"
>
{{ action.label }}
</button>
</div>
<div>Notification countdown</div>
</div>
</template>
<script>
export default {
components: {
},
mixins: [
],
data() {
return {
notification: {}
};
},
computed: {
},
watch: {
},
async created() {
this.logger = new Logger();
window.addEventListener('message', this.handleMessage);
this.sendToParentLowLevel('init-complete', {});
},
destroyed() {
window.removeEventListener('message', this.handleMessage);
},
methods: {
/**
* Mostly intended to process messages received via window.addEventListener('message').
* This method should include minimal logic instead, it should only route messages
* to the correct function down the line.
*/
handleMessage(event) {
switch (event.data.action) {
case 'notification-data':
this.notification = event.data.payload;
}
},
/**
* Sends message to parent _without_ using event bus.
*/
sendToParentLowLevel(action, payload, lowLevelExtras = {}) {
window.parent.postMessage(
{
action, payload, ...lowLevelExtras
},
'*'
);
},
}
}
</script>
<style lang="scss">
.ard-blocked {
color: rgb(219, 125, 48) !important;
background-color: rgba(0,0,0,0.85) !important;
}
</style>
<style lang="scss" src="../../src/res-common/panels.scss" scoped module></style>
<style lang="scss" src="../../src/res-common/common.scss" scoped module></style>
<style lang="scss" scoped>
</style>

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en" style="position: relative">
<head>
<meta charset="UTF-8">
<title>Ultrawidify - Content Script User Interface (in-player overlay)</title>
<!-- <link rel="stylesheet" href="csui.css"> -->
</head>
<body class="uw-ultrawidify-container-root" style="background-color: transparent;">
<div id="app"></div>
<script src="csui-notification.js"></script>
</body>
</html>

View File

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

View File

@ -1,18 +0,0 @@
$text-normal: #ddd;
$text-dim: #999;
$text-dark: #666;
$primary-color: #fb772a;
$primary: $primary-color;
$secondary-color: #e70c0c;
$input-background: #141414;
$input-border: #4e3527;
$page-background: #101010;
$popup-header-background: #7f1416;
$background-primary: #101010;
$selected-color: #f5cbaf;
$background-selected: #412d20;
$info-color: #bda9f3;

View File

@ -1,443 +0,0 @@
/* @import "colors.scss"; */
/*
@import "fonts.scss";
@import "flex.scss";
*/
* {
box-sizing: border-box;
}
body {
background-color: $background-primary;
color: $text-normal;
font-family: 'Overpass', sans-serif;
font-size: 1.2em;
width: 100%;
min-height: 100%;
height: 100vh;
border: 0px;
margin: 0px;
padding: 0px;
}
.main-window::before {
content: ' ';
color: #000;
position: absolute;
left: -0.5rem;
top: -0.5rem;
z-index: -1;
width: calc(100% + 1rem);
height: calc(100% + 1rem);
background-image: url('/csui/res/img/uw-window-bg-texture-3.webp');
opacity: 0.75;
}
/* STANDARD WIDTHS AND HEIGHTS */
.w100, .w-full {
width: 100%;
}
.h100, .h-full {
height: 100%;
}
.w24 {
width: 100px;
}
.m-t-0-33em {
margin-top: 0.33em;
}
.x-pad-1em {
padding-left: 1em;
padding-right: 1em;
}
/* overflow stuff */
.overflow-y-auto {
overflow: auto;
scrollbar-width: thin;
scrollbar-color: rgba($primary-color, 0.7) $background-primary;
}
.scrollbar-darker {
scrollbar-color: rgba($primary-color, 0.5) $background-primary;
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: rgba(#fa6, 0.5);
border-radius: 20px;
border: transparent;
}
/* scrollbars for chrome/webkit */
.overflow-y-auto::-webkit-scrollbar {
width: 3px;
}
.overflow-y-auto::-webkit-scrollbar-track {
background: $background-primary;
}
.overflow-y-auto::-webkit-scrollbar-thumb {
background: rgba($primary-color, 0.7);
}
.overflow-y-auto::-webkit-scrollbar-button {
display: none;
}
.scrollbar-darker::-webkit-scrollbar-thumb {
background: rgba($primary-color, 0.5);
}
.no-overflow {
overflow: hidden;
}
.no-overflow-x {
overflow-x: hidden;
}
.no-overflow-y {
overflow-y: hidden;
}
/* .SELECTED CLASSES */
.selected-tab {
background-color: initial;
border-left: $primary-color 5px solid;
}
.selected-tab-secondary {
background-color: initial;
border-left: $secondary-color 3px solid !important;
}
/* BASIC STYLING */
.description {
color: $text-dim;
font-size: 1rem;
}
.label {
font-size: 1.5rem;
font-variant: small-caps;
font-weight: 600;
}
.label-secondary {
font-weight: 600;
}
.button-box {
padding-top: 0.69rem;
padding-bottom: 0.69rem;
}
.indent {
padding-left: 4.2rem;
}
.row-padding {
padding-top: 0.69rem;
padding-bottom: 0.69rem;
}
a, a:visited {
color: $primary-color;
}
a:hover {
color: lighten($primary-color, 10%);
}
/* INPUT FORMATTING */
input[type="number"], input[type="text"], input {
outline: none;
background-color: $input-background;
color: $text-normal;
padding: 0.1rem;
padding-top: 0.2rem;
padding-bottom: 0.1rem;
margin-left: 1rem;
border: 1px solid $input-border;
}
input:disabled {
background: #444444;
color: darken($text-normal, 50%);
}
/* ELEMENT POSITIONING */
.row {
display: block;
margin-top: 20px;
margin-bottom: 10px;
}
.button-row {
display: block;
margin-top: 5px;
margin-bottom: 10px;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
.inline-block {
display: inline-block;
}
.block {
display: block;
}
.hidden {
display: none !important;
}
.hide {
display: none !important;
}
/* TEXT FORMATTING (no colors) */
small {
font-size: 0.75em;
font-weight: 200;
}
.small {
font-size: 0.75em;
font-weight: 200;
}
.medium-small {
font-size: 0.85em;
font-weight: 200;
}
.smallcaps{
font-variant: small-caps;
}
.center{
text-align: center;
width: 100%;
}
.text-center {
text-align: center;
}
.invalid-input {
border: 1px solid #720 !important;
background-color: #410 !important;
}
button,
.button {
/*display: inline-block;*/
border: 1px solid rgb(39, 39, 39);
margin-top: 3px;
margin-bottom: 3px;
color: $text-dim;
text-align: center;
cursor: pointer;
user-select: none;;
background-color: rgba(11,11,11,0.75);
padding: 0.5rem 2rem;
background-color: rgba(11, 11, 11, 0.5);
border: 1px solid transparent;
user-select: none !important;
&.primary {
background-color: #fa6;
color: #000;
}
&.danger {
background-color: #ff2211 !important;
color:#000;
}
&.disabled {
filter: saturate(0%);
pointer-events: none;
}
}
.selected, .setting-selected {
color: $selected-color !important;
background-color: $background-selected !important;
}
.selected-tab {
color: $selected-color;
}
.setting-selected {
border: 1px solid shade($selected-color, 25%);
}
.button:hover {
color: lighten($selected-color, 10%);
background-color: lighten($background-selected, 10%);
}
.disabled {
pointer-events: none;
/* color: #666; */
filter: contrast(50%) brightness(40%) grayscale(100%);
}
.disabled-button {
color: #666 !important;
cursor: not-allowed !important;
}
.disabled-button:hover {
color: #777 !important;
background-color: #222 !important;
}
/* BROWSER-SPECIFIC DISABLE */
.disabled-edge {
pointer-events: none !important;
filter: contrast(50%) brightness(40%) grayscale(100%) !important;
content: "NOT SUPPORTED IN THIS BROWSER";
}
.disabled-edge::after {
background-color: #333272;
color: #d8d9e6;
display: inline-block;
font-size: .75em;
font-variant: small-caps;
padding-left: 5px;
padding-right: 5px;
margin-left: 10px;
}
/** misc **/
.warning-color {
color: #d6ba4a;
}
.warning, .warning-lite {
color: #d6ba4a;
padding-left: 35px;
float: right;
}
.warning::before, .warning-lite::before {
content: "";
display: inline-block;
}
.warning::before {
font-weight: bold;
font-size: 2.5em;
margin-left: -35px;
padding-right: 10px;
}
.info-button {
color: $info-color;
border: 1px solid $info-color;
}
.info {
color: $info-color;
padding-left: 35px;
float: right;
}
.info-color {
color: $info-color;
}
.info::before {
content: "";
display: inline-block;
font-weight: bold;
margin-left: -35px;
padding-right: 10px;
}
.new {
color: #fff;
}
.new::after {
content: "";
color: $info-color !important;
display: inline-block;
font-weight: bold;
padding-right: 10px;
}
.experimental::after {
content: "";
color: #ffde12;
display: inline-block;
font-size: .75em;
font-variant: small-caps;
padding-left: 5px;
padding-right: 5px;
margin-left: 10px;
margin-top: -4px;
}
.experimental:hover::after {
content: "Ⓔ Experimental";
color: #ffde12;
display: inline-block;
font-size: .75em;
font-variant: small-caps;
padding-left: 5px;
padding-right: 5px;
margin-left: 10px;
margin-top: -4px;
}
.sticky-bottom {
width: 100%;
position: sticky;
bottom: 0px;
background-color: rgba($background-primary, 0.7);
}
.rtl {
direction: rtl;
}
.ltr {
direction: ltr;
}
.monospace {
font-family: 'Overpass Mono';
}
.pointer {
cursor: pointer;
user-select: none;
}

View File

@ -1,57 +0,0 @@
.flex {
display: flex;
}
.flex-row {
flex-direction: row;
}
.flex-col {
flex-direction: column;
}
.flex-auto {
flex: 1 1 auto;
}
.flex-grow, .grow {
flex-grow: 1;
}
.flex-nogrow, .grow-0 {
flex-grow: 0;
}
.flex-shrink, .shrink {
flex-shrink: 1;
}
.flex-noshrink, .shrink-0 {
flex-shrink: 0;
}
.items-center {
align-items: center;
}
.items-baseline {
align-items: baseline;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-center {
align-content: center;
align-items: center;
}
.flex-end {
justify-content: flex-end;
}
.flex-cross-center {
justify-content: center;
justify-items: center;
}
.flex-self-center {
align-self: center;
}

View File

@ -1,91 +0,0 @@
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-thin.woff2') format('woff2'); /* Super Modern Browsers */
font-weight: 200;
font-style: normal;
}
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-thin-italic.woff2') format('woff2');
font-weight: 200;
font-style: italic;
}
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-extralight.woff2') format('woff2');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-extralight-italic.woff2') format('woff2');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-light.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-light-italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-regular.woff2') format('woff2');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-italic.woff2') format('woff2');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-semibold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-semibold-italic.woff2') format('woff2');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'overpass';
src: url('../../fonts/overpass-webfont/overpass-bold-italic.woff2') format('woff2');
font-weight: 700;
font-style: italic;
}

View File

@ -1,111 +0,0 @@
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-thin.woff2') format('woff2'); /* Super Modern Browsers */
font-weight: 200;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-thin-italic.woff2') format('woff2');
font-weight: 200;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-extralight.woff2') format('woff2');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-extralight-italic.woff2') format('woff2');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-light.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-light-italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-regular.woff2') format('woff2');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-italic.woff2') format('woff2');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-semibold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-semibold-italic.woff2') format('woff2');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: url('/csui/res/fonts/overpass-webfont/overpass-bold-italic.woff2') format('woff2');
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: 'Overpass mono';
src: url('/csui/res/fonts/overpass-mono-webfont/overpass-mono-light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Overpass mono';
src: url('/csui/res/fonts/overpass-mono-webfont/overpass-mono-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Overpass mono';
src: url('/csui/res/fonts/overpass-mono-webfont/overpass-mono-semibold.woff2') format('woff2');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Overpass mono';
src: url('/csui/res/fonts/overpass-mono-webfont/overpass-mono-bold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +0,0 @@
/* @import 'flex.scss';
@import 'colors.scss';
@import 'common.scss';
*/
* {
font-family: 'Overpass';
}
.uw-ultrawidify-container-root {
/*
// here's the defaults:
// all: initial;
// * {
// all: unset;
// }
// here's things that we don't want as defaults
// (must come after the all: declaration, otherwise
// all: declaration overrides everything.)
// we put our UI _over_ site's player:
*/
z-index: 999999;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: none transparent !important;
/* Ensure we're display:block */
display: block;
/* we are click-through by default: */
pointer-events: none;
}

View File

@ -1,50 +0,0 @@
## License
Copyright 2015 Red Hat, Inc.,
This Font Software is dual licensed and available under the SIL Open Font License, Version 1.1. and also the LGPL 2.1
The Open Font license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
The GNU LGPL can be found at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
#### 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.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,318 +0,0 @@
<template>
<div
class="popup-panel flex flex-col uw-clickable h-full main-window relative"
>
<div class="popup-window-header">
<div class="header-title">
<div class="popup-title">Ultrawidify <small>{{settings?.active?.version}} - {{BrowserDetect.processEnvChannel}}</small></div>
<div class="site-support-info flex flex-row">
<div class="site-support-site">{{site}}</div>
<SupportLevelIndicator
v-if="inPlayer"
:siteSupportLevel="siteSupportLevel"
>
</SupportLevelIndicator>
</div>
</div>
<div class="header-buttons">
<div
class="header-button close-button"
@click="$emit('close')"
>
<mdicon name="close" :size="36"></mdicon>
</div>
<!-- <a >{{preventClose ? 'allow auto-close' : 'prevent auto-close'}}</a> -->
</div>
</div>
<SettingsWindowContent
:tabs="tabs"
:selectedTab="selectedTab"
:settings="settings"
:eventBus="eventBus"
:logger="logger"
:in-player="inPlayer"
:site="site"
@selectTab="selectTab"
@preventClose="setPreventClose"
></SettingsWindowContent>
</div>
</template>
<script>
import DebugPanel from './PlayerUiPanels/DebugPanel.vue'
import AutodetectionSettingsPanel from './PlayerUiPanels/AutodetectionSettingsPanel.vue'
import BaseExtensionSettings from './PlayerUiPanels/BaseExtensionSettings.vue'
import PlayerDetectionPanel from './PlayerUiPanels/PlayerDetectionPanel.vue'
import VideoSettings from './PlayerUiPanels/VideoSettings.vue'
import BrowserDetect from '../../ext/conf/BrowserDetect'
import ChangelogPanel from './PlayerUiPanels/ChangelogPanel.vue'
import AboutPanel from '@csui/src/PlayerUiPanels/AboutPanel.vue'
import PlayerUiSettings from './PlayerUiPanels/PlayerUiSettings.vue'
import ResetBackupPanel from './PlayerUiPanels/ResetBackupPanel.vue'
import SupportLevelIndicator from '@csui/src/components/SupportLevelIndicator.vue'
export default {
components: {
VideoSettings,
PlayerDetectionPanel,
BaseExtensionSettings,
AutodetectionSettingsPanel,
DebugPanel,
PlayerUiSettings,
ChangelogPanel,
AboutPanel,
SupportLevelIndicator,
ResetBackupPanel,
},
mixins: [],
data() {
return {
statusFlags: {
hasDrm: undefined,
},
BrowserDetect: BrowserDetect,
preventClose: false,
siteSettings: null,
}
},
props: [
'settings',
'eventBus',
'logger',
'in-player',
'site',
'defaultTab'
],
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.settings.listenAfterChange(this.setDebugTabVisibility);
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
}
},
}
)
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);
},
selectTab(tab) {
this.selectedTab = tab;
},
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="scss" scoped>
/*@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';*/
.tab-row {
width: 22rem;
flex-grow: 0;
flex-shrink: 0;
}
.tab-main {
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
}
.content {
flex-grow: 1;
.warning-area {
flex-grow: 0;
flex-shrink: 0;
}
.panel-content {
flex-grow: 1;
flex-shrink: 1;
overflow-y: auto;
padding: 1rem;
}
}
.warning-box {
background: rgb(255, 174, 107);
color: #000;
margin: 1rem;
padding: 1rem;
display: flex;
flex-direction: row;
align-items: center;
.icon-container {
margin-right: 1rem;
flex-shrink: 0;
flex-grow: 0;
}
a {
color: rgba(0,0,0,0.7);
cursor: pointer;
}
}
.site-support-info {
display: flex;
flex-direction: row;
align-items: bottom;
.site-support-site {
font-size: 1.5em;
}
}
.popup-panel {
background-color: rgba(0,0,0,0.50);
color: #fff;
overflow-y: auto;
.popup-window-header {
padding: 1rem;
background-color: rgba(5,5,5, 0.75);
display: flex;
flex-direction: row;
.header-title {
flex: 1 1;
}
.header-buttons {
flex: 0 0;
display: flex;
flex-direction: row;
.header-button {
cursor: pointer;
border-radius: 50%;
width: 48px;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
&.button-active {
background-color: #fa6;
color: #000;
&:hover {
color: #ccc;
}
}
&.close-button {
color: #f00;
}
&:hover {
color: #fa6;
}
}
}
}
.tab-row {
background-color: rgba(11,11,11, 0.75);
.tab {
display: flex;
flex-direction: row;
align-items: center;
padding: 2rem;
font-size: 1.5rem;
height: 6rem;
border-bottom: 1px solid rgba(128, 128, 128, 0.5);
border-top: 1px solid rgba(128, 128, 128, 0.5);
opacity: 0.5;
&:hover {
opacity: 1;
}
&.active {
opacity: 1.0;
background-color: $primaryBg;
color: rgb(255, 174, 107);
border-bottom: 1px solid rgba(116, 78, 47, 0.5);
border-top: 1px solid rgba(116, 78, 47, 0.5);
}
.icon-container {
width: 64px;
flex-grow: 0;
flex-shrink: 0;
}
.label {
flex-grow: 1;
flex-shrink: 1;
padding: 0 !important;
}
&.highlight-tab {
opacity: 0.9;
color: #eee;
}
}
}
.popup-title, .popup-title h1 {
font-size: 48px !important;
}
}
pre {
white-space: pre-wrap;
}
</style>

View File

@ -1,122 +0,0 @@
<template>
<div class="flex flex-col w-full h-full gap-2">
<h1>About Ultrawidify</h1>
<p>
Ultrawidify version: <b>{{addonVersion}}</b><br>
Install source: <b>{{addonSource}}</b>
</p>
<div class="flex flex-row gap-2 bg-black">
<div class="w-[1/2]" style="width: 50%">
<h2>Report a problem</h2>
<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 information:
</p>
<ul>
<li>Extension version</li>
<li>Which browser you're using¹</li>
<li>Which extension store you installed extension from (install source)²</li>
<li>Which site you're having the issue on (preferably link. For issues with autodetection, please also include a timestamp)</li>
<li>What do I need to do in order to make the issue happen</li>
<li>Please include a screenshot of the problem. It usually helps.</li>
</ul>
<p>
<small>
[1] If using anything other than Firefox, Chrome, or Edge, please check if issue also happens in Chrome or Edge. Bugs affecting Opera will only get fixed if they also affect Chrome, because <a target="_blank" href="https://stuff.tamius.net/sacred-texts/2024/06/08/why-im-boycotting-opera/">I'm salty</a>.<br/>
[2] It is recommended that users of Microsoft Edge install Ultrawidify from the Microsoft Edge Addons source
</small>
</p>
</div>
<div class="w-[1/2]" style="width: 50%">
<h2>Thank you monies</h2>
<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>
I also have <a href="https://instagram.com/shaman_urkog" target="_blank">instagram with nerdy shit</a> <small>(mini painting + various fantasy events)</small>.
</p>
</div>
</div>
</div>
</template>
<script>
import BrowserDetect from '../../../ext/conf/BrowserDetect';
export default {
components: {
BrowserDetect,
},
data() {
return {
// 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,
}
},
async created() {
const messageTemplate = encodeURIComponent(
`Describe your issue in more detail. In case of misaligned videos, please provide screenshots. When reporting\
issues with autodetection not detecting aspect ratio correctly, please provide a link with timestamp to the\
problematic video at the time where the problem happens. You may delete this paragraph, as it's only a template.
Extension info (AUTOGENERATED DO NOT CHANGE OR REMOVE):
* Build: ${BrowserDetect.processEnvBrowser}-${this.addonVersion}-stable
Browser-related stuff (please ensure this section is correct):
* User Agent string: ${window.navigator.userAgent}
* vendor: ${window.navigator.vendor}
* Operating system: ${window.navigator.platform}
`
);
this.mailtoLink = `mailto:tamius.han@gmail.com?subject=%5BUltrawidify%5D%20ENTER%20SUMMARY%20OF%20YOUR%20ISSUE%20HERE&body=${messageTemplate}`;
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.flex {
display: flex;
}
.flex-row {
flex-direction: row;
}
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,176 +0,0 @@
<template>
<div class="alignment-box" :class="{large: large}">
<div
class="col top left"
@click="align(VideoAlignment.Left, VideoAlignment.Top)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col top center"
@click="align(VideoAlignment.Center, VideoAlignment.Top)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col top right"
@click="align(VideoAlignment.Right, VideoAlignment.Top)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col ycenter left"
@click="align(VideoAlignment.Left, VideoAlignment.Center)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col ycenter center"
@click="align(VideoAlignment.Center, VideoAlignment.Center)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col ycenter right"
@click="align(VideoAlignment.Right, VideoAlignment.Center)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col bottom left"
@click="align(VideoAlignment.Left, VideoAlignment.Bottom)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col bottom center"
@click="align(VideoAlignment.Center, VideoAlignment.Bottom)"
>
<div class="alignment-ui"></div>
</div>
<div
class="col bottom right"
@click="align(VideoAlignment.Right, VideoAlignment.Bottom)"
>
<div class="alignment-ui"></div>
</div>
</div>
</template>
<script>
import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
export default {
props: [
'eventBus',
'large',
],
data() {
return {
VideoAlignment: VideoAlignmentType
}
},
methods: {
align(alignmentX, alignmentY) {
this.eventBus?.sendToTunnel('set-alignment', {x: alignmentX, y: alignmentY})
}
}
}
</script>
<style lang="scss" scoped module>
.alignment-box {
aspect-ratio: 1;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
&.large {
max-width: 15rem;
.col {
width: 4rem;
height: 4rem;
}
}
.col {
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0,0,0,0.25);
cursor: pointer;
padding: 0.5rem;
width: 1.5rem;
height: 1.5rem;
border-bottom: 1px solid transparent;
.alignment-ui {
width: 50%;
height: 50%;
border-style: solid;
border-width: 0px;
border-color: #fff;
}
&:hover {
background-color: rgba(0,0,0,0.5);
border-bottom: 1px solid #fa6;
.alignment-ui {
border-color: #fa6 !important;
}
&.center.ycenter {
.alignment-ui {
background-color: #fa6;
}
}
}
&.top {
align-items: flex-start;
.alignment-ui {
border-top-width: 3px;
}
}
&.bottom {
align-items: flex-end;
.alignment-ui {
border-bottom-width: 3px;
}
}
&.left {
justify-content: flex-start;
.alignment-ui {
border-left-width: 3px;
}
}
&.right {
justify-content: flex-end;
.alignment-ui {
border-right-width: 3px;
}
}
&.center.ycenter {
.alignment-ui {
width: 25%;
height: 25%;
background-color: #fff;
}
}
}
}
</style>

View File

@ -1,486 +0,0 @@
<template>
<div class="flex flex-col tab-root">
<div class="flex flex-row flex-wrap">
<!-- AARD performance metrics -->
<div>
<div class="flex flex-row">
<h1>Automatic Aspect Ratio Detection</h1>
</div>
<div class="aard-settings-group">
<div class="settings-segment">
<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">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">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 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 class="field">
<div class="label">Maximum allowed vertical video misalignment:</div>
<div class="input">
<input v-model="aardSettings.allowedMisaligned" />
</div>
</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 class="settings-segment">
<div class="warning-area">
Subtitle detection is experimental to high heaven.
</div>
<div class="field">
<div class="label">Subtitle detection:</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 v-if="settings.active.ui.devMode" class="settings-segment">
<p>
<b>Debug options</b>
</p>
<div class="flex flex-row">
<div>
<div>
<button @click="eventBus.sendToTunnel('aard-enable-debug', true)">Show debug overlay</button>
</div>
<div>
<div class="label">Show debug overlay on startup</div>
<input
type="checkbox"
v-model="settings.active.ui.dev.aardDebugOverlay.showOnStartup"
@change="settings.saveWithoutReload"
>
</div>
</div>
<div>
<JsonEditor
v-model="settingsJson"
>
</JsonEditor>
<button @click="saveDebugUiSettings">Save debug UI settings</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Button from '../components/Button.vue'
import KeyboardShortcutParser from '../../../common/js/KeyboardShortcutParser';
import ShortcutButton from '../components/ShortcutButton';
import EditShortcutButton from '../components/EditShortcutButton';
import BrowserDetect from '../../../ext/conf/BrowserDetect';
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
import StretchType from '../../../common/enums/StretchType.enum';
import CropModePersistence from '../../../common/enums/CropModePersistence.enum';
import AlignmentOptionsControlComponent from './AlignmentOptionsControlComponent.vue';
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';
export default {
components: {
ShortcutButton,
EditShortcutButton,
Button,
AlignmentOptionsControlComponent,
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="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 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,316 +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 './PanelComponents/ExtensionSettings/SiteExtensionSettings.vue';
import FrameSiteSettings from './PanelComponents/ExtensionSettings/FrameSiteSettings.vue';
import OtherSiteSettings from './PanelComponents/ExtensionSettings/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>
<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>

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,179 +0,0 @@
<template>
<div class="">
<template v-if="!selectedSite">
<div class="flex flex-col container pointer hoverable" style="margin-top: 1rem; padding: 0.5rem 1rem;" @click="selectedSite = '@global'" >
<div class="flex flex-row">
<div class="flex-grow pointer">
<b style="color: #fa6">Default settings</b>
<!-- <span :style="getSiteTypeColor('@global')"> -->
<!-- (config: {{site.type ?? 'unknown'}}) -->
<!-- </span> -->
</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>
{{getSiteEnabledModes('@global', 'enable') === 'disabled'}}
<span v-if="getSiteEnabledModes('@global', 'enable') === 'disabled'" class="text-[0.9em]">&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="info" 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 v-for="site of sites" :key="site.key" @click="selectedSite = site.key" class="flex flex-col container pointer hoverable" style="margin-top: 4px; padding: 0.5rem 1rem;">
<div class="flex flex-row">
<div class="flex-grow pointer">
<b>{{ site.key }}</b>
<span :style="getSiteTypeColor(site.type)">
(config: {{site.type ?? 'unknown'}})
</span>
</div>
<div>Edit</div>
</div>
<div class="flex flex-row">
<small>
Enabled: <span :style="getSiteEnabledColor(site.key, 'enable')"><small>{{ getSiteEnabledModes(site.key, 'enable') }}</small></span>;&nbsp;
Aard <span :style="getSiteEnabledColor(site.key, 'enableAard')"><small>{{ getSiteEnabledModes(site.key, 'enableAard') }}</small></span>;&nbsp;
kbd: <span :style="getSiteEnabledColor(site.key, 'enableKeyboard')"><small>{{ getSiteEnabledModes(site.key, 'enableKeyboard') }}</small></span>
UI: <span :style="getSiteEnabledColor(site.key, 'enableUI')"><small>{{ getSiteEnabledModes(site.key, 'enableUI') }}</small></span>
</small>
</div>
</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'"
:visibleSegments="{advancedPlayerSettings: role === 'settings'}"
></SiteExtensionSettings>
</div>
</template>
</div>
</template>
<script>
import ExtensionMode from '../../../../../common/enums/ExtensionMode.enum';
import SiteExtensionSettings from './SiteExtensionSettings.vue';
export default {
data() {
return {
selectedSite: null,
siteFilter: '',
filteredSites: []
}
},
props: [
'settings',
'role',
],
components: {
SiteExtensionSettings,
},
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="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,72 +0,0 @@
<template>
<div>
<div class="flex flex-row">
<div class="flex-grow pointer">
<b>{{ host }}</b>
<span :style="getSiteTypeColor(siteSettings?.data?.type)">
(config: {{siteSettings?.data?.type ?? 'unknown'}})
</span>
</div>
<div>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;
Aard <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 '../../../../../common/enums/ExtensionMode.enum';
export default {
data() {
return {
siteSettings: undefined,
supportType: undefined
}
},
props: [
'settings',
'parentHost',
'host',
],
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 'disabled';
}
}
}
</script>

View File

@ -1,157 +0,0 @@
<template>
<div class="panel-root">
<h1>Resizer debug data</h1>
<div class="flex flex-row flex-wrap">
<div class="panel">
<h3 class="panel-title">
Player info
</h3>
<div class="data-item">
<div class="data-title">Window size:
<small>(Inner size)</small>
</div>
<div class="data">{{windowWidth}} x {{windowHeight}}</div>
<div class="button" @click="refreshWindowSize()">
<!-- <Icon icon="arrow-clockwise"></Icon> -->
Refresh</div>
</div>
<div class="data-item">
<div class="data-title">Player dimensions:</div>
<div class="data">{{debugData?.resizer?.playerData?.dimensions?.width ?? 'not detected'}} x {{debugData?.resizer?.playerData?.dimensions?.height ?? 'not detected'}}</div>
</div>
<div class="data-item">
<div class="data-title">Is fullscreen?</div>
<div class="data">{{debugData?.resizer?.playerData?.dimensions?.fullscreen ?? 'unknown'}}</div>
</div>
<div class="data-item">
<div class="data-title">Player id | classlist</div>
<div class="data">{{debugData?.resizer?.playerData?.elementId || '<no ID>'}} | {{debugData?.resizer?.playerData?.classList || '<no classes>'}}</div>
</div>
</div>
<!-- Stream info -->
<div class="panel">
<h3 class="panel-title">
Stream info
</h3>
<div class="data-item">
<div class="data-title">
Stream dimensions: <small>(Native size of the video)</small>
</div>
<div class="data">
{{debugData?.resizer?.videoRawData?.streamDimensions.x}} x {{debugData?.resizer?.videoRawData?.streamDimensions?.y}}
</div>
</div>
<div class="data-item">
<div class="data-title">
Stream displayed dimensions: <small>(Video file is being upscaled to this size)</small>
</div>
<div class="data">
{{debugData?.resizer?.videoRawData?.displayedSize?.x.toFixed()}} x {{debugData?.resizer?.videoRawData?.displayedSize?.y.toFixed()}}
</div>
</div>
<div class="data-item">
<div class="data-title">Video element native size: <small>(Size of the html element. Should be same as above most of the time!)</small></div>
<div class="data">{{debugData?.resizer?.videoRawData?.displayedSize?.x.toFixed(1)}} x {{debugData?.resizer?.videoRawData?.displayedSize?.y.toFixed(1)}}</div>
</div>
<div class="data-item">
<div class="data-title">Size difference to player (raw): <small>(positive numbers: video element is bigger than player element)</small></div>
<div class="data">x: {{debugData?.resizer?.sizeDifferenceToPlayer?.beforeZoom?.wdiff.toFixed(1)}}; &nbsp; &nbsp; y: {{debugData?.resizer?.sizeDifferenceToPlayer?.beforeZoom?.hdiff.toFixed(1)}}</div>
</div>
<div class="data-item">
<div class="data-title">Video element size (post zoom): <small>(Size of the html element after transform:scale is applied. Or at least, that's what Resizer::computeOffsets() thinks the final size is.)</small></div>
<div class="data">{{debugData?.resizer?.transformedSize?.x.toFixed(2)}} x {{debugData?.resizer?.transformedSize?.y.toFixed(2)}}</div>
</div>
<div class="data-item">
<div class="data-title"><b>Size difference to player (post-zoom):</b> <small>(same as above, except after cropping, stretching, panning and zoom are applied)</small></div>
<div class="data">x: {{debugData?.resizer?.sizeDifferenceToPlayer?.afterZoom?.wdiff.toFixed(2)}}; &nbsp; &nbsp; y: {{debugData?.resizer?.sizeDifferenceToPlayer?.afterZoom?.hdiff.toFixed(2)}}</div>
</div>
</div>
<!-- Transform info -->
<div class="panel">
<h3 class="panel-title">
Transformations
</h3>
<div class="data-item">
<div class="data-title">Alignment: <small>(I agree that 'left' and 'right' are both evil, but that's not the kind of alignments we're thinking of)</small></div>
<div class="data">{{debugData?.resizer?.videoTransform?.alignment || '<unknown>'}}</div>
</div>
<div class="data-item">
<div class="data-title">Scale factor</div>
<div class="data">x: {{debugData?.resizer?.videoTransform?.scale.x.toFixed(2)}}; &nbsp; &nbsp; y: {{debugData?.resizer?.videoTransform?.scale.y.toFixed(2)}}</div>
</div>
<div class="data-item">
<div class="data-title">Translation</div>
<div class="data">x: {{debugData?.resizer?.videoTransform?.translate.x.toFixed(2)}}; &nbsp; &nbsp; y: {{debugData?.resizer?.videoTransform?.translate.y.toFixed(2)}}</div>
</div>
</div>
</div>
<!-- <div class="uw-debug-info flex"> -->
<!-- <pre> -->
<!-- {{debugDataPrettified}} -->
<!-- </pre> -->
<!-- </div> -->
</div>
</template>
<script>
// import Icon from '../../common/components/Icon';
export default {
components: {
// Icon,
},
props: {
debugData: Object
},
data() {
return {
windowWidth: window.innerWidth,
windowHeight: window.innerHeight
};
},
methods: {
refreshWindowSize() {
this.windowWidth = window.innerWidth;
this.windowHeight = window.innerHeight;
},
}
}
</script>
<style lang="scss" scoped>
@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';
.panel-root {
display: block;
padding: 16px;
}
.panel {
display: inline-block;
width: 420px;
max-width: 100%;
padding: 12px 6px;
}
.data-item {
display: block;
margin-bottom: 0.69rem;
}
.data-title {
small {
display: block;
opacity: 0.69;
}
}
.data {
color: $primary-color;
}
</style>

View File

@ -1,17 +0,0 @@
<template>
<div>
</div>
</template>
<script>
export default {
props: {
siteSettings: Object,
hasDrm: Boolean,
problems: Object,
}
}
</script>
<style lang="scss">
</style>

View File

@ -1,119 +0,0 @@
<template>
<div class="context-container" @mouseleave="hideContextMenu()">
<GhettoContextMenuItem
class="activator uw-clickable"
:css="{
'expand-left': alignment === 'left',
'expand-right': alignment === 'right',
}"
@click="showContextMenu()"
@mouseenter="showContextMenu()"
>
<slot name="activator"></slot>
</GhettoContextMenuItem>
<div
v-if="contextMenuVisible"
class="context-menu uw-clickable"
:class="{
'menu-left': alignment === 'left',
'menu-right': alignment === 'right'
}"
@mouseleave="hideContextMenu()"
>
<slot></slot>
</div>
</div>
</template>
<script>
import GhettoContextMenuItem from './GhettoContextMenuItem.vue';
export default {
components: {
GhettoContextMenuItem,
},
props: {
alignment: String,
},
data() {
return {
contextMenuVisible: false,
contextMenuHideTimeout: undefined,
}
},
methods: {
showContextMenu() {
this.contextMenuVisible = true;
},
hideContextMenu() {
this.contextMenuHideTimeout = setTimeout( () => {
this.contextMenuVisible = false;
}, 50);
}
}
}
</script>
<style lang="scss" scoped>
.context-container {
position: relative;
}
.context-menu-wrapper {
position: relative;
}
.context-menu {
position: absolute;
display: flex;
flex-direction: column;
min-width: 5rem;
top: 50%;
transform: translateY(-50%);
}
.menu-left {
right: 100%;
}
.menu-right {
left: 100%;
}
</style>
<style lang="scss">
.activator {
position: relative;
padding: 1rem 1.6rem;
font-size: .95rem;
padding: 1rem 1.6rem;
background-color: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(16px) saturate(120%);
white-space: nowrap;
&:hover {
background-color: rgba(255, 128, 64, 0.95);
}
&.expand-left {
padding-left: 2.2rem;
}
&.expand-right {
padding-right: 2.2rem;
}
&.expand-left::before,
&.expand-right::after {
position: absolute;
top: 50%;
transform: translateY(-50%);
font-size: 0.88rem;
}
&.expand-left::before {
content: '⮜';
left: 0.5rem;
}
&.expand-right::after {
content: '⮞';
right: 0.5rem;
}
}
</style>

View File

@ -1,46 +0,0 @@
<template>
<div
class="flex flex-col flex-center center-text item"
:class="{
'can-hover': !disableHover,
...(css ? css : {}),
}"
>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
label: String,
shortcut: String,
childAlignment: String,
css: Object,
disableHover: Boolean,
}
}
</script>
<style lang="scss" scoped>
.item {
position: relative;
font-size: .95rem;
font-family: 'Overpass';
padding: 1rem 1.6rem;
background-color: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(16px) saturate(120%);
white-space: nowrap;
&.can-hover:hover {
background-color: rgba(0,0,0,0.98);
border-bottom: 1px solid #fa6;
}
display: flex;
flex-direction: column;
}
</style>

View File

@ -1,26 +0,0 @@
<template>
<div>
<GhettoContextMenuItem>
<template v-if="label">
{{label}} {{shortcut ? `(${shortcut})` : ''}}
</template>
<template v-else>
<slot></slot>
</template>
</GhettoContextMenuItem>
</div>
</template>
<script>
import GhettoContextMenuItem from './GhettoContextMenuItem.vue';
export default {
components: {
GhettoContextMenuItem,
},
props: {
label: String,
shortcut: String,
}
}
</script>

View File

@ -1,53 +0,0 @@
<template>
<div class="qsb flex flex-row flex-cross-center flex-center flex-nogrow"
:class="{
'id': selector.startsWith('#'),
'class': selector.startsWith('.'),
}">
<div class="flex mt2px">{{selector}}</div> <span class="flex closeButton" @click="remove">×</span>
</div>
</template>
<script>
export default {
props: {
selector: String,
},
methods: {
remove() {
this.$emit('remove');
}
}
}
</script>
<style lang="scss" scoped>
.mt2px {
margin-top: 7px;
}
.id {
border: 1px solid #d00;
background-color: rgba(216, 94, 24, 0.25)
}
.class {
border: 1px solid #33d;
background-color: rgba(69, 69, 255, 0.25)
}
.closeButton {
margin-top: 2px;
margin-left: 4px;
margin-right: 4px;
font-size: 1.5em;
color: #e00;
}
.closeButton:hover {
cursor: pointer;
color: #fa6;
}
.qsb {
cursor:default;
margin-left: 3px;
margin-right: 3px;
margin-bottom: 2px;
}
</style>

View File

@ -1,87 +0,0 @@
<template>
<div class="flex flex-col">
<div v-if="!editing && !adding" class="flex flex-row">
<div class="">
<b>Query selector:</b> {{qs.string}}<br/>
<b>Additional CSS:</b> {{qs.css || 'no style rules'}}
</div>
<div class="flex flex-col flex-nogrow">
<a @click="editing = true">Edit</a>
<a @click="$emit('delete')">Delete</a>
</div>
</div>
<div v-else class="flex flex-row">
<div class="flex flex-col">
<div class="flex flex-row">
<div class="flex label-secondary form-label">
Query selector:
</div>
<div class="flex flex-input">
<input type="text"
v-model="qs.string"
/>
</div>
</div>
<div class="flex flex-row">
<div class="flex label-secondary form-label">
Additional CSS:
</div>
<div class="flex flex-input">
<input type="text"
v-model="qs.css"
/>
</div>
</div>
</div>
<div>
<a v-if="editing" @click="cancelEdit">Cancel</a>
<a v-if="adding" @click="clear">Clear</a>
<a @click="save">Save</a>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
qs: {string: '', css: ''},
editing: false,
}
},
props: {
querySelector: Object,
adding: {
type: Boolean,
required: false,
default: false
}
},
watch: {
querySelector(val) {
this.qs = JSON.parse(JSON.stringify(val));
}
},
methods: {
save() {
if (this.adding) {
this.$emit('create', this.qs);
this.qs = {string: '', css: ''};
} else {
this.emit$('update', this.qs)
}
},
cancelEdit() {
this.qs = JSON.parse(JSON.stringify(this.querySelector));
},
clear() {
this.qs = {string: '', css: ''};
}
}
}
</script>
<style>
</style>

View File

@ -1,30 +0,0 @@
<template>
<div class="flex flex-col flex-center">
<div class="flex flex-self-center">
{{label}}
</div>
<div class="flex dark flex-self-center">
<small>
{{shortcut ? `(${shortcut})` : ''}}
</small>
</div>
</div>
</template>
<script>
export default {
props: {
label: String,
shortcut: String
}
}
</script>
<style lang="scss" scoped>
.center-text {
text-align: center;
}
.dark {
opacity: 50%;
}
</style>

View File

@ -1,148 +0,0 @@
<template>
<div v-if="siteSupportLevel === 'official'" class="site-support official" :style="supportLevelStyle">
<mdicon name="check-decagram" />
<div v-if="!small">Verified</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Verified&nbsp;&nbsp;</template>
The extension is being tested and should work on this site.
</div>
</div>
<div v-if="siteSupportLevel === 'community'" class="site-support community" :style="supportLevelStyle">
<mdicon name="account-group" />
<div v-if="!small">Community</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Community&nbsp;&nbsp;</template>
People say extension works on this site (or have provided help getting the extension to work if it didn't).<br/><br/>
Tamius (the dev) does not test the extension on this site, probably because it requires a subscription or
is geoblocked.
</div>
</div>
<div v-if="siteSupportLevel === 'no-support' || siteSupportLevel === 'unknown'" class="site-support no-support" :style="supportLevelStyle">
<mdicon name="help-circle-outline" />
<div v-if="!small">Unknown</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Unknown&nbsp;&nbsp;</template>
Not officially supported. Extension will try to fix things, but no promises.<br/><br/>
Tamius (the dev) does not test the extension on this site for various reasons
(unaware, not using the site, language barrier, geoblocking, paid services Tam doesn't use).
</div>
</div>
<div v-if="siteSupportLevel === 'user-added' || siteSupportLevel === 'user-defined'" class="site-support user-added" :style="supportLevelStyle">
<mdicon name="account" />
<div v-if="!small">Modified by you</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Modified by you&nbsp;&nbsp;</template>
You have manually changed settings for this site. The extension is doing what you told it to do.
</div>
</div>
<div v-if="siteSupportLevel === 'officially-disabled'" class="site-support officially-disabled" :style="supportLevelStyle">
<mdicon class="site-support no-support" name="checkbox-marked-circle" />
<div v-if="!small">Not supported</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Not supported&nbsp;&nbsp;</template>
Extension is known to not work with this site.
</div>
</div>
</template>
<script>
export default {
props: {
siteSupportLevel: String,
small: Boolean,
supportLevelStyle: String,
tooltipStyle: String,
}
}
</script>
<style lang="scss" scoped>
.site-support {
display: inline-flex;
flex-direction: row;
align-items: center;
margin-left: 1rem;
border-radius: 8px;
padding: 0rem 1.5rem 0rem 1rem;
position: relative;
.tooltip {
padding: 1rem;
display: none;
position: absolute;
bottom: 0;
transform: translateY(110%);
width: 42em;
background-color: rgba(0,0,0,0.90);
color: #ccc;
z-index: 99999 !important;
white-space: normal;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}
&:hover {
.tooltip {
display: block;
}
}
.mdi {
margin-right: 1rem;
}
&.official {
background-color: #fa6;
color: #000;
.mdi {
fill: #000 !important;
}
}
&.community {
background-color: rgb(85, 85, 179);
color: #fff;
.mdi {
fill: #fff !important;
}
}
&.officially-disabled {
background-color: rgb(132, 24, 40);
color: #eee;
.mdi {
fill: #eee !important;
}
}
&.no-support {
background-color: rgb(83, 76, 77);
color: #eee;
.mdi {
fill: #eee !important;
}
}
&.user-added {
border: 1px solid #ff0;
color: #ff0;
.mdi {
fill: #ff0 !important;
}
}
}
</style>

View File

@ -1,471 +0,0 @@
<template>
<!-- Preview + editor -->
<div
v-if="settings?.active?.ui"
class="active-trigger-area uw-clickable"
:style="triggerZoneStyles"
>
<div class="trigger-zone-editor"
@mousedown="(event) => handleMouseDown('offset', event)"
>
<div
class="uw-clickable corner tl"
@mousedown.stop="(event) => handleMouseDown('tl', event)"
>
</div>
<div
class="uw-clickable corner tr"
@mousedown.stop="(event) => handleMouseDown('tr', event)"
>
</div>
<div
class="uw-clickable corner bl"
@mousedown.stop="(event) => handleMouseDown('bl', event)"
>
</div>
<div
class="uw-clickable corner br"
@mousedown.stop="(event) => handleMouseDown('br', event)"
>
</div>
</div>
</div>
<!-- Sliders -->
<div
class="trigger-zone-editor-sliders-container"
>
<div class="panel uw-clickable">
<div class="trigger-zone-editor-window">
<div class="heading">
<h2>Trigger zone editor</h2>
</div>
<div>
<p>
Trigger zone is represented by this very obvious gold grid. Ultrawidify menu button will only show when mouse enters the area marked by the grid.
</p>
<p>
Note that interacting with the grid area is slightly broken. The distance dragged doesn't correspond with the amount of resizing. I don't plan on
fixing that because the amount of effort required to get it working perfectly doesn't correspond with the amount of utility this part of the UI will
see. Like, it works well enough and I want to enjoy some of my end-of-year PTO.
</p>
<p>
Sliders work as they should.
</p>
</div>
<div class="field">
<div class="label">Trigger zone width:</div>
<div class="input range-input">
<input
:value="settings.active.ui.inPlayer.triggerZoneDimensions.width"
class="slider"
type="range"
min="0.1"
max="1"
step="0.01"
@input="(event) => setValue('width', event.target.value)"
@change="(event) => updateSettings(true)"
>
<input
:value="ghettoComputed.width"
@input="(event) => setValue('width', event.target.value, true)"
@change="(event) => updateSettings(true)"
>
</div>
</div>
<div class="hint">
Width of the trigger zone (% of player area).
</div>
<div class="field">
<div class="label">Trigger zone height:</div>
<div class="input range-input">
<input
:value="settings.active.ui.inPlayer.triggerZoneDimensions.height"
type="range"
min="0.1"
max="1"
step="0.01"
@input="(event) => setValue('height', event.target.value)"
@change="(event) => updateSettings(true)"
>
<input
:value="ghettoComputed.height"
@input="(event) => setValue('height', event.target.value, true)"
@change="(event) => updateSettings(true)"
>
</div>
</div>
<div class="hint">
Height of the trigger zone (% of player area).
</div>
<div class="field">
<div class="label">Trigger zone horizontal offset:</div>
<div class="input range-input">
<input
v-model="settings.active.ui.inPlayer.triggerZoneDimensions.offsetX"
type="range"
min="-100"
max="100"
@input="(event) => setValue('offsetX', event.target.value)"
@change="(event) => updateSettings(true)"
>
<input
:value="settings.active.ui.inPlayer.triggerZoneDimensions.offsetX"
@input="(event) => setValue('offsetX', event.target.value)"
@change="(event) => updateSettings(true)"
>
</div>
</div>
<div class="hint">
By default, trigger zone is centered around the button. This option moves trigger zone left and right.
</div>
<div class="field">
<div class="label">Trigger zone vertical offset:</div>
<div class="input range-input">
<input
v-model="settings.active.ui.inPlayer.triggerZoneDimensions.offsetY"
type="range"
min="-100"
max="100"
@input="(event) => setValue('offsetY', event.target.value)"
@change="(event) => updateSettings(true)"
>
<input
:value="settings.active.ui.inPlayer.triggerZoneDimensions.offsetY"
@input="(event) => setValue('offsetY', event.target.value)"
@change="(event) => updateSettings(true)"
>
</div>
</div>
<div class="hint">
By default, trigger zone is centered around the button. This option moves trigger zone up and down.
</div>
<div class="action-row">
<button @click="finishTriggerZoneEdit">Finish editing</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: [
'settings',
'eventBus',
'playerDimensions',
],
watch: {
playerDimensions(newVal, oldVal) {
this.updateTriggerZones();
}
},
data() {
return {
triggerZoneStyles: {},
activeCornerDrag: undefined,
dragStartPosition: undefined,
dragStartConfiguration: undefined,
ghettoComputed: { }
}
},
created() {
document.addEventListener("mouseup", this.handleMouseUp);
document.addEventListener("mousemove", this.handleMouseMove);
this.ghettoComputed = {
width: this.optionalToFixed(this.settings.active.ui.inPlayer.triggerZoneDimensions.width * 100, 0),
height: this.optionalToFixed(this.settings.active.ui.inPlayer.triggerZoneDimensions.height * 100, 0)
};
this.updateTriggerZones(false);
},
methods: {
optionalToFixed(v, n) {
try {
if ((`${v}`.split('.')[1]?.length ?? 0) > n) {
return v.toFixed(n);
}
} catch (e) {
}
return v;
},
updateTriggerZones(forceRefresh = true) {
if (this.playerDimensions && this.settings?.active?.ui?.inPlayer?.triggerZoneDimensions) {
this.triggerZoneStyles = {
width: `${Math.round(this.playerDimensions.width * this.settings.active.ui.inPlayer.triggerZoneDimensions.width)}px`,
height: `${Math.round(this.playerDimensions.height * this.settings.active.ui.inPlayer.triggerZoneDimensions.height)}px`,
transform: `translate(${(this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX)}%, ${this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY}%)`,
};
}
// if (forceRefresh) {
// this.$forceUpdate();
// }
},
handleMouseDown(corner, event) {
this.activeCornerDrag = corner;
// we need to save this because we don't know the location of the player element,
// just its dimensions ... that means we need to
this.dragStartPosition = {
x: event.clientX,
y: event.clientY
};
this.dragStartConfiguration = JSON.parse(JSON.stringify(this.settings.active.ui.inPlayer.triggerZoneDimensions));
},
handleMouseUp(event) {
if (!this.activeCornerDrag) {
return;
}
this.activeCornerDrag = undefined;
this.settings.saveWithoutReload();
},
handleMouseMove(event) {
if (!this.activeCornerDrag) {
return;
}
if (this.activeCornerDrag === 'offset') {
this.handleMove(event);
} else {
this.handleResize(event);
}
this.updateTriggerZones();
},
handleResize(event) {
// drag distance in px
const dx = event.clientX - this.dragStartPosition.x;
const dy = event.clientY - this.dragStartPosition.y;
// convert drag distance to % of current width:
const dxr = dx / this.playerDimensions.width * 2;
const dyr = dy / this.playerDimensions.height * 2;
// // update settings:
let nw, nh;
switch (this.activeCornerDrag) {
case 'tl':
nw = this.dragStartConfiguration.width - dxr;
nh = this.dragStartConfiguration.height - dyr;
break;
case 'tr':
nw = this.dragStartConfiguration.width + dxr;
nh = this.dragStartConfiguration.height - dyr;
break;
case 'bl':
nw = this.dragStartConfiguration.width - dxr;
nh = this.dragStartConfiguration.height + dyr;
break;
case 'br':
nw = this.dragStartConfiguration.width + dxr;
nh = this.dragStartConfiguration.height + dyr;
break;
}
// ensure everything is properly limited
const cw = Math.min(1, Math.max(0.125, nw));
const ch = Math.min(1, Math.max(0.125, nh));
// // update properties
this.settings.active.ui.inPlayer.triggerZoneDimensions.width = cw;
this.settings.active.ui.inPlayer.triggerZoneDimensions.height = ch;
},
handleMove(event) {
const dx = event.clientX - this.dragStartPosition.x;
const dy = event.clientY - this.dragStartPosition.y;
// convert drag distance to % of current width:
const dxr = dx / this.playerDimensions.width;
const dyr = dy / this.playerDimensions.height;
// const [min, max] = this.settings.active.ui.inPlayer.popupAlignment === 'right' ? [5, 90] : [-90, -5];
// const [minCrossAxis, maxCrossAxis] = [-90, 90];
const min = -90;
const max = -5;
const minCrossAxis = -90;
const maxCrossAxis = 90;
const cx = Math.min(max, Math.max(min, this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX + dxr));
const cy = Math.min(maxCrossAxis, Math.max(minCrossAxis, this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY + dyr));
this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX = cx;
this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY = cy;
},
//#region slider window
forceNumber(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, '');
},
setValue(key, originalValue, isTextInput) {
let value = originalValue;
if (isTextInput) {
value = (+this.forceNumber(value) / 100);
} else {
value = +this.forceNumber(value);
}
if (isNaN(+value)) {
value = 0.5;
}
this.settings.active.ui.inPlayer.triggerZoneDimensions[key] = value;
if (isTextInput) {
this.ghettoComputed[key] = this.optionalToFixed(originalValue, 0);
} else {
this.ghettoComputed[key] = this.optionalToFixed(originalValue * 100, 0);
}
this.updateSettings();
},
updateSettings(forceRefresh) {
this.settings.saveWithoutReload();
this.updateTriggerZones();
if (forceRefresh) {
this.$nextTick( () => this.$forceUpdate() );
}
},
//#endregion
finishTriggerZoneEdit() {
this.eventBus.send('finish-trigger-zone-edit');
},
}
}
</script>
<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>
.active-trigger-area {
background-image: url('/res/img/grid_512.webp');
background-position: center;
background-attachment: fixed;
}
.trigger-zone-editor {
width: 100%;
height: 100%;
position: relative;
> .corner {
position: absolute;
width: 32px;
height: 32px;
background-color: #000;
background-image: url('/res/img/corner-marker_64.webp');
background-size: cover;
}
.tr, .tl {
top: 0;
}
.br, .bl {
bottom: 0;
}
.tl, .bl {
left: 0;
}
.tr, .br {
right: 0;
}
.br {
transform: rotate(90deg);
}
.bl {
transform: rotate(180deg);
}
.tl {
transform: rotate(-90deg);
}
}
.trigger-zone-editor-window {
max-width: 69rem;
}
.trigger-zone-editor-sliders-container {
position: fixed;
width: 100dvw;
height: 100dvh;
top: 0;
left: 0;
z-index: 2000;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
.panel {
backdrop-filter: blur(0.5rem) brightness(0.5);
color: #ccc;
pointer-events: all;
padding: 1rem;
}
.range-input {
display: flex;
flex-direction: row;
* {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
input {
max-width: 5rem;
}
input[type=range] {
max-width: none;
}
}
.trigger-zone-editor {
padding-bottom: 2rem;
.field {
margin-bottom: -1em;
}
}
}
.range-input {
display: flex;
flex-direction: row;
* {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
input {
max-width: 5rem;
}
input[type=range] {
max-width: none;
}
}
.action-row {
width: 100%;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
</style>

View File

@ -1,77 +0,0 @@
<template>
<button
class="button drop-zone"
:class="[{'drag-over': isDragOver}, className]"
@click="onClick"
@dragover="isDragOver = true"
@dragleave="isDragOver = false"
@drop="onDrop"
>
<slot></slot>
<input type="file" ref="fileInput" @change="onFileChange" accept=".json" />
</button>
</template>
<script>
export default {
props: {
className: {
type: String,
default: ""
}
},
data() {
return {
isDragOver: false
};
},
methods: {
onClick() {
this.$refs.fileInput.click();
},
onDrop(event) {
event.preventDefault();
this.isDragOver = false;
if (event.dataTransfer.files.length > 0) {
this.handleFile(event.dataTransfer.files[0]);
}
},
onFileChange(event) {
if (event.target.files.length > 0) {
this.handleFile(event.target.files[0]);
}
},
handleFile(file) {
if (file.type !== "application/json") {
this.$emit('error', 'NOT_JSON_FILE');
return;
}
const reader = new FileReader();
reader.onload = async (event) => {
try {
const importedData = JSON.parse(event.target.result);
this.$emit('importedJson', importedData);
} catch (error) {
this.$emit('error', 'INVALID_JSON');
}
};
reader.readAsText(file);
},
}
}
</script>
<style lang="scss" scoped>
input {
display: none;
}
.drop-zone {
&.drag-over {
background-color: #fa6;
color: black;
}
}
</style>

View File

@ -1,6 +0,0 @@
export default {
// computed: {
// aspectRatioActions: this.settings?.active.commands.crop,
// stretchActions: this.settings?.active.commands.stretch
// }
}

View File

@ -1,37 +0,0 @@
export default {
computed: {
scopeActions: function() {
return this.settings?.active.actions?.filter(x => {
if (! x.scopes) {
console.error('This action does not have a scope.', x);
return false;
}
return x.scopes[this.scope] && x.scopes[this.scope].show
}) || [];
},
extensionActions: function(){
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-ExtensionMode') || [];
},
aardActions: function(){
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-autoar-mode') || [];
},
aspectRatioActions: function(){
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-ar') || [];
},
cropModePersistenceActions: function() {
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-ar-persistence') || [];
},
stretchActions: function(){
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-stretch') || [];
},
keyboardActions: function() {
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-keyboard') || [];
},
alignmentActions: function() {
return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-alignment') || [];
},
otherActions: function() {
return this.scopeActions.filter(x => x.cmd.length > 1) || [];
}
}
}

View File

@ -1,20 +0,0 @@
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);
},
}
}

View File

@ -1,129 +0,0 @@
export default {
data() {
return {
editMode: false,
editModeOptions: {}
}
},
methods: {
enableEditMode() {
this.editMode = true;
this.editModeOptions = {};
},
disableEditMode() {
this.editMode = false;
},
editAction(command, index, actionType) {
try {
if (!this.editModeOptions[actionType]) {
this.editModeOptions[actionType] = {selected: command, selectedIndex: index}
} else {
this.editModeOptions[actionType].selected = command;
this.editModeOptions[actionType].selectedIndex = index;
}
} catch (e) {
console.error(`[Ultrawidify] there's a problem with VideoSettings.vue::editAction():`, e);
}
},
updateSelectedShortcut(shortcut, actionType) {
try {
if (!this.editModeOptions[actionType]?.selected) {
return;
} else {
this.editModeOptions[actionType].selected.shortcut = shortcut
}
} catch (e) {
console.error(`[Ultrawidify] there's a problem with VideoSettings.vue::updateShortcut():`, e);
}
},
cancelEdit(actionType) {
try {
if (!this.editModeOptions[actionType]) {
return;
} else {
this.editModeOptions[actionType] = undefined;;
}
} catch (e) {
console.error(`[Ultrawidify] there's a problem with VideoSettings.vue::cancelEdit():`, e);
}
},
validateAspectRatio(actionType) {
// deal with potentially invalid aspect ratios
if (this.editModeOptions[actionType].selected.arguments.ratio.includes(':')) {
const ratioParts = this.editModeOptions[actionType].selected.arguments.ratio.split(':');
if (ratioParts.length !== 2) {
this.editModeOptions[actionType].error = 'Entered aspect ratio is not valid.';
return false;
} else {
const ratio = ratioParts[0] / ratioParts[1];
if (isNaN(ratio) || ratio <= 0) {
this.editModeOptions[actionType].error = 'Entered aspect ratio is not valid.';
return false;
}
this.editModeOptions[actionType].selected.arguments.ratio = ratio;
}
} else {
if (isNaN(this.editModeOptions[actionType].selected.arguments.ratio)) {
this.editModeOptions[actionType].error = "Entered aspect ratio is not valid.";
return false;
}
}
delete this.editModeOptions[actionType].error;
return true;
},
/**
* Updates button label, ideally after aspect ratio is entered,
* after input field blurred, and only if aspect ratio is valid.
*/
updateLabel(actionType) {
if (this.validateAspectRatio(actionType) && this.editModeOptions[actionType].selected.label === 'New aspect ratio') {
this.editModeOptions[actionType].selected.label = this.editModeOptions[actionType].selected.arguments.ratio;
}
},
saveShortcut(actionType) {
if (! this.validateAspectRatio(actionType)) {
return;
}
if (!this.editModeOptions[actionType]?.selectedIndex) {
this.settings.active.commands[actionType].push(this.editModeOptions[actionType].selected);
}
this.settings.active.commands[actionType][this.editModeOptions[actionType].selectedIndex] = this.editModeOptions[actionType]?.selected;
this.settings.saveWithoutReload();
this.editModeOptions[actionType] = undefined;
},
deleteAction(actionType) {
const selectedIndex = this.editModeOptions[actionType].selectedIndex;
// prevent deleting first item if 'delete' button shows on 'add new' dialog
if (selectedIndex === undefined || selectedIndex === null) {
return;
}
this.settings.active.commands[actionType].splice(selectedIndex, 1);
this.settings.saveWithoutReload();
this.editModeOptions[actionType] = undefined;
},
/**
* Parses command's keyboard shortcut into human-readable label
*/
getKeyboardShortcutLabel(command) {
if (! command.shortcut) {
return '';
}
return KeyboardShortcutParser.parseShortcut(command.shortcut);
},
}
}

View File

@ -1,15 +0,0 @@
import KeyboardShortcutParser from '../../../common/js/KeyboardShortcutParser';
export default {
methods: {
/**
* Parses command's keyboard shortcut into human-readable label
*/
getKeyboardShortcutLabel(command) {
if (! command.shortcut) {
return '';
}
return KeyboardShortcutParser.parseShortcut(command.shortcut);
},
}
}

View File

@ -1,157 +0,0 @@
export default {
created() {
/**
* Setup the "companion" onMouseMove handler to the one in the content script.
* We can handle events with the same function we use to handle events from
* the content script.
*/
document.addEventListener('mousemove', (event) => {
this.handleProbe(
{
coords: {
x: event.clientX,
y: event.clientY
},
isCompanion: true,
},
this.origin
);
});
},
data() {
return {
playerDimensions: undefined,
triggerZoneStyles: {
height: '50dvh',
width: '50dvw',
transform: 'translateX(-50%)'
},
hoverStats: {
isOverTriggerZone: false,
isOverMenuTrigger: false,
isOverUIArea: false,
hasMouse: false,
}
}
},
methods: {
playerDimensionsUpdate(dimensions) {
if (!dimensions?.width || !dimensions?.height) {
this.playerDimensions = undefined;
}
if (dimensions?.width !== this.playerDimensions?.width || dimensions?.height !== this.playerDimensions?.height) {
this.playerDimensions = dimensions;
this.updateTriggerZones();
}
},
updateTriggerZones() {
if (this.playerDimensions && this.settings) {
this.triggerZoneStyles = {
width: `${Math.round(this.playerDimensions.width * this.settings.active.ui.inPlayer.triggerZoneDimensions.width)}px`,
height: `${Math.round(this.playerDimensions.height * this.settings.active.ui.inPlayer.triggerZoneDimensions.height)}px`,
transform: `translate(${(this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX)}%, ${this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY}%)`,
};
}
},
/**
* Handles 'uwui-probe' events. It checks whether there's a clickable element under
* cursor, and sends a reply to the content scripts that indicates whether pointer-events
* property of the iframe should be set to capture or ignore the clicks.
*/
handleProbe(eventData, origin) {
if (eventData.ts < this.lastProbeTs) {
return; // i don't know if events can arrive out-of-order. Prolly not. We still check.
}
this.lastProbeTs = eventData.ts;
/* we check if our mouse is hovering over an element.
*
* gentleman's agreement: elements with uw-clickable inside the iframe will
* toggle pointerEvents on the iframe from 'none' to 'auto'
* Children of uw-clickable events should also do that.
*
* TODO: rename uw-clickable to something else, since we pretty much need that on
* our top-level element.
*/
let isClickable = false;
let isOverTriggerZone = false;
let isOverUIArea = false;
let isOverMenuTrigger = false;
const elements = document.elementsFromPoint(eventData.coords.x, eventData.coords.y);
if (!elements.length) {
return;
}
for (const element of elements) {
if (element.classList?.contains('uw-clickable')) {
isClickable = true;
}
if (element.classList?.contains('uw-ui-trigger')) {
isOverTriggerZone = true;
}
if (element.classList?.contains('uw-ui-area')) {
isOverUIArea = true;
}
if (element.classList?.contains('uw-menu-trigger')) {
isOverMenuTrigger = true;
}
}
this.triggerZoneActive = isOverTriggerZone;
// show ultrawidify trigger zone and set it to vanish after 250ms
// but don't show the trigger zone behind an active popup
if (
eventData.canShowUI
&& (this.settings.active.ui.inPlayer.activation === 'player' ? isOverUIArea : isOverTriggerZone)
) {
if (! this.uwWindowVisible) {
this.uwTriggerZoneVisible = true;
clearTimeout(this.uwTriggerZoneTimeout);
const windowParent = window.parent;
this.uwTriggerZoneTimeout = setTimeout(
() => {
this.uwTriggerZoneVisible = false;
windowParent.postMessage(
{action: 'uwui-hidden', opacity: false},
origin
);
},
500
);
}
} else {
// this.uwTriggerZoneVisible = false;
}
const hasMouse = !!document.querySelector(':hover');
this.hoverStats.isOverTriggerZone = isOverTriggerZone;
this.hoverStats.isOverMenuTrigger = isOverMenuTrigger;
this.hoverStats.isOverUIArea = isOverUIArea;
this.hoverStats.hasMouse = hasMouse
window.parent.postMessage(
{
action: 'uwui-clickable',
clickable: isClickable,
opacity: isClickable || this.contextMenuActive || this.uwTriggerZoneVisible,
hoverStats: {
isOverTriggerZone: isOverTriggerZone,
isOverMenuTrigger: isOverMenuTrigger,
isOverUIArea: isOverUIArea,
hasMouse: hasMouse
},
ts: +new Date()
},
origin
);
},
}
}

View File

@ -26,6 +26,7 @@ export const BLANK_LOGGER_CONFIG: LogConfig = {
'App.vue': {enabled: false},
'Popup': {enabled: false},
'UI': {enabled: false},
'SettingsPage': {enabled: false},
},
origins: {

View File

@ -5,7 +5,13 @@ export class ClientMenu {
private host!: HTMLDivElement;
private shadow!: ShadowRoot;
private root!: HTMLDivElement;
private _root!: HTMLDivElement;
private set root(value: HTMLDivElement) {
this._root = value;
}
public get root(): HTMLDivElement {
return this._root;
}
private visible = false;
private menuPositionClasses: string[] = [];
@ -236,7 +242,10 @@ export class ClientMenu {
for (const item of items) {
const el = document.createElement('div');
el.className = `uw-menu-item uw-trigger`;
if (item.customId) {
el.id = item.customId;
}
el.className = `uw-menu-item uw-trigger ${item.customClassList ?? ''}`;
if (item.customHTML) {
if (item.customHTML instanceof HTMLElement) {

View File

@ -1,128 +0,0 @@
import BrowserDetect from '../../conf/BrowserDetect';
function url(file) {
return BrowserDetect.getURL(file);
}
export default class FontLoader {
static loadFonts() {
const fontsStyleElement = document.createElement('style');
fontsStyleElement.type = 'text/css';
fontsStyleElement.textContent = `
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-thin.woff2')} format('woff2'); /* Super Modern Browsers */
font-weight: 200;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-thin-italic.woff2')} format('woff2');
font-weight: 200;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-extralight.woff2')} format('woff2');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-extralight-italic.woff2')} format('woff2');
font-weight: 300;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-light.woff2')} format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-light-italic.woff2')} format('woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-regular.woff2')} format('woff2');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-italic.woff2')} format('woff2');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-semibold.woff2')} format('woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-semibold-italic.woff2')} format('woff2');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-bold.woff2')} format('woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: 'Overpass';
src: ${url('res/fonts/overpass-webfont/overpass-bold-italic.woff2')} format('woff2');
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: 'Overpass mono';
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-light.woff2')} format('woff2');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Overpass mono';
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-regular.woff2')} format('woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Overpass mono';
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-semibold.woff2')} format('woff2');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Overpass mono';
src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-bold.woff2')} format('woff2');
font-weight: 600;
font-style: normal;
}
`;
document.head.appendChild(fontsStyleElement);
}
}

View File

@ -1,186 +0,0 @@
import UI from './UI';
import VuexWebExtensions from 'vuex-webextensions';
import NotificationUi from '../../../csui/NotificationUi.vue';
import Notifications from '../../../common/data/notifications';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading: PlayerNotificationUi");
}
let MuteScope = Object.freeze({
CurrentSite: 'current-site',
Global: 'global'
});
class PlayerNotificationUi extends UI {
constructor (
playerElement,
settings,
eventBus,
) {
super(
'notification',
PlayerNotificationUi.getStoreConfig(),
PlayerNotificationUi.getUiConfig(playerElement),
PlayerNotificationUi.getCommsConfig(),
{ eventBus }
);
this.settings = settings;
this.eventBus = eventBus;
}
//#region constructor helpers
// we will move some things out of the constructor in order to keep things clean
static getStoreConfig() {
return {
plugins: [
VuexWebExtensions({
persistentStates: [
'notificationConfig'
],
}),
],
state: {
// should be null by default!
notificationConfig: {
text: 'sample text <b>now with 100% more html formatting!</b>',
icon: 'exclamation-circle',
timeout: 5000,
}
},
mutations: {
'uw-set-notification'(state, payload) {
state['notificationConfig'] = payload;
}
},
actions: {
'uw-set-notification'({commit}, payload) {
commit('uw-set-notification', payload);
}
}
};
}
static getUiConfig(playerElement) {
return {
parentElement: playerElement,
component: NotificationUi
}
}
static getCommsConfig() {
return {
handlers: {
'show-notification': [(message) => this.showNotification(message)],
}
}
}
//#endregion
//#region lifecycle
replace(playerElement) {
super.replace(PlayerNotificationUi.getUiConfig(playerElement));
}
//#endregion
/**
* Show notification on screen.
*
* @param notificationConfig notification config (or ID of notification config in /common/data/notifications.js)
*
* notificationConfig should resemble this:
* {
* timeout: number how long we'll be displaying the notification. If empty, 10s. -1: until user dismisses it
* icon: string what icon we're gonna show. We're using bootstrap icons. Can be empty.
* text: notification text. Supports HTML.
* notificationActions: [
* {
* command: function that gets executed upon clicking the button.
* label: label of the button
* icon: icon of the button
* }
* ],
* hideActions: [
* // more of notificationActions but with special case
* ]
* }
*
* When notificationConfig is a string, the function will add two additional notifications on the notificationActionsPile
* * never show this notification ever again on any site
* * never show this notification again on this site
*/
showNotification(notificationConfig) {
if (typeof notificationConfig === 'string') {
try {
const config = Notifications[notificationConfig];
// this should _never_ appear on production version of the extension, but it should help with development.
if (!config) {
return this.vuexStore?.dispatch('uw-set-notification', {
icon: 'x-circle-fill',
text: `Notification for key ${notificationConfig} does not exist.`,
timeout: -1,
});
}
// don't show notification if it's muted
if (this.isNotificationMuted(notificationConfig)) {
return;
}
this.vuexStore?.dispatch('uw-set-notification', {
...config,
hideActions: [
{
command: () => this.muteNotification(notificationConfig, MuteScope.CurrentSite),
label: 'this site'
},
{
command: () => this.muteNotification(notificationConfig, MuteScope.Global),
label: 'never ever'
}
]
});
} catch (e) {
console.error('theres been an error:', e)
}
} else {
this.vuexStore?.dispatch('uw-set-notification', notificationConfig);
}
}
muteNotification(notificationId, scope) {
// ensure objects we try to set exist
if (!this.settings.active.mutedNotifications) {
this.settings.active.mutedNotifications = {};
}
if (!this.settings.active.mutedNotifications[notificationId]) {
this.settings.active.mutedNotifications[notificationId] = {};
}
// actually mute notification
if (scope === MuteScope.Global) {
this.settings.active.mutedNotifications[notificationId].$global = true;
} else {
this.settings.active.mutedNotifications[notificationId][window.location.hostname] = true;
}
// persist settings
this.settings.saveWithoutReload();
}
isNotificationMuted(notificationId) {
return this.settings.active.mutedNotifications?.[notificationId]?.$global
|| this.settings.active.mutedNotifications?.[notificationId]?.[window.location.hostname];
}
}
if (process.env.CHANNEL !== 'stable'){
console.info("PlayerNotificationUi loaded");
}
export default PlayerNotificationUi;

View File

@ -7,22 +7,18 @@ import Settings from '../settings/Settings';
import { MenuPosition as MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
import { CommandInterface } from '@src/common/interfaces/SettingsInterface';
import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
import { ComponentLogger } from '../logging/ComponentLogger';
import alignmentIndicatorSvg from '!!raw-loader!@ui/res/img/alignment-indicators.svg';
import lockBarIndicatorSvg from '!!raw-loader!@ui/res/img/lock-bar-indicators.svg';
if (process.env.CHANNEL !== 'stable'){
console.info("Loading: UI");
}
const csuiVersions = {
'normal': 'csui', // csui-overlay-normal.html, maps to csui.html
'light': 'csui-light', // csui-overlay-light.html, maps to csui-light.html
'dark': 'csui-dark' // csui-overlay-dark.html, maps to csui-dark.html
};
const MAX_IFRAME_ERROR_COUNT = 5;
class UI {
isGlobal: boolean;
isIframe: boolean;
@ -32,19 +28,13 @@ class UI {
private settings: Settings;
private siteSettings: SiteSettings;
private csuiScheme;
// These can prolly be removed:
lastProbeResponseTs: number;
private extensionMenu: ClientMenu;
private logger: ComponentLogger;
constructor(
public interfaceId,
public uiConfig, // {parentElement?, eventBus?, isGlobal?, playerData}
) {
this.lastProbeResponseTs = null;
this.isGlobal = uiConfig.isGlobal ?? false;
this.isIframe = window.self !== window.top;
@ -55,12 +45,6 @@ class UI {
this.siteSettings = uiConfig.siteSettings;
this.settings = uiConfig.settings;
// TODO: at some point, UI should be different for global popup and in-player UI
this.csuiScheme = this.getCsuiScheme();
const csuiVersion = this.getCsuiVersion(this.csuiScheme.contentScheme);
// this.uiURI = chrome.runtime.getURL(`/csui/${csuiVersion}.html`);
// this.extensionBase = chrome.runtime.getURL('').replace(/\/$/, "");
// UI will be initialized when setUiVisibility is called
if (!this.isGlobal) {
this.init();
@ -68,33 +52,125 @@ class UI {
}
async init() {
this.destroy()
this.destroy();
if (!this.logger) {
this.logger = new ComponentLogger(this.settings.logAggregator, 'UI');
}
this.createExtensionMenu();
// this.initUIContainer();
// this.initMessaging();
this.eventBus.subscribeMulti({
'uw-config-broadcast': {
function: (message) => {
console.log('UI.ts: received uw-config-broadcast', message);
if (message.type === 'aard-error') {
console.log('received: warning element:', this.extensionMenu.root.querySelector('#uw-cors-warning'));
this.updateMenuWarnings(message);
} else if (message.type === 'drm-status') {
this.updateMenuWarnings(message);
}
}
}
});
}
executeCommand(x: CommandInterface) {
this.eventBus.send(x.action, x.arguments);
}
updateMenuWarnings(errors: {aardErrors?: any, hasDrm?: boolean}) {
try {
if (errors.aardErrors) {
if (errors.aardErrors.cors) {
this.extensionMenu.root.querySelector('#uw-cors-warning').classList.remove('uw-hidden');
}
if (errors.aardErrors.drm) {
this.extensionMenu.root.querySelector('#uw-drm-warning').classList.remove('uw-hidden');
}
if (errors.aardErrors.webglError) {
this.extensionMenu.root.querySelector('#uw-webgl-warning').classList.remove('uw-hidden');
}
}
if (errors.hasDrm) {
this.extensionMenu.root.querySelector('#uw-drm-warning').classList.remove('uw-hidden');
}
} catch (e) {
this.logger.error("UI.ts: failed to update menu warnings:", e);
}
}
private getSiteSupportLevelForDisplay() {
const siteSupportLevel = (this.siteSettings ? this.siteSettings.data.type ?? SiteSupportLevel.Unknown : 'waiting')
switch (siteSupportLevel) {
case SiteSupportLevel.OfficialSupport:
return {cssClass: 'uw-official', text: 'Verified', svg: 'check-decagram'};
case SiteSupportLevel.CommunitySupport:
return {cssClass: 'uw-community', text: 'Community', svg: 'account-group'};
case SiteSupportLevel.Unknown:
return {cssClass: 'uw-no-support', text: 'Untested', svg: 'help-circle-outline'};
case SiteSupportLevel.UserDefined:
return {cssClass: 'uw-user-added', text: 'Modified by you', svg: 'account'};
case SiteSupportLevel.UserModified:
return {cssClass: 'uw-user-added', text: 'Modified by you', svg: 'account'};
default:
return {cssClass: 'uw-unknown', text: 'Unknown', svg: 'help-circle-outline'};
}
}
createExtensionMenu() {
console.log('—>—— creating ext menu:. is enabled?', this.siteSettings?.data.enableUI, ExtensionMode[this.siteSettings?.data.enableUI], this.siteSettings?.data.enableUI === ExtensionMode.Disabled, 'global?', this.isGlobal)
if (+this.siteSettings?.data.enableUI === ExtensionMode.Disabled || this.isGlobal) {
console.log('we said no to UI')
return; // don't
}
if (this.extensionMenu) {
this.extensionMenu.destroy();
}
const svgMap = {
'check-decagram': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>check-decagram</title><path d="M23,12L20.56,9.22L20.9,5.54L17.29,4.72L15.4,1.54L12,3L8.6,1.54L6.71,4.72L3.1,5.53L3.44,9.21L1,12L3.44,14.78L3.1,18.47L6.71,19.29L8.6,22.47L12,21L15.4,22.46L17.29,19.28L20.9,18.46L20.56,14.78L23,12M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9L10,17Z" /></svg>',
'account-group': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>account-group</title><path d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z" /></svg>',
'help-circle-outline': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>help-circle-outline</title><path d="M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z" /></svg>',
'account': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>account</title><path d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z" /></svg>'
};
const supportLevelInfo = this.getSiteSupportLevelForDisplay();
if (this.uiConfig.parentElement) {
const menuConfig = {
isGlobal: this.isGlobal,
menuPosition: MenuPosition.Left,
items: [
{
label: "todo: site + site compatibility info"
customClassList: 'uw-site-info',
customHTML: `
<div class="uw-menu-site">${this.siteSettings?.site ?? '<unknown site>'}</div>
<div class="uw-site-support uw-site-support-level ${supportLevelInfo.cssClass} uw-menu-support-level">
${svgMap[supportLevelInfo.svg]}
<span>${supportLevelInfo.text}</span>
</div>
`
},
{
customId: 'uw-drm-warning',
customClassList: `uw-menu-warning ${this.playerData?.videoData.hasDrm ? '' : 'uw-hidden'}`,
customHTML: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>alert</title><path d="M13 14H11V9H13M13 18H11V16H13M1 21H23L12 2L1 21Z" /></svg>
<div>Autodetection not available<br/> due to DRM</div>
`
},
{
customId: 'uw-cors-warning',
customClassList: 'uw-menu-warning uw-hidden',
customHTML: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>alert</title><path d="M13 14H11V9H13M13 18H11V16H13M1 21H23L12 2L1 21Z" /></svg>
<div>Autodetection not available<br/> due to CORS error</div>
`
},
{
customId: 'uw-webgl-warning',
customClassList: 'uw-menu-warning uw-hidden',
customHTML: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>alert</title><path d="M13 14H11V9H13M13 18H11V16H13M1 21H23L12 2L1 21Z" /></svg>
<div>Autodetection not available<br/> due to WEBGL error<br/>
<small>Check if hardware acceleration is<br/>enabled and working.</small></div>
`
},
{
label: 'Crop',
@ -105,6 +181,15 @@ class UI {
}
})
},
{
label: 'Stretch',
subitems: this.settings.active.commands.stretch.map((x: CommandInterface) => {
return {
label: x.label,
action: () => this.executeCommand(x)
}
})
},
{
label: 'Zoom (presets)',
subitems: [
@ -116,19 +201,70 @@ class UI {
}),
]
},
{
label: 'Stretch',
subitems: this.settings.active.commands.stretch.map((x: CommandInterface) => {
return {
label: x.label,
action: () => this.executeCommand(x)
}
})
},
{
label: 'Zoom (free-form)',
subitems: [
{label: 'todo sliders'}
{
customHTML: `
<div class="uw-freeform-zoom-container">
<!-- SLIDERS BEFORE X/Y LOCK -->
<div class="uw-freeform-zoom-sliders">
<div class="uw-slider">
<div class="uw-slider-label">Width:</div>
<div id="zoomWidth" class="uw-slider-zoom">100%</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"
/>
</div>
</div>
<div class="uw-slider">
<div class="uw-slider-label">Height:</div>
<div id="zoomHeight" class="uw-slider-zoom">100%</div>
<div class="uw-slider-input">
<input id="_input_zoom_slider_2"
class="w-full"
type="range"
step="any"
min="-1"
max="4"
/>
</div>
</div>
</div>
<!-- X/Y LOCK, LINK BAR EDITION -->
<div class="uw-slider-lock-bar-container uw-linked">
${lockBarIndicatorSvg}
</div>
<div class="uw-freeform-zoom-button-container">
<div
id="_button_toggle_aspect_lock"
class="uw-freeform-zoom-button uw-link-button uw-linked"
>
<svg id="_button_toggle_aspect_lock_locked" class="uw-linked" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>link-variant</title><path d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" /></svg>
<svg id="_button_toggle_aspect_lock_unlocked" class="uw-unlinked" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>link-variant-off</title><path d="M2,5.27L3.28,4L20,20.72L18.73,22L13.9,17.17L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L12.5,15.76L10.88,14.15C10.87,14.39 10.77,14.64 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C8.12,13.77 7.63,12.37 7.72,11L2,5.27M12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.79,8.97L9.38,7.55L12.71,4.22M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.2,10.54 16.61,12.5 16.06,14.23L14.28,12.46C14.23,11.78 13.94,11.11 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" /></svg>
</div>
</div>
<div class="uw-freeform-zoom-button-container">
<div
id="_button_reset_zoom"
class="uw-freeform-zoom-button"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>restore</title><path d="M13,3A9,9 0 0,0 4,12H1L4.89,15.89L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3Z" /></svg>
</div>
</div>
</div>
</div>
`,
}
]
},
{
@ -148,103 +284,6 @@ class UI {
}
}
/**
* Returns color scheme we need to use.
*
* contentScheme is used to select the correct HTML template.
* iframeScheme gets applied to the iframe as style
* @returns {contentScheme: string, iframeScheme: string}
*/
getCsuiScheme() {
return {
contentScheme: window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme'),
iframeScheme: document.documentElement.style.colorScheme || document.body.style.colorScheme || undefined
};
}
/**
* Returns correct template for given preferredScheme parameter
* @param {*} preferredScheme
* @returns
*/
getCsuiVersion(preferredScheme) {
return csuiVersions[preferredScheme] ?? csuiVersions.normal;
}
initUIContainer() {
const random = Math.round(Math.random() * 69420);
const uwid = `uw-ultrawidify-${this.interfaceId}-root-${random}`
return;
}
// initMessaging() {
// // subscribe to events coming back to us. Unsubscribe if iframe vanishes.
// window.addEventListener('message', this.messageHandlerFn);
// /* set up event bus tunnel from content script to UI — necessary if we want to receive
// * like current zoom levels & current aspect ratio & stuff. Some of these things are
// * necessary for UI display in the popup.
// */
// this.eventBus.subscribeMulti(
// {
// 'uw-config-broadcast': {
// function: (config, routingData) => {
// this.sendToIframe('uw-config-broadcast', config, routingData);
// }
// },
// 'uw-set-ui-state': {
// function: (config, routingData) => {
// if (config.globalUiVisible !== undefined && !this.isIframe) {
// if (this.isGlobal) {
// this.setUiVisibility(config.globalUiVisible);
// } else {
// this.setUiVisibility(!config.globalUiVisible);
// }
// }
// this.sendToIframe('uw-set-ui-state', {...config, isGlobal: this.isGlobal}, routingData);
// }
// },
// 'uw-get-page-stats': {
// function: (config, routingData) => {
// this.eventBus.send(
// 'uw-page-stats',
// {
// pcsDark: window.matchMedia('(prefers-color-scheme: dark)').matches,
// pcsLight: window.matchMedia('(prefers-color-scheme: light)').matches,
// colorScheme: window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme')
// },
// {
// comms: {
// forwardTo: 'popup'
// }
// }
// );
// }
// },
// 'uw-restore-ui-state': {
// function: (config, routingData) => {
// if (!this.isGlobal) {
// this.setUiVisibility(true);
// this.sendToIframe('uw-restore-ui-state', config, routingData);
// }
// }
// }
// },
// this
// );
// }
// messageHandlerFn = (message) => {
// if (!this.uiIframe?.contentWindow) {
// window.removeEventListener('message', this.messageHandlerFn);
// return;
// }
// this.handleMessage(message);
// }
setUiVisibility(visible) {
return;
}
@ -262,208 +301,19 @@ class UI {
// }
}
/**
* Checks whether mouse is moving over either:
* * <video> element
* * player element ()
* * uwui-clickable element
*/
canShowUI() {
const playerCssClass = 'uw-ultrawidify-player-css';
const result = {
playerDimensions: undefined,
canShowUI: false,
}
if (this.playerData?.environment && this.siteSettings.canRunUI(this.playerData?.environment)) {
return result;
}
if (this.playerData?.dimensions) {
result.playerDimensions = this.playerData.dimensions;
}
// if player is not wide enough, we do nothing
if (
!this.isGlobal && // this.isGlobal is basically 'yes, do as I say'
!document.fullscreenElement && // if we are in full screen, we allow it in every case as player detection is not 100% reliable,
result.playerDimensions?.width && // which makes playerDimensions.width unreliable as well (we assume nobody uses browser in
// fullscreen mode unless watching videos)
result.playerDimensions.width < window.screen.width * (this.uiSettings.inPlayer.minEnabledWidth ?? 0)
) {
return result;
}
result.canShowUI = true;
return result;
}
// /**
// * Handles events received from the iframe.
// * @param {*} event
// */
// handleMessage(event) {
// if (event.origin === this.extensionBase) {
// switch(event.data.action) {
// case 'uwui-clickable':
// if (event.data.ts < this.lastProbeResponseTs) {
// return;
// }
// if (this.disablePointerEvents) {
// return;
// }
// this.lastProbeResponseTs = event.data.ts;
// // If iframe returns 'yes, we are clickable' and iframe is currently set to pointerEvents=auto,
// // but hasMouse is false, then UI is attached to the wrong element. This probably means our
// // detected player element is wrong. We need to perform this check if we aren't in global UI
// /**
// * action: 'uwui-clickable',
// * clickable: isClickable,
// * hoverStats: {
// * isOverTriggerZone,
// * isOverMenuTrigger,
// * isOverUIArea,
// * hasMouse: !!document.querySelector(':hover'),
// * },
// * ts: +new Date()
// */
// if (!this.global) {
// if (
// this.uiIframe.style.pointerEvents === 'auto'
// ) {
// if (
// event.data.hoverStats.isOverMenuTrigger
// && !event.data.hoverStats.hasMouse
// ) {
// if (!this.iframeConfirmed) {
// if (this.iframeErrorCount++ > MAX_IFRAME_ERROR_COUNT && !this.iframeRejected) {
// this.iframeRejected = true;
// this.eventBus.send('change-player-element');
// return;
// }
// }
// } else if (event.data.hoverStats.isOverMenuTrigger && event.data.hoverStats.hasMouse) {
// this.iframeConfirmed = true;
// }
// }
// }
// this.uiIframe.style.pointerEvents = event.data.clickable ? 'auto' : 'none';
// this.uiIframe.style.opacity = event.data.opacity || this.isGlobal ? '100' : '0';
// break;
// case 'uw-bus-tunnel':
// const busCommand = event.data.payload;
// this.eventBus.send(
// busCommand.action,
// busCommand.config,
// {
// ...busCommand?.context,
// borderCrossings: {
// ...busCommand?.context?.borderCrossings,
// iframe: true,
// }
// }
// );
// break;
// case 'uwui-get-role':
// this.sendToIframeLowLevel('uwui-set-role', {role: this.isGlobal ? 'global' : 'player'});
// break;
// case 'uwui-interface-ready':
// this.setUiVisibility(!this.isGlobal);
// break;
// case 'uwui-hidden':
// this.uiIframe.style.opacity = event.data.opacity || this.isGlobal ? '100' : '0';
// break;
// case 'uwui-global-window-hidden':
// if (!this.isGlobal) {
// return; // This shouldn't even happen in non-global windows
// }
// this.setUiVisibility(false);
// this.eventBus.send('uw-restore-ui-state', {});
// }
// }
// }
// /**
// * Sends messages to iframe. Messages sent with this function _generally_
// * bypass eventBus on the receiving end.
// * @param {*} action
// * @param {*} payload
// * @param {*} uiURI
// */
// sendToIframeLowLevel(action, payload, uiURI = this.uiURI) {
// // because existence of UI is not guaranteed — UI is not shown when extension is inactive.
// // If extension is inactive due to "player element isn't big enough to justify it", however,
// // we can still receive eventBus messages.
// if (this.rootDiv && this.uiIframe) {
// this.uiIframe.contentWindow?.postMessage(
// {
// action,
// payload
// },
// uiURI
// )
// };
// }
// /**
// * Sends message to iframe. Messages sent with this function will be routed to eventBus.
// */
// sendToIframe(action, actionConfig, routingData, uiURI = this.uiURI) {
// // if (routingData) {
// // if (routingData.crossedConnections?.includes(EventBusConnector.IframeBoundaryIn)) {
// // console.warn('Denied message propagation. It has already crossed INTO an iframe once.');
// // return;
// // }
// // }
// // if (!routingData) {
// // routingData = { };
// // }
// // if (!routingData.crossedConnections) {
// // routingData.crossedConnections = [];
// // }
// // routingData.crossedConnections.push(EventBusConnector.IframeBoundaryIn);
// this.sendToIframeLowLevel(
// 'uw-bus-tunnel',
// {
// action,
// config: actionConfig,
// routingData
// },
// uiURI
// );
// }
/**
* Replaces ui config and re-inits the UI
* @param {*} newUiConfig
*/
replace(newUiConfig) {
this.uiConfig = newUiConfig;
if (this.rootDiv) {
this.destroy();
this.init();
}
this.init();
}
destroy() {
if (this.extensionMenu) {
this.extensionMenu.destroy();
}
// this.unloadIframe();
// this.eventBus.unsubscribeAll(this);
// // this.comms?.destroy();
// this.rootDiv?.remove();
// delete this.uiIframe;
// delete this.rootDiv;
}
}

View File

@ -334,6 +334,8 @@ class Resizer {
return;
}
this.logger.info('setAr', '%cCalling setAr with arguments:', 'color: #fa6;', {...ar, type: AspectRatioType[ar.type] }, 'last ar:', lastAr);
// If we are missing some data that's necessary for crop calculations,
// set pendingAr and quit
if (!this.videoData.player.dimensions?.width || !this.videoData.player.dimensions?.height) {
@ -465,6 +467,7 @@ class Resizer {
);
}
this.logger.info('setAr', 'Stretch factors are calculated:', stretchFactors);
this.applyScaling(stretchFactors as VideoDimensions);
}

View File

@ -75,25 +75,59 @@ html, body {
@apply opacity-50;
}
}
/* Site support levels */
.site-support-level {
&.official {
@apply text-primary-400;
/* Site support levels */
.uw-site-support {
@apply inline-flex flex-row items-center gap-1 rounded-[0.5rem] relative;
.tooltip {
padding: 1rem;
display: none;
position: absolute;
bottom: 0;
transform: translateY(110%);
width: 42em;
background-color: rgba(0,0,0,0.90);
color: #ccc;
z-index: 99999 !important;
white-space: normal;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}
&:hover {
&.no-tooltip {
.tooltip {
display: none !important;
}
}
&.community {
@apply text-indigo-600;
}
&.officially-disabled {
@apply text-red-600;
}
&.no-support {
@apply text-slate-600;
}
&.user-added {
@apply text-cyan-700;
.tooltip {
display: block;
}
}
/* END site support levels */
}
.uw-site-support-level {
&.uw-official {
@apply text-primary-400;
}
&.uw-community {
@apply text-indigo-600;
}
&.uw-officially-disabled {
@apply text-red-600;
}
&.no-support {
@apply text-slate-600;
}
&.uw-user-added {
@apply text-cyan-700;
}
}
/* END site support levels */

View File

@ -47,7 +47,6 @@
"res/img/settings/about-bg.png",
"res/icons/*",
"res/img/*",
"csui/*",
"ui/*",
"main.css"
],

View File

@ -47,7 +47,6 @@
"res/img/settings/about-bg.png",
"res/icons/*",
"res/img/*",
"csui/*"
],
"permissions": [
"storage",

View File

@ -1,72 +1,76 @@
<template>
<div v-if="computedSiteSupportLevel === SiteSupportLevel.OfficialSupport"
class="site-support site-support-level official"
:class="{'no-tooltip': disableTooltip}"
:style="supportLevelStyle"
>
<mdicon name="check-decagram" />
<div v-if="!small">Verified</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Verified&nbsp;&nbsp;</template>
The extension is being tested and should work on this site.
</div>
<!--
NOTE: extension popup doesn't use this component. Instead, it
re-implements messages and tooltips on its own. Any changes
to logic & wording here should be carried over there as well.
-->
<div v-if="computedSiteSupportLevel === SiteSupportLevel.OfficialSupport"
class="uw-site-support uw-site-support-level uw-official"
:class="{'no-tooltip': disableTooltip}"
:style="supportLevelStyle"
>
<mdicon name="check-decagram" />
<div v-if="!small">Verified</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Verified&nbsp;&nbsp;</template>
The extension is being tested and should work on this site.
</div>
<div v-if="computedSiteSupportLevel === SiteSupportLevel.CommunitySupport"
class="site-support site-support-level community"
:class="{'no-tooltip': disableTooltip}"
:style="supportLevelStyle"
>
<mdicon name="account-group" />
<div v-if="!small">Community</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Community&nbsp;&nbsp;</template>
People say extension works on this site (or have provided help getting the extension to work if it didn't).<br/><br/>
Tamius (the dev) does not test the extension on this site, probably because it requires a subscription or
is geoblocked.
</div>
</div>
<div v-if="computedSiteSupportLevel === SiteSupportLevel.CommunitySupport"
class="uw-site-support uw-site-support-level uw-community"
:class="{'no-tooltip': disableTooltip}"
:style="supportLevelStyle"
>
<mdicon name="account-group" />
<div v-if="!small">Community</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Community&nbsp;&nbsp;</template>
People say extension works on this site (or have provided help getting the extension to work if it didn't).<br/><br/>
Tamius (the dev) does not test the extension on this site, probably because it requires a subscription or
is geoblocked.
</div>
<div v-if="computedSiteSupportLevel === SiteSupportLevel.Unknown"
class="site-support site-support-level no-support"
:class="{'no-tooltip': disableTooltip}"
:style="supportLevelStyle"
>
<mdicon name="help-circle-outline" />
<div v-if="!small">Untested</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Untested&nbsp;&nbsp;</template>
Extension will try to fix things, but no promises; for you are exploring the uncharted lands.<br/><br/>
Tamius (the dev) does not test the extension on this site for various reasons
(unaware, not using the site, language barrier, geoblocking, paid services Tam doesn't use).
</div>
</div>
<div v-if="computedSiteSupportLevel === SiteSupportLevel.Unknown"
class="uw-site-support uw-site-support-level uw-no-support"
:class="{'no-tooltip': disableTooltip}"
:style="supportLevelStyle"
>
<mdicon name="help-circle-outline" />
<div v-if="!small">Untested</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Untested&nbsp;&nbsp;</template>
Extension will try to fix things, but no promises; for you are exploring the uncharted lands.<br/><br/>
Tamius (the dev) does not test the extension on this site for various reasons
(unaware, not using the site, language barrier, geoblocking, paid services Tam doesn't use).
</div>
<div v-if="computedSiteSupportLevel === SiteSupportLevel.UserDefined || computedSiteSupportLevel === SiteSupportLevel.UserModified"
class="site-support site-support-level user-added"
:class="{'no-tooltip': disableTooltip}"
:style="supportLevelStyle"
>
<mdicon name="account" />
<div v-if="!small">Modified by you</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Modified by you&nbsp;&nbsp;</template>
You are on your own. You have manually changed settings for this site. The extension is doing what you told it to do.
</div>
</div>
<div v-if="computedSiteSupportLevel === SiteSupportLevel.UserDefined || computedSiteSupportLevel === SiteSupportLevel.UserModified"
class="uw-site-support uw-site-support-level uw-user-added"
:class="{'no-tooltip': disableTooltip}"
:style="supportLevelStyle"
>
<mdicon name="account" />
<div v-if="!small">Modified by you</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Modified by you&nbsp;&nbsp;</template>
You are on your own. You have manually changed settings for this site. The extension is doing what you told it to do.
</div>
<div v-if="computedSiteSupportLevel === SiteSupportLevel.BetaSupport">
</div>
<div v-if="computedSiteSupportLevel === SiteSupportLevel.BetaSupport">
</div>
<div v-if="computedSiteSupportLevel === 'officially-disabled'"
class="uw-site-support uw-site-support-level uw-officially-disabled"
:class="{'no-tooltip': disableTooltip}"
:style="supportLevelStyle"
>
<mdicon name="close-circle" />
<div v-if="!small">Blacklisted</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Blacklisted&nbsp;&nbsp;</template>
Extension is known to not work with this site.
</div>
<div v-if="computedSiteSupportLevel === 'officially-disabled'"
class="site-support site-support-level officially-disabled"
:class="{'no-tooltip': disableTooltip}"
:style="supportLevelStyle"
>
<mdicon name="close-circle" />
<div v-if="!small">Blacklisted</div>
<div class="tooltip" :style="tooltipStyle">
<template v-if="small">Blacklisted&nbsp;&nbsp;</template>
Extension is known to not work with this site.
</div>
</div>
<!-- </div> -->
</div>
</template>
<script lang="ts">
@ -99,40 +103,6 @@ export default defineComponent({
<style lang="postcss" scoped>
@import '@src/main.css'; /** postcss processor doesn't support aliases */
.site-support {
@apply inline-flex flex-row items-center gap-1 rounded-[0.5rem] relative;
.tooltip {
padding: 1rem;
display: none;
position: absolute;
bottom: 0;
transform: translateY(110%);
width: 42em;
background-color: rgba(0,0,0,0.90);
color: #ccc;
z-index: 99999 !important;
white-space: normal;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}
&:hover {
&.no-tooltip {
.tooltip {
display: none !important;
}
}
.tooltip {
display: block;
}
}
}
</style>

View File

@ -157,10 +157,10 @@
import SiteExtensionSettings from '@components/segments/ExtensionSettings/Panels/SiteExtensionSettings.vue';
import FrameSiteSettings from '@components/segments/ExtensionSettings/Panels/FrameSiteSettings.vue';
import OtherSiteSettings from '@components/segments/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';
import Popup from '@components/common/Popup';
import ConfirmButton from '@components/common/ConfirmButton';
import UploadJsonFileButton from '@components/common/UploadJsonFileButton';
import JsonEditor from '@components/common/JsonEditor';
export default {

View File

@ -0,0 +1,158 @@
<svg
id="_button_toggle_aspect_lock_locked"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 42"
>
<title>slider-lock</title>
<!-- NOTE: bigger Y - more down -->
<!--
SYMBOL FOR X/Y LINKED
(derived from same code as symbol for unlink x/y)
-->
<path class="uw-linked" d="
M0 0
L8 0
L8 12
L8 28
L8 42
L0 42
L0 40
L6 40
L6 2
L0 2
L0 0
" />
<!--
SYMBOL FOR UNLINK X/Y
We're drawing this shape for the link bar (and its mirror form)
start
:
O → → → 1
↑ ↑ * NOT DRAWN TO SCALE
5 ← ← 4 ↓
↑ ↓ * notice that this shape lacks the break bar
↑ ↓ chamfer. The chamfer is added when mirroring
↑ ↓ by offsetting both points by half of the
↑ ↓ line width.
3←2
Here's js function code to generate the path
(copy into browser console and correct variables as needed)
———————————————————————————————————————————————————————————————————————————————————————————
(() => {
const width = 8; // symbol width
const height = 42; // symbol height
const lineWidth = 2; // width of the link bar
const halfCut = 8; // half of the width of the break bar gap
let halfHeight = height/2;
const points = [
[0, 0], // 0
[width, 0], // 1
[width, halfHeight - halfCut ],// 2
[width - lineWidth, halfHeight - halfCut ],// 3
[width - lineWidth, lineWidth], // 4
[0, lineWidth], // 5
[0, 0] // 6/0
];
const flippedPoints = points.map(p => ([p[0], height - p[1]]));
const halfOffset = lineWidth/2;
// chamfer the bracket:
points[2][1] -= halfOffset;
points[3][1] += halfOffset;
flippedPoints[2][1] -= halfOffset;
flippedPoints[3][1] += halfOffset;
const out = `M${
points
.map(x => `${x[0]} ${x[1]}`)
.join(' L')
} M${
flippedPoints
.map(x => `${x[0]} ${x[1]}`)
.join(' L')
}`;
console.log('—————— link bar path: ', out);
return out;
})()
———————————————————————————————————————————————————————————————————————————————————————————
-->
<path class="uw-unlinked" d="M0 0 L8 0 L8 12 L6 14 L6 2 L0 2 L0 0 M0 42 L8 42 L8 28 L6 30 L6 40 L0 40 L0 42" />
<!--
SYMBOL FOR UNLINK X/Y (but it's the break bar)
We're drawing this shape for the break bar
symbol origin O
: ↗ ↘ ↗ X: + Y: -
\ ↗ O ↘ X: + Y: +
O ↙ ↙ X: - Y: +
↗ ↙ ↖ X: - Y: -
↗ ↙
O ↙
/ ↖ ↙
: O
:
Code starts drawing symbol here
Here's js function code to generate the path
(copy into browser console and correct variables as needed)
———————————————————————————————————————————————————————————————————————————————————————————
(() => {
let x = 8, y = 31;
let halfLength = 5;
let width = 1.5;
x -= halfLength;
y -= halfLength;
let coords = [
`M${x} ${y}`
];
let dx = 1;
let dy = -1;
for (let i = 0; i < 4; i++) {
if (i % 2) {
x = x + (width * dx);
y = y + (width * dy);
// flip the dy, so y moves in the opposite direction until reaching the end
dy = -dy;
} else {
x = x + (halfLength * 2 * dx);
y = y + (halfLength * 2 * dy);
// flip the dx, so x moves in the opposite direction until reaching the end
dx = -dx;
}
coords.push(`${x} ${y}`);
}
const out = coords.join(' L');
console.log('——————————— drawing strike line:', out);
return out;
})()
———————————————————————————————————————————————————————————————————————————————————————————
-->
<path class="uw-unlinked" d="M3 26 L13 16 L11.5 14.5 L1.5 24.5 L3 26" />
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,6 +1,14 @@
@import "tailwindcss";
@import "tailwindcss/utilities";
svg {
@apply text-inherit fill-current stroke-current;
* {
@apply text-inherit;
}
}
.uw-menu-root {
@apply h-full w-full font-mono text-stone-200 text-[16px] flex flex-row bg-transparent;
@ -50,9 +58,13 @@
}
}
* {
/* * {
@apply font-mono text-stone-200 text-[1em];
}
} */
}
.uw-hidden {
display: none !important;
}
.uw-trigger, .uw-menu-item, .uw-menu-trigger {
@ -101,14 +113,13 @@
}
* {
@apply font-mono text-stone-200 text-[1em];
@apply text-stone-200;
}
}
.uw-menu-item, .uw-menu-trigger {
&:hover > .uw-submenu,
&:focus-within > .uw-submenu {
&:hover > .uw-submenu {
display: flex;
z-index: 200;
@ -127,4 +138,121 @@
&:hover {
@apply text-primary-200 bg-stone-800/75;
}
&:hover > * {
color: inherit;
}
}
.uw-site-info {
@apply text-[12px] py-3;
}
.uw-menu-support-level {
@apply inline-flex flex-row items-center !gap-4 rounded-[0.5rem] relative;
svg {
@apply fill-current stroke-current;
height: 1em;
}
* {
@apply text-inherit;
}
}
.uw-menu-warning {
@apply flex flex-row gap-4 items-center
bg-amber-950 text-amber-300
border border-amber-300;
border: 1px solid;
svg {
@apply fill-current stroke-current;
height: 1.5em;
}
* {
@apply text-primary-500;
}
}
.uw-freeform-zoom-container {
border-style: solid !important;
* {
border-style: solid !important;
}
@apply flex flex-row items-center justify-center w-[420px];
svg {
@apply fill-current stroke-current text-inherit;
height: 1.5em;
}
.uw-freeform-zoom-sliders {
@apply grow flex flex-col gap-[0.125em];
.uw-slider {
@apply flex flex-row w-full items-center;
.uw-slider-label {
@apply w-[4em] opacity-75 text-right leading-none
}
.uw-slider-zoom {
@apply w-[4.5em] pr-4 text-right leading-none;
}
.uw-slider-input {
@apply grow leading-none pt-[0.25em];
}
}
}
.uw-slider-lock-bar-container {
@apply h-full flex flex-col justify-center ml-2 text-stone-500;
svg {
@apply h-[36px] w-[14px];
}
&.uw-linked {
svg {
.uw-linked {
opacity: 1;
}
.uw-unlinked {
opacity: 0;
}
}
}
&.uw-unlinked {
svg {
.uw-linked {
opacity: 0;
}
.uw-unlinked {
opacity: 1;
}
}
}
}
.uw-freeform-zoom-button-container {
@apply h-full flex flex-col justify-center p-2;
.uw-freeform-zoom-button {
@apply cursor-pointer border border-1 border-stone-800 p-2 hover:bg-stone-700 hover:text-white;
}
}
}
.uw-link-button.uw-linked .uw-unlinked {
display: none;
}
.uw-link-button.uw-unlinked .uw-linked {
display: none;
}

View File

@ -20,7 +20,6 @@
"esModuleInterop": true,
"paths": {
"@src/*": [ "*" ],
"@csui/*": [ "csui/*" ],
"@/*": [ "*" ]
}
},

View File

@ -132,7 +132,6 @@ const config = {
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' },
// we need to get webextension-polyfill and put it in common/lib