Merge branch 'develop' into pizzax-indexeddb
This commit is contained in:
@@ -27,7 +27,7 @@ export function createAiScriptEnv(opts) {
|
||||
if (token) utils.assertString(token);
|
||||
apiRequests++;
|
||||
if (apiRequests > 16) return values.NULL;
|
||||
const res = await os.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]) => {
|
||||
|
@@ -98,7 +98,7 @@ export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
|
||||
export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
|
||||
return collections.reduce((obj: Record<string, T[]>, item: T) => {
|
||||
const key = keySelector(item);
|
||||
if (!obj.hasOwnProperty(key)) {
|
||||
if (typeof obj[key] === 'undefined') {
|
||||
obj[key] = [];
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,7 @@ export class Autocomplete {
|
||||
x: Ref<number>;
|
||||
y: Ref<number>;
|
||||
q: Ref<string | null>;
|
||||
close: Function;
|
||||
close: () => void;
|
||||
} | null;
|
||||
private textarea: HTMLInputElement | HTMLTextAreaElement;
|
||||
private currentType: string;
|
||||
@@ -157,7 +157,7 @@ export class Autocomplete {
|
||||
const _y = ref(y);
|
||||
const _q = ref(q);
|
||||
|
||||
const { dispose } = await popup(defineAsyncComponent(() => import('@/components/autocomplete.vue')), {
|
||||
const { dispose } = await popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), {
|
||||
textarea: this.textarea,
|
||||
close: this.close,
|
||||
type: type,
|
||||
|
@@ -3,7 +3,9 @@ export function checkWordMute(note: Record<string, any>, me: Record<string, any>
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
|
||||
if (mutedWords.length > 0) {
|
||||
if (note.text == null) return false;
|
||||
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||
|
||||
if (text === '') return false;
|
||||
|
||||
const matched = mutedWords.some(filter => {
|
||||
if (Array.isArray(filter)) {
|
||||
@@ -11,7 +13,7 @@ export function checkWordMute(note: Record<string, any>, me: Record<string, any>
|
||||
const filteredFilter = filter.filter(keyword => keyword !== '');
|
||||
if (filteredFilter.length === 0) return false;
|
||||
|
||||
return filteredFilter.every(keyword => note.text!.includes(keyword));
|
||||
return filteredFilter.every(keyword => text.includes(keyword));
|
||||
} else {
|
||||
// represents RegExp
|
||||
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
||||
@@ -20,7 +22,7 @@ export function checkWordMute(note: Record<string, any>, me: Record<string, any>
|
||||
if (!regexp) return false;
|
||||
|
||||
try {
|
||||
return new RegExp(regexp[1], regexp[2]).test(note.text!);
|
||||
return new RegExp(regexp[1], regexp[2]).test(text);
|
||||
} catch (err) {
|
||||
// This should never happen due to input sanitisation.
|
||||
return false;
|
||||
|
18
packages/client/src/scripts/clone.ts
Normal file
18
packages/client/src/scripts/clone.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// structredCloneが遅いため
|
||||
// SEE: http://var.blog.jp/archives/86038606.html
|
||||
|
||||
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
|
||||
|
||||
export function deepClone<T extends Cloneable>(x: T): T {
|
||||
if (typeof x === 'object') {
|
||||
if (x === null) return x;
|
||||
if (Array.isArray(x)) return x.map(deepClone) as T;
|
||||
const obj = {} as Record<string, Cloneable>;
|
||||
for (const [k, v] of Object.entries(x)) {
|
||||
obj[k] = deepClone(v);
|
||||
}
|
||||
return obj as T;
|
||||
} else {
|
||||
return x;
|
||||
}
|
||||
}
|
@@ -8,4 +8,6 @@ export type UnicodeEmojiDef = {
|
||||
}
|
||||
|
||||
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
|
||||
export const emojilist = (await import('../emojilist.json')).default as UnicodeEmojiDef[];
|
||||
import _emojilist from '../emojilist.json';
|
||||
|
||||
export const emojilist = _emojilist as UnicodeEmojiDef[];
|
||||
|
@@ -21,7 +21,6 @@ export async function genSearchQuery(v: any, q: string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return {
|
||||
query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '),
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { defineAsyncComponent, Ref, inject } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { pleaseLogin } from './please-login';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
@@ -7,7 +8,7 @@ import * as os from '@/os';
|
||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||
import { url } from '@/config';
|
||||
import { noteActions } from '@/store';
|
||||
import { pleaseLogin } from './please-login';
|
||||
import { notePage } from '@/filters/note';
|
||||
|
||||
export function getNoteMenu(props: {
|
||||
note: misskey.entities.Note;
|
||||
@@ -34,7 +35,7 @@ export function getNoteMenu(props: {
|
||||
if (canceled) return;
|
||||
|
||||
os.api('notes/delete', {
|
||||
noteId: appearNote.id
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -47,7 +48,7 @@ export function getNoteMenu(props: {
|
||||
if (canceled) return;
|
||||
|
||||
os.api('notes/delete', {
|
||||
noteId: appearNote.id
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
|
||||
os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
|
||||
@@ -56,19 +57,13 @@ export function getNoteMenu(props: {
|
||||
|
||||
function toggleFavorite(favorite: boolean): void {
|
||||
os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
|
||||
noteId: appearNote.id
|
||||
});
|
||||
}
|
||||
|
||||
function toggleWatch(watch: boolean): void {
|
||||
os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
|
||||
noteId: appearNote.id
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
}
|
||||
|
||||
function toggleThreadMute(mute: boolean): void {
|
||||
os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
|
||||
noteId: appearNote.id
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,12 +79,12 @@ export function getNoteMenu(props: {
|
||||
|
||||
function togglePin(pin: boolean): void {
|
||||
os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
|
||||
noteId: appearNote.id
|
||||
noteId: appearNote.id,
|
||||
}, undefined, null, res => {
|
||||
if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.pinLimitExceeded
|
||||
text: i18n.ts.pinLimitExceeded,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -104,26 +99,26 @@ export function getNoteMenu(props: {
|
||||
const { canceled, result } = await os.form(i18n.ts.createNewClip, {
|
||||
name: {
|
||||
type: 'string',
|
||||
label: i18n.ts.name
|
||||
label: i18n.ts.name,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
multiline: true,
|
||||
label: i18n.ts.description
|
||||
label: i18n.ts.description,
|
||||
},
|
||||
isPublic: {
|
||||
type: 'boolean',
|
||||
label: i18n.ts.public,
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
const clip = await os.apiWithDialog('clips/create', result);
|
||||
|
||||
os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
|
||||
}
|
||||
},
|
||||
}, null, ...clips.map(clip => ({
|
||||
text: clip.name,
|
||||
action: () => {
|
||||
@@ -146,9 +141,9 @@ export function getNoteMenu(props: {
|
||||
text: err.message + '\n' + err.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
}))], props.menuButton.value, {
|
||||
}).then(focus);
|
||||
}
|
||||
@@ -178,7 +173,9 @@ export function getNoteMenu(props: {
|
||||
url: `${url}/notes/${appearNote.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
function notedetails(): void {
|
||||
os.pageWindow(`/notes/${appearNote.id}`);
|
||||
}
|
||||
async function translate(): Promise<void> {
|
||||
if (props.translation.value != null) return;
|
||||
props.translating.value = true;
|
||||
@@ -193,86 +190,80 @@ export function getNoteMenu(props: {
|
||||
let menu;
|
||||
if ($i) {
|
||||
const statePromise = os.api('notes/state', {
|
||||
noteId: appearNote.id
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
|
||||
menu = [
|
||||
...(
|
||||
props.currentClipPage?.value.userId === $i.id ? [{
|
||||
icon: 'fas fa-circle-minus',
|
||||
text: i18n.ts.unclip,
|
||||
danger: true,
|
||||
action: unclip,
|
||||
}, null] : []
|
||||
),
|
||||
{
|
||||
icon: 'fas fa-copy',
|
||||
text: i18n.ts.copyContent,
|
||||
action: copyContent
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: copyLink
|
||||
}, (appearNote.url || appearNote.uri) ? {
|
||||
icon: 'fas fa-external-link-square-alt',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
window.open(appearNote.url || appearNote.uri, '_blank');
|
||||
}
|
||||
} : undefined,
|
||||
{
|
||||
icon: 'fas fa-share-alt',
|
||||
text: i18n.ts.share,
|
||||
action: share
|
||||
},
|
||||
instance.translatorAvailable ? {
|
||||
icon: 'fas fa-language',
|
||||
text: i18n.ts.translate,
|
||||
action: translate
|
||||
} : undefined,
|
||||
null,
|
||||
statePromise.then(state => state.isFavorited ? {
|
||||
icon: 'fas fa-star',
|
||||
text: i18n.ts.unfavorite,
|
||||
action: () => toggleFavorite(false)
|
||||
} : {
|
||||
icon: 'fas fa-star',
|
||||
text: i18n.ts.favorite,
|
||||
action: () => toggleFavorite(true)
|
||||
}),
|
||||
{
|
||||
icon: 'fas fa-paperclip',
|
||||
text: i18n.ts.clip,
|
||||
action: () => clip()
|
||||
},
|
||||
(appearNote.userId !== $i.id) ? statePromise.then(state => state.isWatching ? {
|
||||
icon: 'fas fa-eye-slash',
|
||||
text: i18n.ts.unwatch,
|
||||
action: () => toggleWatch(false)
|
||||
} : {
|
||||
icon: 'fas fa-eye',
|
||||
text: i18n.ts.watch,
|
||||
action: () => toggleWatch(true)
|
||||
}) : undefined,
|
||||
statePromise.then(state => state.isMutedThread ? {
|
||||
icon: 'fas fa-comment-slash',
|
||||
text: i18n.ts.unmuteThread,
|
||||
action: () => toggleThreadMute(false)
|
||||
} : {
|
||||
icon: 'fas fa-comment-slash',
|
||||
text: i18n.ts.muteThread,
|
||||
action: () => toggleThreadMute(true)
|
||||
}),
|
||||
appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? {
|
||||
icon: 'fas fa-thumbtack',
|
||||
text: i18n.ts.unpin,
|
||||
action: () => togglePin(false)
|
||||
} : {
|
||||
icon: 'fas fa-thumbtack',
|
||||
text: i18n.ts.pin,
|
||||
action: () => togglePin(true)
|
||||
} : undefined,
|
||||
/*
|
||||
...(
|
||||
props.currentClipPage?.value.userId === $i.id ? [{
|
||||
icon: 'fas fa-circle-minus',
|
||||
text: i18n.ts.unclip,
|
||||
danger: true,
|
||||
action: unclip,
|
||||
}, null] : []
|
||||
), {
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.details,
|
||||
action: notedetails,
|
||||
}, {
|
||||
icon: 'fas fa-copy',
|
||||
text: i18n.ts.copyContent,
|
||||
action: copyContent,
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: copyLink,
|
||||
}, (appearNote.url || appearNote.uri) ? {
|
||||
icon: 'fas fa-external-link-square-alt',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
window.open(appearNote.url || appearNote.uri, '_blank');
|
||||
},
|
||||
} : undefined,
|
||||
{
|
||||
icon: 'fas fa-share-alt',
|
||||
text: i18n.ts.share,
|
||||
action: share,
|
||||
},
|
||||
instance.translatorAvailable ? {
|
||||
icon: 'fas fa-language',
|
||||
text: i18n.ts.translate,
|
||||
action: translate,
|
||||
} : undefined,
|
||||
null,
|
||||
statePromise.then(state => state.isFavorited ? {
|
||||
icon: 'fas fa-star',
|
||||
text: i18n.ts.unfavorite,
|
||||
action: () => toggleFavorite(false),
|
||||
} : {
|
||||
icon: 'fas fa-star',
|
||||
text: i18n.ts.favorite,
|
||||
action: () => toggleFavorite(true),
|
||||
}),
|
||||
{
|
||||
icon: 'fas fa-paperclip',
|
||||
text: i18n.ts.clip,
|
||||
action: () => clip(),
|
||||
},
|
||||
statePromise.then(state => state.isMutedThread ? {
|
||||
icon: 'fas fa-comment-slash',
|
||||
text: i18n.ts.unmuteThread,
|
||||
action: () => toggleThreadMute(false),
|
||||
} : {
|
||||
icon: 'fas fa-comment-slash',
|
||||
text: i18n.ts.muteThread,
|
||||
action: () => toggleThreadMute(true),
|
||||
}),
|
||||
appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? {
|
||||
icon: 'fas fa-thumbtack',
|
||||
text: i18n.ts.unpin,
|
||||
action: () => togglePin(false),
|
||||
} : {
|
||||
icon: 'fas fa-thumbtack',
|
||||
text: i18n.ts.pin,
|
||||
action: () => togglePin(true),
|
||||
} : undefined,
|
||||
/*
|
||||
...($i.isModerator || $i.isAdmin ? [
|
||||
null,
|
||||
{
|
||||
@@ -282,54 +273,58 @@ export function getNoteMenu(props: {
|
||||
}]
|
||||
: []
|
||||
),*/
|
||||
...(appearNote.userId !== $i.id ? [
|
||||
null,
|
||||
{
|
||||
icon: 'fas fa-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: () => {
|
||||
const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`;
|
||||
os.popup(defineAsyncComponent(() => import('@/components/abuse-report-window.vue')), {
|
||||
user: appearNote.user,
|
||||
initialComment: `Note: ${u}\n-----\n`
|
||||
}, {}, 'closed');
|
||||
}
|
||||
}]
|
||||
...(appearNote.userId !== $i.id ? [
|
||||
null,
|
||||
{
|
||||
icon: 'fas fa-exclamation-circle',
|
||||
text: i18n.ts.reportAbuse,
|
||||
action: () => {
|
||||
const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`;
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
|
||||
user: appearNote.user,
|
||||
initialComment: `Note: ${u}\n-----\n`,
|
||||
}, {}, 'closed');
|
||||
},
|
||||
}]
|
||||
: []
|
||||
),
|
||||
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
|
||||
null,
|
||||
appearNote.userId === $i.id ? {
|
||||
icon: 'fas fa-edit',
|
||||
text: i18n.ts.deleteAndEdit,
|
||||
action: delEdit
|
||||
} : undefined,
|
||||
{
|
||||
icon: 'fas fa-trash-alt',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
action: del
|
||||
}]
|
||||
),
|
||||
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
|
||||
null,
|
||||
appearNote.userId === $i.id ? {
|
||||
icon: 'fas fa-edit',
|
||||
text: i18n.ts.deleteAndEdit,
|
||||
action: delEdit,
|
||||
} : undefined,
|
||||
{
|
||||
icon: 'fas fa-trash-alt',
|
||||
text: i18n.ts.delete,
|
||||
danger: true,
|
||||
action: del,
|
||||
}]
|
||||
: []
|
||||
)]
|
||||
.filter(x => x !== undefined);
|
||||
)]
|
||||
.filter(x => x !== undefined);
|
||||
} else {
|
||||
menu = [{
|
||||
icon: 'fas fa-external-link-alt',
|
||||
text: i18n.ts.detailed,
|
||||
action: openDetail,
|
||||
}, {
|
||||
icon: 'fas fa-copy',
|
||||
text: i18n.ts.copyContent,
|
||||
action: copyContent
|
||||
action: copyContent,
|
||||
}, {
|
||||
icon: 'fas fa-link',
|
||||
text: i18n.ts.copyLink,
|
||||
action: copyLink
|
||||
action: copyLink,
|
||||
}, (appearNote.url || appearNote.uri) ? {
|
||||
icon: 'fas fa-external-link-square-alt',
|
||||
text: i18n.ts.showOnRemote,
|
||||
action: () => {
|
||||
window.open(appearNote.url || appearNote.uri, '_blank');
|
||||
}
|
||||
},
|
||||
} : undefined]
|
||||
.filter(x => x !== undefined);
|
||||
.filter(x => x !== undefined);
|
||||
}
|
||||
|
||||
if (noteActions.length > 0) {
|
||||
@@ -338,7 +333,7 @@ export function getNoteMenu(props: {
|
||||
text: action.title,
|
||||
action: () => {
|
||||
action.handler(appearNote);
|
||||
}
|
||||
},
|
||||
}))]);
|
||||
}
|
||||
|
||||
|
@@ -7,8 +7,9 @@ import * as os from '@/os';
|
||||
import { userActions } from '@/store';
|
||||
import { $i, iAmModerator } from '@/account';
|
||||
import { mainRouter } from '@/router';
|
||||
import { Router } from '@/nirax';
|
||||
|
||||
export function getUserMenu(user) {
|
||||
export function getUserMenu(user, router: Router = mainRouter) {
|
||||
const meId = $i ? $i.id : null;
|
||||
|
||||
async function pushList() {
|
||||
@@ -128,7 +129,7 @@ export function getUserMenu(user) {
|
||||
}
|
||||
|
||||
function reportAbuse() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/abuse-report-window.vue')), {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
|
||||
user: user,
|
||||
}, {}, 'closed');
|
||||
}
|
||||
@@ -161,7 +162,7 @@ export function getUserMenu(user) {
|
||||
icon: 'fas fa-info-circle',
|
||||
text: i18n.ts.info,
|
||||
action: () => {
|
||||
os.pageWindow(`/user-info/${user.id}`);
|
||||
router.push(`/user-info/${user.id}`);
|
||||
},
|
||||
}, {
|
||||
icon: 'fas fa-envelope',
|
||||
@@ -227,7 +228,7 @@ export function getUserMenu(user) {
|
||||
icon: 'fas fa-pencil-alt',
|
||||
text: i18n.ts.editProfile,
|
||||
action: () => {
|
||||
mainRouter.push('/settings/profile');
|
||||
router.push('/settings/profile');
|
||||
},
|
||||
}]);
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import keyCode from './keycode';
|
||||
|
||||
type Keymap = Record<string, Function>;
|
||||
type Callback = (ev: KeyboardEvent) => void;
|
||||
|
||||
type Keymap = Record<string, Callback>;
|
||||
|
||||
type Pattern = {
|
||||
which: string[];
|
||||
@@ -11,14 +13,14 @@ type Pattern = {
|
||||
|
||||
type Action = {
|
||||
patterns: Pattern[];
|
||||
callback: Function;
|
||||
callback: Callback;
|
||||
allowRepeat: boolean;
|
||||
};
|
||||
|
||||
const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => {
|
||||
const result = {
|
||||
patterns: [],
|
||||
callback: callback,
|
||||
callback,
|
||||
allowRepeat: true
|
||||
} as Action;
|
||||
|
||||
|
@@ -159,7 +159,6 @@ export class Hpml {
|
||||
|
||||
@autobind
|
||||
private evaluate(expr: Expr, scope: HpmlScope): any {
|
||||
|
||||
if (isLiteralValue(expr)) {
|
||||
if (expr.type === null) {
|
||||
return null;
|
||||
|
@@ -16,7 +16,7 @@ export type TextValue = ExprBase & {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type MultiLineTextValue = ExprBase & {
|
||||
export type MultiLineTextValue = ExprBase & {
|
||||
type: 'multiLineText';
|
||||
value: string;
|
||||
};
|
||||
|
@@ -14,13 +14,13 @@ export type Fn = {
|
||||
export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null;
|
||||
|
||||
export const literalDefs: Record<string, { out: any; category: string; icon: any; }> = {
|
||||
text: { out: 'string', category: 'value', icon: 'fas fa-quote-right', },
|
||||
multiLineText: { out: 'string', category: 'value', icon: 'fas fa-align-left', },
|
||||
textList: { out: 'stringArray', category: 'value', icon: 'fas fa-list', },
|
||||
number: { out: 'number', category: 'value', icon: 'fas fa-sort-numeric-up', },
|
||||
ref: { out: null, category: 'value', icon: 'fas fa-magic', },
|
||||
aiScriptVar: { out: null, category: 'value', icon: 'fas fa-magic', },
|
||||
fn: { out: 'function', category: 'value', icon: 'fas fa-square-root-alt', },
|
||||
text: { out: 'string', category: 'value', icon: 'fas fa-quote-right', },
|
||||
multiLineText: { out: 'string', category: 'value', icon: 'fas fa-align-left', },
|
||||
textList: { out: 'stringArray', category: 'value', icon: 'fas fa-list', },
|
||||
number: { out: 'number', category: 'value', icon: 'fas fa-sort-numeric-up', },
|
||||
ref: { out: null, category: 'value', icon: 'fas fa-magic', },
|
||||
aiScriptVar: { out: null, category: 'value', icon: 'fas fa-magic', },
|
||||
fn: { out: 'function', category: 'value', icon: 'fas fa-square-root-alt', },
|
||||
};
|
||||
|
||||
export const blockDefs = [
|
||||
|
@@ -125,55 +125,56 @@ export function initAiLib(hpml: Hpml) {
|
||||
}
|
||||
});
|
||||
*/
|
||||
})
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = {
|
||||
if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: 'fas fa-share-alt', },
|
||||
for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'fas fa-recycle', },
|
||||
not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag', },
|
||||
or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag', },
|
||||
and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag', },
|
||||
add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-plus', },
|
||||
subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-minus', },
|
||||
multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-times', },
|
||||
divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide', },
|
||||
mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide', },
|
||||
round: { in: ['number'], out: 'number', category: 'operation', icon: 'fas fa-calculator', },
|
||||
eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-equals', },
|
||||
notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-not-equal', },
|
||||
gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than', },
|
||||
lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than', },
|
||||
gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than-equal', },
|
||||
ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than-equal', },
|
||||
strLen: { in: ['string'], out: 'number', category: 'text', icon: 'fas fa-quote-right', },
|
||||
strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'fas fa-quote-right', },
|
||||
strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right', },
|
||||
strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'fas fa-quote-right', },
|
||||
join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right', },
|
||||
stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'fas fa-exchange-alt', },
|
||||
numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'fas fa-exchange-alt', },
|
||||
splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'fas fa-exchange-alt', },
|
||||
pick: { in: [null, 'number'], out: null, category: 'list', icon: 'fas fa-indent', },
|
||||
listLen: { in: [null], out: 'number', category: 'list', icon: 'fas fa-indent', },
|
||||
rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice', },
|
||||
dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice', },
|
||||
seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice', },
|
||||
random: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice', },
|
||||
dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice', },
|
||||
seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'fas fa-dice', },
|
||||
randomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice', },
|
||||
dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice', },
|
||||
seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'fas fa-dice', },
|
||||
DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'fas fa-dice', }, // dailyRandomPickWithProbabilityMapping
|
||||
if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: 'fas fa-share-alt' },
|
||||
for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'fas fa-recycle' },
|
||||
not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' },
|
||||
or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' },
|
||||
and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' },
|
||||
add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-plus' },
|
||||
subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-minus' },
|
||||
multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-times' },
|
||||
divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' },
|
||||
mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' },
|
||||
round: { in: ['number'], out: 'number', category: 'operation', icon: 'fas fa-calculator' },
|
||||
eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-equals' },
|
||||
notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-not-equal' },
|
||||
gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than' },
|
||||
lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than' },
|
||||
gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than-equal' },
|
||||
ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than-equal' },
|
||||
strLen: { in: ['string'], out: 'number', category: 'text', icon: 'fas fa-quote-right' },
|
||||
strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'fas fa-quote-right' },
|
||||
strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right' },
|
||||
strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'fas fa-quote-right' },
|
||||
join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right' },
|
||||
stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'fas fa-exchange-alt' },
|
||||
numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'fas fa-exchange-alt' },
|
||||
splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'fas fa-exchange-alt' },
|
||||
pick: { in: [null, 'number'], out: null, category: 'list', icon: 'fas fa-indent' },
|
||||
listLen: { in: [null], out: 'number', category: 'list', icon: 'fas fa-indent' },
|
||||
rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' },
|
||||
dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' },
|
||||
seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' },
|
||||
random: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' },
|
||||
dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' },
|
||||
seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' },
|
||||
randomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' },
|
||||
dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' },
|
||||
seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'fas fa-dice' },
|
||||
DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'fas fa-dice' }, // dailyRandomPickWithProbabilityMapping
|
||||
};
|
||||
|
||||
export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) {
|
||||
|
||||
const date = new Date();
|
||||
const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
|
||||
|
||||
// SHOULD be fine to ignore since it's intended + function shape isn't defined
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const funcs: Record<string, Function> = {
|
||||
not: (a: boolean) => !a,
|
||||
or: (a: boolean, b: boolean) => a || b,
|
||||
@@ -189,7 +190,7 @@ export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, vi
|
||||
const result: any[] = [];
|
||||
for (let i = 0; i < times; i++) {
|
||||
result.push(fn.exec({
|
||||
[fn.slots[0]]: i + 1
|
||||
[fn.slots[0]]: i + 1,
|
||||
}));
|
||||
}
|
||||
return result;
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import { Type, envVarsDef, PageVar } from '.';
|
||||
import { Expr, isLiteralValue, Variable } from './expr';
|
||||
import { isLiteralValue } from './expr';
|
||||
import { funcDefs } from './lib';
|
||||
import { envVarsDef } from '.';
|
||||
import type { Type, PageVar } from '.';
|
||||
import type { Expr, Variable } from './expr';
|
||||
|
||||
type TypeError = {
|
||||
arg: number;
|
||||
@@ -44,14 +46,14 @@ export class HpmlTypeChecker {
|
||||
return {
|
||||
arg: i,
|
||||
expect: generic[arg],
|
||||
actual: type
|
||||
actual: type,
|
||||
};
|
||||
}
|
||||
} else if (type !== arg) {
|
||||
return {
|
||||
arg: i,
|
||||
expect: arg,
|
||||
actual: type
|
||||
actual: type,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -81,7 +83,7 @@ export class HpmlTypeChecker {
|
||||
}
|
||||
|
||||
if (typeof def.in[slot] === 'number') {
|
||||
return generic[def.in[slot]] || null;
|
||||
return generic[def.in[slot]] ?? null;
|
||||
} else {
|
||||
return def.in[slot];
|
||||
}
|
||||
|
@@ -11,16 +11,15 @@ const fallbackName = (key: string) => `idbfallback::${key}`;
|
||||
let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true;
|
||||
|
||||
if (idbAvailable) {
|
||||
try {
|
||||
await iset('idb-test', 'test');
|
||||
} catch (err) {
|
||||
iset('idb-test', 'test').catch(err => {
|
||||
console.error('idb error', err);
|
||||
console.error('indexedDB is unavailable. It will use localStorage.');
|
||||
idbAvailable = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('indexedDB is unavailable. It will use localStorage.');
|
||||
}
|
||||
|
||||
if (!idbAvailable) console.error('indexedDB is unavailable. It will use localStorage.');
|
||||
|
||||
export async function get(key: string) {
|
||||
if (idbAvailable) return iget(key);
|
||||
return JSON.parse(localStorage.getItem(fallbackName(key)));
|
||||
|
13
packages/client/src/scripts/media-proxy.ts
Normal file
13
packages/client/src/scripts/media-proxy.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { query } from '@/scripts/url';
|
||||
import { url } from '@/config';
|
||||
|
||||
export function getProxiedImageUrl(imageUrl: string): string {
|
||||
return `${url}/proxy/image.webp?${query({
|
||||
url: imageUrl,
|
||||
})}`;
|
||||
}
|
||||
|
||||
export function getProxiedImageUrlNullable(imageUrl: string | null | undefined): string | null {
|
||||
if (imageUrl == null) return null;
|
||||
return getProxiedImageUrl(imageUrl);
|
||||
}
|
@@ -6,7 +6,7 @@ import { popup } from '@/os';
|
||||
export function pleaseLogin(path?: string) {
|
||||
if ($i) return;
|
||||
|
||||
popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
|
||||
autoSet: true,
|
||||
message: i18n.ts.signinRequired
|
||||
}, {
|
||||
@@ -17,5 +17,5 @@ export function pleaseLogin(path?: string) {
|
||||
},
|
||||
}, 'closed');
|
||||
|
||||
throw new Error('signin required');
|
||||
if (!path) throw new Error('signin required');
|
||||
}
|
||||
|
158
packages/client/src/scripts/popup-position.ts
Normal file
158
packages/client/src/scripts/popup-position.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Ref } from 'vue';
|
||||
|
||||
export function calcPopupPosition(el: HTMLElement, props: {
|
||||
anchorElement: HTMLElement | null;
|
||||
innerMargin: number;
|
||||
direction: 'top' | 'bottom' | 'left' | 'right';
|
||||
align: 'top' | 'bottom' | 'left' | 'right' | 'center';
|
||||
alignOffset?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
}): { top: number; left: number; transformOrigin: string; } {
|
||||
const contentWidth = el.offsetWidth;
|
||||
const contentHeight = el.offsetHeight;
|
||||
|
||||
let rect: DOMRect;
|
||||
|
||||
if (props.anchorElement) {
|
||||
rect = props.anchorElement.getBoundingClientRect();
|
||||
}
|
||||
|
||||
const calcPosWhenTop = () => {
|
||||
let left: number;
|
||||
let top: number;
|
||||
|
||||
if (props.anchorElement) {
|
||||
left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
|
||||
top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin;
|
||||
} else {
|
||||
left = props.x;
|
||||
top = (props.y - contentHeight) - props.innerMargin;
|
||||
}
|
||||
|
||||
left -= (el.offsetWidth / 2);
|
||||
|
||||
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
};
|
||||
|
||||
const calcPosWhenBottom = () => {
|
||||
let left: number;
|
||||
let top: number;
|
||||
|
||||
if (props.anchorElement) {
|
||||
left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2);
|
||||
top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin;
|
||||
} else {
|
||||
left = props.x;
|
||||
top = (props.y) + props.innerMargin;
|
||||
}
|
||||
|
||||
left -= (el.offsetWidth / 2);
|
||||
|
||||
if (left + contentWidth - window.pageXOffset > window.innerWidth) {
|
||||
left = window.innerWidth - contentWidth + window.pageXOffset - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
};
|
||||
|
||||
const calcPosWhenLeft = () => {
|
||||
let left: number;
|
||||
let top: number;
|
||||
|
||||
if (props.anchorElement) {
|
||||
left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin;
|
||||
top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
|
||||
} else {
|
||||
left = (props.x - contentWidth) - props.innerMargin;
|
||||
top = props.y;
|
||||
}
|
||||
|
||||
top -= (el.offsetHeight / 2);
|
||||
|
||||
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
|
||||
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
};
|
||||
|
||||
const calcPosWhenRight = () => {
|
||||
let left: number;
|
||||
let top: number;
|
||||
|
||||
if (props.anchorElement) {
|
||||
left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin;
|
||||
|
||||
if (props.align === 'top') {
|
||||
top = rect.top + window.pageYOffset;
|
||||
if (props.alignOffset != null) top += props.alignOffset;
|
||||
} else if (props.align === 'bottom') {
|
||||
// TODO
|
||||
} else { // center
|
||||
top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2);
|
||||
top -= (el.offsetHeight / 2);
|
||||
}
|
||||
} else {
|
||||
left = props.x + props.innerMargin;
|
||||
top = props.y;
|
||||
top -= (el.offsetHeight / 2);
|
||||
}
|
||||
|
||||
if (top + contentHeight - window.pageYOffset > window.innerHeight) {
|
||||
top = window.innerHeight - contentHeight + window.pageYOffset - 1;
|
||||
}
|
||||
|
||||
return [left, top];
|
||||
};
|
||||
|
||||
const calc = (): {
|
||||
left: number;
|
||||
top: number;
|
||||
transformOrigin: string;
|
||||
} => {
|
||||
switch (props.direction) {
|
||||
case 'top': {
|
||||
const [left, top] = calcPosWhenTop();
|
||||
|
||||
// ツールチップを上に向かって表示するスペースがなければ下に向かって出す
|
||||
if (top - window.pageYOffset < 0) {
|
||||
const [left, top] = calcPosWhenBottom();
|
||||
return { left, top, transformOrigin: 'center top' };
|
||||
}
|
||||
|
||||
return { left, top, transformOrigin: 'center bottom' };
|
||||
}
|
||||
|
||||
case 'bottom': {
|
||||
const [left, top] = calcPosWhenBottom();
|
||||
// TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す
|
||||
return { left, top, transformOrigin: 'center top' };
|
||||
}
|
||||
|
||||
case 'left': {
|
||||
const [left, top] = calcPosWhenLeft();
|
||||
|
||||
// ツールチップを左に向かって表示するスペースがなければ右に向かって出す
|
||||
if (left - window.pageXOffset < 0) {
|
||||
const [left, top] = calcPosWhenRight();
|
||||
return { left, top, transformOrigin: 'left center' };
|
||||
}
|
||||
|
||||
return { left, top, transformOrigin: 'right center' };
|
||||
}
|
||||
|
||||
case 'right': {
|
||||
const [left, top] = calcPosWhenRight();
|
||||
// TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す
|
||||
return { left, top, transformOrigin: 'left center' };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return calc();
|
||||
}
|
@@ -12,7 +12,7 @@ class ReactionPicker {
|
||||
}
|
||||
|
||||
public async init() {
|
||||
await popup(defineAsyncComponent(() => import('@/components/emoji-picker-dialog.vue')), {
|
||||
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
|
||||
src: this.src,
|
||||
asReactionPicker: true,
|
||||
manualShowing: this.manualShowing
|
||||
|
7
packages/client/src/scripts/safe-uri-decode.ts
Normal file
7
packages/client/src/scripts/safe-uri-decode.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function safeURIDecode(str: string): string {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch {
|
||||
return str;
|
||||
}
|
||||
}
|
@@ -2,12 +2,8 @@ type ScrollBehavior = 'auto' | 'smooth' | 'instant';
|
||||
|
||||
export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
|
||||
if (el == null || el.tagName === 'HTML') return null;
|
||||
const overflow = window.getComputedStyle(el).getPropertyValue('overflow');
|
||||
if (
|
||||
// xとyを個別に指定している場合、`hidden scroll`みたいな値になる
|
||||
overflow.endsWith('scroll') ||
|
||||
overflow.endsWith('auto')
|
||||
) {
|
||||
const overflow = window.getComputedStyle(el).getPropertyValue('overflow-y');
|
||||
if (overflow === 'scroll' || overflow === 'auto') {
|
||||
return el;
|
||||
} else {
|
||||
return getScrollContainer(el.parentElement);
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { ref } from 'vue';
|
||||
import { DriveFile } from 'misskey-js/built/entities';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import { DriveFile } from 'misskey-js/built/entities';
|
||||
import { uploadFile } from '@/scripts/upload';
|
||||
|
||||
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> {
|
||||
@@ -20,10 +20,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
||||
Promise.all(promises).then(driveFiles => {
|
||||
res(multiple ? driveFiles : driveFiles[0]);
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err
|
||||
});
|
||||
// アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない
|
||||
});
|
||||
|
||||
// 一応廃棄
|
||||
@@ -47,7 +44,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
||||
os.inputText({
|
||||
title: i18n.ts.uploadFromUrl,
|
||||
type: 'url',
|
||||
placeholder: i18n.ts.uploadFromUrlDescription
|
||||
placeholder: i18n.ts.uploadFromUrlDescription,
|
||||
}).then(({ canceled, result: url }) => {
|
||||
if (canceled) return;
|
||||
|
||||
@@ -64,35 +61,35 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
||||
os.api('drive/files/upload-from-url', {
|
||||
url: url,
|
||||
folderId: defaultStore.state.uploadFolder,
|
||||
marker
|
||||
marker,
|
||||
});
|
||||
|
||||
os.alert({
|
||||
title: i18n.ts.uploadFromUrlRequested,
|
||||
text: i18n.ts.uploadFromUrlMayTakeTime
|
||||
text: i18n.ts.uploadFromUrlMayTakeTime,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
os.popupMenu([label ? {
|
||||
text: label,
|
||||
type: 'label'
|
||||
type: 'label',
|
||||
} : undefined, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal
|
||||
ref: keepOriginal,
|
||||
}, {
|
||||
text: i18n.ts.upload,
|
||||
icon: 'fas fa-upload',
|
||||
action: chooseFileFromPc
|
||||
action: chooseFileFromPc,
|
||||
}, {
|
||||
text: i18n.ts.fromDrive,
|
||||
icon: 'fas fa-cloud',
|
||||
action: chooseFileFromDrive
|
||||
action: chooseFileFromDrive,
|
||||
}, {
|
||||
text: i18n.ts.fromUrl,
|
||||
icon: 'fas fa-link',
|
||||
action: chooseFileFromUrl
|
||||
action: chooseFileFromUrl,
|
||||
}], src);
|
||||
});
|
||||
}
|
||||
|
19
packages/client/src/scripts/shuffle.ts
Normal file
19
packages/client/src/scripts/shuffle.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 配列をシャッフル (破壊的)
|
||||
*/
|
||||
export function shuffle<T extends any[]>(array: T): T {
|
||||
let currentIndex = array.length, randomIndex;
|
||||
|
||||
// While there remain elements to shuffle.
|
||||
while (currentIndex !== 0) {
|
||||
// Pick a remaining element.
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
|
||||
// And swap it with the current element.
|
||||
[array[currentIndex], array[randomIndex]] = [
|
||||
array[randomIndex], array[currentIndex]];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import { ref } from 'vue';
|
||||
import { globalEvents } from '@/events';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { globalEvents } from '@/events';
|
||||
|
||||
export type Theme = {
|
||||
id: string;
|
||||
@@ -13,6 +13,7 @@ export type Theme = {
|
||||
|
||||
import lightTheme from '@/themes/_light.json5';
|
||||
import darkTheme from '@/themes/_dark.json5';
|
||||
import { deepClone } from './clone';
|
||||
|
||||
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
||||
|
||||
@@ -25,17 +26,19 @@ export const getBuiltinThemes = () => Promise.all(
|
||||
'l-vivid',
|
||||
'l-cherry',
|
||||
'l-sushi',
|
||||
'l-u0',
|
||||
|
||||
'd-dark',
|
||||
'd-persimmon',
|
||||
'd-astro',
|
||||
'd-future',
|
||||
'd-botanical',
|
||||
'd-green-lime',
|
||||
'd-green-orange',
|
||||
'd-cherry',
|
||||
'd-ice',
|
||||
'd-pumpkin',
|
||||
'd-black',
|
||||
].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default))
|
||||
'd-u0',
|
||||
].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
|
||||
);
|
||||
|
||||
export const getBuiltinThemesRef = () => {
|
||||
@@ -55,8 +58,10 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||
document.documentElement.classList.remove('_themeChanging_');
|
||||
}, 1000);
|
||||
|
||||
const colorSchema = theme.base === 'dark' ? 'dark' : 'light';
|
||||
|
||||
// Deep copy
|
||||
const _theme = JSON.parse(JSON.stringify(theme));
|
||||
const _theme = deepClone(theme);
|
||||
|
||||
if (_theme.base) {
|
||||
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
|
||||
@@ -76,8 +81,11 @@ export function applyTheme(theme: Theme, persist = true) {
|
||||
document.documentElement.style.setProperty(`--${k}`, v.toString());
|
||||
}
|
||||
|
||||
document.documentElement.style.setProperty('color-schema', colorSchema);
|
||||
|
||||
if (persist) {
|
||||
localStorage.setItem('theme', JSON.stringify(props));
|
||||
localStorage.setItem('colorSchema', colorSchema);
|
||||
}
|
||||
|
||||
// 色計算など再度行えるようにクライアント全体に通知
|
||||
|
49
packages/client/src/scripts/timezones.ts
Normal file
49
packages/client/src/scripts/timezones.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export const timezones = [{
|
||||
name: 'UTC',
|
||||
abbrev: 'UTC',
|
||||
offset: 0,
|
||||
}, {
|
||||
name: 'Europe/Berlin',
|
||||
abbrev: 'CET',
|
||||
offset: 60,
|
||||
}, {
|
||||
name: 'Asia/Tokyo',
|
||||
abbrev: 'JST',
|
||||
offset: 540,
|
||||
}, {
|
||||
name: 'Asia/Seoul',
|
||||
abbrev: 'KST',
|
||||
offset: 540,
|
||||
}, {
|
||||
name: 'Asia/Shanghai',
|
||||
abbrev: 'CST',
|
||||
offset: 480,
|
||||
}, {
|
||||
name: 'Australia/Sydney',
|
||||
abbrev: 'AEST',
|
||||
offset: 600,
|
||||
}, {
|
||||
name: 'Australia/Darwin',
|
||||
abbrev: 'ACST',
|
||||
offset: 570,
|
||||
}, {
|
||||
name: 'Australia/Perth',
|
||||
abbrev: 'AWST',
|
||||
offset: 480,
|
||||
}, {
|
||||
name: 'America/New_York',
|
||||
abbrev: 'EST',
|
||||
offset: -300,
|
||||
}, {
|
||||
name: 'America/Mexico_City',
|
||||
abbrev: 'CST',
|
||||
offset: -360,
|
||||
}, {
|
||||
name: 'America/Phoenix',
|
||||
abbrev: 'MST',
|
||||
offset: -420,
|
||||
}, {
|
||||
name: 'America/Los_Angeles',
|
||||
abbrev: 'PST',
|
||||
offset: -480,
|
||||
}];
|
@@ -1,10 +1,11 @@
|
||||
import { reactive, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { readAndCompressImage } from 'browser-image-resizer';
|
||||
import { defaultStore } from '@/store';
|
||||
import { apiUrl } from '@/config';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { $i } from '@/account';
|
||||
import { readAndCompressImage } from 'browser-image-resizer';
|
||||
import { alert } from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
type Uploading = {
|
||||
id: string;
|
||||
@@ -31,7 +32,7 @@ export function uploadFile(
|
||||
file: File,
|
||||
folder?: any,
|
||||
name?: string,
|
||||
keepOriginal: boolean = defaultStore.state.keepOriginalUploading
|
||||
keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
|
||||
): Promise<Misskey.entities.DriveFile> {
|
||||
if (folder && typeof folder === 'object') folder = folder.id;
|
||||
|
||||
@@ -45,7 +46,7 @@ export function uploadFile(
|
||||
name: name || file.name || 'untitled',
|
||||
progressMax: undefined,
|
||||
progressValue: undefined,
|
||||
img: window.URL.createObjectURL(file)
|
||||
img: window.URL.createObjectURL(file),
|
||||
});
|
||||
|
||||
uploads.value.push(ctx);
|
||||
@@ -80,14 +81,37 @@ export function uploadFile(
|
||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||
xhr.onload = (ev) => {
|
||||
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
|
||||
// TODO: 消すのではなくて再送できるようにしたい
|
||||
// TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい
|
||||
uploads.value = uploads.value.filter(x => x.id !== id);
|
||||
|
||||
alert({
|
||||
type: 'error',
|
||||
title: 'Failed to upload',
|
||||
text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`
|
||||
});
|
||||
if (ev.target?.response) {
|
||||
const res = JSON.parse(ev.target.response);
|
||||
if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToUpload,
|
||||
text: i18n.ts.cannotUploadBecauseInappropriate,
|
||||
});
|
||||
} else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToUpload,
|
||||
text: i18n.ts.cannotUploadBecauseNoFreeSpace,
|
||||
});
|
||||
} else {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.failedToUpload,
|
||||
text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
alert({
|
||||
type: 'error',
|
||||
title: 'Failed to upload',
|
||||
text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`,
|
||||
});
|
||||
}
|
||||
|
||||
reject();
|
||||
return;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
export function query(obj: {}): string {
|
||||
export function query(obj: Record<string, any>): string {
|
||||
const params = Object.entries(obj)
|
||||
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
|
||||
.reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
|
||||
|
50
packages/client/src/scripts/use-chart-tooltip.ts
Normal file
50
packages/client/src/scripts/use-chart-tooltip.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import MkChartTooltip from '@/components/MkChartTooltip.vue';
|
||||
|
||||
export function useChartTooltip() {
|
||||
const tooltipShowing = ref(false);
|
||||
const tooltipX = ref(0);
|
||||
const tooltipY = ref(0);
|
||||
const tooltipTitle = ref(null);
|
||||
const tooltipSeries = ref(null);
|
||||
let disposeTooltipComponent;
|
||||
|
||||
os.popup(MkChartTooltip, {
|
||||
showing: tooltipShowing,
|
||||
x: tooltipX,
|
||||
y: tooltipY,
|
||||
title: tooltipTitle,
|
||||
series: tooltipSeries,
|
||||
}, {}).then(({ dispose }) => {
|
||||
disposeTooltipComponent = dispose;
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (disposeTooltipComponent) disposeTooltipComponent();
|
||||
});
|
||||
|
||||
function handler(context) {
|
||||
if (context.tooltip.opacity === 0) {
|
||||
tooltipShowing.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
tooltipTitle.value = context.tooltip.title[0];
|
||||
tooltipSeries.value = context.tooltip.body.map((b, i) => ({
|
||||
backgroundColor: context.tooltip.labelColors[i].backgroundColor,
|
||||
borderColor: context.tooltip.labelColors[i].borderColor,
|
||||
text: b.lines[0],
|
||||
}));
|
||||
|
||||
const rect = context.chart.canvas.getBoundingClientRect();
|
||||
|
||||
tooltipShowing.value = true;
|
||||
tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX;
|
||||
tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY;
|
||||
}
|
||||
|
||||
return {
|
||||
handler,
|
||||
};
|
||||
}
|
24
packages/client/src/scripts/use-interval.ts
Normal file
24
packages/client/src/scripts/use-interval.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
export function useInterval(fn: () => void, interval: number, options: {
|
||||
immediate: boolean;
|
||||
afterMounted: boolean;
|
||||
}): void {
|
||||
if (Number.isNaN(interval)) return;
|
||||
|
||||
let intervalId: number | null = null;
|
||||
|
||||
if (options.afterMounted) {
|
||||
onMounted(() => {
|
||||
if (options.immediate) fn();
|
||||
intervalId = window.setInterval(fn, interval);
|
||||
});
|
||||
} else {
|
||||
if (options.immediate) fn();
|
||||
intervalId = window.setInterval(fn, interval);
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (intervalId) window.clearInterval(intervalId);
|
||||
});
|
||||
}
|
@@ -3,6 +3,7 @@ import { i18n } from '@/i18n';
|
||||
import * as os from '@/os';
|
||||
|
||||
export function useLeaveGuard(enabled: Ref<boolean>) {
|
||||
/* TODO
|
||||
const setLeaveGuard = inject('setLeaveGuard');
|
||||
|
||||
if (setLeaveGuard) {
|
||||
@@ -28,6 +29,7 @@ export function useLeaveGuard(enabled: Ref<boolean>) {
|
||||
return !canceled;
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
function onBeforeLeave(ev: BeforeUnloadEvent) {
|
||||
|
@@ -3,6 +3,7 @@ import { Ref, ref, watch, onUnmounted } from 'vue';
|
||||
export function useTooltip(
|
||||
elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>,
|
||||
onShow: (showing: Ref<boolean>) => void,
|
||||
delay = 300,
|
||||
): void {
|
||||
let isHovering = false;
|
||||
|
||||
@@ -40,7 +41,7 @@ export function useTooltip(
|
||||
if (isHovering) return;
|
||||
if (shouldIgnoreMouseover) return;
|
||||
isHovering = true;
|
||||
timeoutId = window.setTimeout(open, 300);
|
||||
timeoutId = window.setTimeout(open, delay);
|
||||
};
|
||||
|
||||
const onMouseleave = () => {
|
||||
@@ -54,7 +55,7 @@ export function useTooltip(
|
||||
shouldIgnoreMouseover = true;
|
||||
if (isHovering) return;
|
||||
isHovering = true;
|
||||
timeoutId = window.setTimeout(open, 300);
|
||||
timeoutId = window.setTimeout(open, delay);
|
||||
};
|
||||
|
||||
const onTouchend = () => {
|
||||
|
Reference in New Issue
Block a user