Compare commits

..

40 Commits

Author SHA1 Message Date
78395e3ea0 Minor fixes for comms and mutation detectìon 2026-06-06 03:29:46 +02:00
e269ff3d79 Don't trip on subtitles that don't fall outside current crop area
Todo: don't trip on subtitles that wouldn't get cropped, but that would require Aard to know player's aspect ratio, and it currently doesnt
2026-06-06 03:27:49 +02:00
be495b1c56 daon't error if context isnt defined 2026-04-03 16:44:41 +02:00
3f032619b4 Fix firefox errors in cross-iframe communiccation 2026-04-03 16:44:12 +02:00
ef5ba706b7 Changelog update 2026-01-29 00:00:11 +01:00
3b91c2224c Allow zoom modes to be set as default crop 2026-01-28 23:54:26 +01:00
822d069b82 Finish syncing UI with extension state
Add active mode indicators to popup. Add handling of locked zoom & alignment indicator to both popup and UI. Improve message passing through Comms.
2026-01-28 21:49:51 +01:00
8af9f2cedf Indicate currently selected option in the in-player popup menu 2026-01-28 18:11:57 +01:00
3d8d8eb199 broadcast scaling parameters on ar change 2026-01-28 02:44:38 +01:00
a0d599ce2f add amazon to extconfpatches 2026-01-28 02:05:11 +01:00
fd7b5ec04e Fix messaging 2026-01-28 02:02:27 +01:00
c9818c92b3 Update readme 2026-01-27 20:37:32 +01:00
f57163f2bb Player selection should be functional enough 2026-01-27 20:36:50 +01:00
dbdcb4e367 add option to always activate menu while holding CTRL 2026-01-25 23:33:37 +01:00
e1dd0f9c04 Open in-player settings window, get player selection to somewhat work 2026-01-25 21:47:31 +01:00
2cb6a88c5e fix UI settings a bit 2026-01-25 21:41:01 +01:00
eb89ba03db Fix config 2026-01-25 20:54:25 +01:00
cb02c8619d Ensure each command gets executed once, even if it gets duplicated on the way 2026-01-25 20:52:25 +01:00
7d296e6a3d Get in-page player picker to at least show up 2026-01-23 22:46:59 +01:00
35bb5b477b Finalize menu position settings, replace eventBus.sendToTunnel() with eventBus.send() 2026-01-15 02:33:21 +01:00
925a9fdea3 Other minor settings, add CSS we forgot about 2026-01-14 22:41:46 +01:00
f0baa6ad67 Enable distance-based UI showing. Add "show while holding CTRL" option to the in-player UI 2026-01-14 22:39:42 +01:00
f25749470b be a bit more dilligent about including visitedBusses 2026-01-14 22:38:08 +01:00
8a4985e6a3 Fix problem where aspect ratio wasn't getting set at all 2026-01-13 18:15:48 +01:00
2cce60fb9f Rework message passing and simplify tunnels 2026-01-12 01:07:45 +01:00
826acf9a3f add css for info and warnings 2026-01-12 01:06:23 +01:00
186de15781 fix manual player detection 2026-01-12 01:06:12 +01:00
17dc69f87a Fix player detection when top-layer is being used 2026-01-12 01:04:05 +01:00
8268c7a7af Housekeeping/convert some more imports to absolute paths 2026-01-07 18:04:34 +01:00
a71b99ff10 Mostly import fixes 2026-01-07 09:44:44 +01:00
54c4f49edb Sort out types 2026-01-07 01:42:37 +01:00
92cd1cf58b Fix enum parsing in site settings panel 2026-01-06 16:57:47 +01:00
64570c71ce Fix some default keyboard shortcuts + change some other behaviour about kbd 2026-01-06 01:10:14 +01:00
cbc4b7877f Add cross-iframe messaging (for UI windows) 2026-01-04 01:25:35 +01:00
b62f6feaea Open in-page settings inside floating windows 2026-01-04 00:54:59 +01:00
b6dec385dc Fix bug with zoom not working if AR hasn't been set yet 2026-01-03 23:30:45 +01:00
a12581ed0e Setup interactions for the custom elements of the in-player menu 2026-01-03 19:30:46 +01:00
e651e51c50 Add zoom sliders to in-player popup (non-functional) 2026-01-03 03:19:03 +01:00
56321e52fc Fix menu alignment, fix issue where overlay div sometimes incorrectly inherited bg color, allow custom HTML to be passed as string 2026-01-02 19:20:17 +01:00
73eec1f7ba Fix menu css 2026-01-02 17:47:42 +01:00
200 changed files with 4583 additions and 34539 deletions

View File

