Move extension popup into an iframe, get it to display at least something
This commit is contained in:
parent
761f2c21a8
commit
5a2d8d22cb
@ -8,7 +8,8 @@
|
||||
<div class="site-support-info">
|
||||
<div class="site-support-site">{{site}}</div>
|
||||
<div v-if="siteSupportLevel === 'official'" class="site-support official">
|
||||
<mdicon name="check-decagram" />
|
||||
<!-- <mdicon name="check-decagram" /> -->
|
||||
<span class="mdi account-edit mdi-account-edit"></span>
|
||||
<div>Official</div>
|
||||
<div class="tooltip">The extension is being tested and should work on this site.</div>
|
||||
</div>
|
||||
@ -93,7 +94,6 @@
|
||||
<template v-if="settingsInitialized">
|
||||
<VideoSettings
|
||||
:settings="settings"
|
||||
:eventBus="ultrawidify.eventBus"
|
||||
></VideoSettings>
|
||||
<!-- <ResizerDebugPanel :debugData="debugData">
|
||||
</ResizerDebugPanel> -->
|
||||
@ -105,12 +105,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VideoSettings from './PlayerUiPanels/VideoSettings.vue'
|
||||
import VideoSettings from './src/PlayerUiPanels/VideoSettings.vue'
|
||||
import { mapState } from 'vuex';
|
||||
// import Icon from '../common/components/Icon';
|
||||
import ResizerDebugPanel from './PlayerUiPanels/ResizerDebugPanelComponent';
|
||||
import ResizerDebugPanel from './src/PlayerUiPanels/ResizerDebugPanelComponent';
|
||||
import BrowserDetect from '../ext/conf/BrowserDetect';
|
||||
import ExecAction from './ui-libs/ExecAction';
|
||||
import ExecAction from './src/ui-libs/ExecAction';
|
||||
import Logger from '../ext/lib/Logger';
|
||||
import Settings from '../ext/lib/Settings';
|
||||
|
||||
@ -139,11 +139,12 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'showUi',
|
||||
'resizerDebugData',
|
||||
'playerDebugData'
|
||||
]),
|
||||
// we don't have vuex here at the moment, so no mapState yet!
|
||||
// ...mapState([
|
||||
// 'showUi',
|
||||
// 'resizerDebugData',
|
||||
// 'playerDebugData'
|
||||
// ]),
|
||||
// LPT: NO ARROW FUNCTIONS IN COMPUTED,
|
||||
// IS SUPER HARAM
|
||||
// THINGS WILL NOT WORK IF YOU USE ARROWS
|
||||
@ -185,15 +186,6 @@ export default {
|
||||
this.settingsInitialized = true;
|
||||
|
||||
console.log("settings inited")
|
||||
|
||||
this.execAction.setSettings(this.settings);
|
||||
|
||||
console.log("created!");
|
||||
console.log("store:", this.$store, this);
|
||||
|
||||
console.log("settings:", this.settings)
|
||||
console.log("windowPD", window.ultrawidify);
|
||||
console.log("this:", this);
|
||||
} catch (e) {
|
||||
console.error('Failed to initiate ultrawidify player ui.', e);
|
||||
}
|
||||
@ -208,8 +200,10 @@ export default {
|
||||
|
||||
<style lang="scss" src="../res/css/uwui-base.scss" scoped module></style>
|
||||
<style lang="scss" src="../res/css/flex.scss" scoped module></style>
|
||||
<style lang="scss" src="./res-common/common.scss" scoped module></style>
|
||||
<style lang="scss" src="../res/css/mdi.scss" scoped module></style>
|
||||
<style lang="scss" src="./src/res-common/common.scss" scoped module></style>
|
||||
<style lang="scss" scoped module>
|
||||
|
||||
@import '../res/css/uwui-base.scss';
|
||||
@import '../res/css/colors.scss';
|
||||
@import '../res/css/font/overpass.css';
|
16
src/csui/csui.html
Normal file
16
src/csui/csui.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Ultravidify - Content Script User Interface</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 stye="color: #fff; background: #000;">it works!</div>
|
||||
<div id="app"></div>
|
||||
<script src="csui.js"></script>
|
||||
</body>
|
||||
</html>
|
4
src/csui/csui.js
Normal file
4
src/csui/csui.js
Normal file
@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue';
|
||||
import PlayerUiBase from './PlayerUiBase';
|
||||
|
||||
createApp(PlayerUiBase).mount('#app');
|
@ -64,7 +64,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum';
|
||||
import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
|
||||
|
||||
|
||||
export default {
|
||||
@ -79,7 +79,7 @@ export default {
|
||||
methods: {
|
||||
align(alignmentX, alignmentY) {
|
||||
console.warn('sending set alignment:', {x: alignmentX, y: alignmentY});
|
||||
this.eventBus.send('set-alignment', {x: alignmentX, y: alignmentY})
|
||||
// this.eventBus.send('set-alignment', {x: alignmentX, y: alignmentY})
|
||||
}
|
||||
}
|
||||
}
|
3
src/csui/src/PlayerUiPanels/PlayerDetectionPanel.vue
Normal file
3
src/csui/src/PlayerUiPanels/PlayerDetectionPanel.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
|
||||
</template>
|
@ -121,11 +121,11 @@ export default {
|
||||
</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 '../../../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';
|
||||
|
||||
// increase specificy with this one trick (webdevs hate him!)
|
||||
.uw-ultrawidify-container-root {
|
@ -218,15 +218,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Button from '../../common/components/Button.vue'
|
||||
import KeyboardShortcutParser from '../../common/js/KeyboardShortcutParser';
|
||||
import ShortcutButton from '../../common/components/ShortcutButton';
|
||||
import ComputeActionsMixin from '../../common/mixins/ComputeActionsMixin';
|
||||
import Button from '../../../common/components/Button.vue'
|
||||
import KeyboardShortcutParser from '../../../common/js/KeyboardShortcutParser';
|
||||
import ShortcutButton from '../../../common/components/ShortcutButton';
|
||||
import ComputeActionsMixin from '../../../common/mixins/ComputeActionsMixin';
|
||||
import ExecAction from '../ui-libs/ExecAction';
|
||||
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 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';
|
||||
|
||||
export default {
|
||||
@ -255,14 +255,15 @@ export default {
|
||||
],
|
||||
created() {
|
||||
this.exec = new ExecAction(this.settings, window.location.hostname);
|
||||
this.eventBus.subscribe('announce-zoom', {
|
||||
function: (config) => {
|
||||
this.zoom = {
|
||||
x: Math.log2(config.x),
|
||||
y: Math.log2(config.y)
|
||||
};
|
||||
}
|
||||
});
|
||||
// todo: replace event bus with postMessage
|
||||
// this.eventBus.subscribe('announce-zoom', {
|
||||
// function: (config) => {
|
||||
// this.zoom = {
|
||||
// x: Math.log2(config.x),
|
||||
// y: Math.log2(config.y)
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
// this.eventBus.send('get-current-config');
|
||||
},
|
||||
components: {
|
||||
@ -279,7 +280,7 @@ export default {
|
||||
);
|
||||
},
|
||||
siteDefaultCrop() {
|
||||
console.log('default crop for site:', JSON.parse(JSON.stringify(this.settings)), this.settings?.active.sites[window.location.hostname], this.settings?.active.sites[window.location.hostname].defaultCrop)
|
||||
// console.log('default crop for site:', JSON.parse(JSON.stringify(this.settings)), this.settings?.active.sites[window.location.hostname], this.settings?.active.sites[window.location.hostname].defaultCrop)
|
||||
return JSON.stringify(
|
||||
this.settings?.getDefaultCrop() ?? {type: AspectRatioType.Automatic}
|
||||
);
|
||||
@ -308,7 +309,7 @@ export default {
|
||||
BrowserDetect.runtime.openOptionsPage();
|
||||
},
|
||||
execAction(command) {
|
||||
this.eventBus?.send(command.action, command.arguments);
|
||||
// this.eventBus?.send(command.action, command.arguments);
|
||||
},
|
||||
parseShortcut(command) {
|
||||
if (! command.shortcut) {
|
||||
@ -326,8 +327,9 @@ export default {
|
||||
this.zoom = {x: 0, y: 0};
|
||||
|
||||
// we do not use logarithmic zoom elsewhere
|
||||
this.eventBus.send('set-zoom', {zoom: 1, axis: 'y'});
|
||||
this.eventBus.send('set-zoom', {zoom: 1, axis: 'x'});
|
||||
// todo: replace eventBus with postMessage to parent
|
||||
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'y'});
|
||||
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'x'});
|
||||
},
|
||||
changeZoom(newZoom, axis) {
|
||||
// we store zoom logarithmically on this compnent
|
||||
@ -339,7 +341,6 @@ export default {
|
||||
|
||||
// we do not use logarithmic zoom elsewhere, therefore we need to convert
|
||||
newZoom = Math.pow(2, newZoom);
|
||||
this.eventBus.send('set-zoom', {zoom: newZoom, axis: axis, noAnnounce: true});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -348,25 +349,26 @@ export default {
|
||||
setDefaultCrop($event, globalOrSite) {
|
||||
const commandArguments = JSON.parse($event.target.value);
|
||||
|
||||
if (globalOrSite === 'site') {
|
||||
if (!this.settings.active.sites[window.location.hostname]) {
|
||||
this.settings.active.sites[window.location.hostname] = this.settings.getDefaultSiteConfiguration();
|
||||
}
|
||||
this.settings.active.sites[window.location.hostname].defaultCrop = commandArguments;
|
||||
} else {
|
||||
// eventually, this 'if' will be safe to remove (and we'll be able to only
|
||||
// get away with the 'else' section) Maybe in 6 months or so.
|
||||
if (!this.settings.active.crop) {
|
||||
console.log('active settings crop not present. Well add');
|
||||
this.settings.active['crop'] = {
|
||||
default: commandArguments
|
||||
}
|
||||
} else {
|
||||
console.log('default crop settings are present:', JSON.parse(JSON.stringify(this.settings.active.crop)))
|
||||
this.settings.active.crop.default = commandArguments;
|
||||
}
|
||||
}
|
||||
this.settings.saveWithoutReload();
|
||||
// todo: account for the fact that window.host doesnt work the way we want in an iframe
|
||||
// if (globalOrSite === 'site') {
|
||||
// if (!this.settings.active.sites[window.location.hostname]) {
|
||||
// this.settings.active.sites[window.location.hostname] = this.settings.getDefaultSiteConfiguration();
|
||||
// }
|
||||
// this.settings.active.sites[window.location.hostname].defaultCrop = commandArguments;
|
||||
// } else {
|
||||
// // eventually, this 'if' will be safe to remove (and we'll be able to only
|
||||
// // get away with the 'else' section) Maybe in 6 months or so.
|
||||
// if (!this.settings.active.crop) {
|
||||
// console.log('active settings crop not present. Well add');
|
||||
// this.settings.active['crop'] = {
|
||||
// default: commandArguments
|
||||
// }
|
||||
// } else {
|
||||
// console.log('default crop settings are present:', JSON.parse(JSON.stringify(this.settings.active.crop)))
|
||||
// this.settings.active.crop.default = commandArguments;
|
||||
// }
|
||||
// }
|
||||
// this.settings.saveWithoutReload();
|
||||
},
|
||||
|
||||
/**
|
||||
@ -399,7 +401,7 @@ export default {
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss" src="../../res/css/flex.scss" scoped module></style>
|
||||
<style lang="scss" src="../../../res/css/flex.scss" scoped module></style>
|
||||
<style lang="scss" src="../res-common/panels.scss" scoped module></style>
|
||||
<style lang="scss" src="../res-common/common.scss" scoped module></style>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import Comms from '../../ext/lib/comms/Comms';
|
||||
import Comms from '../../../ext/lib/comms/Comms';
|
||||
|
||||
class ExecAction {
|
||||
constructor(settings, site) {
|
||||
@ -27,7 +27,8 @@ class ExecAction {
|
||||
customArg: cmd.customArg
|
||||
}
|
||||
if (useBus) {
|
||||
window.ultrawidify.bus.sendMessage(message.cmd, message);
|
||||
// todo: postMessage out of the iframe!
|
||||
// window.ultrawidify.bus.sendMessage(message.cmd, message);
|
||||
} else {
|
||||
Comms.sendMessage(message);
|
||||
}
|
||||
@ -48,7 +49,8 @@ class ExecAction {
|
||||
// this hopefully delays settings.save() until current crops are saved on the site
|
||||
// and thus avoid any fucky-wuckies
|
||||
if (useBus) {
|
||||
window.ultrawidify.bus.sendMessage(message.cmd, message);
|
||||
// todo: postMessage out of the iframe!
|
||||
// window.ultrawidify.bus.sendMessage(message.cmd, message);
|
||||
} else {
|
||||
await Comms.sendMessage(message);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import UI from './UI';
|
||||
import VuexWebExtensions from 'vuex-webextensions';
|
||||
import PlayerUiComponent from '../../../csui/PlayerUiComponent.vue';
|
||||
import PlayerUiComponent from '../../../csui/PlayerUiBase.vue';
|
||||
|
||||
if (process.env.CHANNEL !== 'stable'){
|
||||
console.info("Loading: PlayerUi");
|
||||
|
@ -1,52 +1,21 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createStore } from 'vuex';
|
||||
import mdiVue from 'mdi-vue/v3';
|
||||
import * as mdijs from '@mdi/js';
|
||||
|
||||
if (process.env.CHANNEL !== 'stable'){
|
||||
console.info("Loading: UI");
|
||||
}
|
||||
|
||||
|
||||
class UI {
|
||||
constructor(
|
||||
interfaceId,
|
||||
storeConfig,
|
||||
uiConfig, // {component, parentElement?}
|
||||
commsConfig,
|
||||
ultrawidify, // or, at least, videoData instance + event bus
|
||||
) {
|
||||
this.interfaceId = interfaceId;
|
||||
this.commsConfig = commsConfig;
|
||||
this.storeConfig = storeConfig;
|
||||
this.uiConfig = uiConfig;
|
||||
this.ultrawidify = ultrawidify;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
// initialize vuejs, but only once (check handled in initVue())
|
||||
// we need to initialize this _after_ initializing comms.
|
||||
|
||||
this.initVue();
|
||||
}
|
||||
|
||||
async initVue() {
|
||||
if (this.storeConfig) {
|
||||
this.vuexStore = createStore(this.storeConfig);
|
||||
}
|
||||
|
||||
this.initUi();
|
||||
}
|
||||
|
||||
async initUi() {
|
||||
if (this.app) {
|
||||
this.app.unmount();
|
||||
}
|
||||
|
||||
const random = Math.round(Math.random() * 69420);
|
||||
const uwid = `uw-${this.interfaceId}-root-${random}`
|
||||
const uwid = `uw-ultrawidify-${this.interfaceId}-root-${random}`
|
||||
|
||||
const rootDiv = document.createElement('div');
|
||||
|
||||
@ -64,26 +33,20 @@ class UI {
|
||||
|
||||
this.element = rootDiv;
|
||||
|
||||
const app = createApp(this.uiConfig.component)
|
||||
.use(mdiVue, {icons: mdijs})
|
||||
.use({ // hand eventBus to the component
|
||||
install: (app, options) => {
|
||||
app.mixin({
|
||||
data() {
|
||||
return {
|
||||
ultrawidify: options.ultrawidify
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}, {ultrawidify: this.ultrawidify});
|
||||
try {
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.setAttribute('src', browser.runtime.getURL('/csui/csui.html'));
|
||||
iframe.style.width = "100%";
|
||||
iframe.style.height = "100%";
|
||||
iframe.style.position = "absolute";
|
||||
iframe.style.zIndex = "1000";
|
||||
|
||||
rootDiv.appendChild(iframe);
|
||||
} catch(e) {
|
||||
|
||||
if (this.vuexStore) {
|
||||
app.use(this.vuexStore);
|
||||
}
|
||||
|
||||
this.app = app;
|
||||
app.mount(`#${uwid}`);
|
||||
console.log('——————————————————————————————————————— UI IS BEING CREATED ', rootDiv);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,17 +55,13 @@ class UI {
|
||||
*/
|
||||
replace(newUiConfig) {
|
||||
this.element?.remove();
|
||||
this.app.unmount();
|
||||
this.app = undefined;
|
||||
this.uiConfig = newUiConfig;
|
||||
this.initUi();
|
||||
this.init();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// this.comms?.destroy();
|
||||
this.element?.remove();
|
||||
this.app.unmount();
|
||||
this.app = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ import Debug from '../../conf/Debug';
|
||||
import ExtensionMode from '../../../common/enums/ExtensionMode.enum'
|
||||
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
|
||||
import PlayerNotificationUi from '../uwui/PlayerNotificationUI';
|
||||
import PlayerUi from '../uwui/PlayerUI';
|
||||
import BrowserDetect from '../../conf/BrowserDetect';
|
||||
import * as _ from 'lodash';
|
||||
import { sleep } from '../../../common/js/utils';
|
||||
@ -10,6 +9,7 @@ import VideoData from './VideoData';
|
||||
import Settings from '../Settings';
|
||||
import Logger from '../Logger';
|
||||
import EventBus from '../EventBus';
|
||||
import UI from '../uwui/UI';
|
||||
|
||||
if (process.env.CHANNEL !== 'stable'){
|
||||
console.info("Loading: PlayerData.js");
|
||||
@ -94,9 +94,9 @@ class PlayerData {
|
||||
this.invalid = false;
|
||||
this.element = this.getPlayer();
|
||||
|
||||
this.notificationService = new PlayerNotificationUi(this.element, this.settings, this.eventBus);
|
||||
this.ui = new PlayerUi(this.element, this.settings, this.eventBus, this.videoData);
|
||||
this.ui.init();
|
||||
// this.notificationService = new PlayerNotificationUi(this.element, this.settings, this.eventBus);
|
||||
this.ui = new UI('ultrawidifyUi', {parentElement: this.element});
|
||||
// this.ui.init();
|
||||
|
||||
this.dimensions = undefined;
|
||||
this.overlayNode = undefined;
|
||||
@ -578,12 +578,12 @@ class PlayerData {
|
||||
|
||||
forceRefreshPlayerElement() {
|
||||
this.element = this.getPlayer();
|
||||
this.notificationService?.replace(this.element);
|
||||
// this.notificationService?.replace(this.element);
|
||||
this.trackDimensionChanges();
|
||||
}
|
||||
|
||||
showNotification(notificationId) {
|
||||
this.notificationService?.showNotification(notificationId);
|
||||
// this.notificationService?.showNotification(notificationId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,7 +43,6 @@
|
||||
"open_in_tab": true
|
||||
},
|
||||
|
||||
|
||||
"web_accessible_resources": [
|
||||
"./*",
|
||||
"ext/*",
|
||||
@ -51,7 +50,8 @@
|
||||
"res/css/*",
|
||||
"res/img/settings/about-bg.png",
|
||||
"res/icons/*",
|
||||
"res/img/*"
|
||||
"res/img/*",
|
||||
"csui/*"
|
||||
],
|
||||
"permissions": [
|
||||
"storage",
|
||||
|
26653
src/res/css/mdi.scss
Normal file
26653
src/res/css/mdi.scss
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/res/fonts/materialdesignicons-webfont.woff2
Normal file
BIN
src/res/fonts/materialdesignicons-webfont.woff2
Normal file
Binary file not shown.
@ -16,6 +16,7 @@ const config = {
|
||||
'ext/uw-bg': './ext/uw-bg.js',
|
||||
'popup/popup': './popup/popup.js',
|
||||
'options/options': './options/options.js',
|
||||
'csui/csui': './csui/csui.js',
|
||||
// 'install/first-time/first-time':'./install/first-time/first-time.js',
|
||||
},
|
||||
output: {
|
||||
@ -26,7 +27,7 @@ const config = {
|
||||
devtool: "source-map",
|
||||
|
||||
resolve: {
|
||||
// maybe we'll move to TS some day, but today is not the day
|
||||
// maybe we'll move vue stuff to TS some day, but today is not the day
|
||||
extensions: [
|
||||
'.ts', '.tsx',
|
||||
'.js', '.vue'
|
||||
@ -112,6 +113,7 @@ const config = {
|
||||
new CopyWebpackPlugin([
|
||||
{ from: 'res', to: 'res', ignore: ['css', 'css/**']},
|
||||
{ from: 'ext', to: 'ext', ignore: ['conf/*', 'lib/**']},
|
||||
{ from: 'csui', to: 'csui', ignore: ['src']},
|
||||
|
||||
// we need to get webextension-polyfill and put it in common/lib
|
||||
{ from: '../node_modules/webextension-polyfill/dist/browser-polyfill.js', to: 'common/lib/browser-polyfill.js'},
|
||||
@ -121,6 +123,7 @@ const config = {
|
||||
// (TODO: check if this copy is even necessary — /icons has same content as /res/icons)
|
||||
{ from: 'icons', to: 'icons', ignore: ['icon.xcf'] },
|
||||
{ from: 'popup/popup.html', to: 'popup/popup.html', transform: transformHtml },
|
||||
{ from: 'csui/csui.html', to: 'csui/csui.html', transform: transformHtml },
|
||||
{ from: 'options/options.html', to: 'options/options.html', transform: transformHtml },
|
||||
// { from: 'install/first-time/first-time.html', to: 'install/first-time/first-time.html', transform: transformHtml},
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user