Merge branch 'develop'
This commit is contained in:
@ -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
7
packages/client/@types/theme.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
declare module '@/themes/*.json5' {
|
||||
import { Theme } from "@/scripts/theme";
|
||||
|
||||
const theme: Theme;
|
||||
|
||||
export default theme;
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -37,7 +37,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const window = ref<InstanceType<typeof XWindow>>();
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,7 @@ type CaptchaContainer = {
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window extends CaptchaContainer {
|
||||
}
|
||||
interface Window extends CaptchaContainer { }
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -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
175
packages/client/src/components/cropper-dialog.vue
Normal file
175
packages/client/src/components/cropper-dialog.vue
Normal 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>
|
@ -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(() => {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
});
|
||||
|
||||
|
@ -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>>();
|
||||
|
@ -24,6 +24,6 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -24,7 +24,7 @@ const props = withDefaults(defineProps<{
|
||||
defaultOpen: boolean;
|
||||
}>(), {
|
||||
defaultOpen: false,
|
||||
})
|
||||
});
|
||||
|
||||
let opened = $ref(props.defaultOpen);
|
||||
let openedAtLeastOnce = $ref(props.defaultOpen);
|
||||
|
@ -14,7 +14,7 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
value: this.modelValue,
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
|
@ -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);
|
||||
|
@ -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>();
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
|
@ -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); }
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -25,7 +25,7 @@ const props = withDefaults(defineProps<{
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const modal = $ref<InstanceType<typeof MkModal>>();
|
||||
|
@ -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%;
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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': {
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -222,7 +222,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
chartEl,
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@ -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;
|
||||
|
@ -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: {
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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>>();
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -19,7 +19,7 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const zIndex = os.claimZIndex('high');
|
||||
|
@ -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) + ')';
|
||||
|
@ -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>
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
const localStoragePrefix = 'ui:folder:';
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -19,7 +19,7 @@ defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
let modal = $ref<InstanceType<typeof MkModal>>();
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
},
|
||||
|
||||
// 高さを適用
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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('');
|
||||
|
@ -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);
|
||||
|
@ -21,8 +21,8 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done');
|
||||
(e: 'closed');
|
||||
(ev: 'done');
|
||||
(ev: 'closed');
|
||||
}>();
|
||||
|
||||
function done() {
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -9,7 +9,7 @@ export default {
|
||||
} else {
|
||||
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const parentBg = getBgColor(src.parentElement);
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -9,7 +9,7 @@ export default {
|
||||
} else {
|
||||
return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const parentBg = getBgColor(src.parentElement);
|
||||
|
||||
|
@ -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 }));
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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"] }
|
||||
]
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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', );
|
||||
});
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -150,6 +150,7 @@ const patrons = [
|
||||
'Weeble',
|
||||
'蝉暮せせせ',
|
||||
'ThatOneCalculator',
|
||||
'pixeldesu',
|
||||
];
|
||||
|
||||
let easterEggReady = false;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
Reference in New Issue
Block a user