Merge branch 'swn' into vite
This commit is contained in:
commit
25f5fa6eaf
27 changed files with 397 additions and 489 deletions
|
|
@ -24,6 +24,7 @@
|
|||
"autwh": "0.1.0",
|
||||
"blurhash": "1.1.5",
|
||||
"broadcast-channel": "4.11.0",
|
||||
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2",
|
||||
"chart.js": "3.7.1",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chartjs-plugin-gradient": "0.2.2",
|
||||
|
|
|
|||
|
|
@ -131,8 +131,8 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done', v: { type: string; value: any }): void;
|
||||
(e: 'closed'): void;
|
||||
(event: 'done', value: { type: string; value: any }): void;
|
||||
(event: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const suggests = ref<Element>();
|
||||
|
|
@ -152,7 +152,7 @@ function complete(type: string, value: any) {
|
|||
emit('closed');
|
||||
if (type === 'emoji') {
|
||||
let recents = defaultStore.state.recentlyUsedEmojis;
|
||||
recents = recents.filter((e: any) => e !== value);
|
||||
recents = recents.filter((emoji: any) => emoji !== value);
|
||||
recents.unshift(value);
|
||||
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||
}
|
||||
|
|
@ -232,7 +232,7 @@ function exec() {
|
|||
} else if (props.type === 'emoji') {
|
||||
if (!props.q || props.q === '') {
|
||||
// 最近使った絵文字をサジェスト
|
||||
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji === emoji)).filter(x => x) as EmojiDef[];
|
||||
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -269,17 +269,17 @@ function exec() {
|
|||
}
|
||||
}
|
||||
|
||||
function onMousedown(e: Event) {
|
||||
if (!contains(rootEl.value, e.target) && (rootEl.value !== e.target)) props.close();
|
||||
function onMousedown(event: Event) {
|
||||
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent) {
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
const cancel = () => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
switch (e.key) {
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
if (select.value !== -1) {
|
||||
cancel();
|
||||
|
|
@ -310,7 +310,7 @@ function onKeydown(e: KeyboardEvent) {
|
|||
break;
|
||||
|
||||
default:
|
||||
e.stopPropagation();
|
||||
event.stopPropagation();
|
||||
props.textarea.focus();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ import * as os from '@/os';
|
|||
import { stream } from '@/stream';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { uploadFile, uploads } from '@/scripts/upload';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialFolder?: Misskey.entities.DriveFolder;
|
||||
|
|
@ -127,8 +128,9 @@ const moreFolders = ref(false);
|
|||
const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
|
||||
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
|
||||
const uploadings = os.uploads;
|
||||
const uploadings = uploads;
|
||||
const connection = stream.useChannel('drive');
|
||||
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
|
||||
|
||||
// ドロップされようとしているか
|
||||
const draghover = ref(false);
|
||||
|
|
@ -355,7 +357,7 @@ function onChangeFileInput() {
|
|||
}
|
||||
|
||||
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
|
||||
os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).then(res => {
|
||||
uploadFile(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
|
||||
addFile(res, true);
|
||||
});
|
||||
}
|
||||
|
|
@ -562,6 +564,10 @@ function fetchMoreFiles() {
|
|||
|
||||
function getMenu() {
|
||||
return [{
|
||||
type: 'switch',
|
||||
text: i18n.ts.keepOriginalUploading,
|
||||
ref: keepOriginal,
|
||||
}, null, {
|
||||
text: i18n.ts.addFile,
|
||||
type: 'label'
|
||||
}, {
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ import MkInfo from '@/components/ui/info.vue';
|
|||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||
import { uploadFile } from '@/scripts/upload';
|
||||
|
||||
const modal = inject('modal');
|
||||
|
||||
|
|
@ -372,7 +373,7 @@ function updateFileName(file, name) {
|
|||
}
|
||||
|
||||
function upload(file: File, name?: string) {
|
||||
os.upload(file, defaultStore.state.uploadFolder, name).then(res => {
|
||||
uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
|
||||
files.push(res);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
|
||||
|
||||
import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
|
||||
import { Component, markRaw, Ref, ref } from 'vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
|
@ -10,7 +10,6 @@ import MkWaitingDialog from '@/components/waiting-dialog.vue';
|
|||
import { MenuItem } from '@/types/menu';
|
||||
import { resolve } from '@/router';
|
||||
import { $i } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
export const pendingApiRequestsCount = ref(0);
|
||||
|
||||
|
|
@ -530,78 +529,6 @@ export function post(props: Record<string, any> = {}) {
|
|||
|
||||
export const deckGlobalEvents = new EventEmitter();
|
||||
|
||||
export const uploads = ref<{
|
||||
id: string;
|
||||
name: string;
|
||||
progressMax: number | undefined;
|
||||
progressValue: number | undefined;
|
||||
img: string;
|
||||
}[]>([]);
|
||||
|
||||
export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise<Misskey.entities.DriveFile> {
|
||||
if (folder && typeof folder === 'object') folder = folder.id;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = Math.random().toString();
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const ctx = reactive({
|
||||
id: id,
|
||||
name: name || file.name || 'untitled',
|
||||
progressMax: undefined,
|
||||
progressValue: undefined,
|
||||
img: window.URL.createObjectURL(file)
|
||||
});
|
||||
|
||||
uploads.value.push(ctx);
|
||||
|
||||
console.log(keepOriginal);
|
||||
|
||||
const data = new FormData();
|
||||
data.append('i', $i.token);
|
||||
data.append('force', 'true');
|
||||
data.append('file', file);
|
||||
|
||||
if (folder) data.append('folderId', folder);
|
||||
if (name) data.append('name', name);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||
xhr.onload = (ev) => {
|
||||
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
|
||||
// TODO: 消すのではなくて再送できるようにしたい
|
||||
uploads.value = uploads.value.filter(x => x.id != id);
|
||||
|
||||
alert({
|
||||
type: 'error',
|
||||
text: 'upload failed'
|
||||
});
|
||||
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
const driveFile = JSON.parse(ev.target.response);
|
||||
|
||||
resolve(driveFile);
|
||||
|
||||
uploads.value = uploads.value.filter(x => x.id != id);
|
||||
};
|
||||
|
||||
xhr.upload.onprogress = e => {
|
||||
if (e.lengthComputable) {
|
||||
ctx.progressMax = e.total;
|
||||
ctx.progressValue = e.loaded;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(data);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
export function checkExistence(fileData: ArrayBuffer): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@
|
|||
</div>
|
||||
<!-- TODO
|
||||
<div class="inputs" style="display: flex; padding-top: 1.2em;">
|
||||
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()">
|
||||
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false">
|
||||
<span>{{ $ts.username }}</span>
|
||||
</MkInput>
|
||||
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'">
|
||||
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" :disabled="pagination.params().origin === 'local'">
|
||||
<span>{{ $ts.host }}</span>
|
||||
</MkInput>
|
||||
</div>
|
||||
|
|
@ -41,8 +41,8 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
|
|
@ -50,45 +50,35 @@ import MkPagination from '@/components/ui/pagination.vue';
|
|||
import XAbuseReport from '@/components/abuse-report.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkInput,
|
||||
MkSelect,
|
||||
MkPagination,
|
||||
XAbuseReport,
|
||||
},
|
||||
let reports = $ref<InstanceType<typeof MkPagination>>();
|
||||
|
||||
emits: ['info'],
|
||||
let state = $ref('unresolved');
|
||||
let reporterOrigin = $ref('combined');
|
||||
let targetUserOrigin = $ref('combined');
|
||||
let searchUsername = $ref('');
|
||||
let searchHost = $ref('');
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.abuseReports,
|
||||
icon: 'fas fa-exclamation-circle',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
searchUsername: '',
|
||||
searchHost: '',
|
||||
state: 'unresolved',
|
||||
reporterOrigin: 'combined',
|
||||
targetUserOrigin: 'combined',
|
||||
pagination: {
|
||||
endpoint: 'admin/abuse-user-reports' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
state: this.state,
|
||||
reporterOrigin: this.reporterOrigin,
|
||||
targetUserOrigin: this.targetUserOrigin,
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
const pagination = {
|
||||
endpoint: 'admin/abuse-user-reports' as const,
|
||||
limit: 10,
|
||||
params: computed(() => ({
|
||||
state,
|
||||
reporterOrigin,
|
||||
targetUserOrigin,
|
||||
})),
|
||||
};
|
||||
|
||||
methods: {
|
||||
resolved(reportId) {
|
||||
this.$refs.reports.removeItem(item => item.id === reportId);
|
||||
},
|
||||
function resolved(reportId) {
|
||||
reports.removeItem(item => item.id === reportId);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.abuseReports,
|
||||
icon: 'fas fa-exclamation-circle',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
|
|
@ -34,63 +34,51 @@ import MkTextarea from '@/components/form/textarea.vue';
|
|||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { Endpoints } from 'misskey-js';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton, MkInput, MkTextarea, MkSwitch,
|
||||
},
|
||||
const body = ref('{}');
|
||||
const endpoint = ref('');
|
||||
const endpoints = ref<any[]>([]);
|
||||
const sending = ref(false);
|
||||
const res = ref('');
|
||||
const withCredential = ref(true);
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'API console',
|
||||
icon: 'fas fa-terminal'
|
||||
},
|
||||
os.api('endpoints').then(endpointResponse => {
|
||||
endpoints.value = endpointResponse;
|
||||
});
|
||||
|
||||
endpoint: '',
|
||||
body: '{}',
|
||||
res: null,
|
||||
sending: false,
|
||||
endpoints: [],
|
||||
withCredential: true,
|
||||
function send() {
|
||||
sending.value = true;
|
||||
const requestBody = JSON5.parse(body.value);
|
||||
os.api(endpoint.value as keyof Endpoints, requestBody, requestBody.i || (withCredential.value ? undefined : null)).then(resp => {
|
||||
sending.value = false;
|
||||
res.value = JSON5.stringify(resp, null, 2);
|
||||
}, err => {
|
||||
sending.value = false;
|
||||
res.value = JSON5.stringify(err, null, 2);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
os.api('endpoints').then(endpoints => {
|
||||
this.endpoints = endpoints;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
send() {
|
||||
this.sending = true;
|
||||
const body = JSON5.parse(this.body);
|
||||
os.api(this.endpoint, body, body.i || (this.withCredential ? undefined : null)).then(res => {
|
||||
this.sending = false;
|
||||
this.res = JSON5.stringify(res, null, 2);
|
||||
}, err => {
|
||||
this.sending = false;
|
||||
this.res = JSON5.stringify(err, null, 2);
|
||||
});
|
||||
},
|
||||
|
||||
onEndpointChange() {
|
||||
os.api('endpoint', { endpoint: this.endpoint }, this.withCredential ? undefined : null).then(endpoint => {
|
||||
const body = {};
|
||||
for (const p of endpoint.params) {
|
||||
body[p.name] =
|
||||
p.type === 'String' ? '' :
|
||||
p.type === 'Number' ? 0 :
|
||||
p.type === 'Boolean' ? false :
|
||||
p.type === 'Array' ? [] :
|
||||
p.type === 'Object' ? {} :
|
||||
null;
|
||||
}
|
||||
this.body = JSON5.stringify(body, null, 2);
|
||||
});
|
||||
function onEndpointChange() {
|
||||
os.api('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => {
|
||||
const endpointBody = {};
|
||||
for (const p of resp.params) {
|
||||
endpointBody[p.name] =
|
||||
p.type === 'String' ? '' :
|
||||
p.type === 'Number' ? 0 :
|
||||
p.type === 'Boolean' ? false :
|
||||
p.type === 'Array' ? [] :
|
||||
p.type === 'Object' ? {} :
|
||||
null;
|
||||
}
|
||||
}
|
||||
body.value = JSON5.stringify(endpointBody, null, 2);
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'API console',
|
||||
icon: 'fas fa-terminal'
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import * as os from '@/os';
|
|||
import { stream } from '@/stream';
|
||||
import { Autocomplete } from '@/scripts/autocomplete';
|
||||
import { throttle } from 'throttle-debounce';
|
||||
import { uploadFile } from '@/scripts/upload';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
|
@ -164,7 +165,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
upload(file: File, name?: string) {
|
||||
os.upload(file, this.$store.state.uploadFolder, name).then(res => {
|
||||
uploadFile(file, this.$store.state.uploadFolder, name).then(res => {
|
||||
this.file = res;
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,20 +6,20 @@
|
|||
</div>
|
||||
|
||||
<MkContainer :foldable="true" class="_gap">
|
||||
<template #header>{{ $ts.output }}</template>
|
||||
<template #header>{{ i18n.ts.output }}</template>
|
||||
<div class="bepmlvbi">
|
||||
<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
|
||||
</div>
|
||||
</MkContainer>
|
||||
|
||||
<div class="_gap">
|
||||
{{ $ts.scratchpadDescription }}
|
||||
{{ i18n.ts.scratchpadDescription }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineExpose, ref, watch } from 'vue';
|
||||
import 'prismjs';
|
||||
import { highlight, languages } from 'prismjs/components/prism-core';
|
||||
import 'prismjs/components/prism-clike';
|
||||
|
|
@ -27,103 +27,90 @@ import 'prismjs/components/prism-javascript';
|
|||
import 'prismjs/themes/prism-okaidia.css';
|
||||
import { PrismEditor } from 'vue-prism-editor';
|
||||
import 'vue-prism-editor/dist/prismeditor.min.css';
|
||||
import { AiScript, parse, utils, values } from '@syuilo/aiscript';
|
||||
import { AiScript, parse, utils } from '@syuilo/aiscript';
|
||||
import MkContainer from '@/components/ui/container.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import { createAiScriptEnv } from '@/scripts/aiscript/api';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkContainer,
|
||||
MkButton,
|
||||
PrismEditor,
|
||||
},
|
||||
const code = ref('');
|
||||
const logs = ref<any[]>([]);
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.scratchpad,
|
||||
icon: 'fas fa-terminal',
|
||||
},
|
||||
code: '',
|
||||
logs: [],
|
||||
}
|
||||
},
|
||||
const saved = localStorage.getItem('scratchpad');
|
||||
if (saved) {
|
||||
code.value = saved;
|
||||
}
|
||||
|
||||
watch: {
|
||||
code() {
|
||||
localStorage.setItem('scratchpad', this.code);
|
||||
}
|
||||
},
|
||||
watch(code, () => {
|
||||
localStorage.setItem('scratchpad', code.value);
|
||||
});
|
||||
|
||||
created() {
|
||||
const saved = localStorage.getItem('scratchpad');
|
||||
if (saved) {
|
||||
this.code = saved;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async run() {
|
||||
this.logs = [];
|
||||
const aiscript = new AiScript(createAiScriptEnv({
|
||||
storageKey: 'scratchpad',
|
||||
token: this.$i?.token,
|
||||
}), {
|
||||
in: (q) => {
|
||||
return new Promise(ok => {
|
||||
os.inputText({
|
||||
title: q,
|
||||
}).then(({ canceled, result: a }) => {
|
||||
ok(a);
|
||||
});
|
||||
});
|
||||
},
|
||||
out: (value) => {
|
||||
this.logs.push({
|
||||
id: Math.random(),
|
||||
text: value.type === 'str' ? value.value : utils.valToString(value),
|
||||
print: true
|
||||
});
|
||||
},
|
||||
log: (type, params) => {
|
||||
switch (type) {
|
||||
case 'end': this.logs.push({
|
||||
id: Math.random(),
|
||||
text: utils.valToString(params.val, true),
|
||||
print: false
|
||||
}); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
async function run() {
|
||||
logs.value = [];
|
||||
const aiscript = new AiScript(createAiScriptEnv({
|
||||
storageKey: 'scratchpad',
|
||||
token: $i?.token,
|
||||
}), {
|
||||
in: (q) => {
|
||||
return new Promise(ok => {
|
||||
os.inputText({
|
||||
title: q,
|
||||
}).then(({ canceled, result: a }) => {
|
||||
ok(a);
|
||||
});
|
||||
});
|
||||
|
||||
let ast;
|
||||
try {
|
||||
ast = parse(this.code);
|
||||
} catch (e) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Syntax error :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await aiscript.exec(ast);
|
||||
} catch (e) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
highlighter(code) {
|
||||
return highlight(code, languages.js, 'javascript');
|
||||
out: (value) => {
|
||||
logs.value.push({
|
||||
id: Math.random(),
|
||||
text: value.type === 'str' ? value.value : utils.valToString(value),
|
||||
print: true
|
||||
});
|
||||
},
|
||||
log: (type, params) => {
|
||||
switch (type) {
|
||||
case 'end': logs.value.push({
|
||||
id: Math.random(),
|
||||
text: utils.valToString(params.val, true),
|
||||
print: false
|
||||
}); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let ast;
|
||||
try {
|
||||
ast = parse(code.value);
|
||||
} catch (error) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: 'Syntax error :('
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await aiscript.exec(ast);
|
||||
} catch (error: any) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function highlighter(code) {
|
||||
return highlight(code, languages.js, 'javascript');
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.scratchpad,
|
||||
icon: 'fas fa-terminal',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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[]> {
|
||||
return new Promise((res, rej) => {
|
||||
|
|
@ -14,7 +15,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
|
|||
input.type = 'file';
|
||||
input.multiple = multiple;
|
||||
input.onchange = () => {
|
||||
const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value));
|
||||
const promises = Array.from(input.files).map(file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value));
|
||||
|
||||
Promise.all(promises).then(driveFiles => {
|
||||
res(multiple ? driveFiles : driveFiles[0]);
|
||||
|
|
|
|||
114
packages/client/src/scripts/upload.ts
Normal file
114
packages/client/src/scripts/upload.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { reactive, ref } from 'vue';
|
||||
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';
|
||||
|
||||
type Uploading = {
|
||||
id: string;
|
||||
name: string;
|
||||
progressMax: number | undefined;
|
||||
progressValue: number | undefined;
|
||||
img: string;
|
||||
};
|
||||
export const uploads = ref<Uploading[]>([]);
|
||||
|
||||
const compressTypeMap = {
|
||||
'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' },
|
||||
'image/webp': { quality: 0.85, mimeType: 'image/jpeg' },
|
||||
'image/svg+xml': { quality: 1, mimeType: 'image/png' },
|
||||
} as const;
|
||||
|
||||
const mimeTypeMap = {
|
||||
'image/webp': 'webp',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
} as const;
|
||||
|
||||
export function uploadFile(
|
||||
file: File,
|
||||
folder?: any,
|
||||
name?: string,
|
||||
keepOriginal: boolean = defaultStore.state.keepOriginalUploading
|
||||
): Promise<Misskey.entities.DriveFile> {
|
||||
if (folder && typeof folder == 'object') folder = folder.id;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = Math.random().toString();
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
const ctx = reactive<Uploading>({
|
||||
id: id,
|
||||
name: name || file.name || 'untitled',
|
||||
progressMax: undefined,
|
||||
progressValue: undefined,
|
||||
img: window.URL.createObjectURL(file)
|
||||
});
|
||||
|
||||
uploads.value.push(ctx);
|
||||
|
||||
let resizedImage: any;
|
||||
if (!keepOriginal && file.type in compressTypeMap) {
|
||||
const imgConfig = compressTypeMap[file.type];
|
||||
|
||||
const config = {
|
||||
maxWidth: 2048,
|
||||
maxHeight: 2048,
|
||||
debug: true,
|
||||
...imgConfig,
|
||||
};
|
||||
|
||||
try {
|
||||
resizedImage = await readAndCompressImage(file, config);
|
||||
ctx.name = file.type !== imgConfig.mimeType ? `${ctx.name}.${mimeTypeMap[compressTypeMap[file.type].mimeType]}` : ctx.name;
|
||||
} catch (e) {
|
||||
console.error('Failed to resize image', e);
|
||||
}
|
||||
}
|
||||
|
||||
const data = new FormData();
|
||||
data.append('i', $i.token);
|
||||
data.append('force', 'true');
|
||||
data.append('file', resizedImage || file);
|
||||
data.append('name', ctx.name);
|
||||
if (folder) data.append('folderId', folder);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', apiUrl + '/drive/files/create', true);
|
||||
xhr.onload = (ev) => {
|
||||
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
|
||||
// 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)}`
|
||||
});
|
||||
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
const driveFile = JSON.parse(ev.target.response);
|
||||
|
||||
resolve(driveFile);
|
||||
|
||||
uploads.value = uploads.value.filter(x => x.id != id);
|
||||
};
|
||||
|
||||
xhr.upload.onprogress = e => {
|
||||
if (e.lengthComputable) {
|
||||
ctx.progressMax = e.total;
|
||||
ctx.progressValue = e.loaded;
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(data);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
|
@ -17,7 +17,8 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import { popup, popups, uploads, pendingApiRequestsCount } from '@/os';
|
||||
import { popup, popups, pendingApiRequestsCount } from '@/os';
|
||||
import { uploads } from '@/scripts/upload';
|
||||
import * as sound from '@/scripts/sound';
|
||||
import { $i } from '@/account';
|
||||
import { swInject } from './sw-inject';
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import { uploads } from '@/scripts/upload';
|
||||
|
||||
const uploads = os.uploads;
|
||||
const zIndex = os.claimZIndex('high');
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -879,6 +879,10 @@ broadcast-channel@4.11.0:
|
|||
rimraf "3.0.2"
|
||||
unload "2.3.1"
|
||||
|
||||
"browser-image-resizer@git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2":
|
||||
version "2.2.1-misskey.2"
|
||||
resolved "git+https://github.com/misskey-dev/browser-image-resizer#a58834f5fe2af9f9f31ff115121aef3de6f9d416"
|
||||
|
||||
browser-stdout@1.3.1:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue