Merge branch 'develop' into pizzax-indexeddb
This commit is contained in:
@@ -1,30 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
|
||||
<div v-for="user in users" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
|
||||
<MkAvatar :user="user" style="width:32px;height:32px;" :show-indicator="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
userIds: {
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
us: []
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
this.us = await os.api('users/show', {
|
||||
userIds: this.userIds
|
||||
});
|
||||
}
|
||||
const props = defineProps<{
|
||||
userIds: string[];
|
||||
}>();
|
||||
|
||||
const users = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
users.value = await os.api('users/show', {
|
||||
userIds: props.userIds
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@@ -5,41 +5,33 @@
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { length } from 'stringz';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { concat } from '@/scripts/array';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
note: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
note: misskey.entities.Note;
|
||||
}>();
|
||||
|
||||
computed: {
|
||||
label(): string {
|
||||
return concat([
|
||||
this.note.text ? [this.$t('_cw.chars', { count: length(this.note.text) })] : [],
|
||||
this.note.files && this.note.files.length !== 0 ? [this.$t('_cw.files', { count: this.note.files.length }) ] : [],
|
||||
this.note.poll != null ? [this.$ts.poll] : []
|
||||
] as string[][]).join(' / ');
|
||||
}
|
||||
},
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void;
|
||||
}>();
|
||||
|
||||
methods: {
|
||||
length,
|
||||
|
||||
toggle() {
|
||||
this.$emit('update:modelValue', !this.modelValue);
|
||||
}
|
||||
}
|
||||
const label = computed(() => {
|
||||
return concat([
|
||||
props.note.text ? [i18n.t('_cw.chars', { count: length(props.note.text) })] : [],
|
||||
props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length }) ] : [],
|
||||
props.note.poll != null ? [i18n.locale.poll] : []
|
||||
] as string[][]).join(' / ');
|
||||
});
|
||||
|
||||
const toggle = () => {
|
||||
emit('update:modelValue', !props.modelValue);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -4,25 +4,12 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
kind(): string {
|
||||
return this.type.split('/')[0];
|
||||
}
|
||||
}
|
||||
});
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
}>();
|
||||
|
||||
const kind = computed(() => props.type.split('/')[0]);
|
||||
</script>
|
||||
|
@@ -5,28 +5,17 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import * as misskey from 'misskey-js';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import { host } from '@/config';
|
||||
import { host as hostRaw } from '@/config';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
detail: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
host: toUnicode(host),
|
||||
};
|
||||
}
|
||||
});
|
||||
defineProps<{
|
||||
user: misskey.entities.UserDetailed;
|
||||
detail?: boolean;
|
||||
}>();
|
||||
|
||||
const host = toUnicode(hostRaw);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -8,19 +8,8 @@
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -5,31 +5,18 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
q: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.query = this.q;
|
||||
},
|
||||
methods: {
|
||||
search() {
|
||||
window.open(`https://www.google.com/search?q=${this.query}`, '_blank');
|
||||
}
|
||||
}
|
||||
});
|
||||
const props = defineProps<{
|
||||
q: string;
|
||||
}>();
|
||||
|
||||
const query = ref(props.q);
|
||||
|
||||
const search = () => {
|
||||
window.open(`https://www.google.com/search?q=${query.value}`, '_blank');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -22,26 +22,16 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
video: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hide: true,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.hide = (this.$store.state.nsfw === 'force') ? true : this.video.isSensitive && (this.$store.state.nsfw !== 'ignore');
|
||||
},
|
||||
});
|
||||
const props = defineProps<{
|
||||
video: misskey.entities.DriveFile;
|
||||
}>();
|
||||
|
||||
const hide = ref((defaultStore.state.nsfw === 'force') ? true : props.video.isSensitive && (defaultStore.state.nsfw !== 'ignore'));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -2,22 +2,10 @@
|
||||
<div class="jmgmzlwq _block"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i>{{ $ts.remoteUserCaution }}<a :href="href" rel="nofollow noopener" target="_blank">{{ $ts.showOnRemote }}</a></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
href: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
}
|
||||
});
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
href: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<XWindow ref="window"
|
||||
:initial-width="370"
|
||||
:initial-height="450"
|
||||
:can-resize="true"
|
||||
@close="$refs.window.close()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>Req Viewer</template>
|
||||
|
||||
<div class="rlkneywz">
|
||||
<MkTab v-model="tab" style="border-bottom: solid 0.5px var(--divider);">
|
||||
<option value="req">Request</option>
|
||||
<option value="res">Response</option>
|
||||
</MkTab>
|
||||
|
||||
<code v-if="tab === 'req'" class="_monospace">{{ reqStr }}</code>
|
||||
<code v-if="tab === 'res'" class="_monospace">{{ resStr }}</code>
|
||||
</div>
|
||||
</XWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as JSON5 from 'json5';
|
||||
import XWindow from '@/components/ui/window.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XWindow,
|
||||
MkTab,
|
||||
},
|
||||
|
||||
props: {
|
||||
req: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['closed'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
tab: 'req',
|
||||
reqStr: JSON5.stringify(this.req.req, null, '\t'),
|
||||
resStr: JSON5.stringify(this.req.res, null, '\t'),
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rlkneywz {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
> code {
|
||||
display: block;
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
font-size: 0.9em;
|
||||
tab-size: 2;
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,234 +0,0 @@
|
||||
<template>
|
||||
<XWindow ref="window" :initial-width="650" :initial-height="420" :can-resize="true" @closed="$emit('closed')">
|
||||
<template #header>
|
||||
<i class="fas fa-terminal" style="margin-right: 0.5em;"></i>Task Manager
|
||||
</template>
|
||||
<div class="qljqmnzj _monospace">
|
||||
<MkTab v-model="tab" style="border-bottom: solid 0.5px var(--divider);">
|
||||
<option value="windows">Windows</option>
|
||||
<option value="stream">Stream</option>
|
||||
<option value="streamPool">Stream (Pool)</option>
|
||||
<option value="api">API</option>
|
||||
</MkTab>
|
||||
|
||||
<div class="content">
|
||||
<div v-if="tab === 'windows'" v-follow class="windows">
|
||||
<div class="header">
|
||||
<div>#ID</div>
|
||||
<div>Component</div>
|
||||
<div>Action</div>
|
||||
</div>
|
||||
<div v-for="p in popups">
|
||||
<div>#{{ p.id }}</div>
|
||||
<div>{{ p.component.name ? p.component.name : '<anonymous>' }}</div>
|
||||
<div><button class="_textButton" @click="killPopup(p)">Kill</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tab === 'stream'" v-follow class="stream">
|
||||
<div class="header">
|
||||
<div>#ID</div>
|
||||
<div>Ch</div>
|
||||
<div>Handle</div>
|
||||
<div>In</div>
|
||||
<div>Out</div>
|
||||
</div>
|
||||
<div v-for="c in connections">
|
||||
<div>#{{ c.id }}</div>
|
||||
<div>{{ c.channel }}</div>
|
||||
<div v-if="c.users !== null">(shared)<span v-if="c.name">{{ ' ' + c.name }}</span></div>
|
||||
<div v-else>{{ c.name ? c.name : '<anonymous>' }}</div>
|
||||
<div>{{ c.in }}</div>
|
||||
<div>{{ c.out }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tab === 'streamPool'" v-follow class="streamPool">
|
||||
<div class="header">
|
||||
<div>#ID</div>
|
||||
<div>Ch</div>
|
||||
<div>Users</div>
|
||||
</div>
|
||||
<div v-for="p in pools">
|
||||
<div>#{{ p.id }}</div>
|
||||
<div>{{ p.channel }}</div>
|
||||
<div>{{ p.users }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tab === 'api'" v-follow class="api">
|
||||
<div class="header">
|
||||
<div>#ID</div>
|
||||
<div>Endpoint</div>
|
||||
<div>State</div>
|
||||
</div>
|
||||
<div v-for="req in apiRequests" @click="showReq(req)">
|
||||
<div>#{{ req.id }}</div>
|
||||
<div>{{ req.endpoint }}</div>
|
||||
<div class="state" :class="req.state">{{ req.state }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div><span class="label">Windows</span>{{ popups.length }}</div>
|
||||
<div><span class="label">Stream</span>{{ connections.length }}</div>
|
||||
<div><span class="label">Stream (Pool)</span>{{ pools.length }}</div>
|
||||
</footer>
|
||||
</div>
|
||||
</XWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw, onBeforeUnmount, ref, shallowRef } from 'vue';
|
||||
import XWindow from '@/components/ui/window.vue';
|
||||
import MkTab from '@/components/tab.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import follow from '@/directives/follow-append';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XWindow,
|
||||
MkTab,
|
||||
MkButton,
|
||||
},
|
||||
|
||||
directives: {
|
||||
follow
|
||||
},
|
||||
|
||||
props: {
|
||||
},
|
||||
|
||||
emits: ['closed'],
|
||||
|
||||
setup() {
|
||||
const connections = shallowRef([]);
|
||||
const pools = shallowRef([]);
|
||||
const refreshStreamInfo = () => {
|
||||
console.log(stream.sharedConnectionPools, stream.sharedConnections, stream.nonSharedConnections);
|
||||
const conn = stream.sharedConnections.map(c => ({
|
||||
id: c.id, name: c.name, channel: c.channel, users: c.pool.users, in: c.inCount, out: c.outCount,
|
||||
})).concat(stream.nonSharedConnections.map(c => ({
|
||||
id: c.id, name: c.name, channel: c.channel, users: null, in: c.inCount, out: c.outCount,
|
||||
})));
|
||||
conn.sort((a, b) => (a.id > b.id) ? 1 : -1);
|
||||
connections.value = conn;
|
||||
pools.value = stream.sharedConnectionPools;
|
||||
};
|
||||
const interval = setInterval(refreshStreamInfo, 1000);
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
|
||||
const killPopup = p => {
|
||||
os.popups.value = os.popups.value.filter(x => x !== p);
|
||||
};
|
||||
|
||||
const showReq = req => {
|
||||
os.popup(import('./taskmanager.api-window.vue'), {
|
||||
req: req
|
||||
}, {
|
||||
}, 'closed');
|
||||
};
|
||||
|
||||
return {
|
||||
tab: ref('stream'),
|
||||
popups: os.popups,
|
||||
apiRequests: os.apiRequests,
|
||||
connections,
|
||||
pools,
|
||||
killPopup,
|
||||
showReq,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qljqmnzj {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
> .content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
> div {
|
||||
display: table;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
|
||||
> div {
|
||||
display: table-row;
|
||||
|
||||
&:nth-child(even) {
|
||||
//background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.header {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: table-cell;
|
||||
white-space: nowrap;
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.api {
|
||||
> div {
|
||||
&:not(.header) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
> .state {
|
||||
&.pending {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
&.failed {
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> footer {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
box-sizing: border-box;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
font-size: 0.9em;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
|
||||
> .label {
|
||||
opacity: 0.7;
|
||||
margin-right: 0.5em;
|
||||
|
||||
&:after {
|
||||
content: ":";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="mk-toast">
|
||||
<transition name="toast" appear @after-leave="$emit('closed')">
|
||||
<transition name="toast" appear @after-leave="emit('closed')">
|
||||
<div v-if="showing" class="body _acrylic" :style="{ zIndex }">
|
||||
<div class="message">
|
||||
{{ message }}
|
||||
@@ -10,29 +10,25 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['closed'],
|
||||
data() {
|
||||
return {
|
||||
showing: true,
|
||||
zIndex: os.claimZIndex('high'),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.showing = false;
|
||||
}, 4000);
|
||||
}
|
||||
defineProps<{
|
||||
message: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const showing = ref(true);
|
||||
const zIndex = os.claimZIndex('high');
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
showing.value = false;
|
||||
}, 4000);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -76,7 +76,7 @@ export default defineComponent({
|
||||
tweetExpanded: this.detail,
|
||||
embedId: `embed${Math.random().toString().replace(/\D/,'')}`,
|
||||
tweetHeight: 150,
|
||||
tweetLeft: 0,
|
||||
tweetLeft: 0,
|
||||
playerEnabled: false,
|
||||
self: self,
|
||||
attr: self ? 'to' : 'href',
|
||||
|
@@ -27,32 +27,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkFollowButton from './follow-button.vue';
|
||||
import { userPage } from '@/filters/user';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkFollowButton
|
||||
},
|
||||
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
userPage,
|
||||
}
|
||||
});
|
||||
defineProps<{
|
||||
user: misskey.entities.UserDetailed;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -173,12 +173,6 @@ export const menuDef = reactive({
|
||||
icon: 'fas fa-terminal',
|
||||
to: '/scratchpad',
|
||||
},
|
||||
rooms: {
|
||||
title: 'rooms',
|
||||
icon: 'fas fa-door-closed',
|
||||
show: computed(() => $i != null),
|
||||
to: computed(() => `/@${$i.username}/room`),
|
||||
},
|
||||
ui: {
|
||||
title: 'switchUi',
|
||||
icon: 'fas fa-columns',
|
||||
|
@@ -4,16 +4,13 @@ import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vu
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { apiUrl, debug, url } from '@/config';
|
||||
import { apiUrl, url } from '@/config';
|
||||
import MkPostFormDialog from '@/components/post-form-dialog.vue';
|
||||
import MkWaitingDialog from '@/components/waiting-dialog.vue';
|
||||
import { resolve } from '@/router';
|
||||
import { $i } from '@/account';
|
||||
import { defaultStore } from '@/store';
|
||||
|
||||
export const pendingApiRequestsCount = ref(0);
|
||||
let apiRequestsCount = 0; // for debug
|
||||
export const apiRequests = ref([]); // for debug
|
||||
|
||||
const apiClient = new Misskey.api.APIClient({
|
||||
origin: url,
|
||||
@@ -26,18 +23,6 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s
|
||||
pendingApiRequestsCount.value--;
|
||||
};
|
||||
|
||||
const log = debug ? reactive({
|
||||
id: ++apiRequestsCount,
|
||||
endpoint,
|
||||
req: markRaw(data),
|
||||
res: null,
|
||||
state: 'pending',
|
||||
}) : null;
|
||||
if (debug) {
|
||||
apiRequests.value.push(log);
|
||||
if (apiRequests.value.length > 128) apiRequests.value.shift();
|
||||
}
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
// Append a credential
|
||||
if ($i) (data as any).i = $i.token;
|
||||
@@ -54,21 +39,10 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s
|
||||
|
||||
if (res.status === 200) {
|
||||
resolve(body);
|
||||
if (debug) {
|
||||
log!.res = markRaw(JSON.parse(JSON.stringify(body)));
|
||||
log!.state = 'success';
|
||||
}
|
||||
} else if (res.status === 204) {
|
||||
resolve();
|
||||
if (debug) {
|
||||
log!.state = 'success';
|
||||
}
|
||||
} else {
|
||||
reject(body.error);
|
||||
if (debug) {
|
||||
log!.res = markRaw(body.error);
|
||||
log!.state = 'failed';
|
||||
}
|
||||
}
|
||||
}).catch(reject);
|
||||
});
|
||||
|
@@ -2,9 +2,5 @@
|
||||
<MkLoading/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({});
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
@@ -67,8 +67,8 @@
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { version, instanceName } from '@/config';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
@@ -79,37 +79,21 @@ import * as os from '@/os';
|
||||
import number from '@/filters/number';
|
||||
import * as symbols from '@/symbols';
|
||||
import { host } from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkKeyValue,
|
||||
FormSection,
|
||||
FormLink,
|
||||
FormSuspense,
|
||||
FormSplit,
|
||||
const stats = ref(null);
|
||||
|
||||
const initStats = () => os.api('stats', {
|
||||
}).then((res) => {
|
||||
stats.value = res;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.locale.instanceInfo,
|
||||
icon: 'fas fa-info-circle',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.instanceInfo,
|
||||
icon: 'fas fa-info-circle',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
host,
|
||||
version,
|
||||
instanceName,
|
||||
stats: null,
|
||||
initStats: () => os.api('stats', {
|
||||
}).then((stats) => {
|
||||
this.stats = stats;
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
number
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@@ -171,11 +171,6 @@ export default defineComponent({
|
||||
text: i18n.locale.security,
|
||||
to: '/admin/security',
|
||||
active: page.value === 'security',
|
||||
}, {
|
||||
icon: 'fas fa-bolt',
|
||||
text: 'ServiceWorker',
|
||||
to: '/admin/service-worker',
|
||||
active: page.value === 'service-worker',
|
||||
}, {
|
||||
icon: 'fas fa-globe',
|
||||
text: i18n.locale.relays,
|
||||
@@ -228,7 +223,6 @@ export default defineComponent({
|
||||
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
|
||||
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
|
||||
case 'security': return defineAsyncComponent(() => import('./security.vue'));
|
||||
case 'service-worker': return defineAsyncComponent(() => import('./service-worker.vue'));
|
||||
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
|
||||
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
|
||||
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
|
||||
|
@@ -1,21 +1,7 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormSection>
|
||||
<FormInput v-model="summalyProxy" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>Summaly Proxy URL</template>
|
||||
</FormInput>
|
||||
</FormSection>
|
||||
<FormSection>
|
||||
<FormInput v-model="deeplAuthKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>DeepL Auth Key</template>
|
||||
</FormInput>
|
||||
<FormSwitch v-model="deeplIsPro" class="_formBlock">
|
||||
<template #label>Pro account</template>
|
||||
</FormSwitch>
|
||||
</FormSection>
|
||||
none
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
@@ -53,9 +39,6 @@ export default defineComponent({
|
||||
handler: this.save,
|
||||
}],
|
||||
},
|
||||
summalyProxy: '',
|
||||
deeplAuthKey: '',
|
||||
deeplIsPro: false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -66,15 +49,9 @@ export default defineComponent({
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('meta', { detail: true });
|
||||
this.summalyProxy = meta.summalyProxy;
|
||||
this.deeplAuthKey = meta.deeplAuthKey;
|
||||
this.deeplIsPro = meta.deeplIsPro;
|
||||
},
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
summalyProxy: this.summalyProxy,
|
||||
deeplAuthKey: this.deeplAuthKey,
|
||||
deeplIsPro: this.deeplIsPro,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
@@ -11,6 +11,19 @@
|
||||
|
||||
<XBotProtection/>
|
||||
</FormFolder>
|
||||
|
||||
<FormFolder class="_formBlock">
|
||||
<template #label>Summaly Proxy</template>
|
||||
|
||||
<div class="_formRoot">
|
||||
<FormInput v-model="summalyProxy" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>Summaly Proxy URL</template>
|
||||
</FormInput>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
</div>
|
||||
</FormFolder>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
@@ -23,6 +36,8 @@ import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import XBotProtection from './bot-protection.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
@@ -35,6 +50,8 @@ export default defineComponent({
|
||||
FormInfo,
|
||||
FormSection,
|
||||
FormSuspense,
|
||||
FormButton,
|
||||
FormInput,
|
||||
XBotProtection,
|
||||
},
|
||||
|
||||
@@ -47,6 +64,7 @@ export default defineComponent({
|
||||
icon: 'fas fa-lock',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
summalyProxy: '',
|
||||
enableHcaptcha: false,
|
||||
enableRecaptcha: false,
|
||||
}
|
||||
@@ -59,9 +77,18 @@ export default defineComponent({
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('meta', { detail: true });
|
||||
this.summalyProxy = meta.summalyProxy;
|
||||
this.enableHcaptcha = meta.enableHcaptcha;
|
||||
this.enableRecaptcha = meta.enableRecaptcha;
|
||||
},
|
||||
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
summalyProxy: this.summalyProxy,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormSwitch v-model="enableServiceWorker" class="_formBlock">
|
||||
<template #label>{{ $ts.enableServiceworker }}</template>
|
||||
<template #caption>{{ $ts.serviceworkerInfo }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableServiceWorker">
|
||||
<FormInput v-model="swPublicKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Public key</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="swPrivateKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Private key</template>
|
||||
</FormInput>
|
||||
</template>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormInput,
|
||||
FormButton,
|
||||
FormSuspense,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'ServiceWorker',
|
||||
icon: 'fas fa-bolt',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
enableServiceWorker: false,
|
||||
swPublicKey: null,
|
||||
swPrivateKey: null,
|
||||
}
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('meta', { detail: true });
|
||||
this.enableServiceWorker = meta.enableServiceWorker;
|
||||
this.swPublicKey = meta.swPublickey;
|
||||
this.swPrivateKey = meta.swPrivateKey;
|
||||
},
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
enableServiceWorker: this.enableServiceWorker,
|
||||
swPublicKey: this.swPublicKey,
|
||||
swPrivateKey: this.swPrivateKey,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@@ -94,6 +94,39 @@
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>ServiceWorker</template>
|
||||
|
||||
<FormSwitch v-model="enableServiceWorker" class="_formBlock">
|
||||
<template #label>{{ $ts.enableServiceworker }}</template>
|
||||
<template #caption>{{ $ts.serviceworkerInfo }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableServiceWorker">
|
||||
<FormInput v-model="swPublicKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Public key</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="swPrivateKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>Private key</template>
|
||||
</FormInput>
|
||||
</template>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>DeepL Translation</template>
|
||||
|
||||
<FormInput v-model="deeplAuthKey" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-key"></i></template>
|
||||
<template #label>DeepL Auth Key</template>
|
||||
</FormInput>
|
||||
<FormSwitch v-model="deeplIsPro" class="_formBlock">
|
||||
<template #label>Pro account</template>
|
||||
</FormSwitch>
|
||||
</FormSection>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
@@ -156,6 +189,11 @@ export default defineComponent({
|
||||
remoteDriveCapacityMb: 0,
|
||||
enableRegistration: false,
|
||||
emailRequiredForSignup: false,
|
||||
enableServiceWorker: false,
|
||||
swPublicKey: null,
|
||||
swPrivateKey: null,
|
||||
deeplAuthKey: '',
|
||||
deeplIsPro: false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -184,6 +222,11 @@ export default defineComponent({
|
||||
this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
|
||||
this.enableRegistration = !meta.disableRegistration;
|
||||
this.emailRequiredForSignup = meta.emailRequiredForSignup;
|
||||
this.enableServiceWorker = meta.enableServiceWorker;
|
||||
this.swPublicKey = meta.swPublickey;
|
||||
this.swPrivateKey = meta.swPrivateKey;
|
||||
this.deeplAuthKey = meta.deeplAuthKey;
|
||||
this.deeplIsPro = meta.deeplIsPro;
|
||||
},
|
||||
|
||||
save() {
|
||||
@@ -206,6 +249,11 @@ export default defineComponent({
|
||||
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
|
||||
disableRegistration: !this.enableRegistration,
|
||||
emailRequiredForSignup: this.emailRequiredForSignup,
|
||||
enableServiceWorker: this.enableServiceWorker,
|
||||
swPublicKey: this.swPublicKey,
|
||||
swPrivateKey: this.swPrivateKey,
|
||||
deeplAuthKey: this.deeplAuthKey,
|
||||
deeplIsPro: this.deeplIsPro,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
|
@@ -1,349 +0,0 @@
|
||||
<template>
|
||||
<div class="t9makv94">
|
||||
<section class="_section">
|
||||
<div class="_content">
|
||||
<details>
|
||||
<summary>{{ $ts.import }}</summary>
|
||||
<MkTextarea v-model="themeToImport">
|
||||
{{ $ts._theme.importInfo }}
|
||||
</MkTextarea>
|
||||
<MkButton :disabled="!themeToImport.trim()" @click="importTheme">{{ $ts.import }}</MkButton>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_section">
|
||||
<div class="_content _card _gap">
|
||||
<div class="_content">
|
||||
<MkInput v-model="name" required><span>{{ $ts.name }}</span></MkInput>
|
||||
<MkInput v-model="author" required><span>{{ $ts.author }}</span></MkInput>
|
||||
<MkTextarea v-model="description"><span>{{ $ts.description }}</span></MkTextarea>
|
||||
<div class="_inputs">
|
||||
<div v-text="$ts._theme.base" />
|
||||
<MkRadio v-model="baseTheme" value="light">{{ $ts.light }}</MkRadio>
|
||||
<MkRadio v-model="baseTheme" value="dark">{{ $ts.dark }}</MkRadio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="_content _card _gap">
|
||||
<div class="list-view _content">
|
||||
<div v-for="([ k, v ], i) in theme" :key="k" class="item">
|
||||
<div class="_inputs">
|
||||
<div>
|
||||
{{ k.startsWith('$') ? `${k} (${$ts._theme.constant})` : $t('_theme.keys.' + k) }}
|
||||
<button v-if="k.startsWith('$')" class="_button _link" @click="del(i)" v-text="$ts.delete" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="type" @click="chooseType($event, i)">
|
||||
{{ getTypeOf(v) }} <i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<!-- default -->
|
||||
<div v-if="v === null" class="default-value" v-text="baseProps[k]" />
|
||||
<!-- color -->
|
||||
<div v-else-if="typeof v === 'string'" class="color">
|
||||
<input type="color" :value="v" @input="colorChanged($event.target.value, i)"/>
|
||||
<MkInput class="select" :value="v" @update:modelValue="colorChanged($event, i)"/>
|
||||
</div>
|
||||
<!-- ref const -->
|
||||
<MkInput v-else-if="v.type === 'refConst'" v-model="v.key">
|
||||
<template #prefix>$</template>
|
||||
<span>{{ $ts.name }}</span>
|
||||
</MkInput>
|
||||
<!-- ref props -->
|
||||
<MkSelect v-else-if="v.type === 'refProp'" v-model="v.key" class="select">
|
||||
<option v-for="key in themeProps" :key="key" :value="key">{{ $t('_theme.keys.' + key) }}</option>
|
||||
</MkSelect>
|
||||
<!-- func -->
|
||||
<template v-else-if="v.type === 'func'">
|
||||
<MkSelect v-model="v.name" class="select">
|
||||
<template #label>{{ $ts._theme.funcKind }}</template>
|
||||
<option v-for="n in ['alpha', 'darken', 'lighten']" :key="n" :value="n">{{ $t('_theme.' + n) }}</option>
|
||||
</MkSelect>
|
||||
<MkInput v-model="v.arg" type="number"><span>{{ $ts._theme.argument }}</span></MkInput>
|
||||
<MkSelect v-model="v.value" class="select">
|
||||
<template #label>{{ $ts._theme.basedProp }}</template>
|
||||
<option v-for="key in themeProps" :key="key" :value="key">{{ $t('_theme.keys.' + key) }}</option>
|
||||
</MkSelect>
|
||||
</template>
|
||||
<!-- CSS -->
|
||||
<MkInput v-else-if="v.type === 'css'" v-model="v.value">
|
||||
<span>CSS</span>
|
||||
</MkInput>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MkButton primary @click="addConst">{{ $ts._theme.addConstant }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="_section">
|
||||
<details class="_content">
|
||||
<summary>{{ $ts.sample }}</summary>
|
||||
<MkSample/>
|
||||
</details>
|
||||
</section>
|
||||
<section class="_section">
|
||||
<div class="_content">
|
||||
<MkButton inline @click="preview">{{ $ts.preview }}</MkButton>
|
||||
<MkButton inline primary :disabled="!name || !author" @click="save">{{ $ts.save }}</MkButton>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as JSON5 from 'json5';
|
||||
import { toUnicode } from 'punycode/';
|
||||
|
||||
import MkRadio from '@/components/form/radio.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import MkSample from '@/components/sample.vue';
|
||||
|
||||
import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@/scripts/theme-editor';
|
||||
import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@/scripts/theme';
|
||||
import { host } from '@/config';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import { addTheme } from '@/theme-store';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkRadio,
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkTextarea,
|
||||
MkSelect,
|
||||
MkSample,
|
||||
},
|
||||
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
if (this.changed && !(await this.confirm())) {
|
||||
next(false);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.themeEditor,
|
||||
icon: 'fas fa-palette',
|
||||
},
|
||||
theme: [] as ThemeViewModel,
|
||||
name: '',
|
||||
description: '',
|
||||
baseTheme: 'light' as 'dark' | 'light',
|
||||
author: `@${this.$i.username}@${toUnicode(host)}`,
|
||||
themeToImport: '',
|
||||
changed: false,
|
||||
lightTheme, darkTheme, themeProps,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
baseProps() {
|
||||
return this.baseTheme === 'light' ? this.lightTheme.props : this.darkTheme.props;
|
||||
},
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('beforeunload', this.beforeunload);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
window.addEventListener('beforeunload', this.beforeunload);
|
||||
const changed = () => this.changed = true;
|
||||
this.$watch('name', changed);
|
||||
this.$watch('description', changed);
|
||||
this.$watch('baseTheme', changed);
|
||||
this.$watch('author', changed);
|
||||
this.$watch('theme', changed);
|
||||
},
|
||||
|
||||
methods: {
|
||||
beforeunload(e: BeforeUnloadEvent) {
|
||||
if (this.changed) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
},
|
||||
|
||||
async confirm(): Promise<boolean> {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$ts.leaveConfirm,
|
||||
});
|
||||
return !canceled;
|
||||
},
|
||||
|
||||
init() {
|
||||
const t: ThemeViewModel = [];
|
||||
for (const key of themeProps) {
|
||||
t.push([ key, null ]);
|
||||
}
|
||||
this.theme = t;
|
||||
},
|
||||
|
||||
async del(i: number) {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$t('_theme.deleteConstantConfirm', { const: this.theme[i][0] }),
|
||||
});
|
||||
if (canceled) return;
|
||||
Vue.delete(this.theme, i);
|
||||
},
|
||||
|
||||
async addConst() {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: this.$ts._theme.inputConstantName,
|
||||
});
|
||||
if (canceled) return;
|
||||
this.theme.push([ '$' + result, '#000000']);
|
||||
},
|
||||
|
||||
save() {
|
||||
const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
|
||||
addTheme(theme);
|
||||
os.alert({
|
||||
type: 'success',
|
||||
text: this.$t('_theme.installed', { name: theme.name })
|
||||
});
|
||||
this.changed = false;
|
||||
},
|
||||
|
||||
preview() {
|
||||
const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
|
||||
try {
|
||||
applyTheme(theme, false);
|
||||
} catch (e) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async importTheme() {
|
||||
if (this.changed && (!await this.confirm())) return;
|
||||
|
||||
try {
|
||||
const theme = JSON5.parse(this.themeToImport) as Theme;
|
||||
if (!validateTheme(theme)) throw new Error(this.$ts._theme.invalid);
|
||||
|
||||
this.name = theme.name;
|
||||
this.description = theme.desc || '';
|
||||
this.author = theme.author;
|
||||
this.baseTheme = theme.base || 'light';
|
||||
this.theme = convertToViewModel(theme);
|
||||
this.themeToImport = '';
|
||||
} catch (e) {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
colorChanged(color: string, i: number) {
|
||||
this.theme[i] = [this.theme[i][0], color];
|
||||
},
|
||||
|
||||
getTypeOf(v: ThemeValue) {
|
||||
return v === null
|
||||
? this.$ts._theme.defaultValue
|
||||
: typeof v === 'string'
|
||||
? this.$ts._theme.color
|
||||
: this.$t('_theme.' + v.type);
|
||||
},
|
||||
|
||||
async chooseType(e: MouseEvent, i: number) {
|
||||
const newValue = await this.showTypeMenu(e);
|
||||
this.theme[i] = [ this.theme[i][0], newValue ];
|
||||
},
|
||||
|
||||
showTypeMenu(e: MouseEvent) {
|
||||
return new Promise<ThemeValue>((resolve) => {
|
||||
os.popupMenu([{
|
||||
text: this.$ts._theme.defaultValue,
|
||||
action: () => resolve(null),
|
||||
}, {
|
||||
text: this.$ts._theme.color,
|
||||
action: () => resolve('#000000'),
|
||||
}, {
|
||||
text: this.$ts._theme.func,
|
||||
action: () => resolve({
|
||||
type: 'func', name: 'alpha', arg: 1, value: 'accent'
|
||||
}),
|
||||
}, {
|
||||
text: this.$ts._theme.refProp,
|
||||
action: () => resolve({
|
||||
type: 'refProp', key: 'accent',
|
||||
}),
|
||||
}, {
|
||||
text: this.$ts._theme.refConst,
|
||||
action: () => resolve({
|
||||
type: 'refConst', key: '',
|
||||
}),
|
||||
}, {
|
||||
text: 'CSS',
|
||||
action: () => resolve({
|
||||
type: 'css', value: '',
|
||||
}),
|
||||
}], e.currentTarget || e.target);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.t9makv94 {
|
||||
> ._section {
|
||||
> ._content {
|
||||
> .list-view {
|
||||
> .item {
|
||||
min-height: 48px;
|
||||
word-break: break-all;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.select {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.type {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.default-value {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.color {
|
||||
> input {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-left: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,49 +1,26 @@
|
||||
<template>
|
||||
<div class="jmelgwjh">
|
||||
<div class="body">
|
||||
<XNotes class="notes" :pagination="pagination" :detail="true" :prop="'note'"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkSpacer :content-max="800">
|
||||
<XNotes :pagination="pagination" :detail="true" :prop="'note'"/>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNotes
|
||||
},
|
||||
const pagination = {
|
||||
endpoint: 'i/favorites',
|
||||
limit: 10,
|
||||
params: () => ({
|
||||
}),
|
||||
};
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.favorites,
|
||||
icon: 'fas fa-star',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
pagination: {
|
||||
endpoint: 'i/favorites',
|
||||
limit: 10,
|
||||
params: () => ({
|
||||
})
|
||||
},
|
||||
};
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.locale.favorites,
|
||||
icon: 'fas fa-star',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.jmelgwjh {
|
||||
background: var(--bg);
|
||||
|
||||
> .body {
|
||||
box-sizing: border-box;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -4,29 +4,22 @@
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNotes
|
||||
},
|
||||
const pagination = {
|
||||
endpoint: 'notes/featured',
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
};
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
pagination: {
|
||||
endpoint: 'notes/featured',
|
||||
limit: 10,
|
||||
offsetMode: true,
|
||||
},
|
||||
};
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.locale.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@@ -10,7 +10,7 @@
|
||||
</FormTextarea>
|
||||
|
||||
<FormGroup>
|
||||
<div v-for="file in files" :key="file.id" class="_debobigegoItem _debobigegoPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
|
||||
<div v-for="file in files" :key="file.id" class="_formGroup wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
|
||||
<div class="name">{{ file.name }}</div>
|
||||
<button v-tooltip="$ts.remove" class="remove _button" @click="remove(file)"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
|
@@ -4,28 +4,21 @@
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNotes
|
||||
},
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions',
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.mentions,
|
||||
icon: 'fas fa-at',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
pagination: {
|
||||
endpoint: 'notes/mentions',
|
||||
limit: 10,
|
||||
},
|
||||
};
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.locale.mentions,
|
||||
icon: 'fas fa-at',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@@ -4,31 +4,24 @@
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import XNotes from '@/components/notes.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XNotes
|
||||
},
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions',
|
||||
limit: 10,
|
||||
params: () => ({
|
||||
visibility: 'specified'
|
||||
}),
|
||||
};
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.directNotes,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
pagination: {
|
||||
endpoint: 'notes/mentions',
|
||||
limit: 10,
|
||||
params: () => ({
|
||||
visibility: 'specified'
|
||||
})
|
||||
},
|
||||
};
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.locale.directNotes,
|
||||
icon: 'fas fa-envelope',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@@ -7,19 +7,15 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as os from '@/os';
|
||||
<script lang="ts" setup>
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.notFound,
|
||||
icon: 'fas fa-exclamation-triangle'
|
||||
},
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.locale.notFound,
|
||||
icon: 'fas fa-exclamation-triangle',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
@@ -1,107 +0,0 @@
|
||||
<template>
|
||||
<canvas width="224" height="128"></canvas>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as THREE from 'three';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
selected: null,
|
||||
objectHeight: 0,
|
||||
orbitRadius: 5
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const canvas = this.$el;
|
||||
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvas,
|
||||
antialias: true,
|
||||
alpha: false
|
||||
});
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(width, height);
|
||||
renderer.setClearColor(0x000000);
|
||||
renderer.autoClear = false;
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.cullFace = THREE.CullFaceBack;
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100);
|
||||
camera.zoom = 10;
|
||||
camera.position.x = 0;
|
||||
camera.position.y = 2;
|
||||
camera.position.z = 0;
|
||||
camera.updateProjectionMatrix();
|
||||
scene.add(camera);
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
|
||||
ambientLight.castShadow = false;
|
||||
scene.add(ambientLight);
|
||||
|
||||
const light = new THREE.PointLight(0xffffff, 1, 100);
|
||||
light.position.set(3, 3, 3);
|
||||
scene.add(light);
|
||||
|
||||
const grid = new THREE.GridHelper(5, 16, 0x444444, 0x222222);
|
||||
scene.add(grid);
|
||||
|
||||
const render = () => {
|
||||
const timer = Date.now() * 0.0004;
|
||||
requestAnimationFrame(render);
|
||||
|
||||
camera.position.y = Math.sin(Math.PI / 6) * this.orbitRadius; // Math.PI / 6 => 30deg
|
||||
camera.position.z = Math.cos(timer) * this.orbitRadius;
|
||||
camera.position.x = Math.sin(timer) * this.orbitRadius;
|
||||
camera.lookAt(new THREE.Vector3(0, this.objectHeight / 2, 0));
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
this.selected = selected => {
|
||||
const obj = selected.clone();
|
||||
|
||||
// Remove current object
|
||||
const current = scene.getObjectByName('obj');
|
||||
if (current != null) {
|
||||
scene.remove(current);
|
||||
}
|
||||
|
||||
// Add new object
|
||||
obj.name = 'obj';
|
||||
obj.position.x = 0;
|
||||
obj.position.y = 0;
|
||||
obj.position.z = 0;
|
||||
obj.rotation.x = 0;
|
||||
obj.rotation.y = 0;
|
||||
obj.rotation.z = 0;
|
||||
obj.traverse(child => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.material = child.material.clone();
|
||||
return child.material.emissive.setHex(0x000000);
|
||||
}
|
||||
});
|
||||
const objectBoundingBox = new THREE.Box3().setFromObject(obj);
|
||||
this.objectHeight = objectBoundingBox.max.y - objectBoundingBox.min.y;
|
||||
|
||||
const objectWidth = objectBoundingBox.max.x - objectBoundingBox.min.x;
|
||||
const objectDepth = objectBoundingBox.max.z - objectBoundingBox.min.z;
|
||||
|
||||
const horizontal = Math.hypot(objectWidth, objectDepth) / camera.aspect;
|
||||
this.orbitRadius = Math.max(horizontal, this.objectHeight) * camera.zoom * 0.625 / Math.tan(camera.fov * 0.5 * (Math.PI / 180));
|
||||
|
||||
scene.add(obj);
|
||||
};
|
||||
|
||||
render();
|
||||
},
|
||||
});
|
||||
</script>
|
@@ -1,279 +0,0 @@
|
||||
<template>
|
||||
<div class="hveuntkp">
|
||||
<div v-if="objectSelected" class="controller _section">
|
||||
<div class="_content">
|
||||
<p class="name">{{ selectedFurnitureName }}</p>
|
||||
<XPreview ref="preview"/>
|
||||
<template v-if="selectedFurnitureInfo.props">
|
||||
<div v-for="k in Object.keys(selectedFurnitureInfo.props)" :key="k">
|
||||
<p>{{ k }}</p>
|
||||
<template v-if="selectedFurnitureInfo.props[k] === 'image'">
|
||||
<MkButton @click="chooseImage(k, $event)">{{ $ts._rooms.chooseImage }}</MkButton>
|
||||
</template>
|
||||
<template v-else-if="selectedFurnitureInfo.props[k] === 'color'">
|
||||
<input type="color" :value="selectedFurnitureProps ? selectedFurnitureProps[k] : null" @change="updateColor(k, $event)"/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<MkButton inline :primary="isTranslateMode" @click="translate()"><i class="fas fa-arrows-alt"></i> {{ $ts._rooms.translate }}</MkButton>
|
||||
<MkButton inline :primary="isRotateMode" @click="rotate()"><i class="fas fa-undo"></i> {{ $ts._rooms.rotate }}</MkButton>
|
||||
<MkButton v-if="isTranslateMode || isRotateMode" inline @click="exit()"><i class="fas fa-ban"></i> {{ $ts._rooms.exit }}</MkButton>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<MkButton @click="remove()"><i class="fas fa-trash-alt"></i> {{ $ts._rooms.remove }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isMyRoom" class="menu _section">
|
||||
<div class="_content">
|
||||
<MkButton @click="add()"><i class="fas fa-box-open"></i> {{ $ts._rooms.addFurniture }}</MkButton>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<MkSelect :model-value="roomType" @update:modelValue="updateRoomType($event)">
|
||||
<template #label>{{ $ts._rooms.roomType }}</template>
|
||||
<option value="default">{{ $ts._rooms._roomType.default }}</option>
|
||||
<option value="washitsu">{{ $ts._rooms._roomType.washitsu }}</option>
|
||||
</MkSelect>
|
||||
<label v-if="roomType === 'default'">
|
||||
<span>{{ $ts._rooms.carpetColor }}</span>
|
||||
<input type="color" :value="carpetColor" @change="updateCarpetColor($event)"/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<MkButton inline :disabled="!changed" primary @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
|
||||
<MkButton inline @click="clear()"><i class="fas fa-broom"></i> {{ $ts._rooms.clear }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { Room } from '@/scripts/room/room';
|
||||
import * as Acct from 'misskey-js/built/acct';
|
||||
import XPreview from './preview.vue';
|
||||
const storeItems = require('@/scripts/room/furnitures.json5');
|
||||
import { query as urlQuery } from '@/scripts/url';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkSelect from '@/components/form/select.vue';
|
||||
import { selectFile } from '@/scripts/select-file';
|
||||
import * as os from '@/os';
|
||||
import { ColdDeviceStorage } from '@/store';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
let room: Room;
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XPreview,
|
||||
MkButton,
|
||||
MkSelect,
|
||||
},
|
||||
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (this.changed) {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$ts.leaveConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) {
|
||||
next(false);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
acct: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: computed(() => this.user ? {
|
||||
title: this.$ts.room,
|
||||
avatar: this.user,
|
||||
} : null),
|
||||
user: null,
|
||||
objectSelected: false,
|
||||
selectedFurnitureName: null,
|
||||
selectedFurnitureInfo: null,
|
||||
selectedFurnitureProps: null,
|
||||
roomType: null,
|
||||
carpetColor: null,
|
||||
isTranslateMode: false,
|
||||
isRotateMode: false,
|
||||
isMyRoom: false,
|
||||
changed: false,
|
||||
};
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
window.addEventListener('beforeunload', this.beforeunload);
|
||||
|
||||
this.user = await os.api('users/show', {
|
||||
...Acct.parse(this.acct)
|
||||
});
|
||||
|
||||
this.isMyRoom = this.$i && (this.$i.id === this.user.id);
|
||||
|
||||
const roomInfo = await os.api('room/show', {
|
||||
userId: this.user.id
|
||||
});
|
||||
|
||||
this.roomType = roomInfo.roomType;
|
||||
this.carpetColor = roomInfo.carpetColor;
|
||||
|
||||
room = new Room(this.user, this.isMyRoom, roomInfo, this.$el, {
|
||||
graphicsQuality: ColdDeviceStorage.get('roomGraphicsQuality'),
|
||||
onChangeSelect: obj => {
|
||||
this.objectSelected = obj != null;
|
||||
if (obj) {
|
||||
const f = room.findFurnitureById(obj.name);
|
||||
this.selectedFurnitureName = this.$t('_rooms._furnitures.' + f.type);
|
||||
this.selectedFurnitureInfo = storeItems.find(x => x.id === f.type);
|
||||
this.selectedFurnitureProps = f.props
|
||||
? JSON.parse(JSON.stringify(f.props)) // Disable reactivity
|
||||
: null;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.preview.selected(obj);
|
||||
});
|
||||
}
|
||||
},
|
||||
useOrthographicCamera: ColdDeviceStorage.get('roomUseOrthographicCamera'),
|
||||
});
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
room.destroy();
|
||||
window.removeEventListener('beforeunload', this.beforeunload);
|
||||
},
|
||||
|
||||
methods: {
|
||||
beforeunload(e: BeforeUnloadEvent) {
|
||||
if (this.changed) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
},
|
||||
|
||||
async add() {
|
||||
const { canceled, result: id } = await os.select({
|
||||
title: this.$ts._rooms.addFurniture,
|
||||
items: storeItems.map(item => ({
|
||||
value: item.id, text: this.$t('_rooms._furnitures.' + item.id)
|
||||
}))
|
||||
});
|
||||
if (canceled) return;
|
||||
room.addFurniture(id);
|
||||
this.changed = true;
|
||||
},
|
||||
|
||||
remove() {
|
||||
this.isTranslateMode = false;
|
||||
this.isRotateMode = false;
|
||||
room.removeFurniture();
|
||||
this.changed = true;
|
||||
},
|
||||
|
||||
save() {
|
||||
os.api('room/update', {
|
||||
room: room.getRoomInfo()
|
||||
}).then(() => {
|
||||
this.changed = false;
|
||||
os.success();
|
||||
}).catch((e: any) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e.message
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
clear() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$ts._rooms.clearConfirm,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
room.removeAllFurnitures();
|
||||
this.changed = true;
|
||||
});
|
||||
},
|
||||
|
||||
chooseImage(key, e) {
|
||||
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;
|
||||
});
|
||||
},
|
||||
|
||||
updateColor(key, ev) {
|
||||
room.updateProp(key, ev.target.value);
|
||||
this.$refs.preview.selected(room.getSelectedObject());
|
||||
this.changed = true;
|
||||
},
|
||||
|
||||
updateCarpetColor(ev) {
|
||||
room.updateCarpetColor(ev.target.value);
|
||||
this.carpetColor = ev.target.value;
|
||||
this.changed = true;
|
||||
},
|
||||
|
||||
updateRoomType(type) {
|
||||
room.changeRoomType(type);
|
||||
this.roomType = type;
|
||||
this.changed = true;
|
||||
},
|
||||
|
||||
translate() {
|
||||
if (this.isTranslateMode) {
|
||||
this.exit();
|
||||
} else {
|
||||
this.isRotateMode = false;
|
||||
this.isTranslateMode = true;
|
||||
room.enterTransformMode('translate');
|
||||
}
|
||||
this.changed = true;
|
||||
},
|
||||
|
||||
rotate() {
|
||||
if (this.isRotateMode) {
|
||||
this.exit();
|
||||
} else {
|
||||
this.isTranslateMode = false;
|
||||
this.isRotateMode = true;
|
||||
room.enterTransformMode('rotate');
|
||||
}
|
||||
this.changed = true;
|
||||
},
|
||||
|
||||
exit() {
|
||||
this.isTranslateMode = false;
|
||||
this.isRotateMode = false;
|
||||
room.exitTransformMode();
|
||||
this.changed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hveuntkp {
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
|
||||
> ::v-deep(canvas) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -63,11 +63,6 @@ export default defineComponent({
|
||||
injectFeaturedNote: v
|
||||
});
|
||||
},
|
||||
|
||||
taskmanager() {
|
||||
os.popup(import('@/components/taskmanager.vue'), {
|
||||
}, {}, 'closed');
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@@ -1,260 +0,0 @@
|
||||
<template>
|
||||
<div class="_section">
|
||||
<div class="_content">
|
||||
<div class="_card _gap">
|
||||
<div class="_title">Dialog</div>
|
||||
<div class="_content">
|
||||
<MkInput v-model="dialogTitle">
|
||||
<template #label>Title</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="dialogBody">
|
||||
<template #label>Body</template>
|
||||
</MkInput>
|
||||
<MkRadio v-model="dialogType" value="info">Info</MkRadio>
|
||||
<MkRadio v-model="dialogType" value="success">Success</MkRadio>
|
||||
<MkRadio v-model="dialogType" value="warning">Warn</MkRadio>
|
||||
<MkRadio v-model="dialogType" value="error">Error</MkRadio>
|
||||
<MkSwitch v-model="dialogCancel">
|
||||
<span>With cancel button</span>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="dialogCancelByBgClick">
|
||||
<span>Can cancel by modal bg click</span>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="dialogInput">
|
||||
<span>With input field</span>
|
||||
</MkSwitch>
|
||||
<MkButton @click="showDialog()">Show</MkButton>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<code>Result: {{ dialogResult }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="_card _gap">
|
||||
<div class="_title">Form</div>
|
||||
<div class="_content">
|
||||
<MkInput v-model="formTitle">
|
||||
<template #label>Title</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="formForm">
|
||||
<template #label>Form</template>
|
||||
</MkTextarea>
|
||||
<MkButton @click="form()">Show</MkButton>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<code>Result: {{ formResult }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="_card _gap">
|
||||
<div class="_title">MFM</div>
|
||||
<div class="_content">
|
||||
<MkTextarea v-model="mfm">
|
||||
<template #label>MFM</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<Mfm :text="mfm"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="_card _gap">
|
||||
<div class="_title">selectDriveFile</div>
|
||||
<div class="_content">
|
||||
<MkSwitch v-model="selectDriveFileMultiple">
|
||||
<span>Multiple</span>
|
||||
</MkSwitch>
|
||||
<MkButton @click="selectDriveFile()">selectDriveFile</MkButton>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<code>Result: {{ JSON.stringify(selectDriveFileResult) }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="_card _gap">
|
||||
<div class="_title">selectDriveFolder</div>
|
||||
<div class="_content">
|
||||
<MkSwitch v-model="selectDriveFolderMultiple">
|
||||
<span>Multiple</span>
|
||||
</MkSwitch>
|
||||
<MkButton @click="selectDriveFolder()">selectDriveFolder</MkButton>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<code>Result: {{ JSON.stringify(selectDriveFolderResult) }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="_card _gap">
|
||||
<div class="_title">selectUser</div>
|
||||
<div class="_content">
|
||||
<MkButton @click="selectUser()">selectUser</MkButton>
|
||||
</div>
|
||||
<div class="_content">
|
||||
<code>Result: {{ user }}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="_card _gap">
|
||||
<div class="_title">Notification</div>
|
||||
<div class="_content">
|
||||
<MkInput v-model="notificationIconUrl">
|
||||
<template #label>Icon URL</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="notificationHeader">
|
||||
<template #label>Header</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="notificationBody">
|
||||
<template #label>Body</template>
|
||||
</MkTextarea>
|
||||
<MkButton @click="createNotification()">createNotification</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="_card _gap">
|
||||
<div class="_title">Waiting dialog</div>
|
||||
<div class="_content">
|
||||
<MkButton inline @click="openWaitingDialog()">icon only</MkButton>
|
||||
<MkButton inline @click="openWaitingDialog('Doing')">with text</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="_card _gap">
|
||||
<div class="_title">Messaging window</div>
|
||||
<div class="_content">
|
||||
<MkButton @click="messagingWindowOpen()">open</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MkButton @click="resetTutorial()">Reset tutorial</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import MkTextarea from '@/components/form/textarea.vue';
|
||||
import MkRadio from '@/components/form/radio.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
MkSwitch,
|
||||
MkTextarea,
|
||||
MkRadio,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'TEST',
|
||||
icon: 'fas fa-exclamation-triangle'
|
||||
},
|
||||
dialogTitle: 'Hello',
|
||||
dialogBody: 'World!',
|
||||
dialogType: 'info',
|
||||
dialogCancel: false,
|
||||
dialogCancelByBgClick: true,
|
||||
dialogInput: false,
|
||||
dialogResult: null,
|
||||
formTitle: 'Test form',
|
||||
formForm: JSON.stringify({
|
||||
foo: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
label: 'This is a boolean property'
|
||||
},
|
||||
bar: {
|
||||
type: 'number',
|
||||
default: 300,
|
||||
label: 'This is a number property'
|
||||
},
|
||||
baz: {
|
||||
type: 'string',
|
||||
default: 'Misskey makes you happy.',
|
||||
label: 'This is a string property'
|
||||
},
|
||||
qux: {
|
||||
type: 'string',
|
||||
multiline: true,
|
||||
default: 'Misskey makes\nyou happy.',
|
||||
label: 'Multiline string'
|
||||
},
|
||||
}, null, '\t'),
|
||||
formResult: null,
|
||||
mfm: '',
|
||||
selectDriveFileMultiple: false,
|
||||
selectDriveFolderMultiple: false,
|
||||
selectDriveFileResult: null,
|
||||
selectDriveFolderResult: null,
|
||||
user: null,
|
||||
notificationIconUrl: null,
|
||||
notificationHeader: '',
|
||||
notificationBody: '',
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async showDialog() {
|
||||
this.dialogResult = null;
|
||||
/*
|
||||
this.dialogResult = await os.dialog({
|
||||
type: this.dialogType,
|
||||
title: this.dialogTitle,
|
||||
text: this.dialogBody,
|
||||
showCancelButton: this.dialogCancel,
|
||||
cancelableByBgClick: this.dialogCancelByBgClick,
|
||||
input: this.dialogInput ? {} : null
|
||||
});*/
|
||||
},
|
||||
|
||||
async form() {
|
||||
this.formResult = null;
|
||||
this.formResult = await os.form(this.formTitle, JSON.parse(this.formForm));
|
||||
},
|
||||
|
||||
async selectDriveFile() {
|
||||
this.selectDriveFileResult = null;
|
||||
this.selectDriveFileResult = await os.selectDriveFile(this.selectDriveFileMultiple);
|
||||
},
|
||||
|
||||
async selectDriveFolder() {
|
||||
this.selectDriveFolderResult = null;
|
||||
this.selectDriveFolderResult = await os.selectDriveFolder(this.selectDriveFolderMultiple);
|
||||
},
|
||||
|
||||
async selectUser() {
|
||||
this.user = null;
|
||||
this.user = await os.selectUser();
|
||||
},
|
||||
|
||||
async createNotification() {
|
||||
os.api('notifications/create', {
|
||||
header: this.notificationHeader,
|
||||
body: this.notificationBody,
|
||||
icon: this.notificationIconUrl,
|
||||
});
|
||||
},
|
||||
|
||||
messagingWindowOpen() {
|
||||
os.pageWindow('/my/messaging');
|
||||
},
|
||||
|
||||
openWaitingDialog(text?) {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, 2000);
|
||||
});
|
||||
os.promiseDialog(promise, null, null, text);
|
||||
},
|
||||
|
||||
resetTutorial() {
|
||||
this.$store.set('tutorial', 0);
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
@@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="_section">
|
||||
<div class="_content" style="text-align: center;">
|
||||
<img src="/static-assets/icons/512.png" alt="" style="display: block; width: 100px; margin: 0 auto; border-radius: 16px;"/>
|
||||
<div style="margin-top: 0.75em;">Misskey</div>
|
||||
<div style="opacity: 0.5;">v{{ version }}</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { version } from '@/config';
|
||||
import * as symbols from '@/symbols';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'Misskey',
|
||||
icon: null
|
||||
},
|
||||
version,
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
@@ -20,7 +20,6 @@ const defaultRoutes = [
|
||||
{ path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) },
|
||||
{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
|
||||
{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
|
||||
{ path: '/@:acct/room', props: true, component: page('room/room') },
|
||||
{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) },
|
||||
{ path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) },
|
||||
{ path: '/signup-complete/:code', component: page('signup-complete'), props: route => ({ code: route.params.code }) },
|
||||
|
@@ -1,21 +0,0 @@
|
||||
export type RoomInfo = {
|
||||
roomType: string;
|
||||
carpetColor: string;
|
||||
furnitures: Furniture[];
|
||||
};
|
||||
|
||||
export type Furniture = {
|
||||
id: string; // 同じ家具が複数ある場合にそれぞれを識別するためのIDであり、家具IDではない
|
||||
type: string; // こっちが家具ID(chairとか)
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
rotation: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
props?: Record<string, any>;
|
||||
};
|
@@ -1,407 +0,0 @@
|
||||
// 家具メタデータ
|
||||
|
||||
// 家具IDはglbファイル及びそのディレクトリ名と一致する必要があります
|
||||
|
||||
// 家具にはユーザーが設定できるプロパティを設定可能です:
|
||||
//
|
||||
// props: {
|
||||
// <propname>: <proptype>
|
||||
// }
|
||||
//
|
||||
// proptype一覧:
|
||||
// * image ... 画像選択ダイアログを出し、その画像のURLが格納されます
|
||||
// * color ... 色選択コントロールを出し、選択された色が格納されます
|
||||
|
||||
// 家具にカスタムテクスチャを適用できるようにするには、textureプロパティに以下の追加の情報を含めます:
|
||||
// 便宜上そのUVのどの部分にカスタムテクスチャを貼り合わせるかのエリアをテクスチャエリアと呼びます。
|
||||
// UVは1024*1024だと仮定します。
|
||||
//
|
||||
// <key>: {
|
||||
// prop: <プロパティ名>,
|
||||
// uv: {
|
||||
// x: <テクスチャエリアX座標>,
|
||||
// y: <テクスチャエリアY座標>,
|
||||
// width: <テクスチャエリアの幅>,
|
||||
// height: <テクスチャエリアの高さ>,
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// <key>には、カスタムテクスチャを適用したいメッシュ名を指定します
|
||||
// <プロパティ名>には、カスタムテクスチャとして使用する画像を格納するプロパティ(前述)名を指定します
|
||||
|
||||
// 家具にカスタムカラーを適用できるようにするには、colorプロパティに以下の追加の情報を含めます:
|
||||
//
|
||||
// <key>: <プロパティ名>
|
||||
//
|
||||
// <key>には、カスタムカラーを適用したいマテリアル名を指定します
|
||||
// <プロパティ名>には、カスタムカラーとして使用する色を格納するプロパティ(前述)名を指定します
|
||||
|
||||
[
|
||||
{
|
||||
id: "milk",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "bed",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "low-table",
|
||||
place: "floor",
|
||||
props: {
|
||||
color: 'color'
|
||||
},
|
||||
color: {
|
||||
Table: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "desk",
|
||||
place: "floor",
|
||||
props: {
|
||||
color: 'color'
|
||||
},
|
||||
color: {
|
||||
Board: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "chair",
|
||||
place: "floor",
|
||||
props: {
|
||||
color: 'color'
|
||||
},
|
||||
color: {
|
||||
Chair: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "chair2",
|
||||
place: "floor",
|
||||
props: {
|
||||
color1: 'color',
|
||||
color2: 'color'
|
||||
},
|
||||
color: {
|
||||
Cushion: 'color1',
|
||||
Leg: 'color2'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "fan",
|
||||
place: "wall"
|
||||
},
|
||||
{
|
||||
id: "pc",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "plant",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "plant2",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "eraser",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "pencil",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "pudding",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "cardboard-box",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "cardboard-box2",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "cardboard-box3",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "book",
|
||||
place: "floor",
|
||||
props: {
|
||||
color: 'color'
|
||||
},
|
||||
color: {
|
||||
Cover: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "book2",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "piano",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "facial-tissue",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "server",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "moon",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "corkboard",
|
||||
place: "wall"
|
||||
},
|
||||
{
|
||||
id: "mousepad",
|
||||
place: "floor",
|
||||
props: {
|
||||
color: 'color'
|
||||
},
|
||||
color: {
|
||||
Pad: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "monitor",
|
||||
place: "floor",
|
||||
props: {
|
||||
screen: 'image'
|
||||
},
|
||||
texture: {
|
||||
Screen: {
|
||||
prop: 'screen',
|
||||
uv: {
|
||||
x: 0,
|
||||
y: 434,
|
||||
width: 1024,
|
||||
height: 588,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "tv",
|
||||
place: "floor",
|
||||
props: {
|
||||
screen: 'image'
|
||||
},
|
||||
texture: {
|
||||
Screen: {
|
||||
prop: 'screen',
|
||||
uv: {
|
||||
x: 0,
|
||||
y: 434,
|
||||
width: 1024,
|
||||
height: 588,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "keyboard",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "carpet-stripe",
|
||||
place: "floor",
|
||||
props: {
|
||||
color1: 'color',
|
||||
color2: 'color'
|
||||
},
|
||||
color: {
|
||||
CarpetAreaA: 'color1',
|
||||
CarpetAreaB: 'color2'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "mat",
|
||||
place: "floor",
|
||||
props: {
|
||||
color: 'color'
|
||||
},
|
||||
color: {
|
||||
Mat: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "color-box",
|
||||
place: "floor",
|
||||
props: {
|
||||
color: 'color'
|
||||
},
|
||||
color: {
|
||||
main: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "wall-clock",
|
||||
place: "wall"
|
||||
},
|
||||
{
|
||||
id: "cube",
|
||||
place: "floor",
|
||||
props: {
|
||||
color: 'color'
|
||||
},
|
||||
color: {
|
||||
Cube: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "photoframe",
|
||||
place: "wall",
|
||||
props: {
|
||||
photo: 'image',
|
||||
color: 'color'
|
||||
},
|
||||
texture: {
|
||||
Photo: {
|
||||
prop: 'photo',
|
||||
uv: {
|
||||
x: 0,
|
||||
y: 342,
|
||||
width: 1024,
|
||||
height: 683,
|
||||
},
|
||||
},
|
||||
},
|
||||
color: {
|
||||
Frame: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pinguin",
|
||||
place: "floor",
|
||||
props: {
|
||||
body: 'color',
|
||||
belly: 'color'
|
||||
},
|
||||
color: {
|
||||
Body: 'body',
|
||||
Belly: 'belly',
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "rubik-cube",
|
||||
place: "floor",
|
||||
},
|
||||
{
|
||||
id: "poster-h",
|
||||
place: "wall",
|
||||
props: {
|
||||
picture: 'image'
|
||||
},
|
||||
texture: {
|
||||
Poster: {
|
||||
prop: 'picture',
|
||||
uv: {
|
||||
x: 0,
|
||||
y: 277,
|
||||
width: 1024,
|
||||
height: 745,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "poster-v",
|
||||
place: "wall",
|
||||
props: {
|
||||
picture: 'image'
|
||||
},
|
||||
texture: {
|
||||
Poster: {
|
||||
prop: 'picture',
|
||||
uv: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 745,
|
||||
height: 1024,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "sofa",
|
||||
place: "floor",
|
||||
props: {
|
||||
color: 'color'
|
||||
},
|
||||
color: {
|
||||
Sofa: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "spiral",
|
||||
place: "floor",
|
||||
props: {
|
||||
color: 'color'
|
||||
},
|
||||
color: {
|
||||
Step: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "bin",
|
||||
place: "floor",
|
||||
props: {
|
||||
color: 'color'
|
||||
},
|
||||
color: {
|
||||
Bin: 'color'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "cup-noodle",
|
||||
place: "floor"
|
||||
},
|
||||
{
|
||||
id: "holo-display",
|
||||
place: "floor",
|
||||
props: {
|
||||
image: 'image'
|
||||
},
|
||||
texture: {
|
||||
Image_Front: {
|
||||
prop: 'image',
|
||||
uv: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
},
|
||||
Image_Back: {
|
||||
prop: 'image',
|
||||
uv: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'energy-drink',
|
||||
place: "floor",
|
||||
},
|
||||
{
|
||||
id: 'doll-ai',
|
||||
place: "floor",
|
||||
},
|
||||
{
|
||||
id: 'banknote',
|
||||
place: "floor",
|
||||
},
|
||||
]
|
@@ -1,775 +0,0 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
|
||||
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
|
||||
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
|
||||
import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js';
|
||||
import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js';
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
|
||||
import { Furniture, RoomInfo } from './furniture';
|
||||
import { query as urlQuery } from '@/scripts/url';
|
||||
const furnitureDefs = require('./furnitures.json5');
|
||||
|
||||
THREE.ImageUtils.crossOrigin = '';
|
||||
|
||||
type Options = {
|
||||
graphicsQuality: Room['graphicsQuality'];
|
||||
onChangeSelect: Room['onChangeSelect'];
|
||||
useOrthographicCamera: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* MisskeyRoom Core Engine
|
||||
*/
|
||||
export class Room {
|
||||
private clock: THREE.Clock;
|
||||
private scene: THREE.Scene;
|
||||
private renderer: THREE.WebGLRenderer;
|
||||
private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera;
|
||||
private controls: OrbitControls;
|
||||
private composer: EffectComposer;
|
||||
private mixers: THREE.AnimationMixer[] = [];
|
||||
private furnitureControl: TransformControls;
|
||||
private roomInfo: RoomInfo;
|
||||
private graphicsQuality: 'cheep' | 'low' | 'medium' | 'high' | 'ultra';
|
||||
private roomObj: THREE.Object3D;
|
||||
private objects: THREE.Object3D[] = [];
|
||||
private selectedObject: THREE.Object3D = null;
|
||||
private onChangeSelect: Function;
|
||||
private isTransformMode = false;
|
||||
private renderFrameRequestId: number;
|
||||
|
||||
private get canvas(): HTMLCanvasElement {
|
||||
return this.renderer.domElement;
|
||||
}
|
||||
|
||||
private get furnitures(): Furniture[] {
|
||||
return this.roomInfo.furnitures;
|
||||
}
|
||||
|
||||
private set furnitures(furnitures: Furniture[]) {
|
||||
this.roomInfo.furnitures = furnitures;
|
||||
}
|
||||
|
||||
private get enableShadow() {
|
||||
return this.graphicsQuality != 'cheep';
|
||||
}
|
||||
|
||||
private get usePostFXs() {
|
||||
return this.graphicsQuality !== 'cheep' && this.graphicsQuality !== 'low';
|
||||
}
|
||||
|
||||
private get shadowQuality() {
|
||||
return (
|
||||
this.graphicsQuality === 'ultra' ? 16384 :
|
||||
this.graphicsQuality === 'high' ? 8192 :
|
||||
this.graphicsQuality === 'medium' ? 4096 :
|
||||
this.graphicsQuality === 'low' ? 1024 :
|
||||
0); // cheep
|
||||
}
|
||||
|
||||
constructor(user, isMyRoom, roomInfo: RoomInfo, container: Element, options: Options) {
|
||||
this.roomInfo = roomInfo;
|
||||
this.graphicsQuality = options.graphicsQuality;
|
||||
this.onChangeSelect = options.onChangeSelect;
|
||||
|
||||
this.clock = new THREE.Clock(true);
|
||||
|
||||
//#region Init a scene
|
||||
this.scene = new THREE.Scene();
|
||||
|
||||
const width = container.clientWidth;
|
||||
const height = container.clientHeight;
|
||||
|
||||
//#region Init a renderer
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
antialias: false,
|
||||
stencil: false,
|
||||
alpha: false,
|
||||
powerPreference:
|
||||
this.graphicsQuality === 'ultra' ? 'high-performance' :
|
||||
this.graphicsQuality === 'high' ? 'high-performance' :
|
||||
this.graphicsQuality === 'medium' ? 'default' :
|
||||
this.graphicsQuality === 'low' ? 'low-power' :
|
||||
'low-power' // cheep
|
||||
});
|
||||
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.renderer.setSize(width, height);
|
||||
this.renderer.autoClear = false;
|
||||
this.renderer.setClearColor(new THREE.Color(0x051f2d));
|
||||
this.renderer.shadowMap.enabled = this.enableShadow;
|
||||
this.renderer.shadowMap.type =
|
||||
this.graphicsQuality === 'ultra' ? THREE.PCFSoftShadowMap :
|
||||
this.graphicsQuality === 'high' ? THREE.PCFSoftShadowMap :
|
||||
this.graphicsQuality === 'medium' ? THREE.PCFShadowMap :
|
||||
this.graphicsQuality === 'low' ? THREE.BasicShadowMap :
|
||||
THREE.BasicShadowMap; // cheep
|
||||
|
||||
container.insertBefore(this.canvas, container.firstChild);
|
||||
//#endregion
|
||||
|
||||
//#region Init a camera
|
||||
this.camera = options.useOrthographicCamera
|
||||
? new THREE.OrthographicCamera(
|
||||
width / - 2, width / 2, height / 2, height / - 2, -10, 10)
|
||||
: new THREE.PerspectiveCamera(45, width / height);
|
||||
|
||||
if (options.useOrthographicCamera) {
|
||||
this.camera.position.x = 2;
|
||||
this.camera.position.y = 2;
|
||||
this.camera.position.z = 2;
|
||||
this.camera.zoom = 100;
|
||||
this.camera.updateProjectionMatrix();
|
||||
} else {
|
||||
this.camera.position.x = 5;
|
||||
this.camera.position.y = 2;
|
||||
this.camera.position.z = 5;
|
||||
}
|
||||
|
||||
this.scene.add(this.camera);
|
||||
//#endregion
|
||||
|
||||
//#region AmbientLight
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 1);
|
||||
this.scene.add(ambientLight);
|
||||
//#endregion
|
||||
|
||||
if (this.graphicsQuality !== 'cheep') {
|
||||
//#region Room light
|
||||
const roomLight = new THREE.SpotLight(0xffffff, 0.1);
|
||||
|
||||
roomLight.position.set(0, 8, 0);
|
||||
roomLight.castShadow = this.enableShadow;
|
||||
roomLight.shadow.bias = -0.0001;
|
||||
roomLight.shadow.mapSize.width = this.shadowQuality;
|
||||
roomLight.shadow.mapSize.height = this.shadowQuality;
|
||||
roomLight.shadow.camera.near = 0.1;
|
||||
roomLight.shadow.camera.far = 9;
|
||||
roomLight.shadow.camera.fov = 45;
|
||||
|
||||
this.scene.add(roomLight);
|
||||
//#endregion
|
||||
}
|
||||
|
||||
//#region Out light
|
||||
const outLight1 = new THREE.SpotLight(0xffffff, 0.4);
|
||||
outLight1.position.set(9, 3, -2);
|
||||
outLight1.castShadow = this.enableShadow;
|
||||
outLight1.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある
|
||||
outLight1.shadow.mapSize.width = this.shadowQuality;
|
||||
outLight1.shadow.mapSize.height = this.shadowQuality;
|
||||
outLight1.shadow.camera.near = 6;
|
||||
outLight1.shadow.camera.far = 15;
|
||||
outLight1.shadow.camera.fov = 45;
|
||||
this.scene.add(outLight1);
|
||||
|
||||
const outLight2 = new THREE.SpotLight(0xffffff, 0.2);
|
||||
outLight2.position.set(-2, 3, 9);
|
||||
outLight2.castShadow = false;
|
||||
outLight2.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある
|
||||
outLight2.shadow.camera.near = 6;
|
||||
outLight2.shadow.camera.far = 15;
|
||||
outLight2.shadow.camera.fov = 45;
|
||||
this.scene.add(outLight2);
|
||||
//#endregion
|
||||
|
||||
//#region Init a controller
|
||||
this.controls = new OrbitControls(this.camera, this.canvas);
|
||||
|
||||
this.controls.target.set(0, 1, 0);
|
||||
this.controls.enableZoom = true;
|
||||
this.controls.enablePan = isMyRoom;
|
||||
this.controls.minPolarAngle = 0;
|
||||
this.controls.maxPolarAngle = Math.PI / 2;
|
||||
this.controls.minAzimuthAngle = 0;
|
||||
this.controls.maxAzimuthAngle = Math.PI / 2;
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.2;
|
||||
//#endregion
|
||||
|
||||
//#region POST FXs
|
||||
if (!this.usePostFXs) {
|
||||
this.composer = null;
|
||||
} else {
|
||||
const renderTarget = new THREE.WebGLRenderTarget(width, height, {
|
||||
minFilter: THREE.LinearFilter,
|
||||
magFilter: THREE.LinearFilter,
|
||||
format: THREE.RGBFormat,
|
||||
stencilBuffer: false,
|
||||
});
|
||||
|
||||
const fxaa = new ShaderPass(FXAAShader);
|
||||
fxaa.uniforms['resolution'].value = new THREE.Vector2(1 / width, 1 / height);
|
||||
fxaa.renderToScreen = true;
|
||||
|
||||
this.composer = new EffectComposer(this.renderer, renderTarget);
|
||||
this.composer.addPass(new RenderPass(this.scene, this.camera));
|
||||
if (this.graphicsQuality === 'ultra') {
|
||||
this.composer.addPass(new BloomPass(0.25, 30, 128.0, 512));
|
||||
}
|
||||
this.composer.addPass(fxaa);
|
||||
}
|
||||
//#endregion
|
||||
//#endregion
|
||||
|
||||
//#region Label
|
||||
//#region Avatar
|
||||
const avatarUrl = `/proxy/?${urlQuery({ url: user.avatarUrl })}`;
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
textureLoader.crossOrigin = 'anonymous';
|
||||
|
||||
const iconTexture = textureLoader.load(avatarUrl);
|
||||
iconTexture.wrapS = THREE.RepeatWrapping;
|
||||
iconTexture.wrapT = THREE.RepeatWrapping;
|
||||
iconTexture.anisotropy = 16;
|
||||
|
||||
const avatarMaterial = new THREE.MeshBasicMaterial({
|
||||
map: iconTexture,
|
||||
side: THREE.DoubleSide,
|
||||
alphaTest: 0.5
|
||||
});
|
||||
|
||||
const iconGeometry = new THREE.PlaneGeometry(1, 1);
|
||||
|
||||
const avatarObject = new THREE.Mesh(iconGeometry, avatarMaterial);
|
||||
avatarObject.position.set(-3, 2.5, 2);
|
||||
avatarObject.rotation.y = Math.PI / 2;
|
||||
avatarObject.castShadow = false;
|
||||
|
||||
this.scene.add(avatarObject);
|
||||
//#endregion
|
||||
|
||||
//#region Username
|
||||
const name = user.username;
|
||||
|
||||
new THREE.FontLoader().load('/assets/fonts/helvetiker_regular.typeface.json', font => {
|
||||
const nameGeometry = new THREE.TextGeometry(name, {
|
||||
size: 0.5,
|
||||
height: 0,
|
||||
curveSegments: 8,
|
||||
font: font,
|
||||
bevelThickness: 0,
|
||||
bevelSize: 0,
|
||||
bevelEnabled: false
|
||||
});
|
||||
|
||||
const nameMaterial = new THREE.MeshLambertMaterial({
|
||||
color: 0xffffff
|
||||
});
|
||||
|
||||
const nameObject = new THREE.Mesh(nameGeometry, nameMaterial);
|
||||
nameObject.position.set(-3, 2.25, 1.25);
|
||||
nameObject.rotation.y = Math.PI / 2;
|
||||
nameObject.castShadow = false;
|
||||
|
||||
this.scene.add(nameObject);
|
||||
});
|
||||
//#endregion
|
||||
//#endregion
|
||||
|
||||
//#region Interaction
|
||||
if (isMyRoom) {
|
||||
this.furnitureControl = new TransformControls(this.camera, this.canvas);
|
||||
this.scene.add(this.furnitureControl);
|
||||
|
||||
// Hover highlight
|
||||
this.canvas.onmousemove = this.onmousemove;
|
||||
|
||||
// Click
|
||||
this.canvas.onmousedown = this.onmousedown;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Init room
|
||||
this.loadRoom();
|
||||
//#endregion
|
||||
|
||||
//#region Load furnitures
|
||||
for (const furniture of this.furnitures) {
|
||||
this.loadFurniture(furniture).then(obj => {
|
||||
this.scene.add(obj.scene);
|
||||
this.objects.push(obj.scene);
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// Start render
|
||||
if (this.usePostFXs) {
|
||||
this.renderWithPostFXs();
|
||||
} else {
|
||||
this.renderWithoutPostFXs();
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
private renderWithoutPostFXs() {
|
||||
this.renderFrameRequestId =
|
||||
window.requestAnimationFrame(this.renderWithoutPostFXs);
|
||||
|
||||
// Update animations
|
||||
const clock = this.clock.getDelta();
|
||||
for (const mixer of this.mixers) {
|
||||
mixer.update(clock);
|
||||
}
|
||||
|
||||
this.controls.update();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
@autobind
|
||||
private renderWithPostFXs() {
|
||||
this.renderFrameRequestId =
|
||||
window.requestAnimationFrame(this.renderWithPostFXs);
|
||||
|
||||
// Update animations
|
||||
const clock = this.clock.getDelta();
|
||||
for (const mixer of this.mixers) {
|
||||
mixer.update(clock);
|
||||
}
|
||||
|
||||
this.controls.update();
|
||||
this.renderer.clear();
|
||||
this.composer.render();
|
||||
}
|
||||
|
||||
@autobind
|
||||
private loadRoom() {
|
||||
const type = this.roomInfo.roomType;
|
||||
new GLTFLoader().load(`/client-assets/room/rooms/${type}/${type}.glb`, gltf => {
|
||||
gltf.scene.traverse(child => {
|
||||
if (!(child instanceof THREE.Mesh)) return;
|
||||
|
||||
child.receiveShadow = this.enableShadow;
|
||||
|
||||
child.material = new THREE.MeshLambertMaterial({
|
||||
color: (child.material as THREE.MeshStandardMaterial).color,
|
||||
map: (child.material as THREE.MeshStandardMaterial).map,
|
||||
name: (child.material as THREE.MeshStandardMaterial).name,
|
||||
});
|
||||
|
||||
// 異方性フィルタリング
|
||||
if ((child.material as THREE.MeshLambertMaterial).map && this.graphicsQuality !== 'cheep') {
|
||||
(child.material as THREE.MeshLambertMaterial).map.minFilter = THREE.LinearMipMapLinearFilter;
|
||||
(child.material as THREE.MeshLambertMaterial).map.magFilter = THREE.LinearMipMapLinearFilter;
|
||||
(child.material as THREE.MeshLambertMaterial).map.anisotropy = 8;
|
||||
}
|
||||
});
|
||||
|
||||
gltf.scene.position.set(0, 0, 0);
|
||||
|
||||
this.scene.add(gltf.scene);
|
||||
this.roomObj = gltf.scene;
|
||||
if (this.roomInfo.roomType === 'default') {
|
||||
this.applyCarpetColor();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
private loadFurniture(furniture: Furniture) {
|
||||
const def = furnitureDefs.find(d => d.id === furniture.type);
|
||||
return new Promise<GLTF>((res, rej) => {
|
||||
const loader = new GLTFLoader();
|
||||
loader.load(`/client-assets/room/furnitures/${furniture.type}/${furniture.type}.glb`, gltf => {
|
||||
const model = gltf.scene;
|
||||
|
||||
// Load animation
|
||||
if (gltf.animations.length > 0) {
|
||||
const mixer = new THREE.AnimationMixer(model);
|
||||
this.mixers.push(mixer);
|
||||
for (const clip of gltf.animations) {
|
||||
mixer.clipAction(clip).play();
|
||||
}
|
||||
}
|
||||
|
||||
model.name = furniture.id;
|
||||
model.position.x = furniture.position.x;
|
||||
model.position.y = furniture.position.y;
|
||||
model.position.z = furniture.position.z;
|
||||
model.rotation.x = furniture.rotation.x;
|
||||
model.rotation.y = furniture.rotation.y;
|
||||
model.rotation.z = furniture.rotation.z;
|
||||
|
||||
model.traverse(child => {
|
||||
if (!(child instanceof THREE.Mesh)) return;
|
||||
child.castShadow = this.enableShadow;
|
||||
child.receiveShadow = this.enableShadow;
|
||||
(child.material as THREE.MeshStandardMaterial).metalness = 0;
|
||||
|
||||
// 異方性フィルタリング
|
||||
if ((child.material as THREE.MeshStandardMaterial).map && this.graphicsQuality !== 'cheep') {
|
||||
(child.material as THREE.MeshStandardMaterial).map.minFilter = THREE.LinearMipMapLinearFilter;
|
||||
(child.material as THREE.MeshStandardMaterial).map.magFilter = THREE.LinearMipMapLinearFilter;
|
||||
(child.material as THREE.MeshStandardMaterial).map.anisotropy = 8;
|
||||
}
|
||||
});
|
||||
|
||||
if (def.color) { // カスタムカラー
|
||||
this.applyCustomColor(model);
|
||||
}
|
||||
|
||||
if (def.texture) { // カスタムテクスチャ
|
||||
this.applyCustomTexture(model);
|
||||
}
|
||||
|
||||
res(gltf);
|
||||
}, null, rej);
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
private applyCarpetColor() {
|
||||
this.roomObj.traverse(child => {
|
||||
if (!(child instanceof THREE.Mesh)) return;
|
||||
if (child.material &&
|
||||
(child.material as THREE.MeshStandardMaterial).name &&
|
||||
(child.material as THREE.MeshStandardMaterial).name === 'Carpet'
|
||||
) {
|
||||
const colorHex = parseInt(this.roomInfo.carpetColor.substr(1), 16);
|
||||
(child.material as THREE.MeshStandardMaterial).color.setHex(colorHex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
private applyCustomColor(model: THREE.Object3D) {
|
||||
const furniture = this.furnitures.find(furniture => furniture.id === model.name);
|
||||
const def = furnitureDefs.find(d => d.id === furniture.type);
|
||||
if (def.color == null) return;
|
||||
model.traverse(child => {
|
||||
if (!(child instanceof THREE.Mesh)) return;
|
||||
for (const t of Object.keys(def.color)) {
|
||||
if (!child.material ||
|
||||
!(child.material as THREE.MeshStandardMaterial).name ||
|
||||
(child.material as THREE.MeshStandardMaterial).name !== t
|
||||
) continue;
|
||||
|
||||
const prop = def.color[t];
|
||||
const val = furniture.props ? furniture.props[prop] : undefined;
|
||||
|
||||
if (val == null) continue;
|
||||
|
||||
const colorHex = parseInt(val.substr(1), 16);
|
||||
(child.material as THREE.MeshStandardMaterial).color.setHex(colorHex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
private applyCustomTexture(model: THREE.Object3D) {
|
||||
const furniture = this.furnitures.find(furniture => furniture.id === model.name);
|
||||
const def = furnitureDefs.find(d => d.id === furniture.type);
|
||||
if (def.texture == null) return;
|
||||
|
||||
model.traverse(child => {
|
||||
if (!(child instanceof THREE.Mesh)) return;
|
||||
for (const t of Object.keys(def.texture)) {
|
||||
if (child.name !== t) continue;
|
||||
|
||||
const prop = def.texture[t].prop;
|
||||
const val = furniture.props ? furniture.props[prop] : undefined;
|
||||
|
||||
if (val == null) continue;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = 1024;
|
||||
canvas.width = 1024;
|
||||
|
||||
child.material = new THREE.MeshLambertMaterial({
|
||||
emissive: 0x111111,
|
||||
side: THREE.DoubleSide,
|
||||
alphaTest: 0.5,
|
||||
});
|
||||
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => {
|
||||
const uvInfo = def.texture[t].uv;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img,
|
||||
0, 0, img.width, img.height,
|
||||
uvInfo.x, uvInfo.y, uvInfo.width, uvInfo.height);
|
||||
|
||||
const texture = new THREE.Texture(canvas);
|
||||
texture.wrapS = THREE.RepeatWrapping;
|
||||
texture.wrapT = THREE.RepeatWrapping;
|
||||
texture.anisotropy = 16;
|
||||
texture.flipY = false;
|
||||
|
||||
(child.material as THREE.MeshLambertMaterial).map = texture;
|
||||
(child.material as THREE.MeshLambertMaterial).needsUpdate = true;
|
||||
(child.material as THREE.MeshLambertMaterial).map.needsUpdate = true;
|
||||
};
|
||||
img.src = val;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onmousemove(ev: MouseEvent) {
|
||||
if (this.isTransformMode) return;
|
||||
|
||||
const rect = (ev.target as HTMLElement).getBoundingClientRect();
|
||||
const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
const pos = new THREE.Vector2(x, y);
|
||||
|
||||
this.camera.updateMatrixWorld();
|
||||
|
||||
const raycaster = new THREE.Raycaster();
|
||||
raycaster.setFromCamera(pos, this.camera);
|
||||
|
||||
const intersects = raycaster.intersectObjects(this.objects, true);
|
||||
|
||||
for (const object of this.objects) {
|
||||
if (this.isSelectedObject(object)) continue;
|
||||
object.traverse(child => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const intersected = this.getRoot(intersects[0].object);
|
||||
if (this.isSelectedObject(intersected)) return;
|
||||
intersected.traverse(child => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x191919);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onmousedown(ev: MouseEvent) {
|
||||
if (this.isTransformMode) return;
|
||||
if (ev.target !== this.canvas || ev.button !== 0) return;
|
||||
|
||||
const rect = (ev.target as HTMLElement).getBoundingClientRect();
|
||||
const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
const pos = new THREE.Vector2(x, y);
|
||||
|
||||
this.camera.updateMatrixWorld();
|
||||
|
||||
const raycaster = new THREE.Raycaster();
|
||||
raycaster.setFromCamera(pos, this.camera);
|
||||
|
||||
const intersects = raycaster.intersectObjects(this.objects, true);
|
||||
|
||||
for (const object of this.objects) {
|
||||
object.traverse(child => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
(child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const selectedObj = this.getRoot(intersects[0].object);
|
||||
this.selectFurniture(selectedObj);
|
||||
} else {
|
||||
this.selectedObject = null;
|
||||
this.onChangeSelect(null);
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
private getRoot(obj: THREE.Object3D): THREE.Object3D {
|
||||
let found = false;
|
||||
let x = obj.parent;
|
||||
while (!found) {
|
||||
if (x.parent.parent == null) {
|
||||
found = true;
|
||||
} else {
|
||||
x = x.parent;
|
||||
}
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
@autobind
|
||||
private isSelectedObject(obj: THREE.Object3D): boolean {
|
||||
if (this.selectedObject == null) {
|
||||
return false;
|
||||
} else {
|
||||
return obj.name === this.selectedObject.name;
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
private selectFurniture(obj: THREE.Object3D) {
|
||||
this.selectedObject = obj;
|
||||
this.onChangeSelect(obj);
|
||||
obj.traverse(child => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
(child.material as THREE.MeshStandardMaterial).emissive.setHex(0xff0000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 家具の移動/回転モードにします
|
||||
* @param type 移動か回転か
|
||||
*/
|
||||
@autobind
|
||||
public enterTransformMode(type: 'translate' | 'rotate') {
|
||||
this.isTransformMode = true;
|
||||
this.furnitureControl.setMode(type);
|
||||
this.furnitureControl.attach(this.selectedObject);
|
||||
this.controls.enableRotate = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 家具の移動/回転モードを終了します
|
||||
*/
|
||||
@autobind
|
||||
public exitTransformMode() {
|
||||
this.isTransformMode = false;
|
||||
this.furnitureControl.detach();
|
||||
this.controls.enableRotate = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 家具プロパティを更新します
|
||||
* @param key プロパティ名
|
||||
* @param value 値
|
||||
*/
|
||||
@autobind
|
||||
public updateProp(key: string, value: any) {
|
||||
const furniture = this.furnitures.find(furniture => furniture.id === this.selectedObject.name);
|
||||
if (furniture.props == null) furniture.props = {};
|
||||
furniture.props[key] = value;
|
||||
this.applyCustomColor(this.selectedObject);
|
||||
this.applyCustomTexture(this.selectedObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 部屋に家具を追加します
|
||||
* @param type 家具の種類
|
||||
*/
|
||||
@autobind
|
||||
public addFurniture(type: string) {
|
||||
const furniture = {
|
||||
id: uuid(),
|
||||
type: type,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
},
|
||||
rotation: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0,
|
||||
},
|
||||
};
|
||||
|
||||
this.furnitures.push(furniture);
|
||||
|
||||
this.loadFurniture(furniture).then(obj => {
|
||||
this.scene.add(obj.scene);
|
||||
this.objects.push(obj.scene);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 現在選択されている家具を部屋から削除します
|
||||
*/
|
||||
@autobind
|
||||
public removeFurniture() {
|
||||
this.exitTransformMode();
|
||||
const obj = this.selectedObject;
|
||||
this.scene.remove(obj);
|
||||
this.objects = this.objects.filter(object => object.name !== obj.name);
|
||||
this.furnitures = this.furnitures.filter(furniture => furniture.id !== obj.name);
|
||||
this.selectedObject = null;
|
||||
this.onChangeSelect(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全ての家具を部屋から削除します
|
||||
*/
|
||||
@autobind
|
||||
public removeAllFurnitures() {
|
||||
this.exitTransformMode();
|
||||
for (const obj of this.objects) {
|
||||
this.scene.remove(obj);
|
||||
}
|
||||
this.objects = [];
|
||||
this.furnitures = [];
|
||||
this.selectedObject = null;
|
||||
this.onChangeSelect(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 部屋の床の色を変更します
|
||||
* @param color 色
|
||||
*/
|
||||
@autobind
|
||||
public updateCarpetColor(color: string) {
|
||||
this.roomInfo.carpetColor = color;
|
||||
this.applyCarpetColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* 部屋の種類を変更します
|
||||
* @param type 種類
|
||||
*/
|
||||
@autobind
|
||||
public changeRoomType(type: string) {
|
||||
this.roomInfo.roomType = type;
|
||||
this.scene.remove(this.roomObj);
|
||||
this.loadRoom();
|
||||
}
|
||||
|
||||
/**
|
||||
* 部屋データを取得します
|
||||
*/
|
||||
@autobind
|
||||
public getRoomInfo() {
|
||||
for (const obj of this.objects) {
|
||||
const furniture = this.furnitures.find(f => f.id === obj.name);
|
||||
furniture.position.x = obj.position.x;
|
||||
furniture.position.y = obj.position.y;
|
||||
furniture.position.z = obj.position.z;
|
||||
furniture.rotation.x = obj.rotation.x;
|
||||
furniture.rotation.y = obj.rotation.y;
|
||||
furniture.rotation.z = obj.rotation.z;
|
||||
}
|
||||
|
||||
return this.roomInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 選択されている家具を取得します
|
||||
*/
|
||||
@autobind
|
||||
public getSelectedObject() {
|
||||
return this.selectedObject;
|
||||
}
|
||||
|
||||
@autobind
|
||||
public findFurnitureById(id: string) {
|
||||
return this.furnitures.find(furniture => furniture.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* レンダリングを終了します
|
||||
*/
|
||||
@autobind
|
||||
public destroy() {
|
||||
// Stop render loop
|
||||
window.cancelAnimationFrame(this.renderFrameRequestId);
|
||||
|
||||
this.controls.dispose();
|
||||
this.scene.dispose();
|
||||
}
|
||||
}
|
@@ -257,8 +257,6 @@ export class ColdDeviceStorage {
|
||||
sound_channel: { type: 'syuilo/square-pico', volume: 1 },
|
||||
sound_reversiPutBlack: { type: 'syuilo/kick', volume: 0.3 },
|
||||
sound_reversiPutWhite: { type: 'syuilo/snare', volume: 0.3 },
|
||||
roomGraphicsQuality: 'medium' as 'cheep' | 'low' | 'medium' | 'high' | 'ultra',
|
||||
roomUseOrthographicCamera: true,
|
||||
};
|
||||
|
||||
public static watchers = [];
|
||||
|
@@ -3,8 +3,6 @@ import { markRaw } from 'vue';
|
||||
import { $i } from '@/account';
|
||||
import { url } from '@/config';
|
||||
|
||||
console.log($i.token);
|
||||
|
||||
export const stream = markRaw(new Misskey.Stream(url, $i ? {
|
||||
token: $i.token,
|
||||
} : null));
|
||||
|
Reference in New Issue
Block a user