mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-26 22:29:28 +09:00
646 lines
16 KiB
TypeScript
646 lines
16 KiB
TypeScript
import Vuex from 'vuex';
|
|
import createPersistedState from 'vuex-persistedstate';
|
|
import * as nestedProperty from 'nested-property';
|
|
import { faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed, faColumns } from '@fortawesome/free-solid-svg-icons';
|
|
import { faBell, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons';
|
|
import { AiScript, utils, values } from '@syuilo/aiscript';
|
|
import { apiUrl, deckmode } from './config';
|
|
import { erase } from '../prelude/array';
|
|
|
|
export const defaultSettings = {
|
|
tutorial: 0,
|
|
keepCw: false,
|
|
showFullAcct: false,
|
|
rememberNoteVisibility: false,
|
|
defaultNoteVisibility: 'public',
|
|
defaultNoteLocalOnly: false,
|
|
uploadFolder: null,
|
|
pastedFileName: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
|
|
memo: null,
|
|
reactions: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
|
|
};
|
|
|
|
export const defaultDeviceUserSettings = {
|
|
visibility: 'public',
|
|
localOnly: false,
|
|
widgets: [],
|
|
tl: {
|
|
src: 'home'
|
|
},
|
|
menu: [
|
|
'notifications',
|
|
'messaging',
|
|
'drive',
|
|
'-',
|
|
'followRequests',
|
|
'featured',
|
|
'explore',
|
|
'announcements',
|
|
'search',
|
|
'-',
|
|
'deck',
|
|
],
|
|
deck: {
|
|
columns: [],
|
|
layout: [],
|
|
},
|
|
plugins: [],
|
|
};
|
|
|
|
export const defaultDeviceSettings = {
|
|
lang: null,
|
|
loadRawImages: false,
|
|
alwaysShowNsfw: false,
|
|
useOsNativeEmojis: false,
|
|
autoReload: false,
|
|
accounts: [],
|
|
recentEmojis: [],
|
|
themes: [],
|
|
darkTheme: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
|
|
lightTheme: '4eea646f-7afa-4645-83e9-83af0333cd37',
|
|
darkMode: false,
|
|
deckMode: false,
|
|
syncDeviceDarkMode: true,
|
|
animation: true,
|
|
animatedMfm: true,
|
|
imageNewTab: false,
|
|
showFixedPostForm: false,
|
|
disablePagesScript: true,
|
|
enableInfiniteScroll: true,
|
|
fixedWidgetsPosition: false,
|
|
useBlurEffectForModal: true,
|
|
roomGraphicsQuality: 'medium',
|
|
roomUseOrthographicCamera: true,
|
|
deckColumnAlign: 'left',
|
|
deckAlwaysShowMainColumn: true,
|
|
deckMainColumnPlace: 'left',
|
|
sfxVolume: 0.3,
|
|
sfxNote: 'syuilo/down',
|
|
sfxNoteMy: 'syuilo/up',
|
|
sfxNotification: 'syuilo/pope2',
|
|
sfxChat: 'syuilo/pope1',
|
|
sfxChatBg: 'syuilo/waon',
|
|
sfxAntenna: 'syuilo/triple',
|
|
userData: {},
|
|
};
|
|
|
|
function copy<T>(data: T): T {
|
|
return JSON.parse(JSON.stringify(data));
|
|
}
|
|
|
|
export default () => new Vuex.Store({
|
|
plugins: [createPersistedState({
|
|
paths: ['i', 'device', 'deviceUser', 'settings', 'instance']
|
|
})],
|
|
|
|
state: {
|
|
i: null,
|
|
pendingApiRequestsCount: 0,
|
|
spinner: null,
|
|
|
|
// Plugin
|
|
pluginContexts: new Map<string, AiScript>(),
|
|
postFormActions: [],
|
|
userActions: [],
|
|
noteActions: [],
|
|
},
|
|
|
|
getters: {
|
|
isSignedIn: state => state.i != null,
|
|
|
|
nav: (state, getters) => actions => ({
|
|
notifications: {
|
|
title: 'notifications',
|
|
icon: faBell,
|
|
get show() { return getters.isSignedIn; },
|
|
get indicated() { return getters.isSignedIn && state.i.hasUnreadNotification; },
|
|
to: '/my/notifications',
|
|
},
|
|
messaging: {
|
|
title: 'messaging',
|
|
icon: faComments,
|
|
get show() { return getters.isSignedIn; },
|
|
get indicated() { return getters.isSignedIn && state.i.hasUnreadMessagingMessage; },
|
|
to: '/my/messaging',
|
|
},
|
|
drive: {
|
|
title: 'drive',
|
|
icon: faCloud,
|
|
get show() { return getters.isSignedIn; },
|
|
to: '/my/drive',
|
|
},
|
|
followRequests: {
|
|
title: 'followRequests',
|
|
icon: faUserClock,
|
|
get show() { return getters.isSignedIn && state.i.isLocked; },
|
|
get indicated() { return getters.isSignedIn && state.i.hasPendingReceivedFollowRequest; },
|
|
to: '/my/follow-requests',
|
|
},
|
|
featured: {
|
|
title: 'featured',
|
|
icon: faFireAlt,
|
|
to: '/featured',
|
|
},
|
|
explore: {
|
|
title: 'explore',
|
|
icon: faHashtag,
|
|
to: '/explore',
|
|
},
|
|
announcements: {
|
|
title: 'announcements',
|
|
icon: faBroadcastTower,
|
|
get indicated() { return getters.isSignedIn && state.i.hasUnreadAnnouncement; },
|
|
to: '/announcements',
|
|
},
|
|
search: {
|
|
title: 'search',
|
|
icon: faSearch,
|
|
action: () => actions.search(),
|
|
},
|
|
lists: {
|
|
title: 'lists',
|
|
icon: faListUl,
|
|
get show() { return getters.isSignedIn; },
|
|
to: '/my/lists',
|
|
},
|
|
groups: {
|
|
title: 'groups',
|
|
icon: faUsers,
|
|
get show() { return getters.isSignedIn; },
|
|
to: '/my/groups',
|
|
},
|
|
antennas: {
|
|
title: 'antennas',
|
|
icon: faSatellite,
|
|
get show() { return getters.isSignedIn; },
|
|
to: '/my/antennas',
|
|
},
|
|
mentions: {
|
|
title: 'mentions',
|
|
icon: faAt,
|
|
get show() { return getters.isSignedIn; },
|
|
get indicated() { return getters.isSignedIn && state.i.hasUnreadMentions; },
|
|
to: '/my/mentions',
|
|
},
|
|
messages: {
|
|
title: 'directNotes',
|
|
icon: faEnvelope,
|
|
get show() { return getters.isSignedIn; },
|
|
get indicated() { return getters.isSignedIn && state.i.hasUnreadSpecifiedNotes; },
|
|
to: '/my/messages',
|
|
},
|
|
favorites: {
|
|
title: 'favorites',
|
|
icon: faStar,
|
|
get show() { return getters.isSignedIn; },
|
|
to: '/my/favorites',
|
|
},
|
|
pages: {
|
|
title: 'pages',
|
|
icon: faFileAlt,
|
|
get show() { return getters.isSignedIn; },
|
|
to: '/my/pages',
|
|
},
|
|
games: {
|
|
title: 'games',
|
|
icon: faGamepad,
|
|
to: '/games',
|
|
},
|
|
scratchpad: {
|
|
title: 'scratchpad',
|
|
icon: faTerminal,
|
|
to: '/scratchpad',
|
|
},
|
|
rooms: {
|
|
title: 'rooms',
|
|
icon: faDoorClosed,
|
|
get show() { return getters.isSignedIn; },
|
|
get to() { return `/@${state.i.username}/room`; },
|
|
},
|
|
deck: {
|
|
title: deckmode ? 'undeck' : 'deck',
|
|
icon: faColumns,
|
|
action: () => {
|
|
localStorage.setItem('deckmode', (!deckmode).toString());
|
|
location.reload();
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
|
|
mutations: {
|
|
updateI(state, x) {
|
|
state.i = x;
|
|
},
|
|
|
|
updateIKeyValue(state, { key, value }) {
|
|
state.i[key] = value;
|
|
},
|
|
|
|
initPlugin(state, { plugin, aiscript }) {
|
|
state.pluginContexts.set(plugin.id, aiscript);
|
|
},
|
|
|
|
registerPostFormAction(state, { pluginId, title, handler }) {
|
|
state.postFormActions.push({
|
|
title, handler: (form, update) => {
|
|
state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
|
|
update(key.value, value.value);
|
|
})]);
|
|
}
|
|
});
|
|
},
|
|
|
|
registerUserAction(state, { pluginId, title, handler }) {
|
|
state.userActions.push({
|
|
title, handler: (user) => {
|
|
state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(user)]);
|
|
}
|
|
});
|
|
},
|
|
|
|
registerNoteAction(state, { pluginId, title, handler }) {
|
|
state.noteActions.push({
|
|
title, handler: (note) => {
|
|
state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]);
|
|
}
|
|
});
|
|
},
|
|
},
|
|
|
|
actions: {
|
|
async login(ctx, i) {
|
|
ctx.commit('updateI', i);
|
|
ctx.commit('settings/init', i.clientData);
|
|
ctx.commit('deviceUser/init', ctx.state.device.userData[i.id] || {});
|
|
// TODO: ローカルストレージを消してページリロードしたときは i が無いのでその場合のハンドリングをよしなにやる
|
|
await ctx.dispatch('addAcount', { id: i.id, i: localStorage.getItem('i') });
|
|
},
|
|
|
|
addAcount(ctx, info) {
|
|
if (!ctx.state.device.accounts.some(x => x.id === info.id)) {
|
|
ctx.commit('device/set', {
|
|
key: 'accounts',
|
|
value: ctx.state.device.accounts.concat([{ id: info.id, token: info.i }])
|
|
});
|
|
}
|
|
},
|
|
|
|
logout(ctx) {
|
|
ctx.commit('device/setUserData', { userId: ctx.state.i.id, data: ctx.state.deviceUser });
|
|
ctx.commit('updateI', null);
|
|
ctx.commit('settings/init', {});
|
|
ctx.commit('deviceUser/init', {});
|
|
localStorage.removeItem('i');
|
|
document.cookie = `igi=; path=/`;
|
|
},
|
|
|
|
async switchAccount(ctx, i) {
|
|
ctx.commit('device/setUserData', { userId: ctx.state.i.id, data: ctx.state.deviceUser });
|
|
localStorage.setItem('i', i.token);
|
|
await ctx.dispatch('login', i);
|
|
},
|
|
|
|
mergeMe(ctx, me) {
|
|
// TODO: プロパティ一つ一つに対してコミットが発生するのはアレなので良い感じにする
|
|
for (const [key, value] of Object.entries(me)) {
|
|
ctx.commit('updateIKeyValue', { key, value });
|
|
}
|
|
|
|
if (me.clientData) {
|
|
ctx.commit('settings/init', me.clientData);
|
|
}
|
|
},
|
|
|
|
api(ctx, { endpoint, data, token }) {
|
|
if (++ctx.state.pendingApiRequestsCount === 1) {
|
|
// TODO: spinnerの表示はstoreでやらない
|
|
ctx.state.spinner = document.createElement('div');
|
|
ctx.state.spinner.setAttribute('id', 'wait');
|
|
document.body.appendChild(ctx.state.spinner);
|
|
}
|
|
|
|
const onFinally = () => {
|
|
if (--ctx.state.pendingApiRequestsCount === 0) ctx.state.spinner.parentNode.removeChild(ctx.state.spinner);
|
|
};
|
|
|
|
const promise = new Promise((resolve, reject) => {
|
|
// Append a credential
|
|
if (ctx.getters.isSignedIn) (data as any).i = ctx.state.i.token;
|
|
if (token !== undefined) (data as any).i = token;
|
|
|
|
// Send request
|
|
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
credentials: 'omit',
|
|
cache: 'no-cache'
|
|
}).then(async (res) => {
|
|
const body = res.status === 204 ? null : await res.json();
|
|
|
|
if (res.status === 200) {
|
|
resolve(body);
|
|
} else if (res.status === 204) {
|
|
resolve();
|
|
} else {
|
|
reject(body.error);
|
|
}
|
|
}).catch(reject);
|
|
});
|
|
|
|
promise.then(onFinally, onFinally);
|
|
|
|
return promise;
|
|
}
|
|
},
|
|
|
|
modules: {
|
|
instance: {
|
|
namespaced: true,
|
|
|
|
state: {
|
|
meta: null
|
|
},
|
|
|
|
mutations: {
|
|
set(state, meta) {
|
|
state.meta = meta;
|
|
},
|
|
},
|
|
|
|
actions: {
|
|
async fetch(ctx) {
|
|
const meta = await ctx.dispatch('api', {
|
|
endpoint: 'meta',
|
|
data: {
|
|
detail: false
|
|
}
|
|
}, { root: true });
|
|
|
|
ctx.commit('set', meta);
|
|
}
|
|
}
|
|
},
|
|
|
|
device: {
|
|
namespaced: true,
|
|
|
|
state: defaultDeviceSettings,
|
|
|
|
mutations: {
|
|
set(state, x: { key: string; value: any }) {
|
|
state[x.key] = x.value;
|
|
},
|
|
|
|
setUserData(state, x: { userId: string; data: any }) {
|
|
state.userData[x.userId] = copy(x.data);
|
|
},
|
|
}
|
|
},
|
|
|
|
deviceUser: {
|
|
namespaced: true,
|
|
|
|
state: defaultDeviceUserSettings,
|
|
|
|
mutations: {
|
|
init(state, x) {
|
|
for (const [key, value] of Object.entries(defaultDeviceUserSettings)) {
|
|
if (x[key]) {
|
|
state[key] = x[key];
|
|
} else {
|
|
state[key] = value;
|
|
}
|
|
}
|
|
},
|
|
|
|
set(state, x: { key: string; value: any }) {
|
|
state[x.key] = x.value;
|
|
},
|
|
|
|
setTl(state, x) {
|
|
state.tl = {
|
|
src: x.src,
|
|
arg: x.arg
|
|
};
|
|
},
|
|
|
|
setMenu(state, menu) {
|
|
state.menu = menu;
|
|
},
|
|
|
|
setVisibility(state, visibility) {
|
|
state.visibility = visibility;
|
|
},
|
|
|
|
setLocalOnly(state, localOnly) {
|
|
state.localOnly = localOnly;
|
|
},
|
|
|
|
setWidgets(state, widgets) {
|
|
state.widgets = widgets;
|
|
},
|
|
|
|
addWidget(state, widget) {
|
|
state.widgets.unshift(widget);
|
|
},
|
|
|
|
removeWidget(state, widget) {
|
|
state.widgets = state.widgets.filter(w => w.id != widget.id);
|
|
},
|
|
|
|
updateWidget(state, x) {
|
|
const w = state.widgets.find(w => w.id === x.id);
|
|
if (w) {
|
|
w.data = x.data;
|
|
}
|
|
},
|
|
|
|
//#region Deck
|
|
addDeckColumn(state, column) {
|
|
if (column.name == undefined) column.name = null;
|
|
state.deck.columns.push(column);
|
|
state.deck.layout.push([column.id]);
|
|
},
|
|
|
|
removeDeckColumn(state, id) {
|
|
state.deck.columns = state.deck.columns.filter(c => c.id != id);
|
|
state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
|
|
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
|
|
},
|
|
|
|
swapDeckColumn(state, x) {
|
|
const a = x.a;
|
|
const b = x.b;
|
|
const aX = state.deck.layout.findIndex(ids => ids.indexOf(a) != -1);
|
|
const aY = state.deck.layout[aX].findIndex(id => id == a);
|
|
const bX = state.deck.layout.findIndex(ids => ids.indexOf(b) != -1);
|
|
const bY = state.deck.layout[bX].findIndex(id => id == b);
|
|
state.deck.layout[aX][aY] = b;
|
|
state.deck.layout[bX][bY] = a;
|
|
},
|
|
|
|
swapLeftDeckColumn(state, id) {
|
|
state.deck.layout.some((ids, i) => {
|
|
if (ids.indexOf(id) != -1) {
|
|
const left = state.deck.layout[i - 1];
|
|
if (left) {
|
|
// https://vuejs.org/v2/guide/list.html#Caveats
|
|
//state.deck.layout[i - 1] = state.deck.layout[i];
|
|
//state.deck.layout[i] = left;
|
|
state.deck.layout.splice(i - 1, 1, state.deck.layout[i]);
|
|
state.deck.layout.splice(i, 1, left);
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
},
|
|
|
|
swapRightDeckColumn(state, id) {
|
|
state.deck.layout.some((ids, i) => {
|
|
if (ids.indexOf(id) != -1) {
|
|
const right = state.deck.layout[i + 1];
|
|
if (right) {
|
|
// https://vuejs.org/v2/guide/list.html#Caveats
|
|
//state.deck.layout[i + 1] = state.deck.layout[i];
|
|
//state.deck.layout[i] = right;
|
|
state.deck.layout.splice(i + 1, 1, state.deck.layout[i]);
|
|
state.deck.layout.splice(i, 1, right);
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
},
|
|
|
|
swapUpDeckColumn(state, id) {
|
|
const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1);
|
|
ids.some((x, i) => {
|
|
if (x == id) {
|
|
const up = ids[i - 1];
|
|
if (up) {
|
|
// https://vuejs.org/v2/guide/list.html#Caveats
|
|
//ids[i - 1] = id;
|
|
//ids[i] = up;
|
|
ids.splice(i - 1, 1, id);
|
|
ids.splice(i, 1, up);
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
},
|
|
|
|
swapDownDeckColumn(state, id) {
|
|
const ids = state.deck.layout.find(ids => ids.indexOf(id) != -1);
|
|
ids.some((x, i) => {
|
|
if (x == id) {
|
|
const down = ids[i + 1];
|
|
if (down) {
|
|
// https://vuejs.org/v2/guide/list.html#Caveats
|
|
//ids[i + 1] = id;
|
|
//ids[i] = down;
|
|
ids.splice(i + 1, 1, id);
|
|
ids.splice(i, 1, down);
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
},
|
|
|
|
stackLeftDeckColumn(state, id) {
|
|
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
|
|
state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
|
|
const left = state.deck.layout[i - 1];
|
|
if (left) state.deck.layout[i - 1].push(id);
|
|
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
|
|
},
|
|
|
|
popRightDeckColumn(state, id) {
|
|
const i = state.deck.layout.findIndex(ids => ids.indexOf(id) != -1);
|
|
state.deck.layout = state.deck.layout.map(ids => erase(id, ids));
|
|
state.deck.layout.splice(i + 1, 0, [id]);
|
|
state.deck.layout = state.deck.layout.filter(ids => ids.length > 0);
|
|
},
|
|
|
|
addDeckWidget(state, x) {
|
|
const column = state.deck.columns.find(c => c.id == x.id);
|
|
if (column == null) return;
|
|
if (column.widgets == null) column.widgets = [];
|
|
column.widgets.unshift(x.widget);
|
|
},
|
|
|
|
removeDeckWidget(state, x) {
|
|
const column = state.deck.columns.find(c => c.id == x.id);
|
|
if (column == null) return;
|
|
column.widgets = column.widgets.filter(w => w.id != x.widget.id);
|
|
},
|
|
|
|
renameDeckColumn(state, x) {
|
|
const column = state.deck.columns.find(c => c.id == x.id);
|
|
if (column == null) return;
|
|
column.name = x.name;
|
|
},
|
|
|
|
updateDeckColumn(state, x) {
|
|
let column = state.deck.columns.find(c => c.id == x.id);
|
|
if (column == null) return;
|
|
column = x;
|
|
},
|
|
//#endregion
|
|
|
|
installPlugin(state, { meta, ast }) {
|
|
state.plugins.push({
|
|
id: meta.id,
|
|
name: meta.name,
|
|
version: meta.version,
|
|
author: meta.author,
|
|
description: meta.description,
|
|
ast: ast
|
|
});
|
|
},
|
|
|
|
uninstallPlugin(state, id) {
|
|
state.plugins = state.plugins.filter(x => x.id != id);
|
|
},
|
|
}
|
|
},
|
|
|
|
settings: {
|
|
namespaced: true,
|
|
|
|
state: defaultSettings,
|
|
|
|
mutations: {
|
|
set(state, x: { key: string; value: any }) {
|
|
nestedProperty.set(state, x.key, x.value);
|
|
},
|
|
|
|
init(state, x) {
|
|
for (const [key, value] of Object.entries(defaultSettings)) {
|
|
if (x[key]) {
|
|
state[key] = x[key];
|
|
} else {
|
|
state[key] = value;
|
|
}
|
|
}
|
|
},
|
|
},
|
|
|
|
actions: {
|
|
set(ctx, x) {
|
|
ctx.commit('set', x);
|
|
|
|
if (ctx.rootGetters.isSignedIn) {
|
|
ctx.dispatch('api', {
|
|
endpoint: 'i/update-client-setting',
|
|
data: {
|
|
name: x.key,
|
|
value: x.value
|
|
}
|
|
}, { root: true });
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
});
|