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:
syuilo
2020-10-17 20:12:00 +09:00
committed by GitHub
parent a40f38b2b5
commit 7199e6f4e0
357 changed files with 15053 additions and 12496 deletions

View File

@ -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),
};
}

View 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);
});
}
}
}

View 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;
}

View File

@ -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);
}

View File

@ -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 {

View File

@ -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 {

View 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;
}

View File

@ -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);
}
}
});
}
};

View File

@ -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 }) => {

View File

@ -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
}
};

View File

@ -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));
},
}
});

View File

@ -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');
};
}

View 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}`);
}
}

View File

@ -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)}`);
}

View File

@ -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]);
});
});
});
}

View File

@ -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]));
});
});
});
}

View File

@ -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);
});
}

View File

@ -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);
});
}

View File

@ -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(),

View File

@ -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}`;
}
};

View File

@ -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;