Merge branch 'develop'

This commit is contained in:
2022-06-11 20:37:28 +09:00
544 changed files with 14952 additions and 12562 deletions

View File

@ -1,68 +1,85 @@
module.exports = {
root: true,
env: {
"node": false
'node': false,
},
parser: "vue-eslint-parser",
parser: 'vue-eslint-parser',
parserOptions: {
"parser": "@typescript-eslint/parser",
'parser': '@typescript-eslint/parser',
tsconfigRootDir: __dirname,
//project: ['./tsconfig.json'],
project: ['./tsconfig.json'],
extraFileExtensions: ['.vue'],
},
extends: [
//"../shared/.eslintrc.js",
"plugin:vue/vue3-recommended"
'../shared/.eslintrc.js',
'plugin:vue/vue3-recommended',
],
rules: {
'@typescript-eslint/no-empty-interface': [
'error',
{
'allowSingleExtends': true,
},
],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// data の禁止理由: 抽象的すぎるため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
"id-denylist": ["error", "window", "data", "e"],
'id-denylist': ['error', 'window', 'data', 'e'],
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
"no-shadow": ["warn"],
"vue/attributes-order": ["error", {
"alphabetical": false
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
'alphabetical': false,
}],
"vue/no-use-v-if-with-v-for": ["error", {
"allowUsingIterationVar": false
'vue/no-use-v-if-with-v-for': ['error', {
'allowUsingIterationVar': false,
}],
"vue/no-ref-as-operand": "error",
"vue/no-multi-spaces": ["error", {
"ignoreProperties": false
'vue/no-ref-as-operand': 'error',
'vue/no-multi-spaces': ['error', {
'ignoreProperties': false,
}],
"vue/no-v-html": "error",
"vue/order-in-components": "error",
"vue/html-indent": ["warn", "tab", {
"attribute": 1,
"baseIndent": 0,
"closeBracket": 0,
"alignAttributesVertically": true,
"ignores": []
'vue/no-v-html': 'error',
'vue/order-in-components': 'error',
'vue/html-indent': ['warn', 'tab', {
'attribute': 1,
'baseIndent': 0,
'closeBracket': 0,
'alignAttributesVertically': true,
'ignores': [],
}],
"vue/html-closing-bracket-spacing": ["warn", {
"startTag": "never",
"endTag": "never",
"selfClosingTag": "never"
'vue/html-closing-bracket-spacing': ['warn', {
'startTag': 'never',
'endTag': 'never',
'selfClosingTag': 'never',
}],
"vue/multi-word-component-names": "warn",
"vue/require-v-for-key": "warn",
"vue/no-unused-components": "warn",
"vue/valid-v-for": "warn",
"vue/return-in-computed-property": "warn",
"vue/no-setup-props-destructure": "warn",
"vue/max-attributes-per-line": "off",
"vue/html-self-closing": "off",
"vue/singleline-html-element-content-newline": "off",
'vue/multi-word-component-names': 'warn',
'vue/require-v-for-key': 'warn',
'vue/no-unused-components': 'warn',
'vue/valid-v-for': 'warn',
'vue/return-in-computed-property': 'warn',
'vue/no-setup-props-destructure': 'warn',
'vue/max-attributes-per-line': 'off',
'vue/html-self-closing': 'off',
'vue/singleline-html-element-content-newline': 'off',
},
globals: {
"require": false,
"_DEV_": false,
"_LANGS_": false,
"_VERSION_": false,
"_ENV_": false,
"_PERF_PREFIX_": false,
"_DATA_TRANSFER_DRIVE_FILE_": false,
"_DATA_TRANSFER_DRIVE_FOLDER_": false,
"_DATA_TRANSFER_DECK_COLUMN_": false
}
}
// Node.js
'module': false,
'require': false,
'__dirname': false,
// Vue
'$$': false,
'$ref': false,
'$computed': false,
// Misskey
'_DEV_': false,
'_LANGS_': false,
'_VERSION_': false,
'_ENV_': false,
'_PERF_PREFIX_': false,
'_DATA_TRANSFER_DRIVE_FILE_': false,
'_DATA_TRANSFER_DRIVE_FOLDER_': false,
'_DATA_TRANSFER_DECK_COLUMN_': false,
},
};

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

@ -0,0 +1,7 @@
declare module '@/themes/*.json5' {
import { Theme } from "@/scripts/theme";
const theme: Theme;
export default theme;
}

View File

@ -1,80 +1,52 @@
{
"private": true,
"scripts": {
"watch": "webpack --watch",
"build": "webpack",
"lint": "eslint --quiet 'src/**/*.{ts,vue}'"
"watch": "vite build --watch --mode development",
"build": "vite build",
"lint": "eslint --quiet \"src/**/*.{ts,vue}\""
},
"resolutions": {
"chokidar": "^3.3.1",
"lodash": "^4.17.21"
},
"dependencies": {
"@discordapp/twemoji": "13.1.1",
"@discordapp/twemoji": "14.0.2",
"@fortawesome/fontawesome-free": "6.1.1",
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"@syuilo/aiscript": "0.11.1",
"@types/escape-regexp": "0.0.1",
"@types/glob": "7.2.0",
"@types/gulp": "4.0.9",
"@types/gulp-rename": "2.0.1",
"@types/is-url": "1.2.30",
"@types/katex": "0.14.0",
"@types/matter-js": "0.17.7",
"@types/mocha": "9.1.0",
"@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": "2.1.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/parser": "5.18.0",
"@vue/compiler-sfc": "3.2.31",
"@vitejs/plugin-vue": "2.3.3",
"@vue/compiler-sfc": "3.2.37",
"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.10.0",
"chart.js": "3.7.1",
"broadcast-channel": "4.13.0",
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2",
"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",
"cropperjs": "2.0.0-beta",
"date-fns": "2.28.0",
"escape-regexp": "0.0.1",
"eslint": "8.13.0",
"eslint-plugin-vue": "8.6.0",
"eventemitter3": "4.0.7",
"feed": "4.2.2",
"glob": "7.2.0",
"idb-keyval": "6.1.0",
"insert-text-at-cursor": "0.3.0",
"ip-cidr": "3.0.4",
"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",
"prismjs": "1.27.0",
"photoswipe": "5.2.7",
"prismjs": "1.28.0",
"private-ip": "2.3.3",
"promise-limit": "2.7.0",
"pug": "3.0.2",
@ -84,43 +56,58 @@
"random-seed": "0.3.0",
"reflect-metadata": "0.1.13",
"rndstr": "1.0.0",
"rollup": "2.75.6",
"s-age": "1.1.2",
"sass": "1.50.0",
"sass-loader": "12.6.0",
"sass": "1.52.3",
"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.0",
"three": "0.141.0",
"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.9",
"tsconfig-paths": "4.0.0",
"twemoji-parser": "14.0.0",
"typescript": "4.6.3",
"typescript": "4.7.3",
"uuid": "8.3.2",
"v-debounce": "0.1.2",
"vanilla-tilt": "1.7.2",
"vue": "3.2.31",
"vue-loader": "17.0.0",
"vite": "2.9.10",
"vue": "3.2.37",
"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.16",
"vuedraggable": "4.0.1",
"webpack": "5.72.0",
"webpack-cli": "4.9.2",
"websocket": "1.0.34",
"ws": "8.5.0"
"ws": "8.8.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "5.18.0",
"@types/escape-regexp": "0.0.1",
"@types/glob": "7.2.0",
"@types/gulp": "4.0.9",
"@types/gulp-rename": "2.0.1",
"@types/is-url": "1.2.30",
"@types/katex": "0.14.0",
"@types/matter-js": "0.17.7",
"@types/mocha": "9.1.1",
"@types/oauth": "0.9.1",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.4.2",
"@types/random-seed": "0.3.3",
"@types/seedrandom": "3.0.2",
"@types/throttle-debounce": "5.0.0",
"@types/tinycolor2": "1.4.3",
"@types/uuid": "8.3.4",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.27.1",
"@typescript-eslint/parser": "5.27.1",
"cross-env": "7.0.3",
"cypress": "9.5.3",
"cypress": "10.0.3",
"eslint": "8.17.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-vue": "9.1.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

@ -131,8 +131,8 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'done', v: { type: string; value: any }): void;
(e: 'closed'): void;
(event: 'done', value: { type: string; value: any }): void;
(event: 'closed'): void;
}>();
const suggests = ref<Element>();
@ -152,7 +152,7 @@ function complete(type: string, value: any) {
emit('closed');
if (type === 'emoji') {
let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((e: any) => e !== value);
recents = recents.filter((emoji: any) => emoji !== value);
recents.unshift(value);
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
}
@ -232,7 +232,7 @@ function exec() {
} else if (props.type === 'emoji') {
if (!props.q || props.q === '') {
// 最近使った絵文字をサジェスト
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji === emoji)).filter(x => x) as EmojiDef[];
emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
return;
}
@ -269,17 +269,17 @@ function exec() {
}
}
function onMousedown(e: Event) {
if (!contains(rootEl.value, e.target) && (rootEl.value !== e.target)) props.close();
function onMousedown(event: Event) {
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
}
function onKeydown(e: KeyboardEvent) {
function onKeydown(event: KeyboardEvent) {
const cancel = () => {
e.preventDefault();
e.stopPropagation();
event.preventDefault();
event.stopPropagation();
};
switch (e.key) {
switch (event.key) {
case 'Enter':
if (select.value !== -1) {
cancel();
@ -310,7 +310,7 @@ function onKeydown(e: KeyboardEvent) {
break;
default:
e.stopPropagation();
event.stopPropagation();
props.textarea.focus();
}
}

View File

@ -27,8 +27,7 @@ type CaptchaContainer = {
};
declare global {
interface Window extends CaptchaContainer {
}
interface Window extends CaptchaContainer { }
}
const props = defineProps<{

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

@ -0,0 +1,175 @@
<template>
<XModalWindow
ref="dialogEl"
:width="800"
:height="500"
:scroll="false"
:with-ok-button="true"
@close="cancel()"
@ok="ok()"
@closed="$emit('closed')"
>
<template #header>{{ $ts.cropImage }}</template>
<template #default="{ width, height }">
<div class="mk-cropper-dialog" :style="`--vw: ${width}px; --vh: ${height}px;`">
<Transition name="fade">
<div v-if="loading" class="loading">
<MkLoading/>
</div>
</Transition>
<div class="container">
<img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
</div>
</div>
</template>
</XModalWindow>
</template>
<script lang="ts" setup>
import { nextTick, onMounted } from 'vue';
import * as misskey from 'misskey-js';
import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2';
import XModalWindow from '@/components/ui/modal-window.vue';
import * as os from '@/os';
import { $i } from '@/account';
import { defaultStore } from '@/store';
import { apiUrl, url } from '@/config';
import { query } from '@/scripts/url';
const emit = defineEmits<{
(ev: 'ok', cropped: misskey.entities.DriveFile): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
const props = defineProps<{
file: misskey.entities.DriveFile;
aspectRatio: number;
}>();
const imgUrl = `${url}/proxy/image.webp?${query({
url: props.file.url,
})}`;
let dialogEl = $ref<InstanceType<typeof XModalWindow>>();
let imgEl = $ref<HTMLImageElement>();
let cropper: Cropper | null = null;
let loading = $ref(true);
const ok = async () => {
const promise = new Promise<misskey.entities.DriveFile>(async (res) => {
const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
croppedCanvas.toBlob(blob => {
const formData = new FormData();
formData.append('file', blob);
formData.append('i', $i.token);
if (defaultStore.state.uploadFolder) {
formData.append('folderId', defaultStore.state.uploadFolder);
}
fetch(apiUrl + '/drive/files/create', {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(f => {
res(f);
});
});
});
os.promiseDialog(promise);
const f = await promise;
emit('ok', f);
dialogEl.close();
};
const cancel = () => {
emit('cancel');
dialogEl.close();
};
const onImageLoad = () => {
loading = false;
if (cropper) {
cropper.getCropperImage()!.$center('contain');
cropper.getCropperSelection()!.$center();
}
};
onMounted(() => {
cropper = new Cropper(imgEl, {
});
const computedStyle = getComputedStyle(document.documentElement);
const selection = cropper.getCropperSelection()!;
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
selection.aspectRatio = props.aspectRatio;
selection.initialAspectRatio = props.aspectRatio;
selection.outlined = true;
window.setTimeout(() => {
cropper.getCropperImage()!.$center('contain');
selection.$center();
}, 100);
// モーダルオープンアニメーションが終わったあとで再度調整
window.setTimeout(() => {
cropper.getCropperImage()!.$center('contain');
selection.$center();
}, 500);
});
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease 0.5s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.mk-cropper-dialog {
display: flex;
flex-direction: column;
width: var(--vw);
height: var(--vh);
position: relative;
> .loading {
position: absolute;
z-index: 10;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
-webkit-backdrop-filter: var(--blur, blur(10px));
backdrop-filter: var(--blur, blur(10px));
background: rgba(0, 0, 0, 0.5);
}
> .container {
flex: 1;
width: 100%;
height: 100%;
> ::v-deep(cropper-canvas) {
width: 100%;
height: 100%;
> cropper-selection > cropper-handle[action="move"] {
background: transparent;
}
}
}
}
</style>

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';
@ -71,7 +71,7 @@ function onMouseover() {
}
function onMouseout() {
hover.value = false
hover.value = false;
}
function onDragover(ev: DragEvent) {
@ -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', {
@ -204,7 +204,7 @@ function deleteFolder() {
defaultStore.set('uploadFolder', null);
}
}).catch(err => {
switch(err.id) {
switch (err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({
type: 'error',
@ -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

@ -97,6 +97,7 @@ import * as os from '@/os';
import { stream } from '@/stream';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
import { uploadFile, uploads } from '@/scripts/upload';
const props = withDefaults(defineProps<{
initialFolder?: Misskey.entities.DriveFolder;
@ -109,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>>();
@ -127,8 +128,9 @@ const moreFolders = ref(false);
const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
const uploadings = os.uploads;
const uploadings = uploads;
const connection = stream.useChannel('drive');
const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
// ドロップされようとしているか
const draghover = ref(false);
@ -141,7 +143,7 @@ const fetching = ref(true);
const ilFilesObserver = new IntersectionObserver(
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles()
)
);
watch(folder, () => emit('cd', folder.value));
@ -151,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);
@ -168,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);
@ -179,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;
@ -209,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,
@ -236,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,
@ -330,7 +332,7 @@ function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
// 削除時に親フォルダに移動
move(folderToDelete.parentId);
}).catch(err => {
switch(err.id) {
switch (err.id) {
case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({
type: 'error',
@ -355,16 +357,16 @@ function onChangeFileInput() {
}
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).then(res => {
uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
addFile(res, true);
});
}
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);
}
@ -380,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);
}
@ -402,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;
}
@ -428,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;
@ -445,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;
@ -462,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) {
@ -510,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();
}
@ -522,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();
}
@ -549,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 {
@ -562,6 +564,10 @@ function fetchMoreFiles() {
function getMenu() {
return [{
type: 'switch',
text: i18n.ts.keepOriginalUploading,
ref: keepOriginal,
}, null, {
text: i18n.ts.addFile,
type: 'label'
}, {
@ -601,7 +607,7 @@ function onContextmenu(ev: MouseEvent) {
onMounted(() => {
if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
nextTick(() => {
ilFilesObserver.observe(loadMoreFiles.value?.$el)
ilFilesObserver.observe(loadMoreFiles.value?.$el);
});
}
@ -622,7 +628,7 @@ onMounted(() => {
onActivated(() => {
if (defaultStore.state.enableInfiniteScroll) {
nextTick(() => {
ilFilesObserver.observe(loadMoreFiles.value?.$el)
ilFilesObserver.observe(loadMoreFiles.value?.$el);
});
}
});

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

@ -1,6 +1,6 @@
<template>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" @paste.stop="paste" @keyup.enter="done()">
<input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @paste.stop="paste" @keyup.enter="done()">
<div ref="emojis" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0">
@ -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

@ -28,7 +28,7 @@
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { onBeforeUnmount, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os';
import { stream } from '@/stream';
@ -43,32 +43,30 @@ const props = withDefaults(defineProps<{
large: false,
});
const isFollowing = ref(props.user.isFollowing);
const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
const wait = ref(false);
let isFollowing = $ref(props.user.isFollowing);
let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou);
let wait = $ref(false);
const connection = stream.useChannel('main');
if (props.user.isFollowing == null) {
os.api('users/show', {
userId: props.user.id
}).then(u => {
isFollowing.value = u.isFollowing;
hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou;
});
})
.then(onFollowChange);
}
function onFollowChange(user: Misskey.entities.UserDetailed) {
if (user.id == props.user.id) {
isFollowing.value = user.isFollowing;
hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
if (user.id === props.user.id) {
isFollowing = user.isFollowing;
hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
}
}
async function onClick() {
wait.value = true;
wait = true;
try {
if (isFollowing.value) {
if (isFollowing) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
@ -80,26 +78,22 @@ async function onClick() {
userId: props.user.id
});
} else {
if (hasPendingFollowRequestFromYou.value) {
if (hasPendingFollowRequestFromYou) {
await os.api('following/requests/cancel', {
userId: props.user.id
});
} else if (props.user.isLocked) {
await os.api('following/create', {
userId: props.user.id
});
hasPendingFollowRequestFromYou.value = true;
hasPendingFollowRequestFromYou = false;
} else {
await os.api('following/create', {
userId: props.user.id
});
hasPendingFollowRequestFromYou.value = true;
hasPendingFollowRequestFromYou = true;
}
}
} catch (e) {
console.error(e);
} catch (err) {
console.error(err);
} finally {
wait.value = false;
wait = 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

@ -24,7 +24,7 @@ const props = withDefaults(defineProps<{
defaultOpen: boolean;
}>(), {
defaultOpen: false,
})
});
let opened = $ref(props.defaultOpen);
let openedAtLeastOnce = $ref(props.defaultOpen);

View File

@ -14,7 +14,7 @@ export default defineComponent({
data() {
return {
value: this.modelValue,
}
};
},
watch: {
value() {

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

@ -5,14 +5,13 @@
</template>
<script lang="ts" setup>
import { inject } from 'vue';
import * as os from '@/os';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { router } from '@/router';
import { url } from '@/config';
import { popout as popout_ } from '@/scripts/popout';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
import { MisskeyNavigator } from '@/scripts/navigate';
const props = withDefaults(defineProps<{
to: string;
@ -23,9 +22,7 @@ const props = withDefaults(defineProps<{
behavior: null,
});
type Navigate = (path: string, record?: boolean) => void;
const navHook = inject<null | Navigate>('navHook', null);
const sideViewHook = inject<null | Navigate>('sideViewHook', null);
const mkNav = new MisskeyNavigator();
const active = $computed(() => {
if (props.activeClass == null) return false;
@ -48,11 +45,11 @@ function onContextmenu(ev) {
action: () => {
os.pageWindow(props.to);
}
}, sideViewHook ? {
}, mkNav.sideViewHook ? {
icon: 'fas fa-columns',
text: i18n.ts.openInSideView,
action: () => {
sideViewHook(props.to);
if (mkNav.sideViewHook) mkNav.sideViewHook(props.to);
}
} : undefined, {
icon: 'fas fa-expand-alt',
@ -101,18 +98,6 @@ function nav() {
}
}
if (navHook) {
navHook(props.to);
} else {
if (defaultStore.state.defaultSideView && sideViewHook && props.to !== '/') {
return sideViewHook(props.to);
}
if (router.currentRoute.value.path === props.to) {
window.scroll({ top: 0, behavior: 'smooth' });
} else {
router.push(props.to);
}
}
mkNav.push(props.to);
}
</script>

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

@ -77,7 +77,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,11 +1,24 @@
<template>
<div class="yxspomdl" :class="{ inline, colored, mini }">
<div class="ring"></div>
<div :class="[$style.root, { [$style.inline]: inline, [$style.colored]: colored, [$style.mini]: mini }]">
<div :class="$style.container">
<svg :class="[$style.spinner, $style.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="[$style.spinner, $style.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>
<script lang="ts" setup>
import { } from 'vue';
import { useCssModule } from 'vue';
useCssModule();
const props = withDefaults(defineProps<{
inline?: boolean;
@ -18,8 +31,8 @@ const props = withDefaults(defineProps<{
});
</script>
<style lang="scss" scoped>
@keyframes ring {
<style lang="scss" module>
@keyframes spinner {
0% {
transform: rotate(0deg);
}
@ -28,12 +41,12 @@ const props = withDefaults(defineProps<{
}
}
.yxspomdl {
.root {
padding: 32px;
text-align: center;
cursor: wait;
--size: 48px;
--size: 40px;
&.colored {
color: var(--accent);
@ -49,34 +62,33 @@ const props = withDefaults(defineProps<{
padding: 16px;
--size: 32px;
}
}
> .ring {
position: relative;
display: inline-block;
vertical-align: middle;
.container {
position: relative;
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;
}
.spinner {
position: absolute;
top: 0;
left: 0;
width: var(--size);
height: var(--size);
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
&:before {
border-color: currentColor;
opacity: 0.3;
}
.bg {
opacity: 0.275;
}
&:after {
position: absolute;
top: 0;
border-color: currentColor transparent transparent transparent;
animation: ring 0.5s linear infinite;
}
}
.fg {
animation: spinner 0.5s linear infinite;
}
</style>

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());
@ -32,8 +32,7 @@ const relative = $computed(() => {
ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
ago >= -1 ? i18n.ts._ago.justNow :
ago < -1 ? i18n.ts._ago.future :
i18n.ts._ago.unknown);
i18n.ts._ago.future);
});
function tick() {

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

@ -8,7 +8,8 @@
<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu">
<video
:poster="video.thumbnailUrl"
:title="video.name"
:title="video.comment"
:alt="video.comment"
preload="none"
controls
@contextmenu.stop

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

@ -39,8 +39,8 @@ export default defineComponent({
inject: {
sideViewHook: {
default: null
}
default: null,
},
},
provide() {
@ -94,31 +94,31 @@ export default defineComponent({
}, {
icon: 'fas fa-expand-alt',
text: this.$ts.showInPage,
action: this.expand
action: this.expand,
}, this.sideViewHook ? {
icon: 'fas fa-columns',
text: this.$ts.openInSideView,
action: () => {
this.sideViewHook(this.path);
this.$refs.window.close();
}
},
} : undefined, {
icon: 'fas fa-external-link-alt',
text: this.$ts.popout,
action: this.popout
action: this.popout,
}, null, {
icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab,
action: () => {
window.open(this.url, '_blank');
this.$refs.window.close();
}
},
}, {
icon: 'fas fa-link',
text: this.$ts.copyLink,
action: () => {
copyToClipboard(this.url);
}
},
}];
},
},
@ -155,7 +155,7 @@ export default defineComponent({
onContextmenu(ev: MouseEvent) {
os.contextMenu(this.contextmenu, ev);
}
},
},
});
</script>

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({
@ -222,7 +222,7 @@ function react(viaKeyboard = false): void {
reactionPicker.show(reactButton.value, reaction => {
os.api('notes/reactions/create', {
noteId: appearNote.id,
reaction: reaction
reaction: reaction,
});
}, () => {
focus();
@ -233,7 +233,7 @@ function undoReact(note): void {
const oldReaction = note.myReaction;
if (!oldReaction) return;
os.api('notes/reactions/delete', {
noteId: note.id
noteId: note.id,
});
}
@ -257,7 +257,7 @@ function onContextmenu(ev: MouseEvent): void {
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
viaKeyboard
viaKeyboard,
}).then(focus);
}
@ -269,12 +269,12 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
noteId: note.id
noteId: note.id,
});
isDeleted.value = true;
}
},
}], renoteTime.value, {
viaKeyboard: viaKeyboard
viaKeyboard: viaKeyboard,
});
}
@ -288,14 +288,14 @@ function blur() {
os.api('notes/children', {
noteId: appearNote.id,
limit: 30
limit: 30,
}).then(res => {
replies.value = res;
});
if (appearNote.replyId) {
os.api('notes/conversation', {
noteId: appearNote.replyId
noteId: appearNote.replyId,
}).then(res => {
conversation.value = res.reverse();
});

View File

@ -5,7 +5,7 @@
<XNoteHeader class="header" :note="note" :mini="true"/>
<div class="body">
<p v-if="note.cw != null" class="cw">
<span v-if="note.cw != ''" class="text">{{ note.cw }}</span>
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
<XCwButton v-model="showContent" :note="note"/>
</p>
<div v-show="note.cw == null || showContent" class="content">

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({
@ -210,7 +210,7 @@ function react(viaKeyboard = false): void {
reactionPicker.show(reactButton.value, reaction => {
os.api('notes/reactions/create', {
noteId: appearNote.id,
reaction: reaction
reaction: reaction,
});
}, () => {
focus();
@ -221,7 +221,7 @@ function undoReact(note): void {
const oldReaction = note.myReaction;
if (!oldReaction) return;
os.api('notes/reactions/delete', {
noteId: note.id
noteId: note.id,
});
}
@ -245,7 +245,7 @@ function onContextmenu(ev: MouseEvent): void {
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
viaKeyboard
viaKeyboard,
}).then(focus);
}
@ -257,12 +257,12 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
noteId: note.id
noteId: note.id,
});
isDeleted.value = true;
}
},
}], renoteTime.value, {
viaKeyboard: viaKeyboard
viaKeyboard: viaKeyboard,
});
}
@ -284,7 +284,7 @@ function focusAfter() {
function readPromo() {
os.api('promo/read', {
noteId: appearNote.id
noteId: appearNote.id,
});
isDeleted.value = true;
}

View File

@ -1,5 +1,6 @@
<template>
<XModalWindow ref="dialog"
<XModalWindow
ref="dialog"
:width="400"
:height="450"
:with-ok-button="true"
@ -28,18 +29,18 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import { notificationTypes } from 'misskey-js';
import MkSwitch from './form/switch.vue';
import MkInfo from './ui/info.vue';
import MkButton from './ui/button.vue';
import { notificationTypes } from 'misskey-js';
import XModalWindow from '@/components/ui/modal-window.vue';
export default defineComponent({
components: {
XModalWindow,
MkSwitch,
MkInfo,
MkButton
MkButton,
},
props: {
@ -53,7 +54,7 @@ export default defineComponent({
type: Boolean,
required: false,
default: true,
}
},
},
emits: ['done', 'closed'],
@ -93,7 +94,7 @@ export default defineComponent({
for (const type in this.typesMap) {
this.typesMap[type as typeof notificationTypes[number]] = true;
}
}
}
},
},
});
</script>

View File

@ -16,7 +16,8 @@
<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i>
<i v-else-if="notification.type === 'pollEnded'" class="fas fa-poll-h"></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon v-else-if="notification.type === 'reaction'"
<XReactionIcon
v-else-if="notification.type === 'reaction'"
ref="reactionRef"
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
:custom-emojis="notification.note.emojis"
@ -72,12 +73,12 @@
</template>
<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
import * as misskey from 'misskey-js';
import { getNoteSummary } from '@/scripts/get-note-summary';
import XReactionIcon from './reaction-icon.vue';
import MkFollowButton from './follow-button.vue';
import XReactionTooltip from './reaction-tooltip.vue';
import { getNoteSummary } from '@/scripts/get-note-summary';
import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user';
import { i18n } from '@/i18n';
@ -87,7 +88,7 @@ import { useTooltip } from '@/scripts/use-tooltip';
export default defineComponent({
components: {
XReactionIcon, MkFollowButton
XReactionIcon, MkFollowButton,
},
props: {
@ -116,7 +117,7 @@ export default defineComponent({
const readObserver = new IntersectionObserver((entries, observer) => {
if (!entries.some(entry => entry.isIntersecting)) return;
stream.send('readNotification', {
id: props.notification.id
id: props.notification.id,
});
observer.disconnect();
});
@ -126,6 +127,10 @@ export default defineComponent({
const connection = stream.useChannel('main');
connection.on('readAllNotifications', () => readObserver.disconnect());
watch(props.notification.isRead, () => {
readObserver.disconnect();
});
onUnmounted(() => {
readObserver.disconnect();
connection.dispose();

View File

@ -19,8 +19,7 @@
<script lang="ts" setup>
import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
import { notificationTypes } from 'misskey-js';
import MkPagination from '@/components/ui/pagination.vue';
import { Paging } from '@/components/ui/pagination.vue';
import MkPagination, { Paging } from '@/components/ui/pagination.vue';
import XNotification from '@/components/notification.vue';
import XList from '@/components/date-separated-list.vue';
import XNote from '@/components/note.vue';
@ -49,14 +48,14 @@ const onNotification = (notification) => {
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') {
stream.send('readNotification', {
id: notification.id
id: notification.id,
});
}
if (!isMuted) {
pagingComponent.value.prepend({
...notification,
isRead: document.visibilityState === 'visible'
isRead: document.visibilityState === 'visible',
});
}
};
@ -64,6 +63,31 @@ const onNotification = (notification) => {
onMounted(() => {
const connection = stream.useChannel('main');
connection.on('notification', onNotification);
connection.on('readAllNotifications', () => {
if (pagingComponent.value) {
for (const item of pagingComponent.value.queue) {
item.isRead = true;
}
for (const item of pagingComponent.value.items) {
item.isRead = true;
}
}
});
connection.on('readNotifications', notificationIds => {
if (pagingComponent.value) {
for (let i = 0; i < pagingComponent.value.queue.length; i++) {
if (notificationIds.includes(pagingComponent.value.queue[i].id)) {
pagingComponent.value.queue[i].isRead = true;
}
}
for (let i = 0; i < (pagingComponent.value.items || []).length; i++) {
if (notificationIds.includes(pagingComponent.value.items[i].id)) {
pagingComponent.value.items[i].isRead = true;
}
}
}
});
onUnmounted(() => {
connection.dispose();
});

View File

@ -12,7 +12,7 @@ export default defineComponent({
props: {
value: {
type: Number,
required: true
required: true,
},
},
@ -26,7 +26,7 @@ export default defineComponent({
isZero,
number,
};
}
},
});
</script>

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,21 +52,21 @@ 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 => {
ok(f);
})
});
});
});
os.promiseDialog(promise);

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

@ -16,7 +16,7 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import MkDriveFileThumbnail from './drive-file-thumbnail.vue'
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
import * as os from '@/os';
export default defineComponent({
@ -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,
@ -114,19 +114,19 @@ export default defineComponent({
this.menu = os.popupMenu([{
text: this.$ts.renameFile,
icon: 'fas fa-i-cursor',
action: () => { this.rename(file) }
action: () => { this.rename(file); }
}, {
text: file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
icon: file.isSensitive ? 'fas fa-eye-slash' : 'fas fa-eye',
action: () => { this.toggleSensitive(file) }
action: () => { this.toggleSensitive(file); }
}, {
text: this.$ts.describeFile,
icon: 'fas fa-i-cursor',
action: () => { this.describe(file) }
action: () => { this.describe(file); }
}, {
text: this.$ts.attachCancel,
icon: 'fas fa-times-circle',
action: () => { this.detachMedia(file.id) }
action: () => { this.detachMedia(file.id); }
}], ev.currentTarget ?? ev.target).then(() => this.menu = null);
}
}

View File

@ -63,7 +63,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';
@ -89,6 +89,7 @@ import MkInfo from '@/components/ui/info.vue';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
import { uploadFile } from '@/scripts/upload';
const modal = inject('modal');
@ -108,7 +109,7 @@ const props = withDefaults(defineProps<{
fixed?: boolean;
autofocus?: boolean;
}>(), {
initialVisibleUsers: [],
initialVisibleUsers: () => [],
autofocus: true,
});
@ -229,7 +230,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) : ''} `;
}
@ -240,16 +241,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} `;
}
@ -304,7 +304,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;
}
@ -317,7 +317,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);
});
@ -364,7 +364,7 @@ function chooseFileFromEnc(ev) {
}
function detachFile(id) {
files = files.filter(x => x.id != id);
files = files.filter(x => x.id !== id);
}
function updateFiles(_files) {
@ -380,7 +380,7 @@ function updateFileName(file, name) {
}
function upload(file: File, name?: string) {
os.upload(file, defaultStore.state.uploadFolder, name).then(res => {
uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
files.push(res);
});
}
@ -391,7 +391,7 @@ function setVisibility() {
return;
}
os.popup(import('./visibility-picker.vue'), {
os.popup(defineAsyncComponent(() => import('./visibility-picker.vue')), {
currentVisibility: visibility,
currentLocalOnly: localOnly,
src: visibilityButton,
@ -434,24 +434,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) : '';
@ -460,10 +460,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',
@ -479,49 +479,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,
@ -534,20 +534,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,
@ -556,18 +556,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)));
}
}
@ -579,13 +579,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))));
}
@ -669,7 +669,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

@ -222,7 +222,7 @@ export default defineComponent({
return {
chartEl,
}
};
},
});
</script>

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

@ -52,7 +52,7 @@ export default defineComponent({
flag: true,
radio: 'misskey',
mfm: `Hello world! This is an @example mention. BTW you are @${this.$i ? this.$i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`
}
};
},
methods: {

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,42 @@
<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>
<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 +48,198 @@
</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,
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;
}
case '22d05606-fbcf-421a-a2db-b32610dcfd1b': {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: i18n.ts.rateLimitExceeded,
});
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

@ -1,11 +1,11 @@
<template>
<form class="qlvuhzng _formRoot" :autocomplete="Math.random()" @submit.prevent="onSubmit">
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
<template v-if="meta">
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :autocomplete="Math.random()" spellcheck="false" required>
<MkInput v-if="meta.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" spellcheck="false" required>
<template #label>{{ $ts.invitationCode }}</template>
<template #prefix><i class="fas fa-key"></i></template>
</MkInput>
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
<template #label>{{ $ts.username }} <div v-tooltip:dialog="$ts.usernameInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
@ -19,7 +19,7 @@
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
</template>
</MkInput>
<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :autocomplete="Math.random()" spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
<MkInput v-if="meta.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
<template #label>{{ $ts.emailAddress }} <div v-tooltip:dialog="$ts._signup.emailAddressInfo" class="_button _help"><i class="far fa-question-circle"></i></div></template>
<template #prefix><i class="fas fa-envelope"></i></template>
<template #caption>
@ -34,7 +34,7 @@
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
</template>
</MkInput>
<MkInput v-model="password" class="_formBlock" type="password" :autocomplete="Math.random()" required data-cy-signup-password @update:modelValue="onChangePassword">
<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
<template #label>{{ $ts.password }}</template>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption>
@ -43,7 +43,7 @@
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
</template>
</MkInput>
<MkInput v-model="retypedPassword" class="_formBlock" type="password" :autocomplete="Math.random()" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption>
@ -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: {
@ -111,7 +111,7 @@ export default defineComponent({
ToSAgreement: false,
hCaptchaResponse: null,
reCaptchaResponse: null,
}
};
},
computed: {
@ -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

@ -91,32 +91,32 @@ 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);
}
function calcCircleScale(boxW, boxH, circleCenterX, circleCenterY) {
const origin = {x: circleCenterX, y: circleCenterY};
const dist1 = distance({x: 0, y: 0}, origin);
const dist2 = distance({x: boxW, y: 0}, origin);
const dist3 = distance({x: 0, y: boxH}, origin);
const dist4 = distance({x: boxW, y: boxH }, origin);
const origin = { x: circleCenterX, y: circleCenterY };
const dist1 = distance({ x: 0, y: 0 }, origin);
const dist2 = distance({ x: boxW, y: 0 }, origin);
const dist3 = distance({ x: 0, y: boxH }, origin);
const dist4 = distance({ x: boxW, y: boxH }, origin);
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

@ -1,5 +1,6 @@
<template>
<div ref="itemsEl" v-hotkey="keymap"
<div
ref="itemsEl" v-hotkey="keymap"
class="rrevdjwt"
:class="{ center: align === 'center', asDrawer }"
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@ -60,7 +61,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'close'): void;
(ev: 'close'): void;
}>();
let itemsEl = $ref<HTMLDivElement>();
@ -162,6 +163,15 @@ function focusDown() {
position: relative;
}
&:not(:disabled):hover {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
&.danger {
color: #ff2a2a;
@ -191,15 +201,6 @@ function focusDown() {
}
}
&:not(:disabled):hover {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
&:not(:active):focus-visible {
box-shadow: 0 0 0 2px var(--focus) inset;
}

View File

@ -1,7 +1,7 @@
<template>
<MkModal ref="modal" :prefer-type="'dialog'" @click="$emit('click')" @closed="$emit('closed')">
<div class="ebkgoccj _window _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
<div class="header">
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
<div ref="rootEl" class="ebkgoccj _window _narrow_" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
<div ref="headerEl" class="header">
<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="fas fa-times"></i></button>
<span class="title">
<slot name="header"></slot>
@ -11,82 +11,82 @@
</div>
<div v-if="padding" class="body">
<div class="_section">
<slot></slot>
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
<div v-else class="body">
<slot></slot>
<slot :width="bodyWidth" :height="bodyHeight"></slot>
</div>
</div>
</MkModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue';
import MkModal from './modal.vue';
export default defineComponent({
components: {
MkModal
},
props: {
withOkButton: {
type: Boolean,
required: false,
default: false
},
okButtonDisabled: {
type: Boolean,
required: false,
default: false
},
padding: {
type: Boolean,
required: false,
default: false
},
width: {
type: Number,
required: false,
default: 400
},
height: {
type: Number,
required: false,
default: null
},
canClose: {
type: Boolean,
required: false,
default: true,
},
scroll: {
type: Boolean,
required: false,
default: true,
},
},
const props = withDefaults(defineProps<{
withOkButton: boolean;
okButtonDisabled: boolean;
padding: boolean;
width: number;
height: number | null;
scroll: boolean;
}>(), {
withOkButton: false,
okButtonDisabled: false,
padding: false,
width: 400,
height: null,
scroll: true,
});
emits: ['click', 'close', 'closed', 'ok'],
const emit = defineEmits<{
(event: 'click'): void;
(event: 'close'): void;
(event: 'closed'): void;
(event: 'ok'): void;
}>();
data() {
return {
};
},
let modal = $ref<InstanceType<typeof MkModal>>();
let rootEl = $ref<HTMLElement>();
let headerEl = $ref<HTMLElement>();
let bodyWidth = $ref(0);
let bodyHeight = $ref(0);
methods: {
close() {
this.$refs.modal.close();
},
const close = () => {
modal.close();
};
onKeydown(e) {
if (e.which === 27) { // Esc
e.preventDefault();
e.stopPropagation();
this.close();
}
},
const onBgClick = () => {
emit('click');
};
const onKeydown = (evt) => {
if (evt.which === 27) { // Esc
evt.preventDefault();
evt.stopPropagation();
close();
}
};
const ro = new ResizeObserver((entries, observer) => {
bodyWidth = rootEl.offsetWidth;
bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight;
});
onMounted(() => {
bodyWidth = rootEl.offsetWidth;
bodyHeight = rootEl.offsetHeight - headerEl.offsetHeight;
ro.observe(rootEl);
});
onUnmounted(() => {
ro.disconnect();
});
defineExpose({
close,
});
</script>

View File

@ -1,5 +1,5 @@
<template>
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="childRendered">
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
@ -48,6 +48,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
(ev: 'opening'): void;
(ev: 'opened'): void;
(ev: 'click'): void;
(ev: 'esc'): void;
(ev: 'close'): void;
@ -212,7 +213,9 @@ const align = () => {
popover.style.top = top + 'px';
};
const childRendered = () => {
const onOpened = () => {
emit('opened');
// モーダルコンテンツにマウスボタンが押され、コンテンツ外でマウスボタンが離されたときにモーダルバックグラウンドクリックと判定させないためにマウスイベントを監視しフラグ管理する
const el = content.value!.children[0];
el.addEventListener('mousedown', ev => {
@ -234,10 +237,10 @@ onMounted(() => {
}
fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
await nextTick()
await nextTick();
align();
}, { immediate: true, });
}, { immediate: true });
nextTick(() => {
const popover = content.value;

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]);
@ -270,11 +281,12 @@ onDeactivated(() => {
defineExpose({
items,
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

@ -63,7 +63,7 @@ const setPosition = () => {
}
return [left, top];
}
};
const calcPosWhenBottom = () => {
let left: number;
@ -84,7 +84,7 @@ const setPosition = () => {
}
return [left, top];
}
};
const calcPosWhenLeft = () => {
let left: number;
@ -105,7 +105,7 @@ const setPosition = () => {
}
return [left, top];
}
};
const calcPosWhenRight = () => {
let left: number;
@ -126,7 +126,7 @@ const setPosition = () => {
}
return [left, top];
}
};
const calc = (): {
left: number;
@ -172,7 +172,7 @@ const setPosition = () => {
}
return null as never;
}
};
const { left, top, transformOrigin } = calc();
el.value.style.transformOrigin = transformOrigin;

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

@ -90,7 +90,7 @@ fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).the
sitename = info.sitename;
fetching = false;
player = info.player;
})
});
});
function adjustTweetHeight(message: any) {

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

@ -2,11 +2,11 @@
<div class="vjoppmmu">
<template v-if="edit">
<header>
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)">
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" class="mk-widget-select">
<template #label>{{ $ts.selectWidget }}</template>
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ $t(`_widgets.${widget}`) }}</option>
</MkSelect>
<MkButton inline primary @click="addWidget"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
<MkButton inline primary class="mk-widget-add" @click="addWidget"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton>
</header>
<XDraggable
@ -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

@ -9,7 +9,7 @@ export default {
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
}
};
const parentBg = getBgColor(src.parentElement);

