Skip to content

Commit

Permalink
Improve ThemeManager (kiwiirc#1835)
Browse files Browse the repository at this point in the history
* Improve ThemeManager

* Make ThemeManager observable
  • Loading branch information
ItsOnlyBinary authored Nov 16, 2023
1 parent b553b69 commit e9eaf17
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 112 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"@panter/vue-i18next": "^0.15.2",
"buffer": "^6.0.3",
"compare-versions": "^5.0.3",
"css-vars-ponyfill": "^2.4.8",
"event-emitter": "^0.3.5",
"font-awesome": "^4.7.0",
"htmlparser2": "^8.0.2",
Expand Down
10 changes: 0 additions & 10 deletions src/components/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
@click="emitDocumentClick"
@paste.capture="emitBufferPaste"
>
<link :href="themeUrl" rel="stylesheet" type="text/css">

<template v-if="!hasStarted || (!fallbackComponent && networks.length === 0)">
<component :is="startupComponent" @start="startUp" />
</template>
Expand Down Expand Up @@ -64,7 +62,6 @@
<script>
'kiwi public';
import cssVarsPonyfill from 'css-vars-ponyfill';
import '@/res/globalStyle.css';
import Tinycon from 'tinycon';
Expand All @@ -76,7 +73,6 @@ import MediaViewer from '@/components/MediaViewer';
import { State as SidebarState } from '@/components/Sidebar';
import * as Notifications from '@/libs/Notifications';
import * as bufferTools from '@/libs/bufferTools';
import ThemeManager from '@/libs/ThemeManager';
import Logger from '@/libs/Logger';
let log = Logger.namespace('App.vue');
Expand Down Expand Up @@ -106,7 +102,6 @@ export default {
mediaviewerComponent: null,
mediaviewerComponentProps: {},
mediaviewerIframe: false,
themeUrl: '',
sidebarState: new SidebarState(),
};
},
Expand Down Expand Up @@ -186,12 +181,7 @@ export default {
});
},
watchForThemes() {
let themes = ThemeManager.instance();
this.themeUrl = ThemeManager.themeUrl(themes.currentTheme());
this.$nextTick(() => cssVarsPonyfill());
this.listen(this.$state, 'theme.change', () => {
this.themeUrl = ThemeManager.themeUrl(themes.currentTheme());
this.$nextTick(() => cssVarsPonyfill());
this.$state.clearNickColours();
});
},
Expand Down
37 changes: 31 additions & 6 deletions src/components/AppSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@
</option>
</select>
</label>
<label v-if="theme==='custom'">
<label
v-if="theme.toLowerCase()==='custom'"
class="kiwi-appsettings-setting-theme-custom"
>
<span>{{ $t('settings_themeurl') }} </span>
<input v-model="customThemeUrl" class="u-input">
</label>
Expand Down Expand Up @@ -349,24 +352,34 @@ export default {
let updateFn = () => {
let theme = themeMgr.currentTheme();
this.theme = theme.name;
this.customThemeUrl = theme.name === 'custom' ?
this.customThemeUrl = theme.name.toLowerCase() === 'custom' ?
theme.url :
'';
};
let failedFn = () => {
if (this.theme.toLowerCase() !== 'custom') {
let theme = themeMgr.currentTheme();
this.theme = theme.name;
}
};
let watchTheme = (newVal) => {
themeMgr.setTheme(newVal);
if (newVal.toLowerCase() !== 'custom') {
themeMgr.setTheme(newVal);
}
};
let watchCustomThemeUrl = (newVal) => {
if (themeMgr.currentTheme().name === 'custom') {
let watchCustomThemeUrl = _.debounce((newVal) => {
if (this.theme.toLowerCase() === 'custom') {
themeMgr.setCustomThemeUrl(newVal);
}
};
}, 800, { leading: false, trailing: true });
// Remove all our attached events to cleanup
let teardownFn = () => {
this.$state.$off('theme.change', updateFn);
this.$state.$off('theme.failed', failedFn);
watches.forEach((unwatchFn) => unwatchFn());
this.$off('hook:destroy', teardownFn);
};
Expand All @@ -376,6 +389,7 @@ export default {
updateFn();
this.$state.$on('theme.change', updateFn);
this.$state.$on('theme.failed', failedFn);
this.$once('hook:destroyed', teardownFn);
// $watch returns a function to stop watching the data field. Add them into
Expand Down Expand Up @@ -444,6 +458,17 @@ export default {
float: right;
}
.kiwi-appsettings-setting-theme-custom {
.kiwi-appsettings .u-form & {
display: flex;
align-items: center;
> input {
flex-grow: 1;
}
}
}
.kiwi-appsettings-setting-showjoinpart span {
max-width: none;
}
Expand Down
210 changes: 135 additions & 75 deletions src/libs/ThemeManager.js
Original file line number Diff line number Diff line change
@@ -1,74 +1,159 @@
'kiwi public';

import _ from 'lodash';
import Vue from 'vue';

import Logger from '@/libs/Logger';

const log = Logger.namespace('ThemeManager');

let createdInstance = null;

export default class ThemeManager {
constructor(state) {
constructor(state, argTheme) {
this.state = state;
this.listenForIrcEvents();
this.varsEl = null;

this.activeTheme = null;
this.previousTheme = null;

this.varsElement = null;
this.currentElement = null;
this.loadingElement = null;

Vue.observable(this);

const initialTheme = this.findTheme(argTheme)
|| this.findTheme(state.setting('theme'))
|| this.availableThemes()[0];

this.setTheme(initialTheme);
}

themeVar(varName) {
if (!this.varsEl) {
this.varsEl = document.querySelector('.kiwi-wrap');
static themeUrl(theme) {
let [url, qs] = theme.url.split('?');
if (url[url.length - 1] !== '/') {
url += '/';
}

let styles = window.getComputedStyle(this.varsEl);
let v = styles.getPropertyValue('--kiwi-' + varName);
return (v || '').trim();
return url + 'theme.css' + (qs ? '?' + qs : '');
}

availableThemes() {
return this.state.settings.themes;
return this.state.getSetting('settings.themes');
}

currentTheme() {
let state = this.state;
let currentThemeName = state.setting('theme');
currentThemeName = currentThemeName.toLowerCase();

let theme = _.find(state.settings.themes, (t) => {
let isMatch = t.name.toLowerCase() === currentThemeName;
return isMatch;
});

// If no theme was set, use the first one in our theme list
if (!theme) {
theme = state.settings.themes[0];
findTheme(name) {
if (!name) {
return null;
}
return _.find(this.availableThemes(), (t) => t.name.toLowerCase() === name.toLowerCase());
}

return theme;
currentTheme() {
const theme = this.activeTheme || this.availableThemes()[0];
return this.findTheme(theme.name);
}

setTheme(theme) {
let theTheme = null;
const nextTheme = Object.assign(
Object.create(null),
(typeof theme === 'string')
? this.findTheme(theme)
: theme,
);

if (!nextTheme || !nextTheme.url) {
// Tried to set an invalid theme, abort
// reset the theme setting name to current if its not valid
if (this.activeTheme.name !== this.state.setting('theme')) {
this.state.setting('theme', this.activeTheme.name);
}
log.error('Invalid theme', nextTheme);
return;
}

if (typeof theme === 'string') {
// Make sure this theme exists
theTheme = _.find(this.availableThemes(), (t) => {
let isMatch = t.name.toLowerCase() === theme.toLowerCase();
return isMatch;
});
if (this.loadingElement) {
// There is already a loading theme
// remove it as we are about to load another
document.head.removeChild(this.loadingElement);
this.loadingElement = null;
}

if (!theTheme) {
return;
}
if (this.activeTheme && this.activeTheme.url === nextTheme.url) {
// Theme did not change, abort
return;
}

const nextNameLower = nextTheme.name.toLowerCase();
const themeElement = document.createElement('link');

if (this.previousTheme) {
// If previousTheme is not set then this is the initial theme
// do not check its loading/error state so there is always an
// active and previous theme set
themeElement.onload = () => {
// New theme loaded successfully
this.previousTheme = this.activeTheme;
this.activeTheme = nextTheme;

if (this.currentElement) {
// Remove the old theme from the DOM
document.head.removeChild(this.currentElement);
}

// Move our loaded element into current position
this.currentElement = this.loadingElement;
this.loadingElement = null;

if (nextTheme.name !== this.state.setting('theme')) {
// Reset the theme setting name to current if its not valid
this.state.setting('theme', nextTheme.name);
}

this.state.$emit('theme.change', nextTheme, this.previousTheme);
};

themeElement.onerror = () => {
// New theme failed to load, remove its loading element
document.head.removeChild(this.loadingElement);
this.loadingElement = null;

if (nextNameLower === 'custom' && !/\/theme\.css(\?|$)/.test(nextTheme.url)) {
// For custom themes try appending /theme.css
this.setCustomThemeUrl(ThemeManager.themeUrl(nextTheme));
return;
}

this.state.$emit('theme.failed', nextTheme, this.activeTheme);
};

this.loadingElement = themeElement;
} else {
theTheme = theme;
// This is our initial theme set by url param or config
this.activeTheme = nextTheme;
this.previousTheme = nextTheme;
this.currentElement = themeElement;
}

let currentTheme = this.state.setting('theme').toLowerCase();
if (currentTheme !== theTheme.name.toLowerCase()) {
this.state.setting('theme', theTheme.name);
this.state.$emit('theme.change');
themeElement.rel = 'stylesheet';
themeElement.type = 'text/css';
themeElement.href = (nextNameLower !== 'custom')
? ThemeManager.themeUrl(nextTheme)
: nextTheme.url;
document.head.appendChild(themeElement);
}

setCustomThemeUrl(url) {
const theme = this.findTheme('custom');
if (!theme) {
return;
}

theme.url = url;
this.setTheme(theme);
}

reload() {
let theme = this.currentTheme();
const theme = this.currentTheme();
if (!theme) {
return;
}
Expand All @@ -83,48 +168,23 @@ export default class ThemeManager {
}

theme.url = url;
this.state.$emit('theme.change');
}

static themeUrl(theme) {
let parts = theme.url.split('?');
let url = parts[0];
let qs = parts[1] || '';

if (url[url.length - 1] !== '/') {
url += '/';
}
return url + 'theme.css' + (qs ? '?' + qs : '');
this.setTheme(theme.name);
}

setCustomThemeUrl(url) {
let theme = _.find(ThemeManager.instance().availableThemes(), {
name: 'custom',
});

if (theme) {
theme.url = url;
}

if (theme.name === 'custom') {
this.state.$emit('theme.change');
themeVar(varName) {
if (!this.varsElement) {
this.varsElement = document.querySelector('.kiwi-wrap');
}
}

// When we get a CTCP 'kiwi theme reload' then reload our theme. Handy for theme devs
listenForIrcEvents() {
this.state.$on('irc.ctcp request', (event, network) => {
let ctcpType = (event.type || '').toLowerCase();
if (ctcpType === 'kiwi' && event.message.indexOf('theme reload') > -1) {
this.reload();
}
});
const styles = window.getComputedStyle(this.varsElement);
const value = styles.getPropertyValue('--kiwi-' + varName);
return (value || '').trim();
}
}

ThemeManager.instance = function instance(state) {
ThemeManager.instance = (...args) => {
if (!createdInstance) {
createdInstance = new ThemeManager(state);
createdInstance = new ThemeManager(...args);
}

return createdInstance;
Expand Down
Loading

0 comments on commit e9eaf17

Please sign in to comment.