Migrate to Vue3 (#6587)
* Update reaction.vue * fix bug * wip * wip * wjio * wip * Revert "wip" This reverts commit e427f2160adf4e8a4147006e25a89854edab0033. * wip * wip * wip * Update init.ts * Update drive-window.vue * wip * wip * Use PascalCase for components * Use PascalCase for components * update dep * wip * wip * wip * Update init.ts * wip * Update paging.ts * Update test.vue * watch deep * wip * lint * wip * wip * wip * wip * wiop * wip * Update webpack.config.ts * alllow null poll * wip * wip * wip * wiop * UI redesign & refactor (#6714) * wip * wip * wip * wip * wip * Update drive.vue * Update word-mute.vue * wip * wip * wip * clean up * wip * Update default.vue * wip * Update notes.vue * Update mfm.ts * Update index.home.vue * Update post-form.vue * Update post-form-attaches.vue * wip * Update post-form.vue * Update sidebar.vue * wip * wip * Update index.vue * wip * Update default.vue * Update index.vue * Update index.vue * wip * Update post-form-attaches.vue * Update note.vue * wip * clean up * Update notes.vue * wip * wip * Update ja-JP.yml * wip * wip * Update index.vue * wip * wip * wip * wip * wip * wip * wip * wip * Update default.vue * wip * Update _dark.json5 * wip * wip * wip * clean up * wip * wip * Update index.vue * Update test.vue * wip * wip * fix * wip * wip * wip * wip * clena yop * wip * wip * Update store.ts * Update messaging-room.vue * Update default.widgets.vue * fix * wip * wip * Update modal.vue * wip * Update os.ts * Update os.ts * Update deck.vue * Update init.ts * wip * Update ja-JP.yml * v-sizeは単にwindowのresizeを監視するだけで良いかもしれない * Update modal.vue * wip * Update tooltip.ts * wip * wip * wip * wip * wip * Update image-viewer.vue * wip * wip * Update style.scss * Update style.scss * Update visitor.vue * wip * Update init.ts * Update init.ts * wip * wip * Update visitor.vue * Update visitor.vue * Update visitor.vue * Update visitor.vue * wip * wip * Update modal.vue * Update header.vue * Update menu.vue * Update about.vue * Update about-misskey.vue * wip * wip * Update visitor.vue * Update tooltip.ts * wip * Update drive.vue * wip * Update style.scss * Update header.vue * wip * wip * Update users.user.vue * Update announcements.vue * wip * wip * wip * Update emojis.vue * wip * Update emojis.vue * Update style.scss * Update users.vue * wip * Update style.scss * wip * Update welcome.entrance.vue * Update radio.vue * Update size.ts * Update emoji-edit-dialog.vue * wip * Update emojis.vue * wip * Update emojis.vue * Update emojis.vue * Update emojis.vue * wip * wip * wip * wip * Update file-dialog.vue * wip * wip * Update token-generate-window.vue * Update notification-setting-window.vue * wip * wip * Update _error_.vue * Update ja-JP.yml * wip * wip * Update store.ts * Update emojis.vue * Update emojis.vue * Update emojis.vue * Update announcements.vue * Update store.ts * wip * Update page-editor.vue * wip * wip * Update modal.vue * wip * Update select-file.ts * Update timeline.vue * Update emojis.vue * Update os.ts * wip * Update user-select.vue * Update mfm.ts * Update get-file-info.ts * Update drive.vue * Update init.ts * Update mfm.ts * wip * wip * Update window.vue * Update note.vue * wip * wip * Update user-info.vue * wip * wip * wip * wip * wip * Update header.vue * Update header.vue * wip * Update explore.vue * wip * wip * wip * Update webpack.config.ts * wip * wip * wip * wip * wip * wip * Update autocomplete.ts * wip * wip * wip * Update toast.vue * wip * Update post-form-dialog.vue * wip * wip * wip * wip * wip * Update users.vue * wip * Update explore.vue * wip * wip * wip * Update package.json * wip * Update icon-dialog.vue * wip * wip * Update user-preview.ts * wip * wip * wip * wip * wip * Update instance.vue * Update user-name.vue * Update federation.vue * Update instance.vue * wip * wip * Update tag.vue * wip * wip * wip * wip * wip * Update instance.vue * wip * Update os.ts * Update os.ts * wip * wip * wip * Update router.ts * wip * Update init.ts * Update note.vue * Update messages.vue * wip * wip * wip * wip * wip * google * wip * wip * wip * wip * Update theme-editor.vue * wip * wip * Update room.vue * Update channel-editor.vue * wip * Update window.vue * Update window.vue * wip * Update window.vue * Update window.vue * wip * Update menu.vue * wip * wip * wip * wip * Update messaging-room.vue * wip * Update post-form.vue * Update default.widgets.vue * Update window.vue * wip
This commit is contained in:
@ -1,22 +1,22 @@
|
||||
import { utils, values } from '@syuilo/aiscript';
|
||||
import { jsToVal } from '@syuilo/aiscript/built/interpreter/util';
|
||||
import { store } from '@/store';
|
||||
import * as os from '@/os';
|
||||
|
||||
// TODO: vue3に移行した折にはvmを渡す必要は無くなるはず
|
||||
export function createAiScriptEnv(vm, opts) {
|
||||
export function createAiScriptEnv(opts) {
|
||||
let apiRequests = 0;
|
||||
return {
|
||||
USER_ID: vm.$store.getters.isSignedIn ? values.STR(vm.$store.state.i.id) : values.NULL,
|
||||
USER_NAME: vm.$store.getters.isSignedIn ? values.STR(vm.$store.state.i.name) : values.NULL,
|
||||
USER_USERNAME: vm.$store.getters.isSignedIn ? values.STR(vm.$store.state.i.username) : values.NULL,
|
||||
USER_ID: store.getters.isSignedIn ? values.STR(store.state.i.id) : values.NULL,
|
||||
USER_NAME: store.getters.isSignedIn ? values.STR(store.state.i.name) : values.NULL,
|
||||
USER_USERNAME: store.getters.isSignedIn ? values.STR(store.state.i.username) : values.NULL,
|
||||
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
|
||||
await vm.$root.dialog({
|
||||
await os.dialog({
|
||||
type: type ? type.value : 'info',
|
||||
title: title.value,
|
||||
text: text.value,
|
||||
});
|
||||
}),
|
||||
'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
|
||||
const confirm = await vm.$root.dialog({
|
||||
const confirm = await os.dialog({
|
||||
type: type ? type.value : 'question',
|
||||
showCancelButton: true,
|
||||
title: title.value,
|
||||
@ -28,7 +28,7 @@ export function createAiScriptEnv(vm, opts) {
|
||||
if (token) utils.assertString(token);
|
||||
apiRequests++;
|
||||
if (apiRequests > 16) return values.NULL;
|
||||
const res = await vm.$root.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null));
|
||||
const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null));
|
||||
return utils.jsToVal(res);
|
||||
}),
|
||||
'Mk:save': values.FN_NATIVE(([key, value]) => {
|
||||
@ -42,45 +42,3 @@ export function createAiScriptEnv(vm, opts) {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: vue3に移行した折にはvmを渡す必要は無くなるはず
|
||||
export function createPluginEnv(vm, opts) {
|
||||
const config = new Map();
|
||||
for (const [k, v] of Object.entries(opts.plugin.config || {})) {
|
||||
config.set(k, jsToVal(opts.plugin.configData[k] || v.default));
|
||||
}
|
||||
|
||||
return {
|
||||
...createAiScriptEnv(vm, { ...opts, token: opts.plugin.token }),
|
||||
//#region Deprecated
|
||||
'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
|
||||
vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => {
|
||||
vm.$store.commit('registerUserAction', { pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
|
||||
vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
//#endregion
|
||||
'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
|
||||
vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => {
|
||||
vm.$store.commit('registerUserAction', { pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => {
|
||||
vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
|
||||
}),
|
||||
'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
|
||||
vm.$store.commit('registerNoteViewInterruptor', { pluginId: opts.plugin.id, handler });
|
||||
}),
|
||||
'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => {
|
||||
vm.$store.commit('registerNotePostInterruptor', { pluginId: opts.plugin.id, handler });
|
||||
}),
|
||||
'Plugin:open_url': values.FN_NATIVE(([url]) => {
|
||||
window.open(url.value, '_blank');
|
||||
}),
|
||||
'Plugin:config': values.OBJ(config),
|
||||
};
|
||||
}
|
||||
|
251
src/client/scripts/autocomplete.ts
Normal file
251
src/client/scripts/autocomplete.ts
Normal file
@ -0,0 +1,251 @@
|
||||
import { Ref, ref } from 'vue';
|
||||
import * as getCaretCoordinates from 'textarea-caret';
|
||||
import { toASCII } from 'punycode';
|
||||
import { popup } from '@/os';
|
||||
|
||||
export class Autocomplete {
|
||||
private suggestion: {
|
||||
x: Ref<number>;
|
||||
y: Ref<number>;
|
||||
q: Ref<string>;
|
||||
close: Function;
|
||||
};
|
||||
private textarea: any;
|
||||
private vm: any;
|
||||
private currentType: string;
|
||||
private opts: {
|
||||
model: string;
|
||||
};
|
||||
private opening: boolean;
|
||||
|
||||
private get text(): string {
|
||||
return this.vm[this.opts.model];
|
||||
}
|
||||
|
||||
private set text(text: string) {
|
||||
this.vm[this.opts.model] = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 対象のテキストエリアを与えてインスタンスを初期化します。
|
||||
*/
|
||||
constructor(textarea, vm, opts) {
|
||||
//#region BIND
|
||||
this.onInput = this.onInput.bind(this);
|
||||
this.complete = this.complete.bind(this);
|
||||
this.close = this.close.bind(this);
|
||||
//#endregion
|
||||
|
||||
this.suggestion = null;
|
||||
this.textarea = textarea;
|
||||
this.vm = vm;
|
||||
this.opts = opts;
|
||||
this.opening = false;
|
||||
|
||||
this.attach();
|
||||
}
|
||||
|
||||
/**
|
||||
* このインスタンスにあるテキストエリアの入力のキャプチャを開始します。
|
||||
*/
|
||||
public attach() {
|
||||
this.textarea.addEventListener('input', this.onInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* このインスタンスにあるテキストエリアの入力のキャプチャを解除します。
|
||||
*/
|
||||
public detach() {
|
||||
this.textarea.removeEventListener('input', this.onInput);
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* テキスト入力時
|
||||
*/
|
||||
private onInput() {
|
||||
const caretPos = this.textarea.selectionStart;
|
||||
const text = this.text.substr(0, caretPos).split('\n').pop();
|
||||
|
||||
const mentionIndex = text.lastIndexOf('@');
|
||||
const hashtagIndex = text.lastIndexOf('#');
|
||||
const emojiIndex = text.lastIndexOf(':');
|
||||
|
||||
const max = Math.max(
|
||||
mentionIndex,
|
||||
hashtagIndex,
|
||||
emojiIndex);
|
||||
|
||||
if (max == -1) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const isMention = mentionIndex != -1;
|
||||
const isHashtag = hashtagIndex != -1;
|
||||
const isEmoji = emojiIndex != -1;
|
||||
|
||||
let opened = false;
|
||||
|
||||
if (isMention) {
|
||||
const username = text.substr(mentionIndex + 1);
|
||||
if (username != '' && username.match(/^[a-zA-Z0-9_]+$/)) {
|
||||
this.open('user', username);
|
||||
opened = true;
|
||||
} else if (username === '') {
|
||||
this.open('user', null);
|
||||
opened = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHashtag && !opened) {
|
||||
const hashtag = text.substr(hashtagIndex + 1);
|
||||
if (!hashtag.includes(' ')) {
|
||||
this.open('hashtag', hashtag);
|
||||
opened = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmoji && !opened) {
|
||||
const emoji = text.substr(emojiIndex + 1);
|
||||
if (!emoji.includes(' ')) {
|
||||
this.open('emoji', emoji);
|
||||
opened = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!opened) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* サジェストを提示します。
|
||||
*/
|
||||
private async open(type: string, q: string) {
|
||||
if (type != this.currentType) {
|
||||
this.close();
|
||||
}
|
||||
if (this.opening) return;
|
||||
this.opening = true;
|
||||
this.currentType = type;
|
||||
|
||||
//#region サジェストを表示すべき位置を計算
|
||||
const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart);
|
||||
|
||||
const rect = this.textarea.getBoundingClientRect();
|
||||
|
||||
const x = rect.left + caretPosition.left - this.textarea.scrollLeft;
|
||||
const y = rect.top + caretPosition.top - this.textarea.scrollTop;
|
||||
//#endregion
|
||||
|
||||
if (this.suggestion) {
|
||||
this.suggestion.x.value = x;
|
||||
this.suggestion.y.value = y;
|
||||
this.suggestion.q.value = q;
|
||||
|
||||
this.opening = false;
|
||||
} else {
|
||||
const MkAutocomplete = await import('@/components/autocomplete.vue');
|
||||
|
||||
const _x = ref(x);
|
||||
const _y = ref(y);
|
||||
const _q = ref(q);
|
||||
|
||||
const { dispose } = popup(MkAutocomplete, {
|
||||
textarea: this.textarea,
|
||||
close: this.close,
|
||||
type: type,
|
||||
q: _q,
|
||||
x: _x,
|
||||
y: _y,
|
||||
}, {
|
||||
done: (res) => {
|
||||
this.complete(res);
|
||||
}
|
||||
});
|
||||
|
||||
this.suggestion = {
|
||||
q: _q,
|
||||
x: _x,
|
||||
y: _y,
|
||||
close: () => dispose(),
|
||||
};
|
||||
|
||||
this.opening = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* サジェストを閉じます。
|
||||
*/
|
||||
private close() {
|
||||
if (this.suggestion == null) return;
|
||||
|
||||
this.suggestion.close();
|
||||
this.suggestion = null;
|
||||
|
||||
this.textarea.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* オートコンプリートする
|
||||
*/
|
||||
private complete({ type, value }) {
|
||||
this.close();
|
||||
|
||||
const caret = this.textarea.selectionStart;
|
||||
|
||||
if (type == 'user') {
|
||||
const source = this.text;
|
||||
|
||||
const before = source.substr(0, caret);
|
||||
const trimmedBefore = before.substring(0, before.lastIndexOf('@'));
|
||||
const after = source.substr(caret);
|
||||
|
||||
const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`;
|
||||
|
||||
// 挿入
|
||||
this.text = `${trimmedBefore}@${acct} ${after}`;
|
||||
|
||||
// キャレットを戻す
|
||||
this.vm.$nextTick(() => {
|
||||
this.textarea.focus();
|
||||
const pos = trimmedBefore.length + (acct.length + 2);
|
||||
this.textarea.setSelectionRange(pos, pos);
|
||||
});
|
||||
} else if (type == 'hashtag') {
|
||||
const source = this.text;
|
||||
|
||||
const before = source.substr(0, caret);
|
||||
const trimmedBefore = before.substring(0, before.lastIndexOf('#'));
|
||||
const after = source.substr(caret);
|
||||
|
||||
// 挿入
|
||||
this.text = `${trimmedBefore}#${value} ${after}`;
|
||||
|
||||
// キャレットを戻す
|
||||
this.vm.$nextTick(() => {
|
||||
this.textarea.focus();
|
||||
const pos = trimmedBefore.length + (value.length + 2);
|
||||
this.textarea.setSelectionRange(pos, pos);
|
||||
});
|
||||
} else if (type == 'emoji') {
|
||||
const source = this.text;
|
||||
|
||||
const before = source.substr(0, caret);
|
||||
const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
|
||||
const after = source.substr(caret);
|
||||
|
||||
// 挿入
|
||||
this.text = trimmedBefore + value + after;
|
||||
|
||||
// キャレットを戻す
|
||||
this.vm.$nextTick(() => {
|
||||
this.textarea.focus();
|
||||
const pos = trimmedBefore.length + value.length;
|
||||
this.textarea.setSelectionRange(pos, pos);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
9
src/client/scripts/extract-avg-color-from-blurhash.ts
Normal file
9
src/client/scripts/extract-avg-color-from-blurhash.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function extractAvgColorFromBlurhash(hash: string) {
|
||||
return typeof hash == 'string'
|
||||
? '#' + [...hash.slice(2, 6)]
|
||||
.map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x))
|
||||
.reduce((a, c) => a * 83 + c, 0)
|
||||
.toString(16)
|
||||
.padStart(6, '0')
|
||||
: undefined;
|
||||
}
|
@ -1,21 +1,25 @@
|
||||
export function focusPrev(el: Element | null, self = false) {
|
||||
export function focusPrev(el: Element | null, self = false, scroll = true) {
|
||||
if (el == null) return;
|
||||
if (!self) el = el.previousElementSibling;
|
||||
if (el) {
|
||||
if (el.hasAttribute('tabindex')) {
|
||||
(el as HTMLElement).focus();
|
||||
(el as HTMLElement).focus({
|
||||
preventScroll: !scroll
|
||||
});
|
||||
} else {
|
||||
focusPrev(el.previousElementSibling, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function focusNext(el: Element | null, self = false) {
|
||||
export function focusNext(el: Element | null, self = false, scroll = true) {
|
||||
if (el == null) return;
|
||||
if (!self) el = el.nextElementSibling;
|
||||
if (el) {
|
||||
if (el.hasAttribute('tabindex')) {
|
||||
(el as HTMLElement).focus();
|
||||
(el as HTMLElement).focus({
|
||||
preventScroll: !scroll
|
||||
});
|
||||
} else {
|
||||
focusPrev(el.nextElementSibling, true);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import parseAcct from '../../misc/acct/parse';
|
||||
import { host as localHost } from '../config';
|
||||
import { host as localHost } from '@/config';
|
||||
|
||||
export async function genSearchQuery(v: any, q: string) {
|
||||
let host: string;
|
||||
@ -13,7 +13,7 @@ export async function genSearchQuery(v: any, q: string) {
|
||||
host = at;
|
||||
}
|
||||
} else {
|
||||
const user = await v.$root.api('users/show', parseAcct(at)).catch(x => null);
|
||||
const user = await v.os.api('users/show', parseAcct(at)).catch(x => null);
|
||||
if (user) {
|
||||
userId = user.id;
|
||||
} else {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { url as instanceUrl } from '../config';
|
||||
import { url as instanceUrl } from '@/config';
|
||||
import * as url from '../../prelude/url';
|
||||
|
||||
export function getStaticImageUrl(baseUrl: string): string {
|
||||
|
194
src/client/scripts/get-user-menu.ts
Normal file
194
src/client/scripts/get-user-menu.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons';
|
||||
import { i18n } from '@/i18n';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { host } from '@/config';
|
||||
import getAcct from '../../misc/acct/render';
|
||||
import * as os from '@/os';
|
||||
import { store, userActions } from '@/store';
|
||||
import { router } from '@/router';
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { popout } from './popout';
|
||||
|
||||
export function getUserMenu(user) {
|
||||
async function pushList() {
|
||||
const t = i18n.global.t('selectList'); // なぜか後で参照すると null になるので最初にメモリに確保しておく
|
||||
const lists = await os.api('users/lists/list');
|
||||
if (lists.length === 0) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: i18n.global.t('youHaveNoLists')
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { canceled, result: listId } = await os.dialog({
|
||||
type: null,
|
||||
title: t,
|
||||
select: {
|
||||
items: lists.map(list => ({
|
||||
value: list.id, text: list.name
|
||||
}))
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('users/lists/push', {
|
||||
listId: listId,
|
||||
userId: user.id
|
||||
});
|
||||
}
|
||||
|
||||
async function inviteGroup() {
|
||||
const groups = await os.api('users/groups/owned');
|
||||
if (groups.length === 0) {
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: i18n.global.t('youHaveNoGroups')
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { canceled, result: groupId } = await os.dialog({
|
||||
type: null,
|
||||
title: i18n.global.t('group'),
|
||||
select: {
|
||||
items: groups.map(group => ({
|
||||
value: group.id, text: group.name
|
||||
}))
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
os.apiWithDialog('users/groups/invite', {
|
||||
groupId: groupId,
|
||||
userId: user.id
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleMute() {
|
||||
os.apiWithDialog(user.isMuted ? 'mute/delete' : 'mute/create', {
|
||||
userId: user.id
|
||||
}).then(() => {
|
||||
user.isMuted = !user.isMuted;
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleBlock() {
|
||||
if (!await getConfirmed(user.isBlocking ? i18n.global.t('unblockConfirm') : i18n.global.t('blockConfirm'))) return;
|
||||
|
||||
os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', {
|
||||
userId: user.id
|
||||
}).then(() => {
|
||||
user.isBlocking = !user.isBlocking;
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleSilence() {
|
||||
if (!await getConfirmed(i18n.global.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return;
|
||||
|
||||
os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', {
|
||||
userId: user.id
|
||||
}).then(() => {
|
||||
user.isSilenced = !user.isSilenced;
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleSuspend() {
|
||||
if (!await getConfirmed(i18n.global.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return;
|
||||
|
||||
os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
|
||||
userId: user.id
|
||||
}).then(() => {
|
||||
user.isSuspended = !user.isSuspended;
|
||||
});
|
||||
}
|
||||
|
||||
async function getConfirmed(text: string): Promise<boolean> {
|
||||
const confirm = await os.dialog({
|
||||
type: 'warning',
|
||||
showCancelButton: true,
|
||||
title: 'confirm',
|
||||
text,
|
||||
});
|
||||
|
||||
return !confirm.canceled;
|
||||
}
|
||||
|
||||
let menu = [{
|
||||
icon: faAt,
|
||||
text: i18n.global.t('copyUsername'),
|
||||
action: () => {
|
||||
copyToClipboard(`@${user.username}@${user.host || host}`);
|
||||
}
|
||||
}, {
|
||||
icon: faEnvelope,
|
||||
text: i18n.global.t('sendMessage'),
|
||||
action: () => {
|
||||
os.post({ specified: user });
|
||||
}
|
||||
}, store.state.i.id != user.id ? {
|
||||
icon: faComments,
|
||||
text: i18n.global.t('startMessaging'),
|
||||
action: () => {
|
||||
const acct = getAcct(user);
|
||||
switch (store.state.device.chatOpenBehavior) {
|
||||
case 'window': { os.pageWindow('/my/messaging/' + acct, defineAsyncComponent(() => import('@/pages/messaging/messaging-room.vue')), { userAcct: acct }); break; }
|
||||
case 'popout': { popout('/my/messaging'); break; }
|
||||
default: { router.push('/my/messaging'); break; }
|
||||
}
|
||||
}
|
||||
} : undefined, null, {
|
||||
icon: faListUl,
|
||||
text: i18n.global.t('addToList'),
|
||||
action: pushList
|
||||
}, store.state.i.id != user.id ? {
|
||||
icon: faUsers,
|
||||
text: i18n.global.t('inviteToGroup'),
|
||||
action: inviteGroup
|
||||
} : undefined] as any;
|
||||
|
||||
if (store.getters.isSignedIn && store.state.i.id != user.id) {
|
||||
menu = menu.concat([null, {
|
||||
icon: user.isMuted ? faEye : faEyeSlash,
|
||||
text: user.isMuted ? i18n.global.t('unmute') : i18n.global.t('mute'),
|
||||
action: toggleMute
|
||||
}, {
|
||||
icon: faBan,
|
||||
text: user.isBlocking ? i18n.global.t('unblock') : i18n.global.t('block'),
|
||||
action: toggleBlock
|
||||
}]);
|
||||
|
||||
if (store.getters.isSignedIn && (store.state.i.isAdmin || store.state.i.isModerator)) {
|
||||
menu = menu.concat([null, {
|
||||
icon: faMicrophoneSlash,
|
||||
text: user.isSilenced ? i18n.global.t('unsilence') : i18n.global.t('silence'),
|
||||
action: toggleSilence
|
||||
}, {
|
||||
icon: faSnowflake,
|
||||
text: user.isSuspended ? i18n.global.t('unsuspend') : i18n.global.t('suspend'),
|
||||
action: toggleSuspend
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
if (store.getters.isSignedIn && store.state.i.id === user.id) {
|
||||
menu = menu.concat([null, {
|
||||
icon: faPencilAlt,
|
||||
text: i18n.global.t('editProfile'),
|
||||
action: () => {
|
||||
router.push('/settings/profile');
|
||||
}
|
||||
}]);
|
||||
}
|
||||
|
||||
if (userActions.length > 0) {
|
||||
menu = menu.concat([null, ...userActions.map(action => ({
|
||||
icon: faPlug,
|
||||
text: action.title,
|
||||
action: () => {
|
||||
action.handler(user);
|
||||
}
|
||||
}))]);
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
import keyCode from './keycode';
|
||||
import { concat } from '../../prelude/array';
|
||||
|
||||
type pattern = {
|
||||
which: string[];
|
||||
ctrl?: boolean;
|
||||
shift?: boolean;
|
||||
alt?: boolean;
|
||||
};
|
||||
|
||||
type action = {
|
||||
patterns: pattern[];
|
||||
|
||||
callback: Function;
|
||||
|
||||
allowRepeat: boolean;
|
||||
};
|
||||
|
||||
const getKeyMap = keymap => Object.entries(keymap).map(([patterns, callback]): action => {
|
||||
const result = {
|
||||
patterns: [],
|
||||
callback: callback,
|
||||
allowRepeat: true
|
||||
} as action;
|
||||
|
||||
if (patterns.match(/^\(.*\)$/) !== null) {
|
||||
result.allowRepeat = false;
|
||||
patterns = patterns.slice(1, -1);
|
||||
}
|
||||
|
||||
result.patterns = patterns.split('|').map(part => {
|
||||
const pattern = {
|
||||
which: [],
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false
|
||||
} as pattern;
|
||||
|
||||
const keys = part.trim().split('+').map(x => x.trim().toLowerCase());
|
||||
for (const key of keys) {
|
||||
switch (key) {
|
||||
case 'ctrl': pattern.ctrl = true; break;
|
||||
case 'alt': pattern.alt = true; break;
|
||||
case 'shift': pattern.shift = true; break;
|
||||
default: pattern.which = keyCode(key).map(k => k.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
return pattern;
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const ignoreElemens = ['input', 'textarea'];
|
||||
|
||||
function match(e: KeyboardEvent, patterns: action['patterns']): boolean {
|
||||
const key = e.code.toLowerCase();
|
||||
return patterns.some(pattern => pattern.which.includes(key) &&
|
||||
pattern.ctrl === e.ctrlKey &&
|
||||
pattern.shift === e.shiftKey &&
|
||||
pattern.alt === e.altKey &&
|
||||
!e.metaKey
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
install(Vue) {
|
||||
Vue.directive('hotkey', {
|
||||
bind(el, binding) {
|
||||
el._hotkey_global = binding.modifiers.global === true;
|
||||
|
||||
const actions = getKeyMap(binding.value);
|
||||
|
||||
// flatten
|
||||
const reservedKeys = concat(actions.map(a => a.patterns));
|
||||
|
||||
el._misskey_reservedKeys = reservedKeys;
|
||||
|
||||
el._keyHandler = (e: KeyboardEvent) => {
|
||||
const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : [];
|
||||
if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return;
|
||||
if (document.activeElement && document.activeElement.attributes['contenteditable']) return;
|
||||
|
||||
for (const action of actions) {
|
||||
const matched = match(e, action.patterns);
|
||||
|
||||
if (matched) {
|
||||
if (!action.allowRepeat && e.repeat) return;
|
||||
if (el._hotkey_global && match(e, targetReservedKeys)) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
action.callback(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (el._hotkey_global) {
|
||||
document.addEventListener('keydown', el._keyHandler);
|
||||
} else {
|
||||
el.addEventListener('keydown', el._keyHandler);
|
||||
}
|
||||
},
|
||||
|
||||
unbind(el) {
|
||||
if (el._hotkey_global) {
|
||||
document.removeEventListener('keydown', el._keyHandler);
|
||||
} else {
|
||||
el.removeEventListener('keydown', el._keyHandler);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
@ -1,11 +1,12 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import * as seedrandom from 'seedrandom';
|
||||
import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.';
|
||||
import { version } from '../../config';
|
||||
import { version } from '@/config';
|
||||
import { AiScript, utils, values } from '@syuilo/aiscript';
|
||||
import { createAiScriptEnv } from '../aiscript/api';
|
||||
import { collectPageVars } from '../collect-page-vars';
|
||||
import { initLib } from './lib';
|
||||
import * as os from '@/os';
|
||||
|
||||
type Fn = {
|
||||
slots: string[];
|
||||
@ -30,19 +31,19 @@ export class Hpml {
|
||||
enableAiScript: boolean;
|
||||
};
|
||||
|
||||
constructor(vm: any, page: Hpml['page'], opts: Hpml['opts']) {
|
||||
constructor(page: Hpml['page'], opts: Hpml['opts']) {
|
||||
this.page = page;
|
||||
this.variables = this.page.variables;
|
||||
this.pageVars = collectPageVars(this.page.content);
|
||||
this.opts = opts;
|
||||
|
||||
if (this.opts.enableAiScript) {
|
||||
this.aiscript = new AiScript({ ...createAiScriptEnv(vm, {
|
||||
this.aiscript = new AiScript({ ...createAiScriptEnv({
|
||||
storageKey: 'pages:' + this.page.id
|
||||
}), ...initLib(this)}, {
|
||||
in: (q) => {
|
||||
return new Promise(ok => {
|
||||
vm.$root.dialog({
|
||||
os.dialog({
|
||||
title: q,
|
||||
input: {}
|
||||
}).then(({ canceled, result: a }) => {
|
||||
|
@ -1,21 +1,11 @@
|
||||
import * as NProgress from 'nprogress';
|
||||
NProgress.configure({
|
||||
trickleSpeed: 500,
|
||||
showSpinner: false
|
||||
});
|
||||
|
||||
const root = document.getElementsByTagName('html')[0];
|
||||
|
||||
export default {
|
||||
start: () => {
|
||||
root.classList.add('progress');
|
||||
NProgress.start();
|
||||
// TODO
|
||||
},
|
||||
done: () => {
|
||||
root.classList.remove('progress');
|
||||
NProgress.done();
|
||||
// TODO
|
||||
},
|
||||
set: val => {
|
||||
NProgress.set(val);
|
||||
// TODO
|
||||
}
|
||||
};
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { markRaw } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import { onScrollTop, isTopVisible } from './scroll';
|
||||
|
||||
const SECOND_FETCH_LIMIT = 30;
|
||||
|
||||
export default (opts) => ({
|
||||
emits: ['queue'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
@ -14,13 +18,6 @@ export default (opts) => ({
|
||||
more: false,
|
||||
backed: false, // 遡り中か否か
|
||||
isBackTop: false,
|
||||
ilObserver: new IntersectionObserver(
|
||||
(entries) => entries.some((entry) => entry.isIntersecting)
|
||||
&& !this.moreFetching
|
||||
&& !this.fetching
|
||||
&& this.fetchMore()
|
||||
),
|
||||
loadMoreElement: null as Element,
|
||||
};
|
||||
},
|
||||
|
||||
@ -35,41 +32,33 @@ export default (opts) => ({
|
||||
},
|
||||
|
||||
watch: {
|
||||
pagination() {
|
||||
this.init();
|
||||
pagination: {
|
||||
handler() {
|
||||
this.init();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
|
||||
queue() {
|
||||
this.$emit('queue', this.queue.length);
|
||||
queue: {
|
||||
handler(a, b) {
|
||||
if (a.length === 0 && b.length === 0) return;
|
||||
this.$emit('queue', this.queue.length);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
opts.displayLimit = opts.displayLimit || 30;
|
||||
this.init();
|
||||
|
||||
this.$on('hook:activated', () => {
|
||||
this.isBackTop = false;
|
||||
});
|
||||
|
||||
this.$on('hook:deactivated', () => {
|
||||
this.isBackTop = window.scrollY === 0;
|
||||
});
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.loadMore) {
|
||||
this.loadMoreElement = this.$refs.loadMore instanceof Element ? this.$refs.loadMore : this.$refs.loadMore.$el;
|
||||
if (this.$store.state.device.enableInfiniteScroll) this.ilObserver.observe(this.loadMoreElement);
|
||||
this.loadMoreElement.addEventListener('click', this.fetchMore);
|
||||
}
|
||||
});
|
||||
activated() {
|
||||
this.isBackTop = false;
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.ilObserver.disconnect();
|
||||
if (this.$refs.loadMore) this.loadMoreElement.removeEventListener('click', this.fetchMore);
|
||||
deactivated() {
|
||||
this.isBackTop = window.scrollY === 0;
|
||||
},
|
||||
|
||||
methods: {
|
||||
@ -78,19 +67,30 @@ export default (opts) => ({
|
||||
this.init();
|
||||
},
|
||||
|
||||
replaceItem(finder, data) {
|
||||
const i = this.items.findIndex(finder);
|
||||
this.items[i] = data;
|
||||
},
|
||||
|
||||
removeItem(finder) {
|
||||
const i = this.items.findIndex(finder);
|
||||
this.items.splice(i, 1);
|
||||
},
|
||||
|
||||
async init() {
|
||||
this.queue = [];
|
||||
this.fetching = true;
|
||||
if (opts.before) opts.before(this);
|
||||
let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params;
|
||||
if (params && params.then) params = await params;
|
||||
if (params === null) return;
|
||||
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
|
||||
await this.$root.api(endpoint, {
|
||||
await os.api(endpoint, {
|
||||
...params,
|
||||
limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
|
||||
}).then(items => {
|
||||
for (const item of items) {
|
||||
Object.freeze(item);
|
||||
markRaw(item);
|
||||
}
|
||||
if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
|
||||
items.pop();
|
||||
@ -111,13 +111,13 @@ export default (opts) => ({
|
||||
},
|
||||
|
||||
async fetchMore() {
|
||||
if (!this.more || this.moreFetching || this.items.length === 0) return;
|
||||
if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return;
|
||||
this.moreFetching = true;
|
||||
this.backed = true;
|
||||
let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
|
||||
if (params && params.then) params = await params;
|
||||
const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
|
||||
await this.$root.api(endpoint, {
|
||||
await os.api(endpoint, {
|
||||
...params,
|
||||
limit: SECOND_FETCH_LIMIT + 1,
|
||||
...(this.pagination.offsetMode ? {
|
||||
@ -129,7 +129,7 @@ export default (opts) => ({
|
||||
}),
|
||||
}).then(items => {
|
||||
for (const item of items) {
|
||||
Object.freeze(item);
|
||||
markRaw(item);
|
||||
}
|
||||
if (items.length > SECOND_FETCH_LIMIT) {
|
||||
items.pop();
|
||||
@ -172,9 +172,5 @@ export default (opts) => ({
|
||||
append(item) {
|
||||
this.items.push(item);
|
||||
},
|
||||
|
||||
remove(find) {
|
||||
this.items = this.items.filter(x => !find(x));
|
||||
},
|
||||
}
|
||||
});
|
||||
|
@ -1,10 +1,14 @@
|
||||
export default ($root: any) => {
|
||||
if ($root.$store.getters.isSignedIn) return;
|
||||
import { i18n } from '@/i18n';
|
||||
import { dialog } from '@/os';
|
||||
import { store } from '@/store';
|
||||
|
||||
$root.dialog({
|
||||
title: $root.$t('signinRequired'),
|
||||
export function pleaseLogin() {
|
||||
if (store.getters.isSignedIn) return;
|
||||
|
||||
dialog({
|
||||
title: i18n.global.t('signinRequired'),
|
||||
text: null
|
||||
});
|
||||
|
||||
throw new Error('signin required');
|
||||
};
|
||||
}
|
||||
|
22
src/client/scripts/popout.ts
Normal file
22
src/client/scripts/popout.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import * as config from '@/config';
|
||||
|
||||
export function popout(path: string, w?: HTMLElement) {
|
||||
let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path;
|
||||
url += '?zen'; // TODO: ちゃんとURLパースしてクエリ付ける
|
||||
if (w) {
|
||||
const position = w.getBoundingClientRect();
|
||||
const width = parseInt(getComputedStyle(w, '').width, 10);
|
||||
const height = parseInt(getComputedStyle(w, '').height, 10);
|
||||
const x = window.screenX + position.left;
|
||||
const y = window.screenY + position.top;
|
||||
window.open(url, url,
|
||||
`width=${width}, height=${height}, top=${y}, left=${x}`);
|
||||
} else {
|
||||
const width = 400;
|
||||
const height = 450;
|
||||
const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2);
|
||||
const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2);
|
||||
window.open(url, url,
|
||||
`width=${width}, height=${height}, top=${x}, left=${y}`);
|
||||
}
|
||||
}
|
@ -1,15 +1,29 @@
|
||||
import { faHistory } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { router } from '@/router';
|
||||
|
||||
export async function search(q?: string | null | undefined) {
|
||||
if (q == null) {
|
||||
const { canceled, result: query } = await os.dialog({
|
||||
title: i18n.global.t('search'),
|
||||
input: true
|
||||
});
|
||||
|
||||
if (canceled || query == null || query === '') return;
|
||||
|
||||
q = query;
|
||||
}
|
||||
|
||||
export async function search(v: any, q: string) {
|
||||
q = q.trim();
|
||||
|
||||
if (q.startsWith('@') && !q.includes(' ')) {
|
||||
v.$router.push(`/${q}`);
|
||||
router.push(`/${q}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (q.startsWith('#')) {
|
||||
v.$router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
|
||||
router.push(`/tags/${encodeURIComponent(q.substr(1))}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -26,7 +40,7 @@ export async function search(v: any, q: string) {
|
||||
}
|
||||
|
||||
v.$root.$emit('warp', date);
|
||||
v.$root.dialog({
|
||||
os.dialog({
|
||||
icon: faHistory,
|
||||
iconOnly: true, autoClose: true
|
||||
});
|
||||
@ -34,31 +48,31 @@ export async function search(v: any, q: string) {
|
||||
}
|
||||
|
||||
if (q.startsWith('https://')) {
|
||||
const dialog = v.$root.dialog({
|
||||
const dialog = os.dialog({
|
||||
type: 'waiting',
|
||||
text: v.$t('fetchingAsApObject') + '...',
|
||||
text: i18n.global.t('fetchingAsApObject') + '...',
|
||||
showOkButton: false,
|
||||
showCancelButton: false,
|
||||
cancelableByBgClick: false
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await v.$root.api('ap/show', {
|
||||
const res = await os.api('ap/show', {
|
||||
uri: q
|
||||
});
|
||||
dialog.close();
|
||||
dialog.cancel();
|
||||
if (res.type === 'User') {
|
||||
v.$router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
router.push(`/@${res.object.username}@${res.object.host}`);
|
||||
} else if (res.type === 'Note') {
|
||||
v.$router.push(`/notes/${res.object.id}`);
|
||||
router.push(`/notes/${res.object.id}`);
|
||||
}
|
||||
} catch (e) {
|
||||
dialog.close();
|
||||
dialog.cancel();
|
||||
// TODO: Show error
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
v.$router.push(`/search?q=${encodeURIComponent(q)}`);
|
||||
router.push(`/search?q=${encodeURIComponent(q)}`);
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
export function selectDriveFile($root: any, multiple) {
|
||||
return new Promise((res, rej) => {
|
||||
import('../components/drive-window.vue').then(m => m.default).then(dialog => {
|
||||
const w = $root.new(dialog, {
|
||||
type: 'file',
|
||||
multiple
|
||||
});
|
||||
w.$once('selected', files => {
|
||||
res(multiple ? files : files[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
export function selectDriveFolder($root: any, multiple) {
|
||||
return new Promise((res, rej) => {
|
||||
import('../components/drive-window.vue').then(m => m.default).then(dialog => {
|
||||
const w = $root.new(dialog, {
|
||||
type: 'folder',
|
||||
multiple
|
||||
});
|
||||
w.$once('selected', folders => {
|
||||
res(multiple ? folders : (folders.length === 0 ? null : folders[0]));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -1,45 +1,23 @@
|
||||
import { faUpload, faCloud } from '@fortawesome/free-solid-svg-icons';
|
||||
import { selectDriveFile } from './select-drive-file';
|
||||
import { apiUrl } from '../config';
|
||||
import { faUpload, faCloud, faLink } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export function selectFile(component: any, src: any, label: string | null, multiple = false) {
|
||||
export function selectFile(src: any, label: string | null, multiple = false) {
|
||||
return new Promise((res, rej) => {
|
||||
const chooseFileFromPc = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = multiple;
|
||||
input.onchange = () => {
|
||||
const dialog = component.$root.dialog({
|
||||
type: 'waiting',
|
||||
text: component.$t('uploading') + '...',
|
||||
showOkButton: false,
|
||||
showCancelButton: false,
|
||||
cancelableByBgClick: false
|
||||
});
|
||||
|
||||
const promises = Array.from(input.files).map(file => new Promise((ok, err) => {
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
data.append('i', component.$store.state.i.token);
|
||||
|
||||
fetch(apiUrl + '/drive/files/create', {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(ok)
|
||||
.catch(err);
|
||||
}));
|
||||
const promises = Array.from(input.files).map(file => os.upload(file));
|
||||
|
||||
Promise.all(promises).then(driveFiles => {
|
||||
res(multiple ? driveFiles : driveFiles[0]);
|
||||
}).catch(e => {
|
||||
component.$root.dialog({
|
||||
os.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
}).finally(() => {
|
||||
dialog.close();
|
||||
});
|
||||
|
||||
// 一応廃棄
|
||||
@ -54,34 +32,57 @@ export function selectFile(component: any, src: any, label: string | null, multi
|
||||
};
|
||||
|
||||
const chooseFileFromDrive = () => {
|
||||
selectDriveFile(component.$root, multiple).then(files => {
|
||||
os.selectDriveFile(multiple).then(files => {
|
||||
res(files);
|
||||
});
|
||||
};
|
||||
|
||||
// TODO
|
||||
const chooseFileFromUrl = () => {
|
||||
os.dialog({
|
||||
title: i18n.global.t('uploadFromUrl'),
|
||||
input: {
|
||||
placeholder: i18n.global.t('uploadFromUrlDescription')
|
||||
}
|
||||
}).then(({ canceled, result: url }) => {
|
||||
if (canceled) return;
|
||||
|
||||
const marker = Math.random().toString(); // TODO: UUIDとか使う
|
||||
|
||||
const connection = os.stream.useSharedConnection('main');
|
||||
connection.on('urlUploadFinished', data => {
|
||||
if (data.marker === marker) {
|
||||
res(multiple ? [data.file] : data.file);
|
||||
connection.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
os.api('drive/files/upload_from_url', {
|
||||
url: url,
|
||||
marker
|
||||
});
|
||||
|
||||
os.dialog({
|
||||
title: i18n.global.t('uploadFromUrlRequested'),
|
||||
text: i18n.global.t('uploadFromUrlMayTakeTime')
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
component.$root.menu({
|
||||
items: [label ? {
|
||||
text: label,
|
||||
type: 'label'
|
||||
} : undefined, {
|
||||
text: component.$t('upload'),
|
||||
icon: faUpload,
|
||||
action: chooseFileFromPc
|
||||
}, {
|
||||
text: component.$t('fromDrive'),
|
||||
icon: faCloud,
|
||||
action: chooseFileFromDrive
|
||||
}, /*{
|
||||
text: component.$t('fromUrl'),
|
||||
icon: faLink,
|
||||
action: chooseFileFromUrl
|
||||
}*/],
|
||||
source: src
|
||||
});
|
||||
os.modalMenu([label ? {
|
||||
text: label,
|
||||
type: 'label'
|
||||
} : undefined, {
|
||||
text: i18n.global.t('upload'),
|
||||
icon: faUpload,
|
||||
action: chooseFileFromPc
|
||||
}, {
|
||||
text: i18n.global.t('fromDrive'),
|
||||
icon: faCloud,
|
||||
action: chooseFileFromDrive
|
||||
}, {
|
||||
text: i18n.global.t('fromUrl'),
|
||||
icon: faLink,
|
||||
action: chooseFileFromUrl
|
||||
}], src);
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import VueI18n from 'vue-i18n';
|
||||
import { clientDb, clear, bulkSet } from '../db';
|
||||
import { deepEntries, delimitEntry } from 'deep-entries';
|
||||
|
||||
export function setI18nContexts(lang: string, version: string, i18n: VueI18n, cleardb = false) {
|
||||
export function setI18nContexts(lang: string, version: string, cleardb = false) {
|
||||
return Promise.all([
|
||||
cleardb ? clear(clientDb.i18n) : Promise.resolve(),
|
||||
fetch(`/assets/locales/${lang}.${version}.json`)
|
||||
@ -11,7 +10,6 @@ export function setI18nContexts(lang: string, version: string, i18n: VueI18n, cl
|
||||
.then(locale => {
|
||||
const flatLocaleEntries = deepEntries(locale, delimitEntry) as [string, string][];
|
||||
bulkSet(flatLocaleEntries, clientDb.i18n);
|
||||
i18n.locale = lang;
|
||||
i18n.setLocaleMessage(lang, Object.fromEntries(flatLocaleEntries));
|
||||
return Object.fromEntries(flatLocaleEntries);
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import ReconnectingWebsocket from 'reconnecting-websocket';
|
||||
import { wsUrl } from '../config';
|
||||
import MiOS from '../mios';
|
||||
import { wsUrl } from '@/config';
|
||||
import { query as urlQuery } from '../../prelude/url';
|
||||
|
||||
/**
|
||||
@ -10,18 +9,13 @@ import { query as urlQuery } from '../../prelude/url';
|
||||
*/
|
||||
export default class Stream extends EventEmitter {
|
||||
private stream: ReconnectingWebsocket;
|
||||
public state: 'initializing' | 'reconnecting' | 'connected';
|
||||
public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
|
||||
private sharedConnectionPools: Pool[] = [];
|
||||
private sharedConnections: SharedConnection[] = [];
|
||||
private nonSharedConnections: NonSharedConnection[] = [];
|
||||
|
||||
constructor(os: MiOS) {
|
||||
super();
|
||||
|
||||
this.state = 'initializing';
|
||||
|
||||
const user = os.store.state.i;
|
||||
|
||||
@autobind
|
||||
public init(user): void {
|
||||
const query = urlQuery({
|
||||
i: user?.token,
|
||||
_t: Date.now(),
|
||||
|
@ -5,11 +5,12 @@ import { themeProps, Theme } from './theme';
|
||||
export type Default = null;
|
||||
export type Color = string;
|
||||
export type FuncName = 'alpha' | 'darken' | 'lighten';
|
||||
export type Func = { type: 'func', name: FuncName, arg: number, value: string };
|
||||
export type RefProp = { type: 'refProp', key: string };
|
||||
export type RefConst = { type: 'refConst', key: string };
|
||||
export type Func = { type: 'func'; name: FuncName; arg: number; value: string; };
|
||||
export type RefProp = { type: 'refProp'; key: string; };
|
||||
export type RefConst = { type: 'refConst'; key: string; };
|
||||
export type Css = { type: 'css'; value: string; };
|
||||
|
||||
export type ThemeValue = Color | Func | RefProp | RefConst | Default;
|
||||
export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default;
|
||||
|
||||
export type ThemeViewModel = [ string, ThemeValue ][];
|
||||
|
||||
@ -31,17 +32,23 @@ export const fromThemeString = (str?: string) : ThemeValue => {
|
||||
type: 'refConst',
|
||||
key: str.slice(1),
|
||||
};
|
||||
} else if (str.startsWith('"')) {
|
||||
return {
|
||||
type: 'css',
|
||||
value: str.substr(1).trim(),
|
||||
};
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
export const toThemeString = (value: Color | Func | RefProp | RefConst) => {
|
||||
export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => {
|
||||
if (typeof value === 'string') return value;
|
||||
switch (value.type) {
|
||||
case 'func': return `:${value.name}<${value.arg}<@${value.value}`;
|
||||
case 'refProp': return `@${value.key}`;
|
||||
case 'refConst': return `$${value.key}`;
|
||||
case 'css': return `" ${value.value}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -101,7 +101,7 @@ function compile(theme: Theme): Record<string, string> {
|
||||
for (const [k, v] of Object.entries(theme.props)) {
|
||||
if (k.startsWith('$')) continue; // ignore const
|
||||
|
||||
props[k] = genValue(getColor(v));
|
||||
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
|
||||
}
|
||||
|
||||
return props;
|
||||
|
Reference in New Issue
Block a user