Merge remote-tracking branch 'upstream/main' into feat/secure-fetch
This commit is contained in:
commit
5ffa80b27d
17 changed files with 82 additions and 403 deletions
|
@ -1,5 +0,0 @@
|
||||||
import { Note } from '@/models/entities/note.js';
|
|
||||||
|
|
||||||
export default function(note: Note): boolean {
|
|
||||||
return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
|
|
||||||
}
|
|
5
packages/backend/src/misc/renote.ts
Normal file
5
packages/backend/src/misc/renote.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { Note } from '@/models/entities/note.js';
|
||||||
|
|
||||||
|
export function isPureRenote(note: Note): boolean {
|
||||||
|
return note.renoteId != null && note.text == null && (renote.fileIds == null || renote.fileIds.length === 0) && !note.hasPoll;
|
||||||
|
}
|
|
@ -15,6 +15,7 @@ import { makePaginationQuery } from '../api/common/make-pagination-query.js';
|
||||||
import { setResponseType } from '../activitypub.js';
|
import { setResponseType } from '../activitypub.js';
|
||||||
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
import checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
|
import { isPureRenote } from '@/misc/renote.js';
|
||||||
|
|
||||||
export default async (ctx: Router.RouterContext) => {
|
export default async (ctx: Router.RouterContext) => {
|
||||||
const verify = await checkFetch(ctx.req);
|
const verify = await checkFetch(ctx.req);
|
||||||
|
@ -113,10 +114,10 @@ export default async (ctx: Router.RouterContext) => {
|
||||||
* @param note Note
|
* @param note Note
|
||||||
*/
|
*/
|
||||||
export async function packActivity(note: Note): Promise<any> {
|
export async function packActivity(note: Note): Promise<any> {
|
||||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
|
if (isPureRenote(note)) {
|
||||||
const renote = await Notes.findOneByOrFail({ id: note.renoteId });
|
const renote = await Notes.findOneByOrFail({ id: note.renoteId });
|
||||||
return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note);
|
return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
return renderCreate(await renderNote(note, false), note);
|
return renderCreate(await renderNote(note, false), note);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { noteVisibilities } from '../../../../types.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
import { getNote } from '../../common/getters.js';
|
import { getNote } from '../../common/getters.js';
|
||||||
|
import { isPureRenote } from '@/misc/renote.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
|
@ -201,7 +202,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) {
|
if (isPureRenote(renote)) {
|
||||||
throw new ApiError(meta.errors.cannotReRenote);
|
throw new ApiError(meta.errors.cannotReRenote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,7 +231,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
|
if (isPureRenote(reply)) {
|
||||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { deliverToFollowers, deliverToUser } from '@/remote/activitypub/deliver-
|
||||||
import { countSameRenotes } from '@/misc/count-same-renotes.js';
|
import { countSameRenotes } from '@/misc/count-same-renotes.js';
|
||||||
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
|
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.js';
|
||||||
import { deliverToRelays } from '../relay.js';
|
import { deliverToRelays } from '../relay.js';
|
||||||
|
import { isPureRenote } from '@/misc/renote.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 投稿を削除します。
|
* 投稿を削除します。
|
||||||
|
@ -43,7 +44,7 @@ export default async function(user: { id: User['id']; uri: User['uri']; host: Us
|
||||||
let renote: Note | null = null;
|
let renote: Note | null = null;
|
||||||
|
|
||||||
// if deletd note is renote
|
// if deletd note is renote
|
||||||
if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length === 0)) {
|
if (isPureRenote(note)) {
|
||||||
renote = await Notes.findOneBy({
|
renote = await Notes.findOneBy({
|
||||||
id: note.renoteId,
|
id: note.renoteId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
<template>
|
|
||||||
<span class="mk-file-type-icon">
|
|
||||||
<template v-if="kind == 'image'"><i class="fas fa-file-image"></i></template>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
type: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const kind = computed(() => props.type.split('/')[0]);
|
|
||||||
</script>
|
|
|
@ -12,7 +12,7 @@
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:pattern="pattern"
|
:pattern="pattern"
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete ? 'on' : 'off'"
|
||||||
:spellcheck="spellcheck"
|
:spellcheck="spellcheck"
|
||||||
@focus="focused = true"
|
@focus="focused = true"
|
||||||
@blur="focused = false"
|
@blur="focused = false"
|
||||||
|
@ -62,34 +62,40 @@ const props = withDefaults(defineProps<{
|
||||||
manualSave: false,
|
manualSave: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { modelValue, autofocus } = toRefs(props);
|
const { modelValue } = toRefs(props);
|
||||||
// modelValue is read only, so a separate ref is needed.
|
// modelValue is read only, so a separate ref is needed.
|
||||||
const v = $ref(modelValue.value);
|
const v = $ref(modelValue.value);
|
||||||
|
|
||||||
const focused = $ref(false);
|
let focused = $ref(false);
|
||||||
const changed = $ref(false);
|
let changed = $ref(false);
|
||||||
const invalid = $ref(false);
|
let invalid = $ref(false);
|
||||||
const filled = computed(() => modelValue.value !== '' && modelValue.value != null);
|
let inputEl: HTMLTextAreaElement | null = $ref(null);
|
||||||
const inputEl = $ref(null);
|
|
||||||
|
|
||||||
const focus = () => inputEl.focus();
|
const filled = computed(() => modelValue.value !== '' && modelValue.value != null);
|
||||||
const onInput = evt => {
|
|
||||||
|
const focus = (): void => {
|
||||||
|
inputEl?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInput = (evt: HTMLInputEvent): void => {
|
||||||
changed = true;
|
changed = true;
|
||||||
emit('change', evt);
|
emit('change', evt);
|
||||||
};
|
};
|
||||||
const onKeydown = (evt: KeyboardEvent) => {
|
|
||||||
|
const onKeydown = (evt: KeyboardEvent): void => {
|
||||||
emit('keydown', evt);
|
emit('keydown', evt);
|
||||||
if (evt.code === 'Enter') {
|
if (evt.code === 'Enter') {
|
||||||
emit('enter');
|
emit('enter');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const updated = () => {
|
|
||||||
|
const updated = (): void => {
|
||||||
changed = false;
|
changed = false;
|
||||||
emit('update:modelValue', v);
|
emit('update:modelValue', v);
|
||||||
};
|
};
|
||||||
const debouncedUpdated = debounce(1000, updated);
|
const debouncedUpdated = debounce(1000, updated);
|
||||||
|
|
||||||
watch(modelValue, newValue => {
|
watch(modelValue, () => {
|
||||||
if (!props.manualSave) {
|
if (!props.manualSave) {
|
||||||
if (props.debounce) {
|
if (props.debounce) {
|
||||||
debouncedUpdated();
|
debouncedUpdated();
|
||||||
|
@ -98,15 +104,12 @@ watch(modelValue, newValue => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invalid = inputEl.validity.badInput;
|
invalid = inputEl?.validity.badInput ?? false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (props.autofocus) {
|
if (props.autofocus) focus();
|
||||||
inputEl.focus();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
<template>
|
|
||||||
<MkModal ref="modal" :z-priority="'middle'" @click="modal.close()" @closed="emit('closed')">
|
|
||||||
<div class="xubzgfga">
|
|
||||||
<header>{{ image.name }}</header>
|
|
||||||
<img :src="image.url" :alt="image.comment" :title="image.comment" @click="modal.close()"/>
|
|
||||||
<footer>
|
|
||||||
<span>{{ image.type }}</span>
|
|
||||||
<span>{{ bytes(image.size) }}</span>
|
|
||||||
<span v-if="image.properties && image.properties.width">{{ number(image.properties.width) }}px × {{ number(image.properties.height) }}px</span>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</MkModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { } from 'vue';
|
|
||||||
import * as misskey from 'misskey-js';
|
|
||||||
import bytes from '@/filters/bytes';
|
|
||||||
import number from '@/filters/number';
|
|
||||||
import MkModal from '@/components/ui/modal.vue';
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
|
||||||
image: misskey.entities.DriveFile;
|
|
||||||
}>(), {
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(ev: 'closed'): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const modal = $ref<InstanceType<typeof MkModal>>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.xubzgfga {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
> header,
|
|
||||||
> footer {
|
|
||||||
align-self: center;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 6px 9px;
|
|
||||||
font-size: 90%;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
border-radius: 6px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
> header {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
> img {
|
|
||||||
display: block;
|
|
||||||
flex: 1;
|
|
||||||
min-height: 0;
|
|
||||||
object-fit: contain;
|
|
||||||
width: 100%;
|
|
||||||
cursor: zoom-out;
|
|
||||||
image-orientation: from-image;
|
|
||||||
}
|
|
||||||
|
|
||||||
> footer {
|
|
||||||
margin-top: 8px;
|
|
||||||
opacity: 0.8;
|
|
||||||
|
|
||||||
> span + span {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
padding-left: 0.5em;
|
|
||||||
border-left: solid 1px rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,17 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="evrzpitu"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import * as os from '@/os';
|
|
||||||
|
|
||||||
export default defineComponent({});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.evrzpitu
|
|
||||||
margin 16px 0
|
|
||||||
border-bottom solid var(--lineWidth) var(--faceDivider)
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -31,7 +31,7 @@ type ModalTypes = 'popup' | 'dialog' | 'dialog:top' | 'drawer';
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
manualShowing?: boolean | null;
|
manualShowing?: boolean | null;
|
||||||
anchor?: { x: string; y: string; };
|
anchor?: { x: string; y: string; };
|
||||||
src?: HTMLElement | null;
|
src?: HTMLElement;
|
||||||
preferType?: ModalTypes | 'auto';
|
preferType?: ModalTypes | 'auto';
|
||||||
zPriority?: 'low' | 'middle' | 'high';
|
zPriority?: 'low' | 'middle' | 'high';
|
||||||
noOverlap?: boolean;
|
noOverlap?: boolean;
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { Directive } from 'vue';
|
|
||||||
import { getScrollContainer, getScrollPosition } from '@/scripts/scroll';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mounted(src, binding, vn) {
|
|
||||||
if (binding.value === false) return;
|
|
||||||
|
|
||||||
let isBottom = true;
|
|
||||||
|
|
||||||
const container = getScrollContainer(src)!;
|
|
||||||
container.addEventListener('scroll', () => {
|
|
||||||
const pos = getScrollPosition(container);
|
|
||||||
const viewHeight = container.clientHeight;
|
|
||||||
const height = container.scrollHeight;
|
|
||||||
isBottom = (pos + viewHeight > height - 32);
|
|
||||||
}, { passive: true });
|
|
||||||
container.scrollTop = container.scrollHeight;
|
|
||||||
|
|
||||||
const ro = new ResizeObserver((entries, observer) => {
|
|
||||||
if (isBottom) {
|
|
||||||
const height = container.scrollHeight;
|
|
||||||
container.scrollTop = height;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ro.observe(src);
|
|
||||||
|
|
||||||
// TODO: 新たにプロパティを作るのをやめMapを使う
|
|
||||||
src._ro_ = ro;
|
|
||||||
},
|
|
||||||
|
|
||||||
unmounted(src, binding, vn) {
|
|
||||||
if (src._ro_) src._ro_.unobserve(src);
|
|
||||||
}
|
|
||||||
} as Directive;
|
|
|
@ -1,98 +1,46 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="driuhtrh">
|
<div class="driuhtrh">
|
||||||
<div class="query">
|
<div class="query">
|
||||||
<MkInput v-model="q" class="" :placeholder="$ts.search">
|
<MkInput v-model="q" class="" :placeholder="i18n.ts.search">
|
||||||
<template #prefix><i class="fas fa-search"></i></template>
|
<template #prefix><i class="fas fa-search"></i></template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
|
|
||||||
<!-- たくさんあると邪魔
|
|
||||||
<div class="tags">
|
|
||||||
<span class="tag _button" v-for="tag in tags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkFolder v-if="searchEmojis" class="emojis">
|
<MkFolder v-if="searchEmojis" class="emojis">
|
||||||
<template #header>{{ $ts.searchResult }}</template>
|
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||||
<div class="zuvgdzyt">
|
<div class="zuvgdzyt">
|
||||||
<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/>
|
<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-for="category in customEmojiCategories" :key="category" class="emojis">
|
<MkFolder v-for="category in emojiCategories" :key="category" class="emojis">
|
||||||
<template #header>{{ category || $ts.other }}</template>
|
<template #header>{{ category || i18n.ts.other }}</template>
|
||||||
<div class="zuvgdzyt">
|
<div class="zuvgdzyt">
|
||||||
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/>
|
<XEmoji v-for="emoji in instance.emojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, computed } from 'vue';
|
import { watch } from 'vue';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from '@/components/form/input.vue';
|
||||||
import MkSelect from '@/components/form/select.vue';
|
|
||||||
import MkFolder from '@/components/ui/folder.vue';
|
import MkFolder from '@/components/ui/folder.vue';
|
||||||
import MkTab from '@/components/tab.vue';
|
import { i18n } from '@/i18n';
|
||||||
import * as os from '@/os';
|
import { emojiCategories, instance } from '@/instance';
|
||||||
import { emojiCategories, emojiTags } from '@/instance';
|
|
||||||
import XEmoji from './emojis.emoji.vue';
|
import XEmoji from './emojis.emoji.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
let q: string = $ref('');
|
||||||
components: {
|
let searchEmojis: null | Record<string, any>[] = $ref(null);
|
||||||
MkButton,
|
|
||||||
MkInput,
|
|
||||||
MkSelect,
|
|
||||||
MkFolder,
|
|
||||||
MkTab,
|
|
||||||
XEmoji,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
function search() {
|
||||||
return {
|
if (q === '') {
|
||||||
q: '',
|
searchEmojis = null;
|
||||||
customEmojiCategories: emojiCategories,
|
|
||||||
customEmojis: this.$instance.emojis,
|
|
||||||
tags: emojiTags,
|
|
||||||
selectedTags: new Set(),
|
|
||||||
searchEmojis: null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
q() { this.search(); },
|
|
||||||
selectedTags: {
|
|
||||||
handler() {
|
|
||||||
this.search();
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
search() {
|
|
||||||
if ((this.q === '' || this.q == null) && this.selectedTags.size === 0) {
|
|
||||||
this.searchEmojis = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.selectedTags.size === 0) {
|
|
||||||
this.searchEmojis = this.customEmojis.filter(emoji => emoji.name.includes(this.q) || emoji.aliases.includes(this.q));
|
|
||||||
} else {
|
} else {
|
||||||
this.searchEmojis = this.customEmojis.filter(emoji => (emoji.name.includes(this.q) || emoji.aliases.includes(this.q)) && [...this.selectedTags].every(t => emoji.aliases.includes(t)));
|
searchEmojis = instance.emojis.filter(emoji => emoji.name.includes(this.q) || emoji.aliases.includes(this.q));
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleTag(tag) {
|
|
||||||
if (this.selectedTags.has(tag)) {
|
|
||||||
this.selectedTags.delete(tag);
|
|
||||||
} else {
|
|
||||||
this.selectedTags.add(tag);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
watch(q, search);
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<XCategory v-if="tab === 'category'"/>
|
<XCategory/>
|
||||||
</div>
|
</div>
|
||||||
</MkStickyContainer>
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
@ -14,8 +14,6 @@ import * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
const tab = ref('category');
|
|
||||||
|
|
||||||
function menu(ev) {
|
function menu(ev) {
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
icon: 'fas fa-download',
|
icon: 'fas fa-download',
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="title"><slot name="header"></slot></div>
|
<div class="title"><slot name="header"></slot></div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<slot name="func"></slot>
|
<slot name="func"></slot>
|
||||||
<button v-if="removable" class="_button" @click="remove()">
|
<button v-if="removable" class="_button" @click="emit('remove')">
|
||||||
<i class="fas fa-trash-alt"></i>
|
<i class="fas fa-trash-alt"></i>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="draggable" class="drag-handle _button">
|
<button v-if="draggable" class="drag-handle _button">
|
||||||
|
@ -16,56 +16,41 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<p v-show="showBody" v-if="error != null" class="error">{{ $t('_pages.script.typeError', { slot: error.arg + 1, expect: $t(`script.types.${error.expect}`), actual: $t(`script.types.${error.actual}`) }) }}</p>
|
<p v-show="showBody" v-if="error != null" class="error">{{ i18n.t('_pages.script.typeError', { slot: error.arg + 1, expect: i18n.t(`script.types.${error.expect}`), actual: i18n.t(`script.types.${error.actual}`) }) }}</p>
|
||||||
<p v-show="showBody" v-if="warn != null" class="warn">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
|
<p v-show="showBody" v-if="warn != null" class="warn">{{ i18n.t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
|
||||||
<div v-show="showBody" class="body">
|
<div v-show="showBody" class="body">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { } from 'vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
const emit = defineEmits<{
|
||||||
props: {
|
(ev: 'toggle', v: boolean): void;
|
||||||
expanded: {
|
(ev: 'remove'): void;
|
||||||
type: Boolean,
|
}>();
|
||||||
default: true
|
|
||||||
},
|
const props = withDefaults(defineProps<{
|
||||||
removable: {
|
expanded?: boolean;
|
||||||
type: Boolean,
|
removable?: boolean;
|
||||||
default: true
|
draggable?: boolean;
|
||||||
},
|
error?: { arg: integer; expect: string; actual: string; };
|
||||||
draggable: {
|
warn?: { slot: integer; };
|
||||||
type: Boolean,
|
}>(), {
|
||||||
default: false
|
expanded: true,
|
||||||
},
|
removable: true,
|
||||||
error: {
|
draggable: false,
|
||||||
required: false,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
warn: {
|
|
||||||
required: false,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['toggle', 'remove'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showBody: this.expanded,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleContent(show: boolean) {
|
|
||||||
this.showBody = show;
|
|
||||||
this.$emit('toggle', show);
|
|
||||||
},
|
|
||||||
remove() {
|
|
||||||
this.$emit('remove');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let showBody = $ref(props.expanded);
|
||||||
|
|
||||||
|
function toggleContent(show: boolean) {
|
||||||
|
showBody = show;
|
||||||
|
emit('toggle', show);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import * as Acct from 'misskey-js/built/acct';
|
|
||||||
import { host as localHost } from '@/config';
|
|
||||||
|
|
||||||
export async function genSearchQuery(v: any, q: string) {
|
|
||||||
let host: string;
|
|
||||||
let userId: string;
|
|
||||||
if (q.split(' ').some(x => x.startsWith('@'))) {
|
|
||||||
for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) {
|
|
||||||
if (at.includes('.')) {
|
|
||||||
if (at === localHost || at === '.') {
|
|
||||||
host = null;
|
|
||||||
} else {
|
|
||||||
host = at;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const user = await v.os.api('users/show', Acct.parse(at)).catch(x => null);
|
|
||||||
if (user) {
|
|
||||||
userId = user.id;
|
|
||||||
} else {
|
|
||||||
// todo: show error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '),
|
|
||||||
host: host,
|
|
||||||
userId: userId
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export default function(user: { name?: string | null, username: string }): string {
|
|
||||||
return user.name || user.username;
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import { themeProps, Theme } from './theme';
|
|
||||||
|
|
||||||
export type Default = null;
|
|
||||||
export type Color = string;
|
|
||||||
export type FuncName = 'alpha' | 'darken' | 'lighten';
|
|
||||||
export type Func = { type: 'func'; name: FuncName; arg: number; value: string; };
|
|
||||||
export type RefProp = { type: 'refProp'; key: string; };
|
|
||||||
export type RefConst = { type: 'refConst'; key: string; };
|
|
||||||
export type Css = { type: 'css'; value: string; };
|
|
||||||
|
|
||||||
export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default;
|
|
||||||
|
|
||||||
export type ThemeViewModel = [ string, ThemeValue ][];
|
|
||||||
|
|
||||||
export const fromThemeString = (str?: string) : ThemeValue => {
|
|
||||||
if (!str) return null;
|
|
||||||
if (str.startsWith(':')) {
|
|
||||||
const parts = str.slice(1).split('<');
|
|
||||||
const name = parts[0] as FuncName;
|
|
||||||
const arg = parseFloat(parts[1]);
|
|
||||||
const value = parts[2].startsWith('@') ? parts[2].slice(1) : '';
|
|
||||||
return { type: 'func', name, arg, value };
|
|
||||||
} else if (str.startsWith('@')) {
|
|
||||||
return {
|
|
||||||
type: 'refProp',
|
|
||||||
key: str.slice(1),
|
|
||||||
};
|
|
||||||
} else if (str.startsWith('$')) {
|
|
||||||
return {
|
|
||||||
type: 'refConst',
|
|
||||||
key: str.slice(1),
|
|
||||||
};
|
|
||||||
} else if (str.startsWith('"')) {
|
|
||||||
return {
|
|
||||||
type: 'css',
|
|
||||||
value: str.substr(1).trim(),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => {
|
|
||||||
if (typeof value === 'string') return value;
|
|
||||||
switch (value.type) {
|
|
||||||
case 'func': return `:${value.name}<${value.arg}<@${value.value}`;
|
|
||||||
case 'refProp': return `@${value.key}`;
|
|
||||||
case 'refConst': return `$${value.key}`;
|
|
||||||
case 'css': return `" ${value.value}`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const convertToMisskeyTheme = (vm: ThemeViewModel, name: string, desc: string, author: string, base: 'dark' | 'light'): Theme => {
|
|
||||||
const props = { } as { [key: string]: string };
|
|
||||||
for (const [ key, value ] of vm) {
|
|
||||||
if (value === null) continue;
|
|
||||||
props[key] = toThemeString(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: uuid(),
|
|
||||||
name, desc, author, props, base
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const convertToViewModel = (theme: Theme): ThemeViewModel => {
|
|
||||||
const vm: ThemeViewModel = [];
|
|
||||||
// プロパティの登録
|
|
||||||
vm.push(...themeProps.map(key => [ key, fromThemeString(theme.props[key])] as [ string, ThemeValue ]));
|
|
||||||
|
|
||||||
// 定数の登録
|
|
||||||
const consts = Object
|
|
||||||
.keys(theme.props)
|
|
||||||
.filter(k => k.startsWith('$'))
|
|
||||||
.map(k => [ k, fromThemeString(theme.props[k]) ] as [ string, ThemeValue ]);
|
|
||||||
|
|
||||||
vm.push(...consts);
|
|
||||||
return vm;
|
|
||||||
};
|
|
Loading…
Reference in a new issue