Sanitized public release v1.0.3
Source-Tag: v1.0.3 Manifest-SHA256: b7b09bf48f9e092ed8ad82f99c0319d6c3837d0a1549bd0cc4a374276c0f3896
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<section class="neo-colour-settings" ng-show="neoSettingsTab === 'colours'" role="tabpanel" aria-label="Colours">
|
||||
<div class="neo-settings-toolbar">
|
||||
<p>These values are read from and saved to WeeChat. Role colours update <code>irc.color.nick_prefixes</code>.</p>
|
||||
<button type="button" class="btn btn-default" ng-click="loadNeoWeeChatColours()" ng-disabled="neoWeeChatColours.loading">Refresh</button>
|
||||
</div>
|
||||
<p class="status" ng-bind="neoWeeChatColours.status"></p>
|
||||
<h5>Chat</h5>
|
||||
<div class="neo-colour-grid">
|
||||
<label ng-repeat="item in neoWeeChatColours.items"><span ng-bind="item.label"></span><select class="form-control" ng-model="neoWeeChatColours.values[item.name]" ng-options="colour for colour in neoWeeChatColourChoices" ng-change="setNeoWeeChatColour(item.name)"></select><i class="neo-colour-swatch" ng-style="{'background-color': neoWeeChatColourPreview(neoWeeChatColours.values[item.name])}" title="{{neoWeeChatColours.values[item.name]}}"></i><code ng-bind="item.name"></code></label>
|
||||
</div>
|
||||
<h5>Nickname roles</h5>
|
||||
<div class="neo-colour-grid">
|
||||
<label><span>Operator</span><select class="form-control" ng-model="neoWeeChatColours.prefixes.o" ng-options="colour for colour in neoWeeChatColourChoices" ng-change="setNeoNickPrefixColour('o')"></select><i class="neo-colour-swatch" ng-style="{'background-color': neoWeeChatColourPreview(neoWeeChatColours.prefixes.o)}" title="{{neoWeeChatColours.prefixes.o}}"></i><code>mode o</code></label>
|
||||
<label><span>Half-operator</span><select class="form-control" ng-model="neoWeeChatColours.prefixes.h" ng-options="colour for colour in neoWeeChatColourChoices" ng-change="setNeoNickPrefixColour('h')"></select><i class="neo-colour-swatch" ng-style="{'background-color': neoWeeChatColourPreview(neoWeeChatColours.prefixes.h)}" title="{{neoWeeChatColours.prefixes.h}}"></i><code>mode h</code></label>
|
||||
<label><span>Voice</span><select class="form-control" ng-model="neoWeeChatColours.prefixes.v" ng-options="colour for colour in neoWeeChatColourChoices" ng-change="setNeoNickPrefixColour('v')"></select><i class="neo-colour-swatch" ng-style="{'background-color': neoWeeChatColourPreview(neoWeeChatColours.prefixes.v)}" title="{{neoWeeChatColours.prefixes.v}}"></i><code>mode v</code></label>
|
||||
<label><span>Other prefixes</span><select class="form-control" ng-model="neoWeeChatColours.prefixes['*']" ng-options="colour for colour in neoWeeChatColourChoices" ng-change="setNeoNickPrefixColour('*')"></select><i class="neo-colour-swatch" ng-style="{'background-color': neoWeeChatColourPreview(neoWeeChatColours.prefixes['*'])}" title="{{neoWeeChatColours.prefixes['*']}}"></i><code>fallback *</code></label>
|
||||
</div>
|
||||
<h5>Channel nickname palette</h5>
|
||||
<div class="neo-palette-setting"><input class="form-control" type="text" ng-model="neoWeeChatColours.values['weechat.color.chat_nick_colors']" ng-change="updateNeoPalettePreview()"><button type="button" class="btn btn-primary" ng-click="setNeoWeeChatColour('weechat.color.chat_nick_colors')">Save palette</button><div class="neo-palette-preview" aria-label="Nickname palette preview"><i ng-repeat="colour in neoWeeChatColours.palettePreview track by $index" ng-style="{'background-color': neoWeeChatColourPreview(colour)}" title="{{colour}}"></i></div><code>weechat.color.chat_nick_colors</code></div>
|
||||
</section>
|
||||
@@ -0,0 +1,102 @@
|
||||
var neoColourOptions = [
|
||||
{name: 'weechat.color.chat_nick_self', label: 'Your nickname', variable: '--neorelay-nick-self'},
|
||||
{name: 'weechat.color.chat_nick', label: 'Regular nickname fallback', variable: '--neorelay-nick'},
|
||||
{name: 'weechat.color.chat_nick_other', label: 'Other private nicknames', variable: '--neorelay-nick-other'},
|
||||
{name: 'weechat.color.nicklist_away', label: 'Away / AFK', variable: '--neorelay-nick-away'},
|
||||
{name: 'weechat.color.chat_highlight', label: 'Highlight foreground', variable: '--neorelay-chat-highlight'},
|
||||
{name: 'weechat.color.chat_highlight_bg', label: 'Highlight background', variable: '--neorelay-chat-highlight-bg'},
|
||||
{name: 'weechat.color.chat_time', label: 'Timestamp', variable: '--neorelay-chat-time'}
|
||||
];
|
||||
var neoPrefixOption = 'irc.color.nick_prefixes';
|
||||
var neoPaletteOption = 'weechat.color.chat_nick_colors';
|
||||
$scope.neoWeeChatColours = {loading: false, status: '', items: neoColourOptions, rows: {}, values: {}, prefixes: {}, prefixRaw: '', palettePreview: []};
|
||||
$scope.neoWeeChatColourPreview = neoResolveWeeChatColour;
|
||||
$scope.updateNeoPalettePreview = function() {
|
||||
$scope.neoWeeChatColours.palettePreview = String($scope.neoWeeChatColours.values[neoPaletteOption] || '').split(',').map(function(value) { return value.trim(); }).filter(neoValidWeeChatColour);
|
||||
};
|
||||
|
||||
function neoParsePrefixes(value) {
|
||||
var result = {};
|
||||
String(value || '').split(';').forEach(function(entry) {
|
||||
var match = entry.match(/^([A-Za-z*]):([^;]+)$/);
|
||||
if (match) result[match[1]] = match[2].replace(/^[*!/_|]+/, '');
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function neoApplyRelayColours() {
|
||||
neoColourOptions.forEach(function(item) {
|
||||
var value = $scope.neoWeeChatColours.values[item.name];
|
||||
document.documentElement.style.setProperty(item.variable, neoResolveWeeChatColour(value));
|
||||
});
|
||||
var prefixes = $scope.neoWeeChatColours.prefixes;
|
||||
document.documentElement.style.setProperty('--neorelay-nick-op', neoResolveWeeChatColour(prefixes.o || prefixes.a || prefixes.q || prefixes['*']));
|
||||
document.documentElement.style.setProperty('--neorelay-nick-halfop', neoResolveWeeChatColour(prefixes.h || prefixes['*']));
|
||||
document.documentElement.style.setProperty('--neorelay-nick-voice', neoResolveWeeChatColour(prefixes.v || prefixes['*']));
|
||||
}
|
||||
|
||||
$scope.loadNeoWeeChatColours = function() {
|
||||
if (!$rootScope.connected || $scope.neoWeeChatColours.loading) return;
|
||||
$scope.neoWeeChatColours.loading = true;
|
||||
$scope.neoWeeChatColours.status = 'Loading WeeChat colours...';
|
||||
var names = neoColourOptions.map(function(item) { return item.name; }).concat([neoPrefixOption, neoPaletteOption]);
|
||||
Promise.all(names.map(function(name) { return connection.requestInfolist('option', 0, name); })).then(function(results) {
|
||||
$scope.$applyAsync(function() {
|
||||
results.forEach(function(rows) {
|
||||
if (!rows[0]) return;
|
||||
var row = neoNormalizeOption(rows[0]);
|
||||
neoLoadedOptions[row.name] = row;
|
||||
$scope.neoWeeChatColours.rows[row.name] = row;
|
||||
$scope.neoWeeChatColours.values[row.name] = row.value;
|
||||
});
|
||||
$scope.neoWeeChatColours.prefixRaw = $scope.neoWeeChatColours.values[neoPrefixOption] || '';
|
||||
$scope.neoWeeChatColours.prefixes = neoParsePrefixes($scope.neoWeeChatColours.prefixRaw);
|
||||
$scope.updateNeoPalettePreview();
|
||||
neoApplyRelayColours();
|
||||
$scope.neoWeeChatColours.loading = false;
|
||||
$scope.neoWeeChatColours.status = 'Loaded from WeeChat.';
|
||||
});
|
||||
}, function() {
|
||||
$scope.$applyAsync(function() {
|
||||
$scope.neoWeeChatColours.loading = false;
|
||||
$scope.neoWeeChatColours.status = 'Unable to load WeeChat colours.';
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setNeoWeeChatColour = function(name) {
|
||||
var row = $scope.neoWeeChatColours.rows[name];
|
||||
var value = $scope.neoWeeChatColours.values[name];
|
||||
neoWriteOption(row, value, function(updated) {
|
||||
$scope.neoWeeChatColours.values[name] = updated.value;
|
||||
if (name === neoPaletteOption) $scope.updateNeoPalettePreview();
|
||||
neoApplyRelayColours();
|
||||
$scope.neoWeeChatColours.status = 'Saved ' + name + ' to WeeChat.';
|
||||
}, function(message) { $scope.neoWeeChatColours.status = message; });
|
||||
};
|
||||
|
||||
$scope.setNeoNickPrefixColour = function(mode) {
|
||||
var row = $scope.neoWeeChatColours.rows[neoPrefixOption];
|
||||
if (!row || !neoValidWeeChatColour($scope.neoWeeChatColours.prefixes[mode])) return;
|
||||
var found = false;
|
||||
var entries = String($scope.neoWeeChatColours.prefixRaw || '').split(';').filter(Boolean).map(function(entry) {
|
||||
if (entry.slice(0, 2) === mode + ':') {
|
||||
found = true;
|
||||
return mode + ':' + $scope.neoWeeChatColours.prefixes[mode];
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
if (!found) entries.push(mode + ':' + $scope.neoWeeChatColours.prefixes[mode]);
|
||||
var value = entries.join(';');
|
||||
neoWriteOption(row, value, function(updated) {
|
||||
$scope.neoWeeChatColours.prefixRaw = updated.value;
|
||||
$scope.neoWeeChatColours.values[neoPrefixOption] = updated.value;
|
||||
$scope.neoWeeChatColours.prefixes = neoParsePrefixes(updated.value);
|
||||
neoApplyRelayColours();
|
||||
$scope.neoWeeChatColours.status = 'Saved IRC prefix colours to WeeChat.';
|
||||
}, function(message) { $scope.neoWeeChatColours.status = message; });
|
||||
};
|
||||
|
||||
$scope.$watch(function() { return $rootScope.connected; }, function(connected) {
|
||||
if (connected) $timeout($scope.loadNeoWeeChatColours, 250);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="command-menu" ng-if="commandMenuOpen" role="listbox" id="{{inputId}}-command-menu">
|
||||
<button type="button" class="command-menu-item" ng-repeat="item in commandMenuMatches track by item.command" ng-class="{selected: $index === commandMenuIndex}" id="{{inputId}}-command-{{$index}}" role="option" aria-selected="{{$index === commandMenuIndex}}" ng-mousedown="$event.preventDefault()" ng-mouseenter="commandMenuIndex = $index" ng-click="selectCommand(item)">
|
||||
<span class="command-menu-name">/{{item.command}}</span>
|
||||
<span class="command-menu-args" ng-bind="item.args"></span>
|
||||
<span class="command-menu-description" ng-bind="item.description"></span>
|
||||
</button>
|
||||
<div class="command-menu-empty" ng-if="commandMenuLoading">Loading commands...</div>
|
||||
<div class="command-menu-empty" ng-if="!commandMenuLoading && !commandMenuMatches.length">No matching commands</div>
|
||||
</div>
|
||||
@@ -0,0 +1,146 @@
|
||||
var commandHooks = null;
|
||||
var commandHooksRequest = null;
|
||||
var commandMenuGeneration = 0;
|
||||
var suppressCommandMenuKeypress = false;
|
||||
var serverCommandFallbacks = [
|
||||
{command:'defcon',args:'[<level>]',description:'Set or show Ergo emergency restrictions',insertion:'/quote DEFCON',serverCommand:true},
|
||||
{command:'deoper',args:'',description:'Remove IRC operator privileges',insertion:'/quote DEOPER',serverCommand:true},
|
||||
{command:'dline',args:'[<duration>] <ip>/<net> [<reason>] || LIST',description:'Add or list IP/network bans',insertion:'/quote DLINE',serverCommand:true},
|
||||
{command:'kline',args:'[<duration>] <mask> [<reason>] || LIST',description:'Add or list client-mask bans',insertion:'/quote KLINE',serverCommand:true},
|
||||
{command:'uban',args:'ADD|DEL|LIST|INFO <target>',description:'Manage unified bans',insertion:'/quote UBAN',serverCommand:true},
|
||||
{command:'undline',args:'<ip>/<net>',description:'Remove an IP/network ban',insertion:'/quote UNDLINE',serverCommand:true},
|
||||
{command:'unkline',args:'<mask>',description:'Remove a client-mask ban',insertion:'/quote UNKLINE',serverCommand:true}
|
||||
];
|
||||
|
||||
var mergeCommandHooks = function(hooks) {
|
||||
var unique = Object.create(null);
|
||||
var hasOwn = Object.prototype.hasOwnProperty;
|
||||
hooks.forEach(function(hook) {
|
||||
var key = String(hook.command || '').toLowerCase();
|
||||
if (key && (!hasOwn.call(unique, key) || hook.priority > unique[key].priority)) {
|
||||
unique[key] = Object.assign({}, hook, {command:key});
|
||||
}
|
||||
});
|
||||
serverCommandFallbacks.forEach(function(hook) {
|
||||
if (!hasOwn.call(unique, hook.command)) unique[hook.command] = hook;
|
||||
});
|
||||
return Object.keys(unique).map(function(key){return unique[key];}).sort(function(left,right){return left.command.localeCompare(right.command);});
|
||||
};
|
||||
|
||||
$scope.commandMenuOpen = false;
|
||||
$scope.commandMenuLoading = false;
|
||||
$scope.commandMenuMatches = [];
|
||||
$scope.commandMenuIndex = 0;
|
||||
|
||||
var closeCommandMenu = function() {
|
||||
$scope.commandMenuOpen = false;
|
||||
$scope.commandMenuLoading = false;
|
||||
$scope.commandMenuMatches = [];
|
||||
$scope.commandMenuIndex = 0;
|
||||
};
|
||||
|
||||
var updateCommandMenu = function() {
|
||||
var match = /^\/([^\s]*)$/.exec($scope.command || '');
|
||||
if (!match) {
|
||||
closeCommandMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.commandMenuOpen = true;
|
||||
$scope.commandMenuIndex = 0;
|
||||
if (commandHooks === null) {
|
||||
$scope.commandMenuLoading = true;
|
||||
if (commandHooksRequest === null) {
|
||||
var requestGeneration = commandMenuGeneration;
|
||||
commandHooksRequest = connection.requestCommandHooks().then(function(hooks) {
|
||||
if (requestGeneration !== commandMenuGeneration) {
|
||||
return;
|
||||
}
|
||||
commandHooks = mergeCommandHooks(hooks);
|
||||
commandHooksRequest = null;
|
||||
updateCommandMenu();
|
||||
}, function() {
|
||||
if (requestGeneration !== commandMenuGeneration) {
|
||||
return;
|
||||
}
|
||||
commandHooks = mergeCommandHooks([]);
|
||||
commandHooksRequest = null;
|
||||
updateCommandMenu();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var prefix = match[1].toLowerCase();
|
||||
var activeBuffer = models.getActiveBuffer();
|
||||
var isIrcBuffer = activeBuffer && activeBuffer.plugin === 'irc';
|
||||
$scope.commandMenuLoading = false;
|
||||
$scope.commandMenuMatches = commandHooks.filter(function(hook) {
|
||||
return (!hook.serverCommand || isIrcBuffer) && hook.command.indexOf(prefix) === 0;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.selectCommand = function(item) {
|
||||
var suffix = item.args ? ' ' : '';
|
||||
$scope.command = (item.insertion || ('/' + item.command.toLowerCase())) + suffix;
|
||||
closeCommandMenu();
|
||||
setTimeout(function() {
|
||||
var inputNode = $scope.getInputNode();
|
||||
inputNode.focus();
|
||||
inputNode.setSelectionRange($scope.command.length, $scope.command.length);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
var scrollCommandMenuSelection = function() {
|
||||
setTimeout(function() {
|
||||
var selected = document.getElementById(
|
||||
$scope.inputId + '-command-' + $scope.commandMenuIndex
|
||||
);
|
||||
if (selected) {
|
||||
selected.scrollIntoView({block: 'nearest'});
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
$scope.handleCommandMenuKey = function(event, code, inputNode) {
|
||||
if (suppressCommandMenuKeypress && event.type === 'keypress' &&
|
||||
(code === 9 || code === 13)) {
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
if (!$scope.commandMenuOpen || event.type !== 'keydown' ||
|
||||
document.activeElement !== inputNode || event.altKey ||
|
||||
event.ctrlKey || event.metaKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var count = $scope.commandMenuMatches.length;
|
||||
if ((code === 38 || code === 40) && count > 0) {
|
||||
var direction = code === 38 ? -1 : 1;
|
||||
$scope.commandMenuIndex = ($scope.commandMenuIndex + direction + count) % count;
|
||||
scrollCommandMenuSelection();
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
if ((code === 9 || code === 13) && count > 0 && !event.shiftKey) {
|
||||
$scope.selectCommand($scope.commandMenuMatches[$scope.commandMenuIndex]);
|
||||
suppressCommandMenuKeypress = true;
|
||||
setTimeout(function() { suppressCommandMenuKeypress = false; }, 0);
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
if (code === 27) {
|
||||
closeCommandMenu();
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
var removeCommandMenuDisconnect = $rootScope.$on('relayDisconnect', function() {
|
||||
commandMenuGeneration++;
|
||||
commandHooks = null;
|
||||
commandHooksRequest = null;
|
||||
closeCommandMenu();
|
||||
});
|
||||
$scope.$on('$destroy', removeCommandMenuDisconnect);
|
||||
@@ -0,0 +1,36 @@
|
||||
<div class="panel connection-panel" data-state="active">
|
||||
<div class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<form class="form-signin" role="form">
|
||||
<div class="relay-terminal">
|
||||
<div class="relay-terminal-bar"><span></span><span></span><span></span><b>relay@{{RELAY_HOST}}:~</b></div>
|
||||
<div class="relay-terminal-body">
|
||||
<p><span class="relay-prompt">>></span> relay config --show</p>
|
||||
<dl>
|
||||
<div><dt>HOSTNAME</dt><dd>{{RELAY_HOST}}</dd></div>
|
||||
<div><dt>PORT</dt><dd>443</dd></div>
|
||||
<div><dt>PATH</dt><dd>/weechat</dd></div>
|
||||
<div><dt>TRANSPORT</dt><dd>WSS / TLS</dd></div>
|
||||
</dl>
|
||||
<div class="relay-password-line">
|
||||
<label for="password"><span class="relay-prompt">>></span> password</label>
|
||||
<span class="relay-password-field">
|
||||
<span class="relay-password-mask" ng-if="password" ng-cloak>{{ maskPassword(password) }}</span>
|
||||
<span class="relay-password-placeholder" ng-if="!password">[ enter relay password ]</span>
|
||||
<input type="password" id="password" ng-model="password" autocomplete="current-password" aria-label="WeeChat relay password">
|
||||
</span>
|
||||
</div>
|
||||
<p class="relay-password-error" ng-show="passwordError" ng-cloak><span class="relay-prompt">!!</span> authentication rejected</p>
|
||||
<p class="relay-input-line"><span class="relay-prompt">>></span> <span class="relay-cursor" aria-hidden="true">_</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="connection-options">
|
||||
<label><input type="checkbox" ng-model="settings.tls" disabled> TLS encryption</label>
|
||||
<label><input type="checkbox" ng-model="settings.savepassword"> Save password in this browser</label>
|
||||
<label><input type="checkbox" ng-model="settings.autoconnect"> Automatically connect</label>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary relay-connect" ng-click="connect()" ng-cloak>{{ connectbutton }} <i ng-class="connectbuttonicon" class="glyphicon"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="neo-context-menu" ng-if="menu.visible" ng-style="menu.position" role="menu" aria-label="{{menu.label}}">
|
||||
<button type="button" role="menuitem" ng-repeat="item in menu.actions track by item.id" ng-class="{destructive:item.destructive}" ng-click="runContextAction(item)" ng-bind="item.label"></button>
|
||||
</div>
|
||||
@@ -0,0 +1,95 @@
|
||||
'use strict';
|
||||
(function() {
|
||||
var weechat = angular.module('weechat');
|
||||
|
||||
weechat.factory('neoContextMenu', ['$rootScope', 'connection', 'models', function($rootScope, connection, models) {
|
||||
var menu = {visible: false, actions: [], position: {}, label: ''};
|
||||
var send = function(buffer, command) { connection.sendMessageToBuffer(buffer.id, command); };
|
||||
var action = function(id, label, run, destructive) { return {id: id, label: label, run: run, destructive: Boolean(destructive)}; };
|
||||
var safeNick = function(value) { return /^[A-Za-z0-9[\]\\`_^{|}-]+$/.test(value || ''); };
|
||||
|
||||
function bufferActions(buffer) {
|
||||
var actions = [];
|
||||
var core = buffer.fullName === 'core.weechat';
|
||||
if (buffer.type === 'server' && buffer.plugin === 'irc') {
|
||||
actions.push(action('edit-server', 'Edit server settings', function() { $rootScope.$broadcast('neoServerManagerOpen', buffer.server || buffer.shortName); }));
|
||||
actions.push(action('connect', 'Connect', function() { send(buffer, '/connect ' + buffer.server); }));
|
||||
actions.push(action('disconnect', 'Disconnect', function() { send(buffer, '/disconnect ' + buffer.server); }, true));
|
||||
}
|
||||
if (buffer.type === 'channel' && buffer.plugin === 'irc') {
|
||||
actions.push(action('mute', 'Mute activity', function() { send(buffer, '/buffer notify none'); }));
|
||||
actions.push(action('unmute', 'Unmute activity', function() { send(buffer, '/buffer notify all'); }));
|
||||
actions.push(action('part', 'Part channel', function() { send(buffer, '/part'); }, true));
|
||||
actions.push(action('rejoin', 'Rejoin channel', function() { send(buffer, '/join'); }));
|
||||
actions.push(action('autojoin-add', 'Add to auto-join', function() { send(buffer, '/autojoin add'); }));
|
||||
actions.push(action('autojoin-del', 'Remove from auto-join', function() { send(buffer, '/autojoin del'); }));
|
||||
}
|
||||
if (/neodrop_manager/.test(buffer.fullName)) {
|
||||
actions.push(action('drops-list', 'Refresh drops', function() { send(buffer, '/drops list'); }));
|
||||
actions.push(action('drops-help', 'NeoDrop help', function() { send(buffer, '/drops help'); }));
|
||||
}
|
||||
if (/neoguard/.test(buffer.fullName)) {
|
||||
actions.push(action('guard-status', 'Refresh NeoGuard', function() { send(buffer, '/neoguard status'); }));
|
||||
actions.push(action('guard-help', 'NeoGuard help', function() { send(buffer, '/neoguard'); }));
|
||||
}
|
||||
actions.push(action('clear', 'Clear buffer', function() { send(buffer, '/buffer clear'); }));
|
||||
if (!core) actions.push(action('close', 'Close buffer', function() {
|
||||
if (window.confirm('Close ' + (buffer.shortName || buffer.fullName) + '?')) send(buffer, '/buffer close');
|
||||
}, true));
|
||||
return actions;
|
||||
}
|
||||
|
||||
function nickActions(target) {
|
||||
var nick = target.nick.name;
|
||||
var buffer = target.buffer;
|
||||
if (!safeNick(nick) || !buffer || buffer.plugin !== 'irc') return [];
|
||||
var actions = [
|
||||
action('dm', 'Direct message', function() {
|
||||
var prefix = buffer.fullName.substring(0, buffer.fullName.lastIndexOf('.') + 1);
|
||||
if (!models.setActiveBuffer(prefix + nick, 'fullName')) { models.outgoingQueries.push(nick); send(buffer, '/query -noswitch ' + nick); }
|
||||
}),
|
||||
action('whois', 'Whois', function() { send(buffer, '/whois ' + nick); })
|
||||
];
|
||||
if (buffer.type === 'channel') {
|
||||
actions.push(action('op', 'Give operator', function() { send(buffer, '/mode +o ' + nick); }));
|
||||
actions.push(action('halfop', 'Give half-operator', function() { send(buffer, '/mode +h ' + nick); }));
|
||||
actions.push(action('voice', 'Give voice', function() { send(buffer, '/mode +v ' + nick); }));
|
||||
actions.push(action('kick', 'Kick', function() { if (window.confirm('Kick ' + nick + '?')) send(buffer, '/kick ' + nick); }, true));
|
||||
actions.push(action('ban', 'Ban', function() { if (window.confirm('Ban ' + nick + '?')) send(buffer, '/ban ' + nick); }, true));
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
return {
|
||||
menu: menu,
|
||||
open: function(event, target) {
|
||||
var actions = target.kind === 'nick' ? nickActions(target) : bufferActions(target.buffer);
|
||||
if (!actions.length) return;
|
||||
menu.actions = actions;
|
||||
menu.label = target.kind === 'nick' ? 'Actions for ' + target.nick.name : 'Buffer actions';
|
||||
menu.position = {left: Math.min(event.clientX, window.innerWidth - 230) + 'px', top: Math.min(event.clientY, window.innerHeight - 360) + 'px'};
|
||||
menu.visible = true;
|
||||
},
|
||||
close: function() { menu.visible = false; },
|
||||
run: function(item) { menu.visible = false; item.run(); }
|
||||
};
|
||||
}]);
|
||||
|
||||
weechat.directive('neoContext', ['neoContextMenu', function(neoContextMenu) {
|
||||
return {restrict: 'A', link: function(scope, element, attrs) {
|
||||
var open = function(event) { event.preventDefault(); event.stopPropagation(); scope.$apply(function() { neoContextMenu.open(event, scope.$eval(attrs.neoContext)); }); };
|
||||
element.on('contextmenu', open);
|
||||
scope.$on('$destroy', function() { element.off('contextmenu', open); });
|
||||
}};
|
||||
}]);
|
||||
|
||||
weechat.directive('neoContextMenuView', ['neoContextMenu', '$document', function(neoContextMenu, $document) {
|
||||
return {restrict: 'E', templateUrl: 'directives/context-menu.html', link: function(scope) {
|
||||
scope.menu = neoContextMenu.menu;
|
||||
scope.runContextAction = neoContextMenu.run;
|
||||
var close = function(event) { if (scope.menu.visible && !event.target.closest('.neo-context-menu')) scope.$apply(neoContextMenu.close); };
|
||||
$document.on('mousedown', close);
|
||||
scope.$on('$destroy', function() { $document.off('mousedown', close); });
|
||||
}};
|
||||
}]);
|
||||
}());
|
||||
@@ -0,0 +1,910 @@
|
||||
@font-face {
|
||||
font-family: "OpenDyslexic";
|
||||
src: url("../assets/fonts/OpenDyslexic-Regular.woff2") format("woff2");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "OpenDyslexic";
|
||||
src: url("../assets/fonts/OpenDyslexic-Italic.woff2") format("woff2");
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "OpenDyslexic";
|
||||
src: url("../assets/fonts/OpenDyslexic-Bold.woff2") format("woff2");
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "OpenDyslexic";
|
||||
src: url("../assets/fonts/OpenDyslexic-BoldItalic.woff2") format("woff2");
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
--neorelay-void: #07030d;
|
||||
--neorelay-ink: #0d0618;
|
||||
--neorelay-panel: #150a24;
|
||||
--neorelay-panel-raised: #211034;
|
||||
--neorelay-violet: #9d5cff;
|
||||
--neorelay-purple: #6f2cff;
|
||||
--neorelay-pink: #ff65c8;
|
||||
--neorelay-cyan: #55e7ff;
|
||||
--neorelay-acid: #b9ff4f;
|
||||
--neorelay-text: #f1eaff;
|
||||
--neorelay-muted: #a99abd;
|
||||
--neorelay-nick: #55e7ff;
|
||||
--neorelay-nick-other: #55e7ff;
|
||||
--neorelay-nick-self: #ff65c8;
|
||||
--neorelay-nick-op: #ff65c8;
|
||||
--neorelay-nick-halfop: #55e7ff;
|
||||
--neorelay-nick-voice: #b9ff4f;
|
||||
--neorelay-nick-away: #a99abd;
|
||||
--neorelay-chat-highlight: #ffff55;
|
||||
--neorelay-chat-highlight-bg: #aa00aa;
|
||||
--neorelay-chat-time: #f1eaff;
|
||||
--neorelay-line: rgba(157, 92, 255, 0.35);
|
||||
--neorelay-sidebar-preferred-width: 140px;
|
||||
--neorelay-sidebar-width: clamp(120px, var(--neorelay-sidebar-preferred-width), min(480px, 40vw));
|
||||
--neorelay-page-gutter: 5px;
|
||||
--neo-preferred-font: "OpenDyslexic", "Atkinson Hyperlegible", system-ui, sans-serif;
|
||||
--neo-preferred-font-size: 14px;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background: var(--neorelay-void) !important;
|
||||
color: var(--neorelay-text) !important;
|
||||
font-family: "OpenDyslexic", "Atkinson Hyperlegible", system-ui, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-image:
|
||||
linear-gradient(rgba(157, 92, 255, 0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(157, 92, 255, 0.035) 1px, transparent 1px),
|
||||
radial-gradient(circle at 75% 5%, rgba(111, 44, 255, 0.2), transparent 34%) !important;
|
||||
background-size: 32px 32px, 32px 32px, auto !important;
|
||||
}
|
||||
|
||||
.favorite-font {
|
||||
font-family: "OpenDyslexic", "Atkinson Hyperlegible", monospace !important;
|
||||
}
|
||||
|
||||
body.neo-preferred-font {
|
||||
font-family: var(--neo-preferred-font) !important;
|
||||
font-size: var(--neo-preferred-font-size) !important;
|
||||
}
|
||||
|
||||
body.neo-preferred-font .content :not(.glyphicon):not(.neo-font-option),
|
||||
body.neo-preferred-font .gb-modal :not(.glyphicon):not(.neo-font-option),
|
||||
body.neo-preferred-font .neo-context-menu :not(.glyphicon):not(.neo-font-option),
|
||||
body.neo-preferred-font .neo-server-manager :not(.glyphicon):not(.neo-font-option) {
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
a,
|
||||
.panel-title,
|
||||
.panel-title a {
|
||||
color: var(--neorelay-cyan);
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus {
|
||||
color: var(--neorelay-acid);
|
||||
}
|
||||
|
||||
/* Logged-out identity */
|
||||
.login-shell {
|
||||
position: relative;
|
||||
width: min(1120px, calc(100% - 40px));
|
||||
margin: 0 auto;
|
||||
padding: 54px 0 72px;
|
||||
}
|
||||
|
||||
.login-shell::before {
|
||||
content: "{{RELAY_GATEWAY_LABEL}}";
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
color: var(--neorelay-muted);
|
||||
font: 700 10px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
letter-spacing: 0.24em;
|
||||
}
|
||||
|
||||
.login-hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(250px, 0.78fr) minmax(360px, 1.35fr);
|
||||
min-height: 390px;
|
||||
margin-bottom: 28px;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid var(--neorelay-violet);
|
||||
border-bottom: 1px solid var(--neorelay-line);
|
||||
background:
|
||||
linear-gradient(115deg, rgba(22, 9, 40, 0.96), rgba(9, 3, 17, 0.78)),
|
||||
linear-gradient(90deg, transparent 49.8%, rgba(85, 231, 255, 0.12) 50%, transparent 50.2%);
|
||||
box-shadow: 20px 20px 0 rgba(72, 24, 122, 0.12);
|
||||
}
|
||||
|
||||
.login-portrait {
|
||||
position: relative;
|
||||
min-height: 390px;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--neorelay-line);
|
||||
}
|
||||
|
||||
.login-portrait::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
content: "";
|
||||
pointer-events: none;
|
||||
background:
|
||||
repeating-linear-gradient(0deg, transparent 0 4px, rgba(7, 3, 13, 0.16) 5px),
|
||||
linear-gradient(180deg, transparent 55%, rgba(7, 3, 13, 0.8));
|
||||
}
|
||||
|
||||
.login-portrait img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 390px;
|
||||
object-fit: cover;
|
||||
object-position: 50% 27%;
|
||||
filter: saturate(1.12) contrast(1.04);
|
||||
}
|
||||
|
||||
.portrait-code {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 14px;
|
||||
bottom: 12px;
|
||||
color: var(--neorelay-acid);
|
||||
font: 700 10px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
|
||||
.login-intro {
|
||||
position: relative;
|
||||
align-self: center;
|
||||
padding: 48px clamp(32px, 7vw, 86px);
|
||||
}
|
||||
|
||||
.login-intro::before {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: 0;
|
||||
width: 46px;
|
||||
height: 3px;
|
||||
content: "";
|
||||
background: var(--neorelay-pink);
|
||||
box-shadow: 19px 8px 0 var(--neorelay-cyan);
|
||||
}
|
||||
|
||||
.login-kicker {
|
||||
margin: 0 0 20px;
|
||||
color: var(--neorelay-acid);
|
||||
font: 700 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
letter-spacing: 0.2em;
|
||||
}
|
||||
|
||||
.login-intro h1 {
|
||||
margin: 0;
|
||||
color: var(--neorelay-text);
|
||||
font-family: "Arial Black", Impact, sans-serif;
|
||||
font-size: clamp(48px, 8vw, 92px);
|
||||
font-weight: 900;
|
||||
line-height: 0.78;
|
||||
letter-spacing: -0.08em;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 4px 4px 0 rgba(111, 44, 255, 0.65);
|
||||
}
|
||||
|
||||
.login-intro h1 span {
|
||||
display: block;
|
||||
margin-left: 0.47em;
|
||||
color: transparent;
|
||||
-webkit-text-stroke: 1px var(--neorelay-pink);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.login-deck {
|
||||
max-width: 520px;
|
||||
margin: 30px 0 25px;
|
||||
color: #cfc3dd;
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.login-status {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-status span {
|
||||
padding: 7px 9px 6px;
|
||||
border: 1px solid rgba(85, 231, 255, 0.35);
|
||||
color: var(--neorelay-cyan);
|
||||
background: rgba(85, 231, 255, 0.04);
|
||||
font: 700 9px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.login-shell .panel-group {
|
||||
display: grid;
|
||||
grid-template-columns: 1.15fr 0.85fr;
|
||||
gap: 1px;
|
||||
padding: 1px;
|
||||
background: var(--neorelay-line);
|
||||
}
|
||||
|
||||
.login-shell .panel {
|
||||
margin: 0 !important;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: var(--neorelay-text);
|
||||
background: rgba(20, 9, 35, 0.97) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-shell .panel:first-of-type,
|
||||
.login-shell .panel:nth-of-type(2) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.login-shell .panel-heading {
|
||||
padding: 17px 20px;
|
||||
border: 0;
|
||||
background: linear-gradient(90deg, rgba(111, 44, 255, 0.18), transparent);
|
||||
}
|
||||
|
||||
.login-shell .panel-title {
|
||||
color: var(--neorelay-text);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.login-shell .panel[data-state=active] .panel-title::before {
|
||||
color: var(--neorelay-acid);
|
||||
}
|
||||
|
||||
.login-shell .panel[data-state=collapsed] .panel-title::before {
|
||||
color: var(--neorelay-pink);
|
||||
}
|
||||
|
||||
.login-shell .panel-body {
|
||||
padding: 24px 20px;
|
||||
border-color: var(--neorelay-line) !important;
|
||||
color: #c9bdd8;
|
||||
}
|
||||
|
||||
.connection-panel .panel-body {
|
||||
padding: clamp(18px, 4vw, 38px);
|
||||
}
|
||||
|
||||
.relay-terminal {
|
||||
border: 1px solid rgba(157, 92, 255, 0.5);
|
||||
background: rgba(5, 2, 10, 0.94);
|
||||
box-shadow: 12px 12px 0 rgba(111, 44, 255, 0.12);
|
||||
font: 700 clamp(12px, 1.8vw, 16px)/1.75 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
.relay-terminal-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--neorelay-line);
|
||||
color: var(--neorelay-muted);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.relay-terminal-bar span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--neorelay-pink);
|
||||
}
|
||||
|
||||
.relay-terminal-bar span:nth-child(2) { background: var(--neorelay-violet); }
|
||||
.relay-terminal-bar span:nth-child(3) { background: var(--neorelay-acid); }
|
||||
.relay-terminal-bar b { margin-left: 6px; font-weight: 700; }
|
||||
.relay-terminal-body { padding: clamp(20px, 4vw, 34px); }
|
||||
.relay-terminal-body p { margin: 0 0 8px; color: #c8c2cf; }
|
||||
.relay-prompt { color: var(--neorelay-violet); }
|
||||
|
||||
.relay-terminal dl {
|
||||
margin: 22px 0;
|
||||
border-top: 1px solid var(--neorelay-line);
|
||||
}
|
||||
|
||||
.relay-terminal dl div {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(100px, 0.35fr) 1fr;
|
||||
gap: 18px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid rgba(157, 92, 255, 0.16);
|
||||
}
|
||||
|
||||
.relay-terminal dt {
|
||||
color: var(--neorelay-muted);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
|
||||
.relay-terminal dd {
|
||||
margin: 0;
|
||||
color: var(--neorelay-cyan);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.relay-password-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.relay-password-line label {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
color: #c8c2cf;
|
||||
font: inherit;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.relay-password-field {
|
||||
position: relative;
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
min-width: 180px;
|
||||
min-height: 31px;
|
||||
padding: 2px 10px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid rgba(85, 231, 255, 0.34);
|
||||
color: var(--neorelay-acid);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.relay-password-field:focus-within {
|
||||
border-color: var(--neorelay-cyan);
|
||||
box-shadow: 0 5px 14px rgba(85, 231, 255, 0.08);
|
||||
}
|
||||
|
||||
.relay-password-placeholder { color: #71687d; font-weight: 400; }
|
||||
.relay-password-mask { letter-spacing: 0.14em; }
|
||||
|
||||
body .relay-password-field input[type="password"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0 !important;
|
||||
opacity: 0;
|
||||
cursor: text;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.relay-password-error { margin-top: 10px !important; color: var(--neorelay-pink) !important; }
|
||||
.relay-input-line { margin-top: 18px !important; }
|
||||
.relay-cursor { color: var(--neorelay-acid); animation: relay-blink 1s steps(1, end) infinite; }
|
||||
|
||||
.connection-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 22px;
|
||||
margin: 30px 0 22px;
|
||||
color: #c9bdd8;
|
||||
font: 700 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
.connection-options label { margin: 0; cursor: pointer; }
|
||||
.connection-options input { margin: 0 7px 0 0; accent-color: var(--neorelay-pink); }
|
||||
.connection-options input:disabled { opacity: 1; cursor: default; }
|
||||
.relay-connect { min-width: 180px; }
|
||||
|
||||
.login-shell .form-control,
|
||||
body .form-control,
|
||||
body input[type="text"],
|
||||
body input[type="password"],
|
||||
body #sendMessage {
|
||||
border: 1px solid rgba(157, 92, 255, 0.38) !important;
|
||||
border-radius: 0 !important;
|
||||
color: var(--neorelay-text) !important;
|
||||
background: rgba(4, 1, 9, 0.65) !important;
|
||||
box-shadow: inset 3px 0 0 rgba(157, 92, 255, 0.25) !important;
|
||||
}
|
||||
|
||||
body .form-control:focus,
|
||||
body input:focus,
|
||||
body #sendMessage:focus {
|
||||
border-color: var(--neorelay-cyan) !important;
|
||||
box-shadow: inset 3px 0 0 var(--neorelay-cyan), 0 0 18px rgba(85, 231, 255, 0.12) !important;
|
||||
}
|
||||
|
||||
@keyframes relay-blink {
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.login-shell .btn-primary {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
color: #09020f;
|
||||
background: linear-gradient(90deg, var(--neorelay-pink), var(--neorelay-violet));
|
||||
box-shadow: 6px 6px 0 rgba(85, 231, 255, 0.16);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.btn-primary:hover,
|
||||
.btn-primary:focus {
|
||||
color: #09020f;
|
||||
background: linear-gradient(90deg, var(--neorelay-acid), var(--neorelay-cyan));
|
||||
}
|
||||
|
||||
.alert {
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 0;
|
||||
background: rgba(10, 4, 18, 0.95);
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #ff8bcf !important;
|
||||
border-color: rgba(255, 101, 200, 0.55);
|
||||
background: rgba(83, 13, 60, 0.36) !important;
|
||||
}
|
||||
|
||||
/* Connected client */
|
||||
body #topbar {
|
||||
border-bottom: 1px solid var(--neorelay-violet);
|
||||
background: linear-gradient(90deg, #1b0a30, #0a0411 65%) !important;
|
||||
box-shadow: 0 8px 24px rgba(3, 0, 8, 0.45);
|
||||
}
|
||||
|
||||
body #topbar .brand img {
|
||||
border-radius: 50%;
|
||||
filter: saturate(1.15);
|
||||
}
|
||||
|
||||
body #topbar .actions {
|
||||
color: var(--neorelay-muted);
|
||||
background: rgba(23, 9, 38, 0.9) !important;
|
||||
}
|
||||
|
||||
body #sidebar,
|
||||
body #nicklist {
|
||||
border-color: var(--neorelay-line);
|
||||
background: rgba(12, 5, 21, 0.98) !important;
|
||||
}
|
||||
|
||||
body #sidebar { display:flex; flex-direction:column; overflow:hidden; }
|
||||
body #sidebar > ul.nav { flex:1 1 auto; min-height:0; margin-bottom:0; overflow-y:auto; }
|
||||
body #nicklist { right:var(--neorelay-page-gutter); box-sizing:border-box; }
|
||||
body #bufferlines.withnicklist { margin-right:105px !important; }
|
||||
body .footer.withnicklist { padding-right:105px; }
|
||||
body #bufferlines > table { width:calc(100% - 10px); margin-right:var(--neorelay-page-gutter); margin-left:var(--neorelay-page-gutter); }
|
||||
body .footer > [input-bar] { display:block; margin-right:var(--neorelay-page-gutter); margin-left:var(--neorelay-page-gutter); }
|
||||
|
||||
#sidebar-resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 969px) {
|
||||
body #sidebar {
|
||||
width: var(--neorelay-sidebar-width);
|
||||
}
|
||||
|
||||
body #topbar .title {
|
||||
left: calc(var(--neorelay-sidebar-width) + 5px);
|
||||
}
|
||||
|
||||
body .content[sidebar-state="visible"] #bufferlines {
|
||||
margin-left: calc(var(--neorelay-sidebar-width) + 5px);
|
||||
}
|
||||
|
||||
body .content[sidebar-state="visible"] .footer {
|
||||
padding-left: calc(var(--neorelay-sidebar-width) + 5px);
|
||||
}
|
||||
|
||||
body .content[sidebar-state="visible"] #sidebar-resizer {
|
||||
position: fixed;
|
||||
z-index: 3;
|
||||
top: 35px;
|
||||
bottom: 0;
|
||||
left: calc(var(--neorelay-sidebar-width) - 4px);
|
||||
display: block;
|
||||
width: 9px;
|
||||
cursor: col-resize;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
body #sidebar .nav-pills > li.buffer > a {
|
||||
padding-top: 2.5px;
|
||||
padding-bottom: 2.5px;
|
||||
}
|
||||
|
||||
body.neorelay-sidebar-resizing,
|
||||
body.neorelay-sidebar-resizing * {
|
||||
cursor: col-resize !important;
|
||||
user-select: none !important;
|
||||
}
|
||||
|
||||
body.neorelay-sidebar-resizing #sidebar,
|
||||
body.neorelay-sidebar-resizing #bufferlines,
|
||||
body.neorelay-sidebar-resizing .footer {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
:root {
|
||||
--neorelay-sidebar-preferred-width: 200px;
|
||||
}
|
||||
|
||||
body #sidebar .nav-pills > li.buffer > a {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
body #bufferlines.withnicklist { margin-right:145px !important; }
|
||||
body .footer.withnicklist { padding-right:153px !important; }
|
||||
}
|
||||
|
||||
body .nav-pills li:nth-child(2n) {
|
||||
background: rgba(157, 92, 255, 0.035);
|
||||
}
|
||||
|
||||
body .nav-pills > li > a {
|
||||
color: #cbbdde;
|
||||
}
|
||||
|
||||
body .nav-pills > li.active > a,
|
||||
body .nav-pills > li.active > a:hover {
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, rgba(111, 44, 255, 0.65), rgba(255, 101, 200, 0.15)) !important;
|
||||
box-shadow: inset 3px 0 0 var(--neorelay-pink);
|
||||
}
|
||||
|
||||
body .nav-pills > li.highlight > a,
|
||||
body .nav-pills > li.highlight > a span {
|
||||
color: #08030d;
|
||||
background: var(--neorelay-acid) !important;
|
||||
}
|
||||
|
||||
.sidebar-actions { position:static; flex:0 0 auto; display:grid; grid-template-columns:repeat(auto-fit,minmax(48px,1fr)); gap:1px; margin-top:0; border-top:1px solid var(--neorelay-line); background:var(--neorelay-void); }
|
||||
.sidebar-actions button { min-width:0; padding:9px 3px; border:0; color:var(--neorelay-muted); background:rgba(21,10,36,.96); font:700 9px/1.2 ui-monospace,monospace; }
|
||||
.sidebar-actions button:hover { color:var(--neorelay-cyan); background:rgba(111,44,255,.28); }
|
||||
.sidebar-actions i,.sidebar-actions span { display:block; margin:auto; }
|
||||
.sidebar-actions span { margin-top:4px; }
|
||||
|
||||
.neo-context-menu { position:fixed; z-index:1200; display:grid; width:220px; overflow:hidden; border:1px solid var(--neorelay-violet); background:rgba(7,3,13,.96); box-shadow:10px 10px 24px rgba(0,0,0,.5); }
|
||||
.neo-context-menu button { padding:9px 12px; border:0; border-bottom:1px solid rgba(157,92,255,.15); color:var(--neorelay-text); text-align:left; background:transparent; }
|
||||
.neo-context-menu button:hover,.neo-context-menu button:focus { color:var(--neorelay-cyan); background:rgba(111,44,255,.35); outline:0; }
|
||||
.neo-context-menu button.destructive { color:var(--neorelay-pink); }
|
||||
|
||||
.neo-server-manager .modal-dialog { width:min(1100px,calc(100% - 24px)); }
|
||||
.neo-server-layout { display:grid; grid-template-columns:190px 1fr; gap:18px; max-height:72vh; overflow:auto; }
|
||||
.neo-server-layout aside { display:flex; flex-direction:column; gap:4px; }
|
||||
.neo-server-layout aside button { padding:8px; border:1px solid var(--neorelay-line); color:var(--neorelay-text); text-align:left; background:rgba(7,3,13,.7); }
|
||||
.neo-server-layout aside .neo-add-server { box-sizing:border-box; width:100%; min-height:38px; padding:8px 10px; border:1px solid var(--neorelay-cyan); color:var(--neorelay-void); text-align:center; white-space:normal; background:linear-gradient(90deg,var(--neorelay-pink),var(--neorelay-violet)); box-shadow:none; font-size:10px; line-height:20px; }
|
||||
.neo-server-layout aside .neo-add-server:hover,.neo-server-layout aside .neo-add-server:focus { color:var(--neorelay-void); background:linear-gradient(90deg,var(--neorelay-acid),var(--neorelay-cyan)); }
|
||||
.neo-server-options { display:grid; grid-template-columns:repeat(2,minmax(240px,1fr)); gap:8px 14px; margin-top:14px; }
|
||||
.neo-server-options label { display:grid; grid-template-columns:minmax(130px,.8fr) minmax(120px,1.2fr) auto; align-items:center; gap:8px; margin:0; color:var(--neorelay-muted); font:11px/1.3 ui-monospace,monospace; }
|
||||
.neo-server-manager,.neo-server-manager button,.neo-server-manager input,.neo-server-manager select { font-family:"OpenDyslexic","Atkinson Hyperlegible",system-ui,sans-serif; }
|
||||
.neo-server-manager .neo-server-control { box-sizing:border-box; width:100%; min-width:0; height:34px; min-height:34px; margin:0 !important; padding:6px 10px; border:1px solid var(--neorelay-line) !important; color:var(--neorelay-text); background:var(--neorelay-void); font-size:12px; line-height:20px; }
|
||||
.neo-server-manager .modal-footer .btn { min-width:92px; height:34px; padding:7px 13px; font-size:11px; line-height:20px; }
|
||||
.neo-server-manager .modal-footer .btn-default { border:1px solid var(--neorelay-line); color:var(--neorelay-text); background:var(--neorelay-panel-raised); }
|
||||
.neo-server-manager .modal-footer .btn-default:hover,.neo-server-manager .modal-footer .btn-default:focus { color:var(--neorelay-cyan); background:rgba(111,44,255,.28); }
|
||||
.neo-server-options .inherit { white-space:nowrap; }
|
||||
|
||||
#settingsModal .modal-dialog { width:min(900px,calc(100% - 24px)); }
|
||||
.neo-font-picker { position:relative; }
|
||||
.neo-font-trigger { display:flex; align-items:center; justify-content:space-between; width:100%; overflow:hidden; text-align:left; }
|
||||
.neo-font-value { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.neo-font-menu { position:absolute; z-index:1300; top:calc(100% + 4px); left:0; display:grid; width:max(100%,320px); padding:4px; border:1px solid var(--neorelay-violet); background:var(--neorelay-void); box-shadow:10px 10px 24px rgba(0,0,0,.55); }
|
||||
.neo-font-option { display:flex; align-items:center; justify-content:space-between; gap:14px; padding:9px 11px; border:0; border-bottom:1px solid rgba(157,92,255,.18); color:var(--neorelay-text); background:transparent; font-size:16px !important; text-align:left; }
|
||||
.neo-font-option:last-child { border-bottom:0; }
|
||||
.neo-font-option:hover,.neo-font-option:focus { color:var(--neorelay-void); background:linear-gradient(90deg,var(--neorelay-cyan),var(--neorelay-acid)); outline:0; }
|
||||
.neo-font-option span { font-size:.88em !important; }
|
||||
.neo-font-preview { margin:8px 0 0; padding:9px 11px; border:1px solid var(--neorelay-line); color:var(--neorelay-text); background:rgba(7,3,13,.55); }
|
||||
.neo-settings-tabs { display:flex; gap:1px; margin:-15px -15px 18px; padding:1px; background:var(--neorelay-line); }
|
||||
.neo-settings-tabs button { flex:1 1 0; padding:11px 16px; border:0; color:var(--neorelay-muted); background:var(--neorelay-ink); font:800 11px/1 ui-monospace,monospace; letter-spacing:.08em; text-transform:uppercase; }
|
||||
.neo-settings-tabs button.active { color:var(--neorelay-void); background:linear-gradient(90deg,var(--neorelay-cyan),var(--neorelay-acid)); }
|
||||
.neo-settings-panel { margin:0; }
|
||||
.neo-colour-settings h5,.neo-weechat-settings h5 { margin:20px 0 9px; color:var(--neorelay-cyan); font:800 11px/1 ui-monospace,monospace; letter-spacing:.1em; text-transform:uppercase; }
|
||||
.neo-colour-settings p,.neo-weechat-settings p { color:var(--neorelay-muted); }
|
||||
.neo-settings-toolbar { display:flex; align-items:end; gap:10px; margin-bottom:12px; }
|
||||
.neo-settings-toolbar p { flex:1 1 auto; margin:0; }
|
||||
.neo-settings-toolbar label { flex:1 1 220px; margin:0; color:var(--neorelay-muted); font-size:11px; }
|
||||
.neo-settings-toolbar .form-control { height:34px; margin:4px 0 0 !important; }
|
||||
.neo-colour-grid { display:grid; grid-template-columns:repeat(2,minmax(250px,1fr)); gap:8px; }
|
||||
.neo-colour-grid label { display:grid; grid-template-columns:minmax(110px,1fr) minmax(120px,.8fr) 28px; align-items:center; gap:8px 10px; margin:0; padding:8px 10px; border:1px solid rgba(157,92,255,.2); background:rgba(7,3,13,.45); color:var(--neorelay-text); font-weight:600; }
|
||||
.neo-colour-grid .form-control { height:34px; margin:0 !important; }
|
||||
.neo-colour-grid code { grid-column:1 / -1; overflow:hidden; padding:0; color:var(--neorelay-muted); background:transparent; font-size:9px; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.neo-colour-swatch { display:block; width:26px; height:26px; border:1px solid rgba(255,255,255,.45); box-shadow:inset 0 0 0 1px rgba(0,0,0,.35); }
|
||||
.neo-option-list { display:grid; gap:7px; max-height:55vh; overflow:auto; }
|
||||
.neo-option { display:grid; grid-template-columns:minmax(260px,1fr) minmax(160px,.65fr) auto; align-items:center; gap:10px; padding:10px; border:1px solid rgba(157,92,255,.2); background:rgba(7,3,13,.45); }
|
||||
.neo-option strong,.neo-option small { display:block; overflow-wrap:anywhere; }
|
||||
.neo-option strong { color:var(--neorelay-cyan); font:700 10px/1.3 ui-monospace,monospace; }
|
||||
.neo-option small { margin-top:3px; color:var(--neorelay-muted); font-size:9px; }
|
||||
.neo-option .form-control { height:34px; margin:0 !important; }
|
||||
.neo-option-actions { display:flex; gap:5px; }
|
||||
.neo-option-actions .btn { min-width:64px; height:34px; }
|
||||
.neo-palette-setting { display:grid; grid-template-columns:1fr auto; gap:8px; padding:10px; border:1px solid rgba(157,92,255,.2); background:rgba(7,3,13,.45); }
|
||||
.neo-palette-setting .form-control { height:34px; margin:0 !important; }
|
||||
.neo-palette-setting code { grid-column:1 / -1; color:var(--neorelay-muted); background:transparent; font-size:9px; }
|
||||
.neo-palette-preview { grid-column:1 / -1; display:flex; align-items:center; gap:3px; min-height:20px; overflow-x:auto; }
|
||||
.neo-palette-preview i { flex:0 0 20px; width:20px; height:20px; border:1px solid rgba(255,255,255,.4); box-shadow:inset 0 0 0 1px rgba(0,0,0,.3); }
|
||||
|
||||
body #nicklist a { cursor:text; }
|
||||
body .neo-nick { color:var(--neorelay-nick) !important; }
|
||||
body .neo-nick-op { color:var(--neorelay-nick-op) !important; }
|
||||
body .neo-nick-halfop { color:var(--neorelay-nick-halfop) !important; }
|
||||
body .neo-nick-voice { color:var(--neorelay-nick-voice) !important; }
|
||||
body .neo-nick-away { color:var(--neorelay-nick-away) !important; font-style:italic; opacity:.85; }
|
||||
body #nicklist .cof-chat_nick_self { color:var(--neorelay-nick-self) !important; font-style:normal; font-weight:900; opacity:1; text-shadow:1px 1px 0 var(--neorelay-purple),0 0 8px rgba(157,92,255,.45); }
|
||||
|
||||
body tr.bufferline:hover {
|
||||
background: rgba(157, 92, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
body #bufferlines tr.bufferline:has(td.prefix .highlight) {
|
||||
background: var(--neorelay-chat-highlight-bg) !important;
|
||||
box-shadow: inset 3px 0 0 var(--neorelay-pink);
|
||||
}
|
||||
|
||||
body #bufferlines td.prefix .highlight {
|
||||
color: var(--neorelay-chat-highlight) !important;
|
||||
font-weight: 900;
|
||||
text-shadow: 0 0 8px rgba(255, 101, 200, 0.48);
|
||||
}
|
||||
|
||||
body td.prefix {
|
||||
border-color: rgba(157, 92, 255, 0.2);
|
||||
}
|
||||
|
||||
body .footer {
|
||||
bottom: 5px;
|
||||
padding-bottom: 0;
|
||||
border-top: 1px solid rgba(157, 92, 255, 0.35);
|
||||
background: var(--neorelay-ink);
|
||||
}
|
||||
|
||||
body #bufferlines { bottom:40px; }
|
||||
|
||||
.command-menu {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
right: 0;
|
||||
bottom: calc(100% + 4px);
|
||||
left: 0;
|
||||
max-height: min(40dvh, 20rem);
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--neorelay-line);
|
||||
background: rgba(7, 3, 13, 0.90);
|
||||
box-shadow: 8px -8px 24px rgba(3, 0, 8, 0.45);
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.command-menu-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(9rem, 0.3fr) minmax(12rem, 0.7fr) minmax(14rem, 1fr);
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(157, 92, 255, 0.14);
|
||||
color: var(--neorelay-text);
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.command-menu-item:hover,
|
||||
.command-menu-item.selected {
|
||||
background: linear-gradient(90deg, rgba(111, 44, 255, 0.55), rgba(255, 101, 200, 0.08));
|
||||
box-shadow: inset 3px 0 0 var(--neorelay-cyan);
|
||||
}
|
||||
|
||||
.command-menu-name {
|
||||
color: var(--neorelay-cyan);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.command-menu-args {
|
||||
color: #8f8699;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.command-menu-description {
|
||||
overflow: hidden;
|
||||
color: var(--neorelay-muted);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.command-menu-empty {
|
||||
padding: 10px;
|
||||
color: var(--neorelay-muted);
|
||||
}
|
||||
|
||||
body .modal-content,
|
||||
body .dropdown-menu {
|
||||
border: 1px solid var(--neorelay-line);
|
||||
border-radius: 0;
|
||||
color: var(--neorelay-text);
|
||||
background: var(--neorelay-panel) !important;
|
||||
box-shadow: 12px 12px 0 rgba(111, 44, 255, 0.15);
|
||||
}
|
||||
|
||||
body .badge.danger,
|
||||
body .danger {
|
||||
color: #09020f;
|
||||
background: var(--neorelay-pink) !important;
|
||||
}
|
||||
|
||||
body #readmarker {
|
||||
border-top-color: var(--neorelay-pink);
|
||||
border-bottom-color: var(--neorelay-cyan);
|
||||
}
|
||||
|
||||
.neorelay-core-title {
|
||||
color: var(--neorelay-text);
|
||||
font: 800 12px/35px "OpenDyslexic", ui-monospace, monospace;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.neorelay-core-title small {
|
||||
margin-left: 8px;
|
||||
color: var(--neorelay-acid);
|
||||
font: 700 9px/35px ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
|
||||
.core-login-hero {
|
||||
width: min(960px, calc(100% - 32px));
|
||||
margin: 6vh auto 60px;
|
||||
}
|
||||
|
||||
body .cof-chat_nick,
|
||||
body .cof-chat_host,
|
||||
body .cof-chat_value {
|
||||
color: var(--neorelay-nick);
|
||||
}
|
||||
|
||||
body .cof-chat_nick_other { color:var(--neorelay-nick-other); }
|
||||
|
||||
body .cof-chat_prefix_join,
|
||||
body .cof-chat_nick_prefix {
|
||||
color: var(--neorelay-acid);
|
||||
}
|
||||
|
||||
body .cof-chat_nick_self {
|
||||
color: var(--neorelay-nick-self);
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
opacity: 1;
|
||||
text-shadow: 1px 1px 0 var(--neorelay-purple), 0 0 8px rgba(157, 92, 255, 0.45);
|
||||
}
|
||||
|
||||
body .cof-chat_time { color:var(--neorelay-chat-time); }
|
||||
|
||||
body .cof-chat_prefix_network,
|
||||
body .cof-chat_prefix_more,
|
||||
body .cof-chat_read_marker {
|
||||
color: var(--neorelay-pink);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background: var(--neorelay-void) !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(var(--neorelay-purple), var(--neorelay-pink)) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.login-shell {
|
||||
width: min(100% - 24px, 560px);
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.neo-server-layout { grid-template-columns:1fr; }
|
||||
.neo-server-options { grid-template-columns:1fr; }
|
||||
.neo-colour-grid { grid-template-columns:1fr; }
|
||||
.neo-settings-toolbar { align-items:stretch; flex-direction:column; }
|
||||
.neo-settings-toolbar label { width:100%; flex-basis:auto; }
|
||||
.neo-option { grid-template-columns:1fr; }
|
||||
.neo-option-actions .btn { flex:1 1 auto; }
|
||||
|
||||
.login-hero {
|
||||
grid-template-columns: 1fr;
|
||||
box-shadow: 10px 10px 0 rgba(72, 24, 122, 0.12);
|
||||
}
|
||||
|
||||
.login-portrait,
|
||||
.login-portrait img {
|
||||
min-height: 300px;
|
||||
max-height: 380px;
|
||||
}
|
||||
|
||||
.login-portrait {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--neorelay-line);
|
||||
}
|
||||
|
||||
.login-intro {
|
||||
padding: 42px 24px 36px;
|
||||
}
|
||||
|
||||
.login-intro h1 {
|
||||
font-size: clamp(48px, 18vw, 76px);
|
||||
}
|
||||
|
||||
.login-shell .panel-group {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.login-shell .panel + .panel {
|
||||
border-top: 1px solid var(--neorelay-line);
|
||||
}
|
||||
|
||||
.connection-panel .panel-body { padding: 14px; }
|
||||
.relay-terminal { box-shadow: 8px 8px 0 rgba(111, 44, 255, 0.1); }
|
||||
.relay-password-line { display: block; }
|
||||
.relay-password-field { margin-top: 5px; }
|
||||
.connection-options { display: grid; }
|
||||
.relay-connect { width: 100%; }
|
||||
|
||||
.command-menu {
|
||||
max-height: min(35dvh, 16rem);
|
||||
}
|
||||
|
||||
.command-menu-item {
|
||||
grid-template-columns: minmax(8rem, 0.45fr) 1fr;
|
||||
min-height: 44px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.command-menu-description {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.neorelay-core-title small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.core-login-hero {
|
||||
margin: 32px 12px 60px;
|
||||
width: calc(100% - 24px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 968px) {
|
||||
body .footer.withnicklist { padding-right:113px !important; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<li class="standard-labels">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div class="form-group">
|
||||
<label for="drop-profile" class="col-sm-3 control-label make-thinner">NeoDrop retention</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="drop-profile" class="form-control" ng-model="settings.dropProfile">
|
||||
<option value="small">50 MiB maximum / 30 days</option>
|
||||
<option value="large">1 GiB maximum / 7 days</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
@@ -0,0 +1,94 @@
|
||||
Copyright (c) 2019-07-29, Abbie Gonzalez (https://abbiecod.es|support@abbiecod.es),
|
||||
with Reserved Font Name OpenDyslexic.
|
||||
Copyright (c) 12/2012 - 2019
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,16 @@
|
||||
<section class="login-hero" aria-labelledby="neorelay-login-title">
|
||||
<div class="login-portrait">
|
||||
<img src="assets/neorelay-avatar-login.jpg" alt="Operator-provided avatar">
|
||||
<span class="portrait-code" aria-hidden="true">{{NODE_LABEL}}</span>
|
||||
</div>
|
||||
<div class="login-intro">
|
||||
<p class="login-kicker">PRIVATE RELAY // TLS 1.3</p>
|
||||
<h1 id="neorelay-login-title">{{RELAY_TITLE_HTML}}</h1>
|
||||
<p class="login-deck">A quiet line into IRC.</p>
|
||||
<div class="login-status" aria-label="Service capabilities">
|
||||
<span>INTERNAL ONLY</span>
|
||||
<span>SASL EXTERNAL</span>
|
||||
<span>ALWAYS ON</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,326 @@
|
||||
(function(root, factory) {
|
||||
'use strict';
|
||||
var api = factory();
|
||||
if (typeof module === 'object' && module.exports) module.exports = api;
|
||||
else root.NeoDropCrypto = api;
|
||||
}(typeof self !== 'undefined' ? self : this, function() {
|
||||
'use strict';
|
||||
|
||||
var CHUNK_SIZE = 4 * 1024 * 1024;
|
||||
var SMALL_MAX = 50 * 1024 * 1024;
|
||||
var LARGE_MAX = 1024 * 1024 * 1024;
|
||||
var encoder = new TextEncoder();
|
||||
var SHA256_K = new Uint32Array([
|
||||
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
||||
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
||||
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
||||
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
||||
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
||||
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
||||
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
||||
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
|
||||
]);
|
||||
|
||||
function rotate(value, bits) {
|
||||
return (value >>> bits) | (value << (32 - bits));
|
||||
}
|
||||
|
||||
function Sha256() {
|
||||
this.state = new Uint32Array([
|
||||
0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,
|
||||
0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19
|
||||
]);
|
||||
this.buffer = new Uint8Array(64);
|
||||
this.bufferLength = 0;
|
||||
this.bytes = 0;
|
||||
}
|
||||
|
||||
Sha256.prototype.transform = function(bytes, offset) {
|
||||
var words = new Uint32Array(64);
|
||||
var view = new DataView(bytes.buffer, bytes.byteOffset + offset, 64);
|
||||
var i;
|
||||
for (i = 0; i < 16; i++) words[i] = view.getUint32(i * 4, false);
|
||||
for (i = 16; i < 64; i++) {
|
||||
var x = words[i - 15];
|
||||
var y = words[i - 2];
|
||||
var s0 = rotate(x, 7) ^ rotate(x, 18) ^ (x >>> 3);
|
||||
var s1 = rotate(y, 17) ^ rotate(y, 19) ^ (y >>> 10);
|
||||
words[i] = (words[i - 16] + s0 + words[i - 7] + s1) >>> 0;
|
||||
}
|
||||
var a = this.state[0], b = this.state[1], c = this.state[2], d = this.state[3];
|
||||
var e = this.state[4], f = this.state[5], g = this.state[6], h = this.state[7];
|
||||
for (i = 0; i < 64; i++) {
|
||||
var sum1 = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25);
|
||||
var choice = (e & f) ^ (~e & g);
|
||||
var temporary1 = (h + sum1 + choice + SHA256_K[i] + words[i]) >>> 0;
|
||||
var sum0 = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22);
|
||||
var majority = (a & b) ^ (a & c) ^ (b & c);
|
||||
var temporary2 = (sum0 + majority) >>> 0;
|
||||
h = g; g = f; f = e; e = (d + temporary1) >>> 0;
|
||||
d = c; c = b; b = a; a = (temporary1 + temporary2) >>> 0;
|
||||
}
|
||||
this.state[0] = (this.state[0] + a) >>> 0;
|
||||
this.state[1] = (this.state[1] + b) >>> 0;
|
||||
this.state[2] = (this.state[2] + c) >>> 0;
|
||||
this.state[3] = (this.state[3] + d) >>> 0;
|
||||
this.state[4] = (this.state[4] + e) >>> 0;
|
||||
this.state[5] = (this.state[5] + f) >>> 0;
|
||||
this.state[6] = (this.state[6] + g) >>> 0;
|
||||
this.state[7] = (this.state[7] + h) >>> 0;
|
||||
};
|
||||
|
||||
Sha256.prototype.update = function(bytes) {
|
||||
this.bytes += bytes.length;
|
||||
var offset = 0;
|
||||
if (this.bufferLength) {
|
||||
var copied = Math.min(64 - this.bufferLength, bytes.length);
|
||||
this.buffer.set(bytes.subarray(0, copied), this.bufferLength);
|
||||
this.bufferLength += copied;
|
||||
offset = copied;
|
||||
if (this.bufferLength === 64) {
|
||||
this.transform(this.buffer, 0);
|
||||
this.bufferLength = 0;
|
||||
}
|
||||
}
|
||||
while (offset + 64 <= bytes.length) {
|
||||
this.transform(bytes, offset);
|
||||
offset += 64;
|
||||
}
|
||||
if (offset < bytes.length) {
|
||||
this.buffer.set(bytes.subarray(offset), 0);
|
||||
this.bufferLength = bytes.length - offset;
|
||||
}
|
||||
};
|
||||
|
||||
Sha256.prototype.hex = function() {
|
||||
var high = Math.floor(this.bytes / 0x20000000);
|
||||
var low = (this.bytes * 8) >>> 0;
|
||||
this.buffer[this.bufferLength++] = 0x80;
|
||||
if (this.bufferLength > 56) {
|
||||
this.buffer.fill(0, this.bufferLength);
|
||||
this.transform(this.buffer, 0);
|
||||
this.bufferLength = 0;
|
||||
}
|
||||
this.buffer.fill(0, this.bufferLength, 56);
|
||||
var view = new DataView(this.buffer.buffer);
|
||||
view.setUint32(56, high, false);
|
||||
view.setUint32(60, low, false);
|
||||
this.transform(this.buffer, 0);
|
||||
var result = '';
|
||||
for (var i = 0; i < this.state.length; i++) {
|
||||
result += this.state[i].toString(16).padStart(8, '0');
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
function b64url(bytes) {
|
||||
var binary = '';
|
||||
for (var i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
function derive(secret, info, length) {
|
||||
return crypto.subtle.importKey('raw', secret, 'HKDF', false, ['deriveBits']).then(function(key) {
|
||||
return crypto.subtle.deriveBits({
|
||||
name: 'HKDF', hash: 'SHA-256',
|
||||
salt: encoder.encode('{{NEODROP_CRYPTO_CONTEXT}}'),
|
||||
info: encoder.encode(info)
|
||||
}, key, length * 8);
|
||||
}).then(function(bits) { return new Uint8Array(bits); });
|
||||
}
|
||||
|
||||
function importAes(raw) {
|
||||
return crypto.subtle.importKey('raw', raw, {name: 'AES-GCM'}, false, ['encrypt']);
|
||||
}
|
||||
|
||||
function setU32(bytes, offset, value) {
|
||||
new DataView(bytes.buffer).setUint32(offset, value, false);
|
||||
}
|
||||
|
||||
function setU64(bytes, offset, value) {
|
||||
new DataView(bytes.buffer).setBigUint64(offset, BigInt(value), false);
|
||||
}
|
||||
|
||||
function makeAad(type, profile, objectId, size, count, index, length) {
|
||||
var bytes = new Uint8Array(60);
|
||||
bytes.set(encoder.encode('NEODRP01'));
|
||||
bytes[8] = type;
|
||||
bytes[9] = profile;
|
||||
bytes.set(objectId, 12);
|
||||
setU64(bytes, 36, size);
|
||||
setU32(bytes, 44, CHUNK_SIZE);
|
||||
setU32(bytes, 48, count);
|
||||
setU32(bytes, 52, index);
|
||||
setU32(bytes, 56, length);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function makeIv(type, index) {
|
||||
var bytes = new Uint8Array(12);
|
||||
setU32(bytes, 0, type);
|
||||
setU64(bytes, 4, index);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function encrypt(key, plaintext, nonce, additionalData) {
|
||||
return crypto.subtle.encrypt({
|
||||
name: 'AES-GCM', iv: nonce, additionalData: additionalData, tagLength: 128
|
||||
}, key, plaintext);
|
||||
}
|
||||
|
||||
function typeFor(type, name) {
|
||||
var value = String(type || '').toLowerCase();
|
||||
if (value && value !== 'application/octet-stream') return value;
|
||||
var match = String(name || '').toLowerCase().match(/\.([a-z0-9]+)$/);
|
||||
var types = {
|
||||
png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif',
|
||||
webp: 'image/webp', avif: 'image/avif', flac: 'audio/flac', m4a: 'audio/mp4',
|
||||
mp3: 'audio/mpeg', oga: 'audio/ogg', ogg: 'audio/ogg', opus: 'audio/opus',
|
||||
wav: 'audio/wav', wave: 'audio/wav', m4v: 'video/mp4', mkv: 'video/x-matroska',
|
||||
mov: 'video/quicktime', mp4: 'video/mp4', ogv: 'video/ogg', webm: 'video/webm'
|
||||
};
|
||||
return match && types[match[1]] ? types[match[1]] : 'application/octet-stream';
|
||||
}
|
||||
|
||||
function kindFor(type) {
|
||||
if (/^image\/(png|jpeg|gif|webp|avif)$/i.test(type)) return 'image';
|
||||
if (/^audio\//i.test(type)) return 'audio';
|
||||
if (/^video\//i.test(type)) return 'video';
|
||||
if (/^text\//i.test(type) || /^(application\/(json|javascript|x-.*script))$/i.test(type)) return 'text';
|
||||
return 'binary';
|
||||
}
|
||||
|
||||
function safeName(name) {
|
||||
var value = String(name || 'drop.bin').split(/[\\/]/).pop()
|
||||
.replace(/[\u0000-\u001f\u007f]/g, '').replace(/^\.+|\.+$/g, '').slice(0, 240);
|
||||
return value || 'drop.bin';
|
||||
}
|
||||
|
||||
function request(base, path, options, jsonResponse) {
|
||||
return fetch(base + path, options).then(function(response) {
|
||||
if (!response.ok) throw new Error('NeoDrop request failed');
|
||||
return jsonResponse ? response.json() : response;
|
||||
}).catch(function(error) {
|
||||
if (error && error.name === 'AbortError') throw error;
|
||||
throw new Error('NeoDrop request failed');
|
||||
});
|
||||
}
|
||||
|
||||
async function upload(file, options) {
|
||||
options = options || {};
|
||||
if (!file || typeof file.size !== 'number') throw new Error('Select a file.');
|
||||
var profile = Number(options.profile);
|
||||
if (profile !== 0 && profile !== 1) throw new Error('Invalid retention profile.');
|
||||
var maximum = profile === 0 ? SMALL_MAX : LARGE_MAX;
|
||||
if (file.size > maximum) throw new Error('File exceeds the selected profile limit.');
|
||||
|
||||
var base = String(options.base || '');
|
||||
var progress = typeof options.onProgress === 'function' ? options.onProgress : function() {};
|
||||
var signal = options.signal;
|
||||
var secret = crypto.getRandomValues(new Uint8Array(32));
|
||||
var objectId;
|
||||
var objectIdText;
|
||||
var uploadSession;
|
||||
var chunks = file.size ? Math.ceil(file.size / CHUNK_SIZE) : 0;
|
||||
var totalSteps = chunks + 3;
|
||||
var completed = 0;
|
||||
|
||||
try {
|
||||
var keys = await Promise.all([
|
||||
derive(secret, 'object-id', 24),
|
||||
derive(secret, 'content-key', 32).then(importAes),
|
||||
derive(secret, 'metadata-key', 32).then(importAes)
|
||||
]);
|
||||
objectId = keys[0];
|
||||
objectIdText = b64url(objectId);
|
||||
uploadSession = await request(base, '/api/v1/uploads', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
v: 1, id: objectIdText, profile: profile, size: file.size,
|
||||
chunkSize: CHUNK_SIZE, chunks: chunks
|
||||
}),
|
||||
signal: signal
|
||||
}, true);
|
||||
if (!uploadSession || !/^[A-Za-z0-9_-]{22}$/.test(uploadSession.uploadId || '') ||
|
||||
!/^[A-Za-z0-9_-]{43}$/.test(uploadSession.uploadToken || '')) {
|
||||
throw new Error('NeoDrop request failed');
|
||||
}
|
||||
progress(++completed / totalSteps);
|
||||
|
||||
var authorization = 'Bearer ' + uploadSession.uploadToken;
|
||||
var hasher = new Sha256();
|
||||
for (var index = 0; index < chunks; index++) {
|
||||
var plaintext = new Uint8Array(await file.slice(
|
||||
index * CHUNK_SIZE, Math.min(file.size, (index + 1) * CHUNK_SIZE)
|
||||
).arrayBuffer());
|
||||
hasher.update(plaintext);
|
||||
var cipher = await encrypt(
|
||||
keys[1], plaintext, makeIv(1, index),
|
||||
makeAad(1, profile, objectId, file.size, chunks, index, plaintext.byteLength)
|
||||
);
|
||||
await request(base, '/api/v1/uploads/' + uploadSession.uploadId + '/chunks/' + index, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/octet-stream', 'Authorization': authorization},
|
||||
body: cipher,
|
||||
signal: signal
|
||||
}, false);
|
||||
progress(++completed / totalSteps);
|
||||
}
|
||||
|
||||
var metadataName = safeName(file.name);
|
||||
var metadataValue = {
|
||||
v: 1,
|
||||
name: metadataName,
|
||||
type: typeFor(file.type, metadataName).slice(0, 255),
|
||||
size: file.size,
|
||||
lastModified: Number.isSafeInteger(file.lastModified) ? file.lastModified : null,
|
||||
sha256: hasher.hex()
|
||||
};
|
||||
var metadata = encoder.encode(JSON.stringify(metadataValue));
|
||||
if (metadata.length > 4096) throw new Error('File metadata is too large.');
|
||||
var encryptedMetadata = await encrypt(
|
||||
keys[2], metadata, makeIv(2, 0),
|
||||
makeAad(0, profile, objectId, file.size, chunks, 0xffffffff, metadata.length)
|
||||
);
|
||||
await request(base, '/api/v1/uploads/' + uploadSession.uploadId + '/metadata', {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/octet-stream', 'Authorization': authorization},
|
||||
body: encryptedMetadata,
|
||||
signal: signal
|
||||
}, false);
|
||||
progress(++completed / totalSteps);
|
||||
|
||||
var committed = await request(base, '/api/v1/uploads/' + uploadSession.uploadId + '/commit', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'Authorization': authorization},
|
||||
body: '{}',
|
||||
signal: signal
|
||||
}, true);
|
||||
if (!committed || !Number.isSafeInteger(committed.expiresAt)) {
|
||||
throw new Error('NeoDrop request failed');
|
||||
}
|
||||
progress(1);
|
||||
return {
|
||||
id: objectIdText,
|
||||
readUrl: base + '/d/' + objectIdText + '?k=' + kindFor(metadataValue.type) + '#d1.' + b64url(secret),
|
||||
deleteCapability: objectIdText + '.' + uploadSession.uploadToken,
|
||||
name: metadataValue.name,
|
||||
type: metadataValue.type,
|
||||
size: file.size,
|
||||
sha256: metadataValue.sha256,
|
||||
profile: profile,
|
||||
expiresAt: committed.expiresAt
|
||||
};
|
||||
} finally {
|
||||
secret.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
CHUNK_SIZE: CHUNK_SIZE,
|
||||
SMALL_MAX: SMALL_MAX,
|
||||
LARGE_MAX: LARGE_MAX,
|
||||
upload: upload
|
||||
};
|
||||
}));
|
||||
@@ -0,0 +1,27 @@
|
||||
function neoDropFrame(url, kind) {
|
||||
return function() {
|
||||
var element = this.getElement();
|
||||
var frame = document.createElement('iframe');
|
||||
frame.src = url.replace('?k=', '?embed=1&k=');
|
||||
frame.title = 'Encrypted NeoDrop ' + kind + ' preview';
|
||||
frame.width = '100%';
|
||||
frame.height = kind === 'audio' ? '80' : kind === 'text' ? '420' : '520';
|
||||
frame.loading = 'lazy';
|
||||
frame.referrerPolicy = 'no-referrer';
|
||||
frame.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-downloads');
|
||||
frame.setAttribute('frameborder', '0');
|
||||
element.replaceChildren(frame);
|
||||
};
|
||||
}
|
||||
|
||||
var neoDropMediaPlugin = new UrlPlugin('NeoDrop media', function(url) {
|
||||
var match = url.match(/^https:\/\/{{DROPS_HOST_REGEX}}\/d\/([A-Za-z0-9_-]{32})\?k=(image|audio|video)#d1\.([A-Za-z0-9_-]{43})$/);
|
||||
return match ? neoDropFrame(url, match[2]) : null;
|
||||
});
|
||||
neoDropMediaPlugin.exclusive = true;
|
||||
|
||||
var neoDropTextPlugin = new UrlPlugin('NeoDrop preview', function(url) {
|
||||
var match = url.match(/^https:\/\/{{DROPS_HOST_REGEX}}\/d\/([A-Za-z0-9_-]{32})\?k=text#d1\.([A-Za-z0-9_-]{43})$/);
|
||||
return match ? neoDropFrame(url, 'text') : null;
|
||||
});
|
||||
neoDropTextPlugin.exclusive = true;
|
||||
@@ -0,0 +1,62 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var weechat = angular.module('weechat');
|
||||
var neoDropCrypto = require('./neodrop-crypto');
|
||||
|
||||
weechat.factory('imgur', ['$rootScope', 'settings', function($rootScope, settings) {
|
||||
var BASE = '{{DROPS_ORIGIN}}';
|
||||
|
||||
var showErrorMsg = function(message) {
|
||||
$rootScope.uploadError = true;
|
||||
$rootScope.uploadErrorMessage = message || 'Encrypted upload failed.';
|
||||
if (!$rootScope.$$phase) $rootScope.$apply();
|
||||
setTimeout(function() {
|
||||
$rootScope.uploadError = false;
|
||||
if (!$rootScope.$$phase) $rootScope.$apply();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
var process = function(file, callback) {
|
||||
if (!file || typeof file.size !== 'number') return;
|
||||
if (file.size > neoDropCrypto.LARGE_MAX) {
|
||||
showErrorMsg('NeoDrop files are limited to 1 GiB.');
|
||||
return;
|
||||
}
|
||||
var profile = (settings.dropProfile === 'large' || file.size > neoDropCrypto.SMALL_MAX) ? 1 : 0;
|
||||
var progressBars = document.getElementById('imgur-upload-progress');
|
||||
var bar = document.createElement('div');
|
||||
bar.className = 'imgur-progress-bar';
|
||||
progressBars.appendChild(bar);
|
||||
|
||||
neoDropCrypto.upload(file, {
|
||||
base: BASE,
|
||||
profile: profile,
|
||||
onProgress: function(value) { bar.style.width = Math.round(value * 100) + '%'; }
|
||||
}).then(function(result) {
|
||||
if (typeof $rootScope.registerNeoDrop === 'function') {
|
||||
$rootScope.registerNeoDrop(result.readUrl, result.deleteCapability);
|
||||
}
|
||||
if (callback) callback(result.readUrl, result.deleteCapability);
|
||||
}).catch(function(error) {
|
||||
showErrorMsg(error.message);
|
||||
}).finally(function() {
|
||||
setTimeout(function() { if (bar.parentNode) bar.parentNode.removeChild(bar); }, 500);
|
||||
});
|
||||
};
|
||||
|
||||
var deleteImage = function(capability, callback) {
|
||||
var split = String(capability || '').split('.');
|
||||
if (split.length !== 2) return showErrorMsg('Invalid deletion capability.');
|
||||
fetch(BASE + '/api/v1/objects/' + split[0], {
|
||||
method: 'DELETE', headers: {'Authorization': 'Bearer ' + split[1]}
|
||||
}).then(function(response) {
|
||||
if (!response.ok) throw new Error('Unable to delete encrypted file.');
|
||||
if (callback) callback(capability);
|
||||
}).catch(function() { showErrorMsg('Unable to delete encrypted file.'); });
|
||||
};
|
||||
|
||||
return {process: process, deleteImage: deleteImage};
|
||||
}]);
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,17 @@
|
||||
<div class="gb-modal neo-server-manager" data-state="visible" ng-if="serverManager.visible">
|
||||
<div class="backdrop" ng-click="closeNeoServerManager()"></div>
|
||||
<div class="modal-dialog"><div class="modal-content">
|
||||
<div class="modal-header"><button type="button" class="close" ng-click="closeNeoServerManager()">×</button><h4>IRC server manager</h4><p>Add a network or edit every WeeChat server option. Password fields are write-only.</p></div>
|
||||
<div class="modal-body neo-server-layout">
|
||||
<aside><button type="button" class="btn btn-primary neo-add-server" ng-click="newNeoServer()">Add new server</button><button type="button" ng-repeat="name in serverManager.servers" ng-click="selectNeoServer(name)" ng-bind="name"></button></aside>
|
||||
<section ng-if="serverManager.selected">
|
||||
<label>Server name<input class="form-control neo-server-control" ng-model="serverManager.selected.name" ng-disabled="!serverManager.isNew"></label>
|
||||
<div class="neo-server-options">
|
||||
<label ng-repeat="option in serverManager.selected.options" title="{{option.description}}"><span ng-bind="option.name"></span><select class="form-control neo-server-control" ng-if="option.boolean" ng-model="option.value" ng-change="option.dirty=true"><option value="on">On</option><option value="off">Off</option></select><input ng-if="!option.boolean" class="form-control neo-server-control" type="{{option.secret?'password':'text'}}" autocomplete="off" ng-model="option.value" ng-change="option.dirty=true" placeholder="{{option.secret?'Leave blank to keep existing value':''}}"><span class="inherit"><input type="checkbox" ng-model="option.inherited" ng-change="option.dirty=true" ng-disabled="serverManager.isNew||option.secret"> inherit</span></label>
|
||||
</div>
|
||||
</section>
|
||||
<p class="status" ng-bind="serverManager.status"></p>
|
||||
</div>
|
||||
<div class="modal-footer"><button type="button" class="btn btn-default" ng-click="closeNeoServerManager()">Cancel</button><button type="button" class="btn btn-primary" ng-click="saveNeoServer(false)" ng-disabled="serverManager.saving">Save</button><button type="button" class="btn btn-primary" ng-click="saveNeoServer(true)" ng-disabled="serverManager.saving">Save and connect</button></div>
|
||||
</div></div>
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
(function() {
|
||||
var weechat = angular.module('weechat');
|
||||
var names = 'addresses proxy ipv6 ssl ssl_cert ssl_password ssl_priorities ssl_dhkey_size ssl_fingerprint ssl_verify password capabilities sasl_mechanism sasl_username sasl_password sasl_key sasl_timeout sasl_fail autoconnect autoreconnect autoreconnect_delay nicks nicks_alternate username realname local_hostname usermode command command_delay autojoin autojoin_dynamic autorejoin autorejoin_delay connection_timeout anti_flood_prio_high anti_flood_prio_low away_check away_check_max_nicks msg_kick msg_part msg_quit notify split_msg_max_length charset_message default_chantypes'.split(' ');
|
||||
var secrets = {ssl_password: true, password: true, sasl_password: true};
|
||||
var booleans = {ipv6:true,ssl:true,ssl_verify:true,autoconnect:true,autoreconnect:true,nicks_alternate:true,autojoin_dynamic:true,autorejoin:true};
|
||||
|
||||
weechat.directive('neoServerManager', ['connection', function(connection) {
|
||||
return {restrict: 'E', templateUrl: 'directives/server-manager.html', link: function(scope) {
|
||||
scope.serverManager = {visible:false, loading:false, saving:false, servers:[], selected:null, isNew:false, status:''};
|
||||
var manager = scope.serverManager;
|
||||
function validName(value) { return /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/.test(value || ''); }
|
||||
function safeValue(value) { return typeof value === 'string' && !/[\r\n\0"]/.test(value); }
|
||||
function optionModel(name, row) { return {name:name, description:row&&row.description||'', value:row&&row.value_is_null?String(row.parent_value||''):String(row&&row.value||''), inherited:Boolean(row&&row.value_is_null), secret:Boolean(secrets[name]), boolean:Boolean(booleans[name]), dirty:false}; }
|
||||
function refresh() {
|
||||
manager.loading = true;
|
||||
return connection.requestInfolist('option', 0, 'irc.server.*.addresses').then(function(rows) {
|
||||
manager.servers = rows.map(function(row) { return row.full_name.slice(11, -10); }).filter(validName).sort();
|
||||
manager.loading = false;
|
||||
}, function() { manager.loading=false; manager.status='Unable to load server settings.'; });
|
||||
}
|
||||
function select(name) {
|
||||
manager.isNew = false; manager.loading = true; manager.status = '';
|
||||
Promise.all(names.map(function(option) {
|
||||
if (secrets[option]) return Promise.resolve(optionModel(option, null));
|
||||
return connection.requestInfolist('option', 0, 'irc.server.' + name + '.' + option).then(function(rows) { return optionModel(option, rows[0]); });
|
||||
})).then(function(options) { scope.$applyAsync(function() { manager.selected={name:name, originalName:name, options:options}; manager.loading=false; }); });
|
||||
}
|
||||
scope.newNeoServer = function() { manager.isNew=true; manager.selected={name:'',originalName:'',options:names.map(function(name){return optionModel(name,null);})}; manager.selected.options[0].inherited=false; manager.status=''; };
|
||||
scope.selectNeoServer = select;
|
||||
scope.closeNeoServerManager = function() { manager.visible=false; manager.selected=null; };
|
||||
scope.saveNeoServer = function(connectAfter) {
|
||||
var server=manager.selected; if(!server||!validName(server.name)){manager.status='Use a safe server name.';return;}
|
||||
var addresses=server.options[0].value; if(manager.isNew&&!safeValue(addresses)){manager.status='Enter a safe server address.';return;}
|
||||
manager.saving=true;
|
||||
if(manager.isNew) connection.sendCoreCommand('/server add '+server.name+' '+addresses);
|
||||
server.options.forEach(function(option) {
|
||||
if ((!manager.isNew&&!option.dirty)||(manager.isNew&&option.name==='addresses')||option.secret&&!option.value) return;
|
||||
if (!safeValue(option.value)){manager.status='Values cannot contain quotes or control characters.';return;}
|
||||
var full='irc.server.'+server.name+'.'+option.name;
|
||||
if (option.secret) {
|
||||
var secureKey='neobear_'+server.name.toLowerCase()+'_'+option.name;
|
||||
connection.sendCoreCommand('/secure set '+secureKey+' '+option.value);
|
||||
connection.sendCoreCommand('/set '+full+' "${sec.data.'+secureKey+'}"');
|
||||
} else {
|
||||
connection.sendCoreCommand(option.inherited?'/unset '+full:'/set '+full+' "'+option.value+'"');
|
||||
}
|
||||
});
|
||||
connection.sendCoreCommand('/save irc');
|
||||
if(connectAfter) connection.sendCoreCommand('/connect '+server.name);
|
||||
manager.status='Changes sent to WeeChat. Check the core buffer for any rejected setting.'; manager.saving=false; refresh();
|
||||
};
|
||||
scope.$on('neoServerManagerOpen', function(event, name) { manager.visible=true; refresh().then(function(){ if(name) select(name); else scope.$applyAsync(scope.newNeoServer); }); });
|
||||
}};
|
||||
}]);
|
||||
}());
|
||||
@@ -0,0 +1,85 @@
|
||||
$timeout(function() {
|
||||
var sidebar = document.getElementById('sidebar');
|
||||
if (!sidebar || document.getElementById('sidebar-resizer')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sidebarResizer = document.createElement('div');
|
||||
var desktopSidebarMedia = window.matchMedia('(min-width: 969px)');
|
||||
var activeSidebarPointer = null;
|
||||
var workingSidebarWidth = 0;
|
||||
|
||||
sidebarResizer.id = 'sidebar-resizer';
|
||||
sidebarResizer.tabIndex = 0;
|
||||
sidebarResizer.setAttribute('role', 'separator');
|
||||
sidebarResizer.setAttribute('aria-orientation', 'vertical');
|
||||
sidebarResizer.setAttribute('aria-controls', 'sidebar');
|
||||
sidebarResizer.setAttribute('aria-label', 'Resize channel sidebar');
|
||||
sidebarResizer.setAttribute('aria-valuemin', '120');
|
||||
sidebarResizer.setAttribute('aria-valuemax', '480');
|
||||
sidebar.parentNode.insertBefore(sidebarResizer, sidebar.nextSibling);
|
||||
|
||||
var maximumSidebarWidth = function() {
|
||||
return Math.min(480, window.innerWidth * 0.4);
|
||||
};
|
||||
|
||||
var applySidebarWidth = function(value, limitToViewport) {
|
||||
var width = Number(value);
|
||||
if (!Number.isFinite(width) || width <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
width = Math.max(120, Math.min(limitToViewport ? maximumSidebarWidth() : 480, width));
|
||||
width = Math.round(width);
|
||||
document.documentElement.style.setProperty('--neorelay-sidebar-preferred-width', width + 'px');
|
||||
sidebarResizer.setAttribute('aria-valuenow', width);
|
||||
workingSidebarWidth = width;
|
||||
};
|
||||
|
||||
applySidebarWidth(settings.neorelaySidebarWidth, false);
|
||||
|
||||
sidebarResizer.addEventListener('pointerdown', function(event) {
|
||||
if (!desktopSidebarMedia.matches || event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeSidebarPointer = event.pointerId;
|
||||
workingSidebarWidth = Math.round(sidebar.getBoundingClientRect().width);
|
||||
sidebarResizer.setPointerCapture(event.pointerId);
|
||||
document.body.classList.add('neorelay-sidebar-resizing');
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
sidebarResizer.addEventListener('pointermove', function(event) {
|
||||
if (event.pointerId === activeSidebarPointer && desktopSidebarMedia.matches) {
|
||||
applySidebarWidth(event.clientX, true);
|
||||
}
|
||||
});
|
||||
|
||||
var finishSidebarResize = function(event) {
|
||||
if (event.pointerId !== activeSidebarPointer) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeSidebarPointer = null;
|
||||
document.body.classList.remove('neorelay-sidebar-resizing');
|
||||
if (workingSidebarWidth > 0) {
|
||||
settings.neorelaySidebarWidth = workingSidebarWidth;
|
||||
}
|
||||
};
|
||||
|
||||
sidebarResizer.addEventListener('pointerup', finishSidebarResize);
|
||||
sidebarResizer.addEventListener('pointercancel', finishSidebarResize);
|
||||
sidebarResizer.addEventListener('keydown', function(event) {
|
||||
if (!desktopSidebarMedia.matches ||
|
||||
(event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var direction = event.key === 'ArrowLeft' ? -1 : 1;
|
||||
var step = event.shiftKey ? 25 : 10;
|
||||
applySidebarWidth(sidebar.getBoundingClientRect().width + direction * step, true);
|
||||
settings.neorelaySidebarWidth = workingSidebarWidth;
|
||||
event.preventDefault();
|
||||
});
|
||||
}, 0);
|
||||
@@ -0,0 +1,20 @@
|
||||
<section class="neo-weechat-settings" ng-show="neoSettingsTab === 'weechat'" role="tabpanel" aria-label="WeeChat settings">
|
||||
<div class="neo-settings-toolbar">
|
||||
<label>Section<select class="form-control" ng-model="neoWeeChatSettings.section" ng-options="section.mask as section.label for section in neoWeeChatSettings.sections" ng-change="loadNeoWeeChatSettings()"></select></label>
|
||||
<label>Filter<input class="form-control" type="text" ng-model="neoWeeChatSettings.filter" placeholder="Filter loaded settings"></label>
|
||||
<button type="button" class="btn btn-default" ng-click="loadNeoWeeChatSettings()" ng-disabled="neoWeeChatSettings.loading">Refresh</button>
|
||||
</div>
|
||||
<p>Changes are validated, written to WeeChat, read back, and then saved to the corresponding configuration file.</p>
|
||||
<p class="status" ng-bind="neoWeeChatSettings.status"></p>
|
||||
<div class="neo-option-list">
|
||||
<article class="neo-option" ng-repeat="option in neoWeeChatSettings.options | filter:neoWeeChatSettings.filter | limitTo:200 track by option.name">
|
||||
<div><strong ng-bind="option.name"></strong><small ng-bind="option.description"></small></div>
|
||||
<select class="form-control" ng-if="option.type === 'boolean'" ng-model="option.editValue"><option value="on">On</option><option value="off">Off</option></select>
|
||||
<select class="form-control" ng-if="option.type !== 'boolean' && option.choices.length" ng-model="option.editValue" ng-options="choice for choice in option.choices"></select>
|
||||
<select class="form-control" ng-if="option.type === 'color' && !option.choices.length" ng-model="option.editValue" ng-options="colour for colour in neoWeeChatColourChoices"></select>
|
||||
<input class="form-control" ng-if="option.type === 'integer' && !option.choices.length" type="number" ng-model="option.editValue" min="{{option.min}}" max="{{option.max}}">
|
||||
<input class="form-control" ng-if="option.type !== 'boolean' && option.type !== 'color' && option.type !== 'integer' && !option.choices.length" type="text" ng-model="option.editValue">
|
||||
<div class="neo-option-actions"><button type="button" class="btn btn-primary" ng-click="saveNeoWeeChatOption(option)" ng-disabled="option.saving">Save</button><button type="button" class="btn btn-default" ng-click="resetNeoWeeChatOption(option)" ng-disabled="option.saving">Reset</button></div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,150 @@
|
||||
var neoBasicColours = ['default','black','darkgray','red','lightred','green','lightgreen','brown','yellow','blue','lightblue','magenta','lightmagenta','cyan','lightcyan','gray','white'];
|
||||
$scope.neoWeeChatColourChoices = neoBasicColours.concat(Array.from({length: 256}, function(_, index) { return String(index); }));
|
||||
var neoColourHex = {default:'#f1eaff',black:'#000000',darkgray:'#555555',red:'#aa0000',lightred:'#ff5555',green:'#00aa00',lightgreen:'#55ff55',brown:'#aa5500',yellow:'#ffff55',blue:'#0000aa',lightblue:'#5555ff',magenta:'#aa00aa',lightmagenta:'#ff55ff',cyan:'#00aaaa',lightcyan:'#55ffff',gray:'#aaaaaa',white:'#ffffff'};
|
||||
var neoAllowedSections = [
|
||||
{label:'WeeChat appearance',mask:'weechat.look.*'},
|
||||
{label:'WeeChat colours',mask:'weechat.color.*'},
|
||||
{label:'Completion',mask:'weechat.completion.*'},
|
||||
{label:'History',mask:'weechat.history.*'},
|
||||
{label:'Network',mask:'weechat.network.*'},
|
||||
{label:'Plugins',mask:'weechat.plugin.*'},
|
||||
{label:'IRC appearance',mask:'irc.look.*'},
|
||||
{label:'IRC colours',mask:'irc.color.*'},
|
||||
{label:'IRC network',mask:'irc.network.*'}
|
||||
];
|
||||
var neoAllowedMasks = neoAllowedSections.reduce(function(result, item) { result[item.mask] = true; return result; }, {});
|
||||
var neoLoadedOptions = {};
|
||||
|
||||
function neoResolveWeeChatColour(value) {
|
||||
value = String(value || 'default').replace(/^[*!/_|]+/, '');
|
||||
if (neoColourHex[value]) return neoColourHex[value];
|
||||
var number = Number(value);
|
||||
if (!Number.isInteger(number) || number < 0 || number > 255) return neoColourHex.default;
|
||||
if (number < 16) return neoColourHex[neoBasicColours[number + 1] || 'default'];
|
||||
if (number >= 232) {
|
||||
var gray = 8 + (number - 232) * 10;
|
||||
return 'rgb(' + gray + ',' + gray + ',' + gray + ')';
|
||||
}
|
||||
number -= 16;
|
||||
var levels = [0,95,135,175,215,255];
|
||||
return 'rgb(' + levels[Math.floor(number / 36)] + ',' + levels[Math.floor(number / 6) % 6] + ',' + levels[number % 6] + ')';
|
||||
}
|
||||
|
||||
function neoValidWeeChatColour(value) {
|
||||
value = String(value || '').replace(/^[*!/_|]+/, '');
|
||||
return neoBasicColours.indexOf(value) >= 0 || (/^(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/.test(value));
|
||||
}
|
||||
|
||||
function neoEffectiveOptionValue(row) {
|
||||
if (!row.value_is_null) return Object.prototype.hasOwnProperty.call(row, 'value') ? String(row.value) : '';
|
||||
if (Object.prototype.hasOwnProperty.call(row, 'parent_value')) return String(row.parent_value);
|
||||
return Object.prototype.hasOwnProperty.call(row, 'default_value') ? String(row.default_value) : '';
|
||||
}
|
||||
|
||||
function neoNormalizeOption(row) {
|
||||
var choices = row.string_values ? String(row.string_values).split('|').filter(Boolean) : [];
|
||||
var type = String(row.type || 'string');
|
||||
var value = neoEffectiveOptionValue(row);
|
||||
return {
|
||||
name: String(row.full_name || ''),
|
||||
configName: String(row.config_name || '').replace(/[^A-Za-z0-9_-]/g, ''),
|
||||
description: String(row.description_nls || row.description || ''),
|
||||
type: type,
|
||||
value: value,
|
||||
editValue: type === 'integer' && !choices.length && /^-?[0-9]+$/.test(value) ? Number(value) : value,
|
||||
defaultValue: Object.prototype.hasOwnProperty.call(row, 'default_value') ? String(row.default_value) : '',
|
||||
min: Number(row.min),
|
||||
max: Number(row.max),
|
||||
choices: choices,
|
||||
saving: false
|
||||
};
|
||||
}
|
||||
|
||||
function neoSafeOption(row) {
|
||||
return row && /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,511}$/.test(row.name) &&
|
||||
/^(weechat|irc)$/.test(row.configName) &&
|
||||
!/(?:password|passphrase|secret|token)(?:$|[._-])/i.test(row.name);
|
||||
}
|
||||
|
||||
function neoValidateOption(row, value) {
|
||||
value = String(value);
|
||||
if (!neoSafeOption(row) || neoLoadedOptions[row.name] !== row) throw new Error('Setting is not editable.');
|
||||
if (value.length > 4096 || /[\u0000-\u001f\u007f-\u009f"\\\u2028\u2029]/.test(value)) throw new Error('Value contains unsupported characters.');
|
||||
if (row.type === 'boolean' && !/^(?:on|off)$/.test(value)) throw new Error('Choose On or Off.');
|
||||
if (row.choices.length && row.choices.indexOf(value) < 0) throw new Error('Choose a listed value.');
|
||||
if (row.type === 'integer' && !row.choices.length) {
|
||||
if (!/^-?[0-9]+$/.test(value)) throw new Error('Enter an integer.');
|
||||
var number = Number(value);
|
||||
if ((Number.isFinite(row.min) && number < row.min) || (Number.isFinite(row.max) && row.max > row.min && number > row.max)) throw new Error('Integer is outside the allowed range.');
|
||||
}
|
||||
if (row.type === 'color' && !neoValidWeeChatColour(value)) throw new Error('Choose a WeeChat colour.');
|
||||
if (row.name === 'irc.color.nick_prefixes') {
|
||||
var prefixEntries = value.split(';').filter(Boolean);
|
||||
if (!prefixEntries.length || prefixEntries.some(function(entry) {
|
||||
var match = entry.match(/^[A-Za-z*]:([*!/_|]*)([^;]+)$/);
|
||||
return !match || !neoValidWeeChatColour(match[2]);
|
||||
})) throw new Error('Invalid IRC prefix colour map.');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function neoRefreshExact(row, expected, success, failure, attempt) {
|
||||
connection.requestInfolist('option', 0, row.name).then(function(rows) {
|
||||
var updated = rows[0] ? neoNormalizeOption(rows[0]) : null;
|
||||
if (updated && (expected === null || updated.value === expected)) {
|
||||
Object.assign(row, updated);
|
||||
neoLoadedOptions[row.name] = row;
|
||||
connection.sendCoreCommand('/save ' + updated.configName);
|
||||
$scope.$applyAsync(function() { success(row); });
|
||||
} else if (attempt < 5) {
|
||||
$timeout(function() { neoRefreshExact(row, expected, success, failure, attempt + 1); }, 200 * attempt);
|
||||
} else {
|
||||
$scope.$applyAsync(function() { failure('WeeChat did not confirm the setting change.'); });
|
||||
}
|
||||
}, function() { $scope.$applyAsync(function() { failure('Unable to verify the setting change.'); }); });
|
||||
}
|
||||
|
||||
function neoWriteOption(row, value, success, failure) {
|
||||
try { value = neoValidateOption(row, value); }
|
||||
catch (error) { failure(error.message); return; }
|
||||
connection.sendCoreCommand('/set ' + row.name + ' "' + value + '"');
|
||||
$timeout(function() { neoRefreshExact(row, value, success, failure, 1); }, 150);
|
||||
}
|
||||
|
||||
$scope.neoWeeChatSettings = {sections: neoAllowedSections, section: neoAllowedSections[0].mask, filter: '', options: [], loading: false, status: ''};
|
||||
$scope.loadNeoWeeChatSettings = function() {
|
||||
var state = $scope.neoWeeChatSettings;
|
||||
if (!$rootScope.connected || state.loading || !neoAllowedMasks[state.section]) return;
|
||||
state.loading = true;
|
||||
state.status = 'Loading settings from WeeChat...';
|
||||
connection.requestInfolist('option', 0, state.section).then(function(rows) {
|
||||
$scope.$applyAsync(function() {
|
||||
state.options = rows.map(neoNormalizeOption).filter(neoSafeOption).sort(function(a, b) { return a.name.localeCompare(b.name); });
|
||||
state.options.forEach(function(row) { neoLoadedOptions[row.name] = row; });
|
||||
state.loading = false;
|
||||
state.status = 'Loaded ' + state.options.length + ' editable settings.';
|
||||
});
|
||||
}, function() { $scope.$applyAsync(function() { state.loading = false; state.status = 'Unable to load settings.'; }); });
|
||||
};
|
||||
|
||||
$scope.saveNeoWeeChatOption = function(row) {
|
||||
row.saving = true;
|
||||
neoWriteOption(row, row.editValue, function(updated) {
|
||||
Object.assign(row, updated);
|
||||
row.saving = false;
|
||||
$scope.neoWeeChatSettings.status = 'Saved ' + row.name + '.';
|
||||
}, function(message) { row.saving = false; $scope.neoWeeChatSettings.status = message; });
|
||||
};
|
||||
|
||||
$scope.resetNeoWeeChatOption = function(row) {
|
||||
if (!neoSafeOption(row) || neoLoadedOptions[row.name] !== row) return;
|
||||
row.saving = true;
|
||||
connection.sendCoreCommand('/unset ' + row.name);
|
||||
$timeout(function() {
|
||||
neoRefreshExact(row, null, function(updated) {
|
||||
Object.assign(row, updated);
|
||||
row.saving = false;
|
||||
$scope.neoWeeChatSettings.status = 'Reset ' + row.name + '.';
|
||||
}, function(message) { row.saving = false; $scope.neoWeeChatSettings.status = message; }, 1);
|
||||
}, 150);
|
||||
};
|
||||
Reference in New Issue
Block a user