Merge branch 'develop' into pizzax-indexeddb

This commit is contained in:
tamaina
2022-01-08 17:00:40 +09:00
192 changed files with 264 additions and 3324 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',

View File

@@ -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>

View File

@@ -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',

View File

@@ -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);
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'));

View File

@@ -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();
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
});

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -63,11 +63,6 @@ export default defineComponent({
injectFeaturedNote: v
});
},
taskmanager() {
os.popup(import('@/components/taskmanager.vue'), {
}, {}, 'closed');
},
}
});
</script>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 }) },

View File

@@ -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>;
};

View File

@@ -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",
},
]

View File

@@ -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();
}
}

View File

@@ -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 = [];

View File

@@ -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));