Merge branch 'notification-read-api' into swn
This commit is contained in:
@@ -11,10 +11,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordapp/twemoji": "13.1.0",
|
||||
"@elastic/elasticsearch": "7.11.0",
|
||||
"@sentry/browser": "5.29.2",
|
||||
"@sentry/tracing": "5.29.2",
|
||||
"@sinonjs/fake-timers": "7.1.2",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/escape-regexp": "0.0.0",
|
||||
@@ -22,70 +20,56 @@
|
||||
"@types/gulp": "4.0.9",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/is-url": "1.2.30",
|
||||
"@types/js-yaml": "4.0.4",
|
||||
"@types/katex": "0.11.1",
|
||||
"@types/matter-js": "0.17.6",
|
||||
"@types/mocha": "8.2.3",
|
||||
"@types/node": "16.11.7",
|
||||
"@types/node-fetch": "2.5.12",
|
||||
"@types/nodemailer": "6.4.4",
|
||||
"@types/nprogress": "0.2.0",
|
||||
"@types/node": "16.11.12",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/parse5": "6.0.2",
|
||||
"@types/parsimmon": "1.10.6",
|
||||
"@types/portscanner": "2.1.1",
|
||||
"@types/pug": "2.0.5",
|
||||
"@types/parse5": "6.0.3",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.4.1",
|
||||
"@types/random-seed": "0.3.3",
|
||||
"@types/rename": "1.0.4",
|
||||
"@types/request-stats": "3.0.0",
|
||||
"@types/seedrandom": "2.4.28",
|
||||
"@types/sinonjs__fake-timers": "6.0.4",
|
||||
"@types/speakeasy": "2.0.6",
|
||||
"@types/throttle-debounce": "2.1.0",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/tmp": "0.2.2",
|
||||
"@types/uuid": "8.3.1",
|
||||
"@types/uuid": "8.3.3",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/webpack": "5.28.0",
|
||||
"@types/webpack-stream": "3.2.12",
|
||||
"@types/websocket": "1.0.4",
|
||||
"@types/ws": "8.2.0",
|
||||
"@typescript-eslint/parser": "5.1.0",
|
||||
"@vue/compiler-sfc": "3.2.21",
|
||||
"@types/ws": "8.2.2",
|
||||
"@typescript-eslint/parser": "5.6.0",
|
||||
"@vue/compiler-sfc": "3.2.24",
|
||||
"abort-controller": "3.0.0",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "4.0.4",
|
||||
"autwh": "0.1.0",
|
||||
"blurhash": "1.1.4",
|
||||
"broadcast-channel": "4.5.0",
|
||||
"chart.js": "3.6.0",
|
||||
"broadcast-channel": "4.7.0",
|
||||
"chart.js": "3.6.2",
|
||||
"chartjs-adapter-date-fns": "2.0.0",
|
||||
"chartjs-plugin-zoom": "1.1.1",
|
||||
"chartjs-plugin-zoom": "1.2.0",
|
||||
"compare-versions": "3.6.0",
|
||||
"concurrently": "6.3.0",
|
||||
"content-disposition": "0.5.3",
|
||||
"crc-32": "1.2.0",
|
||||
"css-loader": "6.5.1",
|
||||
"cssnano": "5.0.10",
|
||||
"date-fns": "2.25.0",
|
||||
"cssnano": "5.0.12",
|
||||
"date-fns": "2.27.0",
|
||||
"dateformat": "4.5.1",
|
||||
"escape-regexp": "0.0.1",
|
||||
"eslint": "8.2.0",
|
||||
"eslint-plugin-vue": "8.1.1",
|
||||
"eslint": "8.4.1",
|
||||
"eslint-plugin-vue": "8.2.0",
|
||||
"eventemitter3": "4.0.7",
|
||||
"feed": "4.2.2",
|
||||
"glob": "7.2.0",
|
||||
"got": "11.8.2",
|
||||
"idb-keyval": "5.1.3",
|
||||
"insert-text-at-cursor": "0.3.0",
|
||||
"ip-cidr": "3.0.4",
|
||||
"is-svg": "4.3.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"json5": "2.2.0",
|
||||
"json5-loader": "4.0.1",
|
||||
"katex": "0.13.18",
|
||||
"katex": "0.15.1",
|
||||
"langmap": "0.0.16",
|
||||
"matter-js": "0.17.1",
|
||||
"mfm-js": "0.20.0",
|
||||
@@ -93,32 +77,26 @@
|
||||
"mocha": "8.4.0",
|
||||
"ms": "2.1.3",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "2.6.1",
|
||||
"parse5": "6.0.1",
|
||||
"photoswipe": "git://github.com/dimsemenov/photoswipe#v5-beta",
|
||||
"portscanner": "2.2.0",
|
||||
"postcss": "8.3.11",
|
||||
"postcss-loader": "6.2.0",
|
||||
"postcss": "8.4.4",
|
||||
"postcss-loader": "6.2.1",
|
||||
"prismjs": "1.25.0",
|
||||
"private-ip": "2.3.3",
|
||||
"probe-image-size": "7.2.1",
|
||||
"promise-limit": "2.7.0",
|
||||
"pug": "3.0.2",
|
||||
"punycode": "2.1.1",
|
||||
"pureimage": "0.3.5",
|
||||
"qrcode": "1.4.4",
|
||||
"qrcode": "1.5.0",
|
||||
"querystring": "0.2.1",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rename": "1.0.4",
|
||||
"request-stats": "3.0.0",
|
||||
"rndstr": "1.0.0",
|
||||
"s-age": "1.1.2",
|
||||
"sass": "1.43.4",
|
||||
"sass-loader": "12.3.0",
|
||||
"sass": "1.44.0",
|
||||
"sass-loader": "12.4.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"speakeasy": "2.0.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"style-loader": "3.3.1",
|
||||
@@ -130,25 +108,25 @@
|
||||
"tmp": "0.2.1",
|
||||
"ts-loader": "9.2.6",
|
||||
"ts-node": "10.4.0",
|
||||
"tsc-alias": "1.3.10",
|
||||
"tsconfig-paths": "3.11.0",
|
||||
"tsc-alias": "1.4.2",
|
||||
"tsconfig-paths": "3.12.0",
|
||||
"twemoji-parser": "13.1.0",
|
||||
"typescript": "4.4.4",
|
||||
"typescript": "4.5.2",
|
||||
"uuid": "8.3.2",
|
||||
"v-debounce": "0.1.2",
|
||||
"vanilla-tilt": "1.7.2",
|
||||
"vue": "3.2.21",
|
||||
"vue-loader": "16.7.0",
|
||||
"vue": "3.2.24",
|
||||
"vue-loader": "16.8.3",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vue-router": "4.0.5",
|
||||
"vue-style-loader": "4.1.3",
|
||||
"vue-svg-loader": "0.17.0-beta.2",
|
||||
"vuedraggable": "4.0.1",
|
||||
"web-push": "3.4.5",
|
||||
"webpack": "5.63.0",
|
||||
"webpack": "5.65.0",
|
||||
"webpack-cli": "4.9.1",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.2.3"
|
||||
"ws": "8.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.54",
|
||||
|
@@ -85,6 +85,10 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
.zdjebgpv {
|
||||
position: relative;
|
||||
display: flex;
|
||||
background: #e1e1e1;
|
||||
border-radius: 8px;
|
||||
overflow: clip;
|
||||
|
||||
> .icon-sub {
|
||||
position: absolute;
|
||||
@@ -95,14 +99,11 @@ export default defineComponent({
|
||||
bottom: 4%;
|
||||
}
|
||||
|
||||
> * {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
pointer-events: none;
|
||||
height: 65%;
|
||||
width: 65%;
|
||||
margin: auto;
|
||||
font-size: 32px;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -210,7 +210,7 @@ export default defineComponent({
|
||||
position: relative;
|
||||
padding: 8px 0 0 0;
|
||||
min-height: 180px;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
|
||||
&, * {
|
||||
cursor: pointer;
|
||||
|
@@ -657,14 +657,14 @@ export default defineComponent({
|
||||
> .path {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
line-height: 38px;
|
||||
line-height: 50px;
|
||||
white-space: nowrap;
|
||||
|
||||
> * {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
line-height: 38px;
|
||||
line-height: 50px;
|
||||
cursor: pointer;
|
||||
|
||||
* {
|
||||
@@ -699,6 +699,7 @@ export default defineComponent({
|
||||
|
||||
> .menu {
|
||||
margin-left: auto;
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -79,7 +79,7 @@ import { emojilist } from '@/scripts/emojilist';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import Particle from '@/components/particle.vue';
|
||||
import * as os from '@/os';
|
||||
import { isDeviceTouch } from '@/scripts/is-device-touch';
|
||||
import { isTouchUsing } from '@/scripts/touch';
|
||||
import { isMobile } from '@/scripts/is-mobile';
|
||||
import { emojiCategories } from '@/instance';
|
||||
import XSection from './emoji-picker.section.vue';
|
||||
@@ -108,7 +108,7 @@ export default defineComponent({
|
||||
pinned: this.$store.reactiveState.reactions,
|
||||
width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
|
||||
height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
|
||||
big: this.asReactionPicker ? isDeviceTouch : false,
|
||||
big: this.asReactionPicker ? isTouchUsing : false,
|
||||
customEmojiCategories: emojiCategories,
|
||||
customEmojis: this.$instance.emojis,
|
||||
q: null,
|
||||
@@ -268,7 +268,7 @@ export default defineComponent({
|
||||
|
||||
methods: {
|
||||
focus() {
|
||||
if (!isMobile && !isDeviceTouch) {
|
||||
if (!isMobile && !isTouchUsing) {
|
||||
this.$refs.search.focus({
|
||||
preventScroll: true
|
||||
});
|
||||
|
@@ -6,9 +6,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { computed, defineComponent, ref, watch } from 'vue';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { twemojiSvgBase } from '@/scripts/twemoji-base';
|
||||
import { defaultStore } from '@/store';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -35,61 +37,33 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
setup(props) {
|
||||
const isCustom = computed(() => props.emoji.startsWith(':'));
|
||||
const char = computed(() => isCustom.value ? null : props.emoji);
|
||||
const useOsNativeEmojis = computed(() => defaultStore.state.useOsNativeEmojis && !props.isReaction);
|
||||
const ce = computed(() => props.customEmojis || instance.emojis || []);
|
||||
const customEmoji = computed(() => isCustom.value ? ce.value.find(x => x.name === props.emoji.substr(1, props.emoji.length - 2)) : null);
|
||||
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');
|
||||
codes = codes.filter(x => x && x.length);
|
||||
return `${twemojiSvgBase}/${codes.join('-')}.svg`;
|
||||
} else {
|
||||
return defaultStore.state.disableShowingAnimatedImages
|
||||
? getStaticImageUrl(customEmoji.value.url)
|
||||
: customEmoji.value.url;
|
||||
}
|
||||
});
|
||||
const alt = computed(() => customEmoji.value ? `:${customEmoji.value.name}:` : char.value);
|
||||
|
||||
return {
|
||||
url: null,
|
||||
char: null,
|
||||
customEmoji: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isCustom(): boolean {
|
||||
return this.emoji.startsWith(':');
|
||||
},
|
||||
|
||||
alt(): string {
|
||||
return this.customEmoji ? `:${this.customEmoji.name}:` : this.char;
|
||||
},
|
||||
|
||||
useOsNativeEmojis(): boolean {
|
||||
return this.$store.state.useOsNativeEmojis && !this.isReaction;
|
||||
},
|
||||
|
||||
ce() {
|
||||
return this.customEmojis || this.$instance?.emojis || [];
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
ce: {
|
||||
handler() {
|
||||
if (this.isCustom) {
|
||||
const customEmoji = this.ce.find(x => x.name === this.emoji.substr(1, this.emoji.length - 2));
|
||||
if (customEmoji) {
|
||||
this.customEmoji = customEmoji;
|
||||
this.url = this.$store.state.disableShowingAnimatedImages
|
||||
? getStaticImageUrl(customEmoji.url)
|
||||
: customEmoji.url;
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
if (!this.isCustom) {
|
||||
this.char = this.emoji;
|
||||
}
|
||||
|
||||
if (this.char) {
|
||||
let codes = Array.from(this.char).map(x => x.codePointAt(0).toString(16));
|
||||
if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
|
||||
codes = codes.filter(x => x && x.length);
|
||||
|
||||
this.url = `${twemojiSvgBase}/${codes.join('-')}.svg`;
|
||||
}
|
||||
url,
|
||||
char,
|
||||
alt,
|
||||
customEmoji,
|
||||
useOsNativeEmojis,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@@ -23,7 +23,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { toUnicode as decodePunycode } from 'punycode/';
|
||||
import { url as local } from '@/config';
|
||||
import { isDeviceTouch } from '@/scripts/is-device-touch';
|
||||
import { isTouchUsing } from '@/scripts/touch';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -91,13 +91,13 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
onMouseover() {
|
||||
if (isDeviceTouch) return;
|
||||
if (isTouchUsing) return;
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.showTimer = setTimeout(this.showPreview, 500);
|
||||
},
|
||||
onMouseleave() {
|
||||
if (isDeviceTouch) return;
|
||||
if (isTouchUsing) return;
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.hideTimer = setTimeout(this.closePreview, 500);
|
||||
|
@@ -12,7 +12,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { url as local } from '@/config';
|
||||
import { isDeviceTouch } from '@/scripts/is-device-touch';
|
||||
import { isTouchUsing } from '@/scripts/touch';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -65,13 +65,13 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
onMouseover() {
|
||||
if (isDeviceTouch) return;
|
||||
if (isTouchUsing) return;
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.showTimer = setTimeout(this.showPreview, 500);
|
||||
},
|
||||
onMouseleave() {
|
||||
if (isDeviceTouch) return;
|
||||
if (isTouchUsing) return;
|
||||
clearTimeout(this.showTimer);
|
||||
clearTimeout(this.hideTimer);
|
||||
this.hideTimer = setTimeout(this.closePreview, 500);
|
||||
|
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="gqnyydlz" :style="{ background: color }">
|
||||
<div v-else class="gqnyydlz">
|
||||
<a
|
||||
:href="image.url"
|
||||
:title="image.name"
|
||||
@@ -16,15 +16,13 @@
|
||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment" :title="image.comment" :cover="false"/>
|
||||
<div v-if="image.type === 'image/gif'" class="gif">GIF</div>
|
||||
</a>
|
||||
<i class="fas fa-eye-slash" @click="hide = true"></i>
|
||||
<button v-tooltip="$ts.hide" class="_button hide" @click="hide = true"><i class="fas fa-eye-slash"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
|
||||
import ImageViewer from './image-viewer.vue';
|
||||
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
@@ -44,7 +42,6 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
hide: true,
|
||||
color: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -64,9 +61,6 @@ export default defineComponent({
|
||||
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
||||
this.$watch('image', () => {
|
||||
this.hide = (this.$store.state.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.nsfw !== 'ignore');
|
||||
if (this.image.blurhash) {
|
||||
this.color = extractAvgColorFromBlurhash(this.image.blurhash);
|
||||
}
|
||||
}, {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
@@ -109,21 +103,26 @@ export default defineComponent({
|
||||
|
||||
.gqnyydlz {
|
||||
position: relative;
|
||||
border: solid 0.5px var(--divider);
|
||||
//box-shadow: 0 0 0 1px var(--divider) inset;
|
||||
background: var(--bg);
|
||||
|
||||
> i {
|
||||
> .hide {
|
||||
display: block;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
background-color: var(--fg);
|
||||
color: var(--accentLighten);
|
||||
font-size: 14px;
|
||||
opacity: .5;
|
||||
padding: 3px 6px;
|
||||
background-color: var(--accentedBg);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
color: var(--accent);
|
||||
font-size: 0.8em;
|
||||
padding: 6px 8px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
|
||||
> i {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
> a {
|
||||
|
@@ -130,7 +130,7 @@ export default defineComponent({
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
grid-gap: 4px;
|
||||
grid-gap: 8px;
|
||||
|
||||
> * {
|
||||
overflow: hidden;
|
||||
|
@@ -19,10 +19,6 @@
|
||||
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
||||
:custom-emojis="notification.note.emojis"
|
||||
:no-style="true"
|
||||
@touchstart.passive="onReactionMouseover"
|
||||
@mouseover="onReactionMouseover"
|
||||
@mouseleave="onReactionMouseleave"
|
||||
@touchend="onReactionMouseleave"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,7 +151,7 @@ export default defineComponent({
|
||||
os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id });
|
||||
};
|
||||
|
||||
const { onMouseover: onReactionMouseover, onMouseleave: onReactionMouseleave } = useTooltip((showing) => {
|
||||
useTooltip(reactionRef, (showing) => {
|
||||
os.popup(XReactionTooltip, {
|
||||
showing,
|
||||
reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
|
||||
@@ -174,8 +170,6 @@ export default defineComponent({
|
||||
rejectFollowRequest,
|
||||
acceptGroupInvitation,
|
||||
rejectGroupInvitation,
|
||||
onReactionMouseover,
|
||||
onReactionMouseleave,
|
||||
elRef,
|
||||
reactionRef,
|
||||
};
|
||||
|
@@ -74,7 +74,7 @@ import { formatTimeString } from '@/scripts/format-time-string';
|
||||
import { Autocomplete } from '@/scripts/autocomplete';
|
||||
import { noteVisibilities } from 'misskey-js';
|
||||
import * as os from '@/os';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import { selectFiles } from '@/scripts/select-file';
|
||||
import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
|
||||
import { throttle } from 'throttle-debounce';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
@@ -456,7 +456,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
chooseFileFrom(ev) {
|
||||
selectFile(ev.currentTarget || ev.target, this.$ts.attachFile, true).then(files => {
|
||||
selectFiles(ev.currentTarget || ev.target, this.$ts.attachFile).then(files => {
|
||||
for (const file of files) {
|
||||
this.files.push(file);
|
||||
}
|
||||
|
@@ -6,10 +6,6 @@
|
||||
class="hkzvhatu _button"
|
||||
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
||||
@click="toggleReaction()"
|
||||
@touchstart.passive="onMouseover"
|
||||
@mouseover="onMouseover"
|
||||
@mouseleave="onMouseleave"
|
||||
@touchend="onMouseleave"
|
||||
>
|
||||
<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
|
||||
<span>{{ count }}</span>
|
||||
@@ -90,7 +86,7 @@ export default defineComponent({
|
||||
if (!props.isInitial) anime();
|
||||
});
|
||||
|
||||
const { onMouseover, onMouseleave } = useTooltip(async (showing) => {
|
||||
useTooltip(buttonRef, async (showing) => {
|
||||
const reactions = await os.api('notes/reactions', {
|
||||
noteId: props.note.id,
|
||||
type: props.reaction,
|
||||
@@ -113,8 +109,6 @@ export default defineComponent({
|
||||
buttonRef,
|
||||
canToggle,
|
||||
toggleReaction,
|
||||
onMouseover,
|
||||
onMouseleave,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@@ -3,10 +3,6 @@
|
||||
ref="buttonRef"
|
||||
class="eddddedb _button canRenote"
|
||||
@click="renote()"
|
||||
@touchstart.passive="onMouseover"
|
||||
@mouseover="onMouseover"
|
||||
@mouseleave="onMouseleave"
|
||||
@touchend="onMouseleave"
|
||||
>
|
||||
<i class="fas fa-retweet"></i>
|
||||
<p v-if="count > 0" class="count">{{ count }}</p>
|
||||
@@ -42,7 +38,7 @@ export default defineComponent({
|
||||
|
||||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
|
||||
|
||||
const { onMouseover, onMouseleave } = useTooltip(async (showing) => {
|
||||
useTooltip(buttonRef, async (showing) => {
|
||||
const renotes = await os.api('notes/renotes', {
|
||||
noteId: props.note.id,
|
||||
limit: 11
|
||||
@@ -87,8 +83,6 @@ export default defineComponent({
|
||||
buttonRef,
|
||||
canRenote,
|
||||
renote,
|
||||
onMouseover,
|
||||
onMouseleave,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="$emit('closed')" @enter="$emit('opening')">
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" ref="content" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" ref="content" class="ccczpooj" :class="{ fixed, top: position === 'top' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<slot :max-height="maxHeight" :close="close"></slot>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick, onMounted, onUnmounted, PropType, ref, watch } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
function getFixedContainer(el: Element | null | undefined): Element | null {
|
||||
if (el == null || el.tagName === 'BODY') return null;
|
||||
@@ -57,6 +58,7 @@ export default defineComponent({
|
||||
const transformOrigin = ref('center');
|
||||
const showing = ref(true);
|
||||
const content = ref<HTMLElement>();
|
||||
const zIndex = os.claimZIndex(props.front);
|
||||
|
||||
const close = () => {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
@@ -204,6 +206,7 @@ export default defineComponent({
|
||||
transformOrigin,
|
||||
maxHeight,
|
||||
close,
|
||||
zIndex,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -226,14 +229,9 @@ export default defineComponent({
|
||||
|
||||
.ccczpooj {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
&.front {
|
||||
z-index: 20000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -53,6 +53,7 @@ export default defineComponent({
|
||||
> .title {
|
||||
opacity: 0.7;
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
> .items {
|
||||
@@ -63,6 +64,7 @@ export default defineComponent({
|
||||
box-sizing: border-box;
|
||||
padding: 10px 16px 10px 8px;
|
||||
border-radius: 9px;
|
||||
font-size: 0.95em;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<transition name="tooltip" appear @after-leave="$emit('closed')">
|
||||
<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ maxWidth: maxWidth + 'px' }">
|
||||
<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
|
||||
<slot>{{ text }}</slot>
|
||||
</div>
|
||||
</transition>
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -33,6 +34,7 @@ export default defineComponent({
|
||||
|
||||
setup(props, context) {
|
||||
const el = ref<HTMLElement>();
|
||||
const zIndex = os.claimZIndex(true);
|
||||
|
||||
const setPosition = () => {
|
||||
if (el.value == null) return;
|
||||
@@ -88,6 +90,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
el,
|
||||
zIndex,
|
||||
};
|
||||
},
|
||||
})
|
||||
@@ -108,7 +111,6 @@ export default defineComponent({
|
||||
|
||||
.buebdbiu {
|
||||
position: absolute;
|
||||
z-index: 11000;
|
||||
font-size: 0.8em;
|
||||
padding: 8px 12px;
|
||||
box-sizing: border-box;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
|
||||
<div v-if="showing" class="ebkgocck" :class="{ front }">
|
||||
<div v-if="showing" class="ebkgocck">
|
||||
<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
|
||||
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
|
||||
<span class="left">
|
||||
@@ -124,10 +124,6 @@ export default defineComponent({
|
||||
this.applyTransformTop((window.innerHeight / 2) - (this.$el.offsetHeight / 2));
|
||||
this.applyTransformLeft((window.innerWidth / 2) - (this.$el.offsetWidth / 2));
|
||||
|
||||
os.windows.set(this.id, {
|
||||
z: Number(document.defaultView.getComputedStyle(this.$el, null).zIndex)
|
||||
});
|
||||
|
||||
// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
|
||||
this.top();
|
||||
|
||||
@@ -135,7 +131,6 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
os.windows.delete(this.id);
|
||||
window.removeEventListener('resize', this.onBrowserResize);
|
||||
},
|
||||
|
||||
@@ -160,17 +155,7 @@ export default defineComponent({
|
||||
|
||||
// 最前面へ移動
|
||||
top() {
|
||||
let z = 0;
|
||||
const ws = Array.from(os.windows.entries()).filter(([k, v]) => k !== this.id).map(([k, v]) => v);
|
||||
for (const w of ws) {
|
||||
if (w.z > z) z = w.z;
|
||||
}
|
||||
if (z > 0) {
|
||||
(this.$el as any).style.zIndex = z + 1;
|
||||
os.windows.set(this.id, {
|
||||
z: z + 1
|
||||
});
|
||||
}
|
||||
(this.$el as any).style.zIndex = os.claimZIndex(this.front);
|
||||
},
|
||||
|
||||
onBodyMousedown() {
|
||||
@@ -394,11 +379,6 @@ export default defineComponent({
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10000; // mk-modalのと同じでなければならない
|
||||
|
||||
&.front {
|
||||
z-index: 11000; // front指定の時は、mk-modalのよりも大きくなければならない
|
||||
}
|
||||
|
||||
> .body {
|
||||
overflow: hidden;
|
||||
|
@@ -2,11 +2,11 @@
|
||||
// ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明
|
||||
|
||||
import { Directive, ref } from 'vue';
|
||||
import { isDeviceTouch } from '@/scripts/is-device-touch';
|
||||
import { isTouchUsing } from '@/scripts/touch';
|
||||
import { popup, alert } from '@/os';
|
||||
|
||||
const start = isDeviceTouch ? 'touchstart' : 'mouseover';
|
||||
const end = isDeviceTouch ? 'touchend' : 'mouseleave';
|
||||
const start = isTouchUsing ? 'touchstart' : 'mouseover';
|
||||
const end = isTouchUsing ? 'touchend' : 'mouseleave';
|
||||
const delay = 100;
|
||||
|
||||
export default {
|
||||
|
@@ -12,24 +12,12 @@ import { resolve } from '@/router';
|
||||
import { $i } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
export let isScreenTouching = false;
|
||||
|
||||
window.addEventListener('touchstart', () => {
|
||||
isScreenTouching = true;
|
||||
}, { passive: true });
|
||||
|
||||
window.addEventListener('touchend', () => {
|
||||
isScreenTouching = false;
|
||||
}, { passive: true });
|
||||
|
||||
export const stream = markRaw(new Misskey.Stream(url, $i));
|
||||
|
||||
export const pendingApiRequestsCount = ref(0);
|
||||
let apiRequestsCount = 0; // for debug
|
||||
export const apiRequests = ref([]); // for debug
|
||||
|
||||
export const windows = new Map();
|
||||
|
||||
const apiClient = new Misskey.api.APIClient({
|
||||
origin: url,
|
||||
});
|
||||
@@ -174,6 +162,18 @@ export const popups = ref([]) as Ref<{
|
||||
props: Record<string, any>;
|
||||
}[]>;
|
||||
|
||||
let popupZIndex = 1000000;
|
||||
let popupZIndexForFront = 2000000;
|
||||
export function claimZIndex(front = false): number {
|
||||
if (front) {
|
||||
popupZIndexForFront += 100;
|
||||
return popupZIndexForFront;
|
||||
} else {
|
||||
popupZIndex += 100;
|
||||
return popupZIndex;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -182,7 +182,6 @@ export async function popup(component: Component | typeof import('*.vue') | Prom
|
||||
|
||||
const id = ++popupIdCount;
|
||||
const dispose = () => {
|
||||
if (_DEV_) console.log('os:popup close', id, component, props, events);
|
||||
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
|
||||
setTimeout(() => {
|
||||
popups.value = popups.value.filter(popup => popup.id !== id);
|
||||
@@ -198,7 +197,6 @@ export async function popup(component: Component | typeof import('*.vue') | Prom
|
||||
id,
|
||||
};
|
||||
|
||||
if (_DEV_) console.log('os:popup open', id, component, props, events);
|
||||
popups.value.push(state);
|
||||
|
||||
return {
|
||||
|
@@ -67,60 +67,82 @@ import { physics } from '@/scripts/physics';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
const patrons = [
|
||||
'Satsuki Yanagi',
|
||||
'noellabo',
|
||||
'まっちゃとーにゅ',
|
||||
'mametsuko',
|
||||
'noellabo',
|
||||
'AureoleArk',
|
||||
'Gargron',
|
||||
'Nokotaro Takeda',
|
||||
'Suji Yan',
|
||||
'Hekovic',
|
||||
'Gitmo Life Services',
|
||||
'nenohi',
|
||||
'naga_rus',
|
||||
'Melilot',
|
||||
'Efertone',
|
||||
'oi_yekssim',
|
||||
'nanami kan',
|
||||
'regtan',
|
||||
'Hekovic',
|
||||
'nenohi',
|
||||
'Gitmo Life Services',
|
||||
'naga_rus',
|
||||
'Efertone',
|
||||
'Melilot',
|
||||
'motcha',
|
||||
'dansup',
|
||||
'nanami kan',
|
||||
'sevvie Rose',
|
||||
'Hayato Ishikawa',
|
||||
'Puniko',
|
||||
'skehmatics',
|
||||
'Quinton Macejkovic',
|
||||
'YUKIMOCHI',
|
||||
'dansup',
|
||||
'mewl hayabusa',
|
||||
'Emilis',
|
||||
'Fristi',
|
||||
'makokunsan',
|
||||
'chidori ninokura',
|
||||
'Peter G.',
|
||||
'Nesakko',
|
||||
'regtan',
|
||||
'見当かなみ',
|
||||
'natalie',
|
||||
'Jerry',
|
||||
'Maronu',
|
||||
'Steffen K9',
|
||||
'takimura',
|
||||
'sikyosyounin',
|
||||
'Nesakko',
|
||||
'YuzuRyo61',
|
||||
'blackskye',
|
||||
'sheeta.s',
|
||||
'osapon',
|
||||
'mkatze',
|
||||
'public_yusuke',
|
||||
'CG',
|
||||
'吴浥',
|
||||
't_w',
|
||||
'Jerry',
|
||||
'nafuchoco',
|
||||
'Takumi Sugita',
|
||||
'chidori ninokura',
|
||||
'mydarkstar',
|
||||
'kiritan',
|
||||
'GLaTAN',
|
||||
'mkatze',
|
||||
'kabo2468y',
|
||||
'weepjp',
|
||||
'Liaizon Wakest',
|
||||
'Steffen K9',
|
||||
'mydarkstar',
|
||||
'Roujo',
|
||||
'DignifiedSilence',
|
||||
'uroco @99',
|
||||
'totokoro',
|
||||
'public_yusuke',
|
||||
'うし',
|
||||
'kiritan',
|
||||
'weepjp',
|
||||
'Liaizon Wakest',
|
||||
'Duponin',
|
||||
'Blue',
|
||||
'Naoki Hirayama',
|
||||
'wara',
|
||||
'S Y',
|
||||
'Wataru Manji (manji0)',
|
||||
'みなしま',
|
||||
'kanoy',
|
||||
'xianon',
|
||||
'Denshi',
|
||||
'Osushimaru',
|
||||
'吴浥',
|
||||
'DignifiedSilence',
|
||||
't_w',
|
||||
'にょんへら',
|
||||
'おのだい',
|
||||
'Leni',
|
||||
'oss',
|
||||
'Weeble',
|
||||
'蝉暮せせせ',
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
|
@@ -1,19 +1,20 @@
|
||||
<template>
|
||||
<div class="uqshojas">
|
||||
<section v-for="ad in ads" class="_card _gap ads">
|
||||
<div class="_content ad">
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="uqshojas">
|
||||
<div v-for="ad in ads" class="_panel _formRoot ad">
|
||||
<MkAd v-if="ad.url" :specify="ad"/>
|
||||
<MkInput v-model="ad.url" type="url">
|
||||
<MkInput v-model="ad.url" type="url" class="_formBlock">
|
||||
<template #label>URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.imageUrl">
|
||||
<MkInput v-model="ad.imageUrl" class="_formBlock">
|
||||
<template #label>{{ $ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<div style="margin: 32px 0;">
|
||||
<MkRadio v-model="ad.place" value="square">square</MkRadio>
|
||||
<MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
|
||||
<MkRadio v-model="ad.place" value="horizontal-big">horizontal-big</MkRadio>
|
||||
</div>
|
||||
<FormRadios v-model="ad.place" class="_formBlock">
|
||||
<template #label>Form</template>
|
||||
<option value="square">square</option>
|
||||
<option value="horizontal">horizontal</option>
|
||||
<option value="horizontal-big">horizontal-big</option>
|
||||
</FormRadios>
|
||||
<!--
|
||||
<div style="margin: 32px 0;">
|
||||
{{ $ts.priority }}
|
||||
@@ -22,22 +23,24 @@
|
||||
<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
|
||||
</div>
|
||||
-->
|
||||
<MkInput v-model="ad.ratio" type="number">
|
||||
<template #label>{{ $ts.ratio }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.expiresAt" type="date">
|
||||
<template #label>{{ $ts.expiration }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="ad.memo">
|
||||
<div class="_inputSplit">
|
||||
<MkInput v-model="ad.ratio" type="number">
|
||||
<template #label>{{ $ts.ratio }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="ad.expiresAt" type="date">
|
||||
<template #label>{{ $ts.expiration }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<MkTextarea v-model="ad.memo" class="_formBlock">
|
||||
<template #label>{{ $ts.memo }}</template>
|
||||
</MkTextarea>
|
||||
<div class="buttons">
|
||||
<MkButton class="button" inline primary @click="save(ad)"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -45,7 +48,7 @@ import { defineComponent } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkRadio from '@/components/form/radio.vue';
|
||||
import FormRadios from '@/components/form/radios.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
@@ -54,7 +57,7 @@ export default defineComponent({
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkRadio,
|
||||
FormRadios,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
@@ -132,6 +135,12 @@ export default defineComponent({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.uqshojas {
|
||||
margin: var(--margin);
|
||||
> .ad {
|
||||
padding: 32px;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: var(--margin);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,50 +1,54 @@
|
||||
<template>
|
||||
<div class="ogwlenmc">
|
||||
<div v-if="tab === 'local'" class="local">
|
||||
<MkInput v-model="query" :debounce="true" type="search" style="margin: var(--margin);">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkPagination ref="emojis" :pagination="pagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template v-slot="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.category }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
<MkSpacer :content-max="900">
|
||||
<div class="ogwlenmc">
|
||||
<div v-if="tab === 'local'" class="local">
|
||||
<MkInput v-model="query" :debounce="true" type="search">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkPagination ref="emojis" :pagination="pagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template v-slot="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.category }}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
|
||||
<div v-else-if="tab === 'remote'" class="remote">
|
||||
<MkInput v-model="queryRemote" :debounce="true" type="search" style="margin: var(--margin);">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="host" :debounce="true" style="margin: var(--margin);">
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
<MkPagination ref="remoteEmojis" :pagination="remotePagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template v-slot="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.host }}</div>
|
||||
<div v-else-if="tab === 'remote'" class="remote">
|
||||
<div class="_inputSplit">
|
||||
<MkInput v-model="queryRemote" :debounce="true" type="search">
|
||||
<template #prefix><i class="fas fa-search"></i></template>
|
||||
<template #label>{{ $ts.search }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="host" :debounce="true">
|
||||
<template #label>{{ $ts.host }}</template>
|
||||
</MkInput>
|
||||
</div>
|
||||
<MkPagination ref="remoteEmojis" :pagination="remotePagination">
|
||||
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
|
||||
<template v-slot="{items}">
|
||||
<div class="ldhfsamy">
|
||||
<div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)">
|
||||
<img :src="emoji.url" class="img" :alt="emoji.name"/>
|
||||
<div class="body">
|
||||
<div class="name _monospace">{{ emoji.name }}</div>
|
||||
<div class="info">{{ emoji.host }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -53,7 +57,7 @@ import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import { selectFiles } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
@@ -78,6 +82,9 @@ export default defineComponent({
|
||||
icon: 'fas fa-plus',
|
||||
text: this.$ts.addEmoji,
|
||||
handler: this.add,
|
||||
}, {
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
handler: this.menu,
|
||||
}],
|
||||
tabs: [{
|
||||
active: this.tab === 'local',
|
||||
@@ -117,7 +124,7 @@ export default defineComponent({
|
||||
|
||||
methods: {
|
||||
async add(e) {
|
||||
const files = await selectFile(e.currentTarget || e.target, null, true);
|
||||
const files = await selectFiles(e.currentTarget || e.target, null);
|
||||
|
||||
const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
|
||||
fileId: file.id,
|
||||
@@ -160,6 +167,28 @@ export default defineComponent({
|
||||
icon: 'fas fa-plus',
|
||||
action: () => { this.im(emoji) }
|
||||
}], ev.currentTarget || ev.target);
|
||||
},
|
||||
|
||||
menu(ev) {
|
||||
os.popupMenu([{
|
||||
icon: 'fas fa-download',
|
||||
text: this.$ts.export,
|
||||
action: async () => {
|
||||
os.api('export-custom-emojis', {
|
||||
})
|
||||
.then(() => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: this.$ts.exportRequested,
|
||||
});
|
||||
}).catch((e) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
}], ev.currentTarget || ev.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -168,15 +197,15 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
.ogwlenmc {
|
||||
> .local {
|
||||
.empty {
|
||||
margin: var(--margin);
|
||||
.empty {
|
||||
margin: var(--margin);
|
||||
}
|
||||
|
||||
.ldhfsamy {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin);
|
||||
margin: var(--margin) 0;
|
||||
|
||||
> .emoji {
|
||||
display: flex;
|
||||
@@ -214,15 +243,15 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
> .remote {
|
||||
.empty {
|
||||
margin: var(--margin);
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin: var(--margin);
|
||||
}
|
||||
|
||||
.ldhfsamy {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
grid-gap: 12px;
|
||||
margin: var(--margin);
|
||||
margin: var(--margin) 0;
|
||||
|
||||
> .emoji {
|
||||
display: flex;
|
||||
|
@@ -6,7 +6,7 @@
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableDiscordIntegration">
|
||||
<FormInfo>Callback URL: {{ `${url}/api/dc/cb` }}</FormInfo>
|
||||
<FormInfo>Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo>
|
||||
|
||||
<FormInput v-model="discordClientId">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
@@ -67,6 +67,7 @@ export default defineComponent({
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('meta', { detail: true });
|
||||
this.uri = meta.uri;
|
||||
this.enableDiscordIntegration = meta.enableDiscordIntegration;
|
||||
this.discordClientId = meta.discordClientId;
|
||||
this.discordClientSecret = meta.discordClientSecret;
|
||||
|
@@ -6,7 +6,7 @@
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableGithubIntegration">
|
||||
<FormInfo>Callback URL: {{ `${url}/api/gh/cb` }}</FormInfo>
|
||||
<FormInfo>Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo>
|
||||
|
||||
<FormInput v-model="githubClientId">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
@@ -67,6 +67,7 @@ export default defineComponent({
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('meta', { detail: true });
|
||||
this.uri = meta.uri;
|
||||
this.enableGithubIntegration = meta.enableGithubIntegration;
|
||||
this.githubClientId = meta.githubClientId;
|
||||
this.githubClientSecret = meta.githubClientSecret;
|
||||
|
@@ -6,7 +6,7 @@
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableTwitterIntegration">
|
||||
<FormInfo>Callback URL: {{ `${url}/api/tw/cb` }}</FormInfo>
|
||||
<FormInfo>Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo>
|
||||
|
||||
<FormInput v-model="twitterConsumerKey">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
@@ -67,6 +67,7 @@ export default defineComponent({
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('meta', { detail: true });
|
||||
this.uri = meta.uri;
|
||||
this.enableTwitterIntegration = meta.enableTwitterIntegration;
|
||||
this.twitterConsumerKey = meta.twitterConsumerKey;
|
||||
this.twitterConsumerSecret = meta.twitterConsumerSecret;
|
||||
|
@@ -67,7 +67,7 @@ export default defineComponent({
|
||||
send() {
|
||||
this.sending = true;
|
||||
const body = JSON5.parse(this.body);
|
||||
os.api(this.endpoint, body, body.i || this.withCredential ? undefined : null).then(res => {
|
||||
os.api(this.endpoint, body, body.i || (this.withCredential ? undefined : null)).then(res => {
|
||||
this.sending = false;
|
||||
this.res = JSON5.stringify(res, null, 2);
|
||||
}, err => {
|
||||
|
@@ -112,7 +112,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
setBannerImage(e) {
|
||||
selectFile(e.currentTarget || e.target, null, false).then(file => {
|
||||
selectFile(e.currentTarget || e.target, null).then(file => {
|
||||
this.bannerId = file.id;
|
||||
});
|
||||
},
|
||||
|
@@ -1,16 +1,18 @@
|
||||
<template>
|
||||
<div v-if="clip" class="_section">
|
||||
<div class="okzinsic _content _panel _gap">
|
||||
<div v-if="clip.description" class="description">
|
||||
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
|
||||
<MkSpacer :content-max="800">
|
||||
<div v-if="clip">
|
||||
<div class="okzinsic _panel">
|
||||
<div v-if="clip.description" class="description">
|
||||
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="clip.user" class="avatar" :show-indicator="true"/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<XNotes class="_content _gap" :pagination="pagination" :detail="true"/>
|
||||
</div>
|
||||
<XNotes :pagination="pagination" :detail="true"/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -40,10 +42,11 @@ export default defineComponent({
|
||||
[symbols.PAGE_INFO]: computed(() => this.clip ? {
|
||||
title: this.clip.name,
|
||||
icon: 'fas fa-paperclip',
|
||||
action: {
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
handler: this.menu
|
||||
}
|
||||
}],
|
||||
} : null),
|
||||
clip: null,
|
||||
pagination: {
|
||||
@@ -133,6 +136,7 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
.okzinsic {
|
||||
position: relative;
|
||||
margin-bottom: var(--margin);
|
||||
|
||||
> .description {
|
||||
padding: 16px;
|
||||
|
@@ -20,6 +20,8 @@ export default defineComponent({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: computed(() => this.folder ? this.folder.name : this.$ts.drive),
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
hideHeader: true,
|
||||
},
|
||||
folder: null,
|
||||
};
|
||||
|
@@ -21,10 +21,38 @@ export default defineComponent({
|
||||
title: this.$ts.customEmojis,
|
||||
icon: 'fas fa-laugh',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
handler: this.menu
|
||||
}],
|
||||
})),
|
||||
tab: 'category',
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
menu(ev) {
|
||||
os.popupMenu([{
|
||||
icon: 'fas fa-download',
|
||||
text: this.$ts.export,
|
||||
action: async () => {
|
||||
os.api('export-custom-emojis', {
|
||||
})
|
||||
.then(() => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: this.$ts.exportRequested,
|
||||
});
|
||||
}).catch((e) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
}], ev.currentTarget || ev.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -37,7 +37,7 @@ import FormTuple from '@/components/debobigego/tuple.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormSuspense from '@/components/debobigego/suspense.vue';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import { selectFiles } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
@@ -95,7 +95,7 @@ export default defineComponent({
|
||||
|
||||
methods: {
|
||||
selectFile(e) {
|
||||
selectFile(e.currentTarget || e.target, null, true).then(files => {
|
||||
selectFiles(e.currentTarget || e.target, null).then(files => {
|
||||
this.files = this.files.concat(files);
|
||||
});
|
||||
},
|
||||
|
@@ -152,7 +152,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
chooseFile(e) {
|
||||
selectFile(e.currentTarget || e.target, this.$ts.selectFile, false).then(file => {
|
||||
selectFile(e.currentTarget || e.target, this.$ts.selectFile).then(file => {
|
||||
this.file = file;
|
||||
});
|
||||
},
|
||||
|
@@ -448,7 +448,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
setEyeCatchingImage(e) {
|
||||
selectFile(e.currentTarget || e.target, null, false).then(file => {
|
||||
selectFile(e.currentTarget || e.target, null).then(file => {
|
||||
this.eyeCatchingImageId = file.id;
|
||||
});
|
||||
},
|
||||
|
@@ -210,7 +210,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
chooseImage(key, e) {
|
||||
selectFile(e.currentTarget || e.target, null, false).then(file => {
|
||||
selectFile(e.currentTarget || e.target, null).then(file => {
|
||||
room.updateProp(key, `/proxy/?${urlQuery({ url: file.thumbnailUrl })}`);
|
||||
this.$refs.preview.selected(room.getSelectedObject());
|
||||
this.changed = true;
|
||||
|
@@ -2,106 +2,158 @@
|
||||
<div class="_formRoot">
|
||||
<FormSection>
|
||||
<template #label>{{ $ts._exportOrImport.allNotes }}</template>
|
||||
<MkButton :class="$style.button" inline @click="doExport('notes')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="exportNotes()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ $ts._exportOrImport.followingList }}</template>
|
||||
<MkButton :class="$style.button" inline @click="doExport('following')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="doImport('following', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
<FormGroup>
|
||||
<FormSwitch v-model="excludeMutingUsers" class="_formBlock">
|
||||
{{ $ts._exportOrImport.excludeMutingUsers }}
|
||||
</FormSwitch>
|
||||
<FormSwitch v-model="excludeInactiveUsers" class="_formBlock">
|
||||
{{ $ts._exportOrImport.excludeInactiveUsers }}
|
||||
</FormSwitch>
|
||||
<MkButton :class="$style.button" inline @click="exportFollowing()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<MkButton :class="$style.button" inline @click="importFollowing($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
</FormGroup>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ $ts._exportOrImport.userLists }}</template>
|
||||
<MkButton :class="$style.button" inline @click="doExport('user-lists')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="doImport('user-lists', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="exportUserLists()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="importUserLists($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ $ts._exportOrImport.muteList }}</template>
|
||||
<MkButton :class="$style.button" inline @click="doExport('muting')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="doImport('muting', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="exportMuting()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="importMuting($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<template #label>{{ $ts._exportOrImport.blockingList }}</template>
|
||||
<MkButton :class="$style.button" inline @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="doImport('blocking', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="exportBlocking()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
|
||||
<MkButton :class="$style.button" inline @click="importBlocking($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
|
||||
</FormSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, onMounted, ref } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import * as os from '@/os';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSection,
|
||||
FormGroup,
|
||||
FormSwitch,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
setup(props, context) {
|
||||
const INFO = {
|
||||
title: i18n.locale.importAndExport,
|
||||
icon: 'fas fa-boxes',
|
||||
bg: 'var(--bg)',
|
||||
};
|
||||
|
||||
const excludeMutingUsers = ref(false);
|
||||
const excludeInactiveUsers = ref(false);
|
||||
|
||||
const onExportSuccess = () => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.locale.exportRequested,
|
||||
});
|
||||
};
|
||||
|
||||
const onImportSuccess = () => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: i18n.locale.importRequested,
|
||||
});
|
||||
};
|
||||
|
||||
const onError = (e) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e.message,
|
||||
});
|
||||
};
|
||||
|
||||
const exportNotes = () => {
|
||||
os.api('i/export-notes', {}).then(onExportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
const exportFollowing = () => {
|
||||
os.api('i/export-following', {
|
||||
excludeMuting: excludeMutingUsers.value,
|
||||
excludeInactive: excludeInactiveUsers.value,
|
||||
})
|
||||
.then(onExportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
const exportBlocking = () => {
|
||||
os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
const exportUserLists = () => {
|
||||
os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
const exportMuting = () => {
|
||||
os.api('i/export-mute', {}).then(onExportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
const importFollowing = async (ev) => {
|
||||
const file = await selectFile(ev.currentTarget || ev.target);
|
||||
os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
const importUserLists = async (ev) => {
|
||||
const file = await selectFile(ev.currentTarget || ev.target);
|
||||
os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
const importMuting = async (ev) => {
|
||||
const file = await selectFile(ev.currentTarget || ev.target);
|
||||
os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
const importBlocking = async (ev) => {
|
||||
const file = await selectFile(ev.currentTarget || ev.target);
|
||||
os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
context.emit('info', INFO);
|
||||
});
|
||||
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.importAndExport,
|
||||
icon: 'fas fa-boxes',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
}
|
||||
[symbols.PAGE_INFO]: INFO,
|
||||
excludeMutingUsers,
|
||||
excludeInactiveUsers,
|
||||
|
||||
exportNotes,
|
||||
exportFollowing,
|
||||
exportBlocking,
|
||||
exportUserLists,
|
||||
exportMuting,
|
||||
|
||||
importFollowing,
|
||||
importUserLists,
|
||||
importMuting,
|
||||
importBlocking,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
doExport(target) {
|
||||
os.api(
|
||||
target === 'notes' ? 'i/export-notes' :
|
||||
target === 'following' ? 'i/export-following' :
|
||||
target === 'blocking' ? 'i/export-blocking' :
|
||||
target === 'user-lists' ? 'i/export-user-lists' :
|
||||
target === 'muting' ? 'i/export-mute' :
|
||||
null, {})
|
||||
.then(() => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: this.$ts.exportRequested
|
||||
});
|
||||
}).catch((e: any) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async doImport(target, e) {
|
||||
const file = await selectFile(e.currentTarget || e.target);
|
||||
|
||||
os.api(
|
||||
target === 'following' ? 'i/import-following' :
|
||||
target === 'user-lists' ? 'i/import-user-lists' :
|
||||
target === 'muting' ? 'i/import-muting' :
|
||||
target === 'blocking' ? 'i/import-blocking' :
|
||||
null, {
|
||||
fileId: file.id
|
||||
}).then(() => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: this.$ts.importRequested
|
||||
});
|
||||
}).catch((e: any) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -1,23 +1,25 @@
|
||||
<template>
|
||||
<div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
|
||||
<div v-if="!narrow || page == null" class="nav">
|
||||
<MkSpacer :content-max="700" :margin-min="20">
|
||||
<div class="baaadecd">
|
||||
<div class="title">{{ $ts.settings }}</div>
|
||||
<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
|
||||
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
|
||||
<MkSpacer :content-max="900" :margin-min="20" :margin-max="32">
|
||||
<div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
|
||||
<div class="header">
|
||||
<div class="title">{{ $ts.settings }}</div>
|
||||
<div v-if="childInfo" class="subtitle">{{ childInfo.title }}</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div v-if="!narrow || page == null" class="nav">
|
||||
<div class="baaadecd">
|
||||
<MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
|
||||
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div class="main">
|
||||
<MkSpacer :content-max="600" :margin-min="20">
|
||||
<div class="bkzroven">
|
||||
<div v-if="childInfo" class="title">{{ childInfo.title }}</div>
|
||||
<component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/>
|
||||
<div class="main">
|
||||
<div class="bkzroven">
|
||||
<component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -136,6 +138,11 @@ export default defineComponent({
|
||||
text: i18n.locale.importAndExport,
|
||||
to: '/settings/import-export',
|
||||
active: page.value === 'import-export',
|
||||
}, {
|
||||
icon: 'fas fa-volume-mute',
|
||||
text: i18n.locale.instanceMute,
|
||||
to: '/settings/instance-mute',
|
||||
active: page.value === 'instance-mute',
|
||||
}, {
|
||||
icon: 'fas fa-ban',
|
||||
text: i18n.locale.muteAndBlock,
|
||||
@@ -190,6 +197,7 @@ export default defineComponent({
|
||||
case 'notifications': return defineAsyncComponent(() => import('./notifications.vue'));
|
||||
case 'mute-block': return defineAsyncComponent(() => import('./mute-block.vue'));
|
||||
case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue'));
|
||||
case 'instance-mute': return defineAsyncComponent(() => import('./instance-mute.vue'));
|
||||
case 'integration': return defineAsyncComponent(() => import('./integration.vue'));
|
||||
case 'security': return defineAsyncComponent(() => import('./security.vue'));
|
||||
case '2fa': return defineAsyncComponent(() => import('./2fa.vue'));
|
||||
@@ -286,66 +294,62 @@ export default defineComponent({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vvcocwet {
|
||||
> .nav {
|
||||
.baaadecd {
|
||||
> .title {
|
||||
margin: 16px;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .info {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
> .accounts {
|
||||
> .avatar {
|
||||
display: block;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 8px auto 16px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .main {
|
||||
.bkzroven {
|
||||
> .title {
|
||||
margin: 4px 0 20px 0;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.wide {
|
||||
> .header {
|
||||
display: flex;
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
margin-bottom: 24px;
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
|
||||
> .title {
|
||||
width: 34%;
|
||||
}
|
||||
|
||||
> .subtitle {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> .body {
|
||||
> .nav {
|
||||
width: 32%;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
|
||||
.baaadecd {
|
||||
> .title {
|
||||
margin: 24px 0;
|
||||
> .info {
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
> .accounts {
|
||||
> .avatar {
|
||||
display: block;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin: 8px auto 16px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
|
||||
.bkzroven {
|
||||
> .title {
|
||||
margin: 6px 0 24px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.wide {
|
||||
> .body {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
> .nav {
|
||||
width: 34%;
|
||||
padding-right: 32px;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
> .main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
72
packages/client/src/pages/settings/instance-mute.vue
Normal file
72
packages/client/src/pages/settings/instance-mute.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="_formRoot">
|
||||
<MkInfo>{{ $ts._instanceMute.title }}</MkInfo>
|
||||
<FormTextarea v-model="instanceMutes" class="_formBlock">
|
||||
<template #label>{{ $ts._instanceMute.heading }}</template>
|
||||
<template #caption>{{ $ts._instanceMute.instanceMuteDescription }}<br>{{ $ts._instanceMute.instanceMuteDescription2 }}</template>
|
||||
</FormTextarea>
|
||||
<MkButton primary :disabled="!changed" class="_formBlock" @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
FormTextarea,
|
||||
MkInfo,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.instanceMute,
|
||||
icon: 'fas fa-volume-mute'
|
||||
},
|
||||
tab: 'soft',
|
||||
instanceMutes: '',
|
||||
changed: false,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
instanceMutes: {
|
||||
handler() {
|
||||
this.changed = true;
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
|
||||
async created() {
|
||||
this.instanceMutes = this.$i.mutedInstances.join('\n');
|
||||
},
|
||||
|
||||
methods: {
|
||||
async save() {
|
||||
let mutes = this.instanceMutes.trim().split('\n').map(el => el.trim()).filter(el => el);
|
||||
await os.api('i/update', {
|
||||
mutedInstances: mutes,
|
||||
});
|
||||
this.changed = false;
|
||||
|
||||
// Refresh filtered list to signal to the user how they've been saved
|
||||
this.instanceMutes = mutes.join('\n');
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
@@ -1,42 +1,41 @@
|
||||
<template>
|
||||
<FormBase>
|
||||
<FormLink to="/settings/update">Misskey Update</FormLink>
|
||||
<div class="_formRoot">
|
||||
<FormLink to="/settings/update" class="_formBlock">Misskey Update</FormLink>
|
||||
|
||||
<FormSwitch :value="$i.injectFeaturedNote" @update:modelValue="onChangeInjectFeaturedNote">
|
||||
<FormSwitch :value="$i.injectFeaturedNote" @update:modelValue="onChangeInjectFeaturedNote" class="_formBlock">
|
||||
{{ $ts.showFeaturedNotesInTimeline }}
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="reportError">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
|
||||
<FormSwitch v-model="reportError" class="_formBlock">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
|
||||
|
||||
<FormLink to="/settings/account-info">{{ $ts.accountInfo }}</FormLink>
|
||||
<FormLink to="/settings/experimental-features">{{ $ts.experimentalFeatures }}</FormLink>
|
||||
<FormLink to="/settings/account-info" class="_formBlock">{{ $ts.accountInfo }}</FormLink>
|
||||
<FormLink to="/settings/experimental-features" class="_formBlock">{{ $ts.experimentalFeatures }}</FormLink>
|
||||
|
||||
<FormGroup>
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.developer }}</template>
|
||||
<FormSwitch v-model="debug" @update:modelValue="changeDebug">
|
||||
<FormSwitch v-model="debug" @update:modelValue="changeDebug" class="_formBlock">
|
||||
DEBUG MODE
|
||||
</FormSwitch>
|
||||
<template v-if="debug">
|
||||
<FormButton @click="taskmanager">Task Manager</FormButton>
|
||||
</template>
|
||||
</FormGroup>
|
||||
</FormSection>
|
||||
|
||||
<FormLink to="/settings/registry"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.registry }}</FormLink>
|
||||
<FormLink to="/settings/registry" class="_formBlock"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.registry }}</FormLink>
|
||||
|
||||
<FormLink to="/bios" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink>
|
||||
<FormLink to="/cli" behavior="browser"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink>
|
||||
<FormLink to="/bios" behavior="browser" class="_formBlock"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink>
|
||||
<FormLink to="/cli" behavior="browser" class="_formBlock"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink>
|
||||
|
||||
<FormLink to="/settings/delete-account"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
|
||||
</FormBase>
|
||||
<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormSelect from '@/components/form/select.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormLink from '@/components/debobigego/link.vue';
|
||||
import FormBase from '@/components/debobigego/base.vue';
|
||||
import FormGroup from '@/components/debobigego/group.vue';
|
||||
import FormButton from '@/components/debobigego/button.vue';
|
||||
import * as os from '@/os';
|
||||
import { debug } from '@/config';
|
||||
@@ -46,12 +45,11 @@ import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormBase,
|
||||
FormSelect,
|
||||
FormSection,
|
||||
FormSwitch,
|
||||
FormButton,
|
||||
FormLink,
|
||||
FormGroup,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
@@ -188,7 +188,7 @@ export default defineComponent({
|
||||
themesCount,
|
||||
wallpaper,
|
||||
setWallpaper(e) {
|
||||
selectFile(e.currentTarget || e.target, null, false).then(file => {
|
||||
selectFile(e.currentTarget || e.target, null).then(file => {
|
||||
wallpaper.value = file.url;
|
||||
});
|
||||
},
|
||||
|
@@ -1 +0,0 @@
|
||||
export const isDeviceTouch = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
|
@@ -1,8 +1,9 @@
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
import { defaultStore } from '@/store';
|
||||
import { DriveFile } from 'misskey-js/built/entities';
|
||||
|
||||
export function selectFile(src: any, label: string | null, multiple = false) {
|
||||
function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> {
|
||||
return new Promise((res, rej) => {
|
||||
const chooseFileFromPc = () => {
|
||||
const input = document.createElement('input');
|
||||
@@ -86,3 +87,11 @@ export function selectFile(src: any, label: string | null, multiple = false) {
|
||||
}], src);
|
||||
});
|
||||
}
|
||||
|
||||
export function selectFile(src: any, label: string | null = null): Promise<DriveFile> {
|
||||
return select(src, label, false) as Promise<DriveFile>;
|
||||
}
|
||||
|
||||
export function selectFiles(src: any, label: string | null = null): Promise<DriveFile[]> {
|
||||
return select(src, label, true) as Promise<DriveFile[]>;
|
||||
}
|
||||
|
19
packages/client/src/scripts/touch.ts
Normal file
19
packages/client/src/scripts/touch.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
|
||||
|
||||
export let isTouchUsing = false;
|
||||
|
||||
export let isScreenTouching = false;
|
||||
|
||||
if (isTouchSupported) {
|
||||
window.addEventListener('touchstart', () => {
|
||||
// maxTouchPointsなどでの判定だけだと、「タッチ機能付きディスプレイを使っているがマウスでしか操作しない」場合にも
|
||||
// タッチで使っていると判定されてしまうため、実際に一度でもタッチされたらtrueにする
|
||||
isTouchUsing = true;
|
||||
|
||||
isScreenTouching = true;
|
||||
}, { passive: true });
|
||||
|
||||
window.addEventListener('touchend', () => {
|
||||
isScreenTouching = false;
|
||||
}, { passive: true });
|
||||
}
|
@@ -1,9 +1,16 @@
|
||||
import { isScreenTouching } from '@/os';
|
||||
import { Ref, ref } from 'vue';
|
||||
import { isDeviceTouch } from './is-device-touch';
|
||||
import { Ref, ref, watch } from 'vue';
|
||||
|
||||
export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
|
||||
export function useTooltip(
|
||||
elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>,
|
||||
onShow: (showing: Ref<boolean>) => void,
|
||||
): void {
|
||||
let isHovering = false;
|
||||
|
||||
// iOS(Androidも?)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、それを無視するためのフラグ
|
||||
// 無視しないと、画面に触れてないのにツールチップが出たりし、ユーザビリティが損なわれる
|
||||
// TODO: 一度でもタップすると二度とマウスでツールチップ出せなくなるのをどうにかする 定期的にfalseに戻すとか...?
|
||||
let shouldIgnoreMouseover = false;
|
||||
|
||||
let timeoutId: number;
|
||||
|
||||
let changeShowingState: (() => void) | null;
|
||||
@@ -12,10 +19,6 @@ export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
|
||||
close();
|
||||
if (!isHovering) return;
|
||||
|
||||
// iOS(Androidも?)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、その対策
|
||||
// これが無いと、画面に触れてないのにツールチップが出たりしてしまう
|
||||
if (isDeviceTouch && !isScreenTouching) return;
|
||||
|
||||
const showing = ref(true);
|
||||
onShow(showing);
|
||||
changeShowingState = () => {
|
||||
@@ -32,6 +35,7 @@ export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
|
||||
|
||||
const onMouseover = () => {
|
||||
if (isHovering) return;
|
||||
if (shouldIgnoreMouseover) return;
|
||||
isHovering = true;
|
||||
timeoutId = window.setTimeout(open, 300);
|
||||
};
|
||||
@@ -43,8 +47,31 @@ export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
|
||||
close();
|
||||
};
|
||||
|
||||
return {
|
||||
onMouseover,
|
||||
onMouseleave,
|
||||
const onTouchstart = () => {
|
||||
shouldIgnoreMouseover = true;
|
||||
if (isHovering) return;
|
||||
isHovering = true;
|
||||
timeoutId = window.setTimeout(open, 300);
|
||||
};
|
||||
|
||||
const onTouchend = () => {
|
||||
if (!isHovering) return;
|
||||
isHovering = false;
|
||||
window.clearTimeout(timeoutId);
|
||||
close();
|
||||
};
|
||||
|
||||
const stop = watch(elRef, () => {
|
||||
if (elRef.value) {
|
||||
stop();
|
||||
const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el;
|
||||
el.addEventListener('mouseover', onMouseover, { passive: true });
|
||||
el.addEventListener('mouseleave', onMouseleave, { passive: true });
|
||||
el.addEventListener('touchstart', onTouchstart, { passive: true });
|
||||
el.addEventListener('touchend', onTouchend, { passive: true });
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
flush: 'post',
|
||||
});
|
||||
}
|
||||
|
@@ -346,7 +346,7 @@ hr {
|
||||
._popup {
|
||||
background: var(--popup);
|
||||
border-radius: var(--radius);
|
||||
contain: layout; // ふき出しがボックスから飛び出て表示されるようなデザインをする場合もあるので paint は contain することができない
|
||||
contain: contain;
|
||||
}
|
||||
|
||||
// TODO: 廃止
|
||||
|
@@ -59,7 +59,7 @@ import * as Acct from 'misskey-js/built/acct';
|
||||
import { formatTimeString } from '@/scripts/format-time-string';
|
||||
import { Autocomplete } from '@/scripts/autocomplete';
|
||||
import * as os from '@/os';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import { selectFiles } from '@/scripts/select-file';
|
||||
import { notePostInterruptors, postFormActions } from '@/store';
|
||||
import { throttle } from 'throttle-debounce';
|
||||
|
||||
@@ -342,7 +342,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
chooseFileFrom(ev) {
|
||||
selectFile(ev.currentTarget || ev.target, this.$ts.attachFile, true).then(files => {
|
||||
selectFiles(ev.currentTarget || ev.target, this.$ts.attachFile).then(files => {
|
||||
for (const file of files) {
|
||||
this.files.push(file);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user