@ -1,6 +1,7 @@
{
"cSpell.words": [
"aard",
"allowtransparency",
"ardetector",
"autodetect",
"autodetection",
@ -8,6 +9,7 @@
"blackbar",
"blackbars",
"blackframe",
"bruteforce",
"canvas",
"clickthrough",
"cmpa",
@ -15,6 +17,7 @@
"com",
"comms",
"csui",
"ctab",
"cycleable",
"decycle",
"dinked",
@ -90,7 +93,8 @@
"wuckies",
"wucky",
"ycenter",
"youtube"
"youtube",
"YYMM"
],
"cSpell.ignoreWords": [
"abcdefghijklmnopqrstuvwxyz",

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "ultrawidify",
"version": "6.3.996",
"version": "6.3.998",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ultrawidify",
"version": "6.3.996",
"version": "6.3.998",
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@mdi/font": "^7.4.47",

View File

@ -1,6 +1,6 @@
{
"name": "ultrawidify",
"version": "6.3.996",
"version": "6.3.998",
"description": "Aspect ratio fixer for youtube and other sites, with automatic aspect ratio detection. Supports ultrawide and other ratios.",
"author": "Tamius Han <tamius.han@gmail.com>",
"scripts": {

View File

@ -2,7 +2,6 @@ const path = require('path')
module.exports = {
'@src': path.resolve(__dirname, 'src'),
'@csui': path.resolve(__dirname, 'src/csui'),
'@ui': path.resolve(__dirname, 'src/ui'),
'@components': path.resolve(__dirname, 'src/ui/components'),
'@res': path.resolve(__dirname, 'src/ui/res'),

View File

@ -9,6 +9,7 @@ enum AspectRatioType {
Fixed = 4, // pre-determined aspect ratio
Manual = 5, // ratio achieved by zooming in/zooming out
AutomaticUpdate = 6, // set by Aard
Cover = 7, // replaces FitWidth and FitHeight
}
export default AspectRatioType;

View File

@ -0,0 +1,5 @@
export enum InputHandlingMode {
Disabled = -1,
Default = 0,
Enabled = 1, // enable, but don't override site shortcuts
}

View File

@ -4,7 +4,8 @@ enum VideoAlignmentType {
Right = 2,
Top = 3,
Bottom = 4,
Default = -1
Default = -1,
Custom = -2,
};
export default VideoAlignmentType;

View File

@ -1,10 +1,17 @@
export interface MenuItemConfig {
label: string;
import { CommandInterface, InPlayerUIOptions } from '@src/common/interfaces/SettingsInterface';
export interface MenuItemBaseConfig {
label?: string;
subitems?: MenuItemConfig[];
command?: CommandInterface,
action?: () => void;
customHTML?: HTMLElement;
customHTML?: HTMLElement | string;
customId?: string;
customClassList?: string;
}
export type MenuItemConfig = MenuItemBaseConfig & ({label: string} | {customHTML: HTMLElement | string});
export enum MenuPosition {
TopLeft = 'top-left',
Left = 'left',
@ -18,7 +25,7 @@ export enum MenuPosition {
export interface MenuConfig {
isGlobal?: boolean;
menuPosition: MenuPosition;
activationRadius?: number;
ui: InPlayerUIOptions;
options?: {forceShow?: boolean};
items: MenuItemConfig[];
}

View File

@ -0,0 +1,43 @@
import { CommsOrigin } from '@/ext/module/comms/CommsClient';
import type { Runtime } from 'chrome';
export interface EventBusCommand {
isGlobal?: boolean,
source?: any,
function: (commandData: any, context?: EventBusContext) => void | Promise<void>
}
export interface EventBusMessage<T = unknown> {
command: string;
config?: T;
context?: EventBusContext;
}
export interface EventBusContext {
stopPropagation?: boolean;
origin?: CommsOrigin;
frameUrl?: string;
// tab?: number;
// frame?: number;
// port?: string;
visitedBusses?: string[];
commandId?: string;
comms?: {
forwardTo?: 'all' | 'active' | 'popup' | 'contentScript' | 'all-frames';
sender?: Runtime.MessageSender;
port?: Runtime.Port;
// frame?: any;
sourceFrame?: {
tabId: number;
frameId: number;
};
};
borderCrossings?: {
commsServer?: boolean,
iframe?: boolean,
};
}

View File

@ -0,0 +1,8 @@
import { ExtensionEnvironment } from '@src/common/interfaces/SettingsInterface';
export interface HostInfo {
host: string,
hasVideo: boolean,
minEnvironment: ExtensionEnvironment,
maxEnvironment: ExtensionEnvironment
};

View File

@ -0,0 +1,6 @@
import { EventBusMessage } from '@src/common/interfaces/EventBusMessage.interface';
export interface IframeTunnelPayload {
action: string,
payload: EventBusMessage,
}

View File

@ -0,0 +1,19 @@
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import { Ar } from '@src/common/interfaces/ArInterface';
import { Stretch } from '@src/common/interfaces/StretchInterface';
export interface ScalingParamsBroadcast {
effectiveZoom: {
x: number,
y: number
},
videoAlignment: {
x: VideoAlignmentType,
y: VideoAlignmentType,
xPos?: number,
yPos?: number,
}
lastAr: Ar,
manualZoom: boolean,
stretch: Stretch,
}

View File

@ -1,16 +1,15 @@
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/module/aard/enums/aard-polling-options.enum'
import { AardSubtitleCropMode } from '@src/ext/module/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';
import { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum';
import { MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
export enum ExtensionEnvironment {
Normal = ExtensionMode.All,
@ -310,6 +309,24 @@ interface DevSettings {
loadFromSnapshot: boolean,
}
export interface InPlayerUIOptions {
activatorAlignment: MenuPosition,
minEnabledWidth: number, // don't show UI if player is narrower than % of screen width
minEnabledHeight: number, // don't show UI if player is narrower than % of screen height
activation: 'player' | 'trigger-zone' | 'distance' | 'none', // what needs to be hovered in order for UI to be visible
activateWithCtrl: boolean,
activationDistance: number,
activationDistanceUnits: '%' | 'px',
activatorPadding: {x: number, y: number}
activatorPaddingUnit: {x: '%' | 'px', y: '%' | 'px'},
triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
width: number
height: number,
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
},
};
interface SettingsInterface {
_updateFlags?: {
requireReload?: SettingsReloadFlags,
@ -321,18 +338,7 @@ interface SettingsInterface {
aard: AardSettings,
ui: {
inPlayer: {
popupAlignment: 'left' | 'right',
minEnabledWidth: number, // don't show UI if player is narrower than % of screen width
minEnabledHeight: number, // don't show UI if player is narrower than % of screen height
activation: 'player' | 'trigger-zone' | 'distance' | 'none', // what needs to be hovered in order for UI to be visible
triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
width: number
height: number,
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
},
},
inPlayer: InPlayerUIOptions,
devMode?: boolean,
dev: DevUiConfig,
}
@ -347,7 +353,7 @@ interface SettingsInterface {
conditionalDifferencePercent: number // black bars less than this wide will trigger stretch
// if mode is set to '1'. 1.0=100%
},
kbm: {
kbm: { // TODO: we dont use this anymore
enabled: boolean, // if keyboard/mouse handler service will run
keyboardEnabled: boolean, // if keyboard shortcuts are processed
mouseEnabled: boolean, // if mouse movement is processed
@ -440,9 +446,11 @@ interface SettingsInterface {
}
export interface SiteSettingsInterface {
notes?: string; // any special things related to this site.
enable: ExtensionMode;
enableAard: ExtensionMode;
enableKeyboard: ExtensionMode;
enableKeyboard: InputHandlingMode;
enableUI: ExtensionMode;
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy; // presumed to be 'Use as default' if not defined

View File

@ -1,17 +0,0 @@
<template>
<div>
<p><i><a href="https://www.youtube.com/watch?v=Mn3YEJTSYs8&feature=youtu.be&t=770" target='_blank'>...</a> but as is normal, you can't use a free extensions without the developers begging for money. It's the bi-daily beggathon here on ... wherever here is.</i>
</p>
<p>
Jokes and references few will get aside, developing this extension does take a decent amount of time, motivation, carefully calibrated quantities
of alcohol and enough coffee to bankrupt a small nation.
</p>
<p>If you want to buy me a beer or bankroll my caffeine addiction, you can do so by <a href="https://paypal.me/tamius">clicking here</a>. All donations are appreciated.
</p>
</div>
</template>
<script>
</script>

View File

@ -1,8 +1,8 @@
import Debug from '../conf/Debug';
import Debug from '../../ext/conf/Debug';
class ObjectCopy {
static addNew(current, newValues){
// clone target
let out = JSON.parse(JSON.stringify(newValues));
@ -20,7 +20,7 @@ class ObjectCopy {
// Types and constructors of objects must match. If they don't, we always use the new value.
if(typeof out[k] === typeof current[k] && out[k].constructor === current[k].constructor) {
// objects are special, we need to check them recursively.
if(out[k] && typeof out[k] === 'object' && out[k].constructor === Object ) {
if(Debug.debug && Debug.settings) {
@ -29,7 +29,7 @@ class ObjectCopy {
out[k] = this.addNew(current[k], out[k]);
} else {
out[k] = current[k];
out[k] = current[k];
}
}
}
@ -52,7 +52,7 @@ class ObjectCopy {
if (current[k] !== undefined) {
// Types and constructors of objects must match. If they don't, we always use the new value.
if (typeof newValues[k] === typeof current[k] && newValues[k].constructor === current[k].constructor) {
// objects are special, we need to check them recursively.
if(current[k] && typeof current[k] === 'object' && current[k].constructor === Object ) {
if(Debug.debug && Debug.settings) {
@ -61,7 +61,7 @@ class ObjectCopy {
current[k] = this.overwrite(current[k], newValues[k]);
} else {
current[k] = newValues[k];
current[k] = newValues[k];
}
} else {
current[k] = newValues[k];
@ -82,4 +82,4 @@ class ObjectCopy {
}
}
export default ObjectCopy;
export default ObjectCopy;

View File

@ -1,7 +1,3 @@
export async function sleep(timeout) {
return new Promise<void>( (resolve, reject) => setTimeout(() => resolve(), timeout));
}
/**
* Creates deep copy of an object
* @param obj

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

@ -1,14 +1,14 @@
import Debug from './conf/Debug';
import Settings from './lib/settings/Settings';
import CommsClient from './lib/comms/CommsClient';
import PageInfo from './lib/video-data/PageInfo';
import EventBus from './lib/EventBus';
import KeyboardHandler from './lib/kbm/KeyboardHandler';
import { SiteSettings } from './lib/settings/SiteSettings';
import UI from './lib/uwui/UI';
import { BLANK_LOGGER_CONFIG, LogAggregator } from './lib/logging/LogAggregator';
import { ComponentLogger } from './lib/logging/ComponentLogger';
import { getIframeParentHost, setupHostnameReporting } from './util/getHost';
import Debug from '@src/ext/conf/Debug';
import CommsClient, { CommsOrigin } from '@src/ext/module/comms/CommsClient';
import EventBus from '@src/ext/module/EventBus';
import KeyboardHandler from '@src/ext/module/kbm/KeyboardHandler';
import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger';
import { BLANK_LOGGER_CONFIG, LogAggregator } from '@src/ext/module/logging/LogAggregator';
import Settings from '@src/ext/module/settings/Settings';
import { SiteSettings } from '@src/ext/module/settings/SiteSettings';
import UI from '@src/ext/module/uwui/UI';
import PageInfo from '@src/ext/module/video-data/PageInfo';
import { getIframeParentHost, setupHostnameReporting } from '@src/common/utils/getHost';
export default class UWContent {
pageInfo: PageInfo;
@ -82,7 +82,7 @@ export default class UWContent {
this.siteSettings = this.settings.getSiteSettings({site: window.location.hostname, isIframe: this.isIframe, parentHostname: this.parentHostname});
}
this.eventBus = new EventBus();
this.eventBus = new EventBus({name: 'content-script', commsOrigin: CommsOrigin.ContentScript});
this.eventBus.subscribe(
'uw-restart',
{
@ -120,9 +120,12 @@ export default class UWContent {
this.logger.debug('setup', "KeyboardHandler initiated.");
this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true});
this.globalUi.enable();
this.globalUi.setUiVisibility(false);
if (this.globalUi) {
this.globalUi.destroy();
}
// this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true});
// this.globalUi.enable();
// this.globalUi.setUiVisibility(false);
} catch (e) {
console.error('Ultrawidify: failed to start extension. Error:', e)

View File

@ -1,11 +1,13 @@
import Debug from './conf/Debug.js';
import BrowserDetect from './conf/BrowserDetect';
import CommsServer from './lib/comms/CommsServer';
import Settings from './lib/settings/Settings';
import { sleep } from '../common/js/utils';
import EventBus, { EventBusCommand } from './lib/EventBus';
import { ComponentLogger } from './lib/logging/ComponentLogger';
import { BLANK_LOGGER_CONFIG, LogAggregator } from './lib/logging/LogAggregator';
import { Runtime } from 'chrome';
import EventBus from '@src/ext/module/EventBus';
import Settings from '@src/ext/module/settings/Settings';
import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger';
import { BLANK_LOGGER_CONFIG, LogAggregator } from '@src/ext/module/logging/LogAggregator';
import CommsServer from '@src/ext/module/comms/CommsServer';
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import { HostInfo } from '@src/common/interfaces/HostData.interface';
import { ExtensionEnvironment } from '@src/common/interfaces/SettingsInterface';
import { CommsOrigin } from '@src/ext/module/comms/CommsClient';
const BASE_LOGGING_STYLES = {
@ -50,7 +52,7 @@ export default class UWServer {
function: (message, context) => this.replaceCss(message.oldCssString, message.newCssString, context.comms.sender)
},
'get-current-site': {
function: (message, context) => this.getCurrentSite()
function: (message, context) => this.getCurrentSite(context.comms.sender)
}
};
@ -80,7 +82,7 @@ export default class UWServer {
this.settings = new Settings({logAggregator: this.logAggregator});
await this.settings.init();
this.eventBus = new EventBus({isUWServer: true});
this.eventBus = new EventBus({isUWServer: true, commsOrigin: CommsOrigin.Server});
this.eventBus.subscribeMulti(this.eventBusCommands, this);
@ -101,9 +103,14 @@ export default class UWServer {
});
}
//#region CSS managemeent
//#region CSS management
async injectCss(css, sender) {
if (!sender?.tab?.id) {
// console.warn('invalid injectCss received!');
return;
}
this.logger.info('injectCss', 'Trying to inject CSS into tab', sender.tab.id, ', frameId:', sender.frameId, 'css:\n', css)
if (!css) {
return;
@ -124,6 +131,11 @@ export default class UWServer {
}
}
async removeCss(css, sender) {
if (!sender?.tab) {
// console.warn('invalid removeCss received!');
return;
}
try {
await chrome.scripting.removeCSS({
target: {
@ -140,6 +152,10 @@ export default class UWServer {
}
}
async replaceCss(oldCss, newCss, sender) {
if (!sender?.tab) {
// console.warn('invalid replaceCss received!');
return;
}
if (oldCss !== newCss) {
this.removeCss(oldCss, sender);
this.injectCss(newCss, sender);
@ -196,12 +212,16 @@ export default class UWServer {
}
registerVideo(sender) {
if (!sender?.tab?.url) {
// console.warn('invalid registerVideo received!');
return;
}
this.logger.info('registerVideo', 'Registering video.\nsender:', sender);
const tabHostname = this.extractHostname(sender.tab.url);
const frameHostname = this.extractHostname(sender.url);
// check for orphaned/outdated values and remove them if neccessary
// check for orphaned/outdated values and remove them if necessary
if (this.videoTabs[sender.tab.id]?.host != tabHostname) {
delete this.videoTabs[sender.tab.id]
} else if(this.videoTabs[sender.tab.id]?.frames[sender.frameId]?.host != frameHostname) {
@ -234,6 +254,11 @@ export default class UWServer {
}
unregisterVideo(sender) {
if (!sender?.tab) {
// console.warn('invalid unregisterVideo received!');
return;
}
this.logger.info('unregisterVideo', 'Unregistering video.\nsender:', sender);
if (this.videoTabs[sender.tab.id]) {
if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) {
@ -252,9 +277,11 @@ export default class UWServer {
this.selectedSubitem[menu] = subitem;
}
async getCurrentSite() {
this.logger.info('getCurrentSite', 'received get-current-site ...');
async getCurrentSite(sender: Runtime.MessageSender) {
// if (!sender?.tab) {
// console.warn('invalid unregisterVideo received!');
// return;
// }
const site = await this.getVideoTab();
// Don't propagate 'INVALID SITE' to the popup.
@ -265,6 +292,7 @@ export default class UWServer {
const tabHostname = await this.getCurrentTabHostname();
this.logger.info('getCurrentSite', 'Returning data:', {site, tabHostname});
console.info('get-current-site : returning data:', {site, tabHostname});
this.eventBus.send(
'set-current-site',
@ -285,6 +313,36 @@ export default class UWServer {
}
private populateFrameVideoStatus(tabId: number, hostnames: string[]) {
const out: HostInfo[] = [];
for (const host of hostnames) {
let video = {hasVideo: false, minEnvironment: ExtensionEnvironment.Normal, maxEnvironment: ExtensionEnvironment.Fullscreen};
for (const frameKey in this.videoTabs[tabId].frames) {
const frame = this.videoTabs[tabId].frames[frameKey];
if (frame.host === host) {
video.hasVideo = true;
if (frame.environment > video.maxEnvironment) {
video.maxEnvironment = frame.environment;
}
if (frame.environment < video.minEnvironment) {
video.minEnvironment = frame.environment;
}
}
}
out.push({
host,
...video,
})
}
return out;
}
private _lastVideoTabData: any | undefined;
async getVideoTab() {
// friendly reminder: if current tab doesn't have a video,
// there won't be anything in this.videoTabs[this.currentTabId]
@ -301,41 +359,52 @@ export default class UWServer {
const hostnames = await this.comms.listUniqueFrameHosts();
if (this.videoTabs[ctab.id]) {
// if video is older than PageInfo's video rescan period (+ 4000ms of grace),
// we clean it up from videoTabs[tabId].frames array.
const ageLimit = Date.now() - this.settings.active.pageInfo.timeouts.rescan - 4000;
try {
for (const key in this.videoTabs[ctab.id].frames) {
if (this.videoTabs[ctab.id].frames[key].registerTime < ageLimit) {
delete this.videoTabs[ctab.id].frames[key];
}
}
} catch (e) {
// something went wrong. There's prolly no frames.
return {
host: this.extractHostname(ctab.url),
frames: [],
selected: this.selectedSubitem
// this probably means we're inside a problematic page
if (!this.videoTabs[ctab.id]) {
return this._lastVideoTabData ?? {
host: 'INVALID SITE',
frames: [],
hostnames: [],
}
}
// if video is older than PageInfo's video rescan period (+ 4000ms of grace),
// we clean it up from videoTabs[tabId].frames array.
const ageLimit = Date.now() - this.settings.active.pageInfo.timeouts.rescan - 4000;
try {
for (const key in this.videoTabs[ctab.id].frames) {
if (this.videoTabs[ctab.id].frames[key].registerTime < ageLimit) {
delete this.videoTabs[ctab.id].frames[key];
}
}
} catch (e) {
// something went wrong. There's prolly no frames.
// this probably shouldn't ever run.
return {
...this.videoTabs[ctab.id],
host: this.extractHostname(ctab.url),
hostnames,
hostnames: [],
frames: [],
selected: this.selectedSubitem
};
}
}
// return something more or less empty if this tab doesn't have
// a video registered for it
return {
// Ensure hostnames with videos come before hostnames without videos
// Also ensure hostnames are sorted alphabetically
const populatedHostnames = this.populateFrameVideoStatus(ctab.id, hostnames);
populatedHostnames.sort((a: HostInfo, b: HostInfo) => {
return a.hasVideo === b.hasVideo
? a.host === b.host ? 0 : a.host < b.host ? -1 : 1
: a.hasVideo < b.hasVideo ? 1 : -1;
});
this._lastVideoTabData = {
host: this.extractHostname(ctab.url),
frames: [],
hostnames,
selected: this.selectedSubitem,
}
hostnames: populatedHostnames.map(x => x.host), // todo: try eliminating this
populatedHostnames: populatedHostnames,
selected: this.selectedSubitem
};
return this._lastVideoTabData;
}
async getCurrentTabHostname() {

View File

@ -1,19 +1,16 @@
// How to use:
// version: {ExtensionConf object, but only properties that get overwritten}
import StretchType from '../../common/enums/StretchType.enum';
// import ExtensionMode from '../../common/enums/ExtensionMode.enum';
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
import BrowserDetect from './BrowserDetect';
import SettingsInterface, { SiteSettingsInterface } from '../../common/interfaces/SettingsInterface';
import { _cp } from '../../common/js/utils';
import CropModePersistence from '../../common/enums/CropModePersistence.enum';
import AspectRatioType from '../../common/enums/AspectRatioType.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 { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum';
import LegacyExtensionMode from '@src/common/enums/LegacyExtensionMode.enum';
import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
import SettingsInterface from '@src/common/interfaces/SettingsInterface';
import { _cp } from '@src/common/utils/_cp';
import { update } from 'lodash';
import EmbeddedContentSettingsOverridePolicy from '../../common/enums/EmbeddedContentSettingsOverridePolicy.enum';
import LegacyExtensionMode from '../../common/enums/LegacyExtensionMode.enum';
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
import { PlayerDetectionMode } from '../../common/enums/PlayerDetectionMode.enum';
import { SiteSupportLevel } from '../../common/enums/SiteSupportLevel.enum';
const ExtensionConfPatch = Object.freeze([
@ -312,7 +309,13 @@ const ExtensionConfPatch = Object.freeze([
logger.log('updateFn', '\n\n ... migrating default enable-state for site', key);
userOptions.sites[key].enable = convertLegacyExtensionMode(userOptions.sites[key].enable as any);
userOptions.sites[key].enableAard = convertLegacyExtensionMode(userOptions.sites[key].enableAard as any);
userOptions.sites[key].enableKeyboard = convertLegacyExtensionMode(userOptions.sites[key].enableKeyboard as any);
if (key === '@global') {
userOptions.sites['@global'].enableKeyboard = userOptions.kbm.enabled && userOptions.kbm.keyboardEnabled ? InputHandlingMode.Enabled : InputHandlingMode.Disabled;
} else {
userOptions.sites[key].enableKeyboard = InputHandlingMode.Default;
}
userOptions.sites[key].enableUI = convertLegacyExtensionMode(
userOptions.sites[key].enableUI ?? (key === '@global' ? ExtensionMode.FullScreen : ExtensionMode.Default) as any
);
@ -460,6 +463,74 @@ const ExtensionConfPatch = Object.freeze([
logger.log('updateFn', 'Migration complete. New site settings:', userOptions.sites);
}
}, {
forVersion: '6.3.997',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
// default zoom key combinations that involved the 'shift' key should have
// shortcut.key in uppercase, but they didn't.
for (const command of userOptions.commands?.zoom) {
if (command.shortcut) {
if (command.shortcut.shiftKey) {
if (command.shortcut.key.toUpperCase() === command.shortcut.code.charAt(3)) {
command.shortcut.key = command.shortcut.key.toUpperCase();
}
}
}
}
}
}, {
forVersion: '6.3.998',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
if (!userOptions.sites["www.amazon.com"]) {
userOptions.sites["www.amazon.com"] = _cp(defaultOptions.sites["www.amazon.com"] );
}
if (userOptions.commands?.zoom) {
const firstFixed = userOptions.commands?.zoom.findIndex(x => x.arguments.type === AspectRatioType.Fixed);
if (firstFixed === -1) {
userOptions.commands.zoom.push({
action: 'set-ar-zoom',
label: 'Cover',
comment: 'Covers the entire screen, cropping as much as needed',
arguments: {
type: AspectRatioType.Cover
},
shortcut: {
key: 'w',
code: 'KeyW',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyUp: true,
onKeyDown: false,
}
});
} else {
userOptions.commands.zoom.splice(firstFixed, 0, {
action: 'set-ar-zoom',
label: 'Cover',
comment: 'Covers the entire screen, cropping as much as needed',
arguments: {
type: AspectRatioType.Cover
},
shortcut: {
key: 'w',
code: 'KeyW',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyUp: true,
onKeyDown: false,
}
});
}
}
}
}
]);

View File

@ -1,19 +1,17 @@
import Debug from './Debug';
import currentBrowser from './BrowserDetect';
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
import StretchType from '../../common/enums/StretchType.enum';
import ExtensionMode from '../../common/enums/ExtensionMode.enum';
import AntiGradientMode from '../../common/enums/AntiGradientMode.enum';
import AspectRatioType from '../../common/enums/AspectRatioType.enum';
import CropModePersistence from '../../common/enums/CropModePersistence.enum';
import SettingsInterface from '../../common/interfaces/SettingsInterface';
import BrowserDetect from './BrowserDetect';
import { Extension } from 'typescript';
import EmbeddedContentSettingsOverridePolicy from '../../common/enums/EmbeddedContentSettingsOverridePolicy.enum';
import { AardPollingOptions } from '../lib/aard/enums/aard-polling-options.enum';
import { AardSubtitleCropMode } from '../lib/aard/enums/aard-subtitle-crop-mode.enum';
import { SiteSupportLevel } from '../../common/enums/SiteSupportLevel.enum';
import { PlayerDetectionMode } from '../../common/enums/PlayerDetectionMode.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 { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum';
import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
import StretchType from '@src/common/enums/StretchType.enum';
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import { MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
import SettingsInterface from '@src/common/interfaces/SettingsInterface';
import Debug from '@src/ext/conf/Debug';
import { AardPollingOptions } from '@src/ext/module/aard/enums/aard-polling-options.enum';
import { AardSubtitleCropMode } from '@src/ext/module/aard/enums/aard-subtitle-crop-mode.enum';
if(Debug.debug)
console.log("Loading: ExtensionConf.js");
@ -267,7 +265,11 @@ const ExtensionConf: SettingsInterface = {
minEnabledWidth: 0.75,
minEnabledHeight: 0.75,
activation: 'player',
popupAlignment: 'left',
activationDistance: 100,
activationDistanceUnits: '%',
activatorAlignment: MenuPosition.Left,
activatorPadding: {x: 16, y: 16},
activatorPaddingUnit: {x: 'px', y: 'px'},
triggerZoneDimensions: {
width: 0.5,
height: 0.5,
@ -293,11 +295,6 @@ const ExtensionConf: SettingsInterface = {
maxLogZoom: 3,
announceDebounce: 200 // we wait this long before announcing new zoom
},
kbm: {
enabled: true,
keyboardEnabled: true,
mouseEnabled: true
},
miscSettings: {
mousePan: {
enabled: false
@ -358,40 +355,6 @@ const ExtensionConf: SettingsInterface = {
onKeyDown: false,
}
}, {
action: 'set-ar',
label: 'Fit width',
comment: 'Make the video fit the entire width of the player, crop top and bottom if necessary',
arguments: {
type: AspectRatioType.FitWidth
},
shortcut: {
key: 'w',
code: 'KeyW',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}
}, {
action: 'set-ar',
label: 'Fit height',
comment: 'Make the video fit the entire height of the player, crop left and right if necessary',
arguments: {
type: AspectRatioType.FitHeight
},
shortcut: {
key: 'e',
code: 'KeyE',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}
},{
action: 'set-ar',
label: 'Cycle',
comment: 'Cycle through crop options',
@ -576,6 +539,23 @@ const ExtensionConf: SettingsInterface = {
onKeyUp: true,
onKeyDown: false,
}
}, {
action: 'set-ar-zoom',
label: 'Cover',
comment: 'Covers the entire screen, cropping as much as needed',
arguments: {
type: AspectRatioType.Cover
},
shortcut: {
key: 'w',
code: 'KeyW',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}
}, {
action: 'set-ar-zoom',
label: 'Cycle',
@ -805,7 +785,7 @@ const ExtensionConf: SettingsInterface = {
// #s — only available for specific site
enable: ExtensionMode.Disabled,
enableAard: ExtensionMode.Theater,
enableKeyboard: ExtensionMode.All,
enableKeyboard: InputHandlingMode.Enabled,
enableUI: ExtensionMode.FullScreen,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.UseAsDefault,
@ -843,7 +823,7 @@ const ExtensionConf: SettingsInterface = {
enable: ExtensionMode.Default,
enableAard: ExtensionMode.Default,
enableKeyboard: ExtensionMode.Default,
enableKeyboard: InputHandlingMode.Enabled,
enableUI: ExtensionMode.Default,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.Default,
overrideWhenEmbedded: EmbeddedContentSettingsOverridePolicy.Default,
@ -880,7 +860,7 @@ const ExtensionConf: SettingsInterface = {
"www.youtube.com": {
enable: ExtensionMode.All,
enableAard: ExtensionMode.All,
enableKeyboard: ExtensionMode.All,
enableKeyboard: InputHandlingMode.Default,
enableUI: ExtensionMode.Default,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.UseAsDefault,
@ -907,7 +887,7 @@ const ExtensionConf: SettingsInterface = {
"www.youtube-nocookie.com": {
enable: ExtensionMode.All,
enableAard: ExtensionMode.All,
enableKeyboard: ExtensionMode.All,
enableKeyboard: InputHandlingMode.Default,
enableUI: ExtensionMode.FullScreen,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.UseAsDefault,
@ -935,7 +915,7 @@ const ExtensionConf: SettingsInterface = {
"www.netflix.com" : {
enable: ExtensionMode.Theater,
enableAard: ExtensionMode.Disabled,
enableKeyboard: ExtensionMode.All,
enableKeyboard: InputHandlingMode.Default,
enableUI: ExtensionMode.All,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.UseAsDefault,
override: false,
@ -960,7 +940,7 @@ const ExtensionConf: SettingsInterface = {
"www.disneyplus.com" : {
enable: ExtensionMode.Theater,
enableAard: ExtensionMode.Theater,
enableKeyboard: ExtensionMode.All,
enableKeyboard: InputHandlingMode.Default,
enableUI: ExtensionMode.FullScreen,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.UseAsDefault,
@ -986,10 +966,33 @@ const ExtensionConf: SettingsInterface = {
}
}
},
"www.amazon.com": {
enable: ExtensionMode.Default,
enableAard: ExtensionMode.Default,
enableKeyboard: InputHandlingMode.Enabled,
enableUI: ExtensionMode.Default,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.Default,
overrideWhenEmbedded: EmbeddedContentSettingsOverridePolicy.Default,
type: SiteSupportLevel.CommunitySupport,
defaultType: SiteSupportLevel.CommunitySupport,
persistCSA: CropModePersistence.Default,
activeDOMConfig: "@community",
DOMConfig: {
"@community": {
type: SiteSupportLevel.CommunitySupport,
elements: {
player: {
detectionMode: PlayerDetectionMode.Auto,
allowAutoFallback: true
}
}
},
},
},
"www.twitch.tv": {
enable: ExtensionMode.All,
enableAard: ExtensionMode.All,
enableKeyboard: ExtensionMode.All,
enableKeyboard: InputHandlingMode.Default,
enableUI: ExtensionMode.FullScreen,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.UseAsDefault,
@ -1013,7 +1016,7 @@ const ExtensionConf: SettingsInterface = {
"old.reddit.com" : {
enable: ExtensionMode.Disabled,
enableAard: ExtensionMode.Disabled,
enableKeyboard: ExtensionMode.Disabled,
enableKeyboard: InputHandlingMode.Disabled,
enableUI: ExtensionMode.Disabled,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.Never,
@ -1023,7 +1026,7 @@ const ExtensionConf: SettingsInterface = {
"www.reddit.com" : {
enable: ExtensionMode.Disabled,
enableAard: ExtensionMode.Disabled,
enableKeyboard: ExtensionMode.Disabled,
enableKeyboard: InputHandlingMode.Disabled,
enableUI: ExtensionMode.Disabled,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.Never,
@ -1033,7 +1036,7 @@ const ExtensionConf: SettingsInterface = {
"imgur.com": {
enable: ExtensionMode.Disabled,
enableAard: ExtensionMode.Disabled,
enableKeyboard: ExtensionMode.Disabled,
enableKeyboard: InputHandlingMode.Disabled,
enableUI: ExtensionMode.Disabled,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.Never,
@ -1043,7 +1046,7 @@ const ExtensionConf: SettingsInterface = {
"www.wakanim.tv": {
enable: ExtensionMode.All,
enableAard: ExtensionMode.All,
enableKeyboard: ExtensionMode.All,
enableKeyboard: InputHandlingMode.Default,
enableUI: ExtensionMode.FullScreen,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.UseAsDefault,
@ -1066,7 +1069,7 @@ const ExtensionConf: SettingsInterface = {
"app.plex.tv": {
enable: ExtensionMode.Theater,
enableAard: ExtensionMode.Theater,
enableKeyboard: ExtensionMode.Theater,
enableKeyboard: InputHandlingMode.Default,
enableUI: ExtensionMode.FullScreen,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.UseAsDefault,
@ -1076,7 +1079,7 @@ const ExtensionConf: SettingsInterface = {
"metaivi.com": {
enable: ExtensionMode.Theater,
enableAard: ExtensionMode.Theater,
enableKeyboard: ExtensionMode.Theater,
enableKeyboard: InputHandlingMode.Default,
enableUI: ExtensionMode.FullScreen,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.UseAsDefault,
type: SiteSupportLevel.CommunitySupport,
@ -1085,7 +1088,7 @@ const ExtensionConf: SettingsInterface = {
"piped.kavin.rocks": {
enable: ExtensionMode.Theater,
enableAard: ExtensionMode.Theater,
enableKeyboard: ExtensionMode.Theater,
enableKeyboard: InputHandlingMode.Default,
enableUI: ExtensionMode.FullScreen,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.UseAsDefault,

View File

@ -1,200 +0,0 @@
import { IframeData } from './video-data/IframeManager';
import CommsClient, { CommsOrigin } from './comms/CommsClient';
import CommsServer from './comms/CommsServer';
export interface EventBusCommand {
isGlobal?: boolean,
source?: any,
function: (commandData: any, context?: any) => void | Promise<void>
}
export interface EventBusContext {
stopPropagation?: boolean,
// Context stuff added by Comms
origin?: CommsOrigin,
comms?: {
sender?: any,
port?: any,
frame?: any,
sourceFrame?: IframeData
forwardTo?: 'all' | 'active' | 'contentScript' | 'server' | 'sameOrigin' | 'popup' | 'all-frames',
};
borderCrossings?: {
commsServer?: boolean,
iframe?: boolean,
}
}
export default class EventBus {
private commands: { [x: string]: EventBusCommand[]} = {};
private comms?: CommsClient | CommsServer;
private disableTunnel: boolean = false;
private popupContext: any = {};
private iframeForwardingList: {iframe: any, fn: (action, payload, context?) => void}[] = [];
// private uiUri = window.location.href;
constructor(options?: {isUWServer?: boolean}) {
if (!options?.isUWServer) {
this.setupIframeTunnelling();
}
}
setupPopupTunnelWorkaround(context: EventBusContext): void {
this.disableTunnel = true;
this.popupContext = context;
}
//#region lifecycle
destroy() {
this.commands = null;
this.destroyIframeTunnelling();
}
//#endregion
setComms(comms: CommsClient | CommsServer) {
this.comms = comms;
}
subscribe(commandString: string, command: EventBusCommand) {
if (!this.commands[commandString]) {
this.commands[commandString] = [command];
} else {
this.commands[commandString].push(command);
}
}
subscribeMulti(commands: {[commandString: string]: EventBusCommand}, source?: any) {
for (const key in commands) {
this.subscribe(
key,
{
...commands[key],
source: source ?? commands[key].source
}
);
}
}
/**
* Removes all commands from a given source
* @param source
*/
unsubscribeAll(source: any) {
for (const commandString in this.commands) {
this.commands[commandString] = this.commands[commandString].filter(x => x.source !== source);
}
}
forwardToIframe(iframe: any, fn: (action: string, payload: any, context?: EventBusContext) => void) {
this.cancelIframeForwarding(iframe);
this.iframeForwardingList.push({iframe, fn});
}
cancelIframeForwarding(iframe: any) {
const existingForwarding = this.iframeForwardingList.findIndex((x: any) => x.iframe === iframe);
if (existingForwarding !== -1) {
this.iframeForwardingList.splice(existingForwarding, 1);
}
}
send(command: string, commandData: any, context?: EventBusContext) {
// execute commands we have subscriptions for
if (this.commands?.[command]) {
for (const eventBusCommand of this.commands[command]) {
eventBusCommand.function(commandData, context);
}
}
// preventing messages from flowing back to their original senders is
// CommsServer's job. EventBus does not have enough data for this decision.
// We do, however, have enough data to prevent backflow of messages that
// crossed CommsServer once already.
if (
this.comms
&& context?.origin !== CommsOrigin.Server
&& !context?.borderCrossings?.commsServer
) {
try {
this.comms.sendMessage({command, config: commandData, context}, context);
} catch (e) {
if (command !== 'reload-required') {
// We shouldn't let reload-required command to trigger new reload-required commands.
this.send('reload-required', {});
}
}
};
// call forwarding functions if they exist
if (!context?.borderCrossings?.iframe) {
for (const forwarding of this.iframeForwardingList) {
forwarding.fn(
command,
commandData,
{
...context,
borderCrossings: {
...context?.borderCrossings,
iframe: true
}
}
);
}
}
if (context?.stopPropagation) {
return;
}
}
//#endregion
/**
* Send, but intended for sending commands from iframe to content scripts
* @param command
* @param config
*/
sendToTunnel(command: string, config: any) {
if (!this.disableTunnel) {
window.parent.postMessage(
{
action: 'uw-bus-tunnel',
payload: {action: command, config}
},
'*'
);
} else {
// because iframe UI components get reused in the popup, we
// also need to set up a detour because the tunnel is closed
// in the popup
if (this.comms) {
try {
this.comms.sendMessage({command, config, context: this.popupContext}, this.popupContext);
} catch (e) {
if (command !== 'reload-required') {
this.send('reload-required', {});
}
}
}
}
}
//#region iframe tunnelling
private setupIframeTunnelling() {
// forward messages coming from iframe tunnels
window.addEventListener('message', this.handleIframeMessage);
}
private destroyIframeTunnelling() {
window.removeEventListener('message', this.handleIframeMessage);
}
private handleIframeMessage(event: any) {
// console.log('GOT IFRAME MESSAGE!', event)
}
//#endregion
}

View File

@ -1,12 +0,0 @@
class Interface {
constructor(videoData) {
this.conf = videoData;
this.player = videoData.player;
}
injectUi() {
this.detectorDiv = document.createElement('div');
this.uiRoot = document.createElement('div');
this.detectorDiv.appendChild(this.uiRoot);
}
}

View File

@ -1,350 +0,0 @@
import { MenuPosition, MenuConfig, MenuItemConfig } from '@src/common/interfaces/ClientUiMenu.interface';
import extensionCss from '@src/main.css?inline';
export class ClientMenu {
private host!: HTMLDivElement;
private shadow!: ShadowRoot;
private root!: HTMLDivElement;
private visible = false;
private menuPositionClasses: string[] = [];
private isHovered = false;
private isWithinActivation = false;
private lastMouseMove = performance.now();
private idleTimer?: number;
private onDocumentMouseMove?: (e: MouseEvent) => void;
private onDocumentMouseLeave?: () => void;
private idleIntervalId?: number;
constructor(private config: MenuConfig) {}
mount(anchorElement: HTMLElement) {
this.buildMenuPositionClassList();
this.createHost();
this.createShadow();
this.createMenu();
this.position(anchorElement);
this.bindGlobalMouse(anchorElement);
// document.documentElement.appendChild(this.host);
anchorElement.appendChild(this.host);
}
public destroy() {
// 1. Stop timers
if (this.idleIntervalId != null) {
clearInterval(this.idleIntervalId);
this.idleIntervalId = undefined;
}
// 2. Remove global listeners
if (this.onDocumentMouseMove) {
document.removeEventListener('mousemove', this.onDocumentMouseMove);
this.onDocumentMouseMove = undefined;
}
if (this.onDocumentMouseLeave) {
document.removeEventListener('mouseleave', this.onDocumentMouseLeave);
this.onDocumentMouseLeave = undefined;
}
// 3. Remove DOM
if (this.host && this.host.parentNode) {
this.host.parentNode.removeChild(this.host);
}
// 4. Clear references
this.shadow = undefined as any;
this.root = undefined as any;
this.host = undefined as any;
// 5. Reset state
this.visible = false;
this.isHovered = false;
this.isWithinActivation = false;
}
private getActivationRadius(anchorEl: HTMLElement): number | null {
if (this.config.activationRadius == null) return null;
if (typeof this.config.activationRadius === 'number') {
return this.config.activationRadius;
}
// percentage string
const rect = anchorEl.getBoundingClientRect();
const pct = parseFloat(this.config.activationRadius);
return Math.max(rect.width, rect.height) * (pct / 100);
}
private injectStyles() {
const style = document.createElement('style');
let css = extensionCss
.trim()
.replace(/'html, body \{/g, ':host {')
.replace(/'body \{/g, ':host {')
;
// this is bad but I can't be bothered to do it the proper way
const cssArr: string[] = css.split('@font-face');
const cssRemainder = cssArr[cssArr.length - 1].split('}').slice(1).join('}');
css = `
${cssArr[0]}
@font-face {
font-family: 'Heebo';
src: url(__FONT_HEEBO__) format('truetype-variations');
font-weight: 100 900;
font-stretch: 75% 125%;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Source Code Pro';
src: url(__FONT_SCP__) format('truetype-variations');
font-weight: 200 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Source Code Pro';
src: url(__FONT_SCPI__) format('truetype-variations');
font-weight: 200 900;
font-style: italic;
font-display: swap;
}
${cssRemainder};
`;
css = css
.replace('__FONT_HEEBO__', chrome.runtime.getURL('/ui/res/fonts/Heebo.ttf'))
.replace('__FONT_SCP__', chrome.runtime.getURL('/ui/res/fonts/SourceCodePro.ttf'))
.replace('__FONT_SCPI__', chrome.runtime.getURL('/ui/res/fonts/SourceCodePro-Italic.ttf'))
.replaceAll('html,', '')
;
style.textContent = css;
this.shadow.appendChild(style);
}
private createHost() {
this.host = document.createElement('div');
this.host.classList.add('uw-ultrawidify-container-root');
Object.assign(this.host.style, {
position: this.config.isGlobal ? 'fixed' : 'absolute',
left: 0,
top: 0,
border: 0,
width: '100%',
height: '100%',
zIndex: this.config.isGlobal ? '2147483647' : '2147483640',
pointerEvents: 'none',
});
console.log('UI host created:', this.host);
}
private createShadow() {
this.shadow = this.host.attachShadow({ mode: 'open' });
this.injectStyles();
}
/**
*
* @returns
*/
private buildMenuPositionClassList() {
let classList;
switch (this.config.menuPosition) {
case MenuPosition.TopLeft:
classList = ['uw-menu-left','uw-menu-top'];
break;
case MenuPosition.Left:
classList = ['uw-menu-left','uw-menu-ycenter'];
break;
case MenuPosition.BottomLeft:
classList = ['uw-menu-left','uw-menu-bottom'];
break;
case MenuPosition.Top:
classList = ['uw-menu-center','uw-menu-top'];
break;
case MenuPosition.Bottom:
classList = ['uw-menu-center', 'uw-menu-bottom'];
break;
case MenuPosition.TopRight:
classList = ['uw-menu-right', 'uw-menu-top'];
break;
case MenuPosition.Right:
classList = ['uw-menu-right', 'uw-menu-ycenter'];
break;
case MenuPosition.BottomRight:
classList = ['uw-menu-right', 'uw-menu-bottom'];
break;
default: // left-center is our default position
classList = ['uw-menu-left', 'uw-menu-ycenter'];
break;
}
this.menuPositionClasses = classList;
}
private createMenu() {
this.root = document.createElement('div');
this.root.className = 'uw-menu-root uw-hidden';
const trigger = document.createElement('div');
trigger.classList = 'uw-menu-trigger uw-trigger';
trigger.textContent = 'Ultrawidify';
const submenu = this.buildSubmenu(this.config.items);
trigger.addEventListener('mouseenter', () => this.show());
this.root.addEventListener('mouseleave', () => {
this.isHovered = false;
this.hide();
});
this.root.classList.add(...this.menuPositionClasses);
trigger.appendChild(submenu);
this.root.append(trigger);
this.shadow.appendChild(this.root);
this.root.addEventListener('mouseenter', () => {
this.isHovered = true;
this.show();
});
this.root.addEventListener('mouseleave', () => {
this.isHovered = false;
this.updateVisibility();
});
}
private buildSubmenu(items: MenuItemConfig[]): HTMLDivElement {
const menu = document.createElement('div');
menu.classList = 'uw-submenu';
menu.classList.add(...this.menuPositionClasses);
for (const item of items) {
const el = document.createElement('div');
el.className = `uw-menu-item uw-trigger`;
if (item.customHTML) {
el.appendChild(item.customHTML);
} else {
el.textContent = item.label;
}
if (item.action) {
el.addEventListener('click', e => {
e.stopPropagation();
item.action?.();
// this.hide(); // maybe dont
});
}
if (item.subitems) {
el.appendChild(this.buildSubmenu(item.subitems));
}
menu.appendChild(el);
}
return menu;
}
private position(anchorEl: HTMLElement) {
let appendClassList: string[] = [];
anchorEl.classList.add(...appendClassList);
}
private bindGlobalMouse(anchorEl: HTMLElement) {
const rect = anchorEl.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const activationRadius = this.getActivationRadius(anchorEl);
this.onDocumentMouseMove = (e: MouseEvent) => {
this.lastMouseMove = performance.now();
if (activationRadius != null) {
const d = Math.hypot(e.clientX - cx, e.clientY - cy);
this.isWithinActivation = d <= activationRadius;
} else {
this.isWithinActivation =
e.clientX >= rect.left &&
e.clientX <= rect.right &&
e.clientY >= rect.top &&
e.clientY <= rect.bottom;
}
this.updateVisibility();
};
this.onDocumentMouseLeave = () => {
this.isHovered = false;
this.isWithinActivation = false;
this.hide();
};
document.addEventListener('mousemove', this.onDocumentMouseMove);
document.addEventListener('mouseleave', this.onDocumentMouseLeave);
this.startIdleWatcher();
}
private startIdleWatcher() {
this.idleIntervalId = window.setInterval(() => {
const idle = performance.now() - this.lastMouseMove > 1000;
if (idle) {
this.hide();
}
}, 200);
}
private updateVisibility() {
const idle = performance.now() - this.lastMouseMove > 1000;
if (this.isHovered) {
this.show();
return;
}
if (this.isWithinActivation && !idle) {
this.show();
return;
}
if (this.visible) {
this.hide();
}
}
private show() {
this.lastMouseMove = performance.now();
if (!this.visible) {
this.visible = true;
this.root.classList.add('uw-visible');
this.root.classList.remove('uw-hidden');
}
}
private hide() {
if (this.visible) {
this.visible = false;
this.root.classList.remove('uw-visible');
this.root.classList.add('uw-hidden');
}
}
}

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);
}
}

Some files were not shown because too many files have changed in this diff Show More