View File

@ -25,12 +25,12 @@ function calc(src: Element) {
return;
}
if (info.intersection) {
info.intersection.disconnect()
info.intersection.disconnect();
delete info.intersection;
};
}
info.fn(width, height);
};
}
export default {
mounted(src, binding, vn) {

View File

@ -9,7 +9,7 @@ export default {
} else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
}
}
};
const parentBg = getBgColor(src.parentElement);

View File

@ -60,9 +60,9 @@ function calc(el: Element) {
return;
}
if (info.intersection) {
info.intersection.disconnect()
info.intersection.disconnect();
delete info.intersection;
};
}
mountings.set(el, Object.assign(info, { previousWidth: width }));

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';
@ -146,8 +146,7 @@ if ($i && $i.token) {
try {
document.body.innerHTML = '<div>Please wait...</div>';
await login(i);
location.reload();
} catch (e) {
} catch (err) {
// Render the error screen
// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
document.body.innerHTML = '<div id="err">Oops!</div>';
@ -169,14 +168,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;
@ -204,8 +203,24 @@ if (splash) splash.addEventListener('transitionend', () => {
splash.remove();
});
const rootEl = document.createElement('div');
document.body.appendChild(rootEl);
// 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のやつを解除
@ -231,10 +246,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) {
}
}
@ -319,7 +334,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, defineAsyncComponent, markRaw, reactive, 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';
@ -10,7 +10,6 @@ import MkWaitingDialog from '@/components/waiting-dialog.vue';
import { MenuItem } from '@/types/menu';
import { resolve } from '@/router';
import { $i } from '@/account';
import { defaultStore } from '@/store';
export const pendingApiRequestsCount = ref(0);
@ -35,7 +34,7 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s
method: 'POST',
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache'
cache: 'no-cache',
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
@ -60,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,
});
});
@ -73,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);
@ -89,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,
});
}
});
@ -111,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;
@ -132,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;
@ -150,7 +142,7 @@ export async function popup(component: Component | typeof import('*.vue') | Prom
props,
events: disposeEvent ? {
...events,
[disposeEvent]: dispose
[disposeEvent]: dispose,
} : events,
id,
};
@ -164,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,
@ -173,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,
@ -181,8 +173,8 @@ export function modalPageWindow(path: string) {
}
export function toast(message: string) {
popup(import('@/components/toast.vue'), {
message
popup(defineAsyncComponent(() => import('@/components/toast.vue')), {
message,
}, {}, 'closed');
}
@ -192,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();
},
@ -206,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,
}, {
@ -227,14 +219,14 @@ 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: {
type: props.type,
placeholder: props.placeholder,
default: props.default,
}
},
}, {
done: result => {
resolve(result ? result : { canceled: true });
@ -252,14 +244,14 @@ 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: {
type: 'number',
placeholder: props.placeholder,
default: props.default,
}
},
}, {
done: result => {
resolve(result ? result : { canceled: true });
@ -277,14 +269,14 @@ 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: {
type: 'date',
placeholder: props.placeholder,
default: props.default,
}
},
}, {
done: result => {
resolve(result ? { result: new Date(result.result), canceled: false } : { canceled: true });
@ -293,7 +285,7 @@ export function inputDate(props: {
});
}
export function select<C extends any = any>(props: {
export function select<C = any>(props: {
title?: string | null;
text?: string | null;
default?: string | null;
@ -314,14 +306,14 @@ 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: {
items: props.items,
groupedItems: props.groupedItems,
default: props.default,
}
},
}, {
done: result => {
resolve(result ? result : { canceled: true });
@ -336,9 +328,9 @@ 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
showing: showing,
}, {
done: () => resolve(),
}, 'closed');
@ -348,9 +340,9 @@ 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
showing: showing,
}, {
done: () => resolve(),
}, 'closed');
@ -359,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);
},
@ -369,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);
},
@ -379,9 +371,9 @@ 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
multiple,
}, {
done: files => {
if (files) {
@ -394,9 +386,9 @@ 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
multiple,
}, {
done: folders => {
if (folders) {
@ -409,9 +401,9 @@ 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
...opts,
}, {
done: emoji => {
resolve(emoji);
@ -420,6 +412,21 @@ export async function pickEmoji(src: HTMLElement | null, opts) {
});
}
export async function cropImage(image: Misskey.entities.DriveFile, options: {
aspectRatio: number;
}): Promise<Misskey.entities.DriveFile> {
return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/cropper-dialog.vue')), {
file: image,
aspectRatio: options.aspectRatio,
}, {
ok: x => {
resolve(x);
},
}, 'closed');
});
}
type AwaitType<T> =
T extends Promise<infer U> ? U :
T extends (...args: any[]) => Promise<infer V> ? V :
@ -459,9 +466,9 @@ 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
...opts,
}, {
chosen: emoji => {
insertTextAtCursor(activeTextarea, emoji);
@ -470,7 +477,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea:
openingEmojiPicker!.dispose();
openingEmojiPicker = null;
observer.disconnect();
}
},
});
}
@ -481,12 +488,12 @@ 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,
align: options?.align,
viaKeyboard: options?.viaKeyboard
viaKeyboard: options?.viaKeyboard,
}, {
closed: () => {
resolve();
@ -502,7 +509,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,
}, {
@ -537,78 +544,6 @@ export function post(props: Record<string, any> = {}) {
export const deckGlobalEvents = new EventEmitter();
export const uploads = ref<{
id: string;
name: string;
progressMax: number | undefined;
progressValue: number | undefined;
img: string;
}[]>([]);
export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise<Misskey.entities.DriveFile> {
if (folder && typeof folder === 'object') folder = folder.id;
return new Promise((resolve, reject) => {
const id = Math.random().toString();
const reader = new FileReader();
reader.onload = (e) => {
const ctx = reactive({
id: id,
name: name || file.name || 'untitled',
progressMax: undefined,
progressValue: undefined,
img: window.URL.createObjectURL(file)
});
uploads.value.push(ctx);
console.log(keepOriginal);
const data = new FormData();
data.append('i', $i.token);
data.append('force', 'true');
data.append('file', file);
if (folder) data.append('folderId', folder);
if (name) data.append('name', name);
const xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + '/drive/files/create', true);
xhr.onload = (ev) => {
if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
// TODO: 消すのではなくて再送できるようにしたい
uploads.value = uploads.value.filter(x => x.id != id);
alert({
type: 'error',
text: 'upload failed'
});
reject();
return;
}
const driveFile = JSON.parse(ev.target.response);
resolve(driveFile);
uploads.value = uploads.value.filter(x => x.id != id);
};
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
ctx.progressMax = e.total;
ctx.progressValue = e.loaded;
}
};
xhr.send(data);
};
reader.readAsArrayBuffer(file);
});
}
/*
export function checkExistence(fileData: ArrayBuffer): Promise<any> {
return new Promise((resolve, reject) => {

View File

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

View File

@ -24,10 +24,10 @@
</div>
<!-- TODO
<div class="inputs" style="display: flex; padding-top: 1.2em;">
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()">
<MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false">
<span>{{ $ts.username }}</span>
</MkInput>
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'">
<MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" :disabled="pagination.params().origin === 'local'">
<span>{{ $ts.host }}</span>
</MkInput>
</div>
@ -41,8 +41,8 @@
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
<script lang="ts" setup>
import { computed } from 'vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
@ -50,45 +50,35 @@ import MkPagination from '@/components/ui/pagination.vue';
import XAbuseReport from '@/components/abuse-report.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkInput,
MkSelect,
MkPagination,
XAbuseReport,
},
let reports = $ref<InstanceType<typeof MkPagination>>();
emits: ['info'],
let state = $ref('unresolved');
let reporterOrigin = $ref('combined');
let targetUserOrigin = $ref('combined');
let searchUsername = $ref('');
let searchHost = $ref('');
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.abuseReports,
icon: 'fas fa-exclamation-circle',
bg: 'var(--bg)',
},
searchUsername: '',
searchHost: '',
state: 'unresolved',
reporterOrigin: 'combined',
targetUserOrigin: 'combined',
pagination: {
endpoint: 'admin/abuse-user-reports' as const,
limit: 10,
params: computed(() => ({
state: this.state,
reporterOrigin: this.reporterOrigin,
targetUserOrigin: this.targetUserOrigin,
})),
},
}
},
const pagination = {
endpoint: 'admin/abuse-user-reports' as const,
limit: 10,
params: computed(() => ({
state,
reporterOrigin,
targetUserOrigin,
})),
};
methods: {
resolved(reportId) {
this.$refs.reports.removeItem(item => item.id === reportId);
},
function resolved(reportId) {
reports.removeItem(item => item.id === reportId);
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.abuseReports,
icon: 'fas fa-exclamation-circle',
bg: 'var(--bg)',
}
});
</script>

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>

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