Merge pull request #113 from bra1n/custom-images

Support custom character icons / edition logos again with opt-in
This commit is contained in:
Steffen 2021-02-11 20:33:18 +01:00 committed by GitHub
commit 729f3981d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 236 additions and 172 deletions

View File

@ -1,5 +1,11 @@
# Release Notes # Release Notes
### Version 2.8.0
- added hands-off live session support for homebrew / custom characters again!
- added custom image opt-in that will prevent any (potentially malicious / harmful) images from loading until a player manually allows them to
---
## Version 2.7.0 ## Version 2.7.0
- added support for assigning duplicate characters to more than one player (like Legion) - added support for assigning duplicate characters to more than one player (like Legion)
- further live session bandwidth optimizations - further live session bandwidth optimizations

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "townsquare", "name": "townsquare",
"version": "2.7.0", "version": "2.8.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "townsquare", "name": "townsquare",
"version": "2.7.0", "version": "2.8.0",
"description": "Blood on the Clocktower Town Square", "description": "Blood on the Clocktower Town Square",
"author": "Steffen Baumgart", "author": "Steffen Baumgart",
"scripts": { "scripts": {

BIN
src/assets/icons/minion.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View File

@ -69,11 +69,21 @@
/> />
</em> </em>
</li> </li>
<li v-if="!edition.isOfficial" @click="imageOptIn">
<small>Show Custom Images</small>
<em
><font-awesome-icon
:icon="[
'fas',
grimoire.isImageOptIn ? 'check-square' : 'square'
]"
/></em>
</li>
<li @click="setBackground"> <li @click="setBackground">
Background image Background image
<em><font-awesome-icon icon="image"/></em> <em><font-awesome-icon icon="image"/></em>
</li> </li>
<li @click="toggleMute"> <li @click="toggleMuted">
Mute Sounds Mute Sounds
<em <em
><font-awesome-icon ><font-awesome-icon
@ -83,40 +93,41 @@
</template> </template>
<template v-if="tab === 'session'"> <template v-if="tab === 'session'">
<!-- Session -->
<li class="headline" v-if="session.sessionId"> <li class="headline" v-if="session.sessionId">
{{ session.isSpectator ? "Playing" : "Hosting" }} {{ session.isSpectator ? "Playing" : "Hosting" }}
</li> </li>
<li class="headline" v-else> <li class="headline" v-else>
Live Session Live Session
</li> </li>
<li @click="hostSession" v-if="!session.sessionId"> <template v-if="!session.sessionId">
Host (Storyteller)<em>[H]</em> <li @click="hostSession">Host (Storyteller)<em>[H]</em></li>
</li> <li @click="joinSession">Join (Player)<em>[J]</em></li>
<li @click="joinSession" v-if="!session.sessionId"> </template>
Join (Player)<em>[J]</em> <template v-else>
</li> <li v-if="session.ping">
<li v-if="session.sessionId && session.ping"> Delay to {{ session.isSpectator ? "host" : "players" }}
Delay to {{ session.isSpectator ? "host" : "players" }} <em>{{ session.ping }}ms</em>
<em>{{ session.ping }}ms</em> </li>
</li> <li @click="copySessionUrl">
<li v-if="session.sessionId" @click="copySessionUrl"> Copy player link
Copy player link <em><font-awesome-icon icon="copy"/></em>
<em><font-awesome-icon icon="copy"/></em> </li>
</li> <li v-if="!session.isSpectator" @click="distributeRoles">
<li v-if="!session.isSpectator" @click="distributeRoles"> Send Characters
Send Characters <em><font-awesome-icon icon="theater-masks"/></em>
<em><font-awesome-icon icon="theater-masks"/></em> </li>
</li> <li
<li v-if="session.voteHistory.length"
v-if="session.voteHistory.length" @click="toggleModal('voteHistory')"
@click="toggleModal('voteHistory')" >
> Nomination history<em>[V]</em>
Nomination history<em>[V]</em> </li>
</li> <li @click="leaveSession">
<li @click="leaveSession" v-if="session.sessionId"> Leave Session
Leave Session <em>{{ session.sessionId }}</em>
<em>{{ session.sessionId }}</em> </li>
</li> </template>
</template> </template>
<template v-if="tab === 'players' && !session.isSpectator"> <template v-if="tab === 'players' && !session.isSpectator">
@ -203,7 +214,7 @@ import { mapMutations, mapState } from "vuex";
export default { export default {
computed: { computed: {
...mapState(["grimoire", "session"]), ...mapState(["grimoire", "session", "edition"]),
...mapState("players", ["players"]) ...mapState("players", ["players"])
}, },
data() { data() {
@ -218,9 +229,6 @@ export default {
this.$store.commit("setBackground", background); this.$store.commit("setBackground", background);
} }
}, },
toggleMute() {
this.$store.commit("setIsMuted", !this.grimoire.isMuted);
},
hostSession() { hostSession() {
if (this.session.sessionId) return; if (this.session.sessionId) return;
const sessionId = prompt( const sessionId = prompt(
@ -253,6 +261,13 @@ export default {
); );
} }
}, },
imageOptIn() {
const popup =
"Are you sure you want to allow custom images? A malicious script file author might track your IP address this way.";
if (this.grimoire.isImageOptIn || confirm(popup)) {
this.toggleImageOptIn();
}
},
joinSession() { joinSession() {
if (this.session.sessionId) return this.leaveSession(); if (this.session.sessionId) return this.leaveSession();
let sessionId = prompt( let sessionId = prompt(
@ -302,6 +317,8 @@ export default {
...mapMutations([ ...mapMutations([
"toggleGrimoire", "toggleGrimoire",
"toggleMenu", "toggleMenu",
"toggleImageOptIn",
"toggleMuted",
"toggleNight", "toggleNight",
"toggleNightOrder", "toggleNightOrder",
"setZoom", "setZoom",

View File

@ -23,7 +23,7 @@
> >
<em>{{ nightOrder.get(player).first }}.</em> <em>{{ nightOrder.get(player).first }}.</em>
<span v-if="player.role.firstNightReminder">{{ <span v-if="player.role.firstNightReminder">{{
player.role.firstNightReminder | handleEmojis player.role.firstNightReminder
}}</span> }}</span>
</div> </div>
<div <div
@ -32,7 +32,7 @@
> >
<em>{{ nightOrder.get(player).other }}.</em> <em>{{ nightOrder.get(player).other }}.</em>
<span v-if="player.role.otherNightReminder">{{ <span v-if="player.role.otherNightReminder">{{
player.role.otherNightReminder | handleEmojis player.role.otherNightReminder
}}</span> }}</span>
</div> </div>
@ -165,8 +165,13 @@
<span <span
class="icon" class="icon"
:style="{ :style="{
backgroundImage: `url(${reminder.image || backgroundImage: `url(${
require('../assets/icons/' + reminder.role + '.png')})` reminder.image && grimoire.isImageOptIn
? reminder.image
: require('../assets/icons/' +
(reminder.imageAlt || reminder.role) +
'.png')
})`
}" }"
></span> ></span>
<span class="text">{{ reminder.name }}</span> <span class="text">{{ reminder.name }}</span>
@ -226,9 +231,6 @@ export default {
isSwap: false isSwap: false
}; };
}, },
filters: {
handleEmojis: text => text.replace(/:([^: ]+?):/g, "").replace(/ •/g, "\n•")
},
methods: { methods: {
toggleStatus() { toggleStatus() {
if (this.grimoire.isPublic) { if (this.grimoire.isPublic) {

View File

@ -4,8 +4,11 @@
class="icon" class="icon"
v-if="role.id" v-if="role.id"
:style="{ :style="{
backgroundImage: `url(${role.image || backgroundImage: `url(${
require('../assets/icons/' + role.id + '.png')})` role.image && grimoire.isImageOptIn
? role.image
: require('../assets/icons/' + (role.imageAlt || role.id) + '.png')
})`
}" }"
></span> ></span>
<span <span
@ -47,6 +50,8 @@
</template> </template>
<script> <script>
import { mapState } from "vuex";
export default { export default {
name: "Token", name: "Token",
props: { props: {
@ -55,6 +60,9 @@ export default {
default: () => ({}) default: () => ({})
} }
}, },
computed: {
...mapState(["grimoire"])
},
data() { data() {
return {}; return {};
}, },

View File

@ -4,8 +4,11 @@
class="edition" class="edition"
:class="['edition-' + edition.id]" :class="['edition-' + edition.id]"
:style="{ :style="{
backgroundImage: `url(${edition.logo || backgroundImage: `url(${
require('../assets/editions/' + edition.id + '.png')})` edition.logo && grimoire.isImageOptIn
? edition.logo
: require('../assets/editions/' + edition.id + '.png')
})`
}" }"
></li> ></li>
<li v-if="players.length - teams.traveler < 5"> <li v-if="players.length - teams.traveler < 5">

View File

@ -41,8 +41,13 @@
class="icon" class="icon"
v-if="role.id" v-if="role.id"
:style="{ :style="{
backgroundImage: `url(${role.image || backgroundImage: `url(${
require('../../assets/icons/' + role.id + '.png')})` role.image && grimoire.isImageOptIn
? role.image
: require('../../assets/icons/' +
(role.imageAlt || role.id) +
'.png')
})`
}" }"
></span> ></span>
<span class="reminder" v-if="role.firstNightReminder"> <span class="reminder" v-if="role.firstNightReminder">
@ -61,8 +66,13 @@
class="icon" class="icon"
v-if="role.id" v-if="role.id"
:style="{ :style="{
backgroundImage: `url(${role.image || backgroundImage: `url(${
require('../../assets/icons/' + role.id + '.png')})` role.image && grimoire.isImageOptIn
? role.image
: require('../../assets/icons/' +
(role.imageAlt || role.id) +
'.png')
})`
}" }"
></span> ></span>
<span class="name"> <span class="name">
@ -107,14 +117,21 @@ export default {
name: "Minion info", name: "Minion info",
firstNight: 2, firstNight: 2,
team: "minion", team: "minion",
players: this.players.filter(p => p.role.team === "minion") players: this.players.filter(p => p.role.team === "minion"),
firstNightReminder:
"• If more than one Minion, they all make eye contact with each other. " +
"• Show the “This is the Demon” card. Point to the Demon."
}, },
{ {
id: "evil", id: "evil",
name: "Demon info & bluffs", name: "Demon info & bluffs",
firstNight: 4, firstNight: 4,
team: "demon", team: "demon",
players: this.players.filter(p => p.role.team === "demon") players: this.players.filter(p => p.role.team === "demon"),
firstNightReminder:
"• Show the “These are your minions” card. Point to each Minion. " +
"• Show the “These characters are not in play” card. Show 3 character tokens of good " +
"characters not in play."
} }
); );
} }

View File

@ -34,8 +34,13 @@
class="icon" class="icon"
v-if="role.id" v-if="role.id"
:style="{ :style="{
backgroundImage: `url(${role.image || backgroundImage: `url(${
require('../../assets/icons/' + role.id + '.png')})` role.image && grimoire.isImageOptIn
? role.image
: require('../../assets/icons/' +
(role.imageAlt || role.id) +
'.png')
})`
}" }"
></span> ></span>
<span class="ability">{{ role.ability }}</span> <span class="ability">{{ role.ability }}</span>
@ -80,7 +85,7 @@ export default {
}); });
return players; return players;
}, },
...mapState(["roles", "modals", "edition"]), ...mapState(["roles", "modals", "edition", "grimoire"]),
...mapState("players", ["players"]) ...mapState("players", ["players"])
}, },
methods: { methods: {

View File

@ -15,8 +15,13 @@
<span <span
class="icon" class="icon"
:style="{ :style="{
backgroundImage: `url(${reminder.image || backgroundImage: `url(${
require('../../assets/icons/' + reminder.role + '.png')})` reminder.image && grimoire.isImageOptIn
? reminder.image
: require('../../assets/icons/' +
(reminder.imageAlt || reminder.role) +
'.png')
})`
}" }"
></span> ></span>
<span class="text">{{ reminder.name }}</span> <span class="text">{{ reminder.name }}</span>
@ -29,6 +34,18 @@
import Modal from "./Modal"; import Modal from "./Modal";
import { mapMutations, mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
/**
* Helper function that maps a reminder name with a role-based object that provides necessary visual data.
* @param role The role for which the reminder should be generated
* @return {function(*): {image: string|string[]|string|*, role: *, name: *, imageAlt: string|*}}
*/
const mapReminder = ({ id, image, imageAlt }) => name => ({
role: id,
image,
imageAlt,
name
});
export default { export default {
components: { Modal }, components: { Modal },
props: ["playerIndex"], props: ["playerIndex"],
@ -39,61 +56,29 @@ export default {
this.$store.state.roles.forEach(role => { this.$store.state.roles.forEach(role => {
// add reminders from player roles // add reminders from player roles
if (players.some(p => p.role.id === role.id)) { if (players.some(p => p.role.id === role.id)) {
reminders = [ reminders = [...reminders, ...role.reminders.map(mapReminder(role))];
...reminders,
...role.reminders.map(name => ({
role: role.id,
image: role.image,
name
}))
];
} }
// add reminders from bluff/other roles // add reminders from bluff/other roles
else if (bluffs.some(bluff => bluff.id === role.id)) { else if (bluffs.some(bluff => bluff.id === role.id)) {
reminders = [ reminders = [...reminders, ...role.reminders.map(mapReminder(role))];
...reminders,
...role.reminders.map(name => ({
role: role.id,
image: role.image,
name
}))
];
} }
// add global reminders // add global reminders
if (role.remindersGlobal && role.remindersGlobal.length) { if (role.remindersGlobal && role.remindersGlobal.length) {
reminders = [ reminders = [
...reminders, ...reminders,
...role.remindersGlobal.map(name => ({ ...role.remindersGlobal.map(mapReminder(role))
role: role.id,
image: role.image,
name
}))
]; ];
} }
}); });
// add fabled reminders // add fabled reminders
this.$store.state.players.fabled.forEach(role => { this.$store.state.players.fabled.forEach(role => {
reminders = [ reminders = [...reminders, ...role.reminders.map(mapReminder(role))];
...reminders,
...role.reminders.map(name => ({
role: role.id,
image: role.image,
name
}))
];
}); });
// add out of script traveler reminders // add out of script traveler reminders
this.$store.state.otherTravelers.forEach(role => { this.$store.state.otherTravelers.forEach(role => {
if (players.some(p => p.role.id === role.id)) { if (players.some(p => p.role.id === role.id)) {
reminders = [ reminders = [...reminders, ...role.reminders.map(mapReminder(role))];
...reminders,
...role.reminders.map(name => ({
role: role.id,
image: role.image,
name
}))
];
} }
}); });
@ -102,7 +87,7 @@ export default {
reminders.push({ role: "custom", name: "Custom note" }); reminders.push({ role: "custom", name: "Custom note" });
return reminders; return reminders;
}, },
...mapState(["modals"]), ...mapState(["modals", "grimoire"]),
...mapState("players", ["players"]) ...mapState("players", ["players"])
}, },
methods: { methods: {

View File

@ -10,12 +10,14 @@ import fabledJSON from "../fabled.json";
Vue.use(Vuex); Vue.use(Vuex);
// global data maps
const editionJSONbyId = new Map( const editionJSONbyId = new Map(
editionJSON.map(edition => [edition.id, edition]) editionJSON.map(edition => [edition.id, edition])
); );
const rolesJSONbyId = new Map(rolesJSON.map(role => [role.id, role])); const rolesJSONbyId = new Map(rolesJSON.map(role => [role.id, role]));
const fabled = new Map(fabledJSON.map(role => [role.id, role])); const fabled = new Map(fabledJSON.map(role => [role.id, role]));
// helper functions
const getRolesByEdition = (edition = editionJSON[0]) => { const getRolesByEdition = (edition = editionJSON[0]) => {
return new Map( return new Map(
rolesJSON rolesJSON
@ -38,11 +40,24 @@ const getTravelersNotInEdition = (edition = editionJSON[0]) => {
); );
}; };
const set = key => ({ grimoire }, val) => {
grimoire[key] = val;
};
const toggle = key => ({ grimoire }, val) => {
if (val === true || val === false) {
grimoire[key] = val;
} else {
grimoire[key] = !grimoire[key];
}
};
// base definition for custom roles // base definition for custom roles
const imageBase =
"https://raw.githubusercontent.com/bra1n/townsquare/main/src/assets/icons/";
const customRole = { const customRole = {
id: "",
name: "",
image: "", image: "",
ability: "",
edition: "custom", edition: "custom",
firstNight: 0, firstNight: 0,
firstNightReminder: "", firstNightReminder: "",
@ -67,6 +82,7 @@ export default new Vuex.Store({
isPublic: true, isPublic: true,
isMenuOpen: false, isMenuOpen: false,
isMuted: false, isMuted: false,
isImageOptIn: false,
zoom: 0, zoom: 0,
background: "" background: ""
}, },
@ -88,27 +104,31 @@ export default new Vuex.Store({
}, },
getters: { getters: {
/** /**
* Return all custom roles, with default values stripped. * Return all custom roles, with default values and non-essential data stripped.
* Role object keys will be replaced with a numerical index to conserve bandwidth.
* @param roles * @param roles
* @returns {[]} * @returns {[]}
*/ */
customRoles: ({ roles }) => { customRolesStripped: ({ roles }) => {
const customRoles = []; const customRoles = [];
const customKeys = Object.keys(customRole);
const strippedProps = [
"firstNightReminder",
"otherNightReminder",
"isCustom"
];
roles.forEach(role => { roles.forEach(role => {
if (!role.isCustom) { if (!role.isCustom) {
customRoles.push({ id: role.id }); customRoles.push({ id: role.id });
} else { } else {
const strippedRole = {}; const strippedRole = {};
for (let prop in role) { for (let prop in role) {
const value = role[prop]; if (strippedProps.includes(prop)) {
if (
prop === "image" &&
value.toLocaleLowerCase().includes(imageBase)
) {
continue; continue;
} }
if (prop !== "isCustom" && value !== customRole[prop]) { const value = role[prop];
strippedRole[prop] = value; if (customKeys.includes(prop) && value !== customRole[prop]) {
strippedRole[customKeys.indexOf(prop)] = value;
} }
} }
customRoles.push(strippedRole); customRoles.push(strippedRole);
@ -119,38 +139,14 @@ export default new Vuex.Store({
rolesJSONbyId: () => rolesJSONbyId rolesJSONbyId: () => rolesJSONbyId
}, },
mutations: { mutations: {
toggleMenu({ grimoire }) { setZoom: set("zoom"),
grimoire.isMenuOpen = !grimoire.isMenuOpen; setBackground: set("background"),
}, toggleMuted: toggle("isMuted"),
toggleGrimoire({ grimoire }, isPublic) { toggleMenu: toggle("isMenuOpen"),
if (isPublic === true || isPublic === false) { toggleNightOrder: toggle("isNightOrder"),
grimoire.isPublic = isPublic; toggleNight: toggle("isNight"),
} else { toggleGrimoire: toggle("isPublic"),
grimoire.isPublic = !grimoire.isPublic; toggleImageOptIn: toggle("isImageOptIn"),
}
document.title = `Blood on the Clocktower ${
grimoire.isPublic ? "Town Square" : "Grimoire"
}`;
},
toggleNight({ grimoire }, isNight) {
if (isNight === true || isNight === false) {
grimoire.isNight = isNight;
} else {
grimoire.isNight = !grimoire.isNight;
}
},
toggleNightOrder({ grimoire }) {
grimoire.isNightOrder = !grimoire.isNightOrder;
},
setZoom({ grimoire }, zoom) {
grimoire.zoom = zoom;
},
setBackground({ grimoire }, background) {
grimoire.background = background;
},
setIsMuted({ grimoire }, isMuted) {
grimoire.isMuted = isMuted;
},
toggleModal({ modals }, name) { toggleModal({ modals }, name) {
if (name) { if (name) {
modals[name] = !modals[name]; modals[name] = !modals[name];
@ -168,6 +164,21 @@ export default new Vuex.Store({
setCustomRoles(state, roles) { setCustomRoles(state, roles) {
state.roles = new Map( state.roles = new Map(
roles roles
// replace numerical role object keys with matching key names
.map(role => {
if (role[0]) {
const customKeys = Object.keys(customRole);
const mappedRole = {};
for (let prop in role) {
if (customKeys[prop]) {
mappedRole[customKeys[prop]] = role[prop];
}
}
return mappedRole;
} else {
return role;
}
})
// map existing roles to base definition or pre-populate custom roles to ensure all properties // map existing roles to base definition or pre-populate custom roles to ensure all properties
.map( .map(
role => role =>
@ -175,16 +186,16 @@ export default new Vuex.Store({
state.roles.get(role.id) || state.roles.get(role.id) ||
Object.assign({}, customRole, role) Object.assign({}, customRole, role)
) )
// default empty icons to good / evil / traveler // default empty icons and placeholders
.map(role => { .map(role => {
if (rolesJSONbyId.get(role.id)) return role; if (rolesJSONbyId.get(role.id)) return role;
if (role.team === "townsfolk" || role.team === "outsider") { role.imageAlt = // map team to generic icon
role.image = role.image || imageBase + "good.png"; {
} else if (role.team === "demon" || role.team === "minion") { townsfolk: "good",
role.image = role.image || imageBase + "evil.png"; outsider: "outsider",
} else { minion: "minion",
role.image = role.image || imageBase + "custom.png"; demon: "evil"
} }[role.team] || "custom";
return role; return role;
}) })
// filter out roles that don't match an existing role and also don't have name/ability/team // filter out roles that don't match an existing role and also don't have name/ability/team

View File

@ -1,8 +1,3 @@
// helper functions
const set = key => (state, val) => {
state[key] = val;
};
/** /**
* Handle a vote request. * Handle a vote request.
* If the vote is from a seat that is already locked, ignore it. * If the vote is from a seat that is already locked, ignore it.
@ -37,6 +32,11 @@ const getters = {};
const actions = {}; const actions = {};
// mutations helper functions
const set = key => (state, val) => {
state[key] = val;
};
const mutations = { const mutations = {
setPlayerId: set("playerId"), setPlayerId: set("playerId"),
setSpectator: set("isSpectator"), setSpectator: set("isSpectator"),

View File

@ -1,16 +1,25 @@
module.exports = store => { module.exports = store => {
const updatePagetitle = isPublic =>
(document.title = `Blood on the Clocktower ${
isPublic ? "Town Square" : "Grimoire"
}`);
// initialize data // initialize data
if (localStorage.getItem("background")) { if (localStorage.getItem("background")) {
store.commit("setBackground", localStorage.background); store.commit("setBackground", localStorage.background);
} }
if (localStorage.getItem("muted")) { if (localStorage.getItem("muted")) {
store.commit("setIsMuted", true); store.commit("toggleMuted", true);
}
if (localStorage.getItem("imageOptIn")) {
store.commit("toggleImageOptIn", true);
} }
if (localStorage.getItem("zoom")) { if (localStorage.getItem("zoom")) {
store.commit("setZoom", parseFloat(localStorage.getItem("zoom"))); store.commit("setZoom", parseFloat(localStorage.getItem("zoom")));
} }
if (localStorage.isPublic !== undefined) { if (localStorage.getItem("isGrimoire")) {
store.commit("toggleGrimoire", JSON.parse(localStorage.isPublic)); store.commit("toggleGrimoire", false);
updatePagetitle(false);
} }
if (localStorage.roles !== undefined) { if (localStorage.roles !== undefined) {
store.commit("setCustomRoles", JSON.parse(localStorage.roles)); store.commit("setCustomRoles", JSON.parse(localStorage.roles));
@ -61,10 +70,12 @@ module.exports = store => {
store.subscribe(({ type, payload }, state) => { store.subscribe(({ type, payload }, state) => {
switch (type) { switch (type) {
case "toggleGrimoire": case "toggleGrimoire":
localStorage.setItem( if (!state.grimoire.isPublic) {
"isPublic", localStorage.setItem("isGrimoire", 1);
JSON.stringify(state.grimoire.isPublic) } else {
); localStorage.removeItem("isGrimoire");
}
updatePagetitle(state.grimoire.isPublic);
break; break;
case "setBackground": case "setBackground":
if (payload) { if (payload) {
@ -73,13 +84,20 @@ module.exports = store => {
localStorage.removeItem("background"); localStorage.removeItem("background");
} }
break; break;
case "setIsMuted": case "toggleMuted":
if (payload) { if (state.grimoire.isMuted) {
localStorage.setItem("muted", 1); localStorage.setItem("muted", 1);
} else { } else {
localStorage.removeItem("muted"); localStorage.removeItem("muted");
} }
break; break;
case "toggleImageOptIn":
if (state.grimoire.isImageOptIn) {
localStorage.setItem("imageOptIn", 1);
} else {
localStorage.removeItem("imageOptIn");
}
break;
case "setZoom": case "setZoom":
if (payload !== 0) { if (payload !== 0) {
localStorage.setItem("zoom", payload); localStorage.setItem("zoom", payload);
@ -97,10 +115,7 @@ module.exports = store => {
if (!payload.length) { if (!payload.length) {
localStorage.removeItem("roles"); localStorage.removeItem("roles");
} else { } else {
localStorage.setItem( localStorage.setItem("roles", JSON.stringify(payload));
"roles",
JSON.stringify(store.getters.customRoles)
);
} }
break; break;
case "players/setBluff": case "players/setBluff":

View File

@ -354,12 +354,10 @@ class LiveSession {
const { edition } = this._store.state; const { edition } = this._store.state;
let roles; let roles;
if (!edition.isOfficial) { if (!edition.isOfficial) {
roles = Array.from(this._store.state.roles.keys()); roles = this._store.getters.customRolesStripped;
} }
this._sendDirect(playerId, "edition", { this._sendDirect(playerId, "edition", {
edition: edition.isOfficial edition: edition.isOfficial ? { id: edition.id } : edition,
? { id: edition.id }
: Object.assign({}, edition, { logo: "" }),
...(roles ? { roles } : {}) ...(roles ? { roles } : {})
}); });
} }
@ -374,10 +372,7 @@ class LiveSession {
if (!this._isSpectator) return; if (!this._isSpectator) return;
this._store.commit("setEdition", edition); this._store.commit("setEdition", edition);
if (roles) { if (roles) {
this._store.commit( this._store.commit("setCustomRoles", roles);
"setCustomRoles",
roles.map(id => ({ id }))
);
if (this._store.state.roles.size !== roles.length) { if (this._store.state.roles.size !== roles.length) {
const missing = []; const missing = [];
roles.forEach(id => { roles.forEach(id => {