Merge branch 'develop' into pizzax-indexeddb

This commit is contained in:
tamaina
2022-05-28 01:31:23 +09:00
340 changed files with 9589 additions and 9298 deletions

5
packages/client/@types/theme.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
import { Theme } from '../src/scripts/theme';
declare module '@/themes/*.json5' {
export = Theme;
}

View File

@@ -1,8 +1,8 @@
{
"private": true,
"scripts": {
"watch": "webpack --watch",
"build": "webpack",
"watch": "vite build --watch --mode development",
"build": "vite build",
"lint": "eslint --quiet \"src/**/*.{ts,vue}\""
},
"resolutions": {
@@ -10,50 +10,38 @@
"lodash": "^4.17.21"
},
"dependencies": {
"@discordapp/twemoji": "13.1.1",
"@discordapp/twemoji": "14.0.2",
"@fortawesome/fontawesome-free": "6.1.1",
"@syuilo/aiscript": "0.11.1",
"@typescript-eslint/parser": "5.20.0",
"@vue/compiler-sfc": "3.2.33",
"abort-controller": "3.0.0",
"autobind-decorator": "2.4.0",
"autosize": "5.0.1",
"autwh": "0.1.0",
"blurhash": "1.1.5",
"broadcast-channel": "4.11.0",
"broadcast-channel": "4.12.0",
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2",
"chart.js": "3.7.1",
"chart.js": "3.8.0",
"chartjs-adapter-date-fns": "2.0.0",
"chartjs-plugin-gradient": "0.2.2",
"chartjs-plugin-gradient": "0.5.0",
"chartjs-plugin-zoom": "1.2.1",
"compare-versions": "4.1.3",
"content-disposition": "0.5.4",
"css-loader": "6.7.1",
"cssnano": "5.1.7",
"date-fns": "2.28.0",
"deepcopy": "2.1.0",
"escape-regexp": "0.0.1",
"eslint": "8.14.0",
"eslint-plugin-vue": "8.7.1",
"eventemitter3": "4.0.7",
"feed": "4.2.2",
"glob": "7.2.0",
"idb-keyval": "6.1.0",
"insert-text-at-cursor": "0.3.0",
"json5": "2.2.1",
"json5-loader": "4.0.1",
"katex": "0.15.3",
"katex": "0.15.6",
"matter-js": "0.18.0",
"mfm-js": "0.21.0",
"mfm-js": "0.22.1",
"misskey-js": "0.0.14",
"mocha": "9.2.2",
"mocha": "10.0.0",
"ms": "2.1.3",
"nested-property": "4.0.0",
"parse5": "6.0.1",
"photoswipe": "5.2.4",
"portscanner": "2.2.0",
"postcss": "8.4.12",
"postcss-loader": "6.2.1",
"photoswipe": "5.2.7",
"prismjs": "1.28.0",
"private-ip": "2.3.3",
"promise-limit": "2.7.0",
@@ -65,36 +53,34 @@
"reflect-metadata": "0.1.13",
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sass": "1.50.1",
"sass-loader": "12.6.0",
"sass": "1.52.1",
"seedrandom": "3.0.5",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"style-loader": "3.3.1",
"syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0",
"three": "0.139.2",
"throttle-debounce": "4.0.1",
"three": "0.140.2",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.4.2",
"ts-loader": "9.2.8",
"tsc-alias": "1.5.0",
"tsconfig-paths": "3.14.1",
"tsc-alias": "1.6.7",
"tsconfig-paths": "4.0.0",
"twemoji-parser": "14.0.0",
"typescript": "4.6.3",
"uuid": "8.3.2",
"v-debounce": "0.1.2",
"vanilla-tilt": "1.7.2",
"vue": "3.2.33",
"vue-loader": "17.0.0",
"vue": "3.2.36",
"vue-prism-editor": "2.0.0-alpha.2",
"vue-router": "4.0.14",
"vue-style-loader": "4.1.3",
"vue-svg-loader": "0.17.0-beta.2",
"vue-router": "4.0.15",
"vuedraggable": "4.0.1",
"webpack": "5.72.0",
"webpack-cli": "4.9.2",
"websocket": "1.0.34",
"ws": "8.5.0"
"@vitejs/plugin-vue": "2.3.3",
"@vue/compiler-sfc": "3.2.36",
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"rollup": "2.74.1",
"typescript": "4.7.2",
"vite": "2.9.9",
"ws": "8.6.0"
},
"devDependencies": {
"@types/escape-regexp": "0.0.1",
@@ -106,21 +92,21 @@
"@types/matter-js": "0.17.7",
"@types/mocha": "9.1.1",
"@types/oauth": "0.9.1",
"@types/parse5": "6.0.3",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.4.2",
"@types/random-seed": "0.3.3",
"@types/seedrandom": "3.0.2",
"@types/throttle-debounce": "4.0.0",
"@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.3",
"@types/uuid": "8.3.4",
"@types/webpack": "5.28.0",
"@types/webpack-stream": "3.2.12",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.20.0",
"@typescript-eslint/eslint-plugin": "5.26.0",
"@typescript-eslint/parser": "5.26.0",
"eslint": "8.16.0",
"eslint-plugin-vue": "9.0.1",
"cross-env": "7.0.3",
"cypress": "9.5.4",
"cypress": "9.7.0",
"eslint-plugin-import": "2.26.0",
"start-server-and-test": "1.14.0"
}

View File

@@ -1,5 +1,5 @@
import { del, get, set } from '@/scripts/idb-proxy';
import { reactive } from 'vue';
import { defineAsyncComponent, reactive } from 'vue';
import * as misskey from 'misskey-js';
import { apiUrl } from '@/config';
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
@@ -11,10 +11,10 @@ import { i18n } from './i18n';
type Account = misskey.entities.MeDetailed;
const data = localStorage.getItem('account');
const accountData = localStorage.getItem('account');
// TODO: 外部からはreadonlyに
export const $i = data ? reactive(JSON.parse(data) as Account) : null;
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
@@ -52,7 +52,7 @@ export async function signout() {
return Promise.all(registrations.map(registration => registration.unregister()));
});
}
} catch (e) {}
} catch (err) {}
//#endregion
document.cookie = `igi=; path=/`;
@@ -104,8 +104,8 @@ function fetchAccount(token: string): Promise<Account> {
});
}
export function updateAccount(data) {
for (const [key, value] of Object.entries(data)) {
export function updateAccount(accountData) {
for (const [key, value] of Object.entries(accountData)) {
$i[key] = value;
}
localStorage.setItem('account', JSON.stringify($i));
@@ -141,7 +141,7 @@ export async function openAccountMenu(opts: {
onChoose?: (account: misskey.entities.UserDetailed) => void;
}, ev: MouseEvent) {
function showSigninDialog() {
popup(import('@/components/signin-dialog.vue'), {}, {
popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, {
done: res => {
addAccount(res.id, res.i);
success();
@@ -150,7 +150,7 @@ export async function openAccountMenu(opts: {
}
function createAccount() {
popup(import('@/components/signup-dialog.vue'), {}, {
popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, {
done: res => {
addAccount(res.id, res.i);
switchAccountWithToken(res.i);

View File

@@ -37,7 +37,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
const window = ref<InstanceType<typeof XWindow>>();

View File

@@ -2,7 +2,7 @@
<div class="bcekxzvu _card _gap">
<div class="_content target">
<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
<MkA class="info" :to="userPage(report.targetUser)" v-user-preview="report.targetUserId">
<MkA v-user-preview="report.targetUserId" class="info" :to="userPage(report.targetUser)">
<MkUserName class="name" :user="report.targetUser"/>
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
</MkA>
@@ -43,20 +43,20 @@ export default defineComponent({
MkSwitch,
},
emits: ['resolved'],
props: {
report: {
type: Object,
required: true,
}
}
},
emits: ['resolved'],
data() {
return {
forward: this.report.forwarded,
};
}
},
methods: {
acct,

View File

@@ -42,7 +42,7 @@
<script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import * as tinycolor from 'tinycolor2';
import tinycolor from 'tinycolor2';
withDefaults(defineProps<{
thickness: number;

View File

@@ -48,8 +48,8 @@ async function onClick() {
});
isFollowing.value = true;
}
} catch (e) {
console.error(e);
} catch (err) {
console.error(err);
} finally {
wait.value = false;
}

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void;
(ev: 'update:modelValue', v: boolean): void;
}>();
const label = computed(() => {

View File

@@ -90,8 +90,8 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'done', v: { canceled: boolean; result: any }): void;
(e: 'closed'): void;
(ev: 'done', v: { canceled: boolean; result: any }): void;
(ev: 'closed'): void;
}>();
const modal = ref<InstanceType<typeof MkModal>>();
@@ -122,14 +122,14 @@ function onBgClick() {
if (props.cancelableByBgClick) cancel();
}
*/
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') cancel();
function onKeydown(evt: KeyboardEvent) {
if (evt.key === 'Escape') cancel();
}
function onInputKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
function onInputKeydown(evt: KeyboardEvent) {
if (evt.key === 'Enter') {
evt.preventDefault();
evt.stopPropagation();
ok();
}
}

View File

@@ -42,7 +42,7 @@ const is = computed(() => {
"application/x-tar",
"application/gzip",
"application/x-7z-compressed"
].some(e => e === props.file.type)) return 'archive';
].some(archiveType => archiveType === props.file.type)) return 'archive';
return 'unknown';
});

View File

@@ -33,8 +33,8 @@ withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'done', r?: Misskey.entities.DriveFile[]): void;
(e: 'closed'): void;
(ev: 'done', r?: Misskey.entities.DriveFile[]): void;
(ev: 'closed'): void;
}>();
const dialog = ref<InstanceType<typeof XModalWindow>>();

View File

@@ -24,6 +24,6 @@ defineProps<{
}>();
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
</script>

View File

@@ -31,7 +31,7 @@
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
@@ -50,9 +50,9 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'chosen', r: Misskey.entities.DriveFile): void;
(e: 'dragstart'): void;
(e: 'dragend'): void;
(ev: 'chosen', r: Misskey.entities.DriveFile): void;
(ev: 'dragstart'): void;
(ev: 'dragend'): void;
}>();
const isDragging = ref(false);
@@ -99,14 +99,14 @@ function onClick(ev: MouseEvent) {
}
}
function onContextmenu(e: MouseEvent) {
os.contextMenu(getMenu(), e);
function onContextmenu(ev: MouseEvent) {
os.contextMenu(getMenu(), ev);
}
function onDragstart(e: DragEvent) {
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
function onDragstart(ev: DragEvent) {
if (ev.dataTransfer) {
ev.dataTransfer.effectAllowed = 'move';
ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
}
isDragging.value = true;
@@ -133,11 +133,11 @@ function rename() {
}
function describe() {
os.popup(import('@/components/media-caption.vue'), {
os.popup(defineAsyncComponent(() => import('@/components/media-caption.vue')), {
title: i18n.ts.describeFile,
input: {
placeholder: i18n.ts.inputNewDescription,
default: props.file.comment !== null ? props.file.comment : '',
default: props.file.comment != null ? props.file.comment : '',
},
image: props.file
}, {
@@ -146,7 +146,7 @@ function describe() {
let comment = result.result;
os.api('drive/files/update', {
fileId: props.file.id,
comment: comment.length == 0 ? null : comment
comment: comment.length === 0 ? null : comment
});
}
}, 'closed');

View File

@@ -27,7 +27,7 @@
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os';
import { i18n } from '@/i18n';
@@ -84,12 +84,12 @@ function onDragover(ev: DragEvent) {
return;
}
const isFile = ev.dataTransfer.items[0].kind == 'file';
const isDriveFile = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
const isFile = ev.dataTransfer.items[0].kind === 'file';
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
if (isFile || isDriveFile || isDriveFolder) {
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
} else {
ev.dataTransfer.dropEffect = 'none';
}
@@ -118,7 +118,7 @@ function onDrop(ev: DragEvent) {
//#region ドライブのファイル
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile);
emit('removeFile', file.id);
os.api('drive/files/update', {
@@ -130,11 +130,11 @@ function onDrop(ev: DragEvent) {
//#region ドライブのフォルダ
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder != '') {
if (driveFolder != null && driveFolder !== '') {
const folder = JSON.parse(driveFolder);
// 移動先が自分自身ならreject
if (folder.id == props.folder.id) return;
if (folder.id === props.folder.id) return;
emit('removeFolder', folder.id);
os.api('drive/folders/update', {
@@ -230,7 +230,7 @@ function onContextmenu(ev: MouseEvent) {
text: i18n.ts.openInWindow,
icon: 'fas fa-window-restore',
action: () => {
os.popup(import('./drive-window.vue'), {
os.popup(defineAsyncComponent(() => import('./drive-window.vue')), {
initialFolder: props.folder
}, {
}, 'closed');

View File

@@ -24,10 +24,10 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'move', v?: Misskey.entities.DriveFolder): void;
(e: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
(e: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
(e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
(ev: 'move', v?: Misskey.entities.DriveFolder): void;
(ev: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
(ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
}>();
const hover = ref(false);
@@ -45,22 +45,22 @@ function onMouseout() {
hover.value = false;
}
function onDragover(e: DragEvent) {
if (!e.dataTransfer) return;
function onDragover(ev: DragEvent) {
if (!ev.dataTransfer) return;
// このフォルダがルートかつカレントディレクトリならドロップ禁止
if (props.folder == null && props.parentFolder == null) {
e.dataTransfer.dropEffect = 'none';
ev.dataTransfer.dropEffect = 'none';
}
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
const isFile = ev.dataTransfer.items[0].kind === 'file';
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
if (isFile || isDriveFile || isDriveFolder) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
} else {
e.dataTransfer.dropEffect = 'none';
ev.dataTransfer.dropEffect = 'none';
}
return false;
@@ -74,22 +74,22 @@ function onDragleave() {
if (props.folder || props.parentFolder) draghover.value = false;
}
function onDrop(e: DragEvent) {
function onDrop(ev: DragEvent) {
draghover.value = false;
if (!e.dataTransfer) return;
if (!ev.dataTransfer) return;
// ファイルだったら
if (e.dataTransfer.files.length > 0) {
for (const file of Array.from(e.dataTransfer.files)) {
if (ev.dataTransfer.files.length > 0) {
for (const file of Array.from(ev.dataTransfer.files)) {
emit('upload', file, props.folder);
}
return;
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile);
emit('removeFile', file.id);
os.api('drive/files/update', {
@@ -100,11 +100,11 @@ function onDrop(e: DragEvent) {
//#endregion
//#region ドライブのフォルダ
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder != '') {
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder !== '') {
const folder = JSON.parse(driveFolder);
// 移動先が自分自身ならreject
if (props.folder && folder.id == props.folder.id) return;
if (props.folder && folder.id === props.folder.id) return;
emit('removeFolder', folder.id);
os.api('drive/folders/update', {
folderId: folder.id,

View File

@@ -110,11 +110,11 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
(e: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
(e: 'move-root'): void;
(e: 'cd', v: Misskey.entities.DriveFolder | null): void;
(e: 'open-folder', v: Misskey.entities.DriveFolder): void;
(ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
(ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
(ev: 'move-root'): void;
(ev: 'cd', v: Misskey.entities.DriveFolder | null): void;
(ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
}>();
const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
@@ -153,7 +153,7 @@ function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) {
const current = folder.value ? folder.value.id : null;
if (current != file.folderId) {
if (current !== file.folderId) {
removeFile(file);
} else {
addFile(file, true);
@@ -170,7 +170,7 @@ function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder)
function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
const current = folder.value ? folder.value.id : null;
if (current != updatedFolder.parentId) {
if (current !== updatedFolder.parentId) {
removeFolder(updatedFolder);
} else {
addFolder(updatedFolder, true);
@@ -181,23 +181,23 @@ function onStreamDriveFolderDeleted(folderId: string) {
removeFolder(folderId);
}
function onDragover(e: DragEvent): any {
if (!e.dataTransfer) return;
function onDragover(ev: DragEvent): any {
if (!ev.dataTransfer) return;
// ドラッグ元が自分自身の所有するアイテムだったら
if (isDragSource.value) {
// 自分自身にはドロップさせない
e.dataTransfer.dropEffect = 'none';
ev.dataTransfer.dropEffect = 'none';
return;
}
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
const isFile = ev.dataTransfer.items[0].kind === 'file';
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
if (isFile || isDriveFile || isDriveFolder) {
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
} else {
e.dataTransfer.dropEffect = 'none';
ev.dataTransfer.dropEffect = 'none';
}
return false;
@@ -211,24 +211,24 @@ function onDragleave() {
draghover.value = false;
}
function onDrop(e: DragEvent): any {
function onDrop(ev: DragEvent): any {
draghover.value = false;
if (!e.dataTransfer) return;
if (!ev.dataTransfer) return;
// ドロップされてきたものがファイルだったら
if (e.dataTransfer.files.length > 0) {
for (const file of Array.from(e.dataTransfer.files)) {
if (ev.dataTransfer.files.length > 0) {
for (const file of Array.from(ev.dataTransfer.files)) {
upload(file, folder.value);
}
return;
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile);
if (files.value.some(f => f.id == file.id)) return;
if (files.value.some(f => f.id === file.id)) return;
removeFile(file.id);
os.api('drive/files/update', {
fileId: file.id,
@@ -238,13 +238,13 @@ function onDrop(e: DragEvent): any {
//#endregion
//#region ドライブのフォルダ
const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder != '') {
const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
if (driveFolder != null && driveFolder !== '') {
const droppedFolder = JSON.parse(driveFolder);
// 移動先が自分自身ならreject
if (folder.value && droppedFolder.id == folder.value.id) return false;
if (folders.value.some(f => f.id == droppedFolder.id)) return false;
if (folder.value && droppedFolder.id === folder.value.id) return false;
if (folders.value.some(f => f.id === droppedFolder.id)) return false;
removeFolder(droppedFolder.id);
os.api('drive/folders/update', {
folderId: droppedFolder.id,
@@ -357,16 +357,16 @@ function onChangeFileInput() {
}
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
uploadFile(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
addFile(res, true);
});
}
function chooseFile(file: Misskey.entities.DriveFile) {
const isAlreadySelected = selectedFiles.value.some(f => f.id == file.id);
const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id);
if (props.multiple) {
if (isAlreadySelected) {
selectedFiles.value = selectedFiles.value.filter(f => f.id != file.id);
selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id);
} else {
selectedFiles.value.push(file);
}
@@ -382,10 +382,10 @@ function chooseFile(file: Misskey.entities.DriveFile) {
}
function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
const isAlreadySelected = selectedFolders.value.some(f => f.id == folderToChoose.id);
const isAlreadySelected = selectedFolders.value.some(f => f.id === folderToChoose.id);
if (props.multiple) {
if (isAlreadySelected) {
selectedFolders.value = selectedFolders.value.filter(f => f.id != folderToChoose.id);
selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToChoose.id);
} else {
selectedFolders.value.push(folderToChoose);
}
@@ -404,7 +404,7 @@ function move(target?: Misskey.entities.DriveFolder) {
if (!target) {
goRoot();
return;
} else if (typeof target == 'object') {
} else if (typeof target === 'object') {
target = target.id;
}
@@ -430,9 +430,9 @@ function move(target?: Misskey.entities.DriveFolder) {
function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
const current = folder.value ? folder.value.id : null;
if (current != folderToAdd.parentId) return;
if (current !== folderToAdd.parentId) return;
if (folders.value.some(f => f.id == folderToAdd.id)) {
if (folders.value.some(f => f.id === folderToAdd.id)) {
const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id);
folders.value[exist] = folderToAdd;
return;
@@ -447,9 +447,9 @@ function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
const current = folder.value ? folder.value.id : null;
if (current != fileToAdd.folderId) return;
if (current !== fileToAdd.folderId) return;
if (files.value.some(f => f.id == fileToAdd.id)) {
if (files.value.some(f => f.id === fileToAdd.id)) {
const exist = files.value.map(f => f.id).indexOf(fileToAdd.id);
files.value[exist] = fileToAdd;
return;
@@ -464,12 +464,12 @@ function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove;
folders.value = folders.value.filter(f => f.id != folderIdToRemove);
folders.value = folders.value.filter(f => f.id !== folderIdToRemove);
}
function removeFile(file: Misskey.entities.DriveFile | string) {
const fileId = typeof file === 'object' ? file.id : file;
files.value = files.value.filter(f => f.id != fileId);
files.value = files.value.filter(f => f.id !== fileId);
}
function appendFile(file: Misskey.entities.DriveFile) {
@@ -512,7 +512,7 @@ async function fetch() {
folderId: folder.value ? folder.value.id : null,
limit: foldersMax + 1
}).then(fetchedFolders => {
if (fetchedFolders.length == foldersMax + 1) {
if (fetchedFolders.length === foldersMax + 1) {
moreFolders.value = true;
fetchedFolders.pop();
}
@@ -524,7 +524,7 @@ async function fetch() {
type: props.type,
limit: filesMax + 1
}).then(fetchedFiles => {
if (fetchedFiles.length == filesMax + 1) {
if (fetchedFiles.length === filesMax + 1) {
moreFiles.value = true;
fetchedFiles.pop();
}
@@ -551,7 +551,7 @@ function fetchMoreFiles() {
untilId: files.value[files.value.length - 1].id,
limit: max + 1
}).then(files => {
if (files.length == max + 1) {
if (files.length === max + 1) {
moreFiles.value = true;
files.pop();
} else {

View File

@@ -25,8 +25,8 @@ withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'chosen', v: any): void;
(e: 'closed'): void;
(ev: 'chosen', v: any): void;
(ev: 'closed'): void;
}>();
function chosen(emoji: any) {

View File

@@ -24,7 +24,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'chosen', v: string, ev: MouseEvent): void;
(ev: 'chosen', v: string, event: MouseEvent): void;
}>();
const shown = ref(!!props.initialShown);

View File

@@ -61,7 +61,7 @@
</div>
<div>
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
<XSection v-for="category in categories" :key="category" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
</div>
</div>
<div class="tabs">
@@ -97,7 +97,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'chosen', v: string): void;
(ev: 'chosen', v: string): void;
}>();
const search = ref<HTMLInputElement>();
@@ -138,7 +138,7 @@ watch(q, () => {
const emojis = customEmojis;
const matches = new Set<Misskey.entities.CustomEmoji>();
const exactMatch = emojis.find(e => e.name === newQ);
const exactMatch = emojis.find(emoji => emoji.name === newQ);
if (exactMatch) matches.add(exactMatch);
if (newQ.includes(' ')) { // AND検索
@@ -201,7 +201,7 @@ watch(q, () => {
const emojis = emojilist;
const matches = new Set<UnicodeEmojiDef>();
const exactMatch = emojis.find(e => e.name === newQ);
const exactMatch = emojis.find(emoji => emoji.name === newQ);
if (exactMatch) matches.add(exactMatch);
if (newQ.includes(' ')) { // AND検索
@@ -295,7 +295,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
// 最近使った絵文字更新
if (!pinned.value.includes(key)) {
let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((e: any) => e !== key);
recents = recents.filter((emoji: any) => emoji !== key);
recents.unshift(key);
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
}
@@ -313,12 +313,12 @@ function done(query?: any): boolean | void {
if (query == null || typeof query !== 'string') return;
const q2 = query.replace(/:/g, '');
const exactMatchCustom = customEmojis.find(e => e.name === q2);
const exactMatchCustom = customEmojis.find(emoji => emoji.name === q2);
if (exactMatchCustom) {
chosen(exactMatchCustom);
return true;
}
const exactMatchUnicode = emojilist.find(e => e.char === q2 || e.name === q2);
const exactMatchUnicode = emojilist.find(emoji => emoji.char === q2 || emoji.name === q2);
if (exactMatchUnicode) {
chosen(exactMatchUnicode);
return true;

View File

@@ -58,7 +58,7 @@ if (props.user.isFollowing == null) {
}
function onFollowChange(user: Misskey.entities.UserDetailed) {
if (user.id == props.user.id) {
if (user.id === props.user.id) {
isFollowing.value = user.isFollowing;
hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
}
@@ -96,8 +96,8 @@ async function onClick() {
hasPendingFollowRequestFromYou.value = true;
}
}
} catch (e) {
console.error(e);
} catch (err) {
console.error(err);
} finally {
wait.value = false;
}

View File

@@ -41,8 +41,8 @@ import { instance } from '@/instance';
import { i18n } from '@/i18n';
const emit = defineEmits<{
(e: 'done'): void;
(e: 'closed'): void;
(ev: 'done'): void;
(ev: 'closed'): void;
}>();
let dialog: InstanceType<typeof XModalWindow> = $ref();

View File

@@ -44,7 +44,7 @@
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
</FormRange>
<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)" class="_formBlock">
<MkButton v-else-if="form[item].type === 'button'" class="_formBlock" @click="form[item].action($event, values)">
<span v-text="form[item].content || item"></span>
</MkButton>
</template>

View File

@@ -16,7 +16,7 @@
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
import { computed, defineAsyncComponent, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
import * as os from '@/os';
export default defineComponent({
@@ -112,7 +112,7 @@ export default defineComponent({
ev.preventDefault();
const tooltipShowing = ref(true);
os.popup(import('@/components/ui/tooltip.vue'), {
os.popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), {
showing: tooltipShowing,
text: computed(() => {
return props.textConverter(finalValue.value);

View File

@@ -31,7 +31,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void;
(ev: 'update:modelValue', v: boolean): void;
}>();
let button = $ref<HTMLElement>();

View File

@@ -32,7 +32,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'click', ev: MouseEvent): void;
(ev: 'click', v: MouseEvent): void;
}>();
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages

View File

@@ -46,7 +46,7 @@ export default defineComponent({
const url = computed(() => {
if (char.value) {
let codes = Array.from(char.value).map(x => x.codePointAt(0).toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
codes = codes.filter(x => x && x.length);
return `${twemojiSvgBase}/${codes.join('-')}.svg`;
} else {

View File

@@ -38,7 +38,7 @@
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, inject } from 'vue';
import * as tinycolor from 'tinycolor2';
import tinycolor from 'tinycolor2';
import { popupMenu } from '@/os';
import { url } from '@/config';
import { scrollToTop } from '@/scripts/scroll';

View File

@@ -1,6 +1,17 @@
<template>
<div class="yxspomdl" :class="{ inline, colored, mini }">
<div class="ring"></div>
<div class="container">
<svg class="spinner bg" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.125,0,0,1.125,12,12)">
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
</g>
</svg>
<svg class="spinner fg" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.125,0,0,1.125,12,12)">
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
</g>
</svg>
</div>
</div>
</template>
@@ -19,7 +30,7 @@ const props = withDefaults(defineProps<{
</script>
<style lang="scss" scoped>
@keyframes ring {
@keyframes spinner {
0% {
transform: rotate(0deg);
}
@@ -33,7 +44,7 @@ const props = withDefaults(defineProps<{
text-align: center;
cursor: wait;
--size: 48px;
--size: 40px;
&.colored {
color: var(--accent);
@@ -50,32 +61,31 @@ const props = withDefaults(defineProps<{
--size: 32px;
}
> .ring {
> .container {
position: relative;
display: inline-block;
vertical-align: middle;
width: var(--size);
height: var(--size);
margin: 0 auto;
&:before,
&:after {
content: " ";
display: block;
box-sizing: border-box;
width: var(--size);
height: var(--size);
border-radius: 50%;
border: solid 4px;
}
&:before {
border-color: currentColor;
opacity: 0.3;
}
&:after {
> .spinner {
position: absolute;
top: 0;
border-color: currentColor transparent transparent transparent;
animation: ring 0.5s linear infinite;
left: 0;
width: var(--size);
height: var(--size);
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
> .bg {
opacity: 0.275;
}
> .fg {
animation: spinner 0.5s linear infinite;
}
}
}

View File

@@ -31,6 +31,32 @@ const props = withDefaults(defineProps<{
}
}
.mfm-x2 {
--mfm-zoom-size: 200%;
}
.mfm-x3 {
--mfm-zoom-size: 400%;
}
.mfm-x4 {
--mfm-zoom-size: 600%;
}
.mfm-x2, .mfm-x3, .mfm-x4 {
font-size: var(--mfm-zoom-size);
.mfm-x2, .mfm-x3, .mfm-x4 {
/* only half effective */
font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
.mfm-x2, .mfm-x3, .mfm-x4 {
/* disabled */
font-size: 100%;
}
}
}
@keyframes mfm-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }

View File

@@ -17,7 +17,7 @@ const props = withDefaults(defineProps<{
mode: 'relative',
});
const _time = typeof props.time == 'string' ? new Date(props.time) : props.time;
const _time = typeof props.time === 'string' ? new Date(props.time) : props.time;
const absolute = _time.toLocaleString();
let now = $ref(new Date());

View File

@@ -18,7 +18,7 @@
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { defineAsyncComponent, defineComponent, ref } from 'vue';
import { toUnicode as decodePunycode } from 'punycode/';
import { url as local } from '@/config';
import * as os from '@/os';
@@ -50,7 +50,7 @@ export default defineComponent({
const el = ref();
useTooltip(el, (showing) => {
os.popup(import('@/components/url-preview-popup.vue'), {
os.popup(defineAsyncComponent(() => import('@/components/url-preview-popup.vue')), {
showing,
url: props.url,
source: el.value,

View File

@@ -25,7 +25,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
const modal = $ref<InstanceType<typeof MkModal>>();

View File

@@ -39,6 +39,19 @@ const bg = {
border-radius: 4px 0 0 4px;
overflow: hidden;
color: #fff;
text-shadow: /* .866 ≈ sin(60deg) */
1px 0 1px #000,
.866px .5px 1px #000,
.5px .866px 1px #000,
0 1px 1px #000,
-.5px .866px 1px #000,
-.866px .5px 1px #000,
-1px 0 1px #000,
-.866px -.5px 1px #000,
-.5px -.866px 1px #000,
0 -1px 1px #000,
.5px -.866px 1px #000,
.866px -.5px 1px #000;
> .icon {
height: 100%;

View File

@@ -8,7 +8,7 @@
</template>
<script lang="ts" setup>
import { } from 'vue';
import { defineAsyncComponent } from 'vue';
import { url as local } from '@/config';
import { useTooltip } from '@/scripts/use-tooltip';
import * as os from '@/os';
@@ -26,7 +26,7 @@ const target = self ? null : '_blank';
const el = $ref();
useTooltip($$(el), (showing) => {
os.popup(import('@/components/url-preview-popup.vue'), {
os.popup(defineAsyncComponent(() => import('@/components/url-preview-popup.vue')), {
showing,
url: props.url,
source: el,

View File

@@ -77,7 +77,7 @@ export default defineComponent({
computed: {
remainingLength(): number {
if (typeof this.inputValue != "string") return 512;
if (typeof this.inputValue !== "string") return 512;
return 512 - length(this.inputValue);
}
},
@@ -116,17 +116,17 @@ export default defineComponent({
}
},
onKeydown(e) {
if (e.which === 27) { // ESC
onKeydown(evt) {
if (evt.which === 27) { // ESC
this.cancel();
}
},
onInputKeydown(e) {
if (e.which === 13) { // Enter
if (e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
onInputKeydown(evt) {
if (evt.which === 13) { // Enter
if (evt.ctrlKey) {
evt.preventDefault();
evt.stopPropagation();
this.ok();
}
}

View File

@@ -1,22 +1,22 @@
<template>
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="ldlomzub" :class="{ isMe }" :to="url" :style="{ background: bg }">
<img class="icon" :src="`/avatar/@${username}@${host}`" alt="">
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" :class="[$style.root, { isMe }]" :to="url" :style="{ background: bg }">
<img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
<span class="main">
<span class="username">@{{ username }}</span>
<span v-if="(host != localHost) || $store.state.showFullAcct" class="host">@{{ toUnicode(host) }}</span>
<span v-if="(host != localHost) || $store.state.showFullAcct" :class="$style.mainHost">@{{ toUnicode(host) }}</span>
</span>
</MkA>
<a v-else class="ldlomzub" :href="url" target="_blank" rel="noopener" :style="{ background: bg }">
<a v-else :class="$style.root" :href="url" target="_blank" rel="noopener" :style="{ background: bg }">
<span class="main">
<span class="username">@{{ username }}</span>
<span class="host">@{{ toUnicode(host) }}</span>
<span :class="$style.mainHost">@{{ toUnicode(host) }}</span>
</span>
</a>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as tinycolor from 'tinycolor2';
import { defineComponent, useCssModule } from 'vue';
import tinycolor from 'tinycolor2';
import { toUnicode } from 'punycode';
import { host as localHost } from '@/config';
import { $i } from '@/account';
@@ -45,6 +45,8 @@ export default defineComponent({
const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
bg.setAlpha(0.1);
useCssModule();
return {
localHost,
isMe,
@@ -57,8 +59,8 @@ export default defineComponent({
});
</script>
<style lang="scss" scoped>
.ldlomzub {
<style lang="scss" module>
.root {
display: inline-block;
padding: 4px 8px 4px 4px;
border-radius: 999px;
@@ -67,18 +69,18 @@ export default defineComponent({
&.isMe {
color: var(--mentionMe);
}
}
> .icon {
width: 1.5em;
margin: 0 0.2em 0 0;
vertical-align: bottom;
border-radius: 100%;
}
.icon {
width: 1.5em;
height: 1.5em;
object-fit: cover;
margin: 0 0.2em 0 0;
vertical-align: bottom;
border-radius: 100%;
}
> .main {
> .host {
opacity: 0.5;
}
}
.mainHost {
opacity: 0.5;
}
</style>

View File

@@ -91,7 +91,8 @@ export default defineComponent({
let style;
switch (token.props.name) {
case 'tada': {
style = `font-size: 150%;` + (this.$store.state.animatedMfm ? 'animation: tada 1s linear infinite both;' : '');
const speed = validTime(token.props.args.speed) || '1s';
style = 'font-size: 150%;' + (this.$store.state.animatedMfm ? `animation: tada ${speed} linear infinite both;` : '');
break;
}
case 'jelly': {
@@ -123,11 +124,13 @@ export default defineComponent({
break;
}
case 'jump': {
style = this.$store.state.animatedMfm ? 'animation: mfm-jump 0.75s linear infinite;' : '';
const speed = validTime(token.props.args.speed) || '0.75s';
style = this.$store.state.animatedMfm ? `animation: mfm-jump ${speed} linear infinite;` : '';
break;
}
case 'bounce': {
style = this.$store.state.animatedMfm ? 'animation: mfm-bounce 0.75s linear infinite; transform-origin: center bottom;' : '';
const speed = validTime(token.props.args.speed) || '0.75s';
style = this.$store.state.animatedMfm ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
break;
}
case 'flip': {
@@ -139,16 +142,19 @@ export default defineComponent({
break;
}
case 'x2': {
style = `font-size: 200%;`;
break;
return h('span', {
class: 'mfm-x2',
}, genEl(token.children));
}
case 'x3': {
style = `font-size: 400%;`;
break;
return h('span', {
class: 'mfm-x3',
}, genEl(token.children));
}
case 'x4': {
style = `font-size: 600%;`;
break;
return h('span', {
class: 'mfm-x4',
}, genEl(token.children));
}
case 'font': {
const family =
@@ -168,7 +174,8 @@ export default defineComponent({
}, genEl(token.children));
}
case 'rainbow': {
style = this.$store.state.animatedMfm ? 'animation: mfm-rainbow 1s linear infinite;' : '';
const speed = validTime(token.props.args.speed) || '1s';
style = this.$store.state.animatedMfm ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
break;
}
case 'sparkle': {

View File

@@ -2,9 +2,9 @@
<div
v-if="!muted"
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
v-size="{ max: [500, 450, 350, 300] }"
ref="el"
class="lxwezrsl _block"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
@@ -197,7 +197,7 @@ const keymap = {
'q': () => renoteButton.value.renote(true),
'esc': blur,
'm|o': () => menu(true),
's': () => showContent.value != showContent.value,
's': () => showContent.value !== showContent.value,
};
useNoteCapture({

View File

@@ -185,7 +185,7 @@ const keymap = {
'down|j|tab': focusAfter,
'esc': blur,
'm|o': () => menu(true),
's': () => showContent.value != showContent.value,
's': () => showContent.value !== showContent.value,
};
useNoteCapture({

View File

@@ -1,34 +1,22 @@
<template>
<div class="lzyxtsnt">
<img v-if="image" :src="image.url"/>
<ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { defineComponent, PropType } from 'vue';
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
import * as os from '@/os';
import { ImageBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
props: {
block: {
type: Object as PropType<ImageBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
setup(props, ctx) {
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
const props = defineProps<{
block: PropType<ImageBlock>,
hpml: PropType<Hpml>,
}>();
return {
image
};
}
});
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
</script>
<style lang="scss" scoped>

View File

@@ -52,16 +52,16 @@ export default defineComponent({
const promise = new Promise((ok) => {
const canvas = this.hpml.canvases[this.block.canvasId];
canvas.toBlob(blob => {
const data = new FormData();
data.append('file', blob);
data.append('i', this.$i.token);
const formData = new FormData();
formData.append('file', blob);
formData.append('i', this.$i.token);
if (this.$store.state.uploadFolder) {
data.append('folderId', this.$store.state.uploadFolder);
formData.append('folderId', this.$store.state.uploadFolder);
}
fetch(apiUrl + '/drive/files/create', {
method: 'POST',
body: data
body: formData,
})
.then(response => response.json())
.then(f => {

View File

@@ -38,8 +38,8 @@ export default defineComponent({
let ast;
try {
ast = parse(props.page.script);
} catch (e) {
console.error(e);
} catch (err) {
console.error(err);
/*os.alert({
type: 'error',
text: 'Syntax error :('
@@ -48,11 +48,11 @@ export default defineComponent({
}
hpml.aiscript.exec(ast).then(() => {
hpml.eval();
}).catch(e => {
console.error(e);
}).catch(err => {
console.error(err);
/*os.alert({
type: 'error',
text: e
text: err
});*/
});
} else {

View File

@@ -104,7 +104,7 @@ function add() {
}
function remove(i) {
choices.value = choices.value.filter((_, _i) => _i != i);
choices.value = choices.value.filter((_, _i) => _i !== i);
}
function get() {

View File

@@ -88,7 +88,7 @@ export default defineComponent({
},
async describe(file) {
os.popup(import("@/components/media-caption.vue"), {
os.popup(defineAsyncComponent(() => import("@/components/media-caption.vue")), {
title: this.$ts.describeFile,
input: {
placeholder: this.$ts.inputNewDescription,
@@ -98,7 +98,7 @@ export default defineComponent({
}, {
done: result => {
if (!result || result.canceled) return;
let comment = result.result.length == 0 ? null : result.result;
let comment = result.result.length === 0 ? null : result.result;
os.api('drive/files/update', {
fileId: file.id,
comment: comment,

View File

@@ -62,7 +62,7 @@
</template>
<script lang="ts" setup>
import { inject, watch, nextTick, onMounted } from 'vue';
import { inject, watch, nextTick, onMounted, defineAsyncComponent } from 'vue';
import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
@@ -107,7 +107,7 @@ const props = withDefaults(defineProps<{
fixed?: boolean;
autofocus?: boolean;
}>(), {
initialVisibleUsers: [],
initialVisibleUsers: () => [],
autofocus: true,
});
@@ -228,7 +228,7 @@ if (props.mention) {
text += ' ';
}
if (props.reply && (props.reply.user.username != $i.username || (props.reply.user.host != null && props.reply.user.host != host))) {
if (props.reply && (props.reply.user.username !== $i.username || (props.reply.user.host != null && props.reply.user.host !== host))) {
text = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `;
}
@@ -239,16 +239,15 @@ if (props.reply && props.reply.text != null) {
for (const x of extractMentions(ast)) {
const mention = x.host ?
`@${x.username}@${toASCII(x.host)}` :
(otherHost == null || otherHost == host) ?
(otherHost == null || otherHost === host) ?
`@${x.username}` :
`@${x.username}@${toASCII(otherHost)}`;
// 自分は除外
if ($i.username == x.username && x.host == null) continue;
if ($i.username == x.username && x.host == host) continue;
if ($i.username === x.username && (x.host == null || x.host === host)) continue;
// 重複は除外
if (text.indexOf(`${mention} `) != -1) continue;
if (text.includes(`${mention} `)) continue;
text += `${mention} `;
}
@@ -303,7 +302,7 @@ function checkMissingMention() {
const ast = mfm.parse(text);
for (const x of extractMentions(ast)) {
if (!visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) {
hasNotSpecifiedMentions = true;
return;
}
@@ -316,7 +315,7 @@ function addMissingMention() {
const ast = mfm.parse(text);
for (const x of extractMentions(ast)) {
if (!visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
if (!visibleUsers.some(u => (u.username === x.username) && (u.host === x.host))) {
os.api('users/show', { username: x.username, host: x.host }).then(user => {
visibleUsers.push(user);
});
@@ -357,7 +356,7 @@ function chooseFileFrom(ev) {
}
function detachFile(id) {
files = files.filter(x => x.id != id);
files = files.filter(x => x.id !== id);
}
function updateFiles(_files) {
@@ -384,7 +383,7 @@ function setVisibility() {
return;
}
os.popup(import('./visibility-picker.vue'), {
os.popup(defineAsyncComponent(() => import('./visibility-picker.vue')), {
currentVisibility: visibility,
currentLocalOnly: localOnly,
src: visibilityButton,
@@ -427,24 +426,24 @@ function clear() {
quoteId = null;
}
function onKeydown(e: KeyboardEvent) {
if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && canPost) post();
if (e.which === 27) emit('esc');
function onKeydown(ev: KeyboardEvent) {
if ((ev.which === 10 || ev.which === 13) && (ev.ctrlKey || ev.metaKey) && canPost) post();
if (ev.which === 27) emit('esc');
typing();
}
function onCompositionUpdate(e: CompositionEvent) {
imeText = e.data;
function onCompositionUpdate(ev: CompositionEvent) {
imeText = ev.data;
typing();
}
function onCompositionEnd(e: CompositionEvent) {
function onCompositionEnd(ev: CompositionEvent) {
imeText = '';
}
async function onPaste(e: ClipboardEvent) {
for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
if (item.kind == 'file') {
async function onPaste(ev: ClipboardEvent) {
for (const { item, i } of Array.from(ev.clipboardData.items).map((item, i) => ({item, i}))) {
if (item.kind === 'file') {
const file = item.getAsFile();
const lio = file.name.lastIndexOf('.');
const ext = lio >= 0 ? file.name.slice(lio) : '';
@@ -453,10 +452,10 @@ async function onPaste(e: ClipboardEvent) {
}
}
const paste = e.clipboardData.getData('text');
const paste = ev.clipboardData.getData('text');
if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) {
e.preventDefault();
ev.preventDefault();
os.confirm({
type: 'info',
@@ -472,49 +471,49 @@ async function onPaste(e: ClipboardEvent) {
}
}
function onDragover(e) {
if (!e.dataTransfer.items[0]) return;
const isFile = e.dataTransfer.items[0].kind == 'file';
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
function onDragover(ev) {
if (!ev.dataTransfer.items[0]) return;
const isFile = ev.dataTransfer.items[0].kind === 'file';
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) {
e.preventDefault();
ev.preventDefault();
draghover = true;
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
}
}
function onDragenter(e) {
function onDragenter(ev) {
draghover = true;
}
function onDragleave(e) {
function onDragleave(ev) {
draghover = false;
}
function onDrop(e): void {
function onDrop(ev): void {
draghover = false;
// ファイルだったら
if (e.dataTransfer.files.length > 0) {
e.preventDefault();
for (const x of Array.from(e.dataTransfer.files)) upload(x);
if (ev.dataTransfer.files.length > 0) {
ev.preventDefault();
for (const x of Array.from(ev.dataTransfer.files)) upload(x);
return;
}
//#region ドライブのファイル
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile != '') {
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile);
files.push(file);
e.preventDefault();
ev.preventDefault();
}
//#endregion
}
function saveDraft() {
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
const draftData = JSON.parse(localStorage.getItem('drafts') || '{}');
data[draftKey] = {
draftData[draftKey] = {
updatedAt: new Date(),
data: {
text: text,
@@ -527,20 +526,20 @@ function saveDraft() {
}
};
localStorage.setItem('drafts', JSON.stringify(data));
localStorage.setItem('drafts', JSON.stringify(draftData));
}
function deleteDraft() {
const data = JSON.parse(localStorage.getItem('drafts') || '{}');
const draftData = JSON.parse(localStorage.getItem('drafts') || '{}');
delete data[draftKey];
delete draftData[draftKey];
localStorage.setItem('drafts', JSON.stringify(data));
localStorage.setItem('drafts', JSON.stringify(draftData));
}
async function post() {
let data = {
text: text == '' ? undefined : text,
let postData = {
text: text === '' ? undefined : text,
fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
replyId: props.reply ? props.reply.id : undefined,
renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
@@ -549,18 +548,18 @@ async function post() {
cw: useCw ? cw || '' : undefined,
localOnly: localOnly,
visibility: visibility,
visibleUserIds: visibility == 'specified' ? visibleUsers.map(u => u.id) : undefined,
visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
};
if (withHashtags && hashtags && hashtags.trim() !== '') {
const hashtags_ = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
data.text = data.text ? `${data.text} ${hashtags_}` : hashtags_;
postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
}
// plugin
if (notePostInterruptors.length > 0) {
for (const interruptor of notePostInterruptors) {
data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
postData = await interruptor.handler(JSON.parse(JSON.stringify(postData)));
}
}
@@ -572,13 +571,13 @@ async function post() {
}
posting = true;
os.api('notes/create', data, token).then(() => {
os.api('notes/create', postData, token).then(() => {
clear();
nextTick(() => {
deleteDraft();
emit('posted');
if (data.text && data.text != '') {
const hashtags_ = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
if (postData.text && postData.text !== '') {
const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
}
@@ -662,7 +661,7 @@ onMounted(() => {
cw = draft.data.cw;
visibility = draft.data.visibility;
localOnly = draft.data.localOnly;
files = (draft.data.files || []).filter(e => e);
files = (draft.data.files || []).filter(draftFile => draftFile);
if (draft.data.poll) {
poll = draft.data.poll;
}

View File

@@ -7,8 +7,8 @@
:class="{ reacted: note.myReaction == reaction, canToggle }"
@click="toggleReaction()"
>
<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
<span>{{ count }}</span>
<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/>
<span class="count">{{ count }}</span>
</button>
</template>
@@ -141,12 +141,16 @@ export default defineComponent({
background: var(--accent);
}
> span {
> .count {
color: var(--fgOnAccent);
}
> .icon {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
}
}
> span {
> .count {
font-size: 0.9em;
line-height: 32px;
margin: 0 0 0 4px;

View File

@@ -2,12 +2,12 @@
<XModalWindow ref="dialog"
:width="370"
:height="400"
@close="dialog.close()"
@close="onClose"
@closed="emit('closed')"
>
<template #header>{{ $ts.login }}</template>
<MkSignin :auto-set="autoSet" @login="onLogin"/>
<MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
</XModalWindow>
</template>
@@ -18,17 +18,25 @@ import MkSignin from './signin.vue';
const props = withDefaults(defineProps<{
autoSet?: boolean;
message?: string,
}>(), {
autoSet: false,
message: ''
});
const emit = defineEmits<{
(e: 'done'): void;
(e: 'closed'): void;
(ev: 'done'): void;
(ev: 'closed'): void;
(ev: 'cancelled'): void;
}>();
const dialog = $ref<InstanceType<typeof XModalWindow>>();
function onClose() {
emit('cancelled');
dialog.close();
}
function onLogin(res) {
emit('done', res);
dialog.close();

View File

@@ -1,39 +1,44 @@
<template>
<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
<div class="auth _section _formRoot">
<div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }"></div>
<div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
<MkInfo v-if="message">
{{ message }}
</MkInfo>
<div v-if="!totpLogin" class="normal-signin">
<MkInput v-model="username" class="_formBlock" :placeholder="$ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="$ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ $ts.forgotPassword }}</button></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
<MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
<MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
<p>{{ $ts.tapSecurityKey }}</p>
<p>{{ i18n.ts.tapSecurityKey }}</p>
<MkButton v-if="!queryingKey" @click="queryKey">
{{ $ts.retry }}
{{ i18n.ts.retry }}
</MkButton>
</div>
<div v-if="user && user.securityKeys" class="or-hr">
<p class="or-msg">{{ $ts.or }}</p>
<p class="or-msg">{{ i18n.ts.or }}</p>
</div>
<div class="twofa-group totp-group">
<p style="margin-bottom:0;">{{ $ts.twoStepAuthentication }}</p>
<p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p>
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" :with-password-toggle="true" required>
<template #label>{{ $ts.password }}</template>
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="fas fa-lock"></i></template>
</MkInput>
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
<template #label>{{ $ts.token }}</template>
<template #label>{{ i18n.ts.token }}</template>
<template #prefix><i class="fas fa-gavel"></i></template>
</MkInput>
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
</div>
</div>
@@ -45,190 +50,192 @@
</form>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import { toUnicode } from 'punycode/';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import { apiUrl, host } from '@/config';
import MkInfo from '@/components/ui/info.vue';
import { apiUrl, host as configHost } from '@/config';
import { byteify, hexify } from '@/scripts/2fa';
import * as os from '@/os';
import { login } from '@/account';
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
MkInput,
const MkCaptcha = defineAsyncComponent(() => import('./captcha.vue'));
let signing = $ref(false);
let user = $ref(null);
let username = $ref('');
let password = $ref('');
let token = $ref('');
let host = $ref(toUnicode(configHost));
let totpLogin = $ref(false);
let credential = $ref(null);
let challengeData = $ref(null);
let queryingKey = $ref(false);
let hCaptchaResponse = $ref(null);
let reCaptchaResponse = $ref(null);
const meta = $computed(() => instance);
const emit = defineEmits<{
(ev: 'login', v: any): void;
}>();
const props = defineProps({
withAvatar: {
type: Boolean,
required: false,
default: true
},
props: {
withAvatar: {
type: Boolean,
required: false,
default: true
},
autoSet: {
type: Boolean,
required: false,
default: false,
}
autoSet: {
type: Boolean,
required: false,
default: false,
},
emits: ['login'],
data() {
return {
signing: false,
user: null,
username: '',
password: '',
token: '',
apiUrl,
host: toUnicode(host),
totpLogin: false,
credential: null,
challengeData: null,
queryingKey: false,
};
},
computed: {
meta() {
return this.$instance;
},
},
methods: {
onUsernameChange() {
os.api('users/show', {
username: this.username
}).then(user => {
this.user = user;
}, () => {
this.user = null;
});
},
onLogin(res) {
if (this.autoSet) {
return login(res.i);
} else {
return;
}
},
queryKey() {
this.queryingKey = true;
return navigator.credentials.get({
publicKey: {
challenge: byteify(this.challengeData.challenge, 'base64'),
allowCredentials: this.challengeData.securityKeys.map(key => ({
id: byteify(key.id, 'hex'),
type: 'public-key',
transports: ['usb', 'nfc', 'ble', 'internal']
})),
timeout: 60 * 1000
}
}).catch(() => {
this.queryingKey = false;
return Promise.reject(null);
}).then(credential => {
this.queryingKey = false;
this.signing = true;
return os.api('signin', {
username: this.username,
password: this.password,
signature: hexify(credential.response.signature),
authenticatorData: hexify(credential.response.authenticatorData),
clientDataJSON: hexify(credential.response.clientDataJSON),
credentialId: credential.id,
challengeId: this.challengeData.challengeId
});
}).then(res => {
this.$emit('login', res);
return this.onLogin(res);
}).catch(err => {
if (err === null) return;
os.alert({
type: 'error',
text: this.$ts.signinFailed
});
this.signing = false;
});
},
onSubmit() {
this.signing = true;
if (!this.totpLogin && this.user && this.user.twoFactorEnabled) {
if (window.PublicKeyCredential && this.user.securityKeys) {
os.api('signin', {
username: this.username,
password: this.password
}).then(res => {
this.totpLogin = true;
this.signing = false;
this.challengeData = res;
return this.queryKey();
}).catch(this.loginFailed);
} else {
this.totpLogin = true;
this.signing = false;
}
} else {
os.api('signin', {
username: this.username,
password: this.password,
token: this.user && this.user.twoFactorEnabled ? this.token : undefined
}).then(res => {
this.$emit('login', res);
this.onLogin(res);
}).catch(this.loginFailed);
}
},
loginFailed(err) {
switch (err.id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.alert({
type: 'error',
title: this.$ts.loginFailed,
text: this.$ts.noSuchUser
});
break;
}
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
os.alert({
type: 'error',
title: this.$ts.loginFailed,
text: this.$ts.incorrectPassword,
});
break;
}
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
showSuspendedDialog();
break;
}
default: {
os.alert({
type: 'error',
title: this.$ts.loginFailed,
text: JSON.stringify(err)
});
}
}
this.challengeData = null;
this.totpLogin = false;
this.signing = false;
},
resetPassword() {
os.popup(import('@/components/forgot-password.vue'), {}, {
}, 'closed');
}
message: {
type: String,
required: false,
default: ''
}
});
function onUsernameChange() {
os.api('users/show', {
username: username
}).then(userResponse => {
user = userResponse;
}, () => {
user = null;
});
}
function onLogin(res) {
if (props.autoSet) {
return login(res.i);
}
}
function queryKey() {
queryingKey = true;
return navigator.credentials.get({
publicKey: {
challenge: byteify(challengeData.challenge, 'base64'),
allowCredentials: challengeData.securityKeys.map(key => ({
id: byteify(key.id, 'hex'),
type: 'public-key',
transports: ['usb', 'nfc', 'ble', 'internal']
})),
timeout: 60 * 1000
}
}).catch(() => {
queryingKey = false;
return Promise.reject(null);
}).then(credential => {
queryingKey = false;
signing = true;
return os.api('signin', {
username,
password,
signature: hexify(credential.response.signature),
authenticatorData: hexify(credential.response.authenticatorData),
clientDataJSON: hexify(credential.response.clientDataJSON),
credentialId: credential.id,
challengeId: challengeData.challengeId,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
});
}).then(res => {
emit('login', res);
return onLogin(res);
}).catch(err => {
if (err === null) return;
os.alert({
type: 'error',
text: i18n.ts.signinFailed
});
signing = false;
});
}
function onSubmit() {
signing = true;
console.log('submit')
if (!totpLogin && user && user.twoFactorEnabled) {
if (window.PublicKeyCredential && user.securityKeys) {
os.api('signin', {
username,
password,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
}).then(res => {
totpLogin = true;
signing = false;
challengeData = res;
return queryKey();
}).catch(loginFailed);
} else {
totpLogin = true;
signing = false;
}
} else {
os.api('signin', {
username,
password,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
token: user && user.twoFactorEnabled ? token : undefined
}).then(res => {
emit('login', res);
onLogin(res);
}).catch(loginFailed);
}
}
function loginFailed(err) {
switch (err.id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: i18n.ts.noSuchUser
});
break;
}
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: i18n.ts.incorrectPassword,
});
break;
}
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
showSuspendedDialog();
break;
}
default: {
console.log(err)
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: JSON.stringify(err)
});
}
}
challengeData = null;
totpLogin = false;
signing = false;
}
function resetPassword() {
os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {
}, 'closed');
}
</script>
<style lang="scss" scoped>

View File

@@ -27,8 +27,8 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'done'): void;
(e: 'closed'): void;
(ev: 'done'): void;
(ev: 'closed'): void;
}>();
const dialog = $ref<InstanceType<typeof XModalWindow>>();

View File

@@ -58,8 +58,8 @@
</template>
</I18n>
</MkSwitch>
<captcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
<captcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
<MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
<MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
</template>
</form>
@@ -67,7 +67,7 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
const getPasswordStrength = require('syuilo-password-strength');
const getPasswordStrength = await import('syuilo-password-strength');
import { toUnicode } from 'punycode/';
import { host, url } from '@/config';
import MkButton from './ui/button.vue';
@@ -81,7 +81,7 @@ export default defineComponent({
MkButton,
MkInput,
MkSwitch,
captcha: defineAsyncComponent(() => import('./captcha.vue')),
MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')),
},
props: {
@@ -124,20 +124,20 @@ export default defineComponent({
this.meta.tosUrl && !this.ToSAgreement ||
this.meta.enableHcaptcha && !this.hCaptchaResponse ||
this.meta.enableRecaptcha && !this.reCaptchaResponse ||
this.passwordRetypeState == 'not-match';
this.passwordRetypeState === 'not-match';
},
shouldShowProfileUrl(): boolean {
return (this.username != '' &&
this.usernameState != 'invalid-format' &&
this.usernameState != 'min-range' &&
this.usernameState != 'max-range');
return (this.username !== '' &&
this.usernameState !== 'invalid-format' &&
this.usernameState !== 'min-range' &&
this.usernameState !== 'max-range');
}
},
methods: {
onChangeUsername() {
if (this.username == '') {
if (this.username === '') {
this.usernameState = null;
return;
}
@@ -165,7 +165,7 @@ export default defineComponent({
},
onChangeEmail() {
if (this.email == '') {
if (this.email === '') {
this.emailState = null;
return;
}
@@ -188,7 +188,7 @@ export default defineComponent({
},
onChangePassword() {
if (this.password == '') {
if (this.password === '') {
this.passwordStrength = '';
return;
}
@@ -198,12 +198,12 @@ export default defineComponent({
},
onChangePasswordRetype() {
if (this.retypedPassword == '') {
if (this.retypedPassword === '') {
this.passwordRetypeState = null;
return;
}
this.passwordRetypeState = this.password == this.retypedPassword ? 'match' : 'not-match';
this.passwordRetypeState = this.password === this.retypedPassword ? 'match' : 'not-match';
},
onSubmit() {

View File

@@ -19,8 +19,8 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'note'): void;
(e: 'queue', count: number): void;
(ev: 'note'): void;
(ev: 'queue', count: number): void;
}>();
provide('inChannel', computed(() => props.src === 'channel'));
@@ -95,7 +95,7 @@ if (props.src === 'antenna') {
visibility: 'specified'
};
const onNote = note => {
if (note.visibility == 'specified') {
if (note.visibility === 'specified') {
prepend(note);
}
};

View File

@@ -19,7 +19,7 @@ defineProps<{
}>();
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
const zIndex = os.claimZIndex('high');

View File

@@ -90,7 +90,7 @@ export default defineComponent({
}
},
methods: {
onMousedown(e: MouseEvent) {
onMousedown(evt: MouseEvent) {
function distance(p, q) {
return Math.hypot(p.x - q.x, p.y - q.y);
}
@@ -104,18 +104,18 @@ export default defineComponent({
return Math.max(dist1, dist2, dist3, dist4) * 2;
}
const rect = e.target.getBoundingClientRect();
const rect = evt.target.getBoundingClientRect();
const ripple = document.createElement('div');
ripple.style.top = (e.clientY - rect.top - 1).toString() + 'px';
ripple.style.left = (e.clientX - rect.left - 1).toString() + 'px';
ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';
this.$refs.ripples.appendChild(ripple);
const circleCenterX = e.clientX - rect.left;
const circleCenterY = e.clientY - rect.top;
const circleCenterX = evt.clientX - rect.left;
const circleCenterY = evt.clientY - rect.top;
const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY);
const scale = calcCircleScale(evt.target.clientWidth, evt.target.clientHeight, circleCenterX, circleCenterY);
window.setTimeout(() => {
ripple.style.transform = 'scale(' + (scale / 2) + ')';

View File

@@ -19,7 +19,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
let rootEl = $ref<HTMLDivElement>();
@@ -63,8 +63,8 @@ onBeforeUnmount(() => {
}
});
function onMousedown(e: Event) {
if (!contains(rootEl, e.target) && (rootEl != e.target)) emit('closed');
function onMousedown(evt: Event) {
if (!contains(rootEl, evt.target) && (rootEl !== evt.target)) emit('closed');
}
</script>

View File

@@ -23,7 +23,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import * as tinycolor from 'tinycolor2';
import tinycolor from 'tinycolor2';
const localStoragePrefix = 'ui:folder:';

View File

@@ -60,7 +60,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'close'): void;
(ev: 'close'): void;
}>();
let itemsEl = $ref<HTMLDivElement>();

View File

@@ -79,10 +79,10 @@ export default defineComponent({
this.$refs.modal.close();
},
onKeydown(e) {
if (e.which === 27) { // Esc
e.preventDefault();
e.stopPropagation();
onKeydown(evt) {
if (evt.which === 27) { // Esc
evt.preventDefault();
evt.stopPropagation();
this.close();
}
},

View File

@@ -14,8 +14,14 @@
</div>
<div v-else ref="rootEl">
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
{{ $ts.loadMore }}
</MkButton>
<MkLoading v-else class="loading"/>
</div>
<slot :items="items"></slot>
<div v-show="more" key="_more_" class="cxiknjgy _gap">
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
{{ $ts.loadMore }}
</MkButton>
@@ -62,7 +68,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'queue', count: number): void;
(ev: 'queue', count: number): void;
}>();
type Item = { id: string; [another: string]: unknown; };
@@ -106,7 +112,7 @@ const init = async (): Promise<void> => {
offset.value = res.length;
error.value = false;
fetching.value = false;
}, e => {
}, err => {
error.value = true;
fetching.value = false;
});
@@ -149,7 +155,7 @@ const fetchMore = async (): Promise<void> => {
}
offset.value += res.length;
moreFetching.value = false;
}, e => {
}, err => {
moreFetching.value = false;
});
};
@@ -177,7 +183,7 @@ const fetchMoreAhead = async (): Promise<void> => {
}
offset.value += res.length;
moreFetching.value = false;
}, e => {
}, err => {
moreFetching.value = false;
});
};
@@ -244,6 +250,11 @@ const append = (item: Item): void => {
items.value.push(item);
};
const removeItem = (finder: (item: Item) => boolean) => {
const i = items.value.findIndex(finder);
items.value.splice(i, 1);
};
const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => {
const i = items.value.findIndex(item => item.id === id);
items.value[i] = replacer(items.value[i]);
@@ -273,9 +284,9 @@ defineExpose({
queue,
backed,
reload,
fetchMoreAhead,
prepend,
append,
removeItem,
updateItem,
});
</script>

View File

@@ -19,7 +19,7 @@ defineProps<{
}>();
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
let modal = $ref<InstanceType<typeof MkModal>>();

View File

@@ -139,10 +139,10 @@ export default defineComponent({
this.showing = false;
},
onKeydown(e) {
if (e.which === 27) { // Esc
e.preventDefault();
e.stopPropagation();
onKeydown(evt) {
if (evt.which === 27) { // Esc
evt.preventDefault();
evt.stopPropagation();
this.close();
}
},
@@ -162,15 +162,15 @@ export default defineComponent({
this.top();
},
onHeaderMousedown(e) {
onHeaderMousedown(evt) {
const main = this.$el as any;
if (!contains(main, document.activeElement)) main.focus();
const position = main.getBoundingClientRect();
const clickX = e.touches && e.touches.length > 0 ? e.touches[0].clientX : e.clientX;
const clickY = e.touches && e.touches.length > 0 ? e.touches[0].clientY : e.clientY;
const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX;
const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY;
const moveBaseX = clickX - position.left;
const moveBaseY = clickY - position.top;
const browserWidth = window.innerWidth;
@@ -204,10 +204,10 @@ export default defineComponent({
},
// 上ハンドル掴み時
onTopHandleMousedown(e) {
onTopHandleMousedown(evt) {
const main = this.$el as any;
const base = e.clientY;
const base = evt.clientY;
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
@@ -230,10 +230,10 @@ export default defineComponent({
},
// 右ハンドル掴み時
onRightHandleMousedown(e) {
onRightHandleMousedown(evt) {
const main = this.$el as any;
const base = e.clientX;
const base = evt.clientX;
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
const browserWidth = window.innerWidth;
@@ -254,10 +254,10 @@ export default defineComponent({
},
// 下ハンドル掴み時
onBottomHandleMousedown(e) {
onBottomHandleMousedown(evt) {
const main = this.$el as any;
const base = e.clientY;
const base = evt.clientY;
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
const browserHeight = window.innerHeight;
@@ -278,10 +278,10 @@ export default defineComponent({
},
// 左ハンドル掴み時
onLeftHandleMousedown(e) {
onLeftHandleMousedown(evt) {
const main = this.$el as any;
const base = e.clientX;
const base = evt.clientX;
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
@@ -304,27 +304,27 @@ export default defineComponent({
},
// 左上ハンドル掴み時
onTopLeftHandleMousedown(e) {
this.onTopHandleMousedown(e);
this.onLeftHandleMousedown(e);
onTopLeftHandleMousedown(evt) {
this.onTopHandleMousedown(evt);
this.onLeftHandleMousedown(evt);
},
// 右上ハンドル掴み時
onTopRightHandleMousedown(e) {
this.onTopHandleMousedown(e);
this.onRightHandleMousedown(e);
onTopRightHandleMousedown(evt) {
this.onTopHandleMousedown(evt);
this.onRightHandleMousedown(evt);
},
// 右下ハンドル掴み時
onBottomRightHandleMousedown(e) {
this.onBottomHandleMousedown(e);
this.onRightHandleMousedown(e);
onBottomRightHandleMousedown(evt) {
this.onBottomHandleMousedown(evt);
this.onRightHandleMousedown(evt);
},
// 左下ハンドル掴み時
onBottomLeftHandleMousedown(e) {
this.onBottomHandleMousedown(e);
this.onLeftHandleMousedown(e);
onBottomLeftHandleMousedown(evt) {
this.onBottomHandleMousedown(evt);
this.onLeftHandleMousedown(evt);
},
// 高さを適用

View File

@@ -70,7 +70,7 @@ export default defineComponent({
},
mounted() {
if (typeof this.q == 'object') {
if (typeof this.q === 'object') {
this.user = this.q;
this.fetched = true;
} else {

View File

@@ -60,9 +60,9 @@ import * as os from '@/os';
import { defaultStore } from '@/store';
const emit = defineEmits<{
(e: 'ok', selected: misskey.entities.UserDetailed): void;
(e: 'cancel'): void;
(e: 'closed'): void;
(ev: 'ok', selected: misskey.entities.UserDetailed): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
let username = $ref('');

View File

@@ -57,9 +57,9 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void;
(e: 'changeLocalOnly', v: boolean): void;
(e: 'closed'): void;
(ev: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void;
(ev: 'changeLocalOnly', v: boolean): void;
(ev: 'closed'): void;
}>();
let v = $ref(props.currentVisibility);

View File

@@ -21,8 +21,8 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'done');
(e: 'closed');
(ev: 'done');
(ev: 'closed');
}>();
function done() {

View File

@@ -19,7 +19,7 @@
<div class="customize-container">
<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button>
<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button>
<component class="handle" :ref="el => widgetRefs[element.id] = el" :is="`mkw-${element.name}`" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
<component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="handle" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
</div>
</template>
</XDraggable>
@@ -37,7 +37,7 @@ import { widgets as widgetDefs } from '@/widgets';
export default defineComponent({
components: {
XDraggable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
XDraggable: defineAsyncComponent(() => import('vuedraggable')),
MkSelect,
MkButton,
},

View File

@@ -1,7 +1,7 @@
// TODO: useTooltip関数使うようにしたい
// ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明
import { Directive, ref } from 'vue';
import { defineAsyncComponent, Directive, ref } from 'vue';
import { isTouchUsing } from '@/scripts/touch';
import { popup, alert } from '@/os';
@@ -45,7 +45,7 @@ export default {
if (self.text == null) return;
const showing = ref(true);
popup(import('@/components/ui/tooltip.vue'), {
popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), {
showing,
text: self.text,
targetElement: el,

View File

@@ -1,4 +1,4 @@
import { Directive, ref } from 'vue';
import { defineAsyncComponent, Directive, ref } from 'vue';
import autobind from 'autobind-decorator';
import { popup } from '@/os';
@@ -24,7 +24,7 @@ export class UserPreview {
const showing = ref(true);
popup(import('@/components/user-preview.vue'), {
popup(defineAsyncComponent(() => import('@/components/user-preview.vue')), {
showing,
q: this.user,
source: this.el

View File

@@ -96,6 +96,13 @@
{ "category": "face", "char": "\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F", "name": "face_in_clouds", "keywords": [] },
{ "category": "face", "char": "\uD83D\uDE2E\u200D\uD83D\uDCA8", "name": "face_exhaling", "keywords": [] },
{ "category": "face", "char": "\uD83D\uDE35\u200D\uD83D\uDCAB", "name": "face_with_spiral_eyes", "keywords": [] },
{ "category": "face", "char": "\uD83E\uDEE0", "name": "melting_face", "keywords": ["disappear", "dissolve", "liquid", "melt", "toketa"] },
{ "category": "face", "char": "\uD83E\uDEE2", "name": "face_with_open_eyes_and_hand_over_mouth", "keywords": ["amazement", "awe", "disbelief", "embarrass", "scared", "surprise", "ohoho"] },
{ "category": "face", "char": "\uD83E\uDEE3", "name": "face_with_peeking_eye", "keywords": ["captivated", "peep", "stare", "chunibyo"] },
{ "category": "face", "char": "\uD83E\uDEE1", "name": "saluting_face", "keywords": ["ok", "salute", "sunny", "troops", "yes", "raja"] },
{ "category": "face", "char": "\uD83E\uDEE5", "name": "dotted_line_face", "keywords": ["depressed", "disappear", "hide", "introvert", "invisible", "tensen"] },
{ "category": "face", "char": "\uD83E\uDEE4", "name": "face_with_diagonal_mouth", "keywords": ["disappointed", "meh", "skeptical", "unsure"] },
{ "category": "face", "char": "\uD83E\uDD79", "name": "face_holding_back_tears", "keywords": ["angry", "cry", "proud", "resist", "sad"] },
{ "category": "face", "char": "💩", "name": "poop", "keywords": ["hankey", "shitface", "fail", "turd", "shit"] },
{ "category": "face", "char": "😈", "name": "smiling_imp", "keywords": ["devil", "horns"] },
{ "category": "face", "char": "👿", "name": "imp", "keywords": ["devil", "angry", "horns"] },
@@ -149,11 +156,19 @@
{ "category": "people", "char": "🤞", "name": "crossed_fingers", "keywords": ["good", "lucky"] },
{ "category": "people", "char": "🖖", "name": "vulcan_salute", "keywords": ["hand", "fingers", "spock", "star trek"] },
{ "category": "people", "char": "✍", "name": "writing_hand", "keywords": ["lower_left_ballpoint_pen", "stationery", "write", "compose"] },
{ "category": "people", "char": "\uD83E\uDEF0", "name": "hand_with_index_finger_and_thumb_crossed", "keywords": [] },
{ "category": "people", "char": "\uD83E\uDEF1", "name": "rightwards_hand", "keywords": [] },
{ "category": "people", "char": "\uD83E\uDEF2", "name": "leftwards_hand", "keywords": [] },
{ "category": "people", "char": "\uD83E\uDEF3", "name": "palm_down_hand", "keywords": [] },
{ "category": "people", "char": "\uD83E\uDEF4", "name": "palm_up_hand", "keywords": [] },
{ "category": "people", "char": "\uD83E\uDEF5", "name": "index_pointing_at_the_viewer", "keywords": [] },
{ "category": "people", "char": "\uD83E\uDEF6", "name": "heart_hands", "keywords": ["moemoekyun"] },
{ "category": "people", "char": "🤏", "name": "pinching_hand", "keywords": ["hand", "fingers"] },
{ "category": "people", "char": "🤌", "name": "pinched_fingers", "keywords": ["hand", "fingers"] },
{ "category": "people", "char": "🤳", "name": "selfie", "keywords": ["camera", "phone"] },
{ "category": "people", "char": "💅", "name": "nail_care", "keywords": ["beauty", "manicure", "finger", "fashion", "nail"] },
{ "category": "people", "char": "👄", "name": "lips", "keywords": ["mouth", "kiss"] },
{ "category": "people", "char": "\uD83E\uDEE6", "name": "biting_lip", "keywords": [] },
{ "category": "people", "char": "🦷", "name": "tooth", "keywords": ["teeth", "dentist"] },
{ "category": "people", "char": "👅", "name": "tongue", "keywords": ["mouth", "playful"] },
{ "category": "people", "char": "👂", "name": "ear", "keywords": ["face", "hear", "sound", "listen"] },
@@ -275,7 +290,11 @@
{ "category": "people", "char": "🧚‍♀️", "name": "woman_fairy", "keywords": ["woman", "female"] },
{ "category": "people", "char": "🧚‍♂️", "name": "man_fairy", "keywords": ["man", "male"] },
{ "category": "people", "char": "👼", "name": "angel", "keywords": ["heaven", "wings", "halo"] },
{ "category": "people", "char": "\uD83E\uDDCC", "name": "troll", "keywords": [] },
{ "category": "people", "char": "🤰", "name": "pregnant_woman", "keywords": ["baby"] },
{ "category": "people", "char": "\uD83E\uDEC3", "name": "pregnant_man", "keywords": [] },
{ "category": "people", "char": "\uD83E\uDEC4", "name": "pregnant_person", "keywords": [] },
{ "category": "people", "char": "\uD83E\uDEC5", "name": "person_with_crown", "keywords": [] },
{ "category": "people", "char": "🤱", "name": "breastfeeding", "keywords": ["nursing", "baby"] },
{ "category": "people", "char": "\uD83D\uDC69\u200D\uD83C\uDF7C", "name": "woman_feeding_baby", "keywords": [] },
{ "category": "people", "char": "\uD83D\uDC68\u200D\uD83C\uDF7C", "name": "man_feeding_baby", "keywords": [] },
@@ -459,7 +478,7 @@
{ "category": "animals_and_nature", "char": "🐛", "name": "bug", "keywords": ["animal", "insect", "nature", "worm"] },
{ "category": "animals_and_nature", "char": "🦋", "name": "butterfly", "keywords": ["animal", "insect", "nature", "caterpillar"] },
{ "category": "animals_and_nature", "char": "🐌", "name": "snail", "keywords": ["slow", "animal", "shell"] },
{ "category": "animals_and_nature", "char": "🐞", "name": "beetle", "keywords": ["animal", "insect", "nature", "ladybug"] },
{ "category": "animals_and_nature", "char": "🐞", "name": "lady_beetle", "keywords": ["animal", "insect", "nature", "ladybug"] },
{ "category": "animals_and_nature", "char": "🐜", "name": "ant", "keywords": ["animal", "insect", "nature", "bug"] },
{ "category": "animals_and_nature", "char": "🦗", "name": "grasshopper", "keywords": ["animal", "cricket", "chirp"] },
{ "category": "animals_and_nature", "char": "🕷", "name": "spider", "keywords": ["animal", "arachnid"] },
@@ -615,6 +634,10 @@
{ "category": "animals_and_nature", "char": "💧", "name": "droplet", "keywords": ["water", "drip", "faucet", "spring"] },
{ "category": "animals_and_nature", "char": "💦", "name": "sweat_drops", "keywords": ["water", "drip", "oops"] },
{ "category": "animals_and_nature", "char": "🌊", "name": "ocean", "keywords": ["sea", "water", "wave", "nature", "tsunami", "disaster"] },
{ "category": "animals_and_nature", "char": "\uD83E\uDEB7", "name": "lotus", "keywords": [] },
{ "category": "animals_and_nature", "char": "\uD83E\uDEB8", "name": "coral", "keywords": [] },
{ "category": "animals_and_nature", "char": "\uD83E\uDEB9", "name": "empty_nest", "keywords": [] },
{ "category": "animals_and_nature", "char": "\uD83E\uDEBA", "name": "nest_with_eggs", "keywords": [] },
{ "category": "food_and_drink", "char": "🍏", "name": "green_apple", "keywords": ["fruit", "nature"] },
{ "category": "food_and_drink", "char": "🍎", "name": "apple", "keywords": ["fruit", "mac", "school"] },
{ "category": "food_and_drink", "char": "🍐", "name": "pear", "keywords": ["fruit", "nature", "food"] },
@@ -737,6 +760,9 @@
{ "category": "food_and_drink", "char": "🥣", "name": "bowl_with_spoon", "keywords": ["food", "breakfast", "cereal", "oatmeal", "porridge"] },
{ "category": "food_and_drink", "char": "🥡", "name": "takeout_box", "keywords": ["food", "leftovers"] },
{ "category": "food_and_drink", "char": "🥢", "name": "chopsticks", "keywords": ["food"] },
{ "category": "food_and_drink", "char": "\uD83E\uDED7", "name": "pouring_liquid", "keywords": [] },
{ "category": "food_and_drink", "char": "\uD83E\uDED8", "name": "beans", "keywords": [] },
{ "category": "food_and_drink", "char": "\uD83E\uDED9", "name": "jar", "keywords": [] },
{ "category": "activity", "char": "⚽", "name": "soccer", "keywords": ["sports", "football"] },
{ "category": "activity", "char": "🏀", "name": "basketball", "keywords": ["sports", "balls", "NBA"] },
{ "category": "activity", "char": "🏈", "name": "football", "keywords": ["sports", "balls", "NFL"] },
@@ -844,6 +870,8 @@
{ "category": "activity", "char": "🪄", "name": "magic_wand", "keywords": [] },
{ "category": "activity", "char": "🪅", "name": "pinata", "keywords": [] },
{ "category": "activity", "char": "🪆", "name": "nesting_dolls", "keywords": [] },
{ "category": "activity", "char": "\uD83E\uDEAC", "name": "hamsa", "keywords": [] },
{ "category": "activity", "char": "\uD83E\uDEA9", "name": "mirror_ball", "keywords": [] },
{ "category": "travel_and_places", "char": "🚗", "name": "red_car", "keywords": ["red", "transportation", "vehicle"] },
{ "category": "travel_and_places", "char": "🚕", "name": "taxi", "keywords": ["uber", "vehicle", "cars", "transportation"] },
{ "category": "travel_and_places", "char": "🚙", "name": "blue_car", "keywords": ["transportation", "vehicle"] },
@@ -971,11 +999,12 @@
{ "category": "travel_and_places", "char": "🕋", "name": "kaaba", "keywords": ["mecca", "mosque", "islam"] },
{ "category": "travel_and_places", "char": "⛩", "name": "shinto_shrine", "keywords": ["temple", "japan", "kyoto"] },
{ "category": "travel_and_places", "char": "🛕", "name": "hindu_temple", "keywords": ["temple"] },
{ "category": "travel_and_places", "char": "🪨", "name": "rock", "keywords": [] },
{ "category": "travel_and_places", "char": "🪵", "name": "wood", "keywords": [] },
{ "category": "travel_and_places", "char": "🛖", "name": "hut", "keywords": [] },
{ "category": "travel_and_places", "char": "\uD83D\uDEDD", "name": "playground_slide", "keywords": [] },
{ "category": "travel_and_places", "char": "\uD83D\uDEDE", "name": "wheel", "keywords": [] },
{ "category": "travel_and_places", "char": "\uD83D\uDEDF", "name": "ring_buoy", "keywords": [] },
{ "category": "objects", "char": "⌚", "name": "watch", "keywords": ["time", "accessories"] },
{ "category": "objects", "char": "📱", "name": "iphone", "keywords": ["technology", "apple", "gadgets", "dial"] },
{ "category": "objects", "char": "📲", "name": "calling", "keywords": ["iphone", "incoming"] },
@@ -1016,6 +1045,7 @@
{ "category": "objects", "char": "⌛", "name": "hourglass", "keywords": ["time", "clock", "oldschool", "limit", "exam", "quiz", "test"] },
{ "category": "objects", "char": "📡", "name": "satellite", "keywords": ["communication", "future", "radio", "space"] },
{ "category": "objects", "char": "🔋", "name": "battery", "keywords": ["power", "energy", "sustain"] },
{ "category": "objects", "char": "\uD83E\uDEAB", "name": "battery", "keywords": [] },
{ "category": "objects", "char": "🔌", "name": "electric_plug", "keywords": ["charger", "power"] },
{ "category": "objects", "char": "💡", "name": "bulb", "keywords": ["light", "electricity", "idea"] },
{ "category": "objects", "char": "🔦", "name": "flashlight", "keywords": ["dark", "camping", "sight", "night"] },
@@ -1031,6 +1061,7 @@
{ "category": "objects", "char": "💰", "name": "moneybag", "keywords": ["dollar", "payment", "coins", "sale"] },
{ "category": "objects", "char": "🪙", "name": "coin", "keywords": ["dollar", "payment", "coins", "sale"] },
{ "category": "objects", "char": "💳", "name": "credit_card", "keywords": ["money", "sales", "dollar", "bill", "payment", "shopping"] },
{ "category": "objects", "char": "\uD83E\uDEAB", "name": "identification_card", "keywords": [] },
{ "category": "objects", "char": "💎", "name": "gem", "keywords": ["blue", "ruby", "diamond", "jewelry"] },
{ "category": "objects", "char": "⚖", "name": "balance_scale", "keywords": ["law", "fairness", "weight"] },
{ "category": "objects", "char": "🧰", "name": "toolbox", "keywords": ["tools", "diy", "fix", "maintainer", "mechanic"] },
@@ -1077,6 +1108,8 @@
{ "category": "objects", "char": "🩹", "name": "adhesive_bandage", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] },
{ "category": "objects", "char": "🩺", "name": "stethoscope", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] },
{ "category": "objects", "char": "🪒", "name": "razor", "keywords": ["health"] },
{ "category": "objects", "char": "\uD83E\uDE7B", "name": "xray", "keywords": [] },
{ "category": "objects", "char": "\uD83E\uDE7C", "name": "crutch", "keywords": [] },
{ "category": "objects", "char": "🧬", "name": "dna", "keywords": ["biologist", "genetics", "life"] },
{ "category": "objects", "char": "🧫", "name": "petri_dish", "keywords": ["bacteria", "biology", "culture", "lab"] },
{ "category": "objects", "char": "🧪", "name": "test_tube", "keywords": ["chemistry", "experiment", "lab", "science"] },
@@ -1111,6 +1144,7 @@
{ "category": "objects", "char": "🪤", "name": "mouse_trap", "keywords": ["household"] },
{ "category": "objects", "char": "🪣", "name": "bucket", "keywords": ["household"] },
{ "category": "objects", "char": "🪥", "name": "toothbrush", "keywords": ["household"] },
{ "category": "objects", "char": "\uD83E\uDEE7", "name": "bubbles", "keywords": [] },
{ "category": "objects", "char": "⛱", "name": "parasol_on_ground", "keywords": ["weather", "summer"] },
{ "category": "objects", "char": "🗿", "name": "moyai", "keywords": ["rock", "easter island", "moai"] },
{ "category": "objects", "char": "🛍", "name": "shopping", "keywords": ["mall", "buy", "purchase"] },
@@ -1404,6 +1438,7 @@
{ "category": "symbols", "char": "", "name": "heavy_minus_sign", "keywords": ["math", "calculation", "subtract", "less"] },
{ "category": "symbols", "char": "➗", "name": "heavy_division_sign", "keywords": ["divide", "math", "calculation"] },
{ "category": "symbols", "char": "✖️", "name": "heavy_multiplication_x", "keywords": ["math", "calculation"] },
{ "category": "symbols", "char": "\uD83D\uDFF0", "name": "heavy_equals_sign", "keywords": [] },
{ "category": "symbols", "char": "♾", "name": "infinity", "keywords": ["forever"] },
{ "category": "symbols", "char": "💲", "name": "heavy_dollar_sign", "keywords": ["money", "sales", "payment", "currency", "buck"] },
{ "category": "symbols", "char": "💱", "name": "currency_exchange", "keywords": ["money", "sales", "dollar", "travel"] },
@@ -1747,3 +1782,4 @@
{ "category": "flags", "char": "🇺🇳", "name": "united_nations", "keywords": ["un", "flag", "banner"] },
{ "category": "flags", "char": "🏴‍☠️", "name": "pirate_flag", "keywords": ["skull", "crossbones", "flag", "banner"] }
]

View File

@@ -1,7 +1,7 @@
export default (v, digits = 0) => {
if (v == null) return '?';
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (v == 0) return '0';
if (v === 0) return '0';
const isMinus = v < 0;
if (isMinus) v = -v;
const i = Math.floor(Math.log(v) / Math.log(1024));

View File

@@ -13,9 +13,9 @@ if (localStorage.getItem('accounts') != null) {
}
//#endregion
import { computed, createApp, watch, markRaw, version as vueVersion } from 'vue';
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue';
import compareVersions from 'compare-versions';
import * as JSON5 from 'json5';
import JSON5 from 'json5';
import widgets from '@/widgets';
import directives from '@/directives';
@@ -149,7 +149,7 @@ if ($i && $i.token) {
try {
document.body.innerHTML = '<div>Please wait...</div>';
await login(i);
} catch (e) {
} catch (err) {
// Render the error screen
// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
document.body.innerHTML = '<div id="err">Oops!</div>';
@@ -171,14 +171,14 @@ fetchInstanceMetaPromise.then(() => {
initializeSw();
});
const app = createApp(await (
window.location.search === '?zen' ? import('@/ui/zen.vue') :
!$i ? import('@/ui/visitor.vue') :
ui === 'deck' ? import('@/ui/deck.vue') :
ui === 'desktop' ? import('@/ui/desktop.vue') :
ui === 'classic' ? import('@/ui/classic.vue') :
import('@/ui/universal.vue')
).then(x => x.default));
const app = createApp(
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'desktop' ? defineAsyncComponent(() => import('@/ui/desktop.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
defineAsyncComponent(() => import('@/ui/universal.vue'))
);
if (_DEV_) {
app.config.performance = true;
@@ -206,11 +206,26 @@ if (splash) splash.addEventListener('transitionend', () => {
splash.remove();
});
const rootEl = document.createElement('div');
document.body.appendChild(rootEl);
if (ui === 'deck') await deckStore.ready;
// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210
// なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する
const rootEl = (() => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
const currentEl = document.getElementById(MISSKEY_MOUNT_DIV_ID);
if (currentEl) {
console.warn('multiple import detected');
return currentEl;
}
const rootEl = document.createElement('div');
rootEl.id = MISSKEY_MOUNT_DIV_ID;
document.body.appendChild(rootEl);
return rootEl;
})();
app.mount(rootEl);
// boot.jsのやつを解除
@@ -236,10 +251,10 @@ if (lastVersion !== version) {
if (lastVersion != null && compareVersions(version, lastVersion) === 1) {
// ログインしてる場合だけ
if ($i) {
popup(import('@/components/updated.vue'), {}, {}, 'closed');
popup(defineAsyncComponent(() => import('@/components/updated.vue')), {}, {}, 'closed');
}
}
} catch (e) {
} catch (err) {
}
}
@@ -324,7 +339,7 @@ stream.on('_disconnected_', async () => {
}
});
stream.on('emojiAdded', data => {
stream.on('emojiAdded', emojiData => {
// TODO
//store.commit('instance/set', );
});

View File

@@ -4,11 +4,11 @@ import { api } from './os';
// TODO: 他のタブと永続化されたstateを同期
const data = localStorage.getItem('instance');
const instanceData = localStorage.getItem('instance');
// TODO: instanceをリアクティブにするかは再考の余地あり
export const instance: Misskey.entities.InstanceMetadata = reactive(data ? JSON.parse(data) : {
export const instance: Misskey.entities.InstanceMetadata = reactive(instanceData ? JSON.parse(instanceData) : {
// TODO: set default values
});

View File

@@ -1,6 +1,6 @@
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
import { Component, markRaw, Ref, ref } from 'vue';
import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
import { EventEmitter } from 'eventemitter3';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as Misskey from 'misskey-js';
@@ -59,10 +59,10 @@ export const apiWithDialog = ((
token?: string | null | undefined,
) => {
const promise = api(endpoint, data, token);
promiseDialog(promise, null, (e) => {
promiseDialog(promise, null, (err) => {
alert({
type: 'error',
text: e.message + '\n' + (e as any).id,
text: err.message + '\n' + (err as any).id,
});
});
@@ -72,7 +72,7 @@ export const apiWithDialog = ((
export function promiseDialog<T extends Promise<any>>(
promise: T,
onSuccess?: ((res: any) => void) | null,
onFailure?: ((e: Error) => void) | null,
onFailure?: ((err: Error) => void) | null,
text?: string,
): T {
const showing = ref(true);
@@ -88,14 +88,14 @@ export function promiseDialog<T extends Promise<any>>(
showing.value = false;
}, 1000);
}
}).catch(e => {
}).catch(err => {
showing.value = false;
if (onFailure) {
onFailure(e);
onFailure(err);
} else {
alert({
type: 'error',
text: e
text: err,
});
}
});
@@ -110,10 +110,6 @@ export function promiseDialog<T extends Promise<any>>(
return promise;
}
function isModule(x: any): x is typeof import('*.vue') {
return x.default != null;
}
let popupIdCount = 0;
export const popups = ref([]) as Ref<{
id: any;
@@ -131,10 +127,7 @@ export function claimZIndex(priority: 'low' | 'middle' | 'high' = 'low'): number
return zIndexes[priority];
}
export async function popup(component: Component | typeof import('*.vue') | Promise<Component | typeof import('*.vue')>, props: Record<string, any>, events = {}, disposeEvent?: string) {
if (component.then) component = await component;
if (isModule(component)) component = component.default;
export async function popup(component: Component, props: Record<string, any>, events = {}, disposeEvent?: string) {
markRaw(component);
const id = ++popupIdCount;
@@ -163,7 +156,7 @@ export async function popup(component: Component | typeof import('*.vue') | Prom
export function pageWindow(path: string) {
const { component, props } = resolve(path);
popup(import('@/components/page-window.vue'), {
popup(defineAsyncComponent(() => import('@/components/page-window.vue')), {
initialPath: path,
initialComponent: markRaw(component),
initialProps: props,
@@ -172,7 +165,7 @@ export function pageWindow(path: string) {
export function modalPageWindow(path: string) {
const { component, props } = resolve(path);
popup(import('@/components/modal-page-window.vue'), {
popup(defineAsyncComponent(() => import('@/components/modal-page-window.vue')), {
initialPath: path,
initialComponent: markRaw(component),
initialProps: props,
@@ -180,7 +173,7 @@ export function modalPageWindow(path: string) {
}
export function toast(message: string) {
popup(import('@/components/toast.vue'), {
popup(defineAsyncComponent(() => import('@/components/toast.vue')), {
message
}, {}, 'closed');
}
@@ -191,7 +184,7 @@ export function alert(props: {
text?: string | null;
}): Promise<void> {
return new Promise((resolve, reject) => {
popup(import('@/components/dialog.vue'), props, {
popup(defineAsyncComponent(() => import('@/components/dialog.vue')), props, {
done: result => {
resolve();
},
@@ -205,7 +198,7 @@ export function confirm(props: {
text?: string | null;
}): Promise<{ canceled: boolean }> {
return new Promise((resolve, reject) => {
popup(import('@/components/dialog.vue'), {
popup(defineAsyncComponent(() => import('@/components/dialog.vue')), {
...props,
showCancelButton: true,
}, {
@@ -226,7 +219,7 @@ export function inputText(props: {
canceled: false; result: string;
}> {
return new Promise((resolve, reject) => {
popup(import('@/components/dialog.vue'), {
popup(defineAsyncComponent(() => import('@/components/dialog.vue')), {
title: props.title,
text: props.text,
input: {
@@ -251,7 +244,7 @@ export function inputNumber(props: {
canceled: false; result: number;
}> {
return new Promise((resolve, reject) => {
popup(import('@/components/dialog.vue'), {
popup(defineAsyncComponent(() => import('@/components/dialog.vue')), {
title: props.title,
text: props.text,
input: {
@@ -276,7 +269,7 @@ export function inputDate(props: {
canceled: false; result: Date;
}> {
return new Promise((resolve, reject) => {
popup(import('@/components/dialog.vue'), {
popup(defineAsyncComponent(() => import('@/components/dialog.vue')), {
title: props.title,
text: props.text,
input: {
@@ -313,7 +306,7 @@ export function select<C extends any = any>(props: {
canceled: false; result: C;
}> {
return new Promise((resolve, reject) => {
popup(import('@/components/dialog.vue'), {
popup(defineAsyncComponent(() => import('@/components/dialog.vue')), {
title: props.title,
text: props.text,
select: {
@@ -335,7 +328,7 @@ export function success() {
window.setTimeout(() => {
showing.value = false;
}, 1000);
popup(import('@/components/waiting-dialog.vue'), {
popup(defineAsyncComponent(() => import('@/components/waiting-dialog.vue')), {
success: true,
showing: showing
}, {
@@ -347,7 +340,7 @@ export function success() {
export function waiting() {
return new Promise((resolve, reject) => {
const showing = ref(true);
popup(import('@/components/waiting-dialog.vue'), {
popup(defineAsyncComponent(() => import('@/components/waiting-dialog.vue')), {
success: false,
showing: showing
}, {
@@ -358,7 +351,7 @@ export function waiting() {
export function form(title, form) {
return new Promise((resolve, reject) => {
popup(import('@/components/form-dialog.vue'), { title, form }, {
popup(defineAsyncComponent(() => import('@/components/form-dialog.vue')), { title, form }, {
done: result => {
resolve(result);
},
@@ -368,7 +361,7 @@ export function form(title, form) {
export async function selectUser() {
return new Promise((resolve, reject) => {
popup(import('@/components/user-select-dialog.vue'), {}, {
popup(defineAsyncComponent(() => import('@/components/user-select-dialog.vue')), {}, {
ok: user => {
resolve(user);
},
@@ -378,7 +371,7 @@ export async function selectUser() {
export async function selectDriveFile(multiple: boolean) {
return new Promise((resolve, reject) => {
popup(import('@/components/drive-select-dialog.vue'), {
popup(defineAsyncComponent(() => import('@/components/drive-select-dialog.vue')), {
type: 'file',
multiple
}, {
@@ -393,7 +386,7 @@ export async function selectDriveFile(multiple: boolean) {
export async function selectDriveFolder(multiple: boolean) {
return new Promise((resolve, reject) => {
popup(import('@/components/drive-select-dialog.vue'), {
popup(defineAsyncComponent(() => import('@/components/drive-select-dialog.vue')), {
type: 'folder',
multiple
}, {
@@ -408,7 +401,7 @@ export async function selectDriveFolder(multiple: boolean) {
export async function pickEmoji(src: HTMLElement | null, opts) {
return new Promise((resolve, reject) => {
popup(import('@/components/emoji-picker-dialog.vue'), {
popup(defineAsyncComponent(() => import('@/components/emoji-picker-dialog.vue')), {
src,
...opts
}, {
@@ -458,7 +451,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
characterData: false,
});
openingEmojiPicker = await popup(import('@/components/emoji-picker-window.vue'), {
openingEmojiPicker = await popup(defineAsyncComponent(() => import('@/components/emoji-picker-window.vue')), {
src,
...opts
}, {
@@ -480,7 +473,7 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
}) {
return new Promise((resolve, reject) => {
let dispose;
popup(import('@/components/ui/popup-menu.vue'), {
popup(defineAsyncComponent(() => import('@/components/ui/popup-menu.vue')), {
items,
src,
width: options?.width,
@@ -501,7 +494,7 @@ export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent)
ev.preventDefault();
return new Promise((resolve, reject) => {
let dispose;
popup(import('@/components/ui/context-menu.vue'), {
popup(defineAsyncComponent(() => import('@/components/ui/context-menu.vue')), {
items,
ev,
}, {

View File

@@ -150,6 +150,7 @@ const patrons = [
'Weeble',
'蝉暮せせせ',
'ThatOneCalculator',
'pixeldesu',
];
let easterEggReady = false;

View File

@@ -7,7 +7,7 @@
<template #label>URL</template>
</MkInput>
<MkInput v-model="ad.imageUrl" class="_formBlock">
<template #label>{{ $ts.imageUrl }}</template>
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<FormRadios v-model="ad.place" class="_formBlock">
<template #label>Form</template>
@@ -17,34 +17,34 @@
</FormRadios>
<!--
<div style="margin: 32px 0;">
{{ $ts.priority }}
<MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
<MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
{{ i18n.ts.priority }}
<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
<MkRadio v-model="ad.priority" value="middle">{{ i18n.ts.middle }}</MkRadio>
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
</div>
-->
<FormSplit>
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ $ts.ratio }}</template>
<template #label>{{ i18n.ts.ratio }}</template>
</MkInput>
<MkInput v-model="ad.expiresAt" type="date">
<template #label>{{ $ts.expiration }}</template>
<template #label>{{ i18n.ts.expiration }}</template>
</MkInput>
</FormSplit>
<MkTextarea v-model="ad.memo" class="_formBlock">
<template #label>{{ $ts.memo }}</template>
<template #label>{{ i18n.ts.memo }}</template>
</MkTextarea>
<div class="buttons _formBlock">
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
<MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
<MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</div>
</div>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
@@ -52,81 +52,65 @@ import FormRadios from '@/components/form/radios.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
MkInput,
MkTextarea,
FormRadios,
FormSplit,
},
let ads: any[] = $ref([]);
emits: ['info'],
os.api('admin/ad/list').then(adsResponse => {
ads = adsResponse;
});
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.ads,
icon: 'fas fa-audio-description',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: this.$ts.add,
handler: this.add,
}],
},
ads: [],
}
},
function add() {
ads.unshift({
id: null,
memo: '',
place: 'square',
priority: 'middle',
ratio: 1,
url: '',
imageUrl: null,
expiresAt: null,
});
}
created() {
os.api('admin/ad/list').then(ads => {
this.ads = ads;
function remove(ad) {
os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: ad.url }),
}).then(({ canceled }) => {
if (canceled) return;
ads = ads.filter(x => x !== ad);
os.apiWithDialog('admin/ad/delete', {
id: ad.id
});
},
});
}
methods: {
add() {
this.ads.unshift({
id: null,
memo: '',
place: 'square',
priority: 'middle',
ratio: 1,
url: '',
imageUrl: null,
expiresAt: null,
});
},
function save(ad) {
if (ad.id == null) {
os.apiWithDialog('admin/ad/create', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
});
} else {
os.apiWithDialog('admin/ad/update', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
});
}
}
remove(ad) {
os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: ad.url }),
}).then(({ canceled }) => {
if (canceled) return;
this.ads = this.ads.filter(x => x != ad);
os.apiWithDialog('admin/ad/delete', {
id: ad.id
});
});
},
save(ad) {
if (ad.id == null) {
os.apiWithDialog('admin/ad/create', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
});
} else {
os.apiWithDialog('admin/ad/update', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime()
});
}
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.ads,
icon: 'fas fa-audio-description',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.add,
handler: add,
}],
}
});
</script>

View File

@@ -3,112 +3,98 @@
<section v-for="announcement in announcements" class="_card _gap announcements">
<div class="_content announcement">
<MkInput v-model="announcement.title">
<template #label>{{ $ts.title }}</template>
<template #label>{{ i18n.ts.title }}</template>
</MkInput>
<MkTextarea v-model="announcement.text">
<template #label>{{ $ts.text }}</template>
<template #label>{{ i18n.ts.text }}</template>
</MkTextarea>
<MkInput v-model="announcement.imageUrl">
<template #label>{{ $ts.imageUrl }}</template>
<template #label>{{ i18n.ts.imageUrl }}</template>
</MkInput>
<p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
<p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
<div class="buttons">
<MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
<MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton>
<MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</div>
</section>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
MkInput,
MkTextarea,
},
let announcements: any[] = $ref([]);
emits: ['info'],
os.api('admin/announcements/list').then(announcementResponse => {
announcements = announcementResponse;
});
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.announcements,
icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: this.$ts.add,
handler: this.add,
}],
},
announcements: [],
}
},
function add() {
announcements.unshift({
id: null,
title: '',
text: '',
imageUrl: null
});
}
created() {
os.api('admin/announcements/list').then(announcements => {
this.announcements = announcements;
function remove(announcement) {
os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: announcement.title }),
}).then(({ canceled }) => {
if (canceled) return;
announcements = announcements.filter(x => x !== announcement);
os.api('admin/announcements/delete', announcement);
});
}
function save(announcement) {
if (announcement.id == null) {
os.api('admin/announcements/create', announcement).then(() => {
os.alert({
type: 'success',
text: i18n.ts.saved
});
}).catch(err => {
os.alert({
type: 'error',
text: err
});
});
},
methods: {
add() {
this.announcements.unshift({
id: null,
title: '',
text: '',
imageUrl: null
} else {
os.api('admin/announcements/update', announcement).then(() => {
os.alert({
type: 'success',
text: i18n.ts.saved
});
},
remove(announcement) {
os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: announcement.title }),
}).then(({ canceled }) => {
if (canceled) return;
this.announcements = this.announcements.filter(x => x != announcement);
os.api('admin/announcements/delete', announcement);
}).catch(err => {
os.alert({
type: 'error',
text: err
});
},
});
}
}
save(announcement) {
if (announcement.id == null) {
os.api('admin/announcements/create', announcement).then(() => {
os.alert({
type: 'success',
text: this.$ts.saved
});
}).catch(e => {
os.alert({
type: 'error',
text: e
});
});
} else {
os.api('admin/announcements/update', announcement).then(() => {
os.alert({
type: 'success',
text: this.$ts.saved
});
}).catch(e => {
os.alert({
type: 'error',
text: e
});
});
}
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.announcements,
icon: 'fas fa-broadcast-tower',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.add,
handler: add,
}],
}
});
</script>

View File

@@ -43,8 +43,8 @@
</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import FormRadios from '@/components/form/radios.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
@@ -54,64 +54,39 @@ import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormRadios,
FormInput,
FormButton,
FormSuspense,
FormSlot,
MkCaptcha: defineAsyncComponent(() => import('@/components/captcha.vue')),
},
const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue'));
emits: ['info'],
let provider = $ref(null);
let hcaptchaSiteKey: string | null = $ref(null);
let hcaptchaSecretKey: string | null = $ref(null);
let recaptchaSiteKey: string | null = $ref(null);
let recaptchaSecretKey: string | null = $ref(null);
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.botProtection,
icon: 'fas fa-shield-alt'
},
provider: null,
enableHcaptcha: false,
hcaptchaSiteKey: null,
hcaptchaSecretKey: null,
enableRecaptcha: false,
recaptchaSiteKey: null,
recaptchaSecretKey: null,
}
},
const enableHcaptcha = $computed(() => provider === 'hcaptcha');
const enableRecaptcha = $computed(() => provider === 'recaptcha');
methods: {
async init() {
const meta = await os.api('admin/meta');
this.enableHcaptcha = meta.enableHcaptcha;
this.hcaptchaSiteKey = meta.hcaptchaSiteKey;
this.hcaptchaSecretKey = meta.hcaptchaSecretKey;
this.enableRecaptcha = meta.enableRecaptcha;
this.recaptchaSiteKey = meta.recaptchaSiteKey;
this.recaptchaSecretKey = meta.recaptchaSecretKey;
async function init() {
const meta = await os.api('admin/meta');
enableHcaptcha = meta.enableHcaptcha;
hcaptchaSiteKey = meta.hcaptchaSiteKey;
hcaptchaSecretKey = meta.hcaptchaSecretKey;
enableRecaptcha = meta.enableRecaptcha;
recaptchaSiteKey = meta.recaptchaSiteKey;
recaptchaSecretKey = meta.recaptchaSecretKey;
this.provider = this.enableHcaptcha ? 'hcaptcha' : this.enableRecaptcha ? 'recaptcha' : null;
provider = enableHcaptcha ? 'hcaptcha' : enableRecaptcha ? 'recaptcha' : null;
}
this.$watch(() => this.provider, () => {
this.enableHcaptcha = this.provider === 'hcaptcha';
this.enableRecaptcha = this.provider === 'recaptcha';
});
},
save() {
os.apiWithDialog('admin/update-meta', {
enableHcaptcha: this.enableHcaptcha,
hcaptchaSiteKey: this.hcaptchaSiteKey,
hcaptchaSecretKey: this.hcaptchaSecretKey,
enableRecaptcha: this.enableRecaptcha,
recaptchaSiteKey: this.recaptchaSiteKey,
recaptchaSecretKey: this.recaptchaSecretKey,
}).then(() => {
fetchInstance();
});
}
}
});
function save() {
os.apiWithDialog('admin/update-meta', {
enableHcaptcha,
hcaptchaSiteKey,
hcaptchaSecretKey,
enableRecaptcha,
recaptchaSiteKey,
recaptchaSecretKey,
}).then(() => {
fetchInstance();
});
}
</script>

View File

@@ -9,36 +9,23 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import bytes from '@/filters/bytes';
import number from '@/filters/number';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSuspense,
MkKeyValue,
},
const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
emits: ['info'],
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.database,
icon: 'fas fa-database',
bg: 'var(--bg)',
},
databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)),
}
},
methods: {
bytes, number,
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.database,
icon: 'fas fa-database',
bg: 'var(--bg)',
}
});
</script>

View File

@@ -3,37 +3,37 @@
<FormSuspense :p="init">
<div class="_formRoot">
<FormSwitch v-model="enableEmail" class="_formBlock">
<template #label>{{ $ts.enableEmail }}</template>
<template #caption>{{ $ts.emailConfigInfo }}</template>
<template #label>{{ i18n.ts.enableEmail }}</template>
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
</FormSwitch>
<template v-if="enableEmail">
<FormInput v-model="email" type="email" class="_formBlock">
<template #label>{{ $ts.emailAddress }}</template>
<template #label>{{ i18n.ts.emailAddress }}</template>
</FormInput>
<FormSection>
<template #label>{{ $ts.smtpConfig }}</template>
<template #label>{{ i18n.ts.smtpConfig }}</template>
<FormSplit :min-width="280">
<FormInput v-model="smtpHost" class="_formBlock">
<template #label>{{ $ts.smtpHost }}</template>
<template #label>{{ i18n.ts.smtpHost }}</template>
</FormInput>
<FormInput v-model="smtpPort" type="number" class="_formBlock">
<template #label>{{ $ts.smtpPort }}</template>
<template #label>{{ i18n.ts.smtpPort }}</template>
</FormInput>
</FormSplit>
<FormSplit :min-width="280">
<FormInput v-model="smtpUser" class="_formBlock">
<template #label>{{ $ts.smtpUser }}</template>
<template #label>{{ i18n.ts.smtpUser }}</template>
</FormInput>
<FormInput v-model="smtpPass" type="password" class="_formBlock">
<template #label>{{ $ts.smtpPass }}</template>
<template #label>{{ i18n.ts.smtpPass }}</template>
</FormInput>
</FormSplit>
<FormInfo class="_formBlock">{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
<FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
<FormSwitch v-model="smtpSecure" class="_formBlock">
<template #label>{{ $ts.smtpSecure }}</template>
<template #caption>{{ $ts.smtpSecureInfo }}</template>
<template #label>{{ i18n.ts.smtpSecure }}</template>
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
</FormSwitch>
</FormSection>
</template>
@@ -42,8 +42,8 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormInfo from '@/components/ui/info.vue';
@@ -52,86 +52,71 @@ import FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { fetchInstance, instance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormSplit,
FormSection,
FormInfo,
FormSuspense,
},
let enableEmail: boolean = $ref(false);
let email: any = $ref(null);
let smtpSecure: boolean = $ref(false);
let smtpHost: string = $ref('');
let smtpPort: number = $ref(0);
let smtpUser: string = $ref('');
let smtpPass: string = $ref('');
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
enableEmail = meta.enableEmail;
email = meta.email;
smtpSecure = meta.smtpSecure;
smtpHost = meta.smtpHost;
smtpPort = meta.smtpPort;
smtpUser = meta.smtpUser;
smtpPass = meta.smtpPass;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.emailServer,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
text: this.$ts.testEmail,
handler: this.testEmail,
}, {
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
},
enableEmail: false,
email: null,
smtpSecure: false,
smtpHost: '',
smtpPort: 0,
smtpUser: '',
smtpPass: '',
}
},
async function testEmail() {
const { canceled, result: destination } = await os.inputText({
title: i18n.ts.destination,
type: 'email',
placeholder: instance.maintainerEmail
});
if (canceled) return;
os.apiWithDialog('admin/send-email', {
to: destination,
subject: 'Test email',
text: 'Yo'
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.enableEmail = meta.enableEmail;
this.email = meta.email;
this.smtpSecure = meta.smtpSecure;
this.smtpHost = meta.smtpHost;
this.smtpPort = meta.smtpPort;
this.smtpUser = meta.smtpUser;
this.smtpPass = meta.smtpPass;
},
function save() {
os.apiWithDialog('admin/update-meta', {
enableEmail,
email,
smtpSecure,
smtpHost,
smtpPort,
smtpUser,
smtpPass,
}).then(() => {
fetchInstance();
});
}
async testEmail() {
const { canceled, result: destination } = await os.inputText({
title: this.$ts.destination,
type: 'email',
placeholder: this.$instance.maintainerEmail
});
if (canceled) return;
os.apiWithDialog('admin/send-email', {
to: destination,
subject: 'Test email',
text: 'Yo'
});
},
save() {
os.apiWithDialog('admin/update-meta', {
enableEmail: this.enableEmail,
email: this.email,
smtpSecure: this.smtpSecure,
smtpHost: this.smtpHost,
smtpPort: this.smtpPort,
smtpUser: this.smtpUser,
smtpPass: this.smtpPass,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.emailServer,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
text: i18n.ts.testEmail,
handler: testEmail,
}, {
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
});
</script>

View File

@@ -27,85 +27,71 @@
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import * as os from '@/os';
import { unique } from '@/scripts/array';
import { i18n } from '@/i18n';
import { emojiCategories } from '@/instance';
export default defineComponent({
components: {
XModalWindow,
MkButton,
MkInput,
},
const props = defineProps<{
emoji: any,
}>();
props: {
emoji: {
required: true,
let dialog = $ref(null);
let name: string = $ref(props.emoji.name);
let category: string = $ref(props.emoji.category);
let aliases: string = $ref(props.emoji.aliases.join(' '));
let categories: string[] = $ref(emojiCategories);
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean, updated?: any }): void,
(ev: 'closed'): void
}>();
function ok() {
update();
}
async function update() {
await os.apiWithDialog('admin/emoji/update', {
id: props.emoji.id,
name,
category,
aliases: aliases.split(' '),
});
emit('done', {
updated: {
id: props.emoji.id,
name,
category,
aliases: aliases.split(' '),
}
},
});
emits: ['done', 'closed'],
dialog.close();
}
data() {
return {
name: this.emoji.name,
category: this.emoji.category,
aliases: this.emoji.aliases?.join(' '),
categories: [],
}
},
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: name }),
});
if (canceled) return;
created() {
os.api('meta', { detail: false }).then(({ emojis }) => {
this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
os.api('admin/emoji/delete', {
id: props.emoji.id
}).then(() => {
emit('done', {
deleted: true
});
},
methods: {
ok() {
this.update();
},
async update() {
await os.apiWithDialog('admin/emoji/update', {
id: this.emoji.id,
name: this.name,
category: this.category,
aliases: this.aliases.split(' '),
});
this.$emit('done', {
updated: {
name: this.name,
category: this.category,
aliases: this.aliases.split(' '),
}
});
this.$refs.dialog.close();
},
async del() {
const { canceled } = await os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.emoji.name }),
});
if (canceled) return;
os.api('admin/emoji/delete', {
id: this.emoji.id
}).then(() => {
this.$emit('done', {
deleted: true
});
this.$refs.dialog.close();
});
},
}
});
dialog.close();
});
}
</script>
<style lang="scss" scoped>

View File

@@ -63,7 +63,7 @@
</template>
<script lang="ts" setup>
import { computed, defineComponent, ref, toRef } from 'vue';
import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkPagination from '@/components/ui/pagination.vue';
@@ -130,17 +130,17 @@ const add = async (ev: MouseEvent) => {
};
const edit = (emoji) => {
os.popup(import('./emoji-edit-dialog.vue'), {
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
emoji: emoji
}, {
done: result => {
if (result.updated) {
emojisPaginationComponent.value.replaceItem(item => item.id === emoji.id, {
...emoji,
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
...oldEmoji,
...result.updated
});
}));
} else if (result.deleted) {
emojisPaginationComponent.value.removeItem(item => item.id === emoji.id);
emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
}
},
}, 'closed');
@@ -175,10 +175,10 @@ const menu = (ev: MouseEvent) => {
type: 'info',
text: i18n.ts.exportRequested,
});
}).catch((e) => {
}).catch((err) => {
os.alert({
type: 'error',
text: e.message,
text: err.message,
});
});
}
@@ -195,10 +195,10 @@ const menu = (ev: MouseEvent) => {
type: 'info',
text: i18n.ts.importRequested,
});
}).catch((e) => {
}).catch((err) => {
os.alert({
type: 'error',
text: e.message,
text: err.message,
});
});
}

View File

@@ -34,74 +34,52 @@
</XModalWindow>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/form/switch.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
import bytes from '@/filters/bytes';
import * as os from '@/os';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
MkSwitch,
XModalWindow,
MkDriveFileThumbnail,
},
let file: any = $ref(null);
let info: any = $ref(null);
let isSensitive: boolean = $ref(false);
props: {
fileId: {
required: true,
}
},
const props = defineProps<{
fileId: string,
}>();
emits: ['closed'],
async function fetch() {
file = await os.api('drive/files/show', { fileId: props.fileId });
info = await os.api('admin/drive/show-file', { fileId: props.fileId });
isSensitive = file.isSensitive;
}
data() {
return {
file: null,
info: null,
isSensitive: false,
};
},
fetch();
created() {
this.fetch();
},
function showUser() {
os.pageWindow(`/user-info/${file.userId}`);
}
methods: {
async fetch() {
this.file = await os.api('drive/files/show', { fileId: this.fileId });
this.info = await os.api('admin/drive/show-file', { fileId: this.fileId });
this.isSensitive = this.file.isSensitive;
},
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: file.name }),
});
if (canceled) return;
showUser() {
os.pageWindow(`/user-info/${this.file.userId}`);
},
os.apiWithDialog('drive/files/delete', {
fileId: file.id
});
}
async del() {
const { canceled } = await os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.file.name }),
});
if (canceled) return;
os.apiWithDialog('drive/files/delete', {
fileId: this.file.id
});
},
async toggleIsSensitive(v) {
await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v });
this.isSensitive = v;
},
bytes
}
});
async function toggleIsSensitive(v) {
await os.api('drive/files/update', { fileId: props.fileId, isSensitive: v });
isSensitive = v;
}
</script>
<style lang="scss" scoped>

View File

@@ -55,7 +55,7 @@
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, defineAsyncComponent } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
@@ -93,7 +93,7 @@ function clear() {
}
function show(file) {
os.popup(import('./file-dialog.vue'), {
os.popup(defineAsyncComponent(() => import('./file-dialog.vue')), {
fileId: file.id
}, {}, 'closed');
}

View File

@@ -1,6 +1,6 @@
<template>
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
<div v-if="!narrow || page == null" class="nav">
<div v-if="!narrow || initialPage == null" class="nav">
<MkHeader :info="header"></MkHeader>
<MkSpacer :content-max="700" :margin-min="16">
@@ -12,21 +12,21 @@
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
<MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
</div>
</MkSpacer>
</div>
<div class="main">
<div v-if="!(narrow && initialPage == null)" class="main">
<MkStickyContainer>
<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
<component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/>
<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/>
</MkStickyContainer>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineAsyncComponent, defineComponent, isRef, nextTick, onMounted, reactive, ref, watch } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
import { i18n } from '@/i18n';
import MkSuperMenu from '@/components/ui/super-menu.vue';
import MkInfo from '@/components/ui/info.vue';
@@ -35,292 +35,277 @@ import { instance } from '@/instance';
import * as symbols from '@/symbols';
import * as os from '@/os';
import { lookupUser } from '@/scripts/lookup-user';
import { MisskeyNavigator } from '@/scripts/navigate';
export default defineComponent({
components: {
MkSuperMenu,
MkInfo,
},
const isEmpty = (x: string | null) => x == null || x === '';
provide: {
shouldOmitHeaderTitle: false,
},
const nav = new MisskeyNavigator();
props: {
initialPage: {
type: String,
required: false
const indexInfo = {
title: i18n.ts.controlPanel,
icon: 'fas fa-cog',
bg: 'var(--bg)',
hideHeader: true,
};
const props = defineProps<{
initialPage?: string,
}>();
provide('shouldOmitHeaderTitle', false);
let INFO = $ref(indexInfo);
let childInfo = $ref(null);
let page = $ref(props.initialPage);
let narrow = $ref(false);
let view = $ref(null);
let el = $ref(null);
let pageProps = $ref({});
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
let noBotProtection = !instance.enableHcaptcha && !instance.enableRecaptcha;
const NARROW_THRESHOLD = 600;
const ro = new ResizeObserver((entries, observer) => {
if (entries.length === 0) return;
narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
});
const menuDef = $computed(() => [{
title: i18n.ts.quickAction,
items: [{
type: 'button',
icon: 'fas fa-search',
text: i18n.ts.lookup,
action: lookup,
}, ...(instance.disableRegistration ? [{
type: 'button',
icon: 'fas fa-user',
text: i18n.ts.invite,
action: invite,
}] : [])],
}, {
title: i18n.ts.administration,
items: [{
icon: 'fas fa-tachometer-alt',
text: i18n.ts.dashboard,
to: '/admin/overview',
active: props.initialPage === 'overview',
}, {
icon: 'fas fa-users',
text: i18n.ts.users,
to: '/admin/users',
active: props.initialPage === 'users',
}, {
icon: 'fas fa-laugh',
text: i18n.ts.customEmojis,
to: '/admin/emojis',
active: props.initialPage === 'emojis',
}, {
icon: 'fas fa-globe',
text: i18n.ts.federation,
to: '/admin/federation',
active: props.initialPage === 'federation',
}, {
icon: 'fas fa-clipboard-list',
text: i18n.ts.jobQueue,
to: '/admin/queue',
active: props.initialPage === 'queue',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.files,
to: '/admin/files',
active: props.initialPage === 'files',
}, {
icon: 'fas fa-broadcast-tower',
text: i18n.ts.announcements,
to: '/admin/announcements',
active: props.initialPage === 'announcements',
}, {
icon: 'fas fa-audio-description',
text: i18n.ts.ads,
to: '/admin/ads',
active: props.initialPage === 'ads',
}, {
icon: 'fas fa-exclamation-circle',
text: i18n.ts.abuseReports,
to: '/admin/abuses',
active: props.initialPage === 'abuses',
}],
}, {
title: i18n.ts.settings,
items: [{
icon: 'fas fa-cog',
text: i18n.ts.general,
to: '/admin/settings',
active: props.initialPage === 'settings',
}, {
icon: 'fas fa-envelope',
text: i18n.ts.emailServer,
to: '/admin/email-settings',
active: props.initialPage === 'email-settings',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.objectStorage,
to: '/admin/object-storage',
active: props.initialPage === 'object-storage',
}, {
icon: 'fas fa-lock',
text: i18n.ts.security,
to: '/admin/security',
active: props.initialPage === 'security',
}, {
icon: 'fas fa-globe',
text: i18n.ts.relays,
to: '/admin/relays',
active: props.initialPage === 'relays',
}, {
icon: 'fas fa-share-alt',
text: i18n.ts.integration,
to: '/admin/integrations',
active: props.initialPage === 'integrations',
}, {
icon: 'fas fa-ban',
text: i18n.ts.instanceBlocking,
to: '/admin/instance-block',
active: props.initialPage === 'instance-block',
}, {
icon: 'fas fa-ghost',
text: i18n.ts.proxyAccount,
to: '/admin/proxy-account',
active: props.initialPage === 'proxy-account',
}, {
icon: 'fas fa-cogs',
text: i18n.ts.other,
to: '/admin/other-settings',
active: props.initialPage === 'other-settings',
}],
}, {
title: i18n.ts.info,
items: [{
icon: 'fas fa-database',
text: i18n.ts.database,
to: '/admin/database',
active: props.initialPage === 'database',
}],
}]);
const component = $computed(() => {
if (props.initialPage == null) return null;
switch (props.initialPage) {
case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
case 'users': return defineAsyncComponent(() => import('./users.vue'));
case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
case 'files': return defineAsyncComponent(() => import('./files.vue'));
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
case 'database': return defineAsyncComponent(() => import('./database.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue'));
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
}
});
watch(component, () => {
pageProps = {};
nextTick(() => {
scroll(el, { top: 0 });
});
}, { immediate: true });
watch(() => props.initialPage, () => {
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
} else {
if (props.initialPage == null) {
INFO = indexInfo;
}
},
}
});
setup(props, context) {
const indexInfo = {
title: i18n.ts.controlPanel,
icon: 'fas fa-cog',
bg: 'var(--bg)',
hideHeader: true,
};
const INFO = ref(indexInfo);
const childInfo = ref(null);
const page = ref(props.initialPage);
const narrow = ref(false);
const view = ref(null);
const el = ref(null);
const pageChanged = (page) => {
if (page == null) return;
const viewInfo = page[symbols.PAGE_INFO];
if (isRef(viewInfo)) {
watch(viewInfo, () => {
childInfo.value = viewInfo.value;
}, { immediate: true });
} else {
childInfo.value = viewInfo;
}
};
const pageProps = ref({});
watch(narrow, () => {
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
}
});
const isEmpty = (x: any) => x == null || x == '';
onMounted(() => {
ro.observe(el);
const noMaintainerInformation = ref(false);
const noBotProtection = ref(false);
narrow = el.offsetWidth < NARROW_THRESHOLD;
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
}
});
os.api('meta', { detail: true }).then(meta => {
// TODO: 設定が完了しても残ったままになるので、ストリーミングでmeta更新イベントを受け取ってよしなに更新する
noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail);
noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha;
onUnmounted(() => {
ro.disconnect();
});
const pageChanged = (page) => {
if (page == null) {
childInfo = null;
} else {
childInfo = page[symbols.PAGE_INFO];
}
};
const invite = () => {
os.api('admin/invite').then(x => {
os.alert({
type: 'info',
text: x.code
});
const menuDef = computed(() => [{
title: i18n.ts.quickAction,
items: [{
type: 'button',
icon: 'fas fa-search',
text: i18n.ts.lookup,
action: lookup,
}, ...(instance.disableRegistration ? [{
type: 'button',
icon: 'fas fa-user',
text: i18n.ts.invite,
action: invite,
}] : [])],
}, {
title: i18n.ts.administration,
items: [{
icon: 'fas fa-tachometer-alt',
text: i18n.ts.dashboard,
to: '/admin/overview',
active: page.value === 'overview',
}, {
icon: 'fas fa-users',
text: i18n.ts.users,
to: '/admin/users',
active: page.value === 'users',
}, {
icon: 'fas fa-laugh',
text: i18n.ts.customEmojis,
to: '/admin/emojis',
active: page.value === 'emojis',
}, {
icon: 'fas fa-globe',
text: i18n.ts.federation,
to: '/admin/federation',
active: page.value === 'federation',
}, {
icon: 'fas fa-clipboard-list',
text: i18n.ts.jobQueue,
to: '/admin/queue',
active: page.value === 'queue',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.files,
to: '/admin/files',
active: page.value === 'files',
}, {
icon: 'fas fa-broadcast-tower',
text: i18n.ts.announcements,
to: '/admin/announcements',
active: page.value === 'announcements',
}, {
icon: 'fas fa-audio-description',
text: i18n.ts.ads,
to: '/admin/ads',
active: page.value === 'ads',
}, {
icon: 'fas fa-exclamation-circle',
text: i18n.ts.abuseReports,
to: '/admin/abuses',
active: page.value === 'abuses',
}],
}, {
title: i18n.ts.settings,
items: [{
icon: 'fas fa-cog',
text: i18n.ts.general,
to: '/admin/settings',
active: page.value === 'settings',
}, {
icon: 'fas fa-envelope',
text: i18n.ts.emailServer,
to: '/admin/email-settings',
active: page.value === 'email-settings',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.objectStorage,
to: '/admin/object-storage',
active: page.value === 'object-storage',
}, {
icon: 'fas fa-lock',
text: i18n.ts.security,
to: '/admin/security',
active: page.value === 'security',
}, {
icon: 'fas fa-globe',
text: i18n.ts.relays,
to: '/admin/relays',
active: page.value === 'relays',
}, {
icon: 'fas fa-share-alt',
text: i18n.ts.integration,
to: '/admin/integrations',
active: page.value === 'integrations',
}, {
icon: 'fas fa-ban',
text: i18n.ts.instanceBlocking,
to: '/admin/instance-block',
active: page.value === 'instance-block',
}, {
icon: 'fas fa-ghost',
text: i18n.ts.proxyAccount,
to: '/admin/proxy-account',
active: page.value === 'proxy-account',
}, {
icon: 'fas fa-cogs',
text: i18n.ts.other,
to: '/admin/other-settings',
active: page.value === 'other-settings',
}],
}, {
title: i18n.ts.info,
items: [{
icon: 'fas fa-database',
text: i18n.ts.database,
to: '/admin/database',
active: page.value === 'database',
}],
}]);
const component = computed(() => {
if (page.value == null) return null;
switch (page.value) {
case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
case 'users': return defineAsyncComponent(() => import('./users.vue'));
case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
case 'files': return defineAsyncComponent(() => import('./files.vue'));
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
case 'database': return defineAsyncComponent(() => import('./database.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue'));
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
}
}).catch(err => {
os.alert({
type: 'error',
text: err,
});
});
};
watch(component, () => {
pageProps.value = {};
const lookup = (ev) => {
os.popupMenu([{
text: i18n.ts.user,
icon: 'fas fa-user',
action: () => {
lookupUser();
}
}, {
text: i18n.ts.note,
icon: 'fas fa-pencil-alt',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.file,
icon: 'fas fa-cloud',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.instance,
icon: 'fas fa-globe',
action: () => {
alert('TODO');
}
}], ev.currentTarget ?? ev.target);
};
nextTick(() => {
scroll(el.value, { top: 0 });
});
}, { immediate: true });
watch(() => props.initialPage, () => {
if (props.initialPage == null && !narrow.value) {
page.value = 'overview';
} else {
page.value = props.initialPage;
if (props.initialPage == null) {
INFO.value = indexInfo;
}
}
});
onMounted(() => {
narrow.value = el.value.offsetWidth < 800;
if (!narrow.value) {
page.value = 'overview';
}
});
const invite = () => {
os.api('admin/invite').then(x => {
os.alert({
type: 'info',
text: x.code
});
}).catch(e => {
os.alert({
type: 'error',
text: e
});
});
};
const lookup = (ev) => {
os.popupMenu([{
text: i18n.ts.user,
icon: 'fas fa-user',
action: () => {
lookupUser();
}
}, {
text: i18n.ts.note,
icon: 'fas fa-pencil-alt',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.file,
icon: 'fas fa-cloud',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.instance,
icon: 'fas fa-globe',
action: () => {
alert('TODO');
}
}], ev.currentTarget ?? ev.target);
};
return {
[symbols.PAGE_INFO]: INFO,
menuDef,
header: {
title: i18n.ts.controlPanel,
},
noMaintainerInformation,
noBotProtection,
page,
narrow,
view,
el,
pageChanged,
childInfo,
pageProps,
component,
invite,
lookup,
};
},
defineExpose({
[symbols.PAGE_INFO]: INFO,
header: {
title: i18n.ts.controlPanel,
}
});
</script>

View File

@@ -2,57 +2,45 @@
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<FormTextarea v-model="blockedHosts" class="_formBlock">
<span>{{ $ts.blockedInstances }}</span>
<template #caption>{{ $ts.blockedInstancesDescription }}</template>
<span>{{ i18n.ts.blockedInstances }}</span>
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</FormTextarea>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</FormSuspense>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormButton from '@/components/ui/button.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormButton,
FormTextarea,
FormSuspense,
},
let blockedHosts: string = $ref('');
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
blockedHosts = meta.blockedHosts.join('\n');
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.instanceBlocking,
icon: 'fas fa-ban',
bg: 'var(--bg)',
},
blockedHosts: '',
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
blockedHosts: blockedHosts.split('\n') || [],
}).then(() => {
fetchInstance();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.blockedHosts = meta.blockedHosts.join('\n');
},
save() {
os.apiWithDialog('admin/update-meta', {
blockedHosts: this.blockedHosts.split('\n') || [],
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.instanceBlocking,
icon: 'fas fa-ban',
bg: 'var(--bg)',
}
});
</script>

View File

@@ -24,57 +24,36 @@
</FormSuspense>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
import FormInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormInfo,
FormButton,
FormSuspense,
},
let uri: string = $ref('');
let enableDiscordIntegration: boolean = $ref(false);
let discordClientId: string | null = $ref(null);
let discordClientSecret: string | null = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableDiscordIntegration = meta.enableDiscordIntegration;
discordClientId = meta.discordClientId;
discordClientSecret = meta.discordClientSecret;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: 'Discord',
icon: 'fab fa-discord'
},
enableDiscordIntegration: false,
discordClientId: null,
discordClientSecret: null,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableDiscordIntegration = meta.enableDiscordIntegration;
this.discordClientId = meta.discordClientId;
this.discordClientSecret = meta.discordClientSecret;
},
save() {
os.apiWithDialog('admin/update-meta', {
enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret,
}).then(() => {
fetchInstance();
});
}
}
});
function save() {
os.apiWithDialog('admin/update-meta', {
enableDiscordIntegration,
discordClientId,
discordClientSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View File

@@ -24,57 +24,36 @@
</FormSuspense>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
import FormInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormInfo,
FormButton,
FormSuspense,
},
let uri: string = $ref('');
let enableGithubIntegration: boolean = $ref(false);
let githubClientId: string | null = $ref(null);
let githubClientSecret: string | null = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableGithubIntegration = meta.enableGithubIntegration;
githubClientId = meta.githubClientId;
githubClientSecret = meta.githubClientSecret;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: 'GitHub',
icon: 'fab fa-github'
},
enableGithubIntegration: false,
githubClientId: null,
githubClientSecret: null,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableGithubIntegration = meta.enableGithubIntegration;
this.githubClientId = meta.githubClientId;
this.githubClientSecret = meta.githubClientSecret;
},
save() {
os.apiWithDialog('admin/update-meta', {
enableGithubIntegration: this.enableGithubIntegration,
githubClientId: this.githubClientId,
githubClientSecret: this.githubClientSecret,
}).then(() => {
fetchInstance();
});
}
}
});
function save() {
os.apiWithDialog('admin/update-meta', {
enableGithubIntegration,
githubClientId,
githubClientSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View File

@@ -24,7 +24,7 @@
</FormSuspense>
</template>
<script lang="ts">
<script lang="ts" setup>
import { defineComponent } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
@@ -32,49 +32,28 @@ import FormButton from '@/components/ui/button.vue';
import FormInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormInfo,
FormButton,
FormSuspense,
},
let uri: string = $ref('');
let enableTwitterIntegration: boolean = $ref(false);
let twitterConsumerKey: string | null = $ref(null);
let twitterConsumerSecret: string | null = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableTwitterIntegration = meta.enableTwitterIntegration;
twitterConsumerKey = meta.twitterConsumerKey;
twitterConsumerSecret = meta.twitterConsumerSecret;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: 'Twitter',
icon: 'fab fa-twitter'
},
enableTwitterIntegration: false,
twitterConsumerKey: null,
twitterConsumerSecret: null,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableTwitterIntegration = meta.enableTwitterIntegration;
this.twitterConsumerKey = meta.twitterConsumerKey;
this.twitterConsumerSecret = meta.twitterConsumerSecret;
},
save() {
os.apiWithDialog('admin/update-meta', {
enableTwitterIntegration: this.enableTwitterIntegration,
twitterConsumerKey: this.twitterConsumerKey,
twitterConsumerSecret: this.twitterConsumerSecret,
}).then(() => {
fetchInstance();
});
}
}
});
function save() {
os.apiWithDialog('admin/update-meta', {
enableTwitterIntegration,
twitterConsumerKey,
twitterConsumerSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View File

@@ -4,69 +4,52 @@
<FormFolder class="_formBlock">
<template #icon><i class="fab fa-twitter"></i></template>
<template #label>Twitter</template>
<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
<template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XTwitter/>
</FormFolder>
<FormFolder to="/admin/integrations/github" class="_formBlock">
<FormFolder class="_formBlock">
<template #icon><i class="fab fa-github"></i></template>
<template #label>GitHub</template>
<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
<template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XGithub/>
</FormFolder>
<FormFolder to="/admin/integrations/discord" class="_formBlock">
<FormFolder class="_formBlock">
<template #icon><i class="fab fa-discord"></i></template>
<template #label>Discord</template>
<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
<template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XDiscord/>
</FormFolder>
</FormSuspense>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormFolder from '@/components/form/folder.vue';
import FormSecion from '@/components/form/section.vue';
import FormSuspense from '@/components/form/suspense.vue';
import XTwitter from './integrations.twitter.vue';
import XGithub from './integrations.github.vue';
import XDiscord from './integrations.discord.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormFolder,
FormSecion,
FormSuspense,
XTwitter,
XGithub,
XDiscord,
},
let enableTwitterIntegration: boolean = $ref(false);
let enableGithubIntegration: boolean = $ref(false);
let enableDiscordIntegration: boolean = $ref(false);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
enableTwitterIntegration = meta.enableTwitterIntegration;
enableGithubIntegration = meta.enableGithubIntegration;
enableDiscordIntegration = meta.enableDiscordIntegration;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
},
enableTwitterIntegration: false,
enableGithubIntegration: false,
enableDiscordIntegration: false,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.enableTwitterIntegration = meta.enableTwitterIntegration;
this.enableGithubIntegration = meta.enableGithubIntegration;
this.enableDiscordIntegration = meta.enableDiscordIntegration;
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
}
});
</script>

View File

@@ -2,32 +2,32 @@
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_formRoot">
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ $ts.useObjectStorage }}</FormSwitch>
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
<template v-if="useObjectStorage">
<FormInput v-model="objectStorageBaseUrl" class="_formBlock">
<template #label>{{ $ts.objectStorageBaseUrl }}</template>
<template #caption>{{ $ts.objectStorageBaseUrlDesc }}</template>
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageBucket" class="_formBlock">
<template #label>{{ $ts.objectStorageBucket }}</template>
<template #caption>{{ $ts.objectStorageBucketDesc }}</template>
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
</FormInput>
<FormInput v-model="objectStoragePrefix" class="_formBlock">
<template #label>{{ $ts.objectStoragePrefix }}</template>
<template #caption>{{ $ts.objectStoragePrefixDesc }}</template>
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageEndpoint" class="_formBlock">
<template #label>{{ $ts.objectStorageEndpoint }}</template>
<template #caption>{{ $ts.objectStorageEndpointDesc }}</template>
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageRegion" class="_formBlock">
<template #label>{{ $ts.objectStorageRegion }}</template>
<template #caption>{{ $ts.objectStorageRegionDesc }}</template>
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
</FormInput>
<FormSplit :min-width="280">
@@ -43,17 +43,17 @@
</FormSplit>
<FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
<template #label>{{ $ts.objectStorageUseSSL }}</template>
<template #caption>{{ $ts.objectStorageUseSSLDesc }}</template>
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
<template #label>{{ $ts.objectStorageUseProxy }}</template>
<template #caption>{{ $ts.objectStorageUseProxyDesc }}</template>
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
<template #label>{{ $ts.objectStorageSetPublicRead }}</template>
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
@@ -65,8 +65,8 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormGroup from '@/components/form/group.vue';
@@ -76,84 +76,70 @@ import FormSection from '@/components/form/section.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormGroup,
FormSuspense,
FormSplit,
FormSection,
},
let useObjectStorage: boolean = $ref(false);
let objectStorageBaseUrl: string | null = $ref(null);
let objectStorageBucket: string | null = $ref(null);
let objectStoragePrefix: string | null = $ref(null);
let objectStorageEndpoint: string | null = $ref(null);
let objectStorageRegion: string | null = $ref(null);
let objectStoragePort: number | null = $ref(null);
let objectStorageAccessKey: string | null = $ref(null);
let objectStorageSecretKey: string | null = $ref(null);
let objectStorageUseSSL: boolean = $ref(false);
let objectStorageUseProxy: boolean = $ref(false);
let objectStorageSetPublicRead: boolean = $ref(false);
let objectStorageS3ForcePathStyle: boolean = $ref(true);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
useObjectStorage = meta.useObjectStorage;
objectStorageBaseUrl = meta.objectStorageBaseUrl;
objectStorageBucket = meta.objectStorageBucket;
objectStoragePrefix = meta.objectStoragePrefix;
objectStorageEndpoint = meta.objectStorageEndpoint;
objectStorageRegion = meta.objectStorageRegion;
objectStoragePort = meta.objectStoragePort;
objectStorageAccessKey = meta.objectStorageAccessKey;
objectStorageSecretKey = meta.objectStorageSecretKey;
objectStorageUseSSL = meta.objectStorageUseSSL;
objectStorageUseProxy = meta.objectStorageUseProxy;
objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.objectStorage,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
},
useObjectStorage: false,
objectStorageBaseUrl: null,
objectStorageBucket: null,
objectStoragePrefix: null,
objectStorageEndpoint: null,
objectStorageRegion: null,
objectStoragePort: null,
objectStorageAccessKey: null,
objectStorageSecretKey: null,
objectStorageUseSSL: false,
objectStorageUseProxy: false,
objectStorageSetPublicRead: false,
objectStorageS3ForcePathStyle: true,
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
useObjectStorage,
objectStorageBaseUrl,
objectStorageBucket,
objectStoragePrefix,
objectStorageEndpoint,
objectStorageRegion,
objectStoragePort,
objectStorageAccessKey,
objectStorageSecretKey,
objectStorageUseSSL,
objectStorageUseProxy,
objectStorageSetPublicRead,
objectStorageS3ForcePathStyle,
}).then(() => {
fetchInstance();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.useObjectStorage = meta.useObjectStorage;
this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
this.objectStorageBucket = meta.objectStorageBucket;
this.objectStoragePrefix = meta.objectStoragePrefix;
this.objectStorageEndpoint = meta.objectStorageEndpoint;
this.objectStorageRegion = meta.objectStorageRegion;
this.objectStoragePort = meta.objectStoragePort;
this.objectStorageAccessKey = meta.objectStorageAccessKey;
this.objectStorageSecretKey = meta.objectStorageSecretKey;
this.objectStorageUseSSL = meta.objectStorageUseSSL;
this.objectStorageUseProxy = meta.objectStorageUseProxy;
this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
},
save() {
os.apiWithDialog('admin/update-meta', {
useObjectStorage: this.useObjectStorage,
objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
objectStorageUseSSL: this.objectStorageUseSSL,
objectStorageUseProxy: this.objectStorageUseProxy,
objectStorageSetPublicRead: this.objectStorageSetPublicRead,
objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.objectStorage,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
});
</script>

View File

@@ -6,52 +6,35 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormSection from '@/components/form/section.vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormSection,
FormSuspense,
},
async function init() {
await os.api('admin/meta');
}
emits: ['info'],
function save() {
os.apiWithDialog('admin/update-meta').then(() => {
fetchInstance();
});
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.other,
icon: 'fas fa-cogs',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
},
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
},
save() {
os.apiWithDialog('admin/update-meta', {
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.other,
icon: 'fas fa-cogs',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
});
</script>

View File

@@ -5,20 +5,20 @@
<div class="label">Users</div>
<div class="value _monospace">
{{ number(stats.originalUsersCount) }}
<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="$ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
</div>
</div>
<div class="number _panel">
<div class="label">Notes</div>
<div class="value _monospace">
{{ number(stats.originalNotesCount) }}
<MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="$ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
<MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"><template #before>(</template><template #after>)</template></MkNumberDiff>
</div>
</div>
</div>
<MkContainer :foldable="true" class="charts">
<template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template>
<template #header><i class="fas fa-chart-bar"></i>{{ i18n.ts.charts }}</template>
<div style="padding: 12px;">
<MkInstanceStats :chart-limit="500" :detailed="true"/>
</div>
@@ -38,7 +38,7 @@
<!--<XMetrics/>-->
<MkFolder style="margin: var(--margin)">
<template #header><i class="fas fa-info-circle"></i> {{ $ts.info }}</template>
<template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template>
<div class="cfcdecdf">
<div class="number _panel">
<div class="label">Misskey</div>
@@ -65,103 +65,61 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent, markRaw, version as vueVersion } from 'vue';
<script lang="ts" setup>
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
import MkInstanceStats from '@/components/instance-stats.vue';
import MkButton from '@/components/ui/button.vue';
import MkSelect from '@/components/form/select.vue';
import MkNumberDiff from '@/components/number-diff.vue';
import MkContainer from '@/components/ui/container.vue';
import MkFolder from '@/components/ui/folder.vue';
import MkQueueChart from '@/components/queue-chart.vue';
import { version, url } from '@/config';
import bytes from '@/filters/bytes';
import number from '@/filters/number';
import XMetrics from './metrics.vue';
import * as os from '@/os';
import { stream } from '@/stream';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkNumberDiff,
MkInstanceStats,
MkContainer,
MkFolder,
MkQueueChart,
XMetrics,
},
let stats: any = $ref(null);
let serverInfo: any = $ref(null);
let usersComparedToThePrevDay: any = $ref(null);
let notesComparedToThePrevDay: any = $ref(null);
const queueStatsConnection = markRaw(stream.useChannel('queueStats'));
emits: ['info'],
onMounted(async () => {
os.api('stats', {}).then(statsResponse => {
stats = statsResponse;
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.dashboard,
icon: 'fas fa-tachometer-alt',
bg: 'var(--bg)',
},
version,
vueVersion,
url,
stats: null,
meta: null,
serverInfo: null,
usersComparedToThePrevDay: null,
notesComparedToThePrevDay: null,
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
queueStatsConnection: markRaw(stream.useChannel('queueStats')),
}
},
async mounted() {
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
os.api('stats', {}).then(stats => {
this.stats = stats;
os.api('charts/users', { limit: 2, span: 'day' }).then(chart => {
this.usersComparedToThePrevDay = this.stats.originalUsersCount - chart.local.total[1];
});
os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => {
this.notesComparedToThePrevDay = this.stats.originalNotesCount - chart.local.total[1];
});
os.api('charts/users', { limit: 2, span: 'day' }).then(chart => {
usersComparedToThePrevDay = stats.originalUsersCount - chart.local.total[1];
});
os.api('admin/server-info', {}).then(serverInfo => {
this.serverInfo = serverInfo;
os.api('charts/notes', { limit: 2, span: 'day' }).then(chart => {
notesComparedToThePrevDay = stats.originalNotesCount - chart.local.total[1];
});
});
this.$nextTick(() => {
this.queueStatsConnection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
os.api('admin/server-info').then(serverInfoResponse => {
serverInfo = serverInfoResponse;
});
nextTick(() => {
queueStatsConnection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
},
});
});
beforeUnmount() {
this.queueStatsConnection.dispose();
},
onBeforeUnmount(() => {
queueStatsConnection.dispose();
});
methods: {
async showInstanceInfo(q) {
let instance = q;
if (typeof q === 'string') {
instance = await os.api('federation/show-instance', {
host: q
});
}
// TODO
},
bytes,
number,
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.dashboard,
icon: 'fas fa-tachometer-alt',
bg: 'var(--bg)',
}
});
</script>

View File

@@ -1,19 +1,19 @@
<template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<MkInfo class="_formBlock">{{ $ts.proxyAccountDescription }}</MkInfo>
<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.proxyAccount }}</template>
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
<template #key>{{ i18n.ts.proxyAccount }}</template>
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
</MkKeyValue>
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ $ts.selectAccount }}</FormButton>
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
</FormSuspense>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkKeyValue from '@/components/key-value.vue';
import FormButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue';
@@ -21,53 +21,40 @@ import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkKeyValue,
FormButton,
MkInfo,
FormSuspense,
},
let proxyAccount: any = $ref(null);
let proxyAccountId: any = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
proxyAccountId = meta.proxyAccountId;
if (proxyAccountId) {
proxyAccount = await os.api('users/show', { userId: proxyAccountId });
}
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.proxyAccount,
icon: 'fas fa-ghost',
bg: 'var(--bg)',
},
proxyAccount: null,
proxyAccountId: null,
}
},
function chooseProxyAccount() {
os.selectUser().then(user => {
proxyAccount = user;
proxyAccountId = user.id;
save();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.proxyAccountId = meta.proxyAccountId;
if (this.proxyAccountId) {
this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId });
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
proxyAccountId: proxyAccountId,
}).then(() => {
fetchInstance();
});
}
chooseProxyAccount() {
os.selectUser().then(user => {
this.proxyAccount = user;
this.proxyAccountId = user.id;
this.save();
});
},
save() {
os.apiWithDialog('admin/update-meta', {
proxyAccountId: this.proxyAccountId,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.proxyAccount,
icon: 'fas fa-ghost',
bg: 'var(--bg)',
}
});
</script>

View File

@@ -26,62 +26,40 @@
</div>
</template>
<script lang="ts">
import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue';
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import number from '@/filters/number';
import MkQueueChart from '@/components/queue-chart.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkQueueChart
},
const activeSincePrevTick = ref(0);
const active = ref(0);
const waiting = ref(0);
const delayed = ref(0);
const jobs = ref([]);
props: {
domain: {
type: String,
required: true,
},
connection: {
required: true,
},
},
const props = defineProps<{
domain: string,
connection: any,
}>();
setup(props) {
const activeSincePrevTick = ref(0);
const active = ref(0);
const waiting = ref(0);
const delayed = ref(0);
const jobs = ref([]);
onMounted(() => {
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
jobs.value = result;
});
onMounted(() => {
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
jobs.value = result;
});
const onStats = (stats) => {
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
active.value = stats[props.domain].active;
waiting.value = stats[props.domain].waiting;
delayed.value = stats[props.domain].delayed;
};
const onStats = (stats) => {
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
active.value = stats[props.domain].active;
waiting.value = stats[props.domain].waiting;
delayed.value = stats[props.domain].delayed;
};
props.connection.on('stats', onStats);
props.connection.on('stats', onStats);
onUnmounted(() => {
props.connection.off('stats', onStats);
});
});
return {
jobs,
activeSincePrevTick,
active,
waiting,
delayed,
number,
};
},
onUnmounted(() => {
props.connection.off('stats', onStats);
});
});
</script>

View File

@@ -6,71 +6,60 @@
<XQueue :connection="connection" domain="deliver">
<template #title>Out</template>
</XQueue>
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</MkButton>
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
<script lang="ts" setup>
import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue';
import MkButton from '@/components/ui/button.vue';
import XQueue from './queue.chart.vue';
import * as os from '@/os';
import { stream } from '@/stream';
import * as symbols from '@/symbols';
import * as config from '@/config';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
XQueue,
},
const connection = markRaw(stream.useChannel('queueStats'))
emits: ['info'],
function clear() {
os.confirm({
type: 'warning',
title: i18n.ts.clearQueueConfirmTitle,
text: i18n.ts.clearQueueConfirmText,
}).then(({ canceled }) => {
if (canceled) return;
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.jobQueue,
icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-up-right-from-square',
text: this.$ts.dashboard,
handler: () => {
window.open(config.url + '/queue', '_blank');
},
}],
},
connection: markRaw(stream.useChannel('queueStats')),
}
},
os.apiWithDialog('admin/queue/clear');
});
}
mounted() {
this.$nextTick(() => {
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
onMounted(() => {
nextTick(() => {
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
},
});
})
beforeUnmount() {
this.connection.dispose();
},
onBeforeUnmount(() => {
connection.dispose();
});
methods: {
clear() {
os.confirm({
type: 'warning',
title: this.$ts.clearQueueConfirmTitle,
text: this.$ts.clearQueueConfirmText,
}).then(({ canceled }) => {
if (canceled) return;
os.apiWithDialog('admin/queue/clear', {});
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.jobQueue,
icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-up-right-from-square',
text: i18n.ts.dashboard,
handler: () => {
window.open(config.url + '/queue', '_blank');
},
}],
}
});
</script>

View File

@@ -8,84 +8,71 @@
<i v-else class="fas fa-clock icon requesting"></i>
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
},
let relays: any[] = $ref([]);
emits: ['info'],
async function addRelay() {
const { canceled, result: inbox } = await os.inputText({
title: i18n.ts.addRelay,
type: 'url',
placeholder: i18n.ts.inboxUrl
});
if (canceled) return;
os.api('admin/relays/add', {
inbox
}).then((relay: any) => {
refresh();
}).catch((err: any) => {
os.alert({
type: 'error',
text: err.message || err
});
});
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.relays,
icon: 'fas fa-globe',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: this.$ts.addRelay,
handler: this.addRelay,
}],
},
relays: [],
inbox: '',
}
},
function remove(inbox: string) {
os.api('admin/relays/remove', {
inbox
}).then(() => {
refresh();
}).catch((err: any) => {
os.alert({
type: 'error',
text: err.message || err
});
});
}
created() {
this.refresh();
},
function refresh() {
os.api('admin/relays/list').then((relayList: any) => {
relays = relayList;
});
}
methods: {
async addRelay() {
const { canceled, result: inbox } = await os.inputText({
title: this.$ts.addRelay,
type: 'url',
placeholder: this.$ts.inboxUrl
});
if (canceled) return;
os.api('admin/relays/add', {
inbox
}).then((relay: any) => {
this.refresh();
}).catch((e: any) => {
os.alert({
type: 'error',
text: e.message || e
});
});
},
refresh();
remove(inbox: string) {
os.api('admin/relays/remove', {
inbox
}).then(() => {
this.refresh();
}).catch((e: any) => {
os.alert({
type: 'error',
text: e.message || e
});
});
},
refresh() {
os.api('admin/relays/list').then((relays: any) => {
this.relays = relays;
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.relays,
icon: 'fas fa-globe',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.addRelay,
handler: addRelay,
}],
}
});
</script>

View File

@@ -4,10 +4,10 @@
<div class="_formRoot">
<FormFolder class="_formBlock">
<template #icon><i class="fas fa-shield-alt"></i></template>
<template #label>{{ $ts.botProtection }}</template>
<template #label>{{ i18n.ts.botProtection }}</template>
<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
<template v-else #suffix>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
<XBotProtection/>
</FormFolder>
@@ -21,7 +21,7 @@
<template #label>Summaly Proxy URL</template>
</FormInput>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</div>
</FormFolder>
</div>
@@ -29,8 +29,8 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormFolder from '@/components/form/folder.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInfo from '@/components/ui/info.vue';
@@ -42,49 +42,32 @@ import XBotProtection from './bot-protection.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormFolder,
FormSwitch,
FormInfo,
FormSection,
FormSuspense,
FormButton,
FormInput,
XBotProtection,
},
let summalyProxy: string = $ref('');
let enableHcaptcha: boolean = $ref(false);
let enableRecaptcha: boolean = $ref(false);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
summalyProxy = meta.summalyProxy;
enableHcaptcha = meta.enableHcaptcha;
enableRecaptcha = meta.enableRecaptcha;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.security,
icon: 'fas fa-lock',
bg: 'var(--bg)',
},
summalyProxy: '',
enableHcaptcha: false,
enableRecaptcha: false,
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
summalyProxy,
}).then(() => {
fetchInstance();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.summalyProxy = meta.summalyProxy;
this.enableHcaptcha = meta.enableHcaptcha;
this.enableRecaptcha = meta.enableRecaptcha;
},
save() {
os.apiWithDialog('admin/update-meta', {
summalyProxy: this.summalyProxy,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.security,
icon: 'fas fa-lock',
bg: 'var(--bg)',
}
});
</script>

View File

@@ -3,104 +3,104 @@
<FormSuspense :p="init">
<div class="_formRoot">
<FormInput v-model="name" class="_formBlock">
<template #label>{{ $ts.instanceName }}</template>
<template #label>{{ i18n.ts.instanceName }}</template>
</FormInput>
<FormTextarea v-model="description" class="_formBlock">
<template #label>{{ $ts.instanceDescription }}</template>
<template #label>{{ i18n.ts.instanceDescription }}</template>
</FormTextarea>
<FormInput v-model="tosUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.tosUrl }}</template>
<template #label>{{ i18n.ts.tosUrl }}</template>
</FormInput>
<FormSplit :min-width="300">
<FormInput v-model="maintainerName" class="_formBlock">
<template #label>{{ $ts.maintainerName }}</template>
<template #label>{{ i18n.ts.maintainerName }}</template>
</FormInput>
<FormInput v-model="maintainerEmail" type="email" class="_formBlock">
<template #prefix><i class="fas fa-envelope"></i></template>
<template #label>{{ $ts.maintainerEmail }}</template>
<template #label>{{ i18n.ts.maintainerEmail }}</template>
</FormInput>
</FormSplit>
<FormTextarea v-model="pinnedUsers" class="_formBlock">
<template #label>{{ $ts.pinnedUsers }}</template>
<template #caption>{{ $ts.pinnedUsersDescription }}</template>
<template #label>{{ i18n.ts.pinnedUsers }}</template>
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
</FormTextarea>
<FormSection>
<FormSwitch v-model="enableRegistration" class="_formBlock">
<template #label>{{ $ts.enableRegistration }}</template>
<template #label>{{ i18n.ts.enableRegistration }}</template>
</FormSwitch>
<FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
<template #label>{{ $ts.emailRequiredForSignup }}</template>
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
</FormSwitch>
</FormSection>
<FormSection>
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ $ts.enableLocalTimeline }}</FormSwitch>
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ $ts.enableGlobalTimeline }}</FormSwitch>
<FormInfo class="_formBlock">{{ $ts.disablingTimelinesInfo }}</FormInfo>
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
</FormSection>
<FormSection>
<template #label>{{ $ts.theme }}</template>
<template #label>{{ i18n.ts.theme }}</template>
<FormInput v-model="iconUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.iconUrl }}</template>
<template #label>{{ i18n.ts.iconUrl }}</template>
</FormInput>
<FormInput v-model="bannerUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.bannerUrl }}</template>
<template #label>{{ i18n.ts.bannerUrl }}</template>
</FormInput>
<FormInput v-model="backgroundImageUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.backgroundImageUrl }}</template>
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
</FormInput>
<FormInput v-model="themeColor" class="_formBlock">
<template #prefix><i class="fas fa-palette"></i></template>
<template #label>{{ $ts.themeColor }}</template>
<template #label>{{ i18n.ts.themeColor }}</template>
<template #caption>#RRGGBB</template>
</FormInput>
<FormTextarea v-model="defaultLightTheme" class="_formBlock">
<template #label>{{ $ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ $ts.instanceDefaultThemeDescription }}</template>
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</FormTextarea>
<FormTextarea v-model="defaultDarkTheme" class="_formBlock">
<template #label>{{ $ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ $ts.instanceDefaultThemeDescription }}</template>
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</FormTextarea>
</FormSection>
<FormSection>
<template #label>{{ $ts.files }}</template>
<template #label>{{ i18n.ts.files }}</template>
<FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
<template #label>{{ $ts.cacheRemoteFiles }}</template>
<template #caption>{{ $ts.cacheRemoteFilesDescription }}</template>
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
</FormSwitch>
<FormSplit :min-width="280">
<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
<template #label>{{ $ts.driveCapacityPerLocalAccount }}</template>
<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ $ts.inMb }}</template>
<template #caption>{{ i18n.ts.inMb }}</template>
</FormInput>
<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
<template #label>{{ $ts.driveCapacityPerRemoteAccount }}</template>
<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ $ts.inMb }}</template>
<template #caption>{{ i18n.ts.inMb }}</template>
</FormInput>
</FormSplit>
</FormSection>
@@ -109,8 +109,8 @@
<template #label>ServiceWorker</template>
<FormSwitch v-model="enableServiceWorker" class="_formBlock">
<template #label>{{ $ts.enableServiceworker }}</template>
<template #caption>{{ $ts.serviceworkerInfo }}</template>
<template #label>{{ i18n.ts.enableServiceworker }}</template>
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
</FormSwitch>
<template v-if="enableServiceWorker">
@@ -142,8 +142,8 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.vue';
@@ -154,119 +154,103 @@ import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormSuspense,
FormTextarea,
FormInfo,
FormSection,
FormSplit,
},
let name: string | null = $ref(null);
let description: string | null = $ref(null);
let tosUrl: string | null = $ref(null);
let maintainerName: string | null = $ref(null);
let maintainerEmail: string | null = $ref(null);
let iconUrl: string | null = $ref(null);
let bannerUrl: string | null = $ref(null);
let backgroundImageUrl: string | null = $ref(null);
let themeColor: any = $ref(null);
let defaultLightTheme: any = $ref(null);
let defaultDarkTheme: any = $ref(null);
let enableLocalTimeline: boolean = $ref(false);
let enableGlobalTimeline: boolean = $ref(false);
let pinnedUsers: string = $ref('');
let cacheRemoteFiles: boolean = $ref(false);
let localDriveCapacityMb: any = $ref(0);
let remoteDriveCapacityMb: any = $ref(0);
let enableRegistration: boolean = $ref(false);
let emailRequiredForSignup: boolean = $ref(false);
let enableServiceWorker: boolean = $ref(false);
let swPublicKey: any = $ref(null);
let swPrivateKey: any = $ref(null);
let deeplAuthKey: string = $ref('');
let deeplIsPro: boolean = $ref(false);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
name = meta.name;
description = meta.description;
tosUrl = meta.tosUrl;
iconUrl = meta.iconUrl;
bannerUrl = meta.bannerUrl;
backgroundImageUrl = meta.backgroundImageUrl;
themeColor = meta.themeColor;
defaultLightTheme = meta.defaultLightTheme;
defaultDarkTheme = meta.defaultDarkTheme;
maintainerName = meta.maintainerName;
maintainerEmail = meta.maintainerEmail;
enableLocalTimeline = !meta.disableLocalTimeline;
enableGlobalTimeline = !meta.disableGlobalTimeline;
pinnedUsers = meta.pinnedUsers.join('\n');
cacheRemoteFiles = meta.cacheRemoteFiles;
localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
enableRegistration = !meta.disableRegistration;
emailRequiredForSignup = meta.emailRequiredForSignup;
enableServiceWorker = meta.enableServiceWorker;
swPublicKey = meta.swPublickey;
swPrivateKey = meta.swPrivateKey;
deeplAuthKey = meta.deeplAuthKey;
deeplIsPro = meta.deeplIsPro;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.general,
icon: 'fas fa-cog',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
},
name: null,
description: null,
tosUrl: null as string | null,
maintainerName: null,
maintainerEmail: null,
iconUrl: null,
bannerUrl: null,
backgroundImageUrl: null,
themeColor: null,
defaultLightTheme: null,
defaultDarkTheme: null,
enableLocalTimeline: false,
enableGlobalTimeline: false,
pinnedUsers: '',
cacheRemoteFiles: false,
localDriveCapacityMb: 0,
remoteDriveCapacityMb: 0,
enableRegistration: false,
emailRequiredForSignup: false,
enableServiceWorker: false,
swPublicKey: null,
swPrivateKey: null,
deeplAuthKey: '',
deeplIsPro: false,
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
name,
description,
tosUrl,
iconUrl,
bannerUrl,
backgroundImageUrl,
themeColor: themeColor === '' ? null : themeColor,
defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme,
defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme,
maintainerName,
maintainerEmail,
disableLocalTimeline: !enableLocalTimeline,
disableGlobalTimeline: !enableGlobalTimeline,
pinnedUsers: pinnedUsers.split('\n'),
cacheRemoteFiles,
localDriveCapacityMb: parseInt(localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(remoteDriveCapacityMb, 10),
disableRegistration: !enableRegistration,
emailRequiredForSignup,
enableServiceWorker,
swPublicKey,
swPrivateKey,
deeplAuthKey,
deeplIsPro,
}).then(() => {
fetchInstance();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.name = meta.name;
this.description = meta.description;
this.tosUrl = meta.tosUrl;
this.iconUrl = meta.iconUrl;
this.bannerUrl = meta.bannerUrl;
this.backgroundImageUrl = meta.backgroundImageUrl;
this.themeColor = meta.themeColor;
this.defaultLightTheme = meta.defaultLightTheme;
this.defaultDarkTheme = meta.defaultDarkTheme;
this.maintainerName = meta.maintainerName;
this.maintainerEmail = meta.maintainerEmail;
this.enableLocalTimeline = !meta.disableLocalTimeline;
this.enableGlobalTimeline = !meta.disableGlobalTimeline;
this.pinnedUsers = meta.pinnedUsers.join('\n');
this.cacheRemoteFiles = meta.cacheRemoteFiles;
this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
this.enableRegistration = !meta.disableRegistration;
this.emailRequiredForSignup = meta.emailRequiredForSignup;
this.enableServiceWorker = meta.enableServiceWorker;
this.swPublicKey = meta.swPublickey;
this.swPrivateKey = meta.swPrivateKey;
this.deeplAuthKey = meta.deeplAuthKey;
this.deeplIsPro = meta.deeplIsPro;
},
save() {
os.apiWithDialog('admin/update-meta', {
name: this.name,
description: this.description,
tosUrl: this.tosUrl,
iconUrl: this.iconUrl,
bannerUrl: this.bannerUrl,
backgroundImageUrl: this.backgroundImageUrl,
themeColor: this.themeColor === '' ? null : this.themeColor,
defaultLightTheme: this.defaultLightTheme === '' ? null : this.defaultLightTheme,
defaultDarkTheme: this.defaultDarkTheme === '' ? null : this.defaultDarkTheme,
maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail,
disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline,
pinnedUsers: this.pinnedUsers.split('\n'),
cacheRemoteFiles: this.cacheRemoteFiles,
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
disableRegistration: !this.enableRegistration,
emailRequiredForSignup: this.emailRequiredForSignup,
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey,
deeplAuthKey: this.deeplAuthKey,
deeplIsPro: this.deeplIsPro,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.general,
icon: 'fas fa-cog',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
});
</script>

View File

@@ -26,8 +26,8 @@
</template>
<script lang="ts" setup>
import { defineExpose, ref } from 'vue';
import * as JSON5 from 'json5';
import { ref } from 'vue';
import JSON5 from 'json5';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';

View File

@@ -111,8 +111,8 @@ export default defineComponent({
}
},
setBannerImage(e) {
selectFile(e.currentTarget ?? e.target, null).then(file => {
setBannerImage(evt) {
selectFile(evt.currentTarget ?? evt.target, null).then(file => {
this.bannerId = file.id;
});
},

Some files were not shown because too many files have changed in this diff Show More