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 checkFetch from '@/remote/activitypub/check-fetch.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { isPureRenote } from '@/misc/renote.js';
|
||||
|
||||
export default async (ctx: Router.RouterContext) => {
|
||||
const verify = await checkFetch(ctx.req);
|
||||
|
@ -113,10 +114,10 @@ export default async (ctx: Router.RouterContext) => {
|
|||
* @param note Note
|
||||
*/
|
||||
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 });
|
||||
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 define from '../../define.js';
|
||||
import { getNote } from '../../common/getters.js';
|
||||
import { isPureRenote } from '@/misc/renote.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
|
@ -201,7 +202,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw e;
|
||||
});
|
||||
|
||||
if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) {
|
||||
if (isPureRenote(renote)) {
|
||||
throw new ApiError(meta.errors.cannotReRenote);
|
||||
}
|
||||
|
||||
|
@ -230,7 +231,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
throw e;
|
||||
});
|
||||
|
||||
if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
|
||||
if (isPureRenote(reply)) {
|
||||
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 { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc.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;
|
||||
|
||||
// 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({
|
||||
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"
|
||||
:placeholder="placeholder"
|
||||
:pattern="pattern"
|
||||
:autocomplete="autocomplete"
|
||||
:autocomplete="autocomplete ? 'on' : 'off'"
|
||||
:spellcheck="spellcheck"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
|
@ -62,34 +62,40 @@ const props = withDefaults(defineProps<{
|
|||
manualSave: false,
|
||||
});
|
||||
|
||||
const { modelValue, autofocus } = toRefs(props);
|
||||
const { modelValue } = toRefs(props);
|
||||
// modelValue is read only, so a separate ref is needed.
|
||||
const v = $ref(modelValue.value);
|
||||
|
||||
const focused = $ref(false);
|
||||
const changed = $ref(false);
|
||||
const invalid = $ref(false);
|
||||
const filled = computed(() => modelValue.value !== '' && modelValue.value != null);
|
||||
const inputEl = $ref(null);
|
||||
let focused = $ref(false);
|
||||
let changed = $ref(false);
|
||||
let invalid = $ref(false);
|
||||
let inputEl: HTMLTextAreaElement | null = $ref(null);
|
||||
|
||||
const focus = () => inputEl.focus();
|
||||
const onInput = evt => {
|
||||
const filled = computed(() => modelValue.value !== '' && modelValue.value != null);
|
||||
|
||||
const focus = (): void => {
|
||||
inputEl?.focus();
|
||||
};
|
||||
|
||||
const onInput = (evt: HTMLInputEvent): void => {
|
||||
changed = true;
|
||||
emit('change', evt);
|
||||
};
|
||||
const onKeydown = (evt: KeyboardEvent) => {
|
||||
|
||||
const onKeydown = (evt: KeyboardEvent): void => {
|
||||
emit('keydown', evt);
|
||||
if (evt.code === 'Enter') {
|
||||
emit('enter');
|
||||
}
|
||||
};
|
||||
const updated = () => {
|
||||
|
||||
const updated = (): void => {
|
||||
changed = false;
|
||||
emit('update:modelValue', v);
|
||||
};
|
||||
const debouncedUpdated = debounce(1000, updated);
|
||||
|
||||
watch(modelValue, newValue => {
|
||||
watch(modelValue, () => {
|
||||
if (!props.manualSave) {
|
||||
if (props.debounce) {
|
||||
debouncedUpdated();
|
||||
|
@ -98,15 +104,12 @@ watch(modelValue, newValue => {
|
|||
}
|
||||
}
|
||||
|
||||
invalid = inputEl.validity.badInput;
|
||||
invalid = inputEl?.validity.badInput ?? false;
|
||||
});
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
if (props.autofocus) {
|
||||
inputEl.focus();
|
||||
}
|
||||
if (props.autofocus) focus();
|
||||
});
|
||||
});
|
||||
</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<{
|
||||
manualShowing?: boolean | null;
|
||||
anchor?: { x: string; y: string; };
|
||||
src?: HTMLElement | null;
|
||||
src?: HTMLElement;
|
||||
preferType?: ModalTypes | 'auto';
|
||||
zPriority?: 'low' | 'middle' | 'high';
|
||||
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>
|
||||
<div class="driuhtrh">
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<MkFolder v-if="searchEmojis" class="emojis">
|
||||
<template #header>{{ $ts.searchResult }}</template>
|
||||
<template #header>{{ i18n.ts.searchResult }}</template>
|
||||
<div class="zuvgdzyt">
|
||||
<XEmoji v-for="emoji in searchEmojis" :key="emoji.name" class="emoji" :emoji="emoji"/>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-for="category in customEmojiCategories" :key="category" class="emojis">
|
||||
<template #header>{{ category || $ts.other }}</template>
|
||||
<MkFolder v-for="category in emojiCategories" :key="category" class="emojis">
|
||||
<template #header>{{ category || i18n.ts.other }}</template>
|
||||
<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>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkFolder from '@/components/ui/folder.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import * as os from '@/os';
|
||||
import { emojiCategories, emojiTags } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { emojiCategories, instance } from '@/instance';
|
||||
import XEmoji from './emojis.emoji.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkSelect,
|
||||
MkFolder,
|
||||
MkTab,
|
||||
XEmoji,
|
||||
},
|
||||
let q: string = $ref('');
|
||||
let searchEmojis: null | Record<string, any>[] = $ref(null);
|
||||
|
||||
data() {
|
||||
return {
|
||||
q: '',
|
||||
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 {
|
||||
this.searchEmojis = this.customEmojis.filter(emoji => (emoji.name.includes(this.q) || emoji.aliases.includes(this.q)) && [...this.selectedTags].every(t => emoji.aliases.includes(t)));
|
||||
}
|
||||
},
|
||||
|
||||
toggleTag(tag) {
|
||||
if (this.selectedTags.has(tag)) {
|
||||
this.selectedTags.delete(tag);
|
||||
} else {
|
||||
this.selectedTags.add(tag);
|
||||
}
|
||||
}
|
||||
function search() {
|
||||
if (q === '') {
|
||||
searchEmojis = null;
|
||||
} else {
|
||||
searchEmojis = instance.emojis.filter(emoji => emoji.name.includes(this.q) || emoji.aliases.includes(this.q));
|
||||
}
|
||||
});
|
||||
}
|
||||
watch(q, search);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<div :class="$style.root">
|
||||
<XCategory v-if="tab === 'category'"/>
|
||||
<XCategory/>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
@ -14,8 +14,6 @@ import * as os from '@/os';
|
|||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
|
||||
const tab = ref('category');
|
||||
|
||||
function menu(ev) {
|
||||
os.popupMenu([{
|
||||
icon: 'fas fa-download',
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="title"><slot name="header"></slot></div>
|
||||
<div class="buttons">
|
||||
<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>
|
||||
</button>
|
||||
<button v-if="draggable" class="drag-handle _button">
|
||||
|
@ -16,56 +16,41 @@
|
|||
</button>
|
||||
</div>
|
||||
</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="warn != null" class="warn">{{ $t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</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">{{ i18n.t('_pages.script.thereIsEmptySlot', { slot: warn.slot + 1 }) }}</p>
|
||||
<div v-show="showBody" class="body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
expanded: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
removable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
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');
|
||||
}
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
(ev: 'toggle', v: boolean): void;
|
||||
(ev: 'remove'): void;
|
||||
}>();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
expanded?: boolean;
|
||||
removable?: boolean;
|
||||
draggable?: boolean;
|
||||
error?: { arg: integer; expect: string; actual: string; };
|
||||
warn?: { slot: integer; };
|
||||
}>(), {
|
||||
expanded: true,
|
||||
removable: true,
|
||||
draggable: false,
|
||||
});
|
||||
|
||||
let showBody = $ref(props.expanded);
|
||||
|
||||
function toggleContent(show: boolean) {
|
||||
showBody = show;
|
||||
emit('toggle', show);
|
||||
}
|
||||
</script>
|
||||
|
||||
